Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
760822a537
commit
7484851b5f
|
@ -855,8 +855,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
|
||||||
- spec/models/abuse_report_spec.rb
|
- spec/models/abuse_report_spec.rb
|
||||||
- spec/models/alert_management/alert_spec.rb
|
- spec/models/alert_management/alert_spec.rb
|
||||||
- spec/models/audit_event_spec.rb
|
- spec/models/audit_event_spec.rb
|
||||||
- spec/models/blob_viewer/gitlab_ci_yml_spec.rb
|
|
||||||
- spec/models/blob_viewer/metrics_dashboard_yml_spec.rb
|
|
||||||
- spec/models/chat_name_spec.rb
|
- spec/models/chat_name_spec.rb
|
||||||
- spec/models/chat_team_spec.rb
|
- spec/models/chat_team_spec.rb
|
||||||
- spec/models/clusters/kubernetes_namespace_spec.rb
|
- spec/models/clusters/kubernetes_namespace_spec.rb
|
||||||
|
@ -898,13 +896,6 @@ RSpec/EmptyLineAfterFinalLetItBe:
|
||||||
- spec/models/user_spec.rb
|
- spec/models/user_spec.rb
|
||||||
- spec/models/wiki_page/meta_spec.rb
|
- spec/models/wiki_page/meta_spec.rb
|
||||||
- spec/models/wiki_page_spec.rb
|
- spec/models/wiki_page_spec.rb
|
||||||
- spec/policies/application_setting/term_policy_spec.rb
|
|
||||||
- spec/policies/ci/build_policy_spec.rb
|
|
||||||
- spec/policies/design_management/design_policy_spec.rb
|
|
||||||
- spec/policies/group_deploy_keys_group_policy_spec.rb
|
|
||||||
- spec/policies/group_policy_spec.rb
|
|
||||||
- spec/policies/project_snippet_policy_spec.rb
|
|
||||||
- spec/policies/service_policy_spec.rb
|
|
||||||
- spec/presenters/alert_management/alert_presenter_spec.rb
|
- spec/presenters/alert_management/alert_presenter_spec.rb
|
||||||
- spec/presenters/ci/pipeline_presenter_spec.rb
|
- spec/presenters/ci/pipeline_presenter_spec.rb
|
||||||
- spec/presenters/label_presenter_spec.rb
|
- spec/presenters/label_presenter_spec.rb
|
||||||
|
|
|
@ -2,11 +2,11 @@
|
||||||
import { GlDrawer } from '@gitlab/ui';
|
import { GlDrawer } from '@gitlab/ui';
|
||||||
import { mapState, mapActions, mapGetters } from 'vuex';
|
import { mapState, mapActions, mapGetters } from 'vuex';
|
||||||
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
||||||
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
|
|
||||||
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
||||||
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
|
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
|
||||||
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
|
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
|
||||||
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
|
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
|
||||||
|
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
|
||||||
import { ISSUABLE } from '~/boards/constants';
|
import { ISSUABLE } from '~/boards/constants';
|
||||||
import { contentTop } from '~/lib/utils/common_utils';
|
import { contentTop } from '~/lib/utils/common_utils';
|
||||||
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
|
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
|
||||||
|
@ -16,7 +16,7 @@ export default {
|
||||||
headerHeight: `${contentTop()}px`,
|
headerHeight: `${contentTop()}px`,
|
||||||
components: {
|
components: {
|
||||||
GlDrawer,
|
GlDrawer,
|
||||||
BoardSidebarIssueTitle,
|
BoardSidebarTitle,
|
||||||
SidebarAssigneesWidget,
|
SidebarAssigneesWidget,
|
||||||
BoardSidebarTimeTracker,
|
BoardSidebarTimeTracker,
|
||||||
BoardSidebarLabelsSelect,
|
BoardSidebarLabelsSelect,
|
||||||
|
@ -67,7 +67,7 @@ export default {
|
||||||
>
|
>
|
||||||
<template #header>{{ __('Issue details') }}</template>
|
<template #header>{{ __('Issue details') }}</template>
|
||||||
<template #default>
|
<template #default>
|
||||||
<board-sidebar-issue-title />
|
<board-sidebar-title />
|
||||||
<sidebar-assignees-widget
|
<sidebar-assignees-widget
|
||||||
:iid="activeBoardItem.iid"
|
:iid="activeBoardItem.iid"
|
||||||
:full-path="fullPath"
|
:full-path="fullPath"
|
||||||
|
|
|
@ -27,12 +27,12 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ issue: 'activeBoardItem' }),
|
...mapGetters({ item: 'activeBoardItem' }),
|
||||||
pendingChangesStorageKey() {
|
pendingChangesStorageKey() {
|
||||||
return this.getPendingChangesKey(this.issue);
|
return this.getPendingChangesKey(this.item);
|
||||||
},
|
},
|
||||||
projectPath() {
|
projectPath() {
|
||||||
const referencePath = this.issue.referencePath || '';
|
const referencePath = this.item.referencePath || '';
|
||||||
return referencePath.slice(0, referencePath.indexOf('#'));
|
return referencePath.slice(0, referencePath.indexOf('#'));
|
||||||
},
|
},
|
||||||
validationState() {
|
validationState() {
|
||||||
|
@ -40,29 +40,29 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
issue: {
|
item: {
|
||||||
handler(updatedIssue, formerIssue) {
|
handler(updatedItem, formerItem) {
|
||||||
if (formerIssue?.title !== this.title) {
|
if (formerItem?.title !== this.title) {
|
||||||
localStorage.setItem(this.getPendingChangesKey(formerIssue), this.title);
|
localStorage.setItem(this.getPendingChangesKey(formerItem), this.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.title = updatedIssue.title;
|
this.title = updatedItem.title;
|
||||||
this.setPendingState();
|
this.setPendingState();
|
||||||
},
|
},
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['setActiveIssueTitle']),
|
...mapActions(['setActiveItemTitle']),
|
||||||
getPendingChangesKey(issue) {
|
getPendingChangesKey(item) {
|
||||||
if (!issue) {
|
if (!item) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
return joinPaths(
|
return joinPaths(
|
||||||
window.location.pathname.slice(1),
|
window.location.pathname.slice(1),
|
||||||
String(issue.id),
|
String(item.id),
|
||||||
'issue-title-pending-changes',
|
'item-title-pending-changes',
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
async setPendingState() {
|
async setPendingState() {
|
||||||
|
@ -78,7 +78,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
cancel() {
|
cancel() {
|
||||||
this.title = this.issue.title;
|
this.title = this.item.title;
|
||||||
this.$refs.sidebarItem.collapse();
|
this.$refs.sidebarItem.collapse();
|
||||||
this.showChangesAlert = false;
|
this.showChangesAlert = false;
|
||||||
localStorage.removeItem(this.pendingChangesStorageKey);
|
localStorage.removeItem(this.pendingChangesStorageKey);
|
||||||
|
@ -86,24 +86,24 @@ export default {
|
||||||
async setTitle() {
|
async setTitle() {
|
||||||
this.$refs.sidebarItem.collapse();
|
this.$refs.sidebarItem.collapse();
|
||||||
|
|
||||||
if (!this.title || this.title === this.issue.title) {
|
if (!this.title || this.title === this.item.title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
await this.setActiveIssueTitle({ title: this.title, projectPath: this.projectPath });
|
await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
|
||||||
localStorage.removeItem(this.pendingChangesStorageKey);
|
localStorage.removeItem(this.pendingChangesStorageKey);
|
||||||
this.showChangesAlert = false;
|
this.showChangesAlert = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.title = this.issue.title;
|
this.title = this.item.title;
|
||||||
createFlash({ message: this.$options.i18n.updateTitleError });
|
createFlash({ message: this.$options.i18n.updateTitleError });
|
||||||
} finally {
|
} finally {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
handleOffClick() {
|
handleOffClick() {
|
||||||
if (this.title !== this.issue.title) {
|
if (this.title !== this.item.title) {
|
||||||
this.showChangesAlert = true;
|
this.showChangesAlert = true;
|
||||||
localStorage.setItem(this.pendingChangesStorageKey, this.title);
|
localStorage.setItem(this.pendingChangesStorageKey, this.title);
|
||||||
} else {
|
} else {
|
||||||
|
@ -112,11 +112,11 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
issueTitlePlaceholder: __('Issue title'),
|
titlePlaceholder: __('Title'),
|
||||||
submitButton: __('Save changes'),
|
submitButton: __('Save changes'),
|
||||||
cancelButton: __('Cancel'),
|
cancelButton: __('Cancel'),
|
||||||
updateTitleError: __('An error occurred when updating the issue title'),
|
updateTitleError: __('An error occurred when updating the title'),
|
||||||
invalidFeedback: __('An issue title is required'),
|
invalidFeedback: __('A title is required'),
|
||||||
reviewYourChanges: __('Changes to the title have not been saved'),
|
reviewYourChanges: __('Changes to the title have not been saved'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -131,10 +131,10 @@ export default {
|
||||||
@off-click="handleOffClick"
|
@off-click="handleOffClick"
|
||||||
>
|
>
|
||||||
<template #title>
|
<template #title>
|
||||||
<span class="gl-font-weight-bold" data-testid="issue-title">{{ issue.title }}</span>
|
<span class="gl-font-weight-bold" data-testid="item-title">{{ item.title }}</span>
|
||||||
</template>
|
</template>
|
||||||
<template #collapsed>
|
<template #collapsed>
|
||||||
<span class="gl-text-gray-800">{{ issue.referencePath }}</span>
|
<span class="gl-text-gray-800">{{ item.referencePath }}</span>
|
||||||
</template>
|
</template>
|
||||||
<gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
|
<gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
|
||||||
{{ $options.i18n.reviewYourChanges }}
|
{{ $options.i18n.reviewYourChanges }}
|
||||||
|
@ -144,7 +144,7 @@ export default {
|
||||||
<gl-form-input
|
<gl-form-input
|
||||||
v-model="title"
|
v-model="title"
|
||||||
v-autofocusonshow
|
v-autofocusonshow
|
||||||
:placeholder="$options.i18n.issueTitlePlaceholder"
|
:placeholder="$options.i18n.titlePlaceholder"
|
||||||
:state="validationState"
|
:state="validationState"
|
||||||
/>
|
/>
|
||||||
</gl-form-group>
|
</gl-form-group>
|
|
@ -1,5 +1,7 @@
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
|
import updateEpicTitleMutation from '~/sidebar/queries/update_epic_title.mutation.graphql';
|
||||||
import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
|
import boardBlockingIssuesQuery from './graphql/board_blocking_issues.query.graphql';
|
||||||
|
import issueSetTitleMutation from './graphql/issue_set_title.mutation.graphql';
|
||||||
|
|
||||||
export const issuableTypes = {
|
export const issuableTypes = {
|
||||||
issue: 'issue',
|
issue: 'issue',
|
||||||
|
@ -52,3 +54,12 @@ export const blockingIssuablesQueries = {
|
||||||
query: boardBlockingIssuesQuery,
|
query: boardBlockingIssuesQuery,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const titleQueries = {
|
||||||
|
[issuableTypes.issue]: {
|
||||||
|
mutation: issueSetTitleMutation,
|
||||||
|
},
|
||||||
|
[issuableTypes.epic]: {
|
||||||
|
mutation: updateEpicTitleMutation,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
mutation issueSetTitle($input: UpdateIssueInput!) {
|
mutation issueSetTitle($input: UpdateIssueInput!) {
|
||||||
updateIssue(input: $input) {
|
updateIssuableTitle: updateIssue(input: $input) {
|
||||||
issue {
|
issue {
|
||||||
title
|
title
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {
|
||||||
inactiveId,
|
inactiveId,
|
||||||
flashAnimationDuration,
|
flashAnimationDuration,
|
||||||
ISSUABLE,
|
ISSUABLE,
|
||||||
|
titleQueries,
|
||||||
} from '~/boards/constants';
|
} from '~/boards/constants';
|
||||||
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
|
||||||
import createGqClient, { fetchPolicies } from '~/lib/graphql';
|
import createGqClient, { fetchPolicies } from '~/lib/graphql';
|
||||||
|
@ -33,7 +34,6 @@ import issueSetDueDateMutation from '../graphql/issue_set_due_date.mutation.grap
|
||||||
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
|
import issueSetLabelsMutation from '../graphql/issue_set_labels.mutation.graphql';
|
||||||
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
|
import issueSetMilestoneMutation from '../graphql/issue_set_milestone.mutation.graphql';
|
||||||
import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
|
import issueSetSubscriptionMutation from '../graphql/issue_set_subscription.mutation.graphql';
|
||||||
import issueSetTitleMutation from '../graphql/issue_set_title.mutation.graphql';
|
|
||||||
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
|
import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
|
||||||
import * as types from './mutation_types';
|
import * as types from './mutation_types';
|
||||||
|
|
||||||
|
@ -526,27 +526,31 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setActiveIssueTitle: async ({ commit, getters }, input) => {
|
setActiveItemTitle: async ({ commit, getters, state }, input) => {
|
||||||
const { activeBoardItem } = getters;
|
const { activeBoardItem, isEpicBoard } = getters;
|
||||||
|
const { fullPath, issuableType } = state;
|
||||||
|
const workspacePath = isEpicBoard
|
||||||
|
? { groupPath: fullPath }
|
||||||
|
: { projectPath: input.projectPath };
|
||||||
const { data } = await gqlClient.mutate({
|
const { data } = await gqlClient.mutate({
|
||||||
mutation: issueSetTitleMutation,
|
mutation: titleQueries[issuableType].mutation,
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
|
...workspacePath,
|
||||||
iid: String(activeBoardItem.iid),
|
iid: String(activeBoardItem.iid),
|
||||||
projectPath: input.projectPath,
|
|
||||||
title: input.title,
|
title: input.title,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.updateIssue?.errors?.length > 0) {
|
if (data.updateIssuableTitle?.errors?.length > 0) {
|
||||||
throw new Error(data.updateIssue.errors);
|
throw new Error(data.updateIssuableTitle.errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
|
commit(types.UPDATE_BOARD_ITEM_BY_ID, {
|
||||||
itemId: activeBoardItem.id,
|
itemId: activeBoardItem.id,
|
||||||
prop: 'title',
|
prop: 'title',
|
||||||
value: data.updateIssue.issue.title,
|
value: data.updateIssuableTitle[issuableType].title,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,6 @@ export default {
|
||||||
},
|
},
|
||||||
actionPrimary: {
|
actionPrimary: {
|
||||||
text: __('Yes, close issue'),
|
text: __('Yes, close issue'),
|
||||||
attributes: [{ variant: 'warning' }],
|
|
||||||
},
|
},
|
||||||
i18n: {
|
i18n: {
|
||||||
promoteErrorMessage: __(
|
promoteErrorMessage: __(
|
||||||
|
@ -220,7 +219,6 @@ export default {
|
||||||
<gl-button
|
<gl-button
|
||||||
v-if="showToggleIssueStateButton"
|
v-if="showToggleIssueStateButton"
|
||||||
class="gl-display-none gl-sm-display-inline-flex!"
|
class="gl-display-none gl-sm-display-inline-flex!"
|
||||||
category="secondary"
|
|
||||||
:data-qa-selector="qaSelector"
|
:data-qa-selector="qaSelector"
|
||||||
:loading="isToggleStateButtonLoading"
|
:loading="isToggleStateButtonLoading"
|
||||||
@click="toggleIssueState"
|
@click="toggleIssueState"
|
||||||
|
|
|
@ -336,7 +336,7 @@ export default {
|
||||||
icon="pencil"
|
icon="pencil"
|
||||||
size="small"
|
size="small"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
class="note-action-button js-note-edit btn btn-transparent"
|
class="note-action-button js-note-edit"
|
||||||
data-qa-selector="note_edit_button"
|
data-qa-selector="note_edit_button"
|
||||||
@click="onEdit"
|
@click="onEdit"
|
||||||
/>
|
/>
|
||||||
|
@ -348,7 +348,7 @@ export default {
|
||||||
size="small"
|
size="small"
|
||||||
icon="remove"
|
icon="remove"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
class="note-action-button js-note-delete btn btn-transparent"
|
class="note-action-button js-note-delete"
|
||||||
@click="onDelete"
|
@click="onDelete"
|
||||||
/>
|
/>
|
||||||
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions">
|
<div v-else-if="shouldShowActionsDropdown" class="dropdown more-actions">
|
||||||
|
@ -359,7 +359,7 @@ export default {
|
||||||
icon="ellipsis_v"
|
icon="ellipsis_v"
|
||||||
size="small"
|
size="small"
|
||||||
category="tertiary"
|
category="tertiary"
|
||||||
class="note-action-button more-actions-toggle btn btn-transparent"
|
class="note-action-button more-actions-toggle"
|
||||||
data-toggle="dropdown"
|
data-toggle="dropdown"
|
||||||
@click="closeTooltip"
|
@click="closeTooltip"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -424,7 +424,7 @@ export default {
|
||||||
<gl-button
|
<gl-button
|
||||||
:disabled="isDisabled"
|
:disabled="isDisabled"
|
||||||
category="primary"
|
category="primary"
|
||||||
variant="success"
|
variant="confirm"
|
||||||
data-qa-selector="reply_comment_button"
|
data-qa-selector="reply_comment_button"
|
||||||
class="gl-mr-3 js-vue-issue-save js-comment-button"
|
class="gl-mr-3 js-vue-issue-save js-comment-button"
|
||||||
@click="handleUpdate()"
|
@click="handleUpdate()"
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
mutation updateEpic($input: UpdateEpicInput!) {
|
||||||
|
updateIssuableTitle: updateEpic(input: $input) {
|
||||||
|
epic {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
errors
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,12 +19,9 @@ module Mutations
|
||||||
|
|
||||||
def resolve(project_path:, iid:, assignee_usernames:, operation_mode:)
|
def resolve(project_path:, iid:, assignee_usernames:, operation_mode:)
|
||||||
resource = authorized_find!(project_path: project_path, iid: iid)
|
resource = authorized_find!(project_path: project_path, iid: iid)
|
||||||
|
users = new_assignees(resource, assignee_usernames)
|
||||||
|
|
||||||
update_service_class.new(
|
assign!(resource, users, operation_mode)
|
||||||
resource.project,
|
|
||||||
current_user,
|
|
||||||
assignee_ids: assignee_ids(resource, assignee_usernames, operation_mode)
|
|
||||||
).execute(resource)
|
|
||||||
|
|
||||||
{
|
{
|
||||||
resource.class.name.underscore.to_sym => resource,
|
resource.class.name.underscore.to_sym => resource,
|
||||||
|
@ -34,10 +31,20 @@ module Mutations
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def assignee_ids(resource, usernames, mode)
|
def assign!(resource, users, operation_mode)
|
||||||
new = UsersFinder.new(current_user, username: usernames).execute.map(&:id)
|
update_service_class.new(
|
||||||
|
resource.project,
|
||||||
|
current_user,
|
||||||
|
assignee_ids: assignee_ids(resource, users, operation_mode)
|
||||||
|
).execute(resource)
|
||||||
|
end
|
||||||
|
|
||||||
transform_list(mode, resource, new)
|
def new_assignees(resource, usernames)
|
||||||
|
UsersFinder.new(current_user, username: usernames).execute.to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def assignee_ids(resource, users, mode)
|
||||||
|
transform_list(mode, resource, users.map(&:id))
|
||||||
end
|
end
|
||||||
|
|
||||||
def current_assignee_ids(resource)
|
def current_assignee_ids(resource)
|
||||||
|
|
|
@ -7,6 +7,19 @@ module Mutations
|
||||||
|
|
||||||
include Assignable
|
include Assignable
|
||||||
|
|
||||||
|
def assign!(issue, users, mode)
|
||||||
|
permitted, forbidden = users.partition { |u| u.can?(:read_issue, issue) }
|
||||||
|
|
||||||
|
super(issue, permitted, mode)
|
||||||
|
|
||||||
|
forbidden.each do |user|
|
||||||
|
issue.errors.add(
|
||||||
|
:assignees,
|
||||||
|
"Cannot assign #{user.to_reference} to #{issue.to_reference}"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def update_service_class
|
def update_service_class
|
||||||
::Issues::UpdateService
|
::Issues::UpdateService
|
||||||
end
|
end
|
||||||
|
|
|
@ -683,7 +683,9 @@ module Ci
|
||||||
end
|
end
|
||||||
|
|
||||||
def has_kubernetes_active?
|
def has_kubernetes_active?
|
||||||
project.deployment_platform&.active?
|
strong_memoize(:has_kubernetes_active) do
|
||||||
|
project.deployment_platform&.active?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def freeze_period?
|
def freeze_period?
|
||||||
|
|
|
@ -8,6 +8,7 @@ module Clusters
|
||||||
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
|
belongs_to :project, class_name: '::Project' # Otherwise, it will load ::Clusters::Project
|
||||||
|
|
||||||
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
|
has_many :agent_tokens, class_name: 'Clusters::AgentToken'
|
||||||
|
has_many :last_used_agent_tokens, -> { order_last_used_at_desc }, class_name: 'Clusters::AgentToken'
|
||||||
|
|
||||||
scope :ordered_by_name, -> { order(:name) }
|
scope :ordered_by_name, -> { order(:name) }
|
||||||
scope :with_name, -> (name) { where(name: name) }
|
scope :with_name, -> (name) { where(name: name) }
|
||||||
|
|
|
@ -6,7 +6,7 @@ module Clusters
|
||||||
include TokenAuthenticatable
|
include TokenAuthenticatable
|
||||||
|
|
||||||
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
|
add_authentication_token_field :token, encrypted: :required, token_generator: -> { Devise.friendly_token(50) }
|
||||||
cached_attr_reader :last_contacted_at
|
cached_attr_reader :last_used_at
|
||||||
|
|
||||||
self.table_name = 'cluster_agent_tokens'
|
self.table_name = 'cluster_agent_tokens'
|
||||||
|
|
||||||
|
@ -21,6 +21,8 @@ module Clusters
|
||||||
validates :description, length: { maximum: 1024 }
|
validates :description, length: { maximum: 1024 }
|
||||||
validates :name, presence: true, length: { maximum: 255 }
|
validates :name, presence: true, length: { maximum: 255 }
|
||||||
|
|
||||||
|
scope :order_last_used_at_desc, -> { order(::Gitlab::Database.nulls_last_order('last_used_at', 'DESC')) }
|
||||||
|
|
||||||
def track_usage
|
def track_usage
|
||||||
track_values = { last_used_at: Time.current.utc }
|
track_values = { last_used_at: Time.current.utc }
|
||||||
|
|
||||||
|
|
|
@ -240,18 +240,10 @@ class Deployment < ApplicationRecord
|
||||||
def previous_deployment
|
def previous_deployment
|
||||||
@previous_deployment ||=
|
@previous_deployment ||=
|
||||||
self.class.for_environment(environment_id)
|
self.class.for_environment(environment_id)
|
||||||
.where(ref: ref)
|
.success
|
||||||
.where.not(id: id)
|
.where('id < ?', id)
|
||||||
.order(id: :desc)
|
.order(id: :desc)
|
||||||
.take
|
.take
|
||||||
end
|
|
||||||
|
|
||||||
def previous_environment_deployment
|
|
||||||
self.class.for_environment(environment_id)
|
|
||||||
.success
|
|
||||||
.where.not(id: self.id)
|
|
||||||
.order(id: :desc)
|
|
||||||
.take
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def stop_action
|
def stop_action
|
||||||
|
|
|
@ -33,7 +33,7 @@ module Deployments
|
||||||
# meaningful way (i.e. they can't just retry the deploy themselves).
|
# meaningful way (i.e. they can't just retry the deploy themselves).
|
||||||
return unless deployment.success?
|
return unless deployment.success?
|
||||||
|
|
||||||
if (prev = deployment.previous_environment_deployment)
|
if (prev = deployment.previous_deployment)
|
||||||
link_merge_requests_for_range(prev.sha, deployment.sha)
|
link_merge_requests_for_range(prev.sha, deployment.sha)
|
||||||
else
|
else
|
||||||
# When no previous deployment is found we fall back to linking all merge
|
# When no previous deployment is found we fall back to linking all merge
|
||||||
|
|
|
@ -7,15 +7,20 @@ module MergeRequests
|
||||||
# This saves a lot of queries for irrelevant things that cannot possibly
|
# This saves a lot of queries for irrelevant things that cannot possibly
|
||||||
# change in the execution of this service.
|
# change in the execution of this service.
|
||||||
def execute(merge_request)
|
def execute(merge_request)
|
||||||
return unless current_user&.can?(:update_merge_request, merge_request)
|
return merge_request unless current_user&.can?(:update_merge_request, merge_request)
|
||||||
|
|
||||||
old_ids = merge_request.assignees.map(&:id)
|
old_ids = merge_request.assignees.map(&:id)
|
||||||
return if old_ids.to_set == update_attrs[:assignee_ids].to_set # no-change
|
new_ids = new_assignee_ids(merge_request)
|
||||||
|
return merge_request if new_ids.size != update_attrs[:assignee_ids].size
|
||||||
|
return merge_request if old_ids.to_set == new_ids.to_set # no-change
|
||||||
|
|
||||||
merge_request.update!(**update_attrs)
|
attrs = update_attrs.merge(assignee_ids: new_ids)
|
||||||
|
merge_request.update!(**attrs)
|
||||||
|
|
||||||
# Defer the more expensive operations (handle_assignee_changes) to the background
|
# Defer the more expensive operations (handle_assignee_changes) to the background
|
||||||
MergeRequests::AssigneesChangeWorker.perform_async(merge_request.id, current_user.id, old_ids)
|
MergeRequests::AssigneesChangeWorker.perform_async(merge_request.id, current_user.id, old_ids)
|
||||||
|
|
||||||
|
merge_request
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_assignee_changes(merge_request, old_assignees)
|
def handle_assignee_changes(merge_request, old_assignees)
|
||||||
|
@ -31,10 +36,33 @@ module MergeRequests
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def new_assignee_ids(merge_request)
|
||||||
|
# prime the cache - prevent N+1 lookup during authorization loop.
|
||||||
|
merge_request.project.team.max_member_access_for_user_ids(update_attrs[:assignee_ids])
|
||||||
|
User.id_in(update_attrs[:assignee_ids]).map do |user|
|
||||||
|
if user.can?(:read_merge_request, merge_request)
|
||||||
|
user.id
|
||||||
|
else
|
||||||
|
merge_request.errors.add(
|
||||||
|
:assignees,
|
||||||
|
"Cannot assign #{user.to_reference} to #{merge_request.to_reference}"
|
||||||
|
)
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
end.compact
|
||||||
|
end
|
||||||
|
|
||||||
def assignee_ids
|
def assignee_ids
|
||||||
params.fetch(:assignee_ids).first(1)
|
params.fetch(:assignee_ids).first(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def params
|
||||||
|
ps = super
|
||||||
|
|
||||||
|
# allow either assignee_id or assignee_ids, preferring assignee_id if passed.
|
||||||
|
{ assignee_ids: ps.key?(:assignee_id) ? Array.wrap(ps[:assignee_id]) : ps[:assignee_ids] }
|
||||||
|
end
|
||||||
|
|
||||||
def update_attrs
|
def update_attrs
|
||||||
@attrs ||= { updated_at: Time.current, updated_by: current_user, assignee_ids: assignee_ids }
|
@attrs ||= { updated_at: Time.current, updated_by: current_user, assignee_ids: assignee_ids }
|
||||||
end
|
end
|
||||||
|
|
|
@ -13,19 +13,20 @@
|
||||||
|
|
||||||
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
|
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
|
||||||
%p.slead
|
%p.slead
|
||||||
GitLab is open source software to collaborate on code.
|
= _('GitLab is open source software to collaborate on code.')
|
||||||
%br
|
%br
|
||||||
Manage git repositories with fine-grained access controls that keep your code secure.
|
= _('Manage git repositories with fine-grained access controls that keep your code secure.')
|
||||||
%br
|
%br
|
||||||
Perform code reviews and enhance collaboration with merge requests.
|
= _('Perform code reviews and enhance collaboration with merge requests.')
|
||||||
%br
|
%br
|
||||||
Each project can also have an issue tracker and a wiki.
|
= _('Each project can also have an issue tracker and a wiki.')
|
||||||
%br
|
%br
|
||||||
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
|
= _('Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.')
|
||||||
%br
|
%br
|
||||||
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer'}.
|
- link_to_promo = link_to(promo_host, promo_url, target: '_blank', rel: 'noopener noreferrer')
|
||||||
|
= _("Read more about GitLab at %{link_to_promo}.").html_safe % { link_to_promo: link_to_promo }
|
||||||
|
|
||||||
%p= link_to 'Check the current instance configuration ', help_instance_configuration_url
|
%p= link_to _('Check the current instance configuration '), help_instance_configuration_url
|
||||||
%hr
|
%hr
|
||||||
|
|
||||||
.row.gl-mt-3
|
.row.gl-mt-3
|
||||||
|
@ -35,15 +36,15 @@
|
||||||
.col-md-4
|
.col-md-4
|
||||||
.card.links-card
|
.card.links-card
|
||||||
.card-header
|
.card-header
|
||||||
Quick help
|
= _('Quick help')
|
||||||
%ul.content-list
|
%ul.content-list
|
||||||
%li= link_to 'See our website for getting help', support_url
|
%li= link_to _('See our website for getting help'), support_url
|
||||||
%li
|
%li
|
||||||
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
|
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
|
||||||
Use the search bar on the top of this page
|
= _('Use the search bar on the top of this page')
|
||||||
%li
|
%li
|
||||||
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
|
%button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
|
||||||
Use shortcuts
|
= _('Use shortcuts')
|
||||||
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
|
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
|
||||||
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
|
%li= link_to _('Get a support subscription'), 'https://about.gitlab.com/pricing/'
|
||||||
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
|
%li= link_to _('Compare GitLab editions'), 'https://about.gitlab.com/features/#compare'
|
||||||
|
|
|
@ -39,13 +39,13 @@
|
||||||
- else
|
- else
|
||||||
.gl-text-center
|
.gl-text-center
|
||||||
%h4= s_('Integrations|No linked namespaces')
|
%h4= s_('Integrations|No linked namespaces')
|
||||||
%p= s_('Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance.')
|
%p= s_('Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance.')
|
||||||
|
|
||||||
%p.jira-connect-app-body.gl-mt-7.gl-font-base.gl-text-center
|
%p.jira-connect-app-body.gl-mt-7.gl-font-base.gl-text-center
|
||||||
%strong= s_('Integrations|Browser limitations')
|
%strong= s_('Integrations|Browser limitations')
|
||||||
- firefox_link_url = 'https://www.mozilla.org/en-US/firefox/'
|
- firefox_link_url = 'https://www.mozilla.org/en-US/firefox/'
|
||||||
- firefox_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: firefox_link_url }
|
- firefox_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: firefox_link_url }
|
||||||
= s_('Integrations|Adding a namespace currently works only in browsers that allow cross‑site cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable cross‑site cookies in your browser when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, firefox_link_end: '</a>'.html_safe }
|
= s_('Integrations|Adding a namespace works only in browsers that allow cross‑site cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable cross‑site cookies in your browser, when adding a namespace.').html_safe % { firefox_link_start: firefox_link_start, firefox_link_end: '</a>'.html_safe }
|
||||||
= link_to _('Learn more'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/284211', target: '_blank', rel: 'noopener noreferrer'
|
= link_to _('Learn more'), 'https://gitlab.com/gitlab-org/gitlab/-/issues/284211', target: '_blank', rel: 'noopener noreferrer'
|
||||||
|
|
||||||
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
|
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
.jira-connect-users-container.gl-text-center
|
.jira-connect-users-container.gl-text-center
|
||||||
- user_link = link_to(current_user.to_reference, user_path(current_user), target: '_blank', rel: 'noopener noreferrer')
|
- user_link = link_to(current_user.to_reference, user_path(current_user), target: '_blank', rel: 'noopener noreferrer')
|
||||||
%h2= _('You are signed into GitLab as %{user_link}').html_safe % { user_link: user_link }
|
%h2= _('You are signed in to GitLab as %{user_link}').html_safe % { user_link: user_link }
|
||||||
|
|
||||||
%p= s_('Integrations|You can now close this window and return to the GitLab for Jira application.')
|
%p= s_('Integrations|You can now close this window and return to the GitLab for Jira application.')
|
||||||
|
|
||||||
|
|
|
@ -84,7 +84,7 @@
|
||||||
.col-lg-8
|
.col-lg-8
|
||||||
-# TODO: might need an entry in user/profile.md to describe some of these settings
|
-# TODO: might need an entry in user/profile.md to describe some of these settings
|
||||||
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/60070
|
-# https://gitlab.com/gitlab-org/gitlab-foss/issues/60070
|
||||||
%h5= ("Time zone")
|
%h5= _("Time zone")
|
||||||
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
|
= dropdown_tag(_("Select a timezone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
|
||||||
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
|
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
|
||||||
.col-lg-12
|
.col-lg-12
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
%input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } }
|
%input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } }
|
||||||
|
|
||||||
- if @note.can_be_discussion_note?
|
- if @note.can_be_discussion_note?
|
||||||
= button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-confirm btn-icon js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
|
= button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-confirm js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
|
||||||
= sprite_icon('chevron-down')
|
= sprite_icon('chevron-down')
|
||||||
|
|
||||||
%ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
|
%ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add ability to order cluster token by last used
|
||||||
|
merge_request: 57520
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add unified metrics definition YAML file API endpoint
|
||||||
|
merge_request: 57270
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add caching to variables calculation of builds
|
||||||
|
merge_request: 58286
|
||||||
|
author:
|
||||||
|
type: performance
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Externalize strings in help/index.html.haml
|
||||||
|
merge_request: 58441
|
||||||
|
author: nuwe1
|
||||||
|
type: other
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix previous deployment fetches wrong deployment
|
||||||
|
merge_request: 58567
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/models/blob_viewer
|
||||||
|
merge_request: 58325
|
||||||
|
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Fix EmptyLineAfterFinalLetItBe offenses in spec/policies
|
||||||
|
merge_request: 58393
|
||||||
|
author: Huzaifa Iftikhar @huzaifaiftikhar
|
||||||
|
type: fixed
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Remove deprecated button classes from issue detail view
|
||||||
|
merge_request: 57763
|
||||||
|
author:
|
||||||
|
type: changed
|
|
@ -0,0 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class AddClusterAgentTokenLastUsed < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
INDEX = 'index_cluster_agent_tokens_on_last_used_at'
|
||||||
|
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
add_concurrent_index :cluster_agent_tokens,
|
||||||
|
:last_used_at,
|
||||||
|
name: INDEX,
|
||||||
|
order: { last_used_at: 'DESC NULLS LAST' }
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
remove_concurrent_index_by_name :cluster_agent_tokens, INDEX
|
||||||
|
end
|
||||||
|
end
|
|
@ -0,0 +1 @@
|
||||||
|
079ca92ac58519ce8f575c4cb94bfe6cf209e0c9eac20d3d3a294f5b468bc586
|
|
@ -22309,6 +22309,8 @@ CREATE INDEX index_cluster_agent_tokens_on_agent_id ON cluster_agent_tokens USIN
|
||||||
|
|
||||||
CREATE INDEX index_cluster_agent_tokens_on_created_by_user_id ON cluster_agent_tokens USING btree (created_by_user_id);
|
CREATE INDEX index_cluster_agent_tokens_on_created_by_user_id ON cluster_agent_tokens USING btree (created_by_user_id);
|
||||||
|
|
||||||
|
CREATE INDEX index_cluster_agent_tokens_on_last_used_at ON cluster_agent_tokens USING btree (last_used_at DESC NULLS LAST);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted);
|
CREATE UNIQUE INDEX index_cluster_agent_tokens_on_token_encrypted ON cluster_agent_tokens USING btree (token_encrypted);
|
||||||
|
|
||||||
CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id);
|
CREATE INDEX index_cluster_agents_on_created_by_user_id ON cluster_agents USING btree (created_by_user_id);
|
||||||
|
|
|
@ -9896,6 +9896,30 @@ Status: `implemented`
|
||||||
|
|
||||||
Tiers: `premium`, `ultimate`
|
Tiers: `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_moved_from_project_monthly`
|
||||||
|
|
||||||
|
Counts of MAU moving epic issues between projects
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_28d/20210405190240_g_project_management_epic_issue_moved_from_project_monthly.yml)
|
||||||
|
|
||||||
|
Group: `group::product planning`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `premium`, `ultimate`
|
||||||
|
|
||||||
|
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_moved_from_project_weekly`
|
||||||
|
|
||||||
|
Counts of WAU moving epic issues between projects
|
||||||
|
|
||||||
|
[YAML definition](https://gitlab.com/gitlab-org/gitlab/-/blob/master/ee/config/metrics/counts_7d/20210405185814_g_project_management_epic_issue_moved_from_project_weekly.yml)
|
||||||
|
|
||||||
|
Group: `group::product planning`
|
||||||
|
|
||||||
|
Status: `implemented`
|
||||||
|
|
||||||
|
Tiers: `premium`, `ultimate`
|
||||||
|
|
||||||
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_removed_monthly`
|
### `redis_hll_counters.epics_usage.g_project_management_epic_issue_removed_monthly`
|
||||||
|
|
||||||
Count of MAU removing issues from epics
|
Count of MAU removing issues from epics
|
||||||
|
|
|
@ -1411,6 +1411,37 @@ bin/rake gitlab:usage_data:dump_sql_in_json
|
||||||
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
|
bin/rake gitlab:usage_data:dump_sql_in_yaml > ~/Desktop/usage-metrics-2020-09-02.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Export metric definitions as a single YAML file
|
||||||
|
|
||||||
|
Use this API endpoint to export all metric definitions as a single YAML file, similar to the [Metrics Dictionary](dictionary.md), for easier importing.
|
||||||
|
|
||||||
|
```plaintext
|
||||||
|
GET /usage_data/metric_definitions
|
||||||
|
```
|
||||||
|
|
||||||
|
Response
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
- key_path: redis_hll_counters.search.i_search_paid_monthly
|
||||||
|
description: Calculated unique users to perform a search with a paid license enabled
|
||||||
|
by month
|
||||||
|
product_section: enablement
|
||||||
|
product_stage: enablement
|
||||||
|
product_group: group::global search
|
||||||
|
product_category: global_search
|
||||||
|
value_type: number
|
||||||
|
status: data_available
|
||||||
|
time_frame: 28d
|
||||||
|
data_source: redis_hll
|
||||||
|
distribution:
|
||||||
|
- ee
|
||||||
|
tier:
|
||||||
|
- premium
|
||||||
|
- ultimate
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
## Generating and troubleshooting usage ping
|
## Generating and troubleshooting usage ping
|
||||||
|
|
||||||
This activity is to be done via a detached screen session on a remote server.
|
This activity is to be done via a detached screen session on a remote server.
|
||||||
|
|
|
@ -424,7 +424,13 @@ module API
|
||||||
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params.has_key?(:remove_source_branch)
|
mr_params[:force_remove_source_branch] = mr_params.delete(:remove_source_branch) if mr_params.has_key?(:remove_source_branch)
|
||||||
mr_params = convert_parameters_from_legacy_format(mr_params)
|
mr_params = convert_parameters_from_legacy_format(mr_params)
|
||||||
|
|
||||||
merge_request = ::MergeRequests::UpdateService.new(user_project, current_user, mr_params).execute(merge_request)
|
service = if mr_params.one? && (mr_params.keys & %i[assignee_id assignee_ids]).one?
|
||||||
|
::MergeRequests::UpdateAssigneesService
|
||||||
|
else
|
||||||
|
::MergeRequests::UpdateService
|
||||||
|
end
|
||||||
|
|
||||||
|
merge_request = service.new(user_project, current_user, mr_params).execute(merge_request)
|
||||||
|
|
||||||
handle_merge_request_errors!(merge_request)
|
handle_merge_request_errors!(merge_request)
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
module API
|
module API
|
||||||
class UsageData < ::API::Base
|
class UsageData < ::API::Base
|
||||||
before { authenticate! }
|
before { authenticate_non_get! }
|
||||||
|
|
||||||
feature_category :usage_ping
|
feature_category :usage_ping
|
||||||
|
|
||||||
|
@ -15,11 +15,9 @@ module API
|
||||||
desc 'Track usage data events' do
|
desc 'Track usage data events' do
|
||||||
detail 'This feature was introduced in GitLab 13.4.'
|
detail 'This feature was introduced in GitLab 13.4.'
|
||||||
end
|
end
|
||||||
|
|
||||||
params do
|
params do
|
||||||
requires :event, type: String, desc: 'The event name that should be tracked'
|
requires :event, type: String, desc: 'The event name that should be tracked'
|
||||||
end
|
end
|
||||||
|
|
||||||
post 'increment_counter' do
|
post 'increment_counter' do
|
||||||
event_name = params[:event]
|
event_name = params[:event]
|
||||||
|
|
||||||
|
@ -31,7 +29,6 @@ module API
|
||||||
params do
|
params do
|
||||||
requires :event, type: String, desc: 'The event name that should be tracked'
|
requires :event, type: String, desc: 'The event name that should be tracked'
|
||||||
end
|
end
|
||||||
|
|
||||||
post 'increment_unique_users' do
|
post 'increment_unique_users' do
|
||||||
event_name = params[:event]
|
event_name = params[:event]
|
||||||
|
|
||||||
|
@ -39,6 +36,16 @@ module API
|
||||||
|
|
||||||
status :ok
|
status :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
desc 'Get a list of all metric definitions' do
|
||||||
|
detail 'This feature was introduced in GitLab 13.11.'
|
||||||
|
end
|
||||||
|
get 'metric_definitions' do
|
||||||
|
content_type 'application/yaml'
|
||||||
|
env['api.format'] = :binary
|
||||||
|
|
||||||
|
Gitlab::Usage::MetricDefinition.dump_metrics_yaml
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -69,6 +69,10 @@ module Gitlab
|
||||||
@schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
|
@schemer ||= ::JSONSchemer.schema(Pathname.new(METRIC_SCHEMA_PATH))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def dump_metrics_yaml
|
||||||
|
@metrics_yaml ||= definitions.values.map(&:to_h).map(&:deep_stringify_keys).to_yaml
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def load_all!
|
def load_all!
|
||||||
|
|
|
@ -93,6 +93,12 @@
|
||||||
aggregation: daily
|
aggregation: daily
|
||||||
feature_flag: track_epics_activity
|
feature_flag: track_epics_activity
|
||||||
|
|
||||||
|
- name: g_project_management_epic_issue_moved_from_project
|
||||||
|
category: epics_usage
|
||||||
|
redis_slot: project_management
|
||||||
|
aggregation: daily
|
||||||
|
feature_flag: track_epics_activity
|
||||||
|
|
||||||
- name: g_project_management_epic_closed
|
- name: g_project_management_epic_closed
|
||||||
category: epics_usage
|
category: epics_usage
|
||||||
redis_slot: project_management
|
redis_slot: project_management
|
||||||
|
|
|
@ -3362,10 +3362,10 @@ msgstr ""
|
||||||
msgid "An error occurred when updating the issue due date"
|
msgid "An error occurred when updating the issue due date"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred when updating the issue title"
|
msgid "An error occurred when updating the issue weight"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred when updating the issue weight"
|
msgid "An error occurred when updating the title"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "An error occurred while acknowledging the notification. Refresh the page and try again."
|
msgid "An error occurred while acknowledging the notification. Refresh the page and try again."
|
||||||
|
@ -3698,9 +3698,6 @@ msgstr ""
|
||||||
msgid "An issue already exists"
|
msgid "An issue already exists"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "An issue title is required"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "An unauthenticated user"
|
msgid "An unauthenticated user"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -5913,6 +5910,9 @@ msgstr ""
|
||||||
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
|
msgid "Check the %{docs_link_start}documentation%{docs_link_end}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Check the current instance configuration "
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Check the elasticsearch.log file to debug why the migration was halted and make any changes before retrying the migration. When you fix the cause of the failure, click \"Retry migration\", and the migration will be scheduled to be retried in the background."
|
msgid "Check the elasticsearch.log file to debug why the migration was halted and make any changes before retrying the migration. When you fix the cause of the failure, click \"Retry migration\", and the migration will be scheduled to be retried in the background."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -7889,6 +7889,9 @@ msgstr ""
|
||||||
msgid "Compare Git revisions"
|
msgid "Compare Git revisions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Compare GitLab editions"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Compare Revisions"
|
msgid "Compare Revisions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -11355,6 +11358,9 @@ msgstr ""
|
||||||
msgid "Dynamic Application Security Testing (DAST)"
|
msgid "Dynamic Application Security Testing (DAST)"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Each project can also have an issue tracker and a wiki."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Edit"
|
msgid "Edit"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -14351,6 +14357,9 @@ msgstr ""
|
||||||
msgid "Get a free instance review"
|
msgid "Get a free instance review"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Get a support subscription"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Get started"
|
msgid "Get started"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -14480,6 +14489,9 @@ msgstr ""
|
||||||
msgid "GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later."
|
msgid "GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GitLab is open source software to collaborate on code."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "GitLab is undergoing maintenance and is operating in a read-only mode."
|
msgid "GitLab is undergoing maintenance and is operating in a read-only mode."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -15095,6 +15107,9 @@ msgstr ""
|
||||||
msgid "GroupSAML|Before enforcing SSO, enable SAML authentication."
|
msgid "GroupSAML|Before enforcing SSO, enable SAML authentication."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "GroupSAML|Before enforcing SSO-access for Git, enable SSO-only authentication for web activity."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "GroupSAML|Certificate fingerprint"
|
msgid "GroupSAML|Certificate fingerprint"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -16847,7 +16862,7 @@ msgstr ""
|
||||||
msgid "Integrations|Add namespace"
|
msgid "Integrations|Add namespace"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|Adding a namespace currently works only in browsers that allow cross‑site cookies. Please make sure to use %{firefox_link_start}Firefox%{firefox_link_end} or enable cross‑site cookies in your browser when adding a namespace."
|
msgid "Integrations|Adding a namespace works only in browsers that allow cross‑site cookies. Use %{firefox_link_start}Firefox%{firefox_link_end}, or enable cross‑site cookies in your browser, when adding a namespace."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|All details"
|
msgid "Integrations|All details"
|
||||||
|
@ -16907,7 +16922,7 @@ msgstr ""
|
||||||
msgid "Integrations|Namespace successfully linked"
|
msgid "Integrations|Namespace successfully linked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|Namespaces are your GitLab groups and subgroups that will be linked to this Jira instance."
|
msgid "Integrations|Namespaces are the GitLab groups and subgroups you link to this Jira instance."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Integrations|No available namespaces."
|
msgid "Integrations|No available namespaces."
|
||||||
|
@ -17420,9 +17435,6 @@ msgstr ""
|
||||||
msgid "Issue published on status page."
|
msgid "Issue published on status page."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Issue title"
|
|
||||||
msgstr ""
|
|
||||||
|
|
||||||
msgid "Issue types"
|
msgid "Issue types"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -18969,6 +18981,9 @@ msgstr ""
|
||||||
msgid "Manage applications that you've authorized to use your account."
|
msgid "Manage applications that you've authorized to use your account."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Manage git repositories with fine-grained access controls that keep your code secure."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Manage group labels"
|
msgid "Manage group labels"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22659,6 +22674,9 @@ msgstr ""
|
||||||
msgid "Pending comments"
|
msgid "Pending comments"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Pending sync…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "People without permission will never get a notification and won't be able to comment."
|
msgid "People without permission will never get a notification and won't be able to comment."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -22674,6 +22692,9 @@ msgstr ""
|
||||||
msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
|
msgid "Perform advanced options such as changing path, transferring, exporting, or removing the group."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Perform code reviews and enhance collaboration with merge requests."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Perform common operations on GitLab project"
|
msgid "Perform common operations on GitLab project"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25545,6 +25566,9 @@ msgstr ""
|
||||||
msgid "Quick actions can be used in the issues description and comment boxes."
|
msgid "Quick actions can be used in the issues description and comment boxes."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Quick help"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Quick range"
|
msgid "Quick range"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -25584,6 +25608,9 @@ msgstr ""
|
||||||
msgid "Read more"
|
msgid "Read more"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Read more about GitLab at %{link_to_promo}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Read more about project permissions %{help_link_open}here%{help_link_close}"
|
msgid "Read more about project permissions %{help_link_open}here%{help_link_close}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -27771,6 +27798,9 @@ msgstr ""
|
||||||
msgid "See metrics"
|
msgid "See metrics"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "See our website for getting help"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "See the affected projects in the GitLab admin panel"
|
msgid "See the affected projects in the GitLab admin panel"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -29854,6 +29884,9 @@ msgstr ""
|
||||||
msgid "Successfully unlocked"
|
msgid "Successfully unlocked"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Successfully updated %{last_updated_timeago}."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Successfully verified domain ownership"
|
msgid "Successfully verified domain ownership"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -32068,6 +32101,9 @@ msgstr ""
|
||||||
msgid "Time until first merge request"
|
msgid "Time until first merge request"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Time zone"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "TimeTrackingEstimated|Est"
|
msgid "TimeTrackingEstimated|Est"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33169,6 +33205,12 @@ msgstr ""
|
||||||
msgid "Update %{sourcePath} file"
|
msgid "Update %{sourcePath} file"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Update Now"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Update Scheduled…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Update all"
|
msgid "Update all"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33241,6 +33283,9 @@ msgstr ""
|
||||||
msgid "Updating"
|
msgid "Updating"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Updating…"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Upgrade offers available!"
|
msgid "Upgrade offers available!"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33586,9 +33631,15 @@ msgstr ""
|
||||||
msgid "Use one line per URI"
|
msgid "Use one line per URI"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Use shortcuts"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Use template"
|
msgid "Use template"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Use the search bar on the top of this page"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Use this token to validate received payloads."
|
msgid "Use this token to validate received payloads."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -33604,6 +33655,9 @@ msgstr ""
|
||||||
msgid "Used by members to sign in to your group in GitLab"
|
msgid "Used by members to sign in to your group in GitLab"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Used programming language"
|
msgid "Used programming language"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -35218,7 +35272,7 @@ msgstr ""
|
||||||
msgid "You are receiving this message because you are a GitLab administrator for %{url}."
|
msgid "You are receiving this message because you are a GitLab administrator for %{url}."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "You are signed into GitLab as %{user_link}"
|
msgid "You are signed in to GitLab as %{user_link}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico."
|
msgid "You are trying to upload something other than an image. Please upload a .png, .jpg, .jpeg, .gif, .bmp, .tiff or .ico."
|
||||||
|
|
|
@ -173,7 +173,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
|
||||||
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
|
it 'allows using markdown buttons after saving a note and then trying to edit it again' do
|
||||||
page.within('.current-note-edit-form') do
|
page.within('.current-note-edit-form') do
|
||||||
fill_in 'note[note]', with: 'This is the new content'
|
fill_in 'note[note]', with: 'This is the new content'
|
||||||
find('.btn-success').click
|
find('.btn-confirm').click
|
||||||
end
|
end
|
||||||
|
|
||||||
find('.note').hover
|
find('.note').hover
|
||||||
|
@ -191,7 +191,7 @@ RSpec.describe 'Merge request > User posts notes', :js do
|
||||||
it 'appends the edited at time to the note' do
|
it 'appends the edited at time to the note' do
|
||||||
page.within('.current-note-edit-form') do
|
page.within('.current-note-edit-form') do
|
||||||
fill_in 'note[note]', with: 'Some new content'
|
fill_in 'note[note]', with: 'Some new content'
|
||||||
find('.btn-success').click
|
find('.btn-confirm').click
|
||||||
end
|
end
|
||||||
|
|
||||||
page.within("#note_#{note.id}") do
|
page.within("#note_#{note.id}") do
|
||||||
|
|
|
@ -4,10 +4,10 @@ import Vuex from 'vuex';
|
||||||
import { stubComponent } from 'helpers/stub_component';
|
import { stubComponent } from 'helpers/stub_component';
|
||||||
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
|
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
|
||||||
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
import BoardSidebarDueDate from '~/boards/components/sidebar/board_sidebar_due_date.vue';
|
||||||
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
|
|
||||||
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
|
||||||
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
|
import BoardSidebarMilestoneSelect from '~/boards/components/sidebar/board_sidebar_milestone_select.vue';
|
||||||
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
|
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
|
||||||
|
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
|
||||||
import { ISSUABLE } from '~/boards/constants';
|
import { ISSUABLE } from '~/boards/constants';
|
||||||
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
|
import { mockIssue, mockIssueGroupPath, mockIssueProjectPath } from '../mock_data';
|
||||||
|
|
||||||
|
@ -102,8 +102,8 @@ describe('BoardContentSidebar', () => {
|
||||||
expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true);
|
expect(wrapper.find(BoardSidebarLabelsSelect).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders BoardSidebarIssueTitle', () => {
|
it('renders BoardSidebarTitle', () => {
|
||||||
expect(wrapper.find(BoardSidebarIssueTitle).exists()).toBe(true);
|
expect(wrapper.find(BoardSidebarTitle).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders BoardSidebarDueDate', () => {
|
it('renders BoardSidebarDueDate', () => {
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
|
import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
|
||||||
import { shallowMount } from '@vue/test-utils';
|
import { shallowMount } from '@vue/test-utils';
|
||||||
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
|
||||||
import BoardSidebarIssueTitle from '~/boards/components/sidebar/board_sidebar_issue_title.vue';
|
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
|
||||||
import { createStore } from '~/boards/stores';
|
import { createStore } from '~/boards/stores';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
|
|
||||||
const TEST_TITLE = 'New issue title';
|
const TEST_TITLE = 'New item title';
|
||||||
const TEST_ISSUE_A = {
|
const TEST_ISSUE_A = {
|
||||||
id: 'gid://gitlab/Issue/1',
|
id: 'gid://gitlab/Issue/1',
|
||||||
iid: 8,
|
iid: 8,
|
||||||
|
@ -21,7 +21,7 @@ const TEST_ISSUE_B = {
|
||||||
|
|
||||||
jest.mock('~/flash');
|
jest.mock('~/flash');
|
||||||
|
|
||||||
describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
let store;
|
let store;
|
||||||
|
|
||||||
|
@ -32,12 +32,12 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
wrapper = null;
|
wrapper = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
const createWrapper = (issue = TEST_ISSUE_A) => {
|
const createWrapper = (item = TEST_ISSUE_A) => {
|
||||||
store = createStore();
|
store = createStore();
|
||||||
store.state.boardItems = { [issue.id]: { ...issue } };
|
store.state.boardItems = { [item.id]: { ...item } };
|
||||||
store.dispatch('setActiveId', { id: issue.id });
|
store.dispatch('setActiveId', { id: item.id });
|
||||||
|
|
||||||
wrapper = shallowMount(BoardSidebarIssueTitle, {
|
wrapper = shallowMount(BoardSidebarTitle, {
|
||||||
store,
|
store,
|
||||||
provide: {
|
provide: {
|
||||||
canUpdate: true,
|
canUpdate: true,
|
||||||
|
@ -53,7 +53,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
const findFormInput = () => wrapper.find(GlFormInput);
|
const findFormInput = () => wrapper.find(GlFormInput);
|
||||||
const findEditableItem = () => wrapper.find(BoardEditableItem);
|
const findEditableItem = () => wrapper.find(BoardEditableItem);
|
||||||
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
|
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
|
||||||
const findTitle = () => wrapper.find('[data-testid="issue-title"]');
|
const findTitle = () => wrapper.find('[data-testid="item-title"]');
|
||||||
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
|
const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]');
|
||||||
|
|
||||||
it('renders title and reference', () => {
|
it('renders title and reference', () => {
|
||||||
|
@ -73,7 +73,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
createWrapper();
|
createWrapper();
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
|
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
|
||||||
store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
|
store.state.boardItems[TEST_ISSUE_A.id].title = TEST_TITLE;
|
||||||
});
|
});
|
||||||
findFormInput().vm.$emit('input', TEST_TITLE);
|
findFormInput().vm.$emit('input', TEST_TITLE);
|
||||||
|
@ -87,7 +87,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('commits change to the server', () => {
|
it('commits change to the server', () => {
|
||||||
expect(wrapper.vm.setActiveIssueTitle).toHaveBeenCalledWith({
|
expect(wrapper.vm.setActiveItemTitle).toHaveBeenCalledWith({
|
||||||
title: TEST_TITLE,
|
title: TEST_TITLE,
|
||||||
projectPath: 'h/b',
|
projectPath: 'h/b',
|
||||||
});
|
});
|
||||||
|
@ -98,14 +98,14 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
createWrapper();
|
createWrapper();
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {});
|
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {});
|
||||||
findFormInput().vm.$emit('input', '');
|
findFormInput().vm.$emit('input', '');
|
||||||
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
findForm().vm.$emit('submit', { preventDefault: () => {} });
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('commits change to the server', () => {
|
it('commits change to the server', () => {
|
||||||
expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
|
expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -122,7 +122,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
it('does not collapses sidebar and shows alert', () => {
|
it('does not collapses sidebar and shows alert', () => {
|
||||||
expect(findCollapsed().isVisible()).toBe(false);
|
expect(findCollapsed().isVisible()).toBe(false);
|
||||||
expect(findAlert().exists()).toBe(true);
|
expect(findAlert().exists()).toBe(true);
|
||||||
expect(localStorage.getItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`)).toBe(
|
expect(localStorage.getItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`)).toBe(
|
||||||
TEST_TITLE,
|
TEST_TITLE,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -130,7 +130,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
|
|
||||||
describe('when accessing the form with pending changes', () => {
|
describe('when accessing the form with pending changes', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
localStorage.setItem(`${TEST_ISSUE_A.id}/issue-title-pending-changes`, TEST_TITLE);
|
localStorage.setItem(`${TEST_ISSUE_A.id}/item-title-pending-changes`, TEST_TITLE);
|
||||||
|
|
||||||
createWrapper();
|
createWrapper();
|
||||||
});
|
});
|
||||||
|
@ -146,7 +146,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
createWrapper(TEST_ISSUE_B);
|
createWrapper(TEST_ISSUE_B);
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
|
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
|
||||||
store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
|
store.state.boardItems[TEST_ISSUE_B.id].title = TEST_TITLE;
|
||||||
});
|
});
|
||||||
findFormInput().vm.$emit('input', TEST_TITLE);
|
findFormInput().vm.$emit('input', TEST_TITLE);
|
||||||
|
@ -155,7 +155,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('collapses sidebar and render former title', () => {
|
it('collapses sidebar and render former title', () => {
|
||||||
expect(wrapper.vm.setActiveIssueTitle).not.toHaveBeenCalled();
|
expect(wrapper.vm.setActiveItemTitle).not.toHaveBeenCalled();
|
||||||
expect(findCollapsed().isVisible()).toBe(true);
|
expect(findCollapsed().isVisible()).toBe(true);
|
||||||
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
|
expect(findTitle().text()).toBe(TEST_ISSUE_B.title);
|
||||||
});
|
});
|
||||||
|
@ -165,7 +165,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
createWrapper(TEST_ISSUE_B);
|
createWrapper(TEST_ISSUE_B);
|
||||||
|
|
||||||
jest.spyOn(wrapper.vm, 'setActiveIssueTitle').mockImplementation(() => {
|
jest.spyOn(wrapper.vm, 'setActiveItemTitle').mockImplementation(() => {
|
||||||
throw new Error(['failed mutation']);
|
throw new Error(['failed mutation']);
|
||||||
});
|
});
|
||||||
findFormInput().vm.$emit('input', 'Invalid title');
|
findFormInput().vm.$emit('input', 'Invalid title');
|
||||||
|
@ -173,7 +173,7 @@ describe('~/boards/components/sidebar/board_sidebar_issue_title.vue', () => {
|
||||||
await wrapper.vm.$nextTick();
|
await wrapper.vm.$nextTick();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('collapses sidebar and renders former issue title', () => {
|
it('collapses sidebar and renders former item title', () => {
|
||||||
expect(findCollapsed().isVisible()).toBe(true);
|
expect(findCollapsed().isVisible()).toBe(true);
|
||||||
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
|
expect(findTitle().text()).toContain(TEST_ISSUE_B.title);
|
||||||
expect(createFlash).toHaveBeenCalled();
|
expect(createFlash).toHaveBeenCalled();
|
|
@ -1223,9 +1223,13 @@ describe('setActiveIssueMilestone', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setActiveIssueTitle', () => {
|
describe('setActiveItemTitle', () => {
|
||||||
const state = { boardItems: { [mockIssue.id]: mockIssue } };
|
const state = {
|
||||||
const getters = { activeBoardItem: mockIssue };
|
boardItems: { [mockIssue.id]: mockIssue },
|
||||||
|
issuableType: 'issue',
|
||||||
|
fullPath: 'path/f',
|
||||||
|
};
|
||||||
|
const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
|
||||||
const testTitle = 'Test Title';
|
const testTitle = 'Test Title';
|
||||||
const input = {
|
const input = {
|
||||||
title: testTitle,
|
title: testTitle,
|
||||||
|
@ -1235,7 +1239,7 @@ describe('setActiveIssueTitle', () => {
|
||||||
it('should commit title after setting the issue', (done) => {
|
it('should commit title after setting the issue', (done) => {
|
||||||
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
|
||||||
data: {
|
data: {
|
||||||
updateIssue: {
|
updateIssuableTitle: {
|
||||||
issue: {
|
issue: {
|
||||||
title: testTitle,
|
title: testTitle,
|
||||||
},
|
},
|
||||||
|
@ -1251,7 +1255,7 @@ describe('setActiveIssueTitle', () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
testAction(
|
testAction(
|
||||||
actions.setActiveIssueTitle,
|
actions.setActiveItemTitle,
|
||||||
input,
|
input,
|
||||||
{ ...state, ...getters },
|
{ ...state, ...getters },
|
||||||
[
|
[
|
||||||
|
@ -1270,7 +1274,7 @@ describe('setActiveIssueTitle', () => {
|
||||||
.spyOn(gqlClient, 'mutate')
|
.spyOn(gqlClient, 'mutate')
|
||||||
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
|
.mockResolvedValue({ data: { updateIssue: { errors: ['failed mutation'] } } });
|
||||||
|
|
||||||
await expect(actions.setActiveIssueTitle({ getters }, input)).rejects.toThrow(Error);
|
await expect(actions.setActiveItemTitle({ getters }, input)).rejects.toThrow(Error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -3,18 +3,11 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
|
RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
|
||||||
let(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let(:user) { create(:user, developer_projects: [project]) }
|
let_it_be(:user) { create(:user, developer_projects: [project]) }
|
||||||
|
|
||||||
let(:seeds_block) { }
|
let(:seeds_block) { }
|
||||||
|
let(:command) { initialize_command }
|
||||||
let(:command) do
|
|
||||||
Gitlab::Ci::Pipeline::Chain::Command.new(
|
|
||||||
project: project,
|
|
||||||
current_user: user,
|
|
||||||
origin_ref: 'master',
|
|
||||||
seeds_block: seeds_block)
|
|
||||||
end
|
|
||||||
|
|
||||||
let(:pipeline) { build(:ci_pipeline, project: project) }
|
let(:pipeline) { build(:ci_pipeline, project: project) }
|
||||||
|
|
||||||
describe '#perform!' do
|
describe '#perform!' do
|
||||||
|
@ -27,13 +20,8 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
|
||||||
end
|
end
|
||||||
|
|
||||||
subject(:run_chain) do
|
subject(:run_chain) do
|
||||||
[
|
run_previous_chain(pipeline, command)
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
|
perform_seed(pipeline, command)
|
||||||
Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
|
|
||||||
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command)
|
|
||||||
].map(&:perform!)
|
|
||||||
|
|
||||||
described_class.new(pipeline, command).perform!
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'allocates next IID' do
|
it 'allocates next IID' do
|
||||||
|
@ -228,5 +216,86 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Seed do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'N+1 queries' do
|
||||||
|
it 'avoids N+1 queries when calculating variables of jobs' do
|
||||||
|
pipeline1, command1 = prepare_pipeline1
|
||||||
|
pipeline2, command2 = prepare_pipeline2
|
||||||
|
|
||||||
|
control = ActiveRecord::QueryRecorder.new do
|
||||||
|
perform_seed(pipeline1, command1)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect { perform_seed(pipeline2, command2) }.not_to exceed_query_limit(
|
||||||
|
control.count + expected_extra_queries
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def prepare_pipeline1
|
||||||
|
config1 = { build: { stage: 'build', script: 'build' } }
|
||||||
|
stub_ci_pipeline_yaml_file(YAML.dump(config1))
|
||||||
|
pipeline1 = build(:ci_pipeline, project: project)
|
||||||
|
command1 = initialize_command
|
||||||
|
|
||||||
|
run_previous_chain(pipeline1, command1)
|
||||||
|
|
||||||
|
[pipeline1, command1]
|
||||||
|
end
|
||||||
|
|
||||||
|
def prepare_pipeline2
|
||||||
|
config2 = { build1: { stage: 'build', script: 'build1' },
|
||||||
|
build2: { stage: 'build', script: 'build2' },
|
||||||
|
test: { stage: 'build', script: 'test' } }
|
||||||
|
stub_ci_pipeline_yaml_file(YAML.dump(config2))
|
||||||
|
pipeline2 = build(:ci_pipeline, project: project)
|
||||||
|
command2 = initialize_command
|
||||||
|
|
||||||
|
run_previous_chain(pipeline2, command2)
|
||||||
|
|
||||||
|
[pipeline2, command2]
|
||||||
|
end
|
||||||
|
|
||||||
|
def expected_extra_queries
|
||||||
|
extra_jobs = 2
|
||||||
|
non_handled_sql_queries = 3
|
||||||
|
|
||||||
|
# 1. Ci::Build Load () SELECT "ci_builds".* FROM "ci_builds"
|
||||||
|
# WHERE "ci_builds"."type" = 'Ci::Build'
|
||||||
|
# AND "ci_builds"."commit_id" IS NULL
|
||||||
|
# AND ("ci_builds"."retried" = FALSE OR "ci_builds"."retried" IS NULL)
|
||||||
|
# AND (stage_idx < 1)
|
||||||
|
# 2. Ci::InstanceVariable Load => `Ci::InstanceVariable#cached_data` => already cached with `fetch_memory_cache`
|
||||||
|
# 3. Ci::Variable Load => `Project#ci_variables_for` => already cached with `Gitlab::SafeRequestStore`
|
||||||
|
|
||||||
|
extra_jobs * non_handled_sql_queries
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def run_previous_chain(pipeline, command)
|
||||||
|
[
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Config::Content.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Config::Process.new(pipeline, command),
|
||||||
|
Gitlab::Ci::Pipeline::Chain::EvaluateWorkflowRules.new(pipeline, command)
|
||||||
|
].map(&:perform!)
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_seed(pipeline, command)
|
||||||
|
described_class.new(pipeline, command).perform!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def initialize_command
|
||||||
|
Gitlab::Ci::Pipeline::Chain::Command.new(
|
||||||
|
project: project,
|
||||||
|
current_user: user,
|
||||||
|
origin_ref: 'master',
|
||||||
|
seeds_block: seeds_block
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -25,6 +25,13 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
|
||||||
let(:definition) { described_class.new(path, attributes) }
|
let(:definition) { described_class.new(path, attributes) }
|
||||||
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
|
let(:yaml_content) { attributes.deep_stringify_keys.to_yaml }
|
||||||
|
|
||||||
|
def write_metric(metric, path, content)
|
||||||
|
path = File.join(metric, path)
|
||||||
|
dir = File.dirname(path)
|
||||||
|
FileUtils.mkdir_p(dir)
|
||||||
|
File.write(path, content)
|
||||||
|
end
|
||||||
|
|
||||||
it 'has all definitons valid' do
|
it 'has all definitons valid' do
|
||||||
expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError)
|
expect { described_class.definitions }.not_to raise_error(Gitlab::Usage::Metric::InvalidMetricError)
|
||||||
end
|
end
|
||||||
|
@ -145,12 +152,54 @@ RSpec.describe Gitlab::Usage::MetricDefinition do
|
||||||
FileUtils.rm_rf(metric1)
|
FileUtils.rm_rf(metric1)
|
||||||
FileUtils.rm_rf(metric2)
|
FileUtils.rm_rf(metric2)
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def write_metric(metric, path, content)
|
describe 'dump_metrics_yaml' do
|
||||||
path = File.join(metric, path)
|
let(:other_attributes) do
|
||||||
dir = File.dirname(path)
|
{
|
||||||
FileUtils.mkdir_p(dir)
|
description: 'Test metric definition',
|
||||||
File.write(path, content)
|
value_type: 'string',
|
||||||
|
product_category: 'collection',
|
||||||
|
product_stage: 'growth',
|
||||||
|
status: 'data_available',
|
||||||
|
default_generation: 'generation_1',
|
||||||
|
key_path: 'counter.category.event',
|
||||||
|
product_group: 'group::product analytics',
|
||||||
|
time_frame: 'none',
|
||||||
|
data_source: 'database',
|
||||||
|
distribution: %w(ee ce),
|
||||||
|
tier: %w(free starter premium ultimate bronze silver gold)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:other_yaml_content) { other_attributes.deep_stringify_keys.to_yaml }
|
||||||
|
let(:other_path) { File.join('metrics', 'test_metric.yml') }
|
||||||
|
let(:metric1) { Dir.mktmpdir('metric1') }
|
||||||
|
let(:metric2) { Dir.mktmpdir('metric2') }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(described_class).to receive(:paths).and_return(
|
||||||
|
[
|
||||||
|
File.join(metric1, '**', '*.yml'),
|
||||||
|
File.join(metric2, '**', '*.yml')
|
||||||
|
]
|
||||||
|
)
|
||||||
|
# Reset memoized `definitions` result
|
||||||
|
described_class.instance_variable_set(:@definitions, nil)
|
||||||
|
end
|
||||||
|
|
||||||
|
after do
|
||||||
|
FileUtils.rm_rf(metric1)
|
||||||
|
FileUtils.rm_rf(metric2)
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.dump_metrics_yaml }
|
||||||
|
|
||||||
|
it 'returns a YAML with both metrics in a sequence' do
|
||||||
|
write_metric(metric1, path, yaml_content)
|
||||||
|
write_metric(metric2, other_path, other_yaml_content)
|
||||||
|
|
||||||
|
is_expected.to eq([attributes, other_attributes].map(&:deep_stringify_keys).to_yaml)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe BlobViewer::GitlabCiYml do
|
||||||
|
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
|
let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
|
||||||
let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
|
let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) }
|
||||||
let(:sha) { sample_commit.id }
|
let(:sha) { sample_commit.id }
|
||||||
|
|
|
@ -7,6 +7,7 @@ RSpec.describe BlobViewer::MetricsDashboardYml do
|
||||||
include RepoHelpers
|
include RepoHelpers
|
||||||
|
|
||||||
let_it_be(:project) { create(:project, :repository) }
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
|
||||||
let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
|
let(:blob) { fake_blob(path: '.gitlab/dashboards/custom-dashboard.yml', data: data) }
|
||||||
let(:sha) { sample_commit.id }
|
let(:sha) { sample_commit.id }
|
||||||
let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
|
let(:data) { fixture_file('lib/gitlab/metrics/dashboard/sample_dashboard.yml') }
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe Clusters::Agent do
|
||||||
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
|
it { is_expected.to belong_to(:created_by_user).class_name('User').optional }
|
||||||
it { is_expected.to belong_to(:project).class_name('::Project') }
|
it { is_expected.to belong_to(:project).class_name('::Project') }
|
||||||
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
|
it { is_expected.to have_many(:agent_tokens).class_name('Clusters::AgentToken') }
|
||||||
|
it { is_expected.to have_many(:last_used_agent_tokens).class_name('Clusters::AgentToken') }
|
||||||
|
|
||||||
it { is_expected.to validate_presence_of(:name) }
|
it { is_expected.to validate_presence_of(:name) }
|
||||||
it { is_expected.to validate_length_of(:name).is_at_most(63) }
|
it { is_expected.to validate_length_of(:name).is_at_most(63) }
|
||||||
|
|
|
@ -9,6 +9,19 @@ RSpec.describe Clusters::AgentToken do
|
||||||
it { is_expected.to validate_length_of(:name).is_at_most(255) }
|
it { is_expected.to validate_length_of(:name).is_at_most(255) }
|
||||||
it { is_expected.to validate_presence_of(:name) }
|
it { is_expected.to validate_presence_of(:name) }
|
||||||
|
|
||||||
|
describe 'scopes' do
|
||||||
|
describe '.order_last_used_at_desc' do
|
||||||
|
let_it_be(:token_1) { create(:cluster_agent_token, last_used_at: 7.days.ago) }
|
||||||
|
let_it_be(:token_2) { create(:cluster_agent_token, last_used_at: nil) }
|
||||||
|
let_it_be(:token_3) { create(:cluster_agent_token, last_used_at: 2.days.ago) }
|
||||||
|
|
||||||
|
it 'sorts by last_used_at descending, with null values at last' do
|
||||||
|
expect(described_class.order_last_used_at_desc)
|
||||||
|
.to eq([token_3, token_1, token_2])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#token' do
|
describe '#token' do
|
||||||
it 'is generated on save' do
|
it 'is generated on save' do
|
||||||
agent_token = build(:cluster_agent_token, token_encrypted: nil)
|
agent_token = build(:cluster_agent_token, token_encrypted: nil)
|
||||||
|
|
|
@ -573,27 +573,39 @@ RSpec.describe Deployment do
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#previous_deployment' do
|
describe '#previous_deployment' do
|
||||||
it 'returns the previous deployment' do
|
using RSpec::Parameterized::TableSyntax
|
||||||
deploy1 = create(:deployment, :success)
|
|
||||||
deploy2 = create(
|
|
||||||
:deployment,
|
|
||||||
project: deploy1.project,
|
|
||||||
environment: deploy1.environment
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(deploy2.previous_deployment).to eq(deploy1)
|
let_it_be(:project) { create(:project, :repository) }
|
||||||
|
let_it_be(:production) { create(:environment, :production, project: project) }
|
||||||
|
let_it_be(:staging) { create(:environment, :staging, project: project) }
|
||||||
|
let_it_be(:production_deployment_1) { create(:deployment, :success, project: project, environment: production) }
|
||||||
|
let_it_be(:production_deployment_2) { create(:deployment, :success, project: project, environment: production) }
|
||||||
|
let_it_be(:production_deployment_3) { create(:deployment, :failed, project: project, environment: production) }
|
||||||
|
let_it_be(:production_deployment_4) { create(:deployment, :canceled, project: project, environment: production) }
|
||||||
|
let_it_be(:staging_deployment_1) { create(:deployment, :failed, project: project, environment: staging) }
|
||||||
|
let_it_be(:staging_deployment_2) { create(:deployment, :success, project: project, environment: staging) }
|
||||||
|
let_it_be(:production_deployment_5) { create(:deployment, :success, project: project, environment: production) }
|
||||||
|
let_it_be(:staging_deployment_3) { create(:deployment, :success, project: project, environment: staging) }
|
||||||
|
|
||||||
|
where(:pointer, :expected_previous_deployment) do
|
||||||
|
'production_deployment_1' | nil
|
||||||
|
'production_deployment_2' | 'production_deployment_1'
|
||||||
|
'production_deployment_3' | 'production_deployment_2'
|
||||||
|
'production_deployment_4' | 'production_deployment_2'
|
||||||
|
'staging_deployment_1' | nil
|
||||||
|
'staging_deployment_2' | nil
|
||||||
|
'production_deployment_5' | 'production_deployment_2'
|
||||||
|
'staging_deployment_3' | 'staging_deployment_2'
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns nothing if the refs do not match' do
|
with_them do
|
||||||
deploy1 = create(:deployment, :success)
|
it 'returns the previous deployment' do
|
||||||
deploy2 = create(
|
if expected_previous_deployment.nil?
|
||||||
:deployment,
|
expect(send(pointer).previous_deployment).to eq(expected_previous_deployment)
|
||||||
:review_app,
|
else
|
||||||
project: deploy1.project,
|
expect(send(pointer).previous_deployment).to eq(send(expected_previous_deployment))
|
||||||
environment: deploy1.environment
|
end
|
||||||
)
|
end
|
||||||
|
|
||||||
expect(deploy2.previous_deployment).to be_nil
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -643,45 +655,6 @@ RSpec.describe Deployment do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#previous_environment_deployment' do
|
|
||||||
it 'returns the previous deployment of the same environment' do
|
|
||||||
deploy1 = create(:deployment, :success)
|
|
||||||
deploy2 = create(
|
|
||||||
:deployment,
|
|
||||||
:success,
|
|
||||||
project: deploy1.project,
|
|
||||||
environment: deploy1.environment
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(deploy2.previous_environment_deployment).to eq(deploy1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'ignores deployments that were not successful' do
|
|
||||||
deploy1 = create(:deployment, :failed)
|
|
||||||
deploy2 = create(
|
|
||||||
:deployment,
|
|
||||||
:success,
|
|
||||||
project: deploy1.project,
|
|
||||||
environment: deploy1.environment
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(deploy2.previous_environment_deployment).to be_nil
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'ignores deployments for different environments' do
|
|
||||||
deploy1 = create(:deployment, :success)
|
|
||||||
preprod = create(:environment, project: deploy1.project, name: 'preprod')
|
|
||||||
deploy2 = create(
|
|
||||||
:deployment,
|
|
||||||
:success,
|
|
||||||
project: deploy1.project,
|
|
||||||
environment: preprod
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(deploy2.previous_environment_deployment).to be_nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#create_ref' do
|
describe '#create_ref' do
|
||||||
let(:deployment) { build(:deployment) }
|
let(:deployment) { build(:deployment) }
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ RSpec.describe ApplicationSetting::TermPolicy do
|
||||||
include TermsHelper
|
include TermsHelper
|
||||||
|
|
||||||
let_it_be(:term) { create(:term) }
|
let_it_be(:term) { create(:term) }
|
||||||
|
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
subject(:policy) { described_class.new(user, term) }
|
subject(:policy) { described_class.new(user, term) }
|
||||||
|
|
|
@ -283,6 +283,7 @@ RSpec.describe Ci::BuildPolicy do
|
||||||
describe 'manage a web ide terminal' do
|
describe 'manage a web ide terminal' do
|
||||||
let(:build_permissions) { %i[read_web_ide_terminal create_build_terminal update_web_ide_terminal create_build_service_proxy] }
|
let(:build_permissions) { %i[read_web_ide_terminal create_build_terminal update_web_ide_terminal create_build_service_proxy] }
|
||||||
let_it_be(:maintainer) { create(:user) }
|
let_it_be(:maintainer) { create(:user) }
|
||||||
|
|
||||||
let(:owner) { create(:owner) }
|
let(:owner) { create(:owner) }
|
||||||
let(:admin) { create(:admin) }
|
let(:admin) { create(:admin) }
|
||||||
let(:maintainer) { create(:user) }
|
let(:maintainer) { create(:user) }
|
||||||
|
|
|
@ -16,6 +16,7 @@ RSpec.describe DesignManagement::DesignPolicy do
|
||||||
let_it_be(:admin) { create(:admin) }
|
let_it_be(:admin) { create(:admin) }
|
||||||
let_it_be(:project) { create(:project, :public, namespace: owner.namespace) }
|
let_it_be(:project) { create(:project, :public, namespace: owner.namespace) }
|
||||||
let_it_be(:issue) { create(:issue, project: project) }
|
let_it_be(:issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
let(:design) { create(:design, issue: issue) }
|
let(:design) { create(:design, issue: issue) }
|
||||||
|
|
||||||
subject(:design_policy) { described_class.new(current_user, design) }
|
subject(:design_policy) { described_class.new(current_user, design) }
|
||||||
|
@ -131,6 +132,7 @@ RSpec.describe DesignManagement::DesignPolicy do
|
||||||
|
|
||||||
context "for guests in private projects" do
|
context "for guests in private projects" do
|
||||||
let_it_be(:project) { create(:project, :private) }
|
let_it_be(:project) { create(:project, :private) }
|
||||||
|
|
||||||
let(:current_user) { guest }
|
let(:current_user) { guest }
|
||||||
|
|
||||||
it_behaves_like "read-only design abilities"
|
it_behaves_like "read-only design abilities"
|
||||||
|
@ -163,6 +165,7 @@ RSpec.describe DesignManagement::DesignPolicy do
|
||||||
context "when the project is archived" do
|
context "when the project is archived" do
|
||||||
let_it_be(:project) { create(:project, :public, :archived) }
|
let_it_be(:project) { create(:project, :public, :archived) }
|
||||||
let_it_be(:issue) { create(:issue, project: project) }
|
let_it_be(:issue) { create(:issue, project: project) }
|
||||||
|
|
||||||
let(:current_user) { owner }
|
let(:current_user) { owner }
|
||||||
|
|
||||||
it_behaves_like "read-only design abilities"
|
it_behaves_like "read-only design abilities"
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe GroupDeployKeysGroupPolicy do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
let_it_be(:group) { create(:group) }
|
let_it_be(:group) { create(:group) }
|
||||||
let_it_be(:group_deploy_key) { create(:group_deploy_key) }
|
let_it_be(:group_deploy_key) { create(:group_deploy_key) }
|
||||||
|
|
||||||
let(:group_deploy_keys_group) { create(:group_deploy_keys_group, group: group, group_deploy_key: group_deploy_key) }
|
let(:group_deploy_keys_group) { create(:group_deploy_keys_group, group: group, group_deploy_key: group_deploy_key) }
|
||||||
|
|
||||||
describe 'edit a group deploy key for a given group' do
|
describe 'edit a group deploy key for a given group' do
|
||||||
|
|
|
@ -722,6 +722,7 @@ RSpec.describe GroupPolicy do
|
||||||
|
|
||||||
describe 'design activity' do
|
describe 'design activity' do
|
||||||
let_it_be(:group) { create(:group, :public) }
|
let_it_be(:group) { create(:group, :public) }
|
||||||
|
|
||||||
let(:current_user) { nil }
|
let(:current_user) { nil }
|
||||||
|
|
||||||
subject { described_class.new(current_user, group) }
|
subject { described_class.new(current_user, group) }
|
||||||
|
|
|
@ -8,6 +8,7 @@ RSpec.describe ProjectSnippetPolicy do
|
||||||
let_it_be(:other_user) { create(:user) }
|
let_it_be(:other_user) { create(:user) }
|
||||||
let_it_be(:external_user) { create(:user, :external) }
|
let_it_be(:external_user) { create(:user, :external) }
|
||||||
let_it_be(:project) { create(:project, :public) }
|
let_it_be(:project) { create(:project, :public) }
|
||||||
|
|
||||||
let(:snippet) { create(:project_snippet, snippet_visibility, project: project, author: author) }
|
let(:snippet) { create(:project_snippet, snippet_visibility, project: project, author: author) }
|
||||||
let(:author) { other_user }
|
let(:author) { other_user }
|
||||||
let(:author_permissions) do
|
let(:author_permissions) do
|
||||||
|
|
|
@ -4,6 +4,7 @@ require 'spec_helper'
|
||||||
|
|
||||||
RSpec.describe ServicePolicy, :models do
|
RSpec.describe ServicePolicy, :models do
|
||||||
let_it_be(:user) { create(:user) }
|
let_it_be(:user) { create(:user) }
|
||||||
|
|
||||||
let(:project) { integration.project }
|
let(:project) { integration.project }
|
||||||
|
|
||||||
subject(:policy) { Ability.policy_for(user, integration) }
|
subject(:policy) { Ability.policy_for(user, integration) }
|
||||||
|
|
|
@ -80,7 +80,7 @@ RSpec.describe 'Setting assignees of a merge request' do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'with assignees already assigned' do
|
context 'with assignees already assigned' do
|
||||||
let(:db_query_limit) { 38 }
|
let(:db_query_limit) { 39 }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
merge_request.assignees = [assignee2]
|
merge_request.assignees = [assignee2]
|
||||||
|
|
|
@ -2151,6 +2151,23 @@ RSpec.describe API::MergeRequests do
|
||||||
let(:entity) { merge_request }
|
let(:entity) { merge_request }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when only assignee_ids are provided' do
|
||||||
|
let(:params) do
|
||||||
|
{
|
||||||
|
assignee_ids: [user2.id]
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sets the assignees' do
|
||||||
|
put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(json_response['assignees']).to contain_exactly(
|
||||||
|
a_hash_including('name' => user2.name)
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'accepts reviewer_ids' do
|
context 'accepts reviewer_ids' do
|
||||||
let(:params) do
|
let(:params) do
|
||||||
{
|
{
|
||||||
|
|
|
@ -161,4 +161,23 @@ RSpec.describe API::UsageData do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'GET /usage_data/metric_definitions' do
|
||||||
|
let(:endpoint) { '/usage_data/metric_definitions' }
|
||||||
|
let(:metric_yaml) do
|
||||||
|
{ 'key_path' => 'counter.category.event', 'description' => 'Metric description' }.to_yaml
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without authentication' do
|
||||||
|
it 'returns a YAML file', :aggregate_failures do
|
||||||
|
allow(Gitlab::Usage::MetricDefinition).to receive(:dump_metrics_yaml).and_return(metric_yaml)
|
||||||
|
|
||||||
|
get api(endpoint)
|
||||||
|
|
||||||
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
|
expect(response.media_type).to eq('application/yaml')
|
||||||
|
expect(response.body).to eq(metric_yaml)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -47,6 +47,12 @@ RSpec.describe MergeRequests::UpdateAssigneesService do
|
||||||
.and change(merge_request, :updated_by).to(user)
|
.and change(merge_request, :updated_by).to(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not update the assignees if they do not have access' do
|
||||||
|
opts[:assignee_ids] = [create(:user).id]
|
||||||
|
|
||||||
|
expect { update_merge_request }.not_to change(merge_request, :assignee_ids)
|
||||||
|
end
|
||||||
|
|
||||||
it 'is more efficient than using the full update-service' do
|
it 'is more efficient than using the full update-service' do
|
||||||
allow(MergeRequests::AssigneesChangeWorker)
|
allow(MergeRequests::AssigneesChangeWorker)
|
||||||
.to receive(:perform_async)
|
.to receive(:perform_async)
|
||||||
|
|
|
@ -22,17 +22,28 @@ RSpec.shared_examples 'an assignable resource' do
|
||||||
assignee_usernames: assignee_usernames)
|
assignee_usernames: assignee_usernames)
|
||||||
end
|
end
|
||||||
|
|
||||||
before do
|
|
||||||
resource.project.add_developer(assignee)
|
|
||||||
resource.project.add_developer(assignee2)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'raises an error if the resource is not accessible to the user' do
|
it 'raises an error if the resource is not accessible to the user' do
|
||||||
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'does not change assignees if the resource is not accessible to the assignees' do
|
||||||
|
resource.project.add_developer(user)
|
||||||
|
|
||||||
|
expect { subject }.not_to change { resource.reload.assignee_ids }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an operational error if the resource is not accessible to the assignees' do
|
||||||
|
resource.project.add_developer(user)
|
||||||
|
|
||||||
|
result = subject
|
||||||
|
|
||||||
|
expect(result[:errors]).to include a_string_matching(/Cannot assign/)
|
||||||
|
end
|
||||||
|
|
||||||
context 'when the user can update the resource' do
|
context 'when the user can update the resource' do
|
||||||
before do
|
before do
|
||||||
|
resource.project.add_developer(assignee)
|
||||||
|
resource.project.add_developer(assignee2)
|
||||||
resource.project.add_developer(user)
|
resource.project.add_developer(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue