Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2019-12-19 12:07:35 +00:00
parent e3d67bcff7
commit e3764d340e
78 changed files with 1036 additions and 239 deletions

View file

@ -3,7 +3,7 @@
*.rake @gitlab-org/maintainers/rails-backend
# Technical writing team are the default reviewers for everything in `doc/`
/doc/ @gl-docsteam
doc/ @gl-docsteam
# Frontend maintainers should see everything in `app/assets/`
app/assets/ @gitlab-org/maintainers/frontend

View file

@ -142,6 +142,12 @@ const Api = {
return axios.get(url);
},
// Update a single project
updateProject(projectPath, data) {
const url = Api.buildUrl(Api.projectPath).replace(':id', encodeURIComponent(projectPath));
return axios.put(url, data);
},
/**
* Get all projects for a forked relationship to a specified project
* @param {string} projectPath - Path or ID of a project

View file

@ -9,7 +9,6 @@ import {
GlDropdownItem,
} from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import boardsStore from '../stores/boards_store';
import BoardForm from './board_form.vue';
@ -19,7 +18,6 @@ const MIN_BOARDS_TO_VIEW_RECENT = 10;
export default {
name: 'BoardsSelector',
components: {
Icon,
BoardForm,
GlLoadingIcon,
GlSearchBoxByType,

View file

@ -2,7 +2,6 @@
/* eslint-disable vue/require-default-prop */
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { GlLink, GlModalDirective } from '@gitlab/ui';
import TimeagoTooltip from '../../vue_shared/components/time_ago_tooltip.vue';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
@ -16,7 +15,6 @@ export default {
components: {
loadingButton,
identicon,
TimeagoTooltip,
GlLink,
UninstallApplicationButton,
UninstallApplicationConfirmationModal,

View file

@ -19,7 +19,6 @@ import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
import KnativeDomainEditor from './knative_domain_editor.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub';
import CrossplaneProviderStack from './crossplane_provider_stack.vue';
@ -27,7 +26,6 @@ export default {
components: {
applicationRow,
clipboardButton,
LoadingButton,
GlLoadingIcon,
KnativeDomainEditor,
CrossplaneProviderStack,

View file

@ -2,7 +2,6 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import Mousetrap from 'mousetrap';
import Icon from '~/vue_shared/components/icon.vue';
import { __ } from '~/locale';
import createFlash from '~/flash';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
@ -27,7 +26,6 @@ import {
export default {
name: 'DiffsApp',
components: {
Icon,
CompareVersions,
DiffFile,
NoChanges,
@ -95,7 +93,6 @@ export default {
parseInt(localStorage.getItem(TREE_LIST_WIDTH_STORAGE_KEY), 10) || INITIAL_TREE_WIDTH;
return {
assignedDiscussions: false,
treeWidth,
};
},
@ -114,6 +111,7 @@ export default {
numVisibleFiles: state => state.diffs.size,
plainDiffPath: state => state.diffs.plainDiffPath,
emailPatchPath: state => state.diffs.emailPatchPath,
retrievingBatches: state => state.diffs.retrievingBatches,
}),
...mapState('diffs', ['showTreeList', 'isLoading', 'startVersion']),
...mapGetters('diffs', ['isParallelView', 'currentDiffIndex']),
@ -144,9 +142,6 @@ export default {
isLimitedContainer() {
return !this.showTreeList && !this.isParallelView && !this.isFluidLayout;
},
shouldSetDiscussions() {
return this.isNotesFetched && !this.assignedDiscussions && !this.isLoading;
},
},
watch: {
diffViewType() {
@ -163,10 +158,8 @@ export default {
},
isLoading: 'adjustView',
showTreeList: 'adjustView',
shouldSetDiscussions(newVal) {
if (newVal) {
this.setDiscussions();
}
retrievingBatches(newVal) {
if (!newVal) this.unwatchDiscussions();
},
},
mounted() {
@ -192,10 +185,14 @@ export default {
},
created() {
this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
eventHub.$once('fetchDiffData', this.fetchData);
eventHub.$on('refetchDiffData', this.refetchDiffData);
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
this.unwatchDiscussions = this.$watch(
() => `${this.diffFiles.length}:${this.$store.state.notes.discussions.length}`,
() => this.setDiscussions(),
);
},
beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData);
@ -217,7 +214,6 @@ export default {
'toggleShowTreeList',
]),
refetchDiffData() {
this.assignedDiscussions = false;
this.fetchData(false);
},
startDiffRendering() {
@ -269,17 +265,13 @@ export default {
}
},
setDiscussions() {
if (this.shouldSetDiscussions) {
this.assignedDiscussions = true;
requestIdleCallback(
() =>
this.assignDiscussionsToDiff()
.then(this.$nextTick)
.then(this.startTaskList),
{ timeout: 1000 },
);
}
requestIdleCallback(
() =>
this.assignDiscussionsToDiff()
.then(this.$nextTick)
.then(this.startTaskList),
{ timeout: 1000 },
);
},
adjustView() {
if (this.shouldShow) {

View file

@ -2,7 +2,6 @@
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import Icon from '~/vue_shared/components/icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CIIcon from '~/vue_shared/components/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import initUserPopovers from '../../user_popovers';
@ -25,7 +24,6 @@ export default {
UserAvatarLink,
Icon,
ClipboardButton,
CIIcon,
TimeAgoTooltip,
CommitPipelineStatus,
},

View file

@ -1,7 +1,7 @@
<script>
import _ from 'underscore';
import { mapActions, mapGetters } from 'vuex';
import { GlButton, GlTooltipDirective, GlTooltip, GlLoadingIcon } from '@gitlab/ui';
import { GlButton, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { polyfillSticky } from '~/lib/utils/sticky';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
@ -15,7 +15,6 @@ import { scrollToElement } from '~/lib/utils/common_utils';
export default {
components: {
GlTooltip,
GlLoadingIcon,
GlButton,
ClipboardButton,

View file

@ -1,11 +1,9 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import { MATCH_LINE_TYPE } from '../constants';
export default {
components: {
Icon,
DiffExpansionCell,
},
props: {

View file

@ -91,6 +91,7 @@ export const fetchDiffFiles = ({ state, commit }) => {
export const fetchDiffFilesBatch = ({ commit, state }) => {
commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true);
const getBatch = page =>
axios
@ -100,9 +101,11 @@ export const fetchDiffFilesBatch = ({ commit, state }) => {
.then(({ data: { pagination, diff_files } }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
commit(types.SET_BATCH_LOADING, false);
if (!pagination.next_page) commit(types.SET_RETRIEVING_BATCHES, false);
return pagination.next_page;
})
.then(nextPage => nextPage && getBatch(nextPage));
.then(nextPage => nextPage && getBatch(nextPage))
.catch(() => commit(types.SET_RETRIEVING_BATCHES, false));
return getBatch()
.then(handleLocationHash)

View file

@ -9,6 +9,7 @@ const defaultViewType = INLINE_DIFF_VIEW_TYPE;
export default () => ({
isLoading: true,
isBatchLoading: false,
retrievingBatches: false,
addedLines: null,
removedLines: null,
endpoint: '',

View file

@ -1,6 +1,7 @@
export const SET_BASE_CONFIG = 'SET_BASE_CONFIG';
export const SET_LOADING = 'SET_LOADING';
export const SET_BATCH_LOADING = 'SET_BATCH_LOADING';
export const SET_RETRIEVING_BATCHES = 'SET_RETRIEVING_BATCHES';
export const SET_DIFF_DATA = 'SET_DIFF_DATA';
export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH';
export const SET_DIFF_VIEW_TYPE = 'SET_DIFF_VIEW_TYPE';

View file

@ -40,6 +40,10 @@ export default {
Object.assign(state, { isBatchLoading });
},
[types.SET_RETRIEVING_BATCHES](state, retrievingBatches) {
Object.assign(state, { retrievingBatches });
},
[types.SET_DIFF_DATA](state, data) {
if (
!(

View file

@ -8,7 +8,6 @@
import { GlTooltipDirective, GlLoadingIcon, GlModalDirective, GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import ConfirmRollbackModal from './confirm_rollback_modal.vue';
import eventHub from '../event_hub';
export default {
@ -16,7 +15,6 @@ export default {
Icon,
GlLoadingIcon,
GlButton,
ConfirmRollbackModal,
},
directives: {
GlTooltip: GlTooltipDirective,

View file

@ -3,7 +3,6 @@
import { GlTooltipDirective } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import { s__, sprintf } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
export default {
@ -12,7 +11,6 @@ export default {
components: {
GlModal: DeprecatedModal2,
LoadingButton,
},
directives: {

View file

@ -1,14 +1,11 @@
<script>
import { GlDropdown, GlDropdownHeader, GlDropdownItem } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { getDisplayName } from '../utils';
export default {
components: {
GlDropdown,
GlDropdownHeader,
GlDropdownItem,
Icon,
},
props: {
dropdownLabel: {

View file

@ -1,5 +1,5 @@
<script>
import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox, GlLink } from '@gitlab/ui';
import { GlButton, GlFormGroup, GlFormInput, GlFormCheckbox } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
@ -9,7 +9,6 @@ export default {
GlFormCheckbox,
GlFormGroup,
GlFormInput,
GlLink,
Icon,
},
data() {

View file

@ -1,5 +1,4 @@
<script>
import icon from '~/vue_shared/components/icon.vue';
import { GlBadge } from '@gitlab/ui';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import {
@ -13,7 +12,6 @@ import isProjectPendingRemoval from 'ee_else_ce/groups/mixins/is_project_pending
export default {
components: {
icon,
timeAgoTooltip,
itemStatsValue,
GlBadge,

View file

@ -3,16 +3,12 @@ import { mapActions } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import StageButton from './stage_button.vue';
import UnstageButton from './unstage_button.vue';
import { viewerTypes } from '../../constants';
import { getCommitIconMap } from '../../utils';
export default {
components: {
Icon,
StageButton,
UnstageButton,
FileIcon,
},
directives: {

View file

@ -1,13 +1,11 @@
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import Icon from '~/vue_shared/components/icon.vue';
import IdeTreeList from './ide_tree_list.vue';
import Upload from './new_dropdown/upload.vue';
import NewEntryButton from './new_dropdown/button.vue';
export default {
components: {
Icon,
Upload,
IdeTreeList,
NewEntryButton,

View file

@ -1,14 +1,12 @@
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlSkeletonLoading } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import FileRow from '~/vue_shared/components/file_row.vue';
import NavDropdown from './nav_dropdown.vue';
import FileRowExtra from './file_row_extra.vue';
export default {
components: {
Icon,
GlSkeletonLoading,
NavDropdown,
FileRow,

View file

@ -1,12 +1,10 @@
<script>
import $ from 'jquery';
import Icon from '~/vue_shared/components/icon.vue';
import NavForm from './nav_form.vue';
import NavDropdownButton from './nav_dropdown_button.vue';
export default {
components: {
Icon,
NavDropdownButton,
NavForm,
},

View file

@ -1,10 +1,8 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
import ItemButton from './button.vue';
export default {
components: {
Icon,
ItemButton,
},
props: {

View file

@ -1,7 +1,6 @@
<script>
import { mapState, mapActions, mapGetters } from 'vuex';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
import CommitFilesList from './commit_sidebar/list.vue';
import EmptyState from './commit_sidebar/empty_state.vue';
@ -11,7 +10,6 @@ import { activityBarViews, stageKeys } from '../constants';
export default {
components: {
DeprecatedModal,
Icon,
CommitFilesList,
EmptyState,
},

View file

@ -1,13 +1,11 @@
<script>
import { mapActions } from 'vuex';
import RepoTab from './repo_tab.vue';
import EditorMode from './editor_mode_dropdown.vue';
import router from '../ide_router';
export default {
components: {
RepoTab,
EditorMode,
},
props: {
activeFile: {

View file

@ -2,7 +2,6 @@
import { mapState, mapGetters, mapActions } from 'vuex';
import Select2Select from '~/vue_shared/components/select2_select.vue';
import { __ } from '~/locale';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '../event_hub';
import { STATUSES } from '../constants';
import ImportStatus from './import_status.vue';
@ -11,7 +10,6 @@ export default {
name: 'ProviderRepoTableRow',
components: {
Select2Select,
LoadingButton,
ImportStatus,
},
props: {

View file

@ -2,12 +2,10 @@
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
CiIcon,
Icon,
GlLink,
},
props: {

View file

@ -1,6 +1,6 @@
<script>
import { flatten, isNumber } from 'underscore';
import { GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { roundOffFloat } from '~/lib/utils/common_utils';
import { hexToRgb } from '~/lib/utils/color_utils';
import { areaOpacityValues, symbolSizes, colorValues } from '../../constants';
@ -48,7 +48,6 @@ const AREA_COLOR_RGBA = `rgba(${hexToRgb(AREA_COLOR).join(',')},${AREA_OPACITY})
*/
export default {
components: {
GlLineChart,
GlChartSeriesLabel,
MonitorTimeSeriesChart,
},

View file

@ -2,7 +2,6 @@
import { mapActions, mapState, mapGetters } from 'vuex';
import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue';
import { getParameterValues, removeParams } from '~/lib/utils/url_utility';
import GraphGroup from './graph_group.vue';
import { sidebarAnimationDuration } from '../constants';
import { getTimeDiff } from '../utils';
@ -10,7 +9,6 @@ let sidebarMutationObserver;
export default {
components: {
GraphGroup,
PanelType,
},
props: {

View file

@ -1,7 +1,6 @@
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import { GlPopover, GlSkeletonLoading } from '@gitlab/ui';
import Icon from '../../vue_shared/components/icon.vue';
import CiIcon from '../../vue_shared/components/ci_icon.vue';
import timeagoMixin from '../../vue_shared/mixins/timeago';
import query from '../queries/merge_request.query.graphql';
@ -13,7 +12,6 @@ export default {
components: {
GlPopover,
GlSkeletonLoading,
Icon,
CiIcon,
},
mixins: [timeagoMixin],

View file

@ -1,12 +1,11 @@
<script>
import { GlLink, GlButton } from '@gitlab/ui';
import { GlButton } from '@gitlab/ui';
import LoadingButton from '../../vue_shared/components/loading_button.vue';
export default {
name: 'PipelineNavControls',
components: {
LoadingButton,
GlLink,
GlButton,
},
props: {

View file

@ -2,7 +2,6 @@
import _ from 'underscore';
import { GlLink } from '@gitlab/ui';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { s__, sprintf } from '~/locale';
@ -15,7 +14,6 @@ export default {
components: {
GlModal: DeprecatedModal2,
GlLink,
ClipboardButton,
CiIcon,
},
props: {

View file

@ -2,7 +2,6 @@
import { GlLink, GlTooltipDirective } from '@gitlab/ui';
import _ from 'underscore';
import { __, sprintf } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import popover from '~/vue_shared/directives/popover';
const popoverTitle = sprintf(
@ -17,7 +16,6 @@ const popoverTitle = sprintf(
export default {
components: {
UserAvatarLink,
GlLink,
},
directives: {

View file

@ -1,5 +1,5 @@
<script>
import { GlButton, GlLink, GlProgressBar } from '@gitlab/ui';
import { GlButton, GlProgressBar } from '@gitlab/ui';
import { __ } from '~/locale';
import { formatTime, secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import Icon from '~/vue_shared/components/icon.vue';
@ -8,7 +8,6 @@ export default {
name: 'TestSummary',
components: {
GlButton,
GlLink,
GlProgressBar,
Icon,
},

View file

@ -1,26 +1,23 @@
<script>
import { mapState } from 'vuex';
import { s__, sprintf } from '~/locale';
import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import SettingsForm from './settings_form.vue';
export default {
components: {},
components: {
GlLoadingIcon,
SettingsForm,
},
computed: {
...mapState({
helpPagePath: 'helpPagePath',
isLoading: 'isLoading',
}),
helpText() {
return sprintf(
s__(
'PackageRegistry|Read more about the %{helpLinkStart}Container Registry tag retention policies%{helpLinkEnd}',
),
{
helpLinkStart: `<a href="${this.helpPagePath}" target="_blank">`,
helpLinkEnd: '</a>',
},
false,
);
},
},
mounted() {
this.fetchSettings();
},
methods: {
...mapActions(['fetchSettings']),
},
};
</script>
@ -28,16 +25,19 @@ export default {
<template>
<div>
<p>
{{ s__('PackageRegistry|Tag retention policies are designed to:') }}
{{ s__('ContainerRegistry|Tag expiration policy is designed to:') }}
</p>
<ul>
<li>{{ s__('PackageRegistry|Keep and protect the images that matter most.') }}</li>
<li>{{ s__('ContainerRegistry|Keep and protect the images that matter most.') }}</li>
<li>
{{
s__("PackageRegistry|Automatically remove extra images that aren't designed to be kept.")
s__(
"ContainerRegistry|Automatically remove extra images that aren't designed to be kept.",
)
}}
</li>
</ul>
<p ref="help-link" v-html="helpText"></p>
<gl-loading-icon v-if="isLoading" ref="loading-icon" />
<settings-form v-else ref="settings-form" />
</div>
</template>

View file

@ -0,0 +1,158 @@
<script>
import { mapActions } from 'vuex';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputed } from '~/vuex_shared/bindings';
export default {
components: {
GlFormGroup,
GlToggle,
GlFormSelect,
GlFormTextarea,
GlButton,
},
labelsConfig: {
cols: 3,
align: 'right',
},
computed: {
...mapComputed('settings', 'updateSettings', [
'enabled',
'cadence',
'older_than',
'keep_n',
'name_regex',
]),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
toggleDescriptionText() {
return sprintf(
s__('ContainerRegistry|Docker tag expiration policy is %{toggleStatus}'),
{
toggleStatus: `<strong>${this.policyEnabledText}</strong>`,
},
false,
);
},
regexHelpText() {
return sprintf(
s__(
'ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported',
),
{
codeStart: '<code>',
codeEnd: '</code>',
},
false,
);
},
nameRegexPlaceholder() {
return '.*';
},
nameRegexState() {
return this.name_regex ? this.name_regex.length <= NAME_REGEX_LENGTH : null;
},
formIsValid() {
return this.nameRegexState === false;
},
},
methods: {
...mapActions(['resetSettings', 'saveSettings']),
},
};
</script>
<template>
<div class="card">
<form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
<div class="card-header">
{{ s__('ContainerRegistry|Tag expiration policy') }}
</div>
<div class="card-body">
<gl-form-group
id="expiration-policy-toggle-group"
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
label-for="expiration-policy-toggle"
:label="s__('ContainerRegistry|Expiration policy:')"
>
<div class="d-flex align-items-start">
<gl-toggle id="expiration-policy-toggle" v-model="enabled" />
<span class="mb-2 ml-1 lh-2" v-html="toggleDescriptionText"></span>
</div>
</gl-form-group>
<gl-form-group
id="expiration-policy-interval-group"
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
label-for="expiration-policy-interval"
:label="s__('ContainerRegistry|Expiration interval:')"
>
<gl-form-select id="expiration-policy-interval" v-model="older_than">
<option value="1">{{ __('Option 1') }}</option>
<option value="2">{{ __('Option 2') }}</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
id="expiration-policy-schedule-group"
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
label-for="expiration-policy-schedule"
:label="s__('ContainerRegistry|Expiration schedule:')"
>
<gl-form-select id="expiration-policy-schedule" v-model="cadence">
<option value="1">{{ __('Option 1') }}</option>
<option value="2">{{ __('Option 2') }}</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
id="expiration-policy-latest-group"
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
label-for="expiration-policy-latest"
:label="s__('ContainerRegistry|Expiration latest:')"
>
<gl-form-select id="expiration-policy-latest" v-model="keep_n">
<option value="1">{{ __('Option 1') }}</option>
<option value="2">{{ __('Option 2') }}</option>
</gl-form-select>
</gl-form-group>
<gl-form-group
id="expiration-policy-name-matching-group"
:label-cols="$options.labelsConfig.cols"
:label-align="$options.labelsConfig.align"
label-for="expiration-policy-name-matching"
:label="s__('ContainerRegistry|Expire Docker tags with name matching:')"
:state="nameRegexState"
:invalid-feedback="
s__('ContainerRegistry|The value of this input should be less than 255 characters')
"
>
<gl-form-textarea
id="expiration-policy-name-matching"
v-model="name_regex"
:placeholder="nameRegexPlaceholder"
:state="nameRegexState"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
</div>
<div class="card-footer text-right">
<gl-button ref="cancel-button" type="reset">{{ __('Cancel') }}</gl-button>
<gl-button ref="save-button" type="submit" :disabled="formIsValid" variant="success">
{{ __('Save Expiration Policy') }}
</gl-button>
</div>
</form>
</div>
</template>

View file

@ -0,0 +1,15 @@
import { s__ } from '~/locale';
export const FETCH_SETTINGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while fetching the expiration policy.',
);
export const UPDATE_SETTINGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while updating the expiration policy.',
);
export const UPDATE_SETTINGS_SUCCESS_MESSAGE = s__(
'ContainerRegistry|Expiration policy successfully saved.',
);
export const NAME_REGEX_LENGTH = 255;

View file

@ -1,6 +1,6 @@
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import store from './stores/';
import store from './store/';
import RegistrySettingsApp from './components/registry_settings_app.vue';
Vue.use(Translate);

View file

@ -0,0 +1,40 @@
import Api from '~/api';
import createFlash from '~/flash';
import {
FETCH_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '../constants';
import * as types from './mutation_types';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
export const receiveSettingsSuccess = ({ commit }, data = {}) => commit(types.SET_SETTINGS, data);
export const receiveSettingsError = () => createFlash(FETCH_SETTINGS_ERROR_MESSAGE);
export const updateSettingsError = () => createFlash(UPDATE_SETTINGS_ERROR_MESSAGE);
export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
export const fetchSettings = ({ dispatch, state }) => {
dispatch('toggleLoading');
return Api.project(state.projectId)
.then(({ tag_expiration_policies }) =>
dispatch('receiveSettingsSuccess', tag_expiration_policies),
)
.catch(() => dispatch('receiveSettingsError'))
.finally(() => dispatch('toggleLoading'));
};
export const saveSettings = ({ dispatch, state }) => {
dispatch('toggleLoading');
return Api.updateProject(state.projectId, { tag_expiration_policies: state.settings })
.then(({ tag_expiration_policies }) => {
dispatch('receiveSettingsSuccess', tag_expiration_policies);
createFlash(UPDATE_SETTINGS_SUCCESS_MESSAGE);
})
.catch(() => dispatch('updateSettingsError'))
.finally(() => dispatch('toggleLoading'));
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};

View file

@ -0,0 +1,5 @@
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_SETTINGS = 'SET_SETTINGS';
export const RESET_SETTINGS = 'RESET_SETTINGS';

View file

@ -0,0 +1,20 @@
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, initialState) {
state.projectId = initialState.projectId;
},
[types.UPDATE_SETTINGS](state, settings) {
state.settings = { ...state.settings, ...settings };
},
[types.SET_SETTINGS](state, settings) {
state.settings = settings;
state.original = Object.freeze(settings);
},
[types.RESET_SETTINGS](state) {
state.settings = { ...state.original };
},
[types.TOGGLE_LOADING](state) {
state.isLoading = !state.isLoading;
},
};

View file

@ -0,0 +1,26 @@
export default () => ({
/*
* Project Id used to build the API call
*/
projectId: '',
/*
* Boolean to determine if the UI is loading data from the API
*/
isLoading: false,
/*
* This contains the data shown and manipulated in the UI
* Has the following structure:
* {
* enabled: Boolean
* cadence: String,
* older_than: String,
* keep_n: String,
* name_regex: String
* }
*/
settings: {},
/*
* Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel'
*/
original: {},
});

View file

@ -1,6 +0,0 @@
import * as types from './mutation_types';
export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
// to avoid eslint error until more actions are added to the store
export default () => {};

View file

@ -1,4 +0,0 @@
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
// to avoid eslint error until more actions are added to the store
export default () => {};

View file

@ -1,8 +0,0 @@
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, initialState) {
state.helpPagePath = initialState.helpPagePath;
state.registrySettingsEndpoint = initialState.registrySettingsEndpoint;
},
};

View file

@ -1,10 +0,0 @@
export default () => ({
/*
* Help page path to generate the link
*/
helpPagePath: '',
/*
* Settings endpoint to call to fetch and update the settings
*/
registrySettingsEndpoint: '',
});

View file

@ -1,7 +1,7 @@
<script>
/* eslint-disable @gitlab/vue-i18n/no-bare-strings */
import _ from 'underscore';
import { GlTooltipDirective, GlLink, GlBadge, GlButton } from '@gitlab/ui';
import { GlTooltipDirective, GlLink, GlBadge } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@ -20,7 +20,6 @@ export default {
EvidenceBlock,
GlLink,
GlBadge,
GlButton,
Icon,
UserAvatarLink,
ReleaseBlockFooter,

View file

@ -1,14 +1,12 @@
<script>
// import { sprintf, __ } from '~/locale';
import DeprecatedModal2 from '~/vue_shared/components/deprecated_modal_2.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { fieldTypes } from '../constants';
export default {
components: {
Modal: DeprecatedModal2,
LoadingButton,
CodeBlock,
},
props: {

View file

@ -2,7 +2,6 @@
import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import FunctionRow from './function_row.vue';
import EnvironmentRow from './environment_row.vue';
import EmptyState from './empty_state.vue';
import { CHECKING_INSTALLED } from '../constants';
@ -10,7 +9,6 @@ import { CHECKING_INSTALLED } from '../constants';
export default {
components: {
EnvironmentRow,
FunctionRow,
EmptyState,
GlLoadingIcon,
},

View file

@ -1,12 +1,11 @@
<script>
import { GlButton, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
GlButton,
GlLink,
GlLoadingIcon,
Icon,
},

View file

@ -2,7 +2,6 @@
import { sprintf, s__ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import statusIcon from '../mr_widget_status_icon.vue';
import mrWidgetMergeHelp from '../../components/mr_widget_merge_help.vue';
export default {
name: 'MRWidgetMissingBranch',
@ -10,7 +9,6 @@ export default {
tooltip,
},
components: {
mrWidgetMergeHelp,
statusIcon,
},
props: {

View file

@ -5,7 +5,6 @@ import SuggestionDiff from './suggestion_diff.vue';
import Flash from '~/flash';
export default {
components: { SuggestionDiff },
props: {
lineType: {
type: String,

View file

@ -1,10 +1,9 @@
<script>
import { GlLink, GlTooltip } from '@gitlab/ui';
import { GlTooltip } from '@gitlab/ui';
export default {
components: {
GlTooltip,
GlLink,
},
props: {
label: {

View file

@ -0,0 +1,16 @@
export const mapComputed = (root, updateFn, list) => {
const result = {};
list.forEach(key => {
result[key] = {
get() {
return this.$store.state[root][key];
},
set(value) {
this.$store.dispatch(updateFn, { [key]: value });
},
};
});
return result;
};
export default () => {};

View file

@ -22,6 +22,12 @@
}
}
@each $index, $size in $type-scale {
#{'.lh-#{$index}'} {
line-height: $size;
}
}
.border-width-1px { border-width: 1px; }
.border-style-dashed { border-style: dashed; }
.border-style-solid { border-style: solid; }

View file

@ -1,2 +1 @@
#js-registry-settings{ data: { registry_settings_endpoint: '',
help_page_path: help_page_path('user/project/operations/linking_to_an_external_dashboard') } }
#js-registry-settings{ data: { project_id: @project.id, } }

View file

@ -63,10 +63,11 @@
%section.settings.no-animate#js-registry-polcies{ class: ('expanded' if expanded) }
.settings-header
%h4
= _("Container Registry tag expiration policies")
= _("Container Registry tag expiration policy")
= link_to icon('question-circle'), help_page_path('user/packages/container_registry/index', anchor: 'retention-and-expiration-policy'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _("Expiration policies for the Container Registry are a perfect solution for keeping the Registry space down while still enjoying the full power of GitLab CI/CD.")
= _("Expiration policy for the Container Registry is a perfect solution for keeping the Registry space down while still enjoying the full power of GitLab CI/CD.")
.settings-content
= render 'projects/registry/settings/index'

View file

@ -0,0 +1,5 @@
---
title: Updated monaco-editor dependency
merge_request: 21938
author:
type: other

View file

@ -312,6 +312,15 @@ variable value.
| `DAST_FULL_SCAN_ENABLED` | no | Switches the tool to execute [ZAP Full Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Full-Scan) instead of [ZAP Baseline Scan](https://github.com/zaproxy/zaproxy/wiki/ZAP-Baseline-Scan). Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. |
| `DAST_FULL_SCAN_DOMAIN_VALIDATION_REQUIRED` | no | Requires [domain validation](#domain-validation) when running DAST full scans. Boolean. `true`, `True`, or `1` are considered as true value, otherwise false. Defaults to `false`. |
## Reports JSON format
CAUTION: **Caution:**
The JSON report artifacts are not a public API of DAST and their format may change in the future.
The DAST tool emits a JSON report report file. Sample report files can be found in the [DAST repository](https://gitlab.com/gitlab-org/security-products/dast/tree/master/test/end-to-end/expect).
There are two formats of data in the JSON document that are used side by side: the proprietary ZAP format which will be eventually deprecated, and a "common" format which will be the default in the future.
## Security Dashboard
The Security Dashboard is a good place to get an overview of all the security

View file

@ -4746,7 +4746,7 @@ msgstr ""
msgid "Container Registry"
msgstr ""
msgid "Container Registry tag expiration policies"
msgid "Container Registry tag expiration policy"
msgstr ""
msgid "Container Scanning"
@ -4758,6 +4758,9 @@ msgstr ""
msgid "Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for AutoDevOps to work."
msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
@ -4773,12 +4776,36 @@ msgstr ""
msgid "ContainerRegistry|Docker connection error"
msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr ""
msgid "ContainerRegistry|Expiration interval:"
msgstr ""
msgid "ContainerRegistry|Expiration latest:"
msgstr ""
msgid "ContainerRegistry|Expiration policy successfully saved."
msgstr ""
msgid "ContainerRegistry|Expiration policy:"
msgstr ""
msgid "ContainerRegistry|Expiration schedule:"
msgstr ""
msgid "ContainerRegistry|Expire Docker tags with name matching:"
msgstr ""
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
msgid "ContainerRegistry|Last Updated"
msgstr ""
@ -4799,12 +4826,27 @@ msgstr[1] ""
msgid "ContainerRegistry|Size"
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
msgstr ""
msgid "ContainerRegistry|The value of this input should be less than 255 characters"
msgstr ""
msgid "ContainerRegistry|There are no container images available in this group"
msgstr ""
@ -4817,6 +4859,9 @@ msgstr ""
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|Wildcards such as %{codeStart}*-stable%{codeEnd} or %{codeStart}production/*%{codeEnd} are supported"
msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
msgstr ""
@ -7295,7 +7340,7 @@ msgstr ""
msgid "Expiration date"
msgstr ""
msgid "Expiration policies for the Container Registry are a perfect solution for keeping the Registry space down while still enjoying the full power of GitLab CI/CD."
msgid "Expiration policy for the Container Registry is a perfect solution for keeping the Registry space down while still enjoying the full power of GitLab CI/CD."
msgstr ""
msgid "Expired"
@ -12416,6 +12461,12 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr ""
msgid "Option 1"
msgstr ""
msgid "Option 2"
msgstr ""
msgid "Optional"
msgstr ""
@ -12476,9 +12527,6 @@ msgstr ""
msgid "Package was removed"
msgstr ""
msgid "PackageRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
msgid "PackageRegistry|Copy Maven XML"
msgstr ""
@ -12518,9 +12566,6 @@ msgstr ""
msgid "PackageRegistry|Installation"
msgstr ""
msgid "PackageRegistry|Keep and protect the images that matter most."
msgstr ""
msgid "PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab."
msgstr ""
@ -12533,18 +12578,12 @@ msgstr ""
msgid "PackageRegistry|Package installation"
msgstr ""
msgid "PackageRegistry|Read more about the %{helpLinkStart}Container Registry tag retention policies%{helpLinkEnd}"
msgstr ""
msgid "PackageRegistry|Registry Setup"
msgstr ""
msgid "PackageRegistry|Remove package"
msgstr ""
msgid "PackageRegistry|Tag retention policies are designed to:"
msgstr ""
msgid "PackageRegistry|There are no packages yet"
msgstr ""
@ -15522,6 +15561,9 @@ msgstr ""
msgid "Save Changes"
msgstr ""
msgid "Save Expiration Policy"
msgstr ""
msgid "Save anyway"
msgstr ""

View file

@ -95,7 +95,7 @@
"katex": "^0.10.0",
"marked": "^0.3.12",
"mermaid": "^8.4.2",
"monaco-editor": "^0.15.6",
"monaco-editor": "^0.18.1",
"monaco-editor-webpack-plugin": "^1.7.0",
"mousetrap": "^1.4.6",
"pdfjs-dist": "^2.0.943",
@ -200,7 +200,8 @@
"yarn-deduplicate": "^1.1.1"
},
"resolutions": {
"vue-jest/ts-jest": "24.0.0"
"vue-jest/ts-jest": "24.0.0",
"monaco-editor" : "0.18.1"
},
"engines": {
"node": ">=8.10.0",

View file

@ -11,7 +11,7 @@ describe Projects::ErrorTracking::ProjectsController do
project.add_maintainer(user)
end
describe 'POST #index' do
describe 'GET #index' do
context 'with insufficient permissions' do
before do
project.add_guest(user)

View file

@ -97,9 +97,7 @@ describe 'User comments on a diff', :js do
end
context 'multiple suggestions in expanded lines' do
# Report issue: https://gitlab.com/gitlab-org/gitlab/issues/38277
# Fix issue: https://gitlab.com/gitlab-org/gitlab/issues/39095
it 'suggestions are appliable', :quarantine do
it 'suggestions are appliable' do
diff_file = merge_request.diffs(paths: ['files/ruby/popen.rb']).diff_files.first
hash = Digest::SHA1.hexdigest(diff_file.file_path)

View file

@ -151,6 +151,21 @@ describe('Api', () => {
});
});
describe('updateProject', () => {
it('update a project with the given payload', done => {
const projectPath = 'foo';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}`;
mock.onPut(expectedUrl).reply(200, { foo: 'bar' });
Api.updateProject(projectPath, { foo: 'bar' })
.then(({ data }) => {
expect(data.foo).toBe('bar');
done();
})
.catch(done.fail);
});
});
describe('projectUsers', () => {
it('fetches all users of a particular project', done => {
const query = 'dummy query';

View file

@ -1,10 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Registry List renders 1`] = `
exports[`Registry Settings App renders 1`] = `
<div>
<p>
Tag retention policies are designed to:
Tag expiration policy is designed to:
</p>
@ -20,14 +20,6 @@ exports[`Registry List renders 1`] = `
</li>
</ul>
<p>
Read more about the
<a
href="foo"
target="_blank"
>
Container Registry tag retention policies
</a>
</p>
<settingsform-stub />
</div>
`;

View file

@ -0,0 +1,155 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Form renders 1`] = `
<div
class="card"
>
<form>
<div
class="card-header"
>
Tag expiration policy
</div>
<div
class="card-body"
>
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
label-cols="3"
label-for="expiration-policy-toggle"
>
<div
class="d-flex align-items-start"
>
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
/>
<span
class="mb-2 ml-1 lh-2"
>
Docker tag expiration policy is
<strong>
disabled
</strong>
</span>
</div>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<glformselect-stub
id="expiration-policy-interval"
>
<option
value="1"
>
Option 1
</option>
<option
value="2"
>
Option 2
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<glformselect-stub
id="expiration-policy-schedule"
>
<option
value="1"
>
Option 1
</option>
<option
value="2"
>
Option 2
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-latest-group"
label="Expiration latest:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<glformselect-stub
id="expiration-policy-latest"
>
<option
value="1"
>
Option 1
</option>
<option
value="2"
>
Option 2
</option>
</glformselect-stub>
</glformgroup-stub>
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Expire Docker tags with name matching:"
label-align="right"
label-cols="3"
label-for="expiration-policy-name-matching"
>
<glformtextarea-stub
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</glformgroup-stub>
</div>
<div
class="card-footer text-right"
>
<glbutton-stub
type="reset"
>
Cancel
</glbutton-stub>
<glbutton-stub
type="submit"
variant="success"
>
Save Expiration Policy
</glbutton-stub>
</div>
</form>
</div>
`;

View file

@ -1,29 +1,34 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import component from '~/registry/settings/components/registry_settings_app.vue';
import { createStore } from '~/registry/settings/stores/';
import { createStore } from '~/registry/settings/store/';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Registry List', () => {
describe('Registry Settings App', () => {
let wrapper;
let store;
let fetchSpy;
const helpPagePath = 'foo';
const findHelpLink = () => wrapper.find({ ref: 'help-link' }).find('a');
const findSettingsComponent = () => wrapper.find({ ref: 'settings-form' });
const findLoadingComponent = () => wrapper.find({ ref: 'loading-icon' });
const mountComponent = (options = {}) =>
shallowMount(component, {
const mountComponent = (options = {}) => {
fetchSpy = jest.fn();
wrapper = shallowMount(component, {
sync: false,
store,
methods: {
fetchSettings: fetchSpy,
},
...options,
});
};
beforeEach(() => {
store = createStore();
store.dispatch('setInitialState', { helpPagePath });
wrapper = mountComponent();
mountComponent();
});
afterEach(() => {
@ -34,7 +39,18 @@ describe('Registry List', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('renders an help link dependant on the helphPagePath', () => {
expect(findHelpLink().attributes('href')).toBe(helpPagePath);
it('call the store function to load the data on mount', () => {
expect(fetchSpy).toHaveBeenCalled();
});
it('renders a loader if isLoading is true', () => {
store.dispatch('toggleLoading');
return wrapper.vm.$nextTick().then(() => {
expect(findLoadingComponent().exists()).toBe(true);
expect(findSettingsComponent().exists()).toBe(false);
});
});
it('renders the setting form', () => {
expect(findSettingsComponent().exists()).toBe(true);
});
});

View file

@ -0,0 +1,154 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import component from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/';
import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Settings Form', () => {
let wrapper;
let store;
let saveSpy;
let resetSpy;
const helpPagePath = 'foo';
const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`);
const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
const findSaveButton = () => wrapper.find({ ref: 'save-button' });
const findForm = () => wrapper.find({ ref: 'form-element' });
const mountComponent = (options = {}) => {
saveSpy = jest.fn();
resetSpy = jest.fn();
wrapper = shallowMount(component, {
sync: false,
store,
methods: {
saveSettings: saveSpy,
resetSettings: resetSpy,
},
...options,
});
};
beforeEach(() => {
store = createStore();
store.dispatch('setInitialState', { helpPagePath });
mountComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
expect(wrapper.element).toMatchSnapshot();
});
describe.each`
elementName | modelName | value
${'toggle'} | ${'enabled'} | ${true}
${'interval'} | ${'older_than'} | ${'foo'}
${'schedule'} | ${'cadence'} | ${'foo'}
${'latest'} | ${'keep_n'} | ${'foo'}
${'name-matching'} | ${'name_regex'} | ${'foo'}
`('%s form element', ({ elementName, modelName, value }) => {
let formGroup;
beforeEach(() => {
formGroup = findFormGroup(elementName);
});
it(`${elementName} form group exist in the dom`, () => {
expect(formGroup.exists()).toBe(true);
});
it(`${elementName} form group has a label-for property`, () => {
expect(formGroup.attributes('label-for')).toBe(`expiration-policy-${elementName}`);
});
it(`${elementName} form group has a label-cols property`, () => {
expect(formGroup.attributes('label-cols')).toBe(`${wrapper.vm.$options.labelsConfig.cols}`);
});
it(`${elementName} form group has a label-align property`, () => {
expect(formGroup.attributes('label-align')).toBe(`${wrapper.vm.$options.labelsConfig.align}`);
});
it(`${elementName} form group contains an input element`, () => {
expect(findFormElements(elementName, formGroup).exists()).toBe(true);
});
it(`${elementName} form element change updated ${modelName} with ${value}`, () => {
const element = findFormElements(elementName, formGroup);
element.vm.$emit('input', value);
expect(wrapper.vm[modelName]).toBe(value);
});
});
describe('form actions', () => {
let form;
beforeEach(() => {
form = findForm();
});
it('cancel has type reset', () => {
expect(findCancelButton().attributes('type')).toBe('reset');
});
it('form reset event call the appropriate function', () => {
form.trigger('reset');
expect(resetSpy).toHaveBeenCalled();
});
it('save has type submit', () => {
expect(findSaveButton().attributes('type')).toBe('submit');
});
it('form submit event call the appropriate function', () => {
form.trigger('submit');
expect(saveSpy).toHaveBeenCalled();
});
});
describe('form validation', () => {
describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => {
const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(',');
beforeEach(() => {
store.dispatch('updateSettings', { name_regex: invalidString });
});
it('save btn is disabled', () => {
expect(findSaveButton().attributes('disabled')).toBeTruthy();
});
it('nameRegexState is false', () => {
expect(wrapper.vm.nameRegexState).toBe(false);
});
});
it('if the user did not type validation is null', () => {
store.dispatch('updateSettings', { name_regex: null });
expect(wrapper.vm.nameRegexState).toBe(null);
return wrapper.vm.$nextTick().then(() => {
expect(findSaveButton().attributes('disabled')).toBeFalsy();
});
});
it(`if the user typed and is less than ${NAME_REGEX_LENGTH} state is true`, () => {
store.dispatch('updateSettings', { name_regex: 'abc' });
expect(wrapper.vm.nameRegexState).toBe(true);
});
});
describe('help text', () => {
it('toggleDescriptionText text reflects enabled property', () => {
const toggleHelpText = findFormGroup('toggle').find('span');
expect(toggleHelpText.html()).toContain('disabled');
wrapper.vm.enabled = true;
return wrapper.vm.$nextTick().then(() => {
expect(toggleHelpText.html()).toContain('enabled');
});
});
});
});

View file

@ -0,0 +1,120 @@
import Api from '~/api';
import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/registry/settings/store/actions';
import * as types from '~/registry/settings/store/mutation_types';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
FETCH_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '~/registry/settings/constants';
jest.mock('~/flash');
describe('Actions Registry Store', () => {
describe.each`
actionName | mutationName | payload
${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'}
${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'}
${'receiveSettingsSuccess'} | ${types.SET_SETTINGS} | ${'foo'}
${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined}
${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined}
`('%s action invokes %s mutation with payload %s', ({ actionName, mutationName, payload }) => {
it('should set the initial state', done => {
testAction(actions[actionName], payload, {}, [{ type: mutationName, payload }], [], done);
});
});
describe.each`
actionName | message
${'receiveSettingsError'} | ${FETCH_SETTINGS_ERROR_MESSAGE}
${'updateSettingsError'} | ${UPDATE_SETTINGS_ERROR_MESSAGE}
`('%s action', ({ actionName, message }) => {
it(`should call createFlash with ${message}`, done => {
testAction(actions[actionName], null, null, [], [], () => {
expect(createFlash).toHaveBeenCalledWith(message);
done();
});
});
});
describe('fetchSettings', () => {
const state = {
projectId: 'bar',
};
const payload = {
tag_expiration_policies: 'foo',
};
it('should fetch the data from the API', done => {
Api.project = jest.fn().mockResolvedValue(payload);
testAction(
actions.fetchSettings,
null,
state,
[],
[
{ type: 'toggleLoading' },
{ type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies },
{ type: 'toggleLoading' },
],
done,
);
});
it('should call receiveSettingsError on error', done => {
Api.project = jest.fn().mockRejectedValue();
testAction(
actions.fetchSettings,
null,
state,
[],
[{ type: 'toggleLoading' }, { type: 'receiveSettingsError' }, { type: 'toggleLoading' }],
done,
);
});
});
describe('saveSettings', () => {
const state = {
projectId: 'bar',
settings: 'baz',
};
const payload = {
tag_expiration_policies: 'foo',
};
it('should fetch the data from the API', done => {
Api.updateProject = jest.fn().mockResolvedValue(payload);
testAction(
actions.saveSettings,
null,
state,
[],
[
{ type: 'toggleLoading' },
{ type: 'receiveSettingsSuccess', payload: payload.tag_expiration_policies },
{ type: 'toggleLoading' },
],
() => {
expect(createFlash).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
done();
},
);
});
it('should call receiveSettingsError on error', done => {
Api.updateProject = jest.fn().mockRejectedValue();
testAction(
actions.saveSettings,
null,
state,
[],
[{ type: 'toggleLoading' }, { type: 'updateSettingsError' }, { type: 'toggleLoading' }],
done,
);
});
});
});

View file

@ -0,0 +1,54 @@
import mutations from '~/registry/settings/store/mutations';
import * as types from '~/registry/settings/store/mutation_types';
import createState from '~/registry/settings/store/state';
describe('Mutations Registry Store', () => {
let mockState;
beforeEach(() => {
mockState = createState();
});
describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => {
const payload = { helpPagePath: 'foo', projectId: 'bar' };
const expectedState = { ...mockState, ...payload };
mutations[types.SET_INITIAL_STATE](mockState, payload);
expect(mockState.projectId).toEqual(expectedState.projectId);
});
});
describe('UPDATE_SETTINGS', () => {
it('should update the settings', () => {
mockState.settings = { foo: 'bar' };
const payload = { foo: 'baz' };
const expectedState = { ...mockState, settings: payload };
mutations[types.UPDATE_SETTINGS](mockState, payload);
expect(mockState.settings).toEqual(expectedState.settings);
});
});
describe('SET_SETTINGS', () => {
it('should set the settings and original', () => {
const payload = { foo: 'baz' };
const expectedState = { ...mockState, settings: payload };
mutations[types.SET_SETTINGS](mockState, payload);
expect(mockState.settings).toEqual(expectedState.settings);
expect(mockState.original).toEqual(expectedState.settings);
});
});
describe('RESET_SETTINGS', () => {
it('should copy original over settings', () => {
mockState.settings = { foo: 'bar' };
mockState.original = { foo: 'baz' };
mutations[types.RESET_SETTINGS](mockState);
expect(mockState.settings).toEqual(mockState.original);
});
});
describe('TOGGLE_LOADING', () => {
it('should toggle the loading', () => {
mutations[types.TOGGLE_LOADING](mockState);
expect(mockState.isLoading).toEqual(true);
});
});
});

View file

@ -1,20 +0,0 @@
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/registry/settings/stores/actions';
import * as types from '~/registry/settings/stores/mutation_types';
jest.mock('~/flash.js');
describe('Actions Registry Store', () => {
describe('setInitialState', () => {
it('should set the initial state', done => {
testAction(
actions.setInitialState,
'foo',
{},
[{ type: types.SET_INITIAL_STATE, payload: 'foo' }],
[],
done,
);
});
});
});

View file

@ -1,21 +0,0 @@
import mutations from '~/registry/settings/stores/mutations';
import * as types from '~/registry/settings/stores/mutation_types';
import createState from '~/registry/settings/stores/state';
describe('Mutations Registry Store', () => {
let mockState;
beforeEach(() => {
mockState = createState();
});
describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => {
const payload = { helpPagePath: 'foo', registrySettingsEndpoint: 'bar' };
const expectedState = { ...mockState, ...payload };
mutations[types.SET_INITIAL_STATE](mockState, payload);
expect(mockState.endpoint).toEqual(expectedState.endpoint);
});
});
});

View file

@ -0,0 +1,51 @@
import { shallowMount } from '@vue/test-utils';
import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => {
describe('mapComputed', () => {
const dummyComponent = {
computed: {
...mapComputed('foo', 'bar', ['baz']),
},
render() {
return null;
},
};
it('returns an object with keys equal to the last fn parameter ', () => {
const keyList = ['foo1', 'foo2'];
const result = mapComputed('foo', 'bar', keyList);
expect(Object.keys(result)).toEqual(keyList);
});
it('returned object has set and get function', () => {
const result = mapComputed('foo', 'bar', ['baz']);
expect(result.baz.set).toBeDefined();
expect(result.baz.get).toBeDefined();
});
it('set function invokes $store.dispatch', () => {
const context = shallowMount(dummyComponent, {
mocks: {
$store: {
dispatch: jest.fn(),
},
},
});
context.vm.baz = 'a';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
});
it('get function returns $store.state[root][key]', () => {
const context = shallowMount(dummyComponent, {
mocks: {
$store: {
state: {
foo: {
baz: 1,
},
},
},
},
});
expect(context.vm.baz).toBe(1);
});
});
});

View file

@ -69,13 +69,19 @@ describe('diffs/components/app', () => {
describe('fetch diff methods', () => {
beforeEach(() => {
const fetchResolver = () => {
store.state.diffs.retrievingBatches = false;
return Promise.resolve();
};
spyOn(window, 'requestIdleCallback').and.callFake(fn => fn());
createComponent();
spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(() => Promise.resolve());
spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(() => Promise.resolve());
spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(() => Promise.resolve());
spyOn(wrapper.vm, 'fetchDiffFiles').and.callFake(fetchResolver);
spyOn(wrapper.vm, 'fetchDiffFilesMeta').and.callFake(fetchResolver);
spyOn(wrapper.vm, 'fetchDiffFilesBatch').and.callFake(fetchResolver);
spyOn(wrapper.vm, 'setDiscussions');
spyOn(wrapper.vm, 'startRenderDiffsQueue');
spyOn(wrapper.vm, 'unwatchDiscussions');
store.state.diffs.retrievingBatches = true;
});
it('calls fetchDiffFiles if diffsBatchLoad is not enabled', done => {
@ -87,6 +93,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).not.toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).not.toHaveBeenCalled();
expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
done();
});
@ -102,6 +109,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
});
});
@ -114,6 +122,7 @@ describe('diffs/components/app', () => {
expect(wrapper.vm.startRenderDiffsQueue).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesMeta).toHaveBeenCalled();
expect(wrapper.vm.fetchDiffFilesBatch).toHaveBeenCalled();
expect(wrapper.vm.unwatchDiscussions).toHaveBeenCalled();
});
});
});

View file

@ -163,10 +163,12 @@ describe('DiffsStoreActions', () => {
{ endpointBatch },
[
{ type: types.SET_BATCH_LOADING, payload: true },
{ type: types.SET_RETRIEVING_BATCHES, payload: true },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: res1.diff_files } },
{ type: types.SET_BATCH_LOADING, payload: false },
{ type: types.SET_DIFF_DATA_BATCH, payload: { diff_files: [] } },
{ type: types.SET_BATCH_LOADING, payload: false },
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
],
[],
() => {
@ -215,6 +217,8 @@ describe('DiffsStoreActions', () => {
describe('assignDiscussionsToDiff', () => {
it('should merge discussions into diffs', done => {
window.location.hash = 'ABC_123';
const state = {
diffFiles: [
{

View file

@ -40,6 +40,16 @@ describe('DiffsStoreMutations', () => {
});
});
describe('SET_RETRIEVING_BATCHES', () => {
it('should set retrievingBatches state', () => {
const state = {};
mutations[types.SET_RETRIEVING_BATCHES](state, false);
expect(state.retrievingBatches).toEqual(false);
});
});
describe('SET_DIFF_DATA', () => {
it('should set diff data type properly', () => {
const state = {};

View file

@ -7820,10 +7820,10 @@ monaco-editor-webpack-plugin@^1.7.0:
dependencies:
"@types/webpack" "^4.4.19"
monaco-editor@^0.15.6:
version "0.15.6"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.15.6.tgz#d63b3b06f86f803464f003b252627c3eb4a09483"
integrity sha512-JoU9V9k6KqT9R9Tiw1RTU8ohZ+Xnf9DMg6Ktqqw5hILumwmq7xqa/KLXw513uTUsWbhtnHoSJYYR++u3pkyxJg==
monaco-editor@0.18.1, monaco-editor@^0.18.1:
version "0.18.1"
resolved "https://registry.yarnpkg.com/monaco-editor/-/monaco-editor-0.18.1.tgz#ced7c305a23109875feeaf395a504b91f6358cfc"
integrity sha512-fmL+RFZ2Hrezy+X/5ZczQW51LUmvzfcqOurnkCIRFTyjdVjzR7JvENzI6+VKBJzJdPh6EYL4RoWl92b2Hrk9fw==
mousetrap@^1.4.6:
version "1.4.6"