Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
e55fd66eae
commit
157c4d9279
|
@ -2,10 +2,10 @@ import { __ } from '~/locale';
|
|||
|
||||
export const INTRO_COOKIE_KEY = 'dev_ops_report_intro_callout_dismissed';
|
||||
|
||||
export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Report');
|
||||
export const INTRO_BANNER_TITLE = __('Introducing Your DevOps Reports');
|
||||
|
||||
export const INTRO_BANNER_BODY = __(
|
||||
'Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations.',
|
||||
'Your DevOps Reports give an overview of how you are using GitLab from a feature perspective. Use them to view how you compare with other organizations, and how your teams compare against each other.',
|
||||
);
|
||||
|
||||
export const INTRO_BANNER_ACTION_TEXT = __('Read more');
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { sortBy, cloneDeep } from 'lodash';
|
||||
import { isGid } from '~/graphql_shared/utils';
|
||||
import { ListType, MilestoneIDs } from './constants';
|
||||
|
||||
export function getMilestone() {
|
||||
|
@ -95,6 +96,9 @@ export function fullMilestoneId(id) {
|
|||
}
|
||||
|
||||
export function fullLabelId(label) {
|
||||
if (isGid(label.id)) {
|
||||
return label.id;
|
||||
}
|
||||
if (label.project_id && label.project_id !== null) {
|
||||
return `gid://gitlab/ProjectLabel/${label.id}`;
|
||||
}
|
||||
|
|
|
@ -54,6 +54,9 @@ export default {
|
|||
allowLabelEdit: {
|
||||
default: false,
|
||||
},
|
||||
labelsFilterBasePath: {
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
inheritAttrs: false,
|
||||
computed: {
|
||||
|
@ -90,6 +93,11 @@ export default {
|
|||
labelType() {
|
||||
return this.isGroupBoard ? LabelType.group : LabelType.project;
|
||||
},
|
||||
labelsFilterPath() {
|
||||
return this.isGroupBoard
|
||||
? this.labelsFilterBasePath.replace(':project_path', this.projectPathForActiveIssue)
|
||||
: this.labelsFilterBasePath;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions([
|
||||
|
@ -212,7 +220,7 @@ export default {
|
|||
:footer-create-label-title="createLabelTitle"
|
||||
:footer-manage-label-title="manageLabelTitle"
|
||||
:labels-create-title="createLabelTitle"
|
||||
:labels-filter-base-path="projectPathForActiveIssue"
|
||||
:labels-filter-base-path="labelsFilterPath"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
workspace-type="project"
|
||||
:issuable-type="issuableType"
|
||||
|
|
|
@ -57,39 +57,16 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsWebUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
scopedIssueBoardFeatureEnabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
weights: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
enableScopedLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
currentBoard: {
|
||||
type: Object,
|
||||
required: true,
|
||||
|
@ -288,16 +265,7 @@ export default {
|
|||
this.board.iteration_id = iterationId;
|
||||
},
|
||||
setBoardLabels(labels) {
|
||||
labels.forEach((label) => {
|
||||
if (label.set && !this.board.labels.find((l) => l.id === label.id)) {
|
||||
this.board.labels.push({
|
||||
...label,
|
||||
textColor: label.text_color,
|
||||
});
|
||||
} else if (!label.set) {
|
||||
this.board.labels = this.board.labels.filter((selected) => selected.id !== label.id);
|
||||
}
|
||||
});
|
||||
this.board.labels = labels;
|
||||
},
|
||||
setAssignee(assigneeId) {
|
||||
this.$set(this.board, 'assignee', {
|
||||
|
@ -371,11 +339,6 @@ export default {
|
|||
:collapse-scope="isNewForm"
|
||||
:board="board"
|
||||
:can-admin-board="canAdminBoard"
|
||||
:labels-path="labelsPath"
|
||||
:labels-web-url="labelsWebUrl"
|
||||
:enable-scoped-labels="enableScopedLabels"
|
||||
:project-id="projectId"
|
||||
:group-id="groupId"
|
||||
:weights="weights"
|
||||
@set-iteration="setIteration"
|
||||
@set-board-labels="setBoardLabels"
|
||||
|
|
|
@ -64,22 +64,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
labelsPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
labelsWebUrl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
scopedIssueBoardFeatureEnabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
|
@ -88,11 +72,6 @@ export default {
|
|||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
enabledScopedLabels: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -354,14 +333,9 @@ export default {
|
|||
|
||||
<board-form
|
||||
v-if="currentPage"
|
||||
:labels-path="labelsPath"
|
||||
:labels-web-url="labelsWebUrl"
|
||||
:project-id="projectId"
|
||||
:group-id="groupId"
|
||||
:can-admin-board="canAdminBoard"
|
||||
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
|
||||
:weights="weights"
|
||||
:enable-scoped-labels="enabledScopedLabels"
|
||||
:current-board="currentBoard"
|
||||
:current-page="currentPage"
|
||||
@cancel="cancel"
|
||||
|
|
|
@ -142,5 +142,7 @@ export default () => {
|
|||
fullPath: $boardApp.dataset.fullPath,
|
||||
rootPath: $boardApp.dataset.boardsEndpoint,
|
||||
recentBoardsEndpoint: $boardApp.dataset.recentBoardsEndpoint,
|
||||
allowScopedLabels: $boardApp.dataset.scopedLabels,
|
||||
labelsManagePath: $boardApp.dataset.labelsManagePath,
|
||||
});
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ const apolloProvider = new VueApollo({
|
|||
|
||||
export default (params = {}) => {
|
||||
const boardsSwitcherElement = document.getElementById('js-multiple-boards-switcher');
|
||||
const { dataset } = boardsSwitcherElement;
|
||||
return new Vue({
|
||||
el: boardsSwitcherElement,
|
||||
components: {
|
||||
|
@ -24,18 +25,17 @@ export default (params = {}) => {
|
|||
fullPath: params.fullPath,
|
||||
rootPath: params.rootPath,
|
||||
recentBoardsEndpoint: params.recentBoardsEndpoint,
|
||||
allowScopedLabels: params.allowScopedLabels,
|
||||
labelsManagePath: params.labelsManagePath,
|
||||
allowLabelCreate: parseBoolean(dataset.canAdminBoard),
|
||||
},
|
||||
data() {
|
||||
const { dataset } = boardsSwitcherElement;
|
||||
|
||||
const boardsSelectorProps = {
|
||||
...dataset,
|
||||
currentBoard: JSON.parse(dataset.currentBoard),
|
||||
hasMissingBoards: parseBoolean(dataset.hasMissingBoards),
|
||||
canAdminBoard: parseBoolean(dataset.canAdminBoard),
|
||||
multipleIssueBoardsAvailable: parseBoolean(dataset.multipleIssueBoardsAvailable),
|
||||
projectId: dataset.projectId ? Number(dataset.projectId) : 0,
|
||||
groupId: Number(dataset.groupId),
|
||||
scopedIssueBoardFeatureEnabled: parseBoolean(dataset.scopedIssueBoardFeatureEnabled),
|
||||
weights: JSON.parse(dataset.weights),
|
||||
};
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
query usersSearch($search: String!, $fullPath: ID!) {
|
||||
workspace: group(fullPath: $fullPath) {
|
||||
id
|
||||
users: groupMembers(search: $search, relations: [DIRECT, INHERITED]) {
|
||||
nodes {
|
||||
user {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
query searchProjectMembers($fullPath: ID!, $search: String) {
|
||||
project(fullPath: $fullPath) {
|
||||
id
|
||||
projectMembers(search: $search) {
|
||||
nodes {
|
||||
user {
|
||||
|
|
|
@ -37,6 +37,7 @@ import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widge
|
|||
import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
|
||||
import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
|
||||
import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
|
||||
import mergeRequestLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql';
|
||||
import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
|
||||
import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
|
||||
import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
|
||||
|
@ -128,6 +129,7 @@ export const issuableLabelsQueries = {
|
|||
mutationName: 'updateIssue',
|
||||
},
|
||||
[IssuableType.MergeRequest]: {
|
||||
issuableQuery: mergeRequestLabelsQuery,
|
||||
mutation: updateMergeRequestLabelsMutation,
|
||||
mutationName: 'mergeRequestSetLabels',
|
||||
},
|
||||
|
|
|
@ -260,6 +260,10 @@ export function mountSidebarLabels() {
|
|||
variant: DropdownVariant.Sidebar,
|
||||
canUpdate: parseBoolean(el.dataset.canEdit),
|
||||
isClassicSidebar: true,
|
||||
issuableType:
|
||||
isInIssuePage() || isInIncidentPage() || isInDesignPage()
|
||||
? IssuableType.Issue
|
||||
: IssuableType.MergeRequest,
|
||||
},
|
||||
render: (createElement) => createElement(SidebarLabels),
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ mutation mergeRequestSetLabels($input: MergeRequestSetLabelsInput!) {
|
|||
mergeRequestSetLabels(input: $input) {
|
||||
errors
|
||||
mergeRequest {
|
||||
id
|
||||
labels {
|
||||
nodes {
|
||||
color
|
||||
|
|
|
@ -45,7 +45,7 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
selected: {
|
||||
type: Object,
|
||||
type: [Object, Array],
|
||||
required: false,
|
||||
default: () => {},
|
||||
},
|
||||
|
@ -54,6 +54,11 @@ export default {
|
|||
required: false,
|
||||
default: '',
|
||||
},
|
||||
allowMultiselect: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isSearchEmpty() {
|
||||
|
@ -66,8 +71,14 @@ export default {
|
|||
methods: {
|
||||
selectOption(option) {
|
||||
this.$emit('set-option', option || null);
|
||||
if (!this.allowMultiselect) {
|
||||
this.$refs.dropdown.hide();
|
||||
}
|
||||
},
|
||||
isSelected(option) {
|
||||
if (Array.isArray(this.selected)) {
|
||||
return this.selected.some((label) => label.title === option.title);
|
||||
}
|
||||
return (
|
||||
this.selected &&
|
||||
((option.name && this.selected.name === option.name) ||
|
||||
|
@ -78,7 +89,7 @@ export default {
|
|||
this.$refs.dropdown.show();
|
||||
},
|
||||
setFocus() {
|
||||
this.$refs.search.focusInput();
|
||||
this.$refs.search?.focusInput();
|
||||
},
|
||||
setSearchTerm(search) {
|
||||
this.$emit('set-search', search);
|
||||
|
@ -108,56 +119,60 @@ export default {
|
|||
@shown="setFocus"
|
||||
>
|
||||
<template #header>
|
||||
<gl-search-box-by-type
|
||||
ref="search"
|
||||
:value="searchTerm"
|
||||
:placeholder="searchText"
|
||||
class="js-dropdown-input-field"
|
||||
@input="setSearchTerm"
|
||||
/>
|
||||
<slot name="header">
|
||||
<gl-search-box-by-type
|
||||
ref="search"
|
||||
:value="searchTerm"
|
||||
:placeholder="searchText"
|
||||
class="js-dropdown-input-field"
|
||||
@input="setSearchTerm"
|
||||
/>
|
||||
</slot>
|
||||
</template>
|
||||
<gl-dropdown-form class="gl-relative gl-min-h-7">
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
size="md"
|
||||
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
|
||||
/>
|
||||
<template v-else>
|
||||
<template v-if="isSearchEmpty && presetOptions.length > 0">
|
||||
<slot name="default">
|
||||
<gl-dropdown-form class="gl-relative gl-min-h-7">
|
||||
<gl-loading-icon
|
||||
v-if="isLoading"
|
||||
size="md"
|
||||
class="gl-absolute gl-left-0 gl-top-0 gl-right-0"
|
||||
/>
|
||||
<template v-else>
|
||||
<template v-if="isSearchEmpty && presetOptions.length > 0">
|
||||
<gl-dropdown-item
|
||||
v-for="option in presetOptions"
|
||||
:key="option.id"
|
||||
:is-checked="isSelected(option)"
|
||||
:is-check-centered="true"
|
||||
:is-check-item="true"
|
||||
@click.native.capture.stop="selectOption(option)"
|
||||
>
|
||||
<slot name="preset-item" :item="option">
|
||||
{{ option.title }}
|
||||
</slot>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
v-for="option in presetOptions"
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:is-checked="isSelected(option)"
|
||||
:is-check-centered="true"
|
||||
:is-check-item="true"
|
||||
@click="selectOption(option)"
|
||||
:avatar-url="avatarUrl(option)"
|
||||
:secondary-text="secondaryText(option)"
|
||||
data-testid="unselected-option"
|
||||
@click.native.capture.stop="selectOption(option)"
|
||||
>
|
||||
<slot name="preset-item" :item="option">
|
||||
<slot name="item" :item="option">
|
||||
{{ option.title }}
|
||||
</slot>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-divider />
|
||||
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
|
||||
{{ $options.i18n.noMatchingResults }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
<gl-dropdown-item
|
||||
v-for="option in options"
|
||||
:key="option.id"
|
||||
:is-checked="isSelected(option)"
|
||||
:is-check-centered="true"
|
||||
:is-check-item="true"
|
||||
:avatar-url="avatarUrl(option)"
|
||||
:secondary-text="secondaryText(option)"
|
||||
data-testid="unselected-option"
|
||||
@click="selectOption(option)"
|
||||
>
|
||||
<slot name="item" :item="option">
|
||||
{{ option.title }}
|
||||
</slot>
|
||||
</gl-dropdown-item>
|
||||
<gl-dropdown-item v-if="noOptionsFound" class="gl-pl-6!">
|
||||
{{ $options.i18n.noMatchingResults }}
|
||||
</gl-dropdown-item>
|
||||
</template>
|
||||
</gl-dropdown-form>
|
||||
</gl-dropdown-form>
|
||||
</slot>
|
||||
<template #footer>
|
||||
<slot name="footer"></slot>
|
||||
</template>
|
||||
|
|
|
@ -53,10 +53,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
isVisible: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
@ -209,7 +205,6 @@ export default {
|
|||
v-model="localSelectedLabels"
|
||||
:search-key="searchKey"
|
||||
:allow-multiselect="allowMultiselect"
|
||||
:issuable-type="issuableType"
|
||||
:full-path="fullPath"
|
||||
:workspace-type="workspaceType"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
|
|
|
@ -32,11 +32,6 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined,
|
||||
},
|
||||
workspaceType: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
|
|
@ -23,10 +23,6 @@ export default {
|
|||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
issuableType: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
localSelectedLabels: {
|
||||
type: Array,
|
||||
required: true,
|
||||
|
@ -119,13 +115,7 @@ export default {
|
|||
({ id }) => id !== getIdFromGraphQLId(label.id) && id !== label.id,
|
||||
);
|
||||
} else {
|
||||
labels = [
|
||||
...this.localSelectedLabels,
|
||||
{
|
||||
...label,
|
||||
id: getIdFromGraphQLId(label.id),
|
||||
},
|
||||
];
|
||||
labels = [...this.localSelectedLabels, label];
|
||||
}
|
||||
this.$emit('input', labels);
|
||||
},
|
||||
|
|
|
@ -39,7 +39,7 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
focusInput() {
|
||||
this.$refs.searchInput.focusInput();
|
||||
this.$refs.searchInput?.focusInput();
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<script>
|
||||
import { GlLabel } from '@gitlab/ui';
|
||||
import { sortBy } from 'lodash';
|
||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||
import { isScopedLabel } from '~/lib/utils/common_utils';
|
||||
|
||||
export default {
|
||||
|
@ -47,7 +46,7 @@ export default {
|
|||
return this.allowScopedLabels && isScopedLabel(label);
|
||||
},
|
||||
removeLabel(labelId) {
|
||||
this.$emit('onLabelRemove', getIdFromGraphQLId(labelId));
|
||||
this.$emit('onLabelRemove', labelId);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
|
||||
query groupLabels($fullPath: ID!, $searchTerm: String) {
|
||||
workspace: group(fullPath: $fullPath) {
|
||||
labels(searchTerm: $searchTerm, onlyGroupLabels: true) {
|
||||
id
|
||||
labels(searchTerm: $searchTerm, onlyGroupLabels: true, includeAncestorGroups: true) {
|
||||
nodes {
|
||||
...Label
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
#import "~/graphql_shared/fragments/label.fragment.graphql"
|
||||
|
||||
query mergeRequestLabels($fullPath: ID!, $iid: String!) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
issuable: mergeRequest(iid: $iid) {
|
||||
id
|
||||
labels {
|
||||
nodes {
|
||||
...Label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
query projectLabels($fullPath: ID!, $searchTerm: String) {
|
||||
workspace: project(fullPath: $fullPath) {
|
||||
id
|
||||
labels(searchTerm: $searchTerm, includeAncestorGroups: true) {
|
||||
nodes {
|
||||
...Label
|
||||
|
|
|
@ -207,7 +207,7 @@ export default {
|
|||
return {
|
||||
iid: currentIid,
|
||||
groupPath: this.fullPath,
|
||||
addLabelIds: labelIds,
|
||||
addLabelIds: labelIds.map((id) => getIdFromGraphQLId(id)),
|
||||
removeLabelIds: this.issuableLabelIds
|
||||
.filter((id) => !labelIds.includes(id))
|
||||
.map((id) => getIdFromGraphQLId(id)),
|
||||
|
@ -232,8 +232,8 @@ export default {
|
|||
}
|
||||
|
||||
this.$emit('updateSelectedLabels', {
|
||||
id: data[mutationName]?.[this.issuableType].id,
|
||||
labels: data[mutationName]?.[this.issuableType].labels?.nodes,
|
||||
id: data[mutationName]?.[this.issuableType]?.id,
|
||||
labels: data[mutationName]?.[this.issuableType]?.labels?.nodes,
|
||||
});
|
||||
})
|
||||
.catch((error) =>
|
||||
|
@ -268,7 +268,7 @@ export default {
|
|||
case IssuableType.Epic:
|
||||
return {
|
||||
iid: this.iid,
|
||||
removeLabelIds: [labelId],
|
||||
removeLabelIds: [getIdFromGraphQLId(labelId)],
|
||||
groupPath: this.fullPath,
|
||||
};
|
||||
default:
|
||||
|
@ -341,7 +341,6 @@ export default {
|
|||
:labels-create-title="labelsCreateTitle"
|
||||
:selected-labels="issuableLabels"
|
||||
:variant="variant"
|
||||
:issuable-type="issuableType"
|
||||
:is-visible="edit"
|
||||
:full-path="fullPath"
|
||||
:workspace-type="workspaceType"
|
||||
|
@ -364,7 +363,6 @@ export default {
|
|||
:labels-create-title="labelsCreateTitle"
|
||||
:selected-labels="issuableLabels"
|
||||
:variant="variant"
|
||||
:issuable-type="issuableType"
|
||||
:full-path="fullPath"
|
||||
:workspace-type="workspaceType"
|
||||
:attr-workspace-path="attrWorkspacePath"
|
||||
|
|
|
@ -470,6 +470,10 @@
|
|||
.labels-select-wrapper.is-embedded .labels-select-wrapper.is-embedded {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.show.dropdown .dropdown-menu {
|
||||
@include gl-w-full;
|
||||
}
|
||||
}
|
||||
|
||||
.board-header-collapsed-info-icon:hover {
|
||||
|
|
|
@ -42,6 +42,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:mr_attention_requests, project, default_enabled: :yaml)
|
||||
push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
|
||||
|
||||
# Usage data feature flags
|
||||
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
|
||||
|
|
|
@ -30,7 +30,7 @@ module Autocomplete
|
|||
|
||||
class NamespacesOnly < self
|
||||
def routables
|
||||
return Namespace.all if current_user.admin?
|
||||
return Namespace.without_project_namespaces if current_user.admin?
|
||||
|
||||
current_user.namespaces
|
||||
end
|
||||
|
|
|
@ -43,7 +43,7 @@ module BoardsHelper
|
|||
|
||||
def build_issue_link_base
|
||||
if board.group_board?
|
||||
"#{group_path(@board.group)}/:project_path/issues"
|
||||
"/:project_path/-/issues"
|
||||
else
|
||||
project_issues_path(@project)
|
||||
end
|
||||
|
|
|
@ -41,9 +41,11 @@ module LoadedInGroupList
|
|||
namespaces = Namespace.arel_table
|
||||
children = namespaces.alias('children')
|
||||
|
||||
# TODO 6473: remove the filtering of the Namespaces::ProjectNamespace see https://gitlab.com/groups/gitlab-org/-/epics/6473
|
||||
namespaces.project(Arel.star.count.as('preloaded_subgroup_count'))
|
||||
.from(children)
|
||||
.where(children[:parent_id].eq(namespaces[:id]))
|
||||
.where(children[:type].is_distinct_from(Namespaces::ProjectNamespace.sti_name))
|
||||
end
|
||||
|
||||
def member_count_sql
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
- page_title _('DevOps Report')
|
||||
- page_title _('DevOps Reports')
|
||||
- add_page_specific_style 'page_bundles/dev_ops_report'
|
||||
|
||||
.container
|
||||
|
|
|
@ -66,9 +66,9 @@
|
|||
= _('Analytics')
|
||||
%li.divider.fly-out-top-item
|
||||
= nav_link(controller: :dev_ops_report) do
|
||||
= link_to admin_dev_ops_report_path, title: _('DevOps Report') do
|
||||
= link_to admin_dev_ops_report_path, title: _('DevOps Reports') do
|
||||
%span
|
||||
= _('DevOps Report')
|
||||
= _('DevOps Reports')
|
||||
= nav_link(controller: :usage_trends) do
|
||||
= link_to admin_usage_trends_path, title: _('Usage Trends') do
|
||||
%span
|
||||
|
|
|
@ -9,9 +9,5 @@
|
|||
has_missing_boards: (!multiple_boards_available? && current_board_parent.boards.size > 1).to_s,
|
||||
can_admin_board: can?(current_user, :admin_issue_board, parent).to_s,
|
||||
multiple_issue_boards_available: parent.multiple_issue_boards_available?.to_s,
|
||||
labels_path: labels_filter_path_with_defaults(only_group_labels: true, include_descendant_groups: true),
|
||||
labels_web_url: parent.is_a?(Project) ? project_labels_path(@project) : group_labels_path(@group),
|
||||
project_id: @project&.id,
|
||||
group_id: @group&.id,
|
||||
scoped_issue_board_feature_enabled: Gitlab.ee? && parent.feature_available?(:scoped_issue_board) ? 'true' : 'false',
|
||||
weights: weights.to_json } }
|
||||
|
|
|
@ -131,12 +131,13 @@ and we recommend you use:
|
|||
|
||||
The following table lists basic ports that must be open between the **primary** and **secondary** sites for Geo.
|
||||
|
||||
| **Primary** site | **Secondary** site | Protocol |
|
||||
|:-----------------|:-------------------|:-------------|
|
||||
| 80 | 80 | HTTP |
|
||||
| 443 | 443 | TCP or HTTPS |
|
||||
| 22 | 22 | TCP |
|
||||
| 5432 | | PostgreSQL |
|
||||
| Source site | Source port | Destination site | Destination port | Protocol |
|
||||
|-------------|-------------|------------------|------------------|-------------|
|
||||
| Primary | Any | Secondary | 80 | TCP (HTTP) |
|
||||
| Primary | Any | Secondary | 443 | TCP (HTTPS) |
|
||||
| Secondary | Any | Primary | 80 | TCP (HTTP) |
|
||||
| Secondary | Any | Primary | 443 | TCP (HTTPS) |
|
||||
| Secondary | Any | Primary | 5432 | TCP |
|
||||
|
||||
See the full list of ports used by GitLab in [Package defaults](../package_information/defaults.md)
|
||||
|
||||
|
|
|
@ -25,3 +25,5 @@ To enable the Puma exporter:
|
|||
for the changes to take effect.
|
||||
|
||||
Prometheus begins collecting performance data from the Puma exporter exposed at `localhost:8083`.
|
||||
|
||||
For more information on using Puma with GitLab, see [Puma](../../operations/puma.md).
|
||||
|
|
|
@ -76,7 +76,13 @@ Mattermost. See the [full table for a complete list](#storage-specific-configura
|
|||
However, backups can be configured with [server side encryption](../raketasks/backup_restore.md#s3-encrypted-buckets) separately.
|
||||
|
||||
Enabling consolidated object storage enables object storage for all object
|
||||
types. If you want to use local storage for specific object types, you can
|
||||
types. If not all buckets are specified, `sudo gitlab-ctl reconfigure` may fail with the error like:
|
||||
|
||||
```plaintext
|
||||
Object storage for <object type> must have a bucket specified
|
||||
```
|
||||
|
||||
If you want to use local storage for specific object types, you can
|
||||
[selectively disable object storages](#selectively-disabling-object-storage).
|
||||
|
||||
Most types of objects, such as CI artifacts, LFS files, upload
|
||||
|
|
|
@ -224,3 +224,8 @@ in Puma when using the Linux package, and which ones have no corresponding count
|
|||
| `unicorn['exporter_enabled']` | `puma['exporter_enabled']` |
|
||||
| `unicorn['exporter_address']` | `puma['exporter_address']` |
|
||||
| `unicorn['exporter_port']` | `puma['exporter_port']` |
|
||||
|
||||
## Puma exporter
|
||||
|
||||
You can use the Puma exporter to measure various Puma metrics. For more information, see
|
||||
[Puma exporter](../monitoring/prometheus/puma_exporter.md).
|
||||
|
|
|
@ -168,7 +168,7 @@ their color is `#428BCA`.
|
|||
`<Category Name>` is the category name as it is in the single source of truth for categories at
|
||||
<https://gitlab.com/gitlab-com/www-gitlab-com/blob/master/data/categories.yml>.
|
||||
|
||||
For instance, the "DevOps Report" category is represented by the
|
||||
For instance, the "DevOps Reports" category is represented by the
|
||||
~"Category:DevOps Reports" label in the `gitlab-org` group since its
|
||||
`devops_reports.name` value is "DevOps Reports".
|
||||
|
||||
|
|
|
@ -55,7 +55,7 @@ We use the following terminology to describe the Service Ping components:
|
|||
|
||||
- The main purpose of Service Ping is to build a better GitLab. Data about how GitLab is used is collected to better understand feature/stage adoption and usage, which helps us understand how GitLab is adding value and helps our team better understand the reasons why people use GitLab and with this knowledge we're able to make better product decisions.
|
||||
- As a benefit of having Service Ping active, GitLab lets you analyze the users' activities over time of your GitLab installation.
|
||||
- As a benefit of having Service Ping active, GitLab provides you with The DevOps Report,which gives you an overview of your entire instance's adoption of Concurrent DevOps from planning to monitoring.
|
||||
- As a benefit of having Service Ping active, GitLab provides you with [DevOps Score](../../user/admin_area/analytics/dev_ops_report.md#devops-score), which gives you an overview of your entire instance's adoption of Concurrent DevOps from planning to monitoring.
|
||||
- You get better, more proactive support. (assuming that our TAMs and support organization used the data to deliver more value)
|
||||
- You get insight and advice into how to get the most value out of your investment in GitLab. Wouldn't you want to know that a number of features or values are not being adopted in your organization?
|
||||
- You get a report that illustrates how you compare against other similar organizations (anonymized), with specific advice and recommendations on how to improve your DevOps processes.
|
||||
|
@ -186,7 +186,7 @@ sequenceDiagram
|
|||
S3 Bucket->>Snowflake DW: Import data
|
||||
Snowflake DW->>Snowflake DW: Transform data using dbt
|
||||
Snowflake DW->>Sisense Dashboards: Data available for querying
|
||||
Versions Application->>GitLab Instance: DevOps Report (Conversational Development Index)
|
||||
Versions Application->>GitLab Instance: DevOps Score (Conversational Development Index)
|
||||
```
|
||||
|
||||
## How Service Ping works
|
||||
|
|
|
@ -4,21 +4,21 @@ group: Optimize
|
|||
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
|
||||
---
|
||||
|
||||
# DevOps Report **(FREE SELF)**
|
||||
# DevOps Reports **(FREE SELF)**
|
||||
|
||||
> [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/20976) from Conversational Development Index in GitLab 12.6.
|
||||
|
||||
The DevOps Report gives you an overview of your entire instance's adoption of
|
||||
DevOps Reports give you an overview of your entire instance's adoption of
|
||||
[Concurrent DevOps](https://about.gitlab.com/topics/concurrent-devops/)
|
||||
from planning to monitoring.
|
||||
|
||||
To see DevOps Report:
|
||||
To see DevOps Reports:
|
||||
|
||||
1. On the top bar, select **Menu > Admin**.
|
||||
1. On the left sidebar, select **Analytics > DevOps Report**.
|
||||
1. On the left sidebar, select **Analytics > DevOps Reports**.
|
||||
|
||||
## DevOps Score
|
||||
|
||||
> [Renamed](https://gitlab.com/gitlab-org/gitlab/-/issues/20976) from Conversational Development Index in GitLab 12.6.
|
||||
|
||||
NOTE:
|
||||
To see the DevOps score, you must activate your GitLab instance's [Service Ping](../settings/usage_statistics.md#service-ping). This is because DevOps Score is a comparative tool, so your score data must be centrally processed by GitLab Inc. first.
|
||||
|
||||
|
@ -72,4 +72,4 @@ DevOps Adoption allows you to:
|
|||
- Identify specific groups that are lagging in their adoption of GitLab, so you can help them along in their DevOps journey.
|
||||
- Find the groups that have adopted certain features, and can provide guidance to other groups on how to use those features.
|
||||
|
||||
![DevOps Report](img/admin_devops_adoption_v14_2.png)
|
||||
![DevOps Adoption](img/admin_devops_adoption_v14_2.png)
|
||||
|
|
|
@ -15,5 +15,5 @@ Administrators have access to instance-wide analytics:
|
|||
|
||||
There are several kinds of statistics:
|
||||
|
||||
- [DevOps Report](dev_ops_report.md): Provides an overview of your entire instance's feature usage.
|
||||
- [DevOps Reports](dev_ops_report.md): Provides an overview of your entire instance's feature usage.
|
||||
- [Usage Trends](usage_trends.md): Shows how much data your instance contains, and how that is changing.
|
||||
|
|
|
@ -48,7 +48,7 @@ With DevOps Adoption you can:
|
|||
- Identify specific subgroups that are lagging in their adoption of GitLab, so you can help them along in their DevOps journey.
|
||||
- Find the subgroups that have adopted certain features, and can provide guidance to other subgroups on how to use those features.
|
||||
|
||||
![DevOps Report](img/group_devops_adoption_v14_2.png)
|
||||
![DevOps Adoption](img/group_devops_adoption_v14_2.png)
|
||||
|
||||
Feature adoption is based on usage in the previous calendar month. Data is updated on the first day
|
||||
of each month. If the monthly update fails, it tries again daily until successful.
|
||||
|
|
|
@ -16,15 +16,13 @@ Use a `.gitlab-ci.yml` template when you have an existing project that you want
|
|||
Your GitLab repository should contain files specific to an SSG, or plain HTML. After you complete
|
||||
these steps, you may have to do additional configuration for the Pages site to generate properly.
|
||||
|
||||
1. On the left sidebar, select **Project information**.
|
||||
1. Click **Set up CI/CD**. If this button is not available, CI/CD is already configured for your project.
|
||||
You may want to browse the `.gitlab-ci.yml` files [in these projects instead](https://gitlab.com/pages).
|
||||
1. From the **Apply a template** list, choose a template for the SSG you're using.
|
||||
You can also choose plain HTML. If you don't find a corresponding template, you can view the
|
||||
[GitLab Pages group of sample projects](https://gitlab.com/pages).
|
||||
These projects contain `.gitlab-ci.yml` files that you can modify for your needs.
|
||||
You can also [learn how to write your own `.gitlab-ci.yml` file for GitLab Pages](pages_from_scratch.md).
|
||||
1. Save and commit the `.gitlab-ci.yml` file.
|
||||
1. On the top bar, select **Menu > Projects** and find your project.
|
||||
1. On the left sidebar, select the project's name.
|
||||
1. From the **Add** (**{plus}**) dropdown, select **New file**.
|
||||
1. From the **Select a template type** dropdown, select `.gitlab-ci.yml`.
|
||||
1. From the **Apply a template** dropdown, in the **Pages** section, select the name of your SSG.
|
||||
1. In the **Commit message** box, type the commit message.
|
||||
1. Select **Commit changes**.
|
||||
|
||||
If everything is configured correctly, the site can take approximately 30 minutes to deploy.
|
||||
|
||||
|
|
|
@ -5491,6 +5491,9 @@ msgstr ""
|
|||
msgid "BoardScope|An error occurred while getting milestones, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|An error occurred while searching for labels, please try again."
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|An error occurred while searching for users, please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -5500,12 +5503,21 @@ msgstr ""
|
|||
msgid "BoardScope|Any assignee"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Any label"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Assignee"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Choose labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Edit"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Milestone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -5518,6 +5530,9 @@ msgstr ""
|
|||
msgid "BoardScope|Select assignee"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Select labels"
|
||||
msgstr ""
|
||||
|
||||
msgid "BoardScope|Select milestone"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11765,7 +11780,7 @@ msgstr ""
|
|||
msgid "DevOps Adoption"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevOps Report"
|
||||
msgid "DevOps Reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "DevOps adoption"
|
||||
|
@ -18629,7 +18644,7 @@ msgstr ""
|
|||
msgid "Interval Pattern"
|
||||
msgstr ""
|
||||
|
||||
msgid "Introducing Your DevOps Report"
|
||||
msgid "Introducing Your DevOps Reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid Insights config file detected"
|
||||
|
@ -39603,7 +39618,7 @@ msgstr ""
|
|||
msgid "Your CSV import for project"
|
||||
msgstr ""
|
||||
|
||||
msgid "Your DevOps Report gives an overview of how you are using GitLab from a feature perspective. Use it to view how you compare with other organizations."
|
||||
msgid "Your DevOps Reports give an overview of how you are using GitLab from a feature perspective. Use them to view how you compare with other organizations, and how your teams compare against each other."
|
||||
msgstr ""
|
||||
|
||||
msgid "Your GPG keys (%{count})"
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'spec_helper'
|
||||
|
||||
RSpec.describe Autocomplete::RoutesFinder do
|
||||
describe '#execute' do
|
||||
let_it_be(:user) { create(:user, username: 'user_path') }
|
||||
let_it_be(:admin) { create(:admin) }
|
||||
let_it_be(:group) { create(:group, path: 'path1') }
|
||||
let_it_be(:group2) { create(:group, path: 'path2') }
|
||||
let_it_be(:group3) { create(:group, path: 'not-matching') }
|
||||
let_it_be(:project) { create(:project, path: 'path3', namespace: user.namespace) }
|
||||
let_it_be(:project2) { create(:project, path: 'path4') }
|
||||
let_it_be(:project_namespace) { create(:project_namespace, parent: group, path: 'path5') }
|
||||
|
||||
let(:current_user) { user }
|
||||
let(:search) { 'path' }
|
||||
|
||||
before do
|
||||
group.add_owner(user)
|
||||
end
|
||||
|
||||
context 'for NamespacesOnly' do
|
||||
subject { Autocomplete::RoutesFinder::NamespacesOnly.new(current_user, search: search).execute }
|
||||
|
||||
let(:user_route) { Route.find_by_path(user.username) }
|
||||
|
||||
it 'finds only user namespace and groups matching the search excluding project namespaces' do
|
||||
is_expected.to match_array([group.route, user_route])
|
||||
end
|
||||
|
||||
context 'when user is admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it 'finds all namespaces matching the search excluding project namespaces' do
|
||||
is_expected.to match_array([group.route, group2.route, user_route])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'for ProjectsOnly' do
|
||||
subject { Autocomplete::RoutesFinder::ProjectsOnly.new(current_user, search: 'path').execute }
|
||||
|
||||
it 'finds only matching projects the user has access to' do
|
||||
is_expected.to match_array([project.route])
|
||||
end
|
||||
|
||||
context 'when user is admin' do
|
||||
let(:current_user) { admin }
|
||||
|
||||
it 'finds all projects matching the search' do
|
||||
is_expected.to match_array([project.route, project2.route])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,6 @@
|
|||
import { GlModal } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import setWindowLocation from 'helpers/set_window_location_helper';
|
||||
import { TEST_HOST } from 'helpers/test_constants';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
|
||||
import BoardForm from '~/boards/components/board_form.vue';
|
||||
|
@ -31,8 +30,6 @@ const currentBoard = {
|
|||
|
||||
const defaultProps = {
|
||||
canAdminBoard: false,
|
||||
labelsPath: `${TEST_HOST}/labels/path`,
|
||||
labelsWebUrl: `${TEST_HOST}/-/labels`,
|
||||
currentBoard,
|
||||
currentPage: '',
|
||||
};
|
||||
|
|
|
@ -78,10 +78,6 @@ describe('BoardsSelector', () => {
|
|||
hasMissingBoards: false,
|
||||
canAdminBoard: true,
|
||||
multipleIssueBoardsAvailable: true,
|
||||
labelsPath: `${TEST_HOST}/labels/path`,
|
||||
labelsWebUrl: `${TEST_HOST}/labels`,
|
||||
projectId: 42,
|
||||
groupId: 19,
|
||||
scopedIssueBoardFeatureEnabled: true,
|
||||
weights: [],
|
||||
},
|
||||
|
|
|
@ -12,6 +12,7 @@ export const boardObj = {
|
|||
id: 1,
|
||||
name: 'test',
|
||||
milestone_id: null,
|
||||
labels: [],
|
||||
};
|
||||
|
||||
export const listObj = {
|
||||
|
@ -609,3 +610,43 @@ export const mockTokens = (fetchLabels, fetchAuthors, fetchMilestones) => [
|
|||
unique: true,
|
||||
},
|
||||
];
|
||||
|
||||
export const mockLabel1 = {
|
||||
id: 'gid://gitlab/GroupLabel/121',
|
||||
title: 'To Do',
|
||||
color: '#F0AD4E',
|
||||
textColor: '#FFFFFF',
|
||||
description: null,
|
||||
};
|
||||
|
||||
export const mockLabel2 = {
|
||||
id: 'gid://gitlab/GroupLabel/122',
|
||||
title: 'Doing',
|
||||
color: '#F0AD4E',
|
||||
textColor: '#FFFFFF',
|
||||
description: null,
|
||||
};
|
||||
|
||||
export const mockProjectLabelsResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/1',
|
||||
labels: {
|
||||
nodes: [mockLabel1, mockLabel2],
|
||||
},
|
||||
},
|
||||
__typename: 'Project',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGroupLabelsResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Group/1',
|
||||
labels: {
|
||||
nodes: [mockLabel1, mockLabel2],
|
||||
},
|
||||
},
|
||||
__typename: 'Group',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -34,6 +34,7 @@ describe('DropdownWidget component', () => {
|
|||
// invokes `show` method of BDropdown used inside GlDropdown.
|
||||
// Context: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54895#note_524281679
|
||||
jest.spyOn(wrapper.vm, 'showDropdown').mockImplementation();
|
||||
jest.spyOn(findDropdown().vm, 'hide').mockImplementation();
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -67,10 +68,7 @@ describe('DropdownWidget component', () => {
|
|||
});
|
||||
|
||||
it('emits set-option event when clicking on an option', async () => {
|
||||
wrapper
|
||||
.findAll('[data-testid="unselected-option"]')
|
||||
.at(1)
|
||||
.vm.$emit('click', new Event('click'));
|
||||
wrapper.findAll('[data-testid="unselected-option"]').at(1).trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.emitted('set-option')).toEqual([[wrapper.props().options[1]]]);
|
||||
|
|
|
@ -10,7 +10,6 @@ import VueApollo from 'vue-apollo';
|
|||
import createMockApollo from 'helpers/mock_apollo_helper';
|
||||
import waitForPromises from 'helpers/wait_for_promises';
|
||||
import createFlash from '~/flash';
|
||||
import { IssuableType } from '~/issue_show/constants';
|
||||
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
|
||||
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
|
||||
import DropdownContentsLabelsView from '~/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue';
|
||||
|
@ -57,7 +56,6 @@ describe('DropdownContentsLabelsView', () => {
|
|||
propsData: {
|
||||
...initialState,
|
||||
localSelectedLabels,
|
||||
issuableType: IssuableType.Issue,
|
||||
searchKey,
|
||||
labelCreateType: 'project',
|
||||
workspaceType: 'project',
|
||||
|
|
|
@ -39,7 +39,6 @@ describe('DropdownContent', () => {
|
|||
footerManageLabelTitle: 'manage',
|
||||
dropdownButtonText: 'Labels',
|
||||
variant: 'sidebar',
|
||||
issuableType: 'issue',
|
||||
fullPath: 'test',
|
||||
workspaceType: 'project',
|
||||
labelCreateType: 'project',
|
||||
|
|
|
@ -92,6 +92,7 @@ export const createLabelSuccessfulResponse = {
|
|||
export const workspaceLabelsQueryResponse = {
|
||||
data: {
|
||||
workspace: {
|
||||
id: 'gid://gitlab/Project/126',
|
||||
labels: {
|
||||
nodes: [
|
||||
{
|
||||
|
|
|
@ -23,14 +23,14 @@ RSpec.describe BoardsHelper do
|
|||
it 'returns correct path for base group' do
|
||||
assign(:board, group_board)
|
||||
|
||||
expect(helper.build_issue_link_base).to eq('/base/:project_path/issues')
|
||||
expect(helper.build_issue_link_base).to eq('/:project_path/-/issues')
|
||||
end
|
||||
|
||||
it 'returns correct path for subgroup' do
|
||||
subgroup = create(:group, parent: base_group, path: 'sub')
|
||||
assign(:board, create(:board, group: subgroup))
|
||||
|
||||
expect(helper.build_issue_link_base).to eq('/base/sub/:project_path/issues')
|
||||
expect(helper.build_issue_link_base).to eq('/:project_path/-/issues')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -149,7 +149,7 @@ RSpec.describe BoardsHelper do
|
|||
end
|
||||
|
||||
it 'returns correct path for base group' do
|
||||
expect(helper.build_issue_link_base).to eq("/#{base_group.full_path}/:project_path/issues")
|
||||
expect(helper.build_issue_link_base).to eq("/:project_path/-/issues")
|
||||
end
|
||||
|
||||
it 'returns required label endpoints' do
|
||||
|
|
|
@ -3,49 +3,67 @@
|
|||
require 'spec_helper'
|
||||
|
||||
RSpec.describe LoadedInGroupList do
|
||||
let(:parent) { create(:group) }
|
||||
let_it_be(:parent) { create(:group) }
|
||||
let_it_be(:group) { create(:group, parent: parent) }
|
||||
let_it_be(:project) { create(:project, namespace: parent) }
|
||||
|
||||
subject(:found_group) { Group.with_selects_for_list.find_by(id: parent.id) }
|
||||
let(:archived_parameter) { nil }
|
||||
|
||||
before do
|
||||
parent.add_developer(create(:user))
|
||||
end
|
||||
|
||||
subject(:found_group) { Group.with_selects_for_list(archived: archived_parameter).find_by(id: parent.id) }
|
||||
|
||||
describe '.with_selects_for_list' do
|
||||
it 'includes the preloaded counts for groups' do
|
||||
create(:group, parent: parent)
|
||||
create(:project, namespace: parent)
|
||||
parent.add_developer(create(:user))
|
||||
|
||||
found_group = Group.with_selects_for_list.find_by(id: parent.id)
|
||||
|
||||
expect(found_group.preloaded_project_count).to eq(1)
|
||||
expect(found_group.preloaded_subgroup_count).to eq(1)
|
||||
expect(found_group.preloaded_member_count).to eq(1)
|
||||
end
|
||||
|
||||
context 'with project namespaces' do
|
||||
let_it_be(:group1) { create(:group, parent: parent) }
|
||||
let_it_be(:group2) { create(:group, parent: parent) }
|
||||
let_it_be(:project_namespace) { create(:project_namespace, project: project) }
|
||||
|
||||
it 'does not include project_namespaces in the count of subgroups' do
|
||||
expect(found_group.preloaded_subgroup_count).to eq(3)
|
||||
expect(parent.subgroup_count).to eq(3)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with archived projects' do
|
||||
let_it_be(:archived_project) { create(:project, namespace: parent, archived: true) }
|
||||
|
||||
let(:archived_parameter) { true }
|
||||
|
||||
it 'counts including archived projects when `true` is passed' do
|
||||
create(:project, namespace: parent, archived: true)
|
||||
create(:project, namespace: parent)
|
||||
|
||||
found_group = Group.with_selects_for_list(archived: 'true').find_by(id: parent.id)
|
||||
|
||||
expect(found_group.preloaded_project_count).to eq(2)
|
||||
end
|
||||
|
||||
it 'counts only archived projects when `only` is passed' do
|
||||
create_list(:project, 2, namespace: parent, archived: true)
|
||||
create(:project, namespace: parent)
|
||||
context 'when not counting archived projects' do
|
||||
let(:archived_parameter) { false }
|
||||
|
||||
found_group = Group.with_selects_for_list(archived: 'only').find_by(id: parent.id)
|
||||
it 'counts projects without archived ones' do
|
||||
expect(found_group.preloaded_project_count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
expect(found_group.preloaded_project_count).to eq(2)
|
||||
context 'with archived only' do
|
||||
let_it_be(:archived_project2) { create(:project, namespace: parent, archived: true) }
|
||||
|
||||
let(:archived_parameter) { 'only' }
|
||||
|
||||
it 'counts only archived projects when `only` is passed' do
|
||||
expect(found_group.preloaded_project_count).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#children_count' do
|
||||
it 'counts groups and projects' do
|
||||
create(:group, parent: parent)
|
||||
create(:project, namespace: parent)
|
||||
|
||||
expect(found_group.children_count).to eq(2)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -6,7 +6,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
let_it_be(:pipeline, refind: true) { create(:ci_pipeline) }
|
||||
let_it_be(:project, refind: true) { create(:project) }
|
||||
|
||||
shared_examples 'succeessful examples' do
|
||||
shared_examples 'successful examples' do
|
||||
context 'outside transaction' do
|
||||
it { expect { run_queries }.not_to raise_error }
|
||||
end
|
||||
|
@ -36,7 +36,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
project.reload
|
||||
end
|
||||
|
||||
include_examples 'succeessful examples'
|
||||
include_examples 'successful examples'
|
||||
end
|
||||
|
||||
context 'when only CI data is modified' do
|
||||
|
@ -45,7 +45,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
project.reload
|
||||
end
|
||||
|
||||
include_examples 'succeessful examples'
|
||||
include_examples 'successful examples'
|
||||
end
|
||||
|
||||
context 'when other data is modified' do
|
||||
|
@ -54,38 +54,42 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
project.touch
|
||||
end
|
||||
|
||||
include_examples 'succeessful examples'
|
||||
include_examples 'successful examples'
|
||||
end
|
||||
|
||||
describe 'with_cross_database_modification_prevented block' do
|
||||
it 'raises error when CI and other data is modified' do
|
||||
expect do
|
||||
with_cross_database_modification_prevented do
|
||||
Project.transaction do
|
||||
project.touch
|
||||
pipeline.touch
|
||||
end
|
||||
end
|
||||
end.to raise_error /Cross-database data modification/
|
||||
context 'when both CI and other data is modified' do
|
||||
def run_queries
|
||||
project.touch
|
||||
pipeline.touch
|
||||
end
|
||||
|
||||
it 'raises an error when an undefined gitlab_schema table is modified with another table' do
|
||||
expect do
|
||||
with_cross_database_modification_prevented do
|
||||
Project.transaction do
|
||||
context 'outside transaction' do
|
||||
it { expect { run_queries }.not_to raise_error }
|
||||
end
|
||||
|
||||
context 'when data modification happens in a transaction' do
|
||||
it 'raises error' do
|
||||
Project.transaction do
|
||||
expect { run_queries }.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
|
||||
context 'when data modification happens in nested transactions' do
|
||||
it 'raises error' do
|
||||
Project.transaction(requires_new: true) do
|
||||
project.touch
|
||||
project.connection.execute('UPDATE foo_bars_undefined_table SET a=1 WHERE id = -1')
|
||||
Project.transaction(requires_new: true) do
|
||||
expect { pipeline.touch }.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
end
|
||||
end.to raise_error /Cross-database data modification.*The gitlab_schema was undefined/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when running tests with prevent_cross_database_modification', :prevent_cross_database_modification do
|
||||
context 'when both CI and other data is modified' do
|
||||
context 'when executing a SELECT FOR UPDATE query' do
|
||||
def run_queries
|
||||
project.touch
|
||||
pipeline.touch
|
||||
pipeline.lock!
|
||||
end
|
||||
|
||||
context 'outside transaction' do
|
||||
|
@ -99,41 +103,11 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when data modification happens in nested transactions' do
|
||||
it 'raises error' do
|
||||
Project.transaction(requires_new: true) do
|
||||
project.touch
|
||||
Project.transaction(requires_new: true) do
|
||||
expect { pipeline.touch }.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
context 'when the modification is inside a factory save! call' do
|
||||
let(:runner) { create(:ci_runner, :project, projects: [build(:project)]) }
|
||||
|
||||
context 'when executing a SELECT FOR UPDATE query' do
|
||||
def run_queries
|
||||
project.touch
|
||||
pipeline.lock!
|
||||
end
|
||||
|
||||
context 'outside transaction' do
|
||||
it { expect { run_queries }.not_to raise_error }
|
||||
end
|
||||
|
||||
context 'when data modification happens in a transaction' do
|
||||
it 'raises error' do
|
||||
Project.transaction do
|
||||
expect { run_queries }.to raise_error /Cross-database data modification/
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the modification is inside a factory save! call' do
|
||||
let(:runner) { create(:ci_runner, :project, projects: [build(:project)]) }
|
||||
|
||||
it 'does not raise an error' do
|
||||
runner
|
||||
end
|
||||
it 'does not raise an error' do
|
||||
runner
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -145,7 +119,7 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
project.save!
|
||||
end
|
||||
|
||||
include_examples 'succeessful examples'
|
||||
include_examples 'successful examples'
|
||||
end
|
||||
|
||||
describe '#allow_cross_database_modification_within_transaction' do
|
||||
|
@ -171,4 +145,15 @@ RSpec.describe 'Database::PreventCrossDatabaseModification' do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when some table with a defined schema and another table with undefined gitlab_schema is modified' do
|
||||
it 'raises an error including including message about undefined schema' do
|
||||
expect do
|
||||
Project.transaction do
|
||||
project.touch
|
||||
project.connection.execute('UPDATE foo_bars_undefined_table SET a=1 WHERE id = -1')
|
||||
end
|
||||
end.to raise_error /Cross-database data modification.*The gitlab_schema was undefined/
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue