Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-06-16 21:09:48 +00:00
parent dbfedde341
commit 9b8433e5ec
61 changed files with 590 additions and 346 deletions

View File

@ -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

View File

@ -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();
});
},

View File

@ -1,3 +1,3 @@
mutation addItems($items: [ItemInput]) {
mutation addItems($items: [Item]) {
addToolbarItems(items: $items) @client
}

View File

@ -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
}

View File

@ -1,3 +1,3 @@
mutation updateItem($id: ID!, $propsToUpdate: ItemInput!) {
mutation updateItem($id: ID!, $propsToUpdate: Item!) {
updateToolbarItem(id: $id, propsToUpdate: $propsToUpdate) @client
}

View File

@ -1,4 +1,4 @@
mutation action($action: LocalActionInput) {
mutation action($action: LocalAction) {
action(action: $action) @client {
errors
}

View File

@ -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
}

View File

@ -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>

View File

@ -1,4 +1,4 @@
mutation importGroups($importRequests: [ImportRequestInput!]!) {
mutation importGroups($importRequests: [ImportGroupInput!]!) {
importGroups(importRequests: $importRequests) @client {
id
lastImportTarget {

View File

@ -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 () => {

View File

@ -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
}

View File

@ -1,10 +0,0 @@
type LogTreeCommit {
sha: String
message: String
titleHtml: String
committedDate: Time
commitPath: String
fileName: String
filePath: String
type: String
}

View File

@ -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 {

View File

@ -1,3 +1,3 @@
mutation addDataToTerraformState($terraformState: LocalTerraformStateInput!) {
mutation addDataToTerraformState($terraformState: State!) {
addDataToTerraformState(terraformState: $terraformState) @client
}

View File

@ -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
}

View File

@ -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"

View File

@ -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);
};

View File

@ -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>

View File

@ -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',
},
);

View File

@ -21,7 +21,7 @@ extend type WorkItem {
mockWidgets: [LocalWorkItemWidget]
}
input LocalWorkItemAssigneesInput {
type LocalWorkItemAssigneesInput {
id: WorkItemID!
assigneeIds: [ID!]
}

View File

@ -82,7 +82,7 @@
}
&:hover {
background-color: $gray-10;
background-color: $gray-normal;
}
svg {

View File

@ -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 {

View File

@ -464,7 +464,7 @@
float: left;
margin-right: 5px;
border-radius: 50%;
border: 1px solid $gray-10;
border: 1px solid $gray-normal;
}
.notification-dot {

View File

@ -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;

View File

@ -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 {

View File

@ -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;

View File

@ -76,7 +76,7 @@
}
.dark-well {
background-color: $gray-10;
background-color: $gray-normal;
}
.card.card-body-centered {

View File

@ -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,

View File

@ -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,

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
2c177b0199019ebdbc06b43d21d47a35453e3b376ccbde21163128c77826478b

View File

@ -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. |

View File

@ -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`)

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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.

View File

@ -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)

View File

@ -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 ""

View File

@ -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",

View File

@ -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]

View File

@ -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}"

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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();
});
});

View File

@ -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();
});
});

View File

@ -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.');
});
});
});
});

View File

@ -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"');
});
});

View File

@ -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) }

View File

@ -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

View File

@ -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"