Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
dbfedde341
commit
9b8433e5ec
|
@ -164,8 +164,8 @@ overrides:
|
|||
#'@graphql-eslint/unique-fragment-name': error
|
||||
# TODO: Uncomment these rules when then `schema` is available
|
||||
#'@graphql-eslint/fragments-on-composite-type': error
|
||||
'@graphql-eslint/known-argument-names': error
|
||||
'@graphql-eslint/known-type-names': error
|
||||
#'@graphql-eslint/known-argument-names': error
|
||||
#'@graphql-eslint/known-type-names': error
|
||||
'@graphql-eslint/no-anonymous-operations': error
|
||||
'@graphql-eslint/unique-operation-name': error
|
||||
'@graphql-eslint/require-id-when-available': error
|
||||
|
|
|
@ -444,7 +444,7 @@ const Api = {
|
|||
},
|
||||
|
||||
// Return group projects list. Filtered by query
|
||||
groupProjects(groupId, query, options, callback = () => {}, useCustomErrorHandler = false) {
|
||||
groupProjects(groupId, query, options, callback = () => {}) {
|
||||
const url = Api.buildUrl(Api.groupProjectsPath).replace(':id', groupId);
|
||||
const defaults = {
|
||||
search: query,
|
||||
|
@ -456,19 +456,7 @@ const Api = {
|
|||
})
|
||||
.then(({ data, headers }) => {
|
||||
callback(data);
|
||||
|
||||
return { data, headers };
|
||||
})
|
||||
.catch((error) => {
|
||||
if (useCustomErrorHandler) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
createAlert({
|
||||
message: __('Something went wrong while fetching projects'),
|
||||
});
|
||||
|
||||
callback();
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
mutation addItems($items: [ItemInput]) {
|
||||
mutation addItems($items: [Item]) {
|
||||
addToolbarItems(items: $items) @client
|
||||
}
|
||||
|
|
|
@ -8,16 +8,6 @@ type Item {
|
|||
selectedLabel: String
|
||||
}
|
||||
|
||||
input ItemInput {
|
||||
id: ID!
|
||||
label: String!
|
||||
icon: String
|
||||
selected: Boolean
|
||||
group: Int!
|
||||
category: String
|
||||
selectedLabel: String
|
||||
}
|
||||
|
||||
type Items {
|
||||
nodes: [Item]!
|
||||
}
|
||||
|
@ -27,7 +17,7 @@ extend type Query {
|
|||
}
|
||||
|
||||
extend type Mutation {
|
||||
updateToolbarItem(id: ID!, propsToUpdate: ItemInput!): LocalErrors
|
||||
updateToolbarItem(id: ID!, propsToUpdate: Item!): LocalErrors
|
||||
removeToolbarItems(ids: [ID!]): LocalErrors
|
||||
addToolbarItems(items: [ItemInput]): LocalErrors
|
||||
addToolbarItems(items: [Item]): LocalErrors
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
mutation updateItem($id: ID!, $propsToUpdate: ItemInput!) {
|
||||
mutation updateItem($id: ID!, $propsToUpdate: Item!) {
|
||||
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mutation action($action: LocalActionInput) {
|
||||
mutation action($action: LocalAction) {
|
||||
action(action: $action) @client {
|
||||
errors
|
||||
}
|
||||
|
|
|
@ -9,11 +9,6 @@ type LocalEnvironment {
|
|||
autoStopPath: String
|
||||
}
|
||||
|
||||
input LocalActionInput {
|
||||
name: String!
|
||||
playPath: String
|
||||
}
|
||||
|
||||
input LocalEnvironmentInput {
|
||||
id: Int!
|
||||
globalId: ID!
|
||||
|
@ -69,7 +64,7 @@ type LocalPageInfo {
|
|||
|
||||
extend type Query {
|
||||
environmentApp(page: Int, scope: String): LocalEnvironmentApp
|
||||
folder(environment: NestedLocalEnvironmentInput, scope: String): LocalEnvironmentFolder
|
||||
folder(environment: NestedLocalEnvironmentInput): LocalEnvironmentFolder
|
||||
environmentToDelete: LocalEnvironment
|
||||
pageInfo: LocalPageInfo
|
||||
environmentToRollback: LocalEnvironment
|
||||
|
@ -87,5 +82,5 @@ extend type Mutation {
|
|||
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
|
||||
setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
|
||||
setEnvironmentToChangeCanary(environment: LocalEnvironmentInput, weight: Int): LocalErrors
|
||||
action(action: LocalActionInput): LocalErrors
|
||||
action(environment: LocalEnvironmentInput): LocalErrors
|
||||
}
|
||||
|
|
|
@ -381,7 +381,7 @@ export default {
|
|||
<h2 class="text-truncate">{{ error.title }}</h2>
|
||||
</tooltip-on-truncate>
|
||||
<template v-if="error.tags">
|
||||
<gl-badge v-if="error.tags.level" :variant="errorSeverityVariant" class="mr-2">
|
||||
<gl-badge v-if="error.tags.level" :variant="errorSeverityVariant" class="gl-mr-3">
|
||||
{{ errorLevel }}
|
||||
</gl-badge>
|
||||
<gl-badge v-if="error.tags.logger" variant="muted">{{ error.tags.logger }} </gl-badge>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
mutation importGroups($importRequests: [ImportRequestInput!]!) {
|
||||
mutation importGroups($importRequests: [ImportGroupInput!]!) {
|
||||
importGroups(importRequests: $importRequests) @client {
|
||||
id
|
||||
lastImportTarget {
|
||||
|
|
|
@ -1,120 +1,122 @@
|
|||
/* eslint-disable func-names */
|
||||
|
||||
import $ from 'jquery';
|
||||
import createFlash from '~/flash';
|
||||
import Api from './api';
|
||||
import { loadCSSFile } from './lib/utils/css_utils';
|
||||
import { s__ } from './locale';
|
||||
import ProjectSelectComboButton from './project_select_combo_button';
|
||||
|
||||
const projectSelect = () => {
|
||||
loadCSSFile(gon.select2_css_path)
|
||||
.then(() => {
|
||||
$('.ajax-project-select').each(function (i, select) {
|
||||
let placeholder;
|
||||
const simpleFilter = $(select).data('simpleFilter') || false;
|
||||
const isInstantiated = $(select).data('select2');
|
||||
this.groupId = $(select).data('groupId');
|
||||
this.userId = $(select).data('userId');
|
||||
this.includeGroups = $(select).data('includeGroups');
|
||||
this.allProjects = $(select).data('allProjects') || false;
|
||||
this.orderBy = $(select).data('orderBy') || 'id';
|
||||
this.withIssuesEnabled = $(select).data('withIssuesEnabled');
|
||||
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
|
||||
this.withShared =
|
||||
$(select).data('withShared') === undefined ? true : $(select).data('withShared');
|
||||
this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
|
||||
this.allowClear = $(select).data('allowClear') || false;
|
||||
const projectSelect = async () => {
|
||||
await loadCSSFile(gon.select2_css_path);
|
||||
|
||||
placeholder = s__('ProjectSelect|Search for project');
|
||||
$('.ajax-project-select').each(function (i, select) {
|
||||
let placeholder;
|
||||
const simpleFilter = $(select).data('simpleFilter') || false;
|
||||
const isInstantiated = $(select).data('select2');
|
||||
this.groupId = $(select).data('groupId');
|
||||
this.userId = $(select).data('userId');
|
||||
this.includeGroups = $(select).data('includeGroups');
|
||||
this.allProjects = $(select).data('allProjects') || false;
|
||||
this.orderBy = $(select).data('orderBy') || 'id';
|
||||
this.withIssuesEnabled = $(select).data('withIssuesEnabled');
|
||||
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
|
||||
this.withShared =
|
||||
$(select).data('withShared') === undefined ? true : $(select).data('withShared');
|
||||
this.includeProjectsInSubgroups = $(select).data('includeProjectsInSubgroups') || false;
|
||||
this.allowClear = $(select).data('allowClear') || false;
|
||||
|
||||
placeholder = s__('ProjectSelect|Search for project');
|
||||
if (this.includeGroups) {
|
||||
placeholder += s__('ProjectSelect| or group');
|
||||
}
|
||||
|
||||
$(select).select2({
|
||||
placeholder,
|
||||
minimumInputLength: 0,
|
||||
query: (query) => {
|
||||
let projectsCallback;
|
||||
const finalCallback = function (projects) {
|
||||
const data = {
|
||||
results: projects,
|
||||
};
|
||||
return query.callback(data);
|
||||
};
|
||||
if (this.includeGroups) {
|
||||
placeholder += s__('ProjectSelect| or group');
|
||||
}
|
||||
|
||||
$(select).select2({
|
||||
placeholder,
|
||||
minimumInputLength: 0,
|
||||
query: (query) => {
|
||||
let projectsCallback;
|
||||
const finalCallback = function (projects) {
|
||||
const data = {
|
||||
results: projects,
|
||||
};
|
||||
return query.callback(data);
|
||||
projectsCallback = function (projects) {
|
||||
const groupsCallback = function (groups) {
|
||||
const data = groups.concat(projects);
|
||||
return finalCallback(data);
|
||||
};
|
||||
if (this.includeGroups) {
|
||||
projectsCallback = function (projects) {
|
||||
const groupsCallback = function (groups) {
|
||||
const data = groups.concat(projects);
|
||||
return finalCallback(data);
|
||||
};
|
||||
return Api.groups(query.term, {}, groupsCallback);
|
||||
};
|
||||
} else {
|
||||
projectsCallback = finalCallback;
|
||||
}
|
||||
if (this.groupId) {
|
||||
return Api.groupProjects(
|
||||
this.groupId,
|
||||
query.term,
|
||||
{
|
||||
with_issues_enabled: this.withIssuesEnabled,
|
||||
with_merge_requests_enabled: this.withMergeRequestsEnabled,
|
||||
with_shared: this.withShared,
|
||||
include_subgroups: this.includeProjectsInSubgroups,
|
||||
order_by: 'similarity',
|
||||
simple: true,
|
||||
},
|
||||
projectsCallback,
|
||||
);
|
||||
} else if (this.userId) {
|
||||
return Api.userProjects(
|
||||
this.userId,
|
||||
query.term,
|
||||
{
|
||||
with_issues_enabled: this.withIssuesEnabled,
|
||||
with_merge_requests_enabled: this.withMergeRequestsEnabled,
|
||||
with_shared: this.withShared,
|
||||
include_subgroups: this.includeProjectsInSubgroups,
|
||||
},
|
||||
projectsCallback,
|
||||
);
|
||||
}
|
||||
return Api.projects(
|
||||
query.term,
|
||||
{
|
||||
order_by: this.orderBy,
|
||||
with_issues_enabled: this.withIssuesEnabled,
|
||||
with_merge_requests_enabled: this.withMergeRequestsEnabled,
|
||||
membership: !this.allProjects,
|
||||
},
|
||||
projectsCallback,
|
||||
);
|
||||
},
|
||||
id(project) {
|
||||
if (simpleFilter) return project.id;
|
||||
return JSON.stringify({
|
||||
name: project.name,
|
||||
url: project.web_url,
|
||||
return Api.groups(query.term, {}, groupsCallback);
|
||||
};
|
||||
} else {
|
||||
projectsCallback = finalCallback;
|
||||
}
|
||||
if (this.groupId) {
|
||||
return Api.groupProjects(
|
||||
this.groupId,
|
||||
query.term,
|
||||
{
|
||||
with_issues_enabled: this.withIssuesEnabled,
|
||||
with_merge_requests_enabled: this.withMergeRequestsEnabled,
|
||||
with_shared: this.withShared,
|
||||
include_subgroups: this.includeProjectsInSubgroups,
|
||||
order_by: 'similarity',
|
||||
simple: true,
|
||||
},
|
||||
projectsCallback,
|
||||
).catch(() => {
|
||||
createFlash({
|
||||
message: s__('ProjectSelect|Something went wrong while fetching projects'),
|
||||
});
|
||||
});
|
||||
} else if (this.userId) {
|
||||
return Api.userProjects(
|
||||
this.userId,
|
||||
query.term,
|
||||
{
|
||||
with_issues_enabled: this.withIssuesEnabled,
|
||||
with_merge_requests_enabled: this.withMergeRequestsEnabled,
|
||||
with_shared: this.withShared,
|
||||
include_subgroups: this.includeProjectsInSubgroups,
|
||||
},
|
||||
projectsCallback,
|
||||
);
|
||||
}
|
||||
return Api.projects(
|
||||
query.term,
|
||||
{
|
||||
order_by: this.orderBy,
|
||||
with_issues_enabled: this.withIssuesEnabled,
|
||||
with_merge_requests_enabled: this.withMergeRequestsEnabled,
|
||||
membership: !this.allProjects,
|
||||
},
|
||||
text(project) {
|
||||
return project.name_with_namespace || project.name;
|
||||
},
|
||||
|
||||
initSelection(el, callback) {
|
||||
// eslint-disable-next-line promise/no-nesting
|
||||
return Api.project(el.val()).then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
allowClear: this.allowClear,
|
||||
|
||||
dropdownCssClass: 'ajax-project-dropdown',
|
||||
projectsCallback,
|
||||
);
|
||||
},
|
||||
id(project) {
|
||||
if (simpleFilter) return project.id;
|
||||
return JSON.stringify({
|
||||
name: project.name,
|
||||
url: project.web_url,
|
||||
});
|
||||
if (isInstantiated || simpleFilter) return select;
|
||||
return new ProjectSelectComboButton(select);
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
},
|
||||
text(project) {
|
||||
return project.name_with_namespace || project.name;
|
||||
},
|
||||
|
||||
initSelection(el, callback) {
|
||||
return Api.project(el.val()).then(({ data }) => callback(data));
|
||||
},
|
||||
|
||||
allowClear: this.allowClear,
|
||||
|
||||
dropdownCssClass: 'ajax-project-dropdown',
|
||||
});
|
||||
if (isInstantiated || simpleFilter) return select;
|
||||
return new ProjectSelectComboButton(select);
|
||||
});
|
||||
};
|
||||
|
||||
export default () => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#import "ee_else_ce/repository/queries/commit.fragment.graphql"
|
||||
|
||||
query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Int!) {
|
||||
query getCommit($fileName: String!, $type: String!, $path: String!, $maxOffset: Number!) {
|
||||
commit(path: $path, fileName: $fileName, type: $type, maxOffset: $maxOffset) @client {
|
||||
...TreeEntryCommit
|
||||
}
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
type LogTreeCommit {
|
||||
sha: String
|
||||
message: String
|
||||
titleHtml: String
|
||||
committedDate: Time
|
||||
commitPath: String
|
||||
fileName: String
|
||||
filePath: String
|
||||
type: String
|
||||
}
|
|
@ -18,7 +18,7 @@ export const fetchGroups = ({ commit }, search) => {
|
|||
});
|
||||
};
|
||||
|
||||
export const fetchProjects = ({ commit, state }, search, emptyCallback = () => {}) => {
|
||||
export const fetchProjects = ({ commit, state }, search) => {
|
||||
commit(types.REQUEST_PROJECTS);
|
||||
const groupId = state.query?.group_id;
|
||||
|
||||
|
@ -31,17 +31,11 @@ export const fetchProjects = ({ commit, state }, search, emptyCallback = () => {
|
|||
};
|
||||
|
||||
if (groupId) {
|
||||
Api.groupProjects(
|
||||
groupId,
|
||||
search,
|
||||
{
|
||||
order_by: 'similarity',
|
||||
with_shared: false,
|
||||
include_subgroups: true,
|
||||
},
|
||||
emptyCallback,
|
||||
true,
|
||||
)
|
||||
Api.groupProjects(groupId, search, {
|
||||
order_by: 'similarity',
|
||||
with_shared: false,
|
||||
include_subgroups: true,
|
||||
})
|
||||
.then(handleSuccess)
|
||||
.catch(handleCatch);
|
||||
} else {
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
mutation addDataToTerraformState($terraformState: LocalTerraformStateInput!) {
|
||||
mutation addDataToTerraformState($terraformState: State!) {
|
||||
addDataToTerraformState(terraformState: $terraformState) @client
|
||||
}
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
extend type TerraformState {
|
||||
_showDetails: Boolean
|
||||
errorMessages: [String]
|
||||
loadingLock: Boolean
|
||||
loadingRemove: Boolean
|
||||
}
|
||||
|
||||
input LocalTerraformStateInput {
|
||||
_showDetails: Boolean
|
||||
errorMessages: [String]
|
||||
loadingLock: Boolean
|
||||
loadingRemove: Boolean
|
||||
id: ID!
|
||||
name: String!
|
||||
lockedAt: Time
|
||||
updatedAt: Time!
|
||||
deletedAt: Time
|
||||
}
|
||||
|
||||
extend type Mutation {
|
||||
addDataToTerraformState(terraformState: LocalTerraformStateInput!): Boolean
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
|
||||
import createFlash from '~/flash';
|
||||
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
|
||||
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
|
||||
|
@ -11,9 +11,11 @@ import eventHub from '../../event_hub';
|
|||
import approvalsMixin from '../../mixins/approvals';
|
||||
import MrWidgetContainer from '../mr_widget_container.vue';
|
||||
import MrWidgetIcon from '../mr_widget_icon.vue';
|
||||
import { INVALID_RULES_DOCS_PATH } from '../../constants';
|
||||
import ApprovalsSummary from './approvals_summary.vue';
|
||||
import ApprovalsSummaryOptional from './approvals_summary_optional.vue';
|
||||
import { FETCH_LOADING, FETCH_ERROR, APPROVE_ERROR, UNAPPROVE_ERROR } from './messages';
|
||||
import { humanizeInvalidApproversRules } from './humanized_text';
|
||||
|
||||
export default {
|
||||
name: 'MRWidgetApprovals',
|
||||
|
@ -23,6 +25,8 @@ export default {
|
|||
ApprovalsSummary,
|
||||
ApprovalsSummaryOptional,
|
||||
GlButton,
|
||||
GlSprintf,
|
||||
GlLink,
|
||||
},
|
||||
mixins: [approvalsMixin, glFeatureFlagsMixin()],
|
||||
props: {
|
||||
|
@ -78,6 +82,15 @@ export default {
|
|||
approvals() {
|
||||
return this.mr.approvals || {};
|
||||
},
|
||||
invalidRules() {
|
||||
return this.approvals.invalid_approvers_rules || [];
|
||||
},
|
||||
hasInvalidRules() {
|
||||
return this.approvals.merge_request_approvers_available && this.invalidRules.length;
|
||||
},
|
||||
invalidRulesText() {
|
||||
return humanizeInvalidApproversRules(this.invalidRules);
|
||||
},
|
||||
approvedBy() {
|
||||
return this.approvals.approved_by ? this.approvals.approved_by.map((x) => x.user) : [];
|
||||
},
|
||||
|
@ -117,6 +130,11 @@ export default {
|
|||
|
||||
return null;
|
||||
},
|
||||
pluralizedRuleText() {
|
||||
return this.invalidRules.length > 1
|
||||
? this.$options.i18n.invalidRulesPlural
|
||||
: this.$options.i18n.invalidRuleSingular;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.refreshApprovals()
|
||||
|
@ -193,6 +211,16 @@ export default {
|
|||
},
|
||||
},
|
||||
FETCH_LOADING,
|
||||
linkToInvalidRules: INVALID_RULES_DOCS_PATH,
|
||||
i18n: {
|
||||
invalidRuleSingular: s__(
|
||||
'mrWidget|Approval rule %{rules} is invalid. GitLab has approved this rule automatically to unblock the merge request. %{link}',
|
||||
),
|
||||
invalidRulesPlural: s__(
|
||||
'mrWidget|Approval rules %{rules} are invalid. GitLab has approved these rules automatically to unblock the merge request. %{link}',
|
||||
),
|
||||
learnMore: __('Learn more.'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
|
@ -201,29 +229,45 @@ export default {
|
|||
<mr-widget-icon name="approval" />
|
||||
<div v-if="fetchingApprovals">{{ $options.FETCH_LOADING }}</div>
|
||||
<template v-else>
|
||||
<gl-button
|
||||
v-if="action"
|
||||
:variant="action.variant"
|
||||
:category="action.category"
|
||||
:loading="isApproving"
|
||||
class="mr-3"
|
||||
data-qa-selector="approve_button"
|
||||
@click="action.action"
|
||||
>
|
||||
{{ action.text }}
|
||||
</gl-button>
|
||||
<approvals-summary-optional
|
||||
v-if="isOptional"
|
||||
:can-approve="hasAction"
|
||||
:help-path="mr.approvalsHelpPath"
|
||||
/>
|
||||
<approvals-summary
|
||||
v-else
|
||||
:approved="isApproved"
|
||||
:approvals-left="approvals.approvals_left || 0"
|
||||
:rules-left="approvals.approvalRuleNamesLeft"
|
||||
:approvers="approvedBy"
|
||||
/>
|
||||
<div class="gl-display-flex gl-flex-direction-column">
|
||||
<div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
|
||||
<gl-button
|
||||
v-if="action"
|
||||
:variant="action.variant"
|
||||
:category="action.category"
|
||||
:loading="isApproving"
|
||||
class="gl-mr-5"
|
||||
data-qa-selector="approve_button"
|
||||
@click="action.action"
|
||||
>
|
||||
{{ action.text }}
|
||||
</gl-button>
|
||||
<approvals-summary-optional
|
||||
v-if="isOptional"
|
||||
:can-approve="hasAction"
|
||||
:help-path="mr.approvalsHelpPath"
|
||||
/>
|
||||
<approvals-summary
|
||||
v-else
|
||||
:approved="isApproved"
|
||||
:approvals-left="approvals.approvals_left || 0"
|
||||
:rules-left="approvals.approvalRuleNamesLeft"
|
||||
:approvers="approvedBy"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="hasInvalidRules" class="gl-text-gray-400 gl-mt-2" data-testid="invalid-rules">
|
||||
<gl-sprintf :message="pluralizedRuleText">
|
||||
<template #rules>
|
||||
{{ invalidRulesText }}
|
||||
</template>
|
||||
<template #link>
|
||||
<gl-link :href="$options.linkToInvalidRules" target="_blank">
|
||||
{{ $options.i18n.learnMore }}
|
||||
</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</div>
|
||||
</div>
|
||||
<slot
|
||||
:is-approving="isApproving"
|
||||
:approve-with-auth="approveWithAuth"
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { __ } from '~/locale';
|
||||
|
||||
const humanizeRules = (invalidRules) => {
|
||||
if (invalidRules.length > 1) {
|
||||
return invalidRules.reduce((rules, { name }, index) => {
|
||||
if (index === invalidRules.length - 1) {
|
||||
return `${rules}${__(' and ')}"${name}"`;
|
||||
}
|
||||
return rules ? `${rules}, "${name}"` : `"${name}"`;
|
||||
}, '');
|
||||
}
|
||||
return `"${invalidRules[0].name}"`;
|
||||
};
|
||||
|
||||
export const humanizeInvalidApproversRules = (invalidRules) => {
|
||||
const ruleCount = invalidRules.length;
|
||||
|
||||
if (!ruleCount) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return humanizeRules(invalidRules);
|
||||
};
|
|
@ -13,7 +13,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="circle-icon-container gl-mr-3 align-self-start align-self-lg-center">
|
||||
<div class="circle-icon-container gl-mr-3 align-self-start">
|
||||
<gl-icon :name="name" :size="24" />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { s__ } from '~/locale';
|
||||
import { helpPagePath } from '~/helpers/help_page_helper';
|
||||
import { stateToComponentMap as classStateMap, stateKey } from './stores/state_maps';
|
||||
|
||||
export const SUCCESS = 'success';
|
||||
|
@ -166,3 +167,10 @@ export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500';
|
|||
export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700';
|
||||
|
||||
export { STATE_MACHINE };
|
||||
|
||||
export const INVALID_RULES_DOCS_PATH = helpPagePath(
|
||||
'user/project/merge_requests/approvals/index.md',
|
||||
{
|
||||
anchor: 'invalid-rules',
|
||||
},
|
||||
);
|
||||
|
|
|
@ -21,7 +21,7 @@ extend type WorkItem {
|
|||
mockWidgets: [LocalWorkItemWidget]
|
||||
}
|
||||
|
||||
input LocalWorkItemAssigneesInput {
|
||||
type LocalWorkItemAssigneesInput {
|
||||
id: WorkItemID!
|
||||
assigneeIds: [ID!]
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $gray-10;
|
||||
background-color: $gray-normal;
|
||||
}
|
||||
|
||||
svg {
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
background-color: $indigo-50;
|
||||
border-top-left-radius: 2px;
|
||||
border-top-right-radius: 2px;
|
||||
border-bottom: 1px solid darken($gray-10, 8%);
|
||||
border-bottom: 1px solid darken($gray-normal, 8%);
|
||||
}
|
||||
|
||||
.feature-highlight-popover {
|
||||
|
|
|
@ -464,7 +464,7 @@
|
|||
float: left;
|
||||
margin-right: 5px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $gray-10;
|
||||
border: 1px solid $gray-normal;
|
||||
}
|
||||
|
||||
.notification-dot {
|
||||
|
|
|
@ -452,7 +452,7 @@
|
|||
@mixin avatar-counter($border-radius: 1em) {
|
||||
background-color: $gray-darkest;
|
||||
color: $white;
|
||||
border: 1px solid $gray-10;
|
||||
border: 1px solid $gray-normal;
|
||||
border-radius: $border-radius;
|
||||
font-family: $regular-font;
|
||||
font-size: 9px;
|
||||
|
|
|
@ -95,7 +95,7 @@
|
|||
display: block;
|
||||
align-self: stretch;
|
||||
min-height: 0;
|
||||
background-color: $gray-10;
|
||||
background-color: $gray-normal;
|
||||
border-top: 1px solid $border-color;
|
||||
|
||||
.table-action-buttons {
|
||||
|
|
|
@ -92,6 +92,7 @@ $white-transparent: rgba($white, 0.8) !default;
|
|||
$gray-lightest: #fdfdfd !default;
|
||||
$gray-light: #fafafa !default;
|
||||
$gray-lighter: #f9f9f9 !default;
|
||||
$gray-normal: #f5f5f5 !default;
|
||||
$gray-dark: darken($gray-light, $darken-dark-factor) !default;
|
||||
$gray-darker: #eee !default;
|
||||
$gray-darkest: #c4c4c4 !default;
|
||||
|
@ -350,13 +351,13 @@ $border-white-light: darken($white, $darken-border-factor) !default;
|
|||
$border-white-normal: darken($white-normal, $darken-border-factor) !default;
|
||||
|
||||
$border-gray-light: darken($gray-light, $darken-border-factor);
|
||||
$border-gray-normal: darken($gray-10, $darken-border-factor);
|
||||
$border-gray-normal-dashed: darken($gray-10, $darken-border-dashed-factor);
|
||||
$border-gray-normal: darken($gray-normal, $darken-border-factor);
|
||||
$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
|
||||
|
||||
/*
|
||||
* UI elements
|
||||
*/
|
||||
$contextual-sidebar-bg-color: $gray-10;
|
||||
$contextual-sidebar-bg-color: #f5f5f5;
|
||||
$contextual-sidebar-border-color: #e9e9e9;
|
||||
$border-color: $gray-100;
|
||||
$shadow-color: $t-gray-a-08;
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
}
|
||||
|
||||
.dark-well {
|
||||
background-color: $gray-10;
|
||||
background-color: $gray-normal;
|
||||
}
|
||||
|
||||
.card.card-body-centered {
|
||||
|
|
|
@ -95,17 +95,17 @@ $conflict-colors: (
|
|||
solarized_dark_header_not_chosen : rgba(#839496, 0.25),
|
||||
solarized_dark_line_not_chosen : rgba(#839496, 0.15),
|
||||
|
||||
none_header_head_neutral : $gray-10,
|
||||
none_line_head_neutral : $gray-10,
|
||||
none_button_head_neutral : $gray-10,
|
||||
none_header_head_neutral : $gray-normal,
|
||||
none_line_head_neutral : $gray-normal,
|
||||
none_button_head_neutral : $gray-normal,
|
||||
|
||||
none_header_head_chosen : $gray-darker,
|
||||
none_line_head_chosen : $gray-darker,
|
||||
none_button_head_chosen : $gray-darker,
|
||||
|
||||
none_header_origin_neutral : $gray-10,
|
||||
none_line_origin_neutral : $gray-10,
|
||||
none_button_origin_neutral : $gray-10,
|
||||
none_header_origin_neutral : $gray-normal,
|
||||
none_line_origin_neutral : $gray-normal,
|
||||
none_button_origin_neutral : $gray-normal,
|
||||
|
||||
none_header_origin_chosen : $gray-darker,
|
||||
none_line_origin_chosen : $gray-darker,
|
||||
|
|
|
@ -140,7 +140,7 @@
|
|||
}
|
||||
|
||||
hr {
|
||||
border-color: var(--ide-border-color, darken($gray-10, 8%));
|
||||
border-color: var(--ide-border-color, darken($gray-normal, 8%));
|
||||
}
|
||||
|
||||
.md h1,
|
||||
|
|
|
@ -93,7 +93,7 @@ $ide-commit-header-height: 48px;
|
|||
display: flex;
|
||||
align-items: center;
|
||||
padding: $grid-size $gl-padding;
|
||||
background-color: var(--ide-background-hover, $gray-10);
|
||||
background-color: var(--ide-background-hover, $gray-normal);
|
||||
border-right: 1px solid var(--ide-border-color, $white-dark);
|
||||
border-bottom: 1px solid var(--ide-border-color, $white-dark);
|
||||
|
||||
|
@ -135,7 +135,7 @@ $ide-commit-header-height: 48px;
|
|||
box-shadow: none !important;
|
||||
font-weight: normal !important;
|
||||
|
||||
background-color: var(--ide-background-hover, $gray-10);
|
||||
background-color: var(--ide-background-hover, $gray-normal);
|
||||
border-right: 1px solid var(--ide-border-color, $white-dark);
|
||||
border-bottom: 1px solid var(--ide-border-color, $white-dark);
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
border-left: 1px solid $border-color;
|
||||
border-bottom: 0;
|
||||
border-radius: $border-radius-small $border-radius-small 0 0;
|
||||
background: $gray-10;
|
||||
background: $gray-normal;
|
||||
}
|
||||
|
||||
#editor,
|
||||
|
|
|
@ -17,14 +17,14 @@
|
|||
|
||||
.issue-token:hover &,
|
||||
.issue-token-link:focus > & {
|
||||
background-color: $gray-10;
|
||||
background-color: $gray-normal;
|
||||
color: $blue-800;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.issue-token-title {
|
||||
background-color: $gray-10;
|
||||
background-color: $gray-normal;
|
||||
transition: background $general-hover-transition-duration $general-hover-transition-curve;
|
||||
|
||||
.issue-token:hover &,
|
||||
|
@ -34,7 +34,7 @@
|
|||
}
|
||||
|
||||
.issue-token-remove-button {
|
||||
background-color: $gray-10;
|
||||
background-color: $gray-normal;
|
||||
transition: background $general-hover-transition-duration $general-hover-transition-curve;
|
||||
|
||||
&:hover,
|
||||
|
|
|
@ -276,7 +276,7 @@ input[type='checkbox']:hover {
|
|||
width: $search-avatar-size;
|
||||
height: $search-avatar-size;
|
||||
border-radius: 50%;
|
||||
border: 1px solid $gray-10;
|
||||
border: 1px solid $gray-normal;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ body {
|
|||
line-height: 1.5;
|
||||
color: #fafafa;
|
||||
text-align: left;
|
||||
background-color: #333;
|
||||
background-color: #1f1f1f;
|
||||
}
|
||||
ul {
|
||||
margin-top: 0;
|
||||
|
@ -430,7 +430,7 @@ a.gl-badge.badge-warning:active {
|
|||
.gl-form-input:not(.form-control-plaintext):not([type="color"]):read-only,
|
||||
.gl-form-input.form-control:disabled,
|
||||
.gl-form-input.form-control:not(.form-control-plaintext):not([type="color"]):read-only {
|
||||
background-color: #333;
|
||||
background-color: #1f1f1f;
|
||||
box-shadow: inset 0 0 0 1px #404040;
|
||||
}
|
||||
.gl-form-input:disabled,
|
||||
|
@ -1034,7 +1034,7 @@ input {
|
|||
z-index: 600;
|
||||
width: 256px;
|
||||
top: var(--header-height, 48px);
|
||||
background-color: #333;
|
||||
background-color: #f5f5f5;
|
||||
border-right: 1px solid #e9e9e9;
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
|
@ -1402,7 +1402,7 @@ input {
|
|||
color: #999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: #333;
|
||||
background-color: #f5f5f5;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
width: 255px;
|
||||
|
@ -1698,7 +1698,7 @@ svg.s16 {
|
|||
border-radius: 4px;
|
||||
}
|
||||
body.gl-dark {
|
||||
--gray-10: #333;
|
||||
--gray-10: #1f1f1f;
|
||||
--gray-50: #303030;
|
||||
--gray-100: #404040;
|
||||
--gray-200: #525252;
|
||||
|
@ -1939,7 +1939,7 @@ body.gl-dark .navbar-gitlab .search form .search-input {
|
|||
}
|
||||
|
||||
body.gl-dark {
|
||||
--gray-10: #333;
|
||||
--gray-10: #1f1f1f;
|
||||
--gray-50: #303030;
|
||||
--gray-100: #404040;
|
||||
--gray-200: #525252;
|
||||
|
|
|
@ -86,7 +86,7 @@ $purple-950: #f4f0ff;
|
|||
$gray-lightest: #222;
|
||||
$gray-light: $gray-50;
|
||||
$gray-lighter: #303030;
|
||||
$gray-10: #333;
|
||||
$gray-normal: #333;
|
||||
$gray-dark: $gray-100;
|
||||
$gray-darker: #4f4f4f;
|
||||
$gray-darkest: #c4c4c4;
|
||||
|
|
|
@ -16,6 +16,8 @@ module Types
|
|||
value 'POPULARITY_DESC', 'Number of upvotes (awarded "thumbs up" emoji) by descending order.', value: :popularity_desc
|
||||
value 'ESCALATION_STATUS_ASC', 'Status from triggered to resolved.', value: :escalation_status_asc
|
||||
value 'ESCALATION_STATUS_DESC', 'Status from resolved to triggered.', value: :escalation_status_desc
|
||||
value 'CLOSED_AT_ASC', 'Closed time by ascending order.', value: :closed_at_asc
|
||||
value 'CLOSED_AT_DESC', 'Closed time by descending order.', value: :closed_at_desc
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -122,12 +122,13 @@ class Issue < ApplicationRecord
|
|||
scope :order_due_date_asc, -> { reorder(arel_table[:due_date].asc.nulls_last) }
|
||||
scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) }
|
||||
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
|
||||
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
|
||||
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
|
||||
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
|
||||
scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
|
||||
scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) }
|
||||
scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) }
|
||||
scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) }
|
||||
scope :order_closed_at_desc, -> { reorder(arel_table[:closed_at].desc.nulls_last) }
|
||||
|
||||
scope :preload_associated_models, -> { preload(:assignees, :labels, project: :namespace) }
|
||||
scope :with_web_entity_associations, -> { preload(:author, project: [:project_feature, :route, namespace: :route]) }
|
||||
|
@ -331,6 +332,8 @@ class Issue < ApplicationRecord
|
|||
when 'severity_desc' then order_severity_desc.with_order_id_desc
|
||||
when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
|
||||
when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
|
||||
when 'closed_at_asc' then order_closed_at_asc
|
||||
when 'closed_at_desc' then order_closed_at_desc
|
||||
else
|
||||
super
|
||||
end
|
||||
|
|
|
@ -20,7 +20,7 @@ module Boards
|
|||
private
|
||||
|
||||
def order(items)
|
||||
return items.order_closed_date_desc if list&.closed?
|
||||
return items.order_closed_at_desc if list&.closed?
|
||||
|
||||
items.order_by_relative_position
|
||||
end
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PrepareIndexIssuesOnProjectIdAndClosedAt < Gitlab::Database::Migration[2.0]
|
||||
NEW_INDEX_NAME_1 = 'index_issues_on_project_id_closed_at_desc_state_id_and_id'
|
||||
NEW_INDEX_NAME_2 = 'index_issues_on_project_id_closed_at_state_id_and_id'
|
||||
|
||||
def up
|
||||
# Index to improve performance when sorting issues by closed_at desc
|
||||
prepare_async_index :issues, 'project_id, closed_at DESC NULLS LAST, state_id, id', name: NEW_INDEX_NAME_1
|
||||
|
||||
# Index to improve performance when sorting issues by closed_at asc
|
||||
# This replaces the old index which didn't account for state_id and id
|
||||
prepare_async_index :issues, [:project_id, :closed_at, :state_id, :id], name: NEW_INDEX_NAME_2
|
||||
end
|
||||
|
||||
def down
|
||||
unprepare_async_index_by_name :issues, NEW_INDEX_NAME_1
|
||||
unprepare_async_index_by_name :issues, NEW_INDEX_NAME_2
|
||||
end
|
||||
end
|
|
@ -0,0 +1 @@
|
|||
2c177b0199019ebdbc06b43d21d47a35453e3b376ccbde21163128c77826478b
|
|
@ -18989,6 +18989,8 @@ Values for sorting issues.
|
|||
| ----- | ----------- |
|
||||
| <a id="issuesortblocking_issues_asc"></a>`BLOCKING_ISSUES_ASC` | Blocking issues count by ascending order. |
|
||||
| <a id="issuesortblocking_issues_desc"></a>`BLOCKING_ISSUES_DESC` | Blocking issues count by descending order. |
|
||||
| <a id="issuesortclosed_at_asc"></a>`CLOSED_AT_ASC` | Closed time by ascending order. |
|
||||
| <a id="issuesortclosed_at_desc"></a>`CLOSED_AT_DESC` | Closed time by descending order. |
|
||||
| <a id="issuesortcreated_asc"></a>`CREATED_ASC` | Created at ascending order. |
|
||||
| <a id="issuesortcreated_desc"></a>`CREATED_DESC` | Created at descending order. |
|
||||
| <a id="issuesortdue_date_asc"></a>`DUE_DATE_ASC` | Due date by ascending order. |
|
||||
|
|
|
@ -590,6 +590,39 @@ curl --request POST --header "Gitlab-Kas-Api-Request: <JWT token>" \
|
|||
--data '{ "uuids": ["102e8a0a-fe29-59bd-b46c-57c3e9bc6411", "5eb12985-0ed5-51f4-b545-fd8871dc2870"] }'
|
||||
```
|
||||
|
||||
### Scan Execution Policies
|
||||
|
||||
Called from GitLab agent server (`kas`) to retrieve `scan_execution_policies`
|
||||
configured for the project belonging to the agent token. GitLab `kas` uses
|
||||
this to configure the agent to scan images in the Kubernetes cluster based on the policy.
|
||||
|
||||
```plaintext
|
||||
GET /internal/kubernetes/modules/starboard_vulnerability/scan_execution_policies
|
||||
```
|
||||
|
||||
Example Request:
|
||||
|
||||
```shell
|
||||
curl --request GET --header "Gitlab-Kas-Api-Request: <JWT token>" \
|
||||
--header "Authorization: Bearer <agent token>" "http://localhost:3000/api/v4/internal/kubernetes/modules/starboard_vulnerability/scan_execution_policies"
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"policies": [
|
||||
{
|
||||
"name": "Policy",
|
||||
"description": "Policy description",
|
||||
"enabled": true,
|
||||
"yaml": "---\nname: Policy\ndescription: 'Policy description'\nenabled: true\nactions:\n- scan: container_scanning\nrules:\n- type: pipeline\n branches:\n - main\n",
|
||||
"updated_at": "2022-06-02T05:36:26+00:00"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Subscriptions
|
||||
|
||||
The subscriptions endpoint is used by [CustomersDot](https://gitlab.com/gitlab-org/customers-gitlab-com) (`customers.gitlab.com`)
|
||||
|
|
|
@ -46,7 +46,7 @@ boards: add_metric('CountBoardsMetric', time_frame: 'all'),
|
|||
|
||||
There are several types of counters for metrics:
|
||||
|
||||
- **[Batch counters](#batch-counters)**: Used for counts and sums.
|
||||
- **[Batch counters](#batch-counters)**: Used for counts, sums, and averages.
|
||||
- **[Redis counters](#redis-counters):** Used for in-memory counts.
|
||||
- **[Alternative counters](#alternative-counters):** Used for settings and configurations.
|
||||
|
||||
|
@ -111,9 +111,23 @@ Method:
|
|||
add_metric('JiraImportsTotalImportedIssuesCountMetric')
|
||||
```
|
||||
|
||||
#### Average batch operation
|
||||
|
||||
Average the values of a given `ActiveRecord_Relation` on given column and handles errors.
|
||||
|
||||
Method:
|
||||
|
||||
```ruby
|
||||
add_metric('CountIssuesWeightAverageMetric')
|
||||
```
|
||||
|
||||
Examples:
|
||||
|
||||
Examples using `usage_data.rb` have been [deprecated](usage_data.md). We recommend to use [instrumentation classes](metrics_instrumentation.md).
|
||||
|
||||
#### Grouping and batch operations
|
||||
|
||||
The `count`, `distinct_count`, and `sum` batch counters can accept an `ActiveRecord::Relation`
|
||||
The `count`, `distinct_count`, `sum`, and `average` batch counters can accept an `ActiveRecord::Relation`
|
||||
object, which groups by a specified column. With a grouped relation, the methods do batch counting,
|
||||
handle errors, and returns a hash table of key-value pairs.
|
||||
|
||||
|
@ -128,6 +142,9 @@ distinct_count(Project.group(:visibility_level), :creator_id)
|
|||
|
||||
sum(Issue.group(:state_id), :weight))
|
||||
# returns => {1=>3542, 2=>6820}
|
||||
|
||||
average(Issue.group(:state_id), :weight))
|
||||
# returns => {1=>3.5, 2=>2.5}
|
||||
```
|
||||
|
||||
#### Add operation
|
||||
|
|
|
@ -38,7 +38,7 @@ We have built a domain-specific language (DSL) to define the metrics instrumenta
|
|||
|
||||
## Database metrics
|
||||
|
||||
- `operation`: Operations for the given `relation`, one of `count`, `distinct_count`, `sum`.
|
||||
- `operation`: Operations for the given `relation`, one of `count`, `distinct_count`, `sum`, and `average`.
|
||||
- `relation`: `ActiveRecord::Relation` for the objects we want to perform the `operation`.
|
||||
- `start`: Specifies the start value of the batch counting, by default is `relation.minimum(:id)`.
|
||||
- `finish`: Specifies the end value of the batch counting, by default is `relation.maximum(:id)`.
|
||||
|
@ -127,6 +127,26 @@ module Gitlab
|
|||
end
|
||||
```
|
||||
|
||||
### Average Example
|
||||
|
||||
```ruby
|
||||
# frozen_string_literal: true
|
||||
|
||||
module Gitlab
|
||||
module Usage
|
||||
module Metrics
|
||||
module Instrumentations
|
||||
class CountIssuesWeightAverageMetric < DatabaseMetric
|
||||
operation :average, column: :weight
|
||||
|
||||
relation { Issue }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Redis metrics
|
||||
|
||||
[Example of a merge request that adds a `Redis` metric](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66582).
|
||||
|
@ -288,7 +308,7 @@ end
|
|||
|
||||
There is support for:
|
||||
|
||||
- `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum` for [database metrics](#database-metrics).
|
||||
- `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum`, and `average` for [database metrics](#database-metrics).
|
||||
- [Redis metrics](#redis-metrics).
|
||||
- [Redis HLL metrics](#redis-hyperloglog-metrics).
|
||||
- `add` for [numbers metrics](#numbers-metrics).
|
||||
|
@ -308,7 +328,7 @@ The generator takes the class name as an argument and the following options:
|
|||
|
||||
- `--type=TYPE` Required. Indicates the metric type. It must be one of: `database`, `generic`, `redis`, `numbers`.
|
||||
- `--operation` Required for `database` & `numebers` type.
|
||||
- For `database` it must be one of: `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum`.
|
||||
- For `database` it must be one of: `count`, `distinct_count`, `estimate_batch_distinct_count`, `sum`, `average`.
|
||||
- For `numbers` it must be: `add`.
|
||||
- `--ee` Indicates if the metric is for EE.
|
||||
|
||||
|
|
|
@ -22,7 +22,20 @@ If you set up a device, also set up a TOTP so you can still access your account
|
|||
## Use personal access tokens with two-factor authentication
|
||||
|
||||
When 2FA is enabled, you can't use your password to authenticate with Git over HTTPS or the [GitLab API](../../../api/index.md).
|
||||
You must use a [personal access token](../personal_access_tokens.md) instead.
|
||||
You can use a [personal access token](../personal_access_tokens.md) instead.
|
||||
|
||||
## Git Credential Manager
|
||||
|
||||
For Git over HTTPS, [Git Credential Manager](https://github.com/GitCredentialManager/git-credential-manager) (GCM) offers an alternative to personal access tokens. By default, GCM
|
||||
authenticates using OAuth, opening GitLab in your web browser. The first time you authenticate, GitLab asks you to authorize the app. If you remain signed in to GitLab, subsequent
|
||||
authentication requires no interaction.
|
||||
|
||||
So you don't need to reauthenticate on every push, GCM supports caching as well as a variety of platform-specific credential stores that persist between sessions. This feature is useful whether you use personal access tokens or OAuth.
|
||||
|
||||
GCM supports GitLab.com out the box. To use with self-managed GitLab, see [GitLab support](https://github.com/GitCredentialManager/git-credential-manager/blob/main/docs/gitlab.md)
|
||||
documentation.
|
||||
|
||||
Git Credential Manager is developed primarily by GitHub, Inc. It is an open-source project and is supported by the community.
|
||||
|
||||
## Enable two-factor authentication
|
||||
|
||||
|
|
|
@ -194,3 +194,8 @@ questions that you know someone might ask.
|
|||
Each scenario can be a third-level heading, e.g. `### Getting error message X`.
|
||||
If you have none to add when creating a doc, leave this section in place
|
||||
but commented out to help encourage others to add to it in the future. -->
|
||||
|
||||
## Alternatives to personal access tokens
|
||||
|
||||
For Git over HTTPS, an alternative to personal access tokens is [Git Credential Manager](account/two_factor_authentication.md#git-credential-manager),
|
||||
which securely authenticates using OAuth.
|
||||
|
|
|
@ -106,6 +106,17 @@ Without the approvals, the work cannot merge. Required approvals enable multiple
|
|||
- Users on GitLab Ultimate can also [require approval from a security team](../../../application_security/index.md#security-approvals-in-merge-requests)
|
||||
before merging code that could introduce a vulnerability.
|
||||
|
||||
## Invalid rules
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334698) in GitLab 15.1.
|
||||
|
||||
Whenever an approval rule cannot be satisfied, the rule will be displayed as `Invalid`. This applies to the following conditions:
|
||||
|
||||
- The only eligible approver is the author of the merge request.
|
||||
- No eligible approvers (either groups or users) have been assigned to the approval rule.
|
||||
|
||||
These rules will be automatically approved to unblock their respective merge requests.
|
||||
|
||||
## Related topics
|
||||
|
||||
- [Merge request approvals API](../../../../api/merge_request_approvals.md)
|
||||
|
|
|
@ -20851,6 +20851,9 @@ msgstr ""
|
|||
msgid "Introducing Your DevOps Reports"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid Insights config file detected"
|
||||
msgstr ""
|
||||
|
||||
|
@ -20926,6 +20929,9 @@ msgstr ""
|
|||
msgid "Invalid repository path"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid rule"
|
||||
msgstr ""
|
||||
|
||||
msgid "Invalid search parameter"
|
||||
msgstr ""
|
||||
|
||||
|
@ -29786,6 +29792,9 @@ msgstr ""
|
|||
msgid "ProjectSelect|Select a project"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSelect|Something went wrong while fetching projects"
|
||||
msgstr ""
|
||||
|
||||
msgid "ProjectSelect|There was an error fetching the projects. Please try again."
|
||||
msgstr ""
|
||||
|
||||
|
@ -45555,6 +45564,12 @@ msgstr ""
|
|||
msgid "mrWidget|Approval password is invalid."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Approval rule %{rules} is invalid. GitLab has approved this rule automatically to unblock the merge request. %{link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Approval rules %{rules} are invalid. GitLab has approved these rules automatically to unblock the merge request. %{link}"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Approve"
|
||||
msgstr ""
|
||||
|
||||
|
@ -45724,6 +45739,9 @@ msgstr ""
|
|||
msgid "mrWidget|More information"
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|No users match the rule's criteria."
|
||||
msgstr ""
|
||||
|
||||
msgid "mrWidget|Please restore it or use a different %{type} branch."
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
"@babel/preset-env": "^7.18.2",
|
||||
"@gitlab/at.js": "1.5.7",
|
||||
"@gitlab/favicon-overlay": "2.0.0",
|
||||
"@gitlab/svgs": "2.18.0",
|
||||
"@gitlab/svgs": "2.21.0",
|
||||
"@gitlab/ui": "41.10.0",
|
||||
"@gitlab/visual-review-tools": "1.7.3",
|
||||
"@rails/actioncable": "6.1.4-7",
|
||||
|
|
|
@ -48,6 +48,15 @@ RSpec.configure do |config|
|
|||
QA::Git::Repository.new.delete_netrc
|
||||
end
|
||||
|
||||
config.prepend_after do |example|
|
||||
if example.exception
|
||||
page = Capybara.page
|
||||
|
||||
QA::Support::PageErrorChecker.log_request_errors(page)
|
||||
QA::Support::PageErrorChecker.check_page_for_error_code(page)
|
||||
end
|
||||
end
|
||||
|
||||
# Add fabrication time to spec metadata
|
||||
config.append_after do |example|
|
||||
example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
module QA
|
||||
module Support
|
||||
class PageErrorChecker
|
||||
PageError = Class.new(StandardError)
|
||||
|
||||
class << self
|
||||
def report!(page, error_code)
|
||||
request_id_string = ''
|
||||
|
@ -17,10 +19,13 @@ module QA
|
|||
status_code_report(error_code)
|
||||
end
|
||||
|
||||
raise "Error Code #{error_code}\n\n"\
|
||||
"#{report}\n\n"\
|
||||
"Path: #{page.current_path}"\
|
||||
"#{request_id_string}"
|
||||
raise(PageError, <<~MSG)
|
||||
Error Code: #{error_code}
|
||||
|
||||
#{report}
|
||||
|
||||
Path: #{page.current_path}#{request_id_string}
|
||||
MSG
|
||||
end
|
||||
|
||||
def parse_five_c_page_request_id(page)
|
||||
|
@ -43,6 +48,8 @@ module QA
|
|||
|
||||
# rubocop:disable Rails/Pluck
|
||||
def check_page_for_error_code(page)
|
||||
QA::Runtime::Logger.debug "Performing page error check!"
|
||||
|
||||
# Test for 404 img alt
|
||||
return report!(page, 404) if page_html(page).xpath("//img").map { |t| t[:alt] }.first.eql?('404')
|
||||
|
||||
|
@ -56,9 +63,9 @@ module QA
|
|||
# GDK shows backtrace rather than error page
|
||||
report!(page, 500) if page_html(page).xpath("//body//section").map { |t| t[:class] }.first.eql?('backtrace')
|
||||
rescue StandardError => e
|
||||
# There are instances where page check can raise errors like: WebDriver::Error::UnexpectedAlertOpenError
|
||||
# Log error but do not fail the test itself
|
||||
Runtime::Logger.error("Page error check raised error: #{e}")
|
||||
raise e if e.is_a?(PageError)
|
||||
|
||||
QA::Runtime::Logger.error("Page error check raised error `#{e.class}`: #{e.message}")
|
||||
end
|
||||
# rubocop:enable Rails/Pluck
|
||||
|
||||
|
@ -68,7 +75,7 @@ module QA
|
|||
# using QA::Runtime::Logger
|
||||
# @param [Capybara::Session] page
|
||||
def log_request_errors(page)
|
||||
return if QA::Runtime::Browser.blank_page?
|
||||
return if !QA::Runtime::Env.can_intercept? || QA::Runtime::Browser.blank_page?
|
||||
|
||||
url = page.driver.browser.current_url
|
||||
QA::Runtime::Logger.debug "Fetching API error cache for #{url}"
|
||||
|
|
|
@ -8,22 +8,20 @@ module QA
|
|||
DEFAULT_MAX_WAIT_TIME = 60
|
||||
|
||||
def wait_for_requests(skip_finished_loading_check: false, skip_resp_code_check: false)
|
||||
# We have tests that use 404 pages, allow them to skip this check
|
||||
unless skip_resp_code_check
|
||||
QA::Support::PageErrorChecker.check_page_for_error_code(Capybara.page)
|
||||
end
|
||||
|
||||
Waiter.wait_until(log: false) do
|
||||
finished_all_ajax_requests? && (!skip_finished_loading_check ? finished_loading?(wait: 1) : true)
|
||||
end
|
||||
QA::Support::PageErrorChecker.log_request_errors(Capybara.page) if QA::Runtime::Env.can_intercept?
|
||||
rescue Repeater::WaitExceededError
|
||||
raise $!, 'Page did not fully load. This could be due to an unending async request or loading icon.'
|
||||
end
|
||||
|
||||
def finished_all_ajax_requests?
|
||||
requests = %w[window.pendingRequests window.pendingRailsUJSRequests 0]
|
||||
requests.unshift('(window.Interceptor && window.Interceptor.activeFetchRequests)') if Runtime::Env.can_intercept?
|
||||
|
||||
if Runtime::Env.can_intercept?
|
||||
requests.unshift('(window.Interceptor && window.Interceptor.activeFetchRequests)')
|
||||
end
|
||||
|
||||
script = requests.join(' || ')
|
||||
Capybara.page.evaluate_script(script).zero? # rubocop:disable Style/NumericPredicate
|
||||
end
|
||||
|
|
|
@ -8,23 +8,37 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
describe '.report!' do
|
||||
context 'reports errors' do
|
||||
let(:expected_chrome_error) do
|
||||
"Error Code 500\n\n"\
|
||||
"chrome errors\n\n"\
|
||||
"Path: #{test_path}\n\n"\
|
||||
"Logging: foo123"
|
||||
<<~MSG
|
||||
Error Code: 500
|
||||
|
||||
chrome errors
|
||||
|
||||
Path: #{test_path}
|
||||
|
||||
Logging: foo123
|
||||
MSG
|
||||
end
|
||||
|
||||
let(:expected_basic_error) do
|
||||
"Error Code 500\n\n"\
|
||||
"foo status\n\n"\
|
||||
"Path: #{test_path}\n\n"\
|
||||
"Logging: foo123"
|
||||
<<~MSG
|
||||
Error Code: 500
|
||||
|
||||
foo status
|
||||
|
||||
Path: #{test_path}
|
||||
|
||||
Logging: foo123
|
||||
MSG
|
||||
end
|
||||
|
||||
let(:expected_basic_404) do
|
||||
"Error Code 404\n\n"\
|
||||
"foo status\n\n"\
|
||||
"Path: #{test_path}"
|
||||
<<~MSG
|
||||
Error Code: 404
|
||||
|
||||
foo status
|
||||
|
||||
Path: #{test_path}
|
||||
MSG
|
||||
end
|
||||
|
||||
it 'reports error message on chrome browser' do
|
||||
|
@ -34,7 +48,10 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
allow(page).to receive(:current_path).and_return(test_path)
|
||||
allow(QA::Runtime::Env).to receive(:browser).and_return(:chrome)
|
||||
|
||||
expect { QA::Support::PageErrorChecker.report!(page, 500) }.to raise_error(RuntimeError, expected_chrome_error)
|
||||
expect { QA::Support::PageErrorChecker.report!(page, 500) }.to raise_error(
|
||||
QA::Support::PageErrorChecker::PageError,
|
||||
expected_chrome_error
|
||||
)
|
||||
end
|
||||
|
||||
it 'reports basic message on non-chrome browser' do
|
||||
|
@ -44,7 +61,10 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
allow(page).to receive(:current_path).and_return(test_path)
|
||||
allow(QA::Runtime::Env).to receive(:browser).and_return(:firefox)
|
||||
|
||||
expect { QA::Support::PageErrorChecker.report!(page, 500) }.to raise_error(RuntimeError, expected_basic_error)
|
||||
expect { QA::Support::PageErrorChecker.report!(page, 500) }.to raise_error(
|
||||
QA::Support::PageErrorChecker::PageError,
|
||||
expected_basic_error
|
||||
)
|
||||
end
|
||||
|
||||
it 'does not report failure metadata on non 500 error' do
|
||||
|
@ -56,7 +76,10 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
allow(page).to receive(:current_path).and_return(test_path)
|
||||
allow(QA::Runtime::Env).to receive(:browser).and_return(:firefox)
|
||||
|
||||
expect { QA::Support::PageErrorChecker.report!(page, 404) }.to raise_error(RuntimeError, expected_basic_404)
|
||||
expect { QA::Support::PageErrorChecker.report!(page, 404) }.to raise_error(
|
||||
QA::Support::PageErrorChecker::PageError,
|
||||
expected_basic_404
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -182,10 +205,10 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
"</div>"
|
||||
end
|
||||
|
||||
let(:error_500_str) { "<head><title>Something went wrong (500)</title></head><body><h1> 500 </h1></body>"}
|
||||
let(:project_name_500_str) {"<head><title>Project</title></head><h1 class=\"home-panel-title gl-mt-3 gl-mb-2\" itemprop=\"name\">qa-test-2022-05-25-12-12-16-d4500c2e79c37289</h1>"}
|
||||
let(:backtrace_str) {"<head><title>Error::Backtrace</title></head><body><section class=\"backtrace\">foo</section></body>"}
|
||||
let(:no_error_str) {"<head><title>Nothing wrong here</title></head><body>no 404 or 500 or backtrace</body>"}
|
||||
let(:error_500_str) { "<head><title>Something went wrong (500)</title></head><body><h1> 500 </h1></body>" }
|
||||
let(:project_name_500_str) { "<head><title>Project</title></head><h1 class=\"home-panel-title gl-mt-3 gl-mb-2\" itemprop=\"name\">qa-test-2022-05-25-12-12-16-d4500c2e79c37289</h1>" }
|
||||
let(:backtrace_str) { "<head><title>Error::Backtrace</title></head><body><section class=\"backtrace\">foo</section></body>" }
|
||||
let(:no_error_str) { "<head><title>Nothing wrong here</title></head><body>no 404 or 500 or backtrace</body>" }
|
||||
|
||||
it 'calls report with 404 if 404 found' do
|
||||
allow(page).to receive(:html).and_return(error_404_str)
|
||||
|
@ -242,7 +265,7 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
|
||||
it 'returns error report array of log messages' do
|
||||
expect(QA::Support::PageErrorChecker.error_report_for([LogOne, LogTwo]))
|
||||
.to eq(%W(foo\n bar))
|
||||
.to eq(%W[foo\n bar])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -254,6 +277,7 @@ RSpec.describe QA::Support::PageErrorChecker do
|
|||
|
||||
before do
|
||||
allow(Capybara).to receive(:current_session).and_return(session)
|
||||
allow(QA::Runtime::Env).to receive(:can_intercept?).and_return(true)
|
||||
end
|
||||
|
||||
it 'logs from the error cache' do
|
||||
|
|
|
@ -16,22 +16,6 @@ RSpec.describe QA::Support::WaitForRequests do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when skip_finished_loading_check is true' do
|
||||
it 'does not call finished_loading?' do
|
||||
subject.wait_for_requests(skip_finished_loading_check: true)
|
||||
|
||||
expect(subject).not_to have_received(:finished_loading?)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_resp_code_check is defaulted to false' do
|
||||
it 'call report' do
|
||||
subject.wait_for_requests
|
||||
|
||||
expect(QA::Support::PageErrorChecker).to have_received(:check_page_for_error_code).with(Capybara.page)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when skip_resp_code_check is true' do
|
||||
it 'does not parse for an error code' do
|
||||
subject.wait_for_requests(skip_resp_code_check: true)
|
||||
|
|
|
@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
|
|||
import Api, { DEFAULT_PER_PAGE } from '~/api';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import httpStatus from '~/lib/utils/http_status';
|
||||
import { createAlert } from '~/flash';
|
||||
|
||||
jest.mock('~/flash');
|
||||
|
||||
|
@ -608,39 +607,19 @@ describe('Api', () => {
|
|||
},
|
||||
]);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
Api.groupProjects(groupId, query, {}, (response) => {
|
||||
expect(response.length).toBe(1);
|
||||
expect(response[0].name).toBe('test');
|
||||
resolve();
|
||||
});
|
||||
return Api.groupProjects(groupId, query, {}).then((response) => {
|
||||
expect(response.data.length).toBe(1);
|
||||
expect(response.data[0].name).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
it('uses flesh on error by default', async () => {
|
||||
const groupId = '123456';
|
||||
const query = 'dummy query';
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
|
||||
const flashCallback = (callCount) => {
|
||||
expect(createAlert).toHaveBeenCalledTimes(callCount);
|
||||
createAlert.mockClear();
|
||||
};
|
||||
|
||||
mock.onGet(expectedUrl).reply(500, null);
|
||||
|
||||
const response = await Api.groupProjects(groupId, query, {}, () => {}).then(() => {
|
||||
flashCallback(1);
|
||||
});
|
||||
expect(response).toBeUndefined();
|
||||
});
|
||||
|
||||
it('NOT uses flesh on error with param useCustomErrorHandler', async () => {
|
||||
const groupId = '123456';
|
||||
const query = 'dummy query';
|
||||
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
|
||||
|
||||
mock.onGet(expectedUrl).reply(500, null);
|
||||
const apiCall = Api.groupProjects(groupId, query, {}, () => {}, true);
|
||||
const apiCall = Api.groupProjects(groupId, query, {});
|
||||
await expect(apiCall).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -121,19 +121,12 @@ describe('Global Search Store Actions', () => {
|
|||
|
||||
describe('when groupId is set', () => {
|
||||
it('calls Api.groupProjects with expected parameters', () => {
|
||||
const callbackTest = jest.fn();
|
||||
actions.fetchProjects({ commit: mockCommit, state }, undefined, callbackTest);
|
||||
expect(Api.groupProjects).toHaveBeenCalledWith(
|
||||
state.query.group_id,
|
||||
state.query.search,
|
||||
{
|
||||
order_by: 'similarity',
|
||||
include_subgroups: true,
|
||||
with_shared: false,
|
||||
},
|
||||
callbackTest,
|
||||
true,
|
||||
);
|
||||
actions.fetchProjects({ commit: mockCommit, state }, undefined);
|
||||
expect(Api.groupProjects).toHaveBeenCalledWith(state.query.group_id, state.query.search, {
|
||||
order_by: 'similarity',
|
||||
include_subgroups: true,
|
||||
with_shared: false,
|
||||
});
|
||||
expect(Api.projects).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { nextTick } from 'vue';
|
||||
import { GlButton } from '@gitlab/ui';
|
||||
import { GlButton, GlSprintf } from '@gitlab/ui';
|
||||
import { shallowMount } from '@vue/test-utils';
|
||||
import createFlash from '~/flash';
|
||||
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
|
||||
|
@ -15,6 +15,7 @@ import eventHub from '~/vue_merge_request_widget/event_hub';
|
|||
|
||||
jest.mock('~/flash');
|
||||
|
||||
const RULE_NAME = 'first_rule';
|
||||
const TEST_HELP_PATH = 'help/path';
|
||||
const testApprovedBy = () => [1, 7, 10].map((id) => ({ id }));
|
||||
const testApprovals = () => ({
|
||||
|
@ -26,6 +27,7 @@ const testApprovals = () => ({
|
|||
user_can_approve: true,
|
||||
user_has_approved: true,
|
||||
require_password_to_approve: false,
|
||||
invalid_approvers_rules: [],
|
||||
});
|
||||
const testApprovalRulesResponse = () => ({ rules: [{ id: 2 }] });
|
||||
|
||||
|
@ -41,6 +43,9 @@ describe('MRWidget approvals', () => {
|
|||
service,
|
||||
...props,
|
||||
},
|
||||
stubs: {
|
||||
GlSprintf,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -58,6 +63,7 @@ describe('MRWidget approvals', () => {
|
|||
};
|
||||
const findSummary = () => wrapper.find(ApprovalsSummary);
|
||||
const findOptionalSummary = () => wrapper.find(ApprovalsSummaryOptional);
|
||||
const findInvalidRules = () => wrapper.find('[data-testid="invalid-rules"]');
|
||||
|
||||
beforeEach(() => {
|
||||
service = {
|
||||
|
@ -383,4 +389,36 @@ describe('MRWidget approvals', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid rules', () => {
|
||||
beforeEach(() => {
|
||||
mr.approvals.merge_request_approvers_available = true;
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('does not render related components', () => {
|
||||
expect(findInvalidRules().exists()).toBe(false);
|
||||
});
|
||||
|
||||
describe('when invalid rules are present', () => {
|
||||
beforeEach(() => {
|
||||
mr.approvals.invalid_approvers_rules = [{ name: RULE_NAME }];
|
||||
createComponent();
|
||||
});
|
||||
|
||||
it('renders related components', () => {
|
||||
const invalidRules = findInvalidRules();
|
||||
|
||||
expect(invalidRules.exists()).toBe(true);
|
||||
|
||||
const invalidRulesText = invalidRules.text();
|
||||
|
||||
expect(invalidRulesText).toContain(RULE_NAME);
|
||||
expect(invalidRulesText).toContain(
|
||||
'GitLab has approved this rule automatically to unblock the merge request.',
|
||||
);
|
||||
expect(invalidRulesText).toContain('Learn more.');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { humanizeInvalidApproversRules } from '~/vue_merge_request_widget/components/approvals/humanized_text';
|
||||
|
||||
const testRules = [{ name: 'Lorem' }, { name: 'Ipsum' }, { name: 'Dolar' }];
|
||||
|
||||
describe('humanizeInvalidApproversRules', () => {
|
||||
it('returns text in regards to a single rule', () => {
|
||||
const [singleRule] = testRules;
|
||||
expect(humanizeInvalidApproversRules([singleRule])).toBe('"Lorem"');
|
||||
});
|
||||
|
||||
it('returns empty text when there is no rule', () => {
|
||||
expect(humanizeInvalidApproversRules([])).toBe('');
|
||||
});
|
||||
|
||||
it('returns text in regards to multiple rules', () => {
|
||||
expect(humanizeInvalidApproversRules(testRules)).toBe('"Lorem", "Ipsum" and "Dolar"');
|
||||
});
|
||||
});
|
|
@ -428,6 +428,22 @@ RSpec.describe Resolvers::IssuesResolver do
|
|||
end
|
||||
end
|
||||
|
||||
context 'when sorting by closed at' do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:closed_issue1) { create(:issue, project: project, closed_at: 3.days.from_now) }
|
||||
let_it_be(:closed_issue2) { create(:issue, project: project, closed_at: nil) }
|
||||
let_it_be(:closed_issue3) { create(:issue, project: project, closed_at: 2.days.ago) }
|
||||
let_it_be(:closed_issue4) { create(:issue, project: project, closed_at: nil) }
|
||||
|
||||
it 'sorts issues ascending' do
|
||||
expect(resolve_issues(sort: :closed_at_asc).to_a).to eq [closed_issue3, closed_issue1, closed_issue4, closed_issue2]
|
||||
end
|
||||
|
||||
it 'sorts issues descending' do
|
||||
expect(resolve_issues(sort: :closed_at_desc).to_a).to eq [closed_issue1, closed_issue3, closed_issue4, closed_issue2]
|
||||
end
|
||||
end
|
||||
|
||||
context 'when sorting by due date' do
|
||||
let_it_be(:project) { create(:project, :public) }
|
||||
let_it_be(:due_issue1) { create(:issue, project: project, due_date: 3.days.from_now) }
|
||||
|
|
|
@ -1607,4 +1607,24 @@ RSpec.describe Issue do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'order by closed_at' do
|
||||
let!(:issue_a) { create(:issue, closed_at: 1.day.ago) }
|
||||
let!(:issue_b) { create(:issue, closed_at: 5.days.ago) }
|
||||
let!(:issue_c_nil) { create(:issue, closed_at: nil) }
|
||||
let!(:issue_d) { create(:issue, closed_at: 3.days.ago) }
|
||||
let!(:issue_e_nil) { create(:issue, closed_at: nil) }
|
||||
|
||||
describe '.order_closed_at_asc' do
|
||||
it 'orders on closed at' do
|
||||
expect(described_class.order_closed_at_asc.to_a).to eq([issue_b, issue_d, issue_a, issue_c_nil, issue_e_nil])
|
||||
end
|
||||
end
|
||||
|
||||
describe '.order_closed_at_desc' do
|
||||
it 'orders on closed at' do
|
||||
expect(described_class.order_closed_at_desc.to_a).to eq([issue_a, issue_d, issue_b, issue_c_nil, issue_e_nil])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1043,10 +1043,10 @@
|
|||
stylelint-declaration-strict-value "1.8.0"
|
||||
stylelint-scss "4.1.0"
|
||||
|
||||
"@gitlab/svgs@2.18.0":
|
||||
version "2.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.18.0.tgz#aafff929bc5365f7cad736b6d061895b3f9aa381"
|
||||
integrity sha512-Okbm4dAAf/aiaRojUT57yfqY/TVka/zAXN4T+hOx/Yho6wUT2eAJ8CcFpctPdt3kUNM4bHU2CZYoGqklbtXkmg==
|
||||
"@gitlab/svgs@2.21.0":
|
||||
version "2.21.0"
|
||||
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-2.21.0.tgz#bc71951dc35a61647fb2c0267cca6fb55a04d317"
|
||||
integrity sha512-cVa5cgvVmY2MsRdV61id+rLTsY/tAGPq7Og9ETblUuZXl06ciw8H/g7cYPMxN39DdEfDklzbUnS98OJlMmD9TQ==
|
||||
|
||||
"@gitlab/ui@41.10.0":
|
||||
version "41.10.0"
|
||||
|
|
Loading…
Reference in New Issue