Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
8099b2824b
commit
de0e57e387
|
|
@ -9,7 +9,7 @@ export default function initDatePickers() {
|
|||
|
||||
const calendar = new Pikaday({
|
||||
field: $datePicker.get(0),
|
||||
theme: 'gitlab-theme animate-picker',
|
||||
theme: 'gl-datepicker-theme animate-picker',
|
||||
format: 'yyyy-mm-dd',
|
||||
container: $datePicker.parent().get(0),
|
||||
parse: (dateString) => parsePikadayDate(dateString),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
<script>
|
||||
import { debounce, uniq } from 'lodash';
|
||||
import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox } from '@gitlab/ui';
|
||||
import { __, sprintf } from '~/locale';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
import { __, s__, sprintf } from '~/locale';
|
||||
import { convertEnvironmentScope } from '../utils';
|
||||
|
||||
export default {
|
||||
|
|
@ -10,7 +12,12 @@ export default {
|
|||
GlDropdownItem,
|
||||
GlCollapsibleListbox,
|
||||
},
|
||||
mixins: [glFeatureFlagsMixin()],
|
||||
props: {
|
||||
areEnvironmentsLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
environments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
|
@ -33,24 +40,52 @@ export default {
|
|||
},
|
||||
filteredEnvironments() {
|
||||
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
|
||||
return this.environments.filter((environment) => {
|
||||
return environment.toLowerCase().includes(lowerCasedSearchTerm);
|
||||
});
|
||||
},
|
||||
isEnvScopeLimited() {
|
||||
return this.glFeatures?.ciLimitEnvironmentScope;
|
||||
},
|
||||
searchedEnvironments() {
|
||||
// If FF is enabled, search query will be fired so this component will already
|
||||
// receive filtered environments during the refetch.
|
||||
// If FF is disabled, search the existing list of environments in the frontend
|
||||
let filtered = this.isEnvScopeLimited ? this.environments : this.filteredEnvironments;
|
||||
|
||||
return this.environments
|
||||
.filter((environment) => {
|
||||
return environment.toLowerCase().includes(lowerCasedSearchTerm);
|
||||
})
|
||||
.map((environment) => ({
|
||||
value: environment,
|
||||
text: environment,
|
||||
}));
|
||||
// If there is no search term, make sure to include *
|
||||
if (this.isEnvScopeLimited && !this.searchTerm) {
|
||||
filtered = uniq([...filtered, '*']);
|
||||
}
|
||||
|
||||
return filtered.sort().map((environment) => ({
|
||||
value: environment,
|
||||
text: environment,
|
||||
}));
|
||||
},
|
||||
shouldShowSearchLoading() {
|
||||
return this.areEnvironmentsLoading && this.isEnvScopeLimited;
|
||||
},
|
||||
shouldRenderCreateButton() {
|
||||
return this.searchTerm && !this.environments.includes(this.searchTerm);
|
||||
},
|
||||
shouldRenderDivider() {
|
||||
return (
|
||||
(this.isEnvScopeLimited || this.shouldRenderCreateButton) && !this.shouldShowSearchLoading
|
||||
);
|
||||
},
|
||||
environmentScopeLabel() {
|
||||
return convertEnvironmentScope(this.selectedEnvironmentScope);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
debouncedSearch: debounce(function debouncedSearch(searchTerm) {
|
||||
const newSearchTerm = searchTerm.trim();
|
||||
this.searchTerm = newSearchTerm;
|
||||
if (this.isEnvScopeLimited) {
|
||||
this.$emit('search-environment-scope', newSearchTerm);
|
||||
}
|
||||
}, 500),
|
||||
selectEnvironment(selected) {
|
||||
this.$emit('select-environment', selected);
|
||||
this.selectedEnvironment = selected;
|
||||
|
|
@ -60,22 +95,43 @@ export default {
|
|||
this.selectEnvironment(this.searchTerm);
|
||||
},
|
||||
},
|
||||
i18n: {
|
||||
maxEnvsNote: s__(
|
||||
'CiVariable|Maximum of 20 environments listed. For more environments, enter a search query.',
|
||||
),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<gl-collapsible-listbox
|
||||
v-model="selectedEnvironment"
|
||||
block
|
||||
searchable
|
||||
:items="filteredEnvironments"
|
||||
:items="searchedEnvironments"
|
||||
:searching="shouldShowSearchLoading"
|
||||
:toggle-text="environmentScopeLabel"
|
||||
@search="searchTerm = $event.trim()"
|
||||
@search="debouncedSearch"
|
||||
@select="selectEnvironment"
|
||||
>
|
||||
<template v-if="shouldRenderCreateButton" #footer>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item data-testid="create-wildcard-button" @click="createEnvironmentScope">
|
||||
{{ composedCreateButtonLabel }}
|
||||
</gl-dropdown-item>
|
||||
<template #footer>
|
||||
<gl-dropdown-divider v-if="shouldRenderDivider" />
|
||||
<div v-if="isEnvScopeLimited" data-testid="max-envs-notice">
|
||||
<gl-dropdown-item class="gl-list-style-none" disabled>
|
||||
<span class="gl-font-sm">
|
||||
{{ $options.i18n.maxEnvsNote }}
|
||||
</span>
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
<div v-if="shouldRenderCreateButton">
|
||||
<!-- TODO: Rethink create wildcard button. https://gitlab.com/gitlab-org/gitlab/-/issues/396928 -->
|
||||
<gl-dropdown-item
|
||||
class="gl-list-style-none"
|
||||
data-testid="create-wildcard-button"
|
||||
@click="createEnvironmentScope"
|
||||
>
|
||||
{{ composedCreateButtonLabel }}
|
||||
</gl-dropdown-item>
|
||||
</div>
|
||||
</template>
|
||||
</gl-collapsible-listbox>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -74,6 +74,10 @@ export default {
|
|||
'maskableRegex',
|
||||
],
|
||||
props: {
|
||||
areEnvironmentsLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
areScopedVariablesAvailable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -142,7 +146,11 @@ export default {
|
|||
isTipVisible() {
|
||||
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
|
||||
},
|
||||
joinedEnvironments() {
|
||||
environmentsList() {
|
||||
if (this.glFeatures?.ciLimitEnvironmentScope) {
|
||||
return this.environments;
|
||||
}
|
||||
|
||||
return createJoinedEnvironments(this.variables, this.environments, this.newEnvironments);
|
||||
},
|
||||
maskedFeedback() {
|
||||
|
|
@ -368,10 +376,12 @@ export default {
|
|||
</template>
|
||||
<ci-environments-dropdown
|
||||
v-if="areScopedVariablesAvailable"
|
||||
:are-environments-loading="areEnvironmentsLoading"
|
||||
:selected-environment-scope="variable.environmentScope"
|
||||
:environments="joinedEnvironments"
|
||||
:environments="environmentsList"
|
||||
@select-environment="setEnvironmentScope"
|
||||
@create-environment-scope="createEnvironmentScope"
|
||||
@search-environment-scope="$emit('search-environment-scope', $event)"
|
||||
/>
|
||||
|
||||
<gl-form-input v-else :value="$options.i18n.defaultScope" class="gl-w-full" readonly />
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ export default {
|
|||
CiVariableModal,
|
||||
},
|
||||
props: {
|
||||
areEnvironmentsLoading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
areScopedVariablesAvailable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
@ -100,6 +104,7 @@ export default {
|
|||
/>
|
||||
<ci-variable-modal
|
||||
v-if="showModal"
|
||||
:are-environments-loading="areEnvironmentsLoading"
|
||||
:are-scoped-variables-available="areScopedVariablesAvailable"
|
||||
:environments="environments"
|
||||
:hide-environment-scope="hideEnvironmentScope"
|
||||
|
|
@ -110,6 +115,7 @@ export default {
|
|||
@delete-variable="deleteVariable"
|
||||
@hideModal="hideModal"
|
||||
@update-variable="updateVariable"
|
||||
@search-environment-scope="$emit('search-environment-scope', $event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
|
|||
import {
|
||||
ADD_MUTATION_ACTION,
|
||||
DELETE_MUTATION_ACTION,
|
||||
ENVIRONMENT_QUERY_LIMIT,
|
||||
SORT_DIRECTIONS,
|
||||
UPDATE_MUTATION_ACTION,
|
||||
environmentFetchErrorText,
|
||||
|
|
@ -162,6 +163,7 @@ export default {
|
|||
variables() {
|
||||
return {
|
||||
fullPath: this.fullPath,
|
||||
...this.environmentQueryVariables,
|
||||
};
|
||||
},
|
||||
update(data) {
|
||||
|
|
@ -173,10 +175,26 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
areEnvironmentsLoading() {
|
||||
return this.$apollo.queries.environments.loading;
|
||||
},
|
||||
environmentQueryVariables() {
|
||||
if (this.glFeatures?.ciLimitEnvironmentScope) {
|
||||
return {
|
||||
first: ENVIRONMENT_QUERY_LIMIT,
|
||||
search: '',
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
},
|
||||
isLoading() {
|
||||
// TODO: Remove areEnvironmentsLoading and show loading icon in dropdown when
|
||||
// environment query is loading and FF is enabled
|
||||
// https://gitlab.com/gitlab-org/gitlab/-/issues/396990
|
||||
return (
|
||||
(this.$apollo.queries.ciVariables.loading && this.isInitialLoading) ||
|
||||
this.$apollo.queries.environments.loading ||
|
||||
this.areEnvironmentsLoading ||
|
||||
this.isLoadingMoreItems
|
||||
);
|
||||
},
|
||||
|
|
@ -228,6 +246,11 @@ export default {
|
|||
updateVariable(variable) {
|
||||
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
|
||||
},
|
||||
async searchEnvironmentScope(searchTerm) {
|
||||
if (this.glFeatures?.ciLimitEnvironmentScope) {
|
||||
this.$apollo.queries.environments.refetch({ search: searchTerm });
|
||||
}
|
||||
},
|
||||
async variableMutation(mutationAction, variable) {
|
||||
try {
|
||||
const currentMutation = this.mutationData[mutationAction];
|
||||
|
|
@ -264,6 +287,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<ci-variable-settings
|
||||
:are-environments-loading="areEnvironmentsLoading"
|
||||
:are-scoped-variables-available="areScopedVariablesAvailable"
|
||||
:entity="entity"
|
||||
:environments="environments"
|
||||
|
|
@ -277,6 +301,7 @@ export default {
|
|||
@handle-prev-page="handlePrevPage"
|
||||
@handle-next-page="handleNextPage"
|
||||
@sort-changed="handleSortChanged"
|
||||
@search-environment-scope="searchEnvironmentScope"
|
||||
@update-variable="updateVariable"
|
||||
/>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { __, s__ } from '~/locale';
|
||||
|
||||
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
|
||||
export const ENVIRONMENT_QUERY_LIMIT = 20;
|
||||
|
||||
export const SORT_DIRECTIONS = {
|
||||
ASC: 'KEY_ASC',
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
query getProjectEnvironments($fullPath: ID!) {
|
||||
query getProjectEnvironments($fullPath: ID!, $first: Int, $search: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
environments {
|
||||
environments(first: $first, search: $search) {
|
||||
nodes {
|
||||
id
|
||||
name
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ import {
|
|||
idleCallback,
|
||||
allDiscussionWrappersExpanded,
|
||||
prepareLineForRenamedFile,
|
||||
parseUrlHashAsFileHash,
|
||||
isUrlHashNoteLink,
|
||||
} from './utils';
|
||||
|
||||
export const setBaseConfig = ({ commit }, options) => {
|
||||
|
|
@ -101,6 +103,47 @@ export const setBaseConfig = ({ commit }, options) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const fetchFileByFile = async ({ state, getters, commit }) => {
|
||||
const isNoteLink = isUrlHashNoteLink(window?.location?.hash);
|
||||
const id = parseUrlHashAsFileHash(window?.location?.hash, state.currentDiffFileId);
|
||||
const treeEntry = id
|
||||
? getters.flatBlobsList.find(({ fileHash }) => fileHash === id)
|
||||
: getters.flatBlobsList[0];
|
||||
|
||||
if (treeEntry && !treeEntry.diffLoaded && !getters.getDiffFileByHash(id)) {
|
||||
// Overloading "batch" loading indicators so the UI stays mostly the same
|
||||
commit(types.SET_BATCH_LOADING_STATE, 'loading');
|
||||
commit(types.SET_RETRIEVING_BATCHES, true);
|
||||
|
||||
const urlParams = {
|
||||
old_path: treeEntry.filePaths.old,
|
||||
new_path: treeEntry.filePaths.new,
|
||||
w: state.showWhitespace ? '0' : '1',
|
||||
view: 'inline',
|
||||
};
|
||||
|
||||
axios
|
||||
.get(mergeUrlParams({ ...urlParams }, state.endpointDiffForPath))
|
||||
.then(({ data: diffData }) => {
|
||||
commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files });
|
||||
|
||||
if (!isNoteLink && !state.currentDiffFileId) {
|
||||
commit(types.SET_CURRENT_DIFF_FILE, state.diffFiles[0]?.file_hash || '');
|
||||
}
|
||||
|
||||
commit(types.SET_BATCH_LOADING_STATE, 'loaded');
|
||||
|
||||
eventHub.$emit('diffFilesModified');
|
||||
})
|
||||
.catch(() => {
|
||||
commit(types.SET_BATCH_LOADING_STATE, 'error');
|
||||
})
|
||||
.finally(() => {
|
||||
commit(types.SET_RETRIEVING_BATCHES, false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
|
||||
let perPage = state.viewDiffsFileByFile ? 1 : 5;
|
||||
let increaseAmount = 1.4;
|
||||
|
|
|
|||
|
|
@ -559,19 +559,19 @@ export const allDiscussionWrappersExpanded = (diff) => {
|
|||
return discussionsExpanded;
|
||||
};
|
||||
|
||||
export function isUrlHashNoteLink(urlHash) {
|
||||
export function isUrlHashNoteLink(urlHash = '') {
|
||||
const id = urlHash.replace(/^#/, '');
|
||||
|
||||
return id.startsWith('note');
|
||||
}
|
||||
|
||||
export function isUrlHashFileHeader(urlHash) {
|
||||
export function isUrlHashFileHeader(urlHash = '') {
|
||||
const id = urlHash.replace(/^#/, '');
|
||||
|
||||
return id.startsWith('diff-content');
|
||||
}
|
||||
|
||||
export function parseUrlHashAsFileHash(urlHash, currentDiffFileId = '') {
|
||||
export function parseUrlHashAsFileHash(urlHash = '', currentDiffFileId = '') {
|
||||
const isNoteLink = isUrlHashNoteLink(urlHash);
|
||||
let id = urlHash.replace(/^#/, '');
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export default {
|
|||
data-container="body"
|
||||
data-placement="right"
|
||||
data-qa-selector="edit_mode_tab"
|
||||
data-testid="edit-mode-button"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-edit-mode"
|
||||
@click.prevent="changedActivityView($event, $options.leftSidebarViews.edit.name)"
|
||||
|
|
@ -60,6 +61,7 @@ export default {
|
|||
:aria-label="s__('IDE|Review')"
|
||||
data-container="body"
|
||||
data-placement="right"
|
||||
data-testid="review-mode-button"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-review-mode"
|
||||
@click.prevent="changedActivityView($event, $options.leftSidebarViews.review.name)"
|
||||
|
|
@ -78,6 +80,7 @@ export default {
|
|||
data-container="body"
|
||||
data-placement="right"
|
||||
data-qa-selector="commit_mode_tab"
|
||||
data-testid="commit-mode-button"
|
||||
type="button"
|
||||
class="ide-sidebar-link js-ide-commit-mode"
|
||||
@click.prevent="changedActivityView($event, $options.leftSidebarViews.commit.name)"
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ export default class IssuableForm {
|
|||
if ($issuableDueDate.length) {
|
||||
const calendar = new Pikaday({
|
||||
field: $issuableDueDate.get(0),
|
||||
theme: 'gitlab-theme animate-picker',
|
||||
theme: 'gl-datepicker-theme animate-picker',
|
||||
format: 'yyyy-mm-dd',
|
||||
container: $issuableDueDate.parent().get(0),
|
||||
parse: (dateString) => parsePikadayDate(dateString),
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default class OAuthRememberMe {
|
|||
}
|
||||
|
||||
bindEvents() {
|
||||
$('#remember_me', this.container).on('click', this.toggleRememberMe);
|
||||
$('#remember_me_omniauth', this.container).on('click', this.toggleRememberMe);
|
||||
}
|
||||
|
||||
toggleRememberMe(event) {
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ export default {
|
|||
|
||||
<template>
|
||||
<gl-form
|
||||
class="new-note common-note-form"
|
||||
class="new-note common-note-form gl-mb-6"
|
||||
data-testid="saved-reply-form"
|
||||
@submit.prevent="onSubmit"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="gl-border-t gl-pt-4">
|
||||
<gl-loading-icon v-if="loading" size="lg" />
|
||||
<template v-else>
|
||||
<h5 class="gl-font-lg" data-testid="title">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<script>
|
||||
import { uniqueId } from 'lodash';
|
||||
import { GlButton, GlModal, GlModalDirective, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
|
||||
import { GlDisclosureDropdown, GlTooltip, GlModal, GlModalDirective, GlSprintf } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import deleteSavedReplyMutation from '../queries/delete_saved_reply.mutation.graphql';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
GlButton,
|
||||
GlDisclosureDropdown,
|
||||
GlTooltip,
|
||||
GlModal,
|
||||
GlSprintf,
|
||||
},
|
||||
directives: {
|
||||
GlModal: GlModalDirective,
|
||||
GlTooltip: GlTooltipDirective,
|
||||
},
|
||||
props: {
|
||||
reply: {
|
||||
|
|
@ -25,12 +25,32 @@ export default {
|
|||
return {
|
||||
isDeleting: false,
|
||||
modalId: uniqueId('delete-saved-reply-'),
|
||||
toggleId: uniqueId('actions-toggle-'),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
id() {
|
||||
return getIdFromGraphQLId(this.reply.id);
|
||||
},
|
||||
dropdownItems() {
|
||||
return [
|
||||
{
|
||||
text: __('Edit'),
|
||||
action: () => this.$router.push({ name: 'edit', params: { id: this.id } }),
|
||||
extraAttrs: {
|
||||
'data-testid': 'saved-reply-edit-btn',
|
||||
},
|
||||
},
|
||||
{
|
||||
text: __('Delete'),
|
||||
action: () => this.$refs['delete-modal'].show(),
|
||||
extraAttrs: {
|
||||
'data-testid': 'saved-reply-delete-btn',
|
||||
class: 'gl-text-red-500!',
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onDelete() {
|
||||
|
|
@ -54,34 +74,29 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<li class="gl-mb-5">
|
||||
<li class="gl-pt-4 gl-pb-5 gl-border-b">
|
||||
<div class="gl-display-flex gl-align-items-center">
|
||||
<strong data-testid="saved-reply-name">{{ reply.name }}</strong>
|
||||
<h6 class="gl-mr-3 gl-my-0" data-testid="saved-reply-name">{{ reply.name }}</h6>
|
||||
<div class="gl-ml-auto">
|
||||
<gl-button
|
||||
v-gl-tooltip
|
||||
:to="{ name: 'edit', params: { id: id } }"
|
||||
icon="pencil"
|
||||
:title="__('Edit')"
|
||||
:aria-label="__('Edit')"
|
||||
class="gl-mr-3"
|
||||
data-testid="saved-reply-edit-btn"
|
||||
/>
|
||||
<gl-button
|
||||
v-gl-modal="modalId"
|
||||
v-gl-tooltip
|
||||
icon="remove"
|
||||
:aria-label="__('Delete')"
|
||||
:title="__('Delete')"
|
||||
variant="danger"
|
||||
category="secondary"
|
||||
data-testid="saved-reply-delete-btn"
|
||||
<gl-disclosure-dropdown
|
||||
:items="dropdownItems"
|
||||
:toggle-id="toggleId"
|
||||
icon="ellipsis_v"
|
||||
no-caret
|
||||
text-sr-only
|
||||
placement="right"
|
||||
:toggle-text="__('Saved reply actions')"
|
||||
:loading="isDeleting"
|
||||
category="tertiary"
|
||||
/>
|
||||
<gl-tooltip :target="toggleId">
|
||||
{{ __('Saved reply actions') }}
|
||||
</gl-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gl-mt-3 gl-font-monospace">{{ reply.content }}</div>
|
||||
<gl-modal
|
||||
ref="delete-modal"
|
||||
:title="__('Delete saved reply')"
|
||||
:action-primary="$options.actionPrimary"
|
||||
:action-secondary="$options.actionSecondary"
|
||||
|
|
|
|||
|
|
@ -20,56 +20,3 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.pika-single.gitlab-theme {
|
||||
.pika-label {
|
||||
color: $gl-text-color-secondary;
|
||||
font-size: 14px;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
}
|
||||
|
||||
th {
|
||||
padding: 2px 0;
|
||||
color: $note-disabled-comment-color;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
text-transform: lowercase;
|
||||
border-top: 1px solid $calendar-border-color;
|
||||
}
|
||||
|
||||
abbr {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
td {
|
||||
border: 1px solid $calendar-border-color;
|
||||
|
||||
&:first-child {
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pika-day {
|
||||
border-radius: 0;
|
||||
background-color: $white;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.is-today {
|
||||
.pika-day {
|
||||
color: inherit;
|
||||
font-weight: $gl-font-weight-normal;
|
||||
}
|
||||
}
|
||||
|
||||
.is-selected .pika-day,
|
||||
.pika-day:hover,
|
||||
.is-today .pika-day {
|
||||
background: $gray-darker;
|
||||
color: $gl-text-color;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -669,8 +669,6 @@ $note-targe3-inside: #ffffd3;
|
|||
/*
|
||||
* Calendar
|
||||
*/
|
||||
$calendar-hover-bg: #ecf3fe;
|
||||
$calendar-border-color: rgba(#000, 0.1);
|
||||
$calendar-user-contrib-text: #959494;
|
||||
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ module Projects
|
|||
|
||||
before_action do
|
||||
push_frontend_feature_flag(:ci_variables_pages, current_user)
|
||||
push_frontend_feature_flag(:ci_limit_environment_scope, @project)
|
||||
end
|
||||
|
||||
helper_method :highlight_badge
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ module BreadcrumbsHelper
|
|||
def breadcrumb_title_link
|
||||
return @breadcrumb_link if @breadcrumb_link
|
||||
|
||||
request.path
|
||||
request.fullpath
|
||||
end
|
||||
|
||||
def breadcrumb_title(title)
|
||||
|
|
|
|||
|
|
@ -354,7 +354,6 @@ class ProjectPolicy < BasePolicy
|
|||
enable :metrics_dashboard
|
||||
enable :read_confidential_issues
|
||||
enable :read_package
|
||||
enable :read_product_analytics
|
||||
enable :read_ci_cd_analytics
|
||||
enable :read_external_emails
|
||||
enable :read_grafana
|
||||
|
|
|
|||
|
|
@ -27,6 +27,11 @@
|
|||
#
|
||||
module BulkImports
|
||||
class CreateService
|
||||
ENTITY_TYPES_MAPPING = {
|
||||
'group_entity' => 'groups',
|
||||
'project_entity' => 'projects'
|
||||
}.freeze
|
||||
|
||||
attr_reader :current_user, :params, :credentials
|
||||
|
||||
def initialize(current_user, params, credentials)
|
||||
|
|
@ -57,6 +62,7 @@ module BulkImports
|
|||
|
||||
def validate!
|
||||
client.validate_instance_version!
|
||||
validate_setting_enabled!
|
||||
client.validate_import_scopes!
|
||||
end
|
||||
|
||||
|
|
@ -88,6 +94,28 @@ module BulkImports
|
|||
end
|
||||
end
|
||||
|
||||
def validate_setting_enabled!
|
||||
source_full_path, source_type = params[0].values_at(:source_full_path, :source_type)
|
||||
entity_type = ENTITY_TYPES_MAPPING.fetch(source_type)
|
||||
if source_full_path =~ /^[0-9]+$/
|
||||
query = query_type(entity_type)
|
||||
response = graphql_client.execute(
|
||||
graphql_client.parse(query.to_s),
|
||||
{ full_path: source_full_path }
|
||||
).original_hash
|
||||
|
||||
source_entity_identifier = ::GlobalID.parse(response.dig(*query.data_path, 'id')).model_id
|
||||
else
|
||||
source_entity_identifier = ERB::Util.url_encode(source_full_path)
|
||||
end
|
||||
|
||||
client.get("/#{entity_type}/#{source_entity_identifier}/export_relations/status")
|
||||
# the source instance will return a 404 if the feature is disabled as the endpoint won't be available
|
||||
rescue Gitlab::HTTP::BlockedUrlError
|
||||
rescue BulkImports::NetworkError
|
||||
raise ::BulkImports::Error.setting_not_enabled
|
||||
end
|
||||
|
||||
def track_access_level(entity_params)
|
||||
Gitlab::Tracking.event(
|
||||
self.class.name,
|
||||
|
|
@ -140,5 +168,20 @@ module BulkImports
|
|||
token: @credentials[:access_token]
|
||||
)
|
||||
end
|
||||
|
||||
def graphql_client
|
||||
@graphql_client ||= BulkImports::Clients::Graphql.new(
|
||||
url: @credentials[:url],
|
||||
token: @credentials[:access_token]
|
||||
)
|
||||
end
|
||||
|
||||
def query_type(entity_type)
|
||||
if entity_type == 'groups'
|
||||
BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil)
|
||||
else
|
||||
BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@
|
|||
%span.gl-button-text
|
||||
= label_for_provider(provider)
|
||||
- unless hide_remember_me
|
||||
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me', value: nil) do |c|
|
||||
= render Pajamas::CheckboxTagComponent.new(name: 'remember_me_omniauth', value: nil) do |c|
|
||||
= c.label do
|
||||
= _('Remember me')
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
.explore-title.text-center
|
||||
%h2
|
||||
= _("Explore GitLab")
|
||||
%p.lead
|
||||
= _("Discover projects, groups and snippets. Share your projects with others")
|
||||
%br
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
- breadcrumb_title _("Projects")
|
||||
- page_title _("Explore projects")
|
||||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
|
||||
.page-title-holder.gl-display-flex.gl-align-items-center
|
||||
%h1.page-title.gl-font-size-h-display= page_title
|
||||
.page-title-controls
|
||||
- if current_user&.can_create_project?
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
|
||||
= _("New project")
|
||||
|
|
@ -1,15 +1,5 @@
|
|||
- breadcrumb_title _("Projects")
|
||||
- page_title _("Explore projects")
|
||||
- page_canonical_link explore_projects_url
|
||||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
|
||||
.page-title-holder.gl-display-flex.gl-align-items-center
|
||||
%h1.page-title.gl-font-size-h-display= page_title
|
||||
.page-title-controls
|
||||
- if current_user&.can_create_project?
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
|
||||
= _("New project")
|
||||
|
||||
= render 'explore/projects/head'
|
||||
= render 'explore/projects/nav'
|
||||
= render 'projects', projects: @projects
|
||||
|
|
|
|||
|
|
@ -1,14 +1,4 @@
|
|||
- @hide_top_links = true
|
||||
- page_title _("Projects")
|
||||
- header_title _("Projects"), dashboard_projects_path
|
||||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
|
||||
- if current_user
|
||||
= render 'dashboard/projects_head', project_tab_filter: :explore
|
||||
- else
|
||||
= render 'explore/head'
|
||||
|
||||
= render 'explore/projects/head'
|
||||
= render 'explore/projects/nav'
|
||||
|
||||
.nothing-here-block
|
||||
|
|
|
|||
|
|
@ -1,14 +1,3 @@
|
|||
- @hide_top_links = true
|
||||
- page_title _("Explore projects")
|
||||
- header_title _("Projects"), dashboard_projects_path
|
||||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
|
||||
.page-title-holder.gl-display-flex.gl-align-items-center
|
||||
%h1.page-title.gl-font-size-h-display= page_title
|
||||
.page-title-controls
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
|
||||
= _("New project")
|
||||
|
||||
= render 'explore/projects/head'
|
||||
= render 'explore/projects/nav'
|
||||
= render 'projects', projects: @projects
|
||||
|
|
|
|||
|
|
@ -1,15 +1,3 @@
|
|||
- @hide_top_links = true
|
||||
- page_title _("Explore projects")
|
||||
- header_title _("Projects"), dashboard_projects_path
|
||||
|
||||
= render_dashboard_ultimate_trial(current_user)
|
||||
|
||||
.page-title-holder.gl-display-flex.gl-align-items-center
|
||||
%h1.page-title.gl-font-size-h-display= page_title
|
||||
.page-title-controls
|
||||
- if current_user&.can_create_project?
|
||||
= render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm) do
|
||||
= _("New project")
|
||||
|
||||
= render 'explore/projects/head'
|
||||
= render 'explore/projects/nav'
|
||||
= render 'projects', projects: @projects
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
name: ci_limit_environment_scope
|
||||
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/113171
|
||||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/395003
|
||||
milestone: '15.10'
|
||||
type: development
|
||||
group: group::pipeline security
|
||||
default_enabled: false
|
||||
|
|
@ -4,5 +4,5 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/111935
|
|||
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/390330
|
||||
milestone: '15.9'
|
||||
type: development
|
||||
group: group::certify
|
||||
group: group::product planning
|
||||
default_enabled: false
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
- name: Automatically resolve SAST findings when rules are disabled
|
||||
description: |
|
||||
On GitLab.com, GitLab SAST now automatically [resolves](https://docs.gitlab.com/ee/user/application_security/vulnerabilities/#vulnerability-status-values) vulnerabilities from the [Semgrep](https://gitlab.com/gitlab-org/security-products/analyzers/semgrep)- and [KICS](https://gitlab.com/gitlab-org/security-products/analyzers/kics)-based analyzers when either you [disable a predefined rule](https://docs.gitlab.com/ee/user/application_security/sast/customize_rulesets.html#disable-predefined-rules), or we remove a rule from the default ruleset. The Vulnerability Management system then leaves a comment explaining that the rule was removed, so you still have a historical record of the vulnerability. This feature is enabled by default on GitLab.com and in GitLab 15.10.
|
||||
stage: Secure
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
available_in: [Free, Premium, Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/application_security/sast/#automatic-vulnerability-resolution'
|
||||
image_url: 'https://about.gitlab.com/images/15_10/automatic-resolution.png'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
- name: See all branch-related settings together
|
||||
description: |
|
||||
All branch-related protections now display on a single page. To see a unified list of your branches and all their protection methods, go to **Settings > Repository > Branch rules**. Each branch shows the merge request approvals, security approvals, protected branches, and status checks configured for it. It is now easier to see a holistic view of a specific branch's protections. We hope this change helps you discover, use, and monitor these settings more easily. We'd love your feedback [in issue #388149](https://gitlab.com/gitlab-org/gitlab/-/issues/388149).
|
||||
stage: Create
|
||||
self-managed: false
|
||||
gitlab-com: true
|
||||
available_in: [Free, Premium, Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/project/repository/branches/'
|
||||
image_url: 'https://img.youtube.com/vi/AUrwtjIr124/hqdefault.jpg'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
- name: Create and switch branches in the Web IDE Beta
|
||||
description: |
|
||||
When you open the Web IDE Beta from a repository or merge request, the currently selected branch is used by default. You can create a new branch with your changes or, if you're not on a protected branch, commit to the current branch. Starting with GitLab 15.10, you can now also create a new branch any time while making changes or switch branches in the Web IDE Beta. This way, you can boost your productivity by not having to close the Web IDE Beta to switch contexts.
|
||||
stage: Create
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
available_in: [Free, Premium, Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/project/web_ide_beta/#switch-branches'
|
||||
image_url: 'https://about.gitlab.com/images/15_10/create-web-ide-manage-branches.png'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
- name: Compliance frameworks report
|
||||
description: |
|
||||
Previous versions of GitLab provided a compliance report that shows compliance violations. In GitLab 15.10, we've added a compliance framework report so you can quickly see which compliance frameworks have been applied to the projects in your group.
|
||||
stage: Govern
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
available_in: [Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/compliance/compliance_report/#compliance-frameworks-report'
|
||||
image_url: 'https://about.gitlab.com/images/15_10/govern-compliance-framework-report.png'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
- name: Suggested Reviewers generally available
|
||||
description: |
|
||||
Since release in closed beta, Suggested Reviewers has been enabled in over 1,000 projects and suggested over 200,000 reviewers. We've also made the service more reliable and are now making it generally available to all Ultimate customers. Now, GitLab can now recommend a reviewer with [Suggested Reviewers](https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#suggested-reviewers). With this feature, machine learning (ML)-powered suggestions appear in the [reviewer dropdown](https://docs.gitlab.com/ee/user/project/merge_requests/getting_started.html#reviewer) in the merge request sidebar. Suggested Reviewers is our [first of many fully available ML features](https://about.gitlab.com/blog/2023/03/16/what-the-ml-ai/) at GitLab.
|
||||
stage: Modelops
|
||||
self-managed: false
|
||||
gitlab-com: true
|
||||
available_in: [Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/project/merge_requests/reviews/#suggested-reviewers'
|
||||
image_url: 'https://about.gitlab.com/images/15_10/create-code-review-suggested-reviewers.png'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
- name: Create diagrams in wikis by using the diagrams.net editor
|
||||
description: |
|
||||
With GitLab 15.10, you can more easily create and edit diagrams in wikis by using the diagrams.net GUI editor. This feature is available in the Markdown editor and the content editor and was implemented in close collaboration with the GitLab wider community.
|
||||
stage: Plan
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
available_in: [Free, Premium, Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/markdown.html#diagramsnet-editor'
|
||||
image_url: 'https://img.youtube.com/vi/F6kfhpRN3ZE/hqdefault.jpg'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
- name: Apple App Store integration
|
||||
description: |
|
||||
From GitLab 15.10, you can configure and validate your projects with Apple App Store credentials. You can then use those credentials in CI/CD pipelines to automate releases to Test Flight and the App Store. To record your experiences with the App Store integration, see this [feedback issue](https://gitlab.com/gitlab-org/incubation-engineering/mobile-devops/feedback/-/issues/10).
|
||||
stage: Deploy
|
||||
self-managed: true
|
||||
gitlab-com: true
|
||||
available_in: [Free, Premium, Ultimate]
|
||||
documentation_link: 'https://docs.gitlab.com/ee/user/project/integrations/apple_app_store.html'
|
||||
image_url: 'https://img.youtube.com/vi/CwzAWVgJeK8/hqdefault.jpg'
|
||||
published_at: 2023-03-22
|
||||
release: 15.10
|
||||
|
|
@ -8,7 +8,7 @@ type: reference
|
|||
# CI/CD minutes quota **(PREMIUM)**
|
||||
|
||||
NOTE:
|
||||
`CI/CD minutes` is being renamed to `compute credits`. During this transition, you might see references in the UI and documentation to `CI minutes`, `CI/CD minutes`, `pipeline minutes`, `CI pipeline minutes`, and `compute credits`. All of these terms refer to compute credits.
|
||||
`CI/CD minutes` is being renamed to `compute credits`. During this transition, you might see references in the UI and documentation to `CI/CD minutes`, `CI minutes`, `pipeline minutes`, `CI pipeline minutes`, `pipeline minutes quota`, and `compute credits`. For more information, see [issue 5218](https://gitlab.com/gitlab-com/Product/-/issues/5218).
|
||||
|
||||
Administrators can limit the amount of time that projects can use to run jobs on
|
||||
[shared runners](../runners/runners_scope.md#shared-runners) each month. This limit
|
||||
|
|
|
|||
|
|
@ -286,8 +286,7 @@ CI/CD is always uppercase. No need to spell it out on first use.
|
|||
|
||||
## CI/CD minutes
|
||||
|
||||
Use **CI/CD minutes** instead of **CI minutes**, **pipeline minutes**, **pipeline minutes quota**, or
|
||||
**CI pipeline minutes**. This decision was made in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/342813).
|
||||
Do not use **CI/CD minutes**. This term was renamed to [**compute credits**](#compute-credits).
|
||||
|
||||
## click
|
||||
|
||||
|
|
@ -311,6 +310,19 @@ Use **From the command line** to introduce commands.
|
|||
|
||||
Hyphenate when using as an adjective. For example, **a command-line tool**.
|
||||
|
||||
## compute credits
|
||||
|
||||
Use **compute credits** instead of these (or similar) terms:
|
||||
|
||||
- **CI/CD minutes**
|
||||
- **CI minutes**
|
||||
- **pipeline minutes**
|
||||
- **CI pipeline minutes**
|
||||
- **pipeline minutes quota**
|
||||
|
||||
As of March, 2022, this language is still being standardized in the documentation and UI.
|
||||
For more information, see [issue 5218](https://gitlab.com/gitlab-com/Product/-/issues/5218).
|
||||
|
||||
## confirmation dialog
|
||||
|
||||
Use **confirmation dialog** to describe the dialog box that asks you to confirm your action. For example:
|
||||
|
|
|
|||
|
|
@ -307,7 +307,7 @@ To enable automatic linking for SAML, see the [SAML setup instructions](saml.md#
|
|||
## Create an external providers list
|
||||
|
||||
You can define a list of external OmniAuth providers.
|
||||
Users who create accounts or sign in to GitLab through the listed providers do not get access to [internal projects](../user/public_access.md#internal-projects-and-groups).
|
||||
Users who create accounts or sign in to GitLab through the listed providers do not get access to [internal projects](../user/public_access.md#internal-projects-and-groups) and are marked as [external users](../user/admin_area/external_users.md).
|
||||
|
||||
To define the external providers list, use the full name of the provider,
|
||||
for example, `google_oauth2` for Google. For provider names, see the
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ If an incident template is configured for the project, then the template content
|
|||
Comments are displayed in threads, but can be displayed chronologically
|
||||
[by toggling on the recent updates view](#recent-updates-view).
|
||||
|
||||
When you make changes to an incident, GitLab creates system notes and
|
||||
When you make changes to an incident, GitLab creates [system notes](../../user/project/system_notes.md) and
|
||||
displays them below the summary.
|
||||
|
||||
### Metrics **(PREMIUM)**
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ Additionally, users can be set as external users using:
|
|||
|
||||
- [SAML groups](../../integration/saml.md#external-groups).
|
||||
- [LDAP groups](../../administration/auth/ldap/ldap_synchronization.md#external-groups).
|
||||
- the [External providers list](../../integration/omniauth.md#create-an-external-providers-list).
|
||||
|
||||
## Set a new user to external
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ type: reference
|
|||
To manage labels for the GitLab instance, select **Labels** (**{labels}**) from the Admin Area sidebar. For more details on how to manage labels, see [Labels](../project/labels.md).
|
||||
|
||||
Labels created in the Admin Area are automatically added to new projects.
|
||||
They are not available to new groups.
|
||||
Updating or adding labels in the Admin Area does not modify labels in existing projects.
|
||||
|
||||

|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ group: Composition Analysis
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# License compliance (deprecated) **(ULTIMATE)**
|
||||
# License Compliance (deprecated) **(ULTIMATE)**
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/5483) in GitLab 11.0.
|
||||
> - [Deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9.
|
||||
|
||||
WARNING:
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9. Users should migrate over to use the [new method of license scanning](../license_scanning_of_cyclonedx_files/index.md) prior to GitLab 16.0.
|
||||
This feature was [deprecated](https://gitlab.com/gitlab-org/gitlab/-/issues/387561) in GitLab 15.9. You should instead migrate to use [License approval policies](../license_approval_policies.md) and the [new method of license scanning](../license_scanning_of_cyclonedx_files/index.md) prior to GitLab 16.0.
|
||||
|
||||
If you're using [GitLab CI/CD](../../../ci/index.md), you can use License Compliance to search your
|
||||
project's dependencies for their licenses. You can then decide whether to allow or deny the use of
|
||||
|
|
@ -49,6 +49,38 @@ that you can later download and analyze.
|
|||
WARNING:
|
||||
License Compliance Scanning does not support run-time installation of compilers and interpreters.
|
||||
|
||||
## Enable License Compliance
|
||||
|
||||
To enable License Compliance in your project's pipeline, either:
|
||||
|
||||
- Enable [Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance)
|
||||
(provided by [Auto DevOps](../../../topics/autodevops/index.md)).
|
||||
- Include the [`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml) in your `.gitlab-ci.yml` file.
|
||||
|
||||
License Compliance is not supported when GitLab is run with FIPS mode enabled.
|
||||
|
||||
### Include the License Scanning template
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- [GitLab Runner](../../../ci/runners/index.md) available, with the
|
||||
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). If you're using the
|
||||
shared runners on GitLab.com, this is enabled by default.
|
||||
- License Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the
|
||||
`.gitlab-ci.yml` file, the `test` stage is required.
|
||||
- [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) must be disabled.
|
||||
|
||||
To [include](../../../ci/yaml/index.md#includetemplate) the
|
||||
[`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml), add it to your `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: Security/License-Scanning.gitlab-ci.yml
|
||||
```
|
||||
|
||||
The included template creates a `license_scanning` job in your CI/CD pipeline and scans your
|
||||
dependencies to find their licenses.
|
||||
|
||||
## License expressions
|
||||
|
||||
GitLab has limited support for [composite licenses](https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/).
|
||||
|
|
@ -89,39 +121,7 @@ The reported licenses might be incomplete or inaccurate.
|
|||
| Rust | [Cargo](https://crates.io/) |
|
||||
| PHP | [Composer](https://getcomposer.org/) |
|
||||
|
||||
## Enable License Compliance
|
||||
|
||||
To enable License Compliance in your project's pipeline, either:
|
||||
|
||||
- Enable [Auto License Compliance](../../../topics/autodevops/stages.md#auto-license-compliance)
|
||||
(provided by [Auto DevOps](../../../topics/autodevops/index.md)).
|
||||
- Include the [`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml) in your `.gitlab-ci.yml` file.
|
||||
|
||||
License Compliance is not supported when GitLab is run with FIPS mode enabled.
|
||||
|
||||
### Include the License Scanning template
|
||||
|
||||
Prerequisites:
|
||||
|
||||
- [GitLab Runner](../../../ci/runners/index.md) available, with the
|
||||
[`docker` executor](https://docs.gitlab.com/runner/executors/docker.html). If you're using the
|
||||
shared runners on GitLab.com, this is enabled by default.
|
||||
- License Scanning runs in the `test` stage, which is available by default. If you redefine the stages in the
|
||||
`.gitlab-ci.yml` file, the `test` stage is required.
|
||||
- [FIPS mode](../../../development/fips_compliance.md#enable-fips-mode) must be disabled.
|
||||
|
||||
To [include](../../../ci/yaml/index.md#includetemplate) the
|
||||
[`License-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/License-Scanning.gitlab-ci.yml), add it to your `.gitlab-ci.yml` file:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- template: Security/License-Scanning.gitlab-ci.yml
|
||||
```
|
||||
|
||||
The included template creates a `license_scanning` job in your CI/CD pipeline and scans your
|
||||
dependencies to find their licenses.
|
||||
|
||||
### Available CI/CD variables
|
||||
## Available CI/CD variables
|
||||
|
||||
License Compliance can be configured using CI/CD variables.
|
||||
|
||||
|
|
@ -141,7 +141,7 @@ License Compliance can be configured using CI/CD variables.
|
|||
| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. |
|
||||
| `SETUP_CMD` | no | Custom setup for the dependency installation (experimental). |
|
||||
|
||||
### Installing custom dependencies
|
||||
## Installing custom dependencies
|
||||
|
||||
> Introduced in GitLab 11.4.
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ variables:
|
|||
In this example, `my-custom-install-script.sh` is a shell script at the root
|
||||
directory of your project.
|
||||
|
||||
### Working with Monorepos
|
||||
## Working with Monorepos
|
||||
|
||||
Depending on your language, you may need to specify the path to the individual
|
||||
projects of a monorepo using the `LICENSE_FINDER_CLI_OPTS` variable. Passing in
|
||||
|
|
@ -184,7 +184,7 @@ variables:
|
|||
LICENSE_FINDER_CLI_OPTS: "--aggregate_paths=relative-path/to/sub-project/one relative-path/to/sub-project/two"
|
||||
```
|
||||
|
||||
### Overriding the template
|
||||
## Overriding the template
|
||||
|
||||
WARNING:
|
||||
Beginning in GitLab 13.0, the use of [`only` and `except`](../../../ci/yaml/index.md#only--except)
|
||||
|
|
@ -203,7 +203,7 @@ license_scanning:
|
|||
CI_DEBUG_TRACE: "true"
|
||||
```
|
||||
|
||||
### Configuring Maven projects
|
||||
## Configuring Maven projects
|
||||
|
||||
The License Compliance tool provides a `MAVEN_CLI_OPTS` CI/CD variable which can hold
|
||||
the command line arguments to pass to the `mvn install` command which is executed under the hood.
|
||||
|
|
@ -227,7 +227,7 @@ to explicitly add `-DskipTests` to your options.
|
|||
If you still need to run tests during `mvn install`, add `-DskipTests=false` to
|
||||
`MAVEN_CLI_OPTS`.
|
||||
|
||||
#### Using private Maven repositories
|
||||
### Using private Maven repositories
|
||||
|
||||
If you have a private Maven repository which requires login credentials,
|
||||
you can use the `MAVEN_CLI_OPTS` CI/CD variable.
|
||||
|
|
@ -250,13 +250,13 @@ Alternatively, you can use a Java key store to verify the TLS connection. For in
|
|||
generate a key store file, see the
|
||||
[Maven Guide to Remote repository access through authenticated HTTPS](https://maven.apache.org/guides/mini/guide-repository-ssl.html).
|
||||
|
||||
### Selecting the version of Java
|
||||
## Selecting the version of Java
|
||||
|
||||
License Compliance uses Java 8 by default. You can specify a different Java version using `LM_JAVA_VERSION`.
|
||||
|
||||
`LM_JAVA_VERSION` only accepts versions: 8, 11, 14, 15.
|
||||
|
||||
### Selecting the version of Python
|
||||
## Selecting the version of Python
|
||||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/security-products/license-management/-/merge_requests/36) in GitLab 12.0.
|
||||
> - In [GitLab 12.2](https://gitlab.com/gitlab-org/gitlab/-/issues/12032), Python 3.5 became the default.
|
||||
|
|
@ -277,22 +277,22 @@ license_scanning:
|
|||
|
||||
`LM_PYTHON_VERSION` or `ASDF_PYTHON_VERSION` can be used to specify the desired version of Python. When both variables are specified `LM_PYTHON_VERSION` takes precedence.
|
||||
|
||||
### Custom root certificates for Python
|
||||
## Custom root certificates for Python
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
|
||||
|
||||
#### Using private Python repositories
|
||||
### Using private Python repositories
|
||||
|
||||
If you have a private Python repository you can use the `PIP_INDEX_URL` [CI/CD variable](#available-cicd-variables)
|
||||
to specify its location.
|
||||
|
||||
### Configuring npm projects
|
||||
## Configuring npm projects
|
||||
|
||||
You can configure npm projects by using an [`.npmrc`](https://docs.npmjs.com/configuring-npm/npmrc.html/)
|
||||
file.
|
||||
|
||||
#### Using private npm registries
|
||||
### Using private npm registries
|
||||
|
||||
If you have a private npm registry you can use the
|
||||
[`registry`](https://docs.npmjs.com/using-npm/config/#registry)
|
||||
|
|
@ -304,7 +304,7 @@ For example:
|
|||
registry = https://npm.example.com
|
||||
```
|
||||
|
||||
#### Custom root certificates for npm
|
||||
### Custom root certificates for npm
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
|
||||
|
|
@ -318,12 +318,12 @@ For example:
|
|||
strict-ssl = false
|
||||
```
|
||||
|
||||
### Configuring Yarn projects
|
||||
## Configuring Yarn projects
|
||||
|
||||
You can configure Yarn projects by using a [`.yarnrc.yml`](https://yarnpkg.com/configuration/yarnrc/)
|
||||
file.
|
||||
|
||||
#### Using private Yarn registries
|
||||
### Using private Yarn registries
|
||||
|
||||
If you have a private Yarn registry you can use the
|
||||
[`npmRegistryServer`](https://yarnpkg.com/configuration/yarnrc/#npmRegistryServer)
|
||||
|
|
@ -335,17 +335,17 @@ For example:
|
|||
npmRegistryServer: "https://npm.example.com"
|
||||
```
|
||||
|
||||
#### Custom root certificates for Yarn
|
||||
### Custom root certificates for Yarn
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
|
||||
|
||||
### Configuring Bower projects
|
||||
## Configuring Bower projects
|
||||
|
||||
You can configure Bower projects by using a [`.bowerrc`](https://bower.io/docs/config/#bowerrc-specification)
|
||||
file.
|
||||
|
||||
#### Using private Bower registries
|
||||
### Using private Bower registries
|
||||
|
||||
If you have a private Bower registry you can use the
|
||||
[`registry`](https://bower.io/docs/config/#bowerrc-specification)
|
||||
|
|
@ -359,16 +359,16 @@ For example:
|
|||
}
|
||||
```
|
||||
|
||||
#### Custom root certificates for Bower
|
||||
### Custom root certificates for Bower
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables), or by
|
||||
specifying a `ca` setting in a [`.bowerrc`](https://bower.io/docs/config/#bowerrc-specification)
|
||||
file.
|
||||
|
||||
### Configuring Bundler projects
|
||||
## Configuring Bundler projects
|
||||
|
||||
#### Using private Bundler registries
|
||||
### Using private Bundler registries
|
||||
|
||||
If you have a private Bundler registry you can use the
|
||||
[`source`](https://bundler.io/man/gemfile.5.html#GLOBAL-SOURCES)
|
||||
|
|
@ -380,7 +380,7 @@ For example:
|
|||
source "https://gems.example.com"
|
||||
```
|
||||
|
||||
#### Custom root certificates for Bundler
|
||||
### Custom root certificates for Bundler
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables), or by
|
||||
|
|
@ -388,9 +388,9 @@ specifying a [`BUNDLE_SSL_CA_CERT`](https://bundler.io/v2.0/man/bundle-config.1.
|
|||
[variable](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
|
||||
in the job definition.
|
||||
|
||||
### Configuring Cargo projects
|
||||
## Configuring Cargo projects
|
||||
|
||||
#### Using private Cargo registries
|
||||
### Using private Cargo registries
|
||||
|
||||
If you have a private Cargo registry you can use the
|
||||
[`registries`](https://doc.rust-lang.org/cargo/reference/registries.html)
|
||||
|
|
@ -403,7 +403,7 @@ For example:
|
|||
my-registry = { index = "https://my-intranet:8080/git/index" }
|
||||
```
|
||||
|
||||
#### Custom root certificates for Cargo
|
||||
### Custom root certificates for Cargo
|
||||
|
||||
To supply a custom root certificate to complete TLS verification, do one of the following:
|
||||
|
||||
|
|
@ -412,9 +412,9 @@ To supply a custom root certificate to complete TLS verification, do one of the
|
|||
[variable](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
|
||||
in the job definition.
|
||||
|
||||
### Configuring Composer projects
|
||||
## Configuring Composer projects
|
||||
|
||||
#### Using private Composer registries
|
||||
### Using private Composer registries
|
||||
|
||||
If you have a private Composer registry you can use the
|
||||
[`repositories`](https://getcomposer.org/doc/05-repositories.md)
|
||||
|
|
@ -437,7 +437,7 @@ For example:
|
|||
}
|
||||
```
|
||||
|
||||
#### Custom root certificates for Composer
|
||||
### Custom root certificates for Composer
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables), or by
|
||||
|
|
@ -445,7 +445,7 @@ specifying a [`COMPOSER_CAFILE`](https://getcomposer.org/doc/03-cli.md#composer-
|
|||
[variable](../../../ci/variables/index.md#define-a-cicd-variable-in-the-gitlab-ciyml-file)
|
||||
in the job definition.
|
||||
|
||||
### Configuring Conan projects
|
||||
## Configuring Conan projects
|
||||
|
||||
You can configure [Conan](https://conan.io/) projects by adding a `.conan` directory to your
|
||||
project root. The project root serves as the [`CONAN_USER_HOME`](https://docs.conan.io/en/latest/reference/env_vars.html#conan-user-home).
|
||||
|
|
@ -474,7 +474,7 @@ NOTE:
|
|||
`license_scanning` image ships with [Mono](https://www.mono-project.com/) and [MSBuild](https://github.com/mono/msbuild#microsoftbuild-msbuild).
|
||||
Additional setup may be required to build packages for this project configuration.
|
||||
|
||||
#### Using private Conan registries
|
||||
### Using private Conan registries
|
||||
|
||||
By default, [Conan](https://conan.io/) uses the `conan-center` remote. For example:
|
||||
|
||||
|
|
@ -508,7 +508,7 @@ example:
|
|||
If credentials are required to authenticate then you can configure a [protected CI/CD variable](../../../ci/variables/index.md#protect-a-cicd-variable)
|
||||
following the naming convention described in the [`CONAN_LOGIN_USERNAME` documentation](https://docs.conan.io/en/latest/reference/env_vars.html#conan-login-username-conan-login-username-remote-name).
|
||||
|
||||
#### Custom root certificates for Conan
|
||||
### Custom root certificates for Conan
|
||||
|
||||
You can provide custom certificates by adding a `.conan/cacert.pem` file to the project root and
|
||||
setting [`CA_CERT_PATH`](https://docs.conan.io/en/latest/reference/env_vars.html#conan-cacert-path)
|
||||
|
|
@ -518,7 +518,7 @@ If you specify the `ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-
|
|||
variable's X.509 certificates are installed in the Docker image's default trust store and Conan is
|
||||
configured to use this as the default `CA_CERT_PATH`.
|
||||
|
||||
### Configuring Go projects
|
||||
## Configuring Go projects
|
||||
|
||||
To configure [Go modules](https://github.com/golang/go/wiki/Modules)
|
||||
based projects, specify [CI/CD variables](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
|
||||
|
|
@ -528,14 +528,14 @@ If a project has [vendored](https://pkg.go.dev/cmd/go#hdr-Vendor_Directories) it
|
|||
then the combination of the `vendor` directory and `mod.sum` file are used to detect the software
|
||||
licenses associated with the Go module dependencies.
|
||||
|
||||
#### Using private Go registries
|
||||
### Using private Go registries
|
||||
|
||||
You can use the [`GOPRIVATE`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
|
||||
and [`GOPROXY`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
|
||||
environment variables to control where modules are sourced from. Alternatively, you can use
|
||||
[`go mod vendor`](https://go.dev/ref/mod#tmp_28) to vendor a project's modules.
|
||||
|
||||
#### Custom root certificates for Go
|
||||
### Custom root certificates for Go
|
||||
|
||||
You can specify the [`-insecure`](https://pkg.go.dev/cmd/go/internal/get) flag by exporting the
|
||||
[`GOFLAGS`](https://pkg.go.dev/cmd/go#hdr-Environment_variables)
|
||||
|
|
@ -550,7 +550,7 @@ license_scanning:
|
|||
GOFLAGS: '-insecure'
|
||||
```
|
||||
|
||||
#### Using private NuGet registries
|
||||
### Using private NuGet registries
|
||||
|
||||
If you have a private NuGet registry you can add it as a source
|
||||
by adding it to the [`packageSources`](https://learn.microsoft.com/en-us/nuget/reference/nuget-config-file#package-source-sections)
|
||||
|
|
@ -568,7 +568,7 @@ For example:
|
|||
</configuration>
|
||||
```
|
||||
|
||||
#### Custom root certificates for NuGet
|
||||
### Custom root certificates for NuGet
|
||||
|
||||
You can supply a custom root certificate to complete TLS verification by using the
|
||||
`ADDITIONAL_CA_CERT_BUNDLE` [CI/CD variable](#available-cicd-variables).
|
||||
|
|
|
|||
|
|
@ -18,13 +18,14 @@ Other 3rd party scanners may also be used as long as they produce a CycloneDX fi
|
|||
This method of scanning is also capable of parsing and identifying over 500 different types of licenses, as defined in [the SPDX list](https://spdx.org/licenses/).
|
||||
Licenses not in the SPDX list are reported as "Unknown". License information can also be extracted from packages that are dual-licensed, or have multiple different licenses that apply.
|
||||
|
||||
To enable license detection using Dependency Scanning in a project,
|
||||
include the `Jobs/Dependency-Scanning.gitlab-ci.yml` template in its CI configuration,
|
||||
but do not include the `Jobs/License-Scanning.gitlab-ci.yml` template.
|
||||
## Enable license scanning
|
||||
|
||||
## Requirements
|
||||
Prerequisites:
|
||||
|
||||
The license scanning requirements are the same as those for [Dependency Scanning](../../application_security/dependency_scanning/index.md#requirements).
|
||||
- Enable [Dependency Scanning](../../application_security/dependency_scanning/index.md#configuration).
|
||||
|
||||
From the `.gitlab-ci.yml` file, remove the deprecated line `Jobs/License-Scanning.gitlab-ci.yml`, if
|
||||
it's present.
|
||||
|
||||
## Supported languages and package managers
|
||||
|
||||
|
|
@ -103,11 +104,6 @@ License scanning is supported for the following languages and package managers:
|
|||
The supported files and versions are the ones supported by
|
||||
[Dependency Scanning](../../application_security/dependency_scanning/index.md#supported-languages-and-package-managers).
|
||||
|
||||
## Configuration
|
||||
|
||||
To enable license scanning of CycloneDX files,
|
||||
you must configure [Dependency Scanning](../../application_security/dependency_scanning/index.md#configuration).
|
||||
|
||||
## License expressions
|
||||
|
||||
GitLab has limited support for [composite licenses](https://spdx.github.io/spdx-spec/v2-draft/SPDX-license-expressions/).
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ For information on automatically managing GitLab group membership, see [SAML Gro
|
|||
|
||||
The [Generated passwords for users created through integrated authentication](../../../security/passwords_for_integrated_authentication_methods.md) guide provides an overview of how GitLab generates and sets passwords for users created via SAML SSO for Groups.
|
||||
|
||||
### NameID
|
||||
## NameID
|
||||
|
||||
GitLab.com uses the SAML NameID to identify users. The NameID element:
|
||||
|
||||
|
|
@ -516,7 +516,7 @@ The relevant field name and recommended value for supported providers are in the
|
|||
WARNING:
|
||||
Once users have signed into GitLab using the SSO SAML setup, changing the `NameID` breaks the configuration and potentially locks users out of the GitLab group.
|
||||
|
||||
#### NameID Format
|
||||
### NameID Format
|
||||
|
||||
We recommend setting the NameID format to `Persistent` unless using a field (such as email) that requires a different format.
|
||||
Most NameID formats can be used, except `Transient` due to the temporary nature of this format.
|
||||
|
|
|
|||
|
|
@ -119,7 +119,6 @@ To edit an OKR:
|
|||
|
||||
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.7 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
|
||||
> - [Moved](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) to feature flag named `work_items_mvc` in GitLab 15.8. Disabled by default.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.
|
||||
> - Changing activity sort order [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378949) in GitLab 15.8.
|
||||
> - Filtering activity [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/389971) in GitLab 15.10.
|
||||
> - [Enabled on GitLab.com and self-managed](https://gitlab.com/gitlab-org/gitlab/-/issues/334812) in GitLab 15.10.
|
||||
|
|
|
|||
|
|
@ -112,6 +112,43 @@ to work with all third-party registries in the same predictable way. If you use
|
|||
Registry, this workaround is not required because we implemented a special tag delete operation. In
|
||||
this case, you can expect cleanup policies to be consistent and predictable.
|
||||
|
||||
#### Example cleanup policy workflow
|
||||
|
||||
The interaction between the keep and remove rules for the cleanup policy can be complex.
|
||||
For example, with a project with this cleanup policy configuration:
|
||||
|
||||
- **Keep the most recent**: 1 tag per image name.
|
||||
- **Keep tags matching**: `production-.*`
|
||||
- **Remove tags older than**: 7 days.
|
||||
- **Remove tags matching**: `.*`.
|
||||
|
||||
And a container repository with these tags:
|
||||
|
||||
- `latest`, published 2 hours ago.
|
||||
- `production-v44`, published 3 days ago.
|
||||
- `production-v43`, published 6 days ago.
|
||||
- `production-v42`, published 11 days ago.
|
||||
- `dev-v44`, published 2 days ago.
|
||||
- `dev-v43`, published 5 day ago.
|
||||
- `dev-v42`, published 10 days ago.
|
||||
- `v44`, published yesterday.
|
||||
- `v43`, published 12 days ago.
|
||||
- `v42`, published 20 days ago.
|
||||
|
||||
In this example, the tags that would be deleted in the next cleanup run are `dev-v42`, `v43`, and `v42`.
|
||||
You can interpret the rules as applying with this precedence:
|
||||
|
||||
1. The keep rules have highest precedence. Tags must be kept when they match **any** rule.
|
||||
- The `latest` tag must be kept, because `latest` tags are always kept.
|
||||
- The `production-v44`, `production-v43`, and `production-v42` tags must be kept,
|
||||
because they match the **Keep tags matching** rule.
|
||||
- The `v44` tag must be kept because it's the most recent, matching the **Keep the most recent** rule.
|
||||
1. The remove rules have lower precedence, and tags are only deleted if **all** rules match.
|
||||
For the tags not matching any keep rules (`dev-44`, `dev-v43`, `dev-v42`, `v43`, and `v42`):
|
||||
- `dev-44` and `dev-43` do **not** match the **Remove tags older than**, and are kept.
|
||||
- `dev-v42`, `v43`, and `v42` match both **Remove tags older than** and **Remove tags matching**
|
||||
rules, so these three tags can be deleted.
|
||||
|
||||
### Create a cleanup policy
|
||||
|
||||
You can create a cleanup policy in [the API](#use-the-cleanup-policy-api) or the UI.
|
||||
|
|
|
|||
|
|
@ -601,7 +601,7 @@ eventually pick it up. When they're done, they move it to **Done**, to close the
|
|||
issue.
|
||||
|
||||
This process can be seen clearly when visiting an issue. With every move
|
||||
to another list, the label changes and a system note is recorded.
|
||||
to another list, the label changes and a [system note](system_notes.md) is recorded.
|
||||
|
||||

|
||||
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ When bulk editing issues in a group, you can edit the following attributes:
|
|||
## Move an issue
|
||||
|
||||
When you move an issue, it's closed and copied to the target project.
|
||||
The original issue is not deleted. A system note, which indicates
|
||||
The original issue is not deleted. A [system note](../system_notes.md), which indicates
|
||||
where it came from and went to, is added to both issues.
|
||||
|
||||
Be careful when moving an issue to a project with different access rules. Before moving the issue, make sure it does not contain sensitive data.
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ project, from the GitLab user interface:
|
|||
|
||||
## View system notes for cherry-picked commits
|
||||
|
||||
When you cherry-pick a merge commit in the GitLab UI or API, GitLab adds a system note
|
||||
When you cherry-pick a merge commit in the GitLab UI or API, GitLab adds a [system note](../system_notes.md)
|
||||
to the related merge request thread in the format **{cherry-pick-commit}**
|
||||
`[USER]` **picked the changes into the branch** `[BRANCHNAME]` with commit** `[SHA]` `[DATE]`:
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
---
|
||||
stage: Create
|
||||
group: Source Code
|
||||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# System notes **(FREE)**
|
||||
|
||||
System notes are short descriptions that help you understand the history of events
|
||||
that occur during the life cycle of a GitLab object, such as:
|
||||
|
||||
- [Alerts](../../operations/incident_management/alerts.md).
|
||||
- [Designs](issues/design_management.md).
|
||||
- [Issues](issues/index.md).
|
||||
- [Merge requests](merge_requests/index.md).
|
||||
- [Objectives and key results](../okrs.md) (OKRs).
|
||||
- [Tasks](../tasks.md).
|
||||
|
||||
GitLab logs information about events triggered by Git or the GitLab application
|
||||
in system notes.
|
||||
|
||||
## Show or filter system notes
|
||||
|
||||
By default, system notes do not display. When displayed, they are shown oldest first.
|
||||
If you change the filter or sort options, your selection is remembered across sections.
|
||||
The filtering options are:
|
||||
|
||||
- **Show all activity** displays both comments and history.
|
||||
- **Show comments only** hides system notes.
|
||||
- **Show history only** hides user comments.
|
||||
|
||||
### On an epic
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Epics** (**{epic}**).
|
||||
1. Identify your desired epic, and select its title.
|
||||
1. Go to the **Activity** section.
|
||||
1. For **Sort or filter**, select **Show all activity**.
|
||||
|
||||
### On an issue
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Issues** and find your issue.
|
||||
1. Go to **Activity**.
|
||||
1. For **Sort or filter**, select **Show all activity**.
|
||||
|
||||
### On a merge request
|
||||
|
||||
1. On the top bar, select **Main menu > Projects** and find your project.
|
||||
1. On the left sidebar, select **Merge requests** and find your merge request.
|
||||
1. Go to **Activity**.
|
||||
1. For **Sort or filter**, select **Show all activity**.
|
||||
|
||||
## Related topics
|
||||
|
||||
- The [Notes API](../../api/notes.md) can add system notes to objects in GitLab.
|
||||
|
|
@ -15,6 +15,10 @@ module Backup
|
|||
repositories_paths: 'REPOSITORIES_PATHS'
|
||||
}.freeze
|
||||
|
||||
YAML_PERMITTED_CLASSES = [
|
||||
ActiveSupport::TimeWithZone, ActiveSupport::TimeZone, Symbol, Time
|
||||
].freeze
|
||||
|
||||
TaskDefinition = Struct.new(
|
||||
:enabled, # `true` if the task can be used. Treated as `true` when not specified.
|
||||
:human_name, # Name of the task used for logging.
|
||||
|
|
@ -247,7 +251,9 @@ module Backup
|
|||
end
|
||||
|
||||
def read_backup_information
|
||||
@backup_information ||= YAML.load_file(File.join(backup_path, MANIFEST_NAME))
|
||||
@backup_information ||= YAML.safe_load_file(
|
||||
File.join(backup_path, MANIFEST_NAME),
|
||||
permitted_classes: YAML_PERMITTED_CLASSES)
|
||||
end
|
||||
|
||||
def write_backup_information
|
||||
|
|
|
|||
|
|
@ -165,6 +165,8 @@ module BulkImports
|
|||
|
||||
raise ::BulkImports::NetworkError.new("Unsuccessful response #{response.code} from #{response.request.path.path}. Body: #{response.parsed_response}", response: response)
|
||||
|
||||
rescue Gitlab::HTTP::BlockedUrlError => e
|
||||
raise e
|
||||
rescue *Gitlab::HTTP::HTTP_ERRORS => e
|
||||
raise ::BulkImports::NetworkError, e
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
module BulkImports
|
||||
class Error < StandardError
|
||||
def self.unsupported_gitlab_version
|
||||
self.new("Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} " \
|
||||
"or later.")
|
||||
self.new("Unsupported GitLab version. Minimum supported version is #{BulkImport::MIN_MAJOR_VERSION}.")
|
||||
end
|
||||
|
||||
def self.scope_validation_failure
|
||||
|
|
@ -19,5 +18,11 @@ module BulkImports
|
|||
def self.destination_full_path_validation_failure(full_path)
|
||||
self.new("Import aborted as '#{full_path}' already exists. Change the destination and try again.")
|
||||
end
|
||||
|
||||
def self.setting_not_enabled
|
||||
self.new("Group import disabled on source or destination instance. " \
|
||||
"Ask an administrator to enable it on both instances and try again."
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ module Sidebars
|
|||
|
||||
override :active_routes
|
||||
def active_routes
|
||||
{ page: [link, explore_root_path] }
|
||||
{ page: [link, explore_root_path, starred_explore_projects_path, trending_explore_projects_path] }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9158,6 +9158,9 @@ msgstr ""
|
|||
msgid "CiVariable|Define a CI/CD variable in the UI"
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|Maximum of 20 environments listed. For more environments, enter a search query."
|
||||
msgstr ""
|
||||
|
||||
msgid "CiVariable|New environment"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -15122,9 +15125,6 @@ msgstr ""
|
|||
msgid "Discover GitLab Geo"
|
||||
msgstr ""
|
||||
|
||||
msgid "Discover projects, groups and snippets. Share your projects with others"
|
||||
msgstr ""
|
||||
|
||||
msgid "Discover|Check your application for security vulnerabilities that may lead to unauthorized access, data leaks, and denial of services."
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -17256,9 +17256,6 @@ msgstr ""
|
|||
msgid "Explore"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore GitLab"
|
||||
msgstr ""
|
||||
|
||||
msgid "Explore groups"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -35339,6 +35336,9 @@ msgstr ""
|
|||
msgid "ProtectedEnvironments|Number of approvals must be between 1 and 5"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironments|Required approval count"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProtectedEnvironments|Save"
|
||||
msgstr ""
|
||||
|
||||
|
|
@ -38284,6 +38284,9 @@ msgstr ""
|
|||
msgid "Saved replies can be used when creating comments inside issues, merge requests, and epics."
|
||||
msgstr ""
|
||||
|
||||
msgid "Saved reply actions"
|
||||
msgstr ""
|
||||
|
||||
msgid "Saving"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
|||
|
|
@ -55,8 +55,8 @@
|
|||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/fonts": "^1.2.0",
|
||||
"@gitlab/svgs": "3.26.0",
|
||||
"@gitlab/ui": "58.2.0",
|
||||
"@gitlab/svgs": "3.28.0",
|
||||
"@gitlab/ui": "58.2.1",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@gitlab/web-ide": "0.0.1-dev-20230223005157",
|
||||
"@mattiasbuelens/web-streams-adapter": "^0.1.0",
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe 'User explores projects', feature_category: :user_profile do
|
||||
describe '"All" tab' do
|
||||
it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :explore_projects_path, :projects
|
||||
end
|
||||
|
||||
describe '"Most starred" tab' do
|
||||
it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :starred_explore_projects_path, :projects
|
||||
end
|
||||
|
||||
describe '"Trending" tab' do
|
||||
it_behaves_like 'an "Explore" page with sidebar and breadcrumbs', :trending_explore_projects_path, :projects
|
||||
end
|
||||
|
||||
context 'when some projects exist' do
|
||||
let_it_be(:archived_project) { create(:project, :archived) }
|
||||
let_it_be(:internal_project) { create(:project, :internal) }
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ RSpec.describe 'Profile > Saved replies > User deletes saved reply', :js,
|
|||
it 'shows the user a list of their saved replies' do
|
||||
visit profile_saved_replies_path
|
||||
|
||||
click_button 'Saved reply actions'
|
||||
find('[data-testid="saved-reply-delete-btn"]').click
|
||||
|
||||
page.within('.gl-modal') do
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ RSpec.describe 'Profile > Saved replies > User updated saved reply', :js,
|
|||
end
|
||||
|
||||
it 'shows the user a list of their saved replies' do
|
||||
click_button 'Saved reply actions'
|
||||
find('[data-testid="saved-reply-edit-btn"]').click
|
||||
find('[data-testid="saved-reply-name-input"]').set('test')
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ require 'spec_helper'
|
|||
RSpec.describe 'Upload Dropzone Field', feature_category: :integrations do
|
||||
include_context 'project integration activation'
|
||||
|
||||
it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures do
|
||||
it 'uploads the file data to the correct form fields and updates the messaging correctly', :js, :aggregate_failures,
|
||||
quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/398636' do
|
||||
visit_project_integration('Apple App Store Connect')
|
||||
|
||||
expect(page).to have_content('Drag your Private Key file here or click to upload.')
|
||||
|
|
|
|||
|
|
@ -8,40 +8,30 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
|
|||
let_it_be(:work_item) { create(:work_item, project: project) }
|
||||
let_it_be(:milestone) { create(:milestone, project: project) }
|
||||
let_it_be(:milestones) { create_list(:milestone, 25, project: project) }
|
||||
let(:work_items_path) { project_work_items_path(project, work_items_path: work_item.iid, iid_path: true) }
|
||||
|
||||
context 'for signed in user' do
|
||||
before do
|
||||
project.add_developer(user)
|
||||
|
||||
sign_in(user)
|
||||
|
||||
visit work_items_path
|
||||
end
|
||||
|
||||
context 'with internal id' do
|
||||
before do
|
||||
visit project_work_items_path(project, work_items_path: work_item.iid, iid_path: true)
|
||||
it 'uses IID path in breadcrumbs' do
|
||||
within('[data-testid="breadcrumb-current-link"]') do
|
||||
expect(page).to have_link('Work Items', href: work_items_path)
|
||||
end
|
||||
|
||||
it_behaves_like 'work items title'
|
||||
it_behaves_like 'work items status'
|
||||
it_behaves_like 'work items assignees'
|
||||
it_behaves_like 'work items labels'
|
||||
it_behaves_like 'work items comments'
|
||||
it_behaves_like 'work items description'
|
||||
it_behaves_like 'work items milestone'
|
||||
end
|
||||
|
||||
context 'with global id' do
|
||||
before do
|
||||
stub_feature_flags(use_iid_in_work_items_path: false)
|
||||
visit project_work_items_path(project, work_items_path: work_item.id)
|
||||
end
|
||||
|
||||
it_behaves_like 'work items status'
|
||||
it_behaves_like 'work items assignees'
|
||||
it_behaves_like 'work items labels'
|
||||
it_behaves_like 'work items comments'
|
||||
it_behaves_like 'work items description'
|
||||
end
|
||||
it_behaves_like 'work items title'
|
||||
it_behaves_like 'work items status'
|
||||
it_behaves_like 'work items assignees'
|
||||
it_behaves_like 'work items labels'
|
||||
it_behaves_like 'work items comments'
|
||||
it_behaves_like 'work items description'
|
||||
it_behaves_like 'work items milestone'
|
||||
end
|
||||
|
||||
context 'for signed in owner' do
|
||||
|
|
@ -50,7 +40,7 @@ RSpec.describe 'Work item', :js, feature_category: :team_planning do
|
|||
|
||||
sign_in(user)
|
||||
|
||||
visit project_work_items_path(project, work_items_path: work_item.id)
|
||||
visit work_items_path
|
||||
end
|
||||
|
||||
it_behaves_like 'work items invite members'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { mountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import { allEnvironments } from '~/ci/ci_variable_list/constants';
|
||||
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
|
||||
|
||||
|
|
@ -7,7 +7,11 @@ describe('Ci environments dropdown', () => {
|
|||
let wrapper;
|
||||
|
||||
const envs = ['dev', 'prod', 'staging'];
|
||||
const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
|
||||
const defaultProps = {
|
||||
areEnvironmentsLoading: false,
|
||||
environments: envs,
|
||||
selectedEnvironmentScope: '',
|
||||
};
|
||||
|
||||
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
|
||||
const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
|
||||
|
|
@ -15,13 +19,19 @@ describe('Ci environments dropdown', () => {
|
|||
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
|
||||
const findListboxText = () => findListbox().props('toggleText');
|
||||
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
|
||||
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
|
||||
|
||||
const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
|
||||
wrapper = mount(CiEnvironmentsDropdown, {
|
||||
const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
|
||||
wrapper = mountExtended(CiEnvironmentsDropdown, {
|
||||
propsData: {
|
||||
...defaultProps,
|
||||
...props,
|
||||
},
|
||||
provide: {
|
||||
glFeatures: {
|
||||
ciLimitEnvironmentScope: enableFeatureFlag,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
findListbox().vm.$emit('search', searchTerm);
|
||||
|
|
@ -40,19 +50,32 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
|
||||
describe('Search term is empty', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { environments: envs } });
|
||||
});
|
||||
describe.each`
|
||||
featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
|
||||
${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
|
||||
${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
|
||||
`(
|
||||
'when ciLimitEnvironmentScope feature flag is $flagStatus',
|
||||
({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
|
||||
beforeEach(() => {
|
||||
createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
|
||||
});
|
||||
|
||||
it('renders all environments when search term is empty', () => {
|
||||
expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
|
||||
expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
|
||||
expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
|
||||
});
|
||||
it(`${defaultEnvStatus} * in listbox`, () => {
|
||||
expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
|
||||
});
|
||||
|
||||
it('does not display active checkmark on the inactive stage', () => {
|
||||
expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
|
||||
});
|
||||
it('renders all environments', () => {
|
||||
expect(findListboxItemByIndex(envIndices[0]).text()).toBe(envs[0]);
|
||||
expect(findListboxItemByIndex(envIndices[1]).text()).toBe(envs[1]);
|
||||
expect(findListboxItemByIndex(envIndices[2]).text()).toBe(envs[2]);
|
||||
});
|
||||
|
||||
it('does not display active checkmark', () => {
|
||||
expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('when `*` is the value of selectedEnvironmentScope props', () => {
|
||||
|
|
@ -68,46 +91,91 @@ describe('Ci environments dropdown', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('Environments found', () => {
|
||||
describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
|
||||
const currentEnv = envs[2];
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent({ searchTerm: currentEnv });
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders only the environment searched for', () => {
|
||||
it('filters on the frontend and renders only the environment searched for', async () => {
|
||||
await findListbox().vm.$emit('search', currentEnv);
|
||||
|
||||
expect(findAllListboxItems()).toHaveLength(1);
|
||||
expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
|
||||
});
|
||||
|
||||
it('does not display create button', () => {
|
||||
expect(findCreateWildcardButton().exists()).toBe(false);
|
||||
it('does not emit event when searching', async () => {
|
||||
expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
|
||||
|
||||
await findListbox().vm.$emit('search', currentEnv);
|
||||
|
||||
expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
|
||||
});
|
||||
|
||||
describe('Custom events', () => {
|
||||
describe('when selecting an environment', () => {
|
||||
const itemIndex = 0;
|
||||
it('does not display note about max environments shown', () => {
|
||||
expect(findMaxEnvNote().exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
|
||||
const currentEnv = envs[2];
|
||||
|
||||
it('emits `select-environment` when an environment is clicked', () => {
|
||||
findListbox().vm.$emit('select', envs[itemIndex]);
|
||||
expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
|
||||
});
|
||||
beforeEach(() => {
|
||||
createComponent({ enableFeatureFlag: true });
|
||||
});
|
||||
|
||||
it('renders environments passed down to it', async () => {
|
||||
await findListbox().vm.$emit('search', currentEnv);
|
||||
|
||||
expect(findAllListboxItems()).toHaveLength(envs.length);
|
||||
});
|
||||
|
||||
it('emits event when searching', async () => {
|
||||
expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
|
||||
|
||||
await findListbox().vm.$emit('search', currentEnv);
|
||||
|
||||
expect(wrapper.emitted('search-environment-scope')).toHaveLength(2);
|
||||
expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
|
||||
});
|
||||
|
||||
it('renders loading icon while search query is loading', async () => {
|
||||
createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
|
||||
|
||||
expect(findListbox().props('searching')).toBe(true);
|
||||
});
|
||||
|
||||
it('displays note about max environments shown', () => {
|
||||
expect(findMaxEnvNote().exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom events', () => {
|
||||
describe('when selecting an environment', () => {
|
||||
const itemIndex = 0;
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
describe('when creating a new environment from a search term', () => {
|
||||
const search = 'new-env';
|
||||
beforeEach(() => {
|
||||
createComponent({ searchTerm: search });
|
||||
});
|
||||
it('emits `select-environment` when an environment is clicked', () => {
|
||||
findListbox().vm.$emit('select', envs[itemIndex]);
|
||||
|
||||
it('emits create-environment-scope', () => {
|
||||
findCreateWildcardButton().vm.$emit('click');
|
||||
expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
|
||||
});
|
||||
expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating a new environment from a search term', () => {
|
||||
const search = 'new-env';
|
||||
beforeEach(() => {
|
||||
createComponent({ searchTerm: search });
|
||||
});
|
||||
|
||||
it('emits create-environment-scope', () => {
|
||||
findCreateWildcardButton().vm.$emit('click');
|
||||
|
||||
expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,10 +10,12 @@ import {
|
|||
EVENT_LABEL,
|
||||
EVENT_ACTION,
|
||||
ENVIRONMENT_SCOPE_LINK_TITLE,
|
||||
groupString,
|
||||
instanceString,
|
||||
projectString,
|
||||
variableOptions,
|
||||
} from '~/ci/ci_variable_list/constants';
|
||||
import { mockVariablesWithScopes } from '../mocks';
|
||||
import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
|
||||
import ModalStub from '../stubs';
|
||||
|
||||
describe('Ci variable modal', () => {
|
||||
|
|
@ -42,12 +44,13 @@ describe('Ci variable modal', () => {
|
|||
};
|
||||
|
||||
const defaultProps = {
|
||||
areEnvironmentsLoading: false,
|
||||
areScopedVariablesAvailable: true,
|
||||
environments: [],
|
||||
hideEnvironmentScope: false,
|
||||
mode: ADD_VARIABLE_ACTION,
|
||||
selectedVariable: {},
|
||||
variable: [],
|
||||
variables: [],
|
||||
};
|
||||
|
||||
const createComponent = ({ mountFn = shallowMountExtended, props = {}, provide = {} } = {}) => {
|
||||
|
|
@ -349,6 +352,42 @@ describe('Ci variable modal', () => {
|
|||
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
|
||||
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
|
||||
});
|
||||
|
||||
describe('when feature flag is enabled', () => {
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
environments: mockEnvs,
|
||||
variables: mockVariablesWithUniqueScopes(projectString),
|
||||
},
|
||||
provide: { glFeatures: { ciLimitEnvironmentScope: true } },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not merge environment scope sources', () => {
|
||||
const expectedLength = mockEnvs.length;
|
||||
|
||||
expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when feature flag is disabled', () => {
|
||||
const mockGroupVariables = mockVariablesWithUniqueScopes(groupString);
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
props: {
|
||||
environments: mockEnvs,
|
||||
variables: mockGroupVariables,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('merges environment scope sources', () => {
|
||||
const expectedLength = mockGroupVariables.length + mockEnvs.length;
|
||||
|
||||
expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('and section is hidden', () => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
|
||||
import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
|
||||
|
|
@ -16,6 +15,7 @@ describe('Ci variable table', () => {
|
|||
let wrapper;
|
||||
|
||||
const defaultProps = {
|
||||
areEnvironmentsLoading: false,
|
||||
areScopedVariablesAvailable: true,
|
||||
entity: 'project',
|
||||
environments: mapEnvironmentNames(mockEnvs),
|
||||
|
|
@ -54,10 +54,10 @@ describe('Ci variable table', () => {
|
|||
it('passes props down correctly to the ci modal', async () => {
|
||||
createComponent();
|
||||
|
||||
findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
|
||||
expect(findCiVariableModal().props()).toEqual({
|
||||
areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
|
||||
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
|
||||
environments: defaultProps.environments,
|
||||
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
|
||||
|
|
@ -74,15 +74,13 @@ describe('Ci variable table', () => {
|
|||
});
|
||||
|
||||
it('passes down ADD mode when receiving an empty variable', async () => {
|
||||
findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
|
||||
expect(findCiVariableModal().props('mode')).toBe(ADD_VARIABLE_ACTION);
|
||||
});
|
||||
|
||||
it('passes down EDIT mode when receiving a variable', async () => {
|
||||
findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
|
||||
|
||||
expect(findCiVariableModal().props('mode')).toBe(EDIT_VARIABLE_ACTION);
|
||||
});
|
||||
|
|
@ -98,25 +96,21 @@ describe('Ci variable table', () => {
|
|||
});
|
||||
|
||||
it('shows modal when adding a new variable', async () => {
|
||||
findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
|
||||
expect(findCiVariableModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows modal when updating a variable', async () => {
|
||||
findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable', newVariable);
|
||||
|
||||
expect(findCiVariableModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('hides modal when receiving the event from the modal', async () => {
|
||||
findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
|
||||
findCiVariableModal().vm.$emit('hideModal');
|
||||
await nextTick();
|
||||
await findCiVariableModal().vm.$emit('hideModal');
|
||||
|
||||
expect(findCiVariableModal().exists()).toBe(false);
|
||||
});
|
||||
|
|
@ -133,11 +127,9 @@ describe('Ci variable table', () => {
|
|||
${'update-variable'}
|
||||
${'delete-variable'}
|
||||
`('bubbles up the $eventName event', async ({ eventName }) => {
|
||||
findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
|
||||
findCiVariableModal().vm.$emit(eventName, newVariable);
|
||||
await nextTick();
|
||||
await findCiVariableModal().vm.$emit(eventName, newVariable);
|
||||
|
||||
expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
|
||||
});
|
||||
|
|
@ -154,10 +146,23 @@ describe('Ci variable table', () => {
|
|||
${'handle-next-page'} | ${undefined}
|
||||
${'sort-changed'} | ${{ sortDesc: true }}
|
||||
`('bubbles up the $eventName event', async ({ args, eventName }) => {
|
||||
findCiVariableTable().vm.$emit(eventName, args);
|
||||
await nextTick();
|
||||
await findCiVariableTable().vm.$emit(eventName, args);
|
||||
|
||||
expect(wrapper.emitted(eventName)).toEqual([[args]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('environment events', () => {
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('bubbles up the search event', async () => {
|
||||
await findCiVariableTable().vm.$emit('set-selected-variable');
|
||||
|
||||
await findCiVariableModal().vm.$emit('search-environment-scope', 'staging');
|
||||
|
||||
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_v
|
|||
import {
|
||||
ADD_MUTATION_ACTION,
|
||||
DELETE_MUTATION_ACTION,
|
||||
ENVIRONMENT_QUERY_LIMIT,
|
||||
UPDATE_MUTATION_ACTION,
|
||||
environmentFetchErrorText,
|
||||
genericMutationErrorText,
|
||||
|
|
@ -111,11 +112,11 @@ describe('Ci Variable Shared Component', () => {
|
|||
${true} | ${'enabled'}
|
||||
${false} | ${'disabled'}
|
||||
`('When Pages FF is $text', ({ isVariablePagesEnabled }) => {
|
||||
const featureFlagProvide = isVariablePagesEnabled
|
||||
const pagesFeatureFlagProvide = isVariablePagesEnabled
|
||||
? { glFeatures: { ciVariablesPages: true } }
|
||||
: {};
|
||||
|
||||
describe('while queries are being fetch', () => {
|
||||
describe('while queries are being fetched', () => {
|
||||
beforeEach(() => {
|
||||
createComponentWithApollo({ isLoading: true });
|
||||
});
|
||||
|
|
@ -133,7 +134,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
mockVariables.mockResolvedValue(mockProjectVariables);
|
||||
|
||||
await createComponentWithApollo({
|
||||
provide: { ...createProjectProvide(), ...featureFlagProvide },
|
||||
provide: { ...createProjectProvide(), ...pagesFeatureFlagProvide },
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
|
||||
mockVariables.mockRejectedValue();
|
||||
|
||||
await createComponentWithApollo({ provide: featureFlagProvide });
|
||||
await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
|
||||
});
|
||||
|
||||
it('calls createAlert with the expected error message', () => {
|
||||
|
|
@ -176,7 +177,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
mockEnvironments.mockRejectedValue();
|
||||
mockVariables.mockResolvedValue(mockProjectVariables);
|
||||
|
||||
await createComponentWithApollo({ provide: featureFlagProvide });
|
||||
await createComponentWithApollo({ provide: pagesFeatureFlagProvide });
|
||||
});
|
||||
|
||||
it('calls createAlert with the expected error message', () => {
|
||||
|
|
@ -187,33 +188,91 @@ describe('Ci Variable Shared Component', () => {
|
|||
|
||||
describe('environment query', () => {
|
||||
describe('when there is an environment key in queryData', () => {
|
||||
beforeEach(async () => {
|
||||
mockEnvironments.mockResolvedValue(mockProjectEnvironments);
|
||||
mockVariables.mockResolvedValue(mockProjectVariables);
|
||||
beforeEach(() => {
|
||||
mockEnvironments
|
||||
.mockResolvedValueOnce(mockProjectEnvironments)
|
||||
.mockResolvedValueOnce(mockProjectEnvironments);
|
||||
|
||||
mockVariables.mockResolvedValue(mockProjectVariables);
|
||||
});
|
||||
|
||||
it('environments are fetched', async () => {
|
||||
await createComponentWithApollo({
|
||||
props: { ...createProjectProps() },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
|
||||
expect(mockEnvironments).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('when Limit Environment Scope FF is enabled', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponentWithApollo({
|
||||
props: { ...createProjectProps() },
|
||||
provide: {
|
||||
glFeatures: {
|
||||
ciLimitEnvironmentScope: true,
|
||||
ciVariablesPages: isVariablePagesEnabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('initial query is called with the correct variables', () => {
|
||||
expect(mockEnvironments).toHaveBeenCalledWith({
|
||||
first: ENVIRONMENT_QUERY_LIMIT,
|
||||
fullPath: '/namespace/project/',
|
||||
search: '',
|
||||
});
|
||||
});
|
||||
|
||||
it(`refetches environments when search term is present`, async () => {
|
||||
expect(mockEnvironments).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnvironments).toHaveBeenCalledWith(expect.objectContaining({ search: '' }));
|
||||
|
||||
await findCiSettings().vm.$emit('search-environment-scope', 'staging');
|
||||
|
||||
expect(mockEnvironments).toHaveBeenCalledTimes(2);
|
||||
expect(mockEnvironments).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ search: 'staging' }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('is executed', () => {
|
||||
expect(mockVariables).toHaveBeenCalled();
|
||||
describe('when Limit Environment Scope FF is disabled', () => {
|
||||
beforeEach(async () => {
|
||||
await createComponentWithApollo({
|
||||
props: { ...createProjectProps() },
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
});
|
||||
|
||||
it('initial query is called with the correct variables', async () => {
|
||||
expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
|
||||
});
|
||||
|
||||
it(`does not refetch environments when search term is present`, async () => {
|
||||
expect(mockEnvironments).toHaveBeenCalledTimes(1);
|
||||
|
||||
await findCiSettings().vm.$emit('search-environment-scope', 'staging');
|
||||
|
||||
expect(mockEnvironments).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when there isnt an environment key in queryData', () => {
|
||||
describe("when there isn't an environment key in queryData", () => {
|
||||
beforeEach(async () => {
|
||||
mockVariables.mockResolvedValue(mockGroupVariables);
|
||||
|
||||
await createComponentWithApollo({
|
||||
props: { ...createGroupProps() },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
});
|
||||
|
||||
it('is skipped', () => {
|
||||
expect(mockVariables).not.toHaveBeenCalled();
|
||||
it('fetching environments is skipped', () => {
|
||||
expect(mockEnvironments).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -227,7 +286,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
await createComponentWithApollo({
|
||||
customHandlers: [[getGroupVariables, mockVariables]],
|
||||
props: groupProps,
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
});
|
||||
it.each`
|
||||
|
|
@ -299,7 +358,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
await createComponentWithApollo({
|
||||
customHandlers: [[getAdminVariables, mockVariables]],
|
||||
props: createInstanceProps(),
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -359,10 +418,11 @@ describe('Ci Variable Shared Component', () => {
|
|||
await createComponentWithApollo({
|
||||
customHandlers,
|
||||
props,
|
||||
provide: { ...provide, ...featureFlagProvide },
|
||||
provide: { ...provide, ...pagesFeatureFlagProvide },
|
||||
});
|
||||
|
||||
expect(findCiSettings().props()).toEqual({
|
||||
areEnvironmentsLoading: false,
|
||||
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
|
||||
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
|
||||
pageInfo: defaultProps.pageInfo,
|
||||
|
|
@ -385,7 +445,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
`('when $bool it $text', async ({ bool }) => {
|
||||
await createComponentWithApollo({
|
||||
props: { ...createInstanceProps(), refetchAfterMutation: bool },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
|
||||
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({ data: {} });
|
||||
|
|
@ -418,7 +478,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
await createComponentWithApollo({
|
||||
customHandlers: [[getGroupVariables, mockVariables]],
|
||||
props: { ...createGroupProps() },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
|
|
@ -433,7 +493,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
await createComponentWithApollo({
|
||||
customHandlers: [[getGroupVariables, mockVariables]],
|
||||
props: { ...createGroupProps(), queryData: { wrongKey: {} } },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
|
|
@ -455,7 +515,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
try {
|
||||
await createComponentWithApollo({
|
||||
props: { ...createGroupProps() },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
|
|
@ -469,7 +529,7 @@ describe('Ci Variable Shared Component', () => {
|
|||
try {
|
||||
await createComponentWithApollo({
|
||||
props: { ...createGroupProps(), mutationData: { wrongKey: {} } },
|
||||
provide: featureFlagProvide,
|
||||
provide: pagesFeatureFlagProvide,
|
||||
});
|
||||
} catch (e) {
|
||||
error = e;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,11 @@ export const mockVariablesWithScopes = (kind) =>
|
|||
return { ...variable, environmentScope: '*' };
|
||||
});
|
||||
|
||||
export const mockVariablesWithUniqueScopes = (kind) =>
|
||||
mockVariables(kind).map((variable) => {
|
||||
return { ...variable, environmentScope: variable.value };
|
||||
});
|
||||
|
||||
const createDefaultVars = ({ withScope = true, kind } = {}) => {
|
||||
let base = mockVariables(kind);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import MockAdapter from 'axios-mock-adapter';
|
||||
import Cookies from '~/lib/utils/cookies';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import testAction from 'helpers/vuex_action_helper';
|
||||
|
|
@ -24,6 +25,7 @@ import {
|
|||
} from '~/lib/utils/http_status';
|
||||
import { mergeUrlParams } from '~/lib/utils/url_utility';
|
||||
import eventHub from '~/notes/event_hub';
|
||||
import diffsEventHub from '~/diffs/event_hub';
|
||||
import { diffMetadata } from '../mock_data/diff_metadata';
|
||||
|
||||
jest.mock('~/alert');
|
||||
|
|
@ -135,6 +137,112 @@ describe('DiffsStoreActions', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('fetchFileByFile', () => {
|
||||
beforeEach(() => {
|
||||
window.location.hash = 'e334a2a10f036c00151a04cea7938a5d4213a818';
|
||||
});
|
||||
|
||||
it('should do nothing if there is no tree entry for the file ID', () => {
|
||||
return testAction(diffActions.fetchFileByFile, {}, { flatBlobsList: [] }, [], []);
|
||||
});
|
||||
|
||||
it('should do nothing if the tree entry for the file ID has already been marked as loaded', () => {
|
||||
return testAction(
|
||||
diffActions.fetchFileByFile,
|
||||
{},
|
||||
{
|
||||
flatBlobsList: [
|
||||
{ fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818', diffLoaded: true },
|
||||
],
|
||||
},
|
||||
[],
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
describe('when a tree entry exists for the file, but it has not been marked as loaded', () => {
|
||||
let state;
|
||||
let commit;
|
||||
let hubSpy;
|
||||
const endpointDiffForPath = '/diffs/set/endpoint/path';
|
||||
const diffForPath = mergeUrlParams(
|
||||
{
|
||||
old_path: 'old/123',
|
||||
new_path: 'new/123',
|
||||
w: '1',
|
||||
view: 'inline',
|
||||
},
|
||||
endpointDiffForPath,
|
||||
);
|
||||
const treeEntry = {
|
||||
fileHash: 'e334a2a10f036c00151a04cea7938a5d4213a818',
|
||||
filePaths: { old: 'old/123', new: 'new/123' },
|
||||
};
|
||||
const fileResult = {
|
||||
diff_files: [{ file_hash: 'e334a2a10f036c00151a04cea7938a5d4213a818' }],
|
||||
};
|
||||
const getters = {
|
||||
flatBlobsList: [treeEntry],
|
||||
getDiffFileByHash(hash) {
|
||||
return state.diffFiles?.find((entry) => entry.file_hash === hash);
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
commit = jest.fn();
|
||||
state = {
|
||||
endpointDiffForPath,
|
||||
diffFiles: [],
|
||||
};
|
||||
getters.flatBlobsList = [treeEntry];
|
||||
hubSpy = jest.spyOn(diffsEventHub, '$emit');
|
||||
});
|
||||
|
||||
it('does nothing if the file already exists in the loaded diff files', () => {
|
||||
state.diffFiles = fileResult.diff_files;
|
||||
|
||||
return testAction(diffActions.fetchFileByFile, state, getters, [], []);
|
||||
});
|
||||
|
||||
it('does some standard work every time', async () => {
|
||||
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
|
||||
|
||||
await diffActions.fetchFileByFile({ state, getters, commit });
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loading');
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_RETRIEVING_BATCHES, true);
|
||||
|
||||
// wait for the mocked network request to return and start processing the .then
|
||||
await waitForPromises();
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_DIFF_DATA_BATCH, fileResult);
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_BATCH_LOADING_STATE, 'loaded');
|
||||
|
||||
expect(hubSpy).toHaveBeenCalledWith('diffFilesModified');
|
||||
});
|
||||
|
||||
it.each`
|
||||
urlHash | diffFiles | expected
|
||||
${treeEntry.fileHash} | ${[]} | ${''}
|
||||
${'abcdef1234567890'} | ${fileResult.diff_files} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
|
||||
`(
|
||||
"sets the current file to the first diff file ('$id') if it's not a note hash and there isn't a current ID set",
|
||||
async ({ urlHash, diffFiles, expected }) => {
|
||||
window.location.hash = urlHash;
|
||||
mock.onGet(diffForPath).reply(HTTP_STATUS_OK, fileResult);
|
||||
state.diffFiles = diffFiles;
|
||||
|
||||
await diffActions.fetchFileByFile({ state, getters, commit });
|
||||
|
||||
// wait for the mocked network request to return and start processing the .then
|
||||
await waitForPromises();
|
||||
|
||||
expect(commit).toHaveBeenCalledWith(types.SET_CURRENT_DIFF_FILE, expected);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchDiffFilesBatch', () => {
|
||||
it('should fetch batch diff files', () => {
|
||||
const endpointBatch = '/fetch/diffs_batch';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<div id="oauth-container">
|
||||
<input id="remember_me" type="checkbox" />
|
||||
<input id="remember_me_omniauth" type="checkbox" />
|
||||
|
||||
<form method="post" action="http://example.com/">
|
||||
<button class="js-oauth-login twitter" type="submit">
|
||||
|
|
|
|||
|
|
@ -1,14 +1,20 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlBadge } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
|
||||
import ActivityBar from '~/ide/components/activity_bar.vue';
|
||||
import { leftSidebarViews } from '~/ide/constants';
|
||||
import { createStore } from '~/ide/stores';
|
||||
|
||||
const { edit, ...VIEW_OBJECTS_WITHOUT_EDIT } = leftSidebarViews;
|
||||
const MODES_WITHOUT_EDIT = Object.keys(VIEW_OBJECTS_WITHOUT_EDIT);
|
||||
const MODES = Object.keys(leftSidebarViews);
|
||||
|
||||
describe('IDE ActivityBar component', () => {
|
||||
let wrapper;
|
||||
let store;
|
||||
|
||||
const findChangesBadge = () => wrapper.findComponent(GlBadge);
|
||||
const findModeButton = (mode) => wrapper.findByTestId(`${mode}-mode-button`);
|
||||
|
||||
const mountComponent = (state) => {
|
||||
store = createStore();
|
||||
|
|
@ -19,45 +25,43 @@ describe('IDE ActivityBar component', () => {
|
|||
...state,
|
||||
});
|
||||
|
||||
wrapper = shallowMount(ActivityBar, { store });
|
||||
wrapper = shallowMountExtended(ActivityBar, { store });
|
||||
};
|
||||
|
||||
describe('updateActivityBarView', () => {
|
||||
beforeEach(() => {
|
||||
mountComponent();
|
||||
jest.spyOn(wrapper.vm, 'updateActivityBarView').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
it('calls updateActivityBarView with edit value on click', () => {
|
||||
wrapper.find('.js-ide-edit-mode').trigger('click');
|
||||
|
||||
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.edit.name);
|
||||
});
|
||||
|
||||
it('calls updateActivityBarView with commit value on click', () => {
|
||||
wrapper.find('.js-ide-commit-mode').trigger('click');
|
||||
|
||||
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.commit.name);
|
||||
});
|
||||
|
||||
it('calls updateActivityBarView with review value on click', () => {
|
||||
wrapper.find('.js-ide-review-mode').trigger('click');
|
||||
|
||||
expect(wrapper.vm.updateActivityBarView).toHaveBeenCalledWith(leftSidebarViews.review.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe('active item', () => {
|
||||
it('sets edit item active', () => {
|
||||
mountComponent();
|
||||
// Test that mode button does not have 'active' class before click,
|
||||
// and does have 'active' class after click
|
||||
const testSettingActiveItem = async (mode) => {
|
||||
const button = findModeButton(mode);
|
||||
|
||||
expect(wrapper.find('.js-ide-edit-mode').classes()).toContain('active');
|
||||
expect(button.classes('active')).toBe(false);
|
||||
|
||||
button.trigger('click');
|
||||
await nextTick();
|
||||
|
||||
expect(button.classes('active')).toBe(true);
|
||||
};
|
||||
|
||||
it.each(MODES)('is initially set to %s mode', (mode) => {
|
||||
mountComponent({ currentActivityView: leftSidebarViews[mode].name });
|
||||
|
||||
const button = findModeButton(mode);
|
||||
|
||||
expect(button.classes('active')).toBe(true);
|
||||
});
|
||||
|
||||
it('sets commit item active', () => {
|
||||
mountComponent({ currentActivityView: leftSidebarViews.commit.name });
|
||||
it.each(MODES_WITHOUT_EDIT)('is correctly set after clicking %s mode button', async (mode) => {
|
||||
mountComponent();
|
||||
|
||||
expect(wrapper.find('.js-ide-commit-mode').classes()).toContain('active');
|
||||
testSettingActiveItem(mode);
|
||||
});
|
||||
|
||||
it('is correctly set after clicking edit mode button', async () => {
|
||||
// The default currentActivityView is leftSidebarViews.edit.name,
|
||||
// so for the 'edit' mode, we pass a different currentActivityView.
|
||||
mountComponent({ currentActivityView: leftSidebarViews.review.name });
|
||||
|
||||
testSettingActiveItem('edit');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -65,7 +69,6 @@ describe('IDE ActivityBar component', () => {
|
|||
it('is rendered when files are staged', () => {
|
||||
mountComponent({ stagedFiles: [{ path: '/path/to/file' }] });
|
||||
|
||||
expect(findChangesBadge().exists()).toBe(true);
|
||||
expect(findChangesBadge().text()).toBe('1');
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { mount } from '@vue/test-utils';
|
||||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import { keepAlive } from 'helpers/keep_alive_component_helper';
|
||||
import { viewerTypes } from '~/ide/constants';
|
||||
import IdeTree from '~/ide/components/ide_tree.vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { createStoreOptions } from '~/ide/stores';
|
||||
import { file } from '../helpers';
|
||||
import { projectData } from '../mock_data';
|
||||
|
||||
|
|
@ -13,42 +13,72 @@ describe('IdeTree', () => {
|
|||
let store;
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createStore();
|
||||
const actionSpies = {
|
||||
updateViewer: jest.fn(),
|
||||
};
|
||||
|
||||
store.state.currentProjectId = 'abcproject';
|
||||
store.state.currentBranchId = 'main';
|
||||
store.state.projects.abcproject = { ...projectData };
|
||||
Vue.set(store.state.trees, 'abcproject/main', {
|
||||
tree: [file('fileName')],
|
||||
loading: false,
|
||||
const testState = {
|
||||
currentProjectId: 'abcproject',
|
||||
currentBranchId: 'main',
|
||||
projects: {
|
||||
abcproject: { ...projectData },
|
||||
},
|
||||
trees: {
|
||||
'abcproject/main': {
|
||||
tree: [file('fileName')],
|
||||
loading: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createComponent = (replaceState) => {
|
||||
const defaultStore = createStoreOptions();
|
||||
|
||||
store = new Vuex.Store({
|
||||
...defaultStore,
|
||||
state: {
|
||||
...defaultStore.state,
|
||||
...testState,
|
||||
replaceState,
|
||||
},
|
||||
actions: {
|
||||
...defaultStore.actions,
|
||||
...actionSpies,
|
||||
},
|
||||
});
|
||||
|
||||
wrapper = mount(keepAlive(IdeTree), {
|
||||
wrapper = mount(IdeTree, {
|
||||
store,
|
||||
});
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders list of files', () => {
|
||||
expect(wrapper.text()).toContain('fileName');
|
||||
afterEach(() => {
|
||||
actionSpies.updateViewer.mockClear();
|
||||
});
|
||||
|
||||
describe('renders properly', () => {
|
||||
it('renders list of files', () => {
|
||||
expect(wrapper.text()).toContain('fileName');
|
||||
});
|
||||
});
|
||||
|
||||
describe('activated', () => {
|
||||
let inititializeSpy;
|
||||
|
||||
beforeEach(async () => {
|
||||
inititializeSpy = jest.spyOn(wrapper.findComponent(IdeTree).vm, 'initialize');
|
||||
store.state.viewer = 'diff';
|
||||
|
||||
await wrapper.vm.reactivate();
|
||||
beforeEach(() => {
|
||||
createComponent({
|
||||
viewer: viewerTypes.diff,
|
||||
});
|
||||
});
|
||||
|
||||
it('re initializes the component', () => {
|
||||
expect(inititializeSpy).toHaveBeenCalled();
|
||||
expect(actionSpies.updateViewer).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('updates viewer to "editor" by default', () => {
|
||||
expect(store.state.viewer).toBe('editor');
|
||||
expect(actionSpies.updateViewer).toHaveBeenCalledWith(expect.any(Object), viewerTypes.edit);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,19 +17,16 @@ describe('OAuthRememberMe', () => {
|
|||
resetHTMLFixture();
|
||||
});
|
||||
|
||||
it('adds the "remember_me" query parameter to all OAuth login buttons', () => {
|
||||
$('#oauth-container #remember_me').click();
|
||||
it('adds and removes the "remember_me" query parameter from all OAuth login buttons', () => {
|
||||
$('#oauth-container #remember_me_omniauth').click();
|
||||
|
||||
expect(findFormAction('.twitter')).toBe('http://example.com/?remember_me=1');
|
||||
expect(findFormAction('.github')).toBe('http://example.com/?remember_me=1');
|
||||
expect(findFormAction('.facebook')).toBe(
|
||||
'http://example.com/?redirect_fragment=L1&remember_me=1',
|
||||
);
|
||||
});
|
||||
|
||||
it('removes the "remember_me" query parameter from all OAuth login buttons', () => {
|
||||
$('#oauth-container #remember_me').click();
|
||||
$('#oauth-container #remember_me').click();
|
||||
$('#oauth-container #remember_me_omniauth').click();
|
||||
|
||||
expect(findFormAction('.twitter')).toBe('http://example.com/');
|
||||
expect(findFormAction('.github')).toBe('http://example.com/');
|
||||
|
|
|
|||
|
|
@ -2,43 +2,130 @@
|
|||
|
||||
exports[`Saved replies list item component renders list item 1`] = `
|
||||
<li
|
||||
class="gl-mb-5"
|
||||
class="gl-pt-4 gl-pb-5 gl-border-b"
|
||||
>
|
||||
<div
|
||||
class="gl-display-flex gl-align-items-center"
|
||||
>
|
||||
<strong
|
||||
<h6
|
||||
class="gl-mr-3 gl-my-0"
|
||||
data-testid="saved-reply-name"
|
||||
>
|
||||
test
|
||||
</strong>
|
||||
</h6>
|
||||
|
||||
<div
|
||||
class="gl-ml-auto"
|
||||
>
|
||||
<gl-button-stub
|
||||
aria-label="Edit"
|
||||
buttontextclasses=""
|
||||
category="primary"
|
||||
class="gl-mr-3"
|
||||
data-testid="saved-reply-edit-btn"
|
||||
icon="pencil"
|
||||
size="medium"
|
||||
title="Edit"
|
||||
to="[object Object]"
|
||||
variant="default"
|
||||
/>
|
||||
<div
|
||||
class="gl-new-dropdown gl-disclosure-dropdown"
|
||||
>
|
||||
<button
|
||||
aria-controls="base-dropdown-5"
|
||||
aria-labelledby="actions-toggle-3"
|
||||
class="btn btn-default btn-md gl-button btn-default-tertiary gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
|
||||
data-testid="base-dropdown-toggle"
|
||||
id="actions-toggle-3"
|
||||
listeners="[object Object]"
|
||||
type="button"
|
||||
>
|
||||
<!---->
|
||||
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="gl-button-icon gl-icon s16"
|
||||
data-testid="ellipsis_v-icon"
|
||||
role="img"
|
||||
>
|
||||
<use
|
||||
href="#ellipsis_v"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span
|
||||
class="gl-button-text"
|
||||
>
|
||||
<span
|
||||
class="gl-new-dropdown-button-text gl-sr-only"
|
||||
>
|
||||
|
||||
Saved reply actions
|
||||
|
||||
</span>
|
||||
|
||||
<!---->
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="gl-new-dropdown-panel"
|
||||
data-testid="base-dropdown-menu"
|
||||
id="base-dropdown-5"
|
||||
>
|
||||
<div
|
||||
class="gl-new-dropdown-inner"
|
||||
>
|
||||
|
||||
<ul
|
||||
aria-labelledby="actions-toggle-3"
|
||||
class="gl-new-dropdown-contents"
|
||||
data-testid="disclosure-content"
|
||||
id="disclosure-4"
|
||||
tabindex="-1"
|
||||
>
|
||||
<li
|
||||
class="gl-new-dropdown-item"
|
||||
data-testid="disclosure-dropdown-item"
|
||||
tabindex="0"
|
||||
>
|
||||
<button
|
||||
class="gl-new-dropdown-item-content"
|
||||
data-testid="saved-reply-edit-btn"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="gl-new-dropdown-item-text-wrapper"
|
||||
>
|
||||
|
||||
Edit
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
<li
|
||||
class="gl-new-dropdown-item"
|
||||
data-testid="disclosure-dropdown-item"
|
||||
tabindex="0"
|
||||
>
|
||||
<button
|
||||
class="gl-new-dropdown-item-content gl-text-red-500!"
|
||||
data-testid="saved-reply-delete-btn"
|
||||
tabindex="-1"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="gl-new-dropdown-item-text-wrapper"
|
||||
>
|
||||
|
||||
Delete
|
||||
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<gl-button-stub
|
||||
aria-label="Delete"
|
||||
buttontextclasses=""
|
||||
category="secondary"
|
||||
data-testid="saved-reply-delete-btn"
|
||||
icon="remove"
|
||||
size="medium"
|
||||
title="Delete"
|
||||
variant="danger"
|
||||
/>
|
||||
<div
|
||||
class="gl-tooltip"
|
||||
>
|
||||
|
||||
Saved reply actions
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -48,20 +135,6 @@ exports[`Saved replies list item component renders list item 1`] = `
|
|||
/assign_reviewer
|
||||
</div>
|
||||
|
||||
<gl-modal-stub
|
||||
actionprimary="[object Object]"
|
||||
actionsecondary="[object Object]"
|
||||
arialabel=""
|
||||
dismisslabel="Close"
|
||||
modalclass=""
|
||||
modalid="delete-saved-reply-2"
|
||||
size="sm"
|
||||
title="Delete saved reply"
|
||||
titletag="h4"
|
||||
>
|
||||
<gl-sprintf-stub
|
||||
message="Are you sure you want to delete %{name}? This action cannot be undone."
|
||||
/>
|
||||
</gl-modal-stub>
|
||||
<!---->
|
||||
</li>
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -1,50 +1,154 @@
|
|||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import { GlModal } from '@gitlab/ui';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { GlDisclosureDropdown, GlDisclosureDropdownItem, GlModal } from '@gitlab/ui';
|
||||
import { __ } from '~/locale';
|
||||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import { createMockDirective } from 'helpers/vue_mock_directive';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import ListItem from '~/saved_replies/components/list_item.vue';
|
||||
import deleteSavedReplyMutation from '~/saved_replies/queries/delete_saved_reply.mutation.graphql';
|
||||
|
||||
let wrapper;
|
||||
let deleteSavedReplyMutationResponse;
|
||||
|
||||
function createComponent(propsData = {}) {
|
||||
function createMockApolloProvider(requestHandlers = [deleteSavedReplyMutation]) {
|
||||
Vue.use(VueApollo);
|
||||
|
||||
deleteSavedReplyMutationResponse = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
|
||||
|
||||
return shallowMount(ListItem, {
|
||||
propsData,
|
||||
directives: {
|
||||
GlModal: createMockDirective('gl-modal'),
|
||||
},
|
||||
apolloProvider: createMockApollo([
|
||||
[deleteSavedReplyMutation, deleteSavedReplyMutationResponse],
|
||||
]),
|
||||
});
|
||||
return createMockApollo([requestHandlers]);
|
||||
}
|
||||
|
||||
describe('Saved replies list item component', () => {
|
||||
let wrapper;
|
||||
let $router;
|
||||
|
||||
function createComponent(propsData = {}, apolloProvider = createMockApolloProvider) {
|
||||
$router = {
|
||||
push: jest.fn(),
|
||||
};
|
||||
|
||||
return mount(ListItem, {
|
||||
propsData,
|
||||
directives: {
|
||||
GlModal: createMockDirective('gl-modal'),
|
||||
},
|
||||
apolloProvider,
|
||||
mocks: {
|
||||
$router,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
|
||||
const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
|
||||
const findModal = () => wrapper.findComponent(GlModal);
|
||||
|
||||
it('renders list item', async () => {
|
||||
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
|
||||
|
||||
expect(wrapper.element).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('delete button', () => {
|
||||
it('calls Apollo mutate', async () => {
|
||||
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer', id: 1 } });
|
||||
describe('saved reply actions dropdown', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
|
||||
});
|
||||
|
||||
wrapper.findComponent(GlModal).vm.$emit('primary');
|
||||
it('exists', () => {
|
||||
expect(findDropdown().exists()).toBe(true);
|
||||
});
|
||||
|
||||
await waitForPromises();
|
||||
it('has correct toggle text', () => {
|
||||
expect(findDropdown().props('toggleText')).toBe(__('Saved reply actions'));
|
||||
});
|
||||
|
||||
it('has correct amount of dropdown items', () => {
|
||||
const items = findDropdownItems();
|
||||
|
||||
expect(items.exists()).toBe(true);
|
||||
expect(items).toHaveLength(2);
|
||||
});
|
||||
|
||||
describe('edit option', () => {
|
||||
it('exists', () => {
|
||||
const items = findDropdownItems();
|
||||
|
||||
const editItem = items.filter((item) => item.text() === __('Edit'));
|
||||
|
||||
expect(editItem.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows as first dropdown item', () => {
|
||||
const items = findDropdownItems();
|
||||
|
||||
expect(items.at(0).text()).toBe(__('Edit'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete option', () => {
|
||||
it('exists', () => {
|
||||
const items = findDropdownItems();
|
||||
|
||||
const deleteItem = items.filter((item) => item.text() === __('Delete'));
|
||||
|
||||
expect(deleteItem.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('shows as first dropdown item', () => {
|
||||
const items = findDropdownItems();
|
||||
|
||||
expect(items.at(1).text()).toBe(__('Delete'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Delete modal', () => {
|
||||
let deleteSavedReplyMutationResponse;
|
||||
|
||||
beforeEach(() => {
|
||||
deleteSavedReplyMutationResponse = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ data: { savedReplyDestroy: { errors: [] } } });
|
||||
|
||||
const apolloProvider = createMockApolloProvider([
|
||||
deleteSavedReplyMutation,
|
||||
deleteSavedReplyMutationResponse,
|
||||
]);
|
||||
|
||||
wrapper = createComponent(
|
||||
{ reply: { name: 'test', content: '/assign_reviewer', id: 1 } },
|
||||
apolloProvider,
|
||||
);
|
||||
});
|
||||
|
||||
it('exists', () => {
|
||||
expect(findModal().exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('has correct title', () => {
|
||||
expect(findModal().props('title')).toBe(__('Delete saved reply'));
|
||||
});
|
||||
|
||||
it('delete button calls Apollo mutate', async () => {
|
||||
await findModal().vm.$emit('primary');
|
||||
|
||||
expect(deleteSavedReplyMutationResponse).toHaveBeenCalledWith({ id: 1 });
|
||||
});
|
||||
|
||||
it('cancel button does not trigger Apollo mutation', async () => {
|
||||
await findModal().vm.$emit('secondary');
|
||||
|
||||
expect(deleteSavedReplyMutationResponse).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dropdown Edit', () => {
|
||||
beforeEach(() => {
|
||||
wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
|
||||
});
|
||||
|
||||
it('click triggers router push', async () => {
|
||||
const editComponent = findDropdownItems().at(0);
|
||||
|
||||
await editComponent.find('button').trigger('click');
|
||||
|
||||
expect($router.push).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ RSpec.describe Ci::Catalog::ResourcesHelper, feature_category: :pipeline_composi
|
|||
|
||||
before do
|
||||
allow(helper).to receive(:can_collaborate_with_project?).and_return(true)
|
||||
stub_licensed_features(ci_private_catalog: false)
|
||||
stub_licensed_features(ci_namespace_catalog: false)
|
||||
end
|
||||
|
||||
it 'user cannot view the Catalog in CE regardless of permissions' do
|
||||
|
|
|
|||
|
|
@ -77,7 +77,9 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
end
|
||||
|
||||
before do
|
||||
allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'))
|
||||
allow(YAML).to receive(:safe_load_file).with(
|
||||
File.join(Gitlab.config.backup.path, 'backup_information.yml'),
|
||||
permitted_classes: described_class::YAML_PERMITTED_CLASSES)
|
||||
.and_return(backup_information)
|
||||
end
|
||||
|
||||
|
|
@ -603,14 +605,16 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
end
|
||||
|
||||
expect(Kernel).not_to have_received(:system).with(*pack_tar_cmdline)
|
||||
expect(YAML.load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'))).to include(
|
||||
backup_created_at: backup_time.localtime,
|
||||
db_version: be_a(String),
|
||||
gitlab_version: Gitlab::VERSION,
|
||||
installation_type: Gitlab::INSTALLATION_TYPE,
|
||||
skipped: 'tar',
|
||||
tar_version: be_a(String)
|
||||
)
|
||||
expect(YAML.safe_load_file(
|
||||
File.join(Gitlab.config.backup.path, 'backup_information.yml'),
|
||||
permitted_classes: described_class::YAML_PERMITTED_CLASSES)).to include(
|
||||
backup_created_at: backup_time.localtime,
|
||||
db_version: be_a(String),
|
||||
gitlab_version: Gitlab::VERSION,
|
||||
installation_type: Gitlab::INSTALLATION_TYPE,
|
||||
skipped: 'tar',
|
||||
tar_version: be_a(String)
|
||||
)
|
||||
expect(FileUtils).to have_received(:rm_rf).with(File.join(Gitlab.config.backup.path, 'tmp'))
|
||||
end
|
||||
end
|
||||
|
|
@ -629,8 +633,10 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
end
|
||||
|
||||
before do
|
||||
allow(YAML).to receive(:load_file).and_call_original
|
||||
allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'))
|
||||
allow(YAML).to receive(:safe_load_file).and_call_original
|
||||
allow(YAML).to receive(:safe_load_file).with(
|
||||
File.join(Gitlab.config.backup.path, 'backup_information.yml'),
|
||||
permitted_classes: described_class::YAML_PERMITTED_CLASSES)
|
||||
.and_return(backup_information)
|
||||
end
|
||||
|
||||
|
|
@ -892,12 +898,13 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
.with(a_string_matching('Non tarred backup found '))
|
||||
expect(progress).to have_received(:puts)
|
||||
.with(a_string_matching("Backup #{backup_id} is done"))
|
||||
expect(YAML.load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'))).to include(
|
||||
backup_created_at: backup_time,
|
||||
full_backup_id: full_backup_id,
|
||||
gitlab_version: Gitlab::VERSION,
|
||||
skipped: 'something,tar'
|
||||
)
|
||||
expect(YAML.safe_load_file(File.join(Gitlab.config.backup.path, 'backup_information.yml'),
|
||||
permitted_classes: described_class::YAML_PERMITTED_CLASSES)).to include(
|
||||
backup_created_at: backup_time,
|
||||
full_backup_id: full_backup_id,
|
||||
gitlab_version: Gitlab::VERSION,
|
||||
skipped: 'something,tar'
|
||||
)
|
||||
end
|
||||
|
||||
context 'on version mismatch' do
|
||||
|
|
@ -943,7 +950,8 @@ RSpec.describe Backup::Manager, feature_category: :backup_restore do
|
|||
allow(Gitlab::BackupLogger).to receive(:info)
|
||||
allow(task1).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'task1.tar.gz'))
|
||||
allow(task2).to receive(:restore).with(File.join(Gitlab.config.backup.path, 'task2.tar.gz'))
|
||||
allow(YAML).to receive(:load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'))
|
||||
allow(YAML).to receive(:safe_load_file).with(File.join(Gitlab.config.backup.path, 'backup_information.yml'),
|
||||
permitted_classes: described_class::YAML_PERMITTED_CLASSES)
|
||||
.and_return(backup_information)
|
||||
allow(Rake::Task['gitlab:shell:setup']).to receive(:invoke)
|
||||
allow(Rake::Task['cache:clear']).to receive(:invoke)
|
||||
|
|
|
|||
|
|
@ -13,10 +13,6 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
|
|||
let(:response_double) { double }
|
||||
let(:version) { '14.0.0' }
|
||||
|
||||
before do
|
||||
stub_const('BulkImports::MINIMUM_COMPATIBLE_MAJOR_VERSION', version)
|
||||
end
|
||||
|
||||
describe 'source instance validation' do
|
||||
before do
|
||||
allow(graphql_client_double).to receive(:execute)
|
||||
|
|
@ -37,7 +33,7 @@ RSpec.describe BulkImports::Clients::Graphql, feature_category: :importers do
|
|||
let(:version) { '13.0.0' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Source instance must run GitLab version #{BulkImport::MIN_MAJOR_VERSION} or later.")
|
||||
expect { subject.execute('test') }.to raise_error(::BulkImports::Error, "Unsupported GitLab version. Minimum supported version is 14.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -93,6 +93,9 @@ RSpec.describe API::BulkImports, feature_category: :importers do
|
|||
}
|
||||
end
|
||||
|
||||
let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[:entities][0][:source_type]) }
|
||||
let(:source_entity_identifier) { ERB::Util.url_encode(params[:entities][0][:source_full_path]) }
|
||||
|
||||
before do
|
||||
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
|
||||
allow(instance)
|
||||
|
|
@ -103,6 +106,8 @@ RSpec.describe API::BulkImports, feature_category: :importers do
|
|||
.to receive(:instance_enterprise)
|
||||
.and_return(false)
|
||||
end
|
||||
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
|
||||
.to_return(status: 200, body: "", headers: {})
|
||||
end
|
||||
|
||||
shared_examples 'starting a new migration' do
|
||||
|
|
@ -271,12 +276,41 @@ RSpec.describe API::BulkImports, feature_category: :importers do
|
|||
}
|
||||
end
|
||||
|
||||
it 'returns blocked url error' do
|
||||
it 'returns blocked url message in the error' do
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
|
||||
expect(json_response['message']).to eq('Validation failed: Url is blocked: Only allowed schemes are http, https')
|
||||
expect(json_response['message']).to include("Url is blocked: Only allowed schemes are http, https")
|
||||
end
|
||||
end
|
||||
|
||||
context 'when source instance setting is disabled' do
|
||||
let(:params) do
|
||||
{
|
||||
configuration: {
|
||||
url: 'http://gitlab.example',
|
||||
access_token: 'access_token'
|
||||
},
|
||||
entities: [
|
||||
source_type: 'group_entity',
|
||||
source_full_path: 'full_path',
|
||||
destination_slug: 'destination_slug',
|
||||
destination_namespace: 'destination_namespace'
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns blocked url error' do
|
||||
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=access_token")
|
||||
.to_return(status: 404, body: "", headers: {})
|
||||
|
||||
request
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
|
||||
expect(json_response['message']).to include("Group import disabled on source or destination instance. " \
|
||||
"Ask an administrator to enable it on both instances and try again.")
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
|
|||
]
|
||||
end
|
||||
|
||||
let(:source_entity_identifier) { ERB::Util.url_encode(params[0][:source_full_path]) }
|
||||
let(:source_entity_type) { BulkImports::CreateService::ENTITY_TYPES_MAPPING.fetch(params[0][:source_type]) }
|
||||
|
||||
subject { described_class.new(user, params, credentials) }
|
||||
|
||||
describe '#execute' do
|
||||
|
|
@ -59,6 +62,34 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when direct transfer setting query returns a 404' do
|
||||
it 'raises a ServiceResponse::Error' do
|
||||
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
|
||||
stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
|
||||
.to_return(
|
||||
status: 200,
|
||||
body: source_version.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
|
||||
.to_return(status: 404)
|
||||
|
||||
expect_next_instance_of(BulkImports::Clients::HTTP) do |client|
|
||||
expect(client).to receive(:get).and_raise(BulkImports::Error.setting_not_enabled)
|
||||
end
|
||||
|
||||
result = subject.execute
|
||||
|
||||
expect(result).to be_a(ServiceResponse)
|
||||
expect(result).to be_error
|
||||
expect(result.message)
|
||||
.to eq(
|
||||
"Group import disabled on source or destination instance. " \
|
||||
"Ask an administrator to enable it on both instances and try again."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when required scopes are not present' do
|
||||
it 'returns ServiceResponse with error if token does not have api scope' do
|
||||
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
|
||||
|
|
@ -68,9 +99,13 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
|
|||
body: source_version.to_json,
|
||||
headers: { 'Content-Type' => 'application/json' }
|
||||
)
|
||||
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
|
||||
.to_return(
|
||||
status: 200
|
||||
)
|
||||
|
||||
allow_next_instance_of(BulkImports::Clients::HTTP) do |client|
|
||||
allow(client).to receive(:validate_instance_version!).and_raise(BulkImports::Error.scope_validation_failure)
|
||||
allow(client).to receive(:validate_import_scopes!).and_raise(BulkImports::Error.scope_validation_failure)
|
||||
end
|
||||
|
||||
result = subject.execute
|
||||
|
|
@ -90,6 +125,10 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
|
|||
stub_request(:get, 'http://gitlab.example/api/v4/version?private_token=token').to_return(status: 404)
|
||||
stub_request(:get, 'http://gitlab.example/api/v4/metadata?private_token=token')
|
||||
.to_return(status: 200, body: source_version.to_json, headers: { 'Content-Type' => 'application/json' })
|
||||
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
|
||||
.to_return(
|
||||
status: 200
|
||||
)
|
||||
stub_request(:get, 'http://gitlab.example/api/v4/personal_access_tokens/self?private_token=token')
|
||||
.to_return(
|
||||
status: 200,
|
||||
|
|
@ -169,6 +208,10 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
|
|||
allow_next_instance_of(BulkImports::Clients::HTTP) do |instance|
|
||||
allow(instance).to receive(:instance_version).and_return(source_version)
|
||||
allow(instance).to receive(:instance_enterprise).and_return(false)
|
||||
stub_request(:get, "http://gitlab.example/api/v4/#{source_entity_type}/#{source_entity_identifier}/export_relations/status?page=1&per_page=30&private_token=token")
|
||||
.to_return(
|
||||
status: 200
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -325,6 +368,105 @@ RSpec.describe BulkImports::CreateService, feature_category: :importers do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.validate_setting_enabled!' do
|
||||
let(:entity_source_id) { 'gid://gitlab/Model/12345' }
|
||||
let(:graphql_client) { instance_double(BulkImports::Clients::Graphql) }
|
||||
let(:http_client) { instance_double(BulkImports::Clients::HTTP) }
|
||||
let(:http_response) { double(code: 200, success?: true) } # rubocop:disable RSpec/VerifiedDoubles
|
||||
|
||||
before do
|
||||
allow(BulkImports::Clients::HTTP).to receive(:new).and_return(http_client)
|
||||
allow(BulkImports::Clients::Graphql).to receive(:new).and_return(graphql_client)
|
||||
|
||||
allow(http_client).to receive(:instance_version).and_return(status: 200)
|
||||
allow(http_client).to receive(:instance_enterprise).and_return(false)
|
||||
allow(http_client).to receive(:validate_instance_version!).and_return(source_version)
|
||||
allow(http_client).to receive(:validate_import_scopes!).and_return(true)
|
||||
end
|
||||
|
||||
context 'when the source_type is a group' do
|
||||
context 'when the source_full_path contains only integer characters' do
|
||||
let(:query_string) { BulkImports::Groups::Graphql::GetGroupQuery.new(context: nil).to_s }
|
||||
let(:graphql_response) do
|
||||
double(original_hash: { 'data' => { 'group' => { 'id' => entity_source_id } } }) # rubocop:disable RSpec/VerifiedDoubles
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
[
|
||||
{
|
||||
source_type: 'group_entity',
|
||||
source_full_path: '67890',
|
||||
destination_slug: 'destination-group-1',
|
||||
destination_namespace: 'destination1'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
allow(graphql_client).to receive(:parse).with(query_string)
|
||||
allow(graphql_client).to receive(:execute).and_return(graphql_response)
|
||||
|
||||
allow(http_client).to receive(:get)
|
||||
.with("/groups/12345/export_relations/status")
|
||||
.and_return(http_response)
|
||||
|
||||
stub_request(:get, "http://gitlab.example/api/v4/groups/12345/export_relations/status?page=1&per_page=30&private_token=token")
|
||||
.to_return(status: 200, body: "", headers: {})
|
||||
end
|
||||
|
||||
it 'makes a graphql request using the group full path and an http request with the correct id' do
|
||||
expect(graphql_client).to receive(:parse).with(query_string)
|
||||
expect(graphql_client).to receive(:execute).and_return(graphql_response)
|
||||
|
||||
expect(http_client).to receive(:get).with("/groups/12345/export_relations/status")
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the source_type is a project' do
|
||||
context 'when the source_full_path contains only integer characters' do
|
||||
let(:query_string) { BulkImports::Projects::Graphql::GetProjectQuery.new(context: nil).to_s }
|
||||
let(:graphql_response) do
|
||||
double(original_hash: { 'data' => { 'project' => { 'id' => entity_source_id } } }) # rubocop:disable RSpec/VerifiedDoubles
|
||||
end
|
||||
|
||||
let(:params) do
|
||||
[
|
||||
{
|
||||
source_type: 'project_entity',
|
||||
source_full_path: '67890',
|
||||
destination_slug: 'destination-group-1',
|
||||
destination_namespace: 'destination1'
|
||||
}
|
||||
]
|
||||
end
|
||||
|
||||
before do
|
||||
allow(graphql_client).to receive(:parse).with(query_string)
|
||||
allow(graphql_client).to receive(:execute).and_return(graphql_response)
|
||||
|
||||
allow(http_client).to receive(:get)
|
||||
.with("/projects/12345/export_relations/status")
|
||||
.and_return(http_response)
|
||||
|
||||
stub_request(:get, "http://gitlab.example/api/v4/projects/12345/export_relations/status?page=1&per_page=30&private_token=token")
|
||||
.to_return(status: 200, body: "", headers: {})
|
||||
end
|
||||
|
||||
it 'makes a graphql request using the group full path and an http request with the correct id' do
|
||||
expect(graphql_client).to receive(:parse).with(query_string)
|
||||
expect(graphql_client).to receive(:execute).and_return(graphql_response)
|
||||
|
||||
expect(http_client).to receive(:get).with("/projects/12345/export_relations/status")
|
||||
|
||||
subject.execute
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.validate_destination_full_path' do
|
||||
context 'when the source_type is a group' do
|
||||
context 'when the provided destination_slug already exists in the destination_namespace' do
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ module LoginHelpers
|
|||
visit new_user_session_path
|
||||
expect(page).to have_content('Sign in with')
|
||||
|
||||
check 'remember_me' if remember_me
|
||||
check 'remember_me_omniauth' if remember_me
|
||||
|
||||
click_button "oauth-login-#{provider}"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.shared_examples 'an "Explore" page with sidebar and breadcrumbs' do |page_path, menu_label|
|
||||
before do
|
||||
visit send(page_path)
|
||||
end
|
||||
|
||||
let(:sidebar_css) { 'aside.nav-sidebar[aria-label="Explore"]' }
|
||||
let(:active_menu_item_css) { "li.active[data-track-label=\"#{menu_label}_menu\"]" }
|
||||
|
||||
it 'shows the "Explore" sidebar' do
|
||||
expect(page).to have_css(sidebar_css)
|
||||
end
|
||||
|
||||
it 'shows the correct sidebar menu item as active' do
|
||||
within(sidebar_css) do
|
||||
expect(page).to have_css(active_menu_item_css)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'breadcrumbs' do
|
||||
it 'has "Explore" as its root breadcrumb' do
|
||||
within '.breadcrumbs-list' do
|
||||
expect(page).to have_css("li:first a[href=\"#{explore_root_path}\"]", text: 'Explore')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -107,7 +107,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
|
|||
with_them do
|
||||
before do
|
||||
allow(Kernel).to receive(:system).and_return(true)
|
||||
allow(YAML).to receive(:load_file).and_return({ gitlab_version: Gitlab::VERSION })
|
||||
allow(YAML).to receive(:safe_load_file).and_return({ gitlab_version: Gitlab::VERSION })
|
||||
allow(File).to receive(:delete).with(backup_restore_pid_path).and_return(1)
|
||||
allow(File).to receive(:open).and_call_original
|
||||
allow(File).to receive(:open).with(backup_restore_pid_path, any_args).and_yield(pid_file)
|
||||
|
|
@ -158,7 +158,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
|
|||
|
||||
context 'when restore matches gitlab version' do
|
||||
before do
|
||||
allow(YAML).to receive(:load_file)
|
||||
allow(YAML).to receive(:safe_load_file)
|
||||
.and_return({ gitlab_version: gitlab_version })
|
||||
expect_next_instance_of(::Backup::Manager) do |instance|
|
||||
backup_types.each do |subtask|
|
||||
|
|
@ -212,7 +212,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
|
|||
allow(Kernel).to receive(:system).and_return(true)
|
||||
allow(FileUtils).to receive(:cp_r).and_return(true)
|
||||
allow(FileUtils).to receive(:mv).and_return(true)
|
||||
allow(YAML).to receive(:load_file)
|
||||
allow(YAML).to receive(:safe_load_file)
|
||||
.and_return({ gitlab_version: Gitlab::VERSION })
|
||||
|
||||
expect_next_instance_of(::Backup::Manager) do |instance|
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ require 'logger'
|
|||
module CloudProfilerAgent
|
||||
GoogleCloudProfiler = ::Google::Cloud::Profiler::V2
|
||||
|
||||
# We temporarily removed wall time profiling here, since it does not work properly.
|
||||
# See https://gitlab.com/gitlab-org/gitlab/-/issues/397060
|
||||
PROFILE_TYPES = {
|
||||
CPU: :cpu,
|
||||
WALL: :wall
|
||||
CPU: :cpu
|
||||
}.freeze
|
||||
# This regexp will ensure the service name is valid.
|
||||
# See https://cloud.google.com/ruby/docs/reference/google-cloud-profiler-v2/latest/Google-Cloud-Profiler-V2-Deployment#Google__Cloud__Profiler__V2__Deployment_target_instance_
|
||||
|
|
|
|||
16
yarn.lock
16
yarn.lock
|
|
@ -1122,15 +1122,15 @@
|
|||
stylelint-declaration-strict-value "1.8.0"
|
||||
stylelint-scss "4.2.0"
|
||||
|
||||
"@gitlab/svgs@3.26.0":
|
||||
version "3.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.26.0.tgz#0324301f2aada66c39259ff050a567947db34515"
|
||||
integrity sha512-tidak1UyCsrJ2jylybChNjJNnXUSoQ7rLzxMV2NJ/l2JiDDG7Bh8gn2CL2gk2icWa4Z2/DUfu2EhAAtdJtE0fQ==
|
||||
"@gitlab/svgs@3.28.0":
|
||||
version "3.28.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-3.28.0.tgz#e7f49c5d2144b3a4e8edbe366731f0a054d01ec5"
|
||||
integrity sha512-H4m9jeZEByIp7k2U3aCgM+wNF4I681JEhpUtWsnxaa8vsX2K/maBD8/q7M2hXrTBLAeisGldN12pLfJRRZr/QQ==
|
||||
|
||||
"@gitlab/ui@58.2.0":
|
||||
version "58.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-58.2.0.tgz#3fa8f34ab77783feba0d71abd8db644322796c4c"
|
||||
integrity sha512-LWuNTzLd7+FoZxFt5i3C3/0hVOVIlnZbAZgQ6WwDbTlFn1f/slN7B5L89kleVE1OkgLLoqFuYwRUDPI0eySmvQ==
|
||||
"@gitlab/ui@58.2.1":
|
||||
version "58.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-58.2.1.tgz#5bd4889c2c6e32ba56a8766083c3d34b441e6288"
|
||||
integrity sha512-4OmhjVZhIYL150pCZOK14dT8X9wJKkrVw8L4KlYCJ+iSoodoFt2s1h2eE0zQ4O0zZ1BMN++augb26bWnWo8cqQ==
|
||||
dependencies:
|
||||
"@popperjs/core" "^2.11.2"
|
||||
bootstrap-vue "2.23.1"
|
||||
|
|
|
|||
Loading…
Reference in New Issue