Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-11-14 15:11:29 +00:00
parent 61a82b8ec0
commit 2d80ade702
134 changed files with 2247 additions and 1905 deletions

View File

@ -123,7 +123,7 @@ review-deploy:
- run_timed_command "check_kube_domain"
- run_timed_command "download_chart"
- run_timed_command "deploy" || (display_deployment_debug && exit 1)
- run_timed_command "verify_deploy" || exit 1
- run_timed_command "verify_deploy"|| (display_deployment_debug && exit 1)
- run_timed_command "disable_sign_ups" || (delete_release && exit 1)
after_script:
# Run seed-dast-test-data.sh only when DAST_RUN is set to true. This is to pupulate review app with data for DAST scan.

View File

@ -1 +1 @@
a415ff702cfd0755db5d1a09c63c13ce13b54f58
4b3f2921b5f0d659b44aee6323d82fc3698a8ede

View File

@ -93,6 +93,12 @@ export const GO_TO_YOUR_MERGE_REQUESTS = {
defaultKeys: ['shift+m'],
};
export const GO_TO_YOUR_REVIEW_REQUESTS = {
id: 'globalShortcuts.goToYourReviewRequests',
description: __('Go to your review requests'),
defaultKeys: ['shift+r'],
};
export const GO_TO_YOUR_TODO_LIST = {
id: 'globalShortcuts.goToYourTodoList',
description: __('Go to your To-Do list'),
@ -523,6 +529,7 @@ export const GLOBAL_SHORTCUTS_GROUP = {
FOCUS_FILTER_BAR,
GO_TO_YOUR_ISSUES,
GO_TO_YOUR_MERGE_REQUESTS,
GO_TO_YOUR_REVIEW_REQUESTS,
GO_TO_YOUR_TODO_LIST,
TOGGLE_PERFORMANCE_BAR,
HIDE_APPEARING_CONTENT,

View File

@ -24,6 +24,7 @@ import {
GO_TO_MILESTONE_LIST,
GO_TO_YOUR_SNIPPETS,
GO_TO_PROJECT_FIND_FILE,
GO_TO_YOUR_REVIEW_REQUESTS,
} from './keybindings';
import { disableShortcuts, shouldDisableShortcuts } from './shortcuts_toggle';
@ -94,6 +95,9 @@ export default class Shortcuts {
Mousetrap.bind(keysFor(GO_TO_YOUR_MERGE_REQUESTS), () =>
findAndFollowLink('.dashboard-shortcuts-merge_requests'),
);
Mousetrap.bind(keysFor(GO_TO_YOUR_REVIEW_REQUESTS), () =>
findAndFollowLink('.dashboard-shortcuts-review_requests'),
);
Mousetrap.bind(keysFor(GO_TO_YOUR_PROJECTS), () =>
findAndFollowLink('.dashboard-shortcuts-projects'),
);

View File

@ -117,31 +117,34 @@ export default {
const { errors, deletedIds } = data.bulkRunnerDelete;
if (errors?.length) {
this.onError(new Error(errors.join(' ')));
this.$refs.modal.hide();
return;
createAlert({
message: s__(
'Runners|An error occurred while deleting. Some runners may not have been deleted.',
),
captureError: true,
error: new Error(errors.join(' ')),
});
}
this.$emit('deleted', {
message: this.toastConfirmationMessage(deletedIds.length),
});
if (deletedIds?.length) {
this.$emit('deleted', {
message: this.toastConfirmationMessage(deletedIds.length),
});
// Clean up
// Remove deleted runners from the cache
deletedIds.forEach((id) => {
const cacheId = cache.identify({ __typename: RUNNER_TYPENAME, id });
cache.evict({ id: cacheId });
});
cache.gc();
this.$refs.modal.hide();
// Remove deleted runners from the cache
deletedIds.forEach((id) => {
const cacheId = cache.identify({ __typename: RUNNER_TYPENAME, id });
cache.evict({ id: cacheId });
});
cache.gc();
}
},
});
} catch (error) {
this.onError(error);
} finally {
this.isDeleting = false;
this.$refs.modal.hide();
}
},
onError(error) {

View File

@ -2,7 +2,7 @@
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
import { createAlert } from '~/flash';
import { sprintf } from '~/locale';
import { sprintf, s__ } from '~/locale';
import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { I18N_DELETE_RUNNER, I18N_DELETED_TOAST } from '../constants';
@ -122,8 +122,11 @@ export default {
onError(error) {
this.deleting = false;
const { message } = error;
const title = sprintf(s__('Runner|Runner %{runnerName} failed to delete'), {
runnerName: this.runnerName,
});
createAlert({ message });
createAlert({ title, message });
captureException({ error, component: this.$options.name });
},
},

View File

@ -1,143 +1,35 @@
<script>
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { reportMessageToSentry } from '../utils';
import { ADD_MUTATION_ACTION, DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION } from '../constants';
import getAdminVariables from '../graphql/queries/variables.query.graphql';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
UPDATE_MUTATION_ACTION,
genericMutationErrorText,
variableFetchErrorText,
} from '../constants';
import addAdminVariable from '../graphql/mutations/admin_add_variable.mutation.graphql';
import deleteAdminVariable from '../graphql/mutations/admin_delete_variable.mutation.graphql';
import updateAdminVariable from '../graphql/mutations/admin_update_variable.mutation.graphql';
import CiVariableSettings from './ci_variable_settings.vue';
import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
CiVariableSettings,
},
inject: ['endpoint'],
data() {
return {
adminVariables: [],
hasNextPage: false,
isInitialLoading: true,
isLoadingMoreItems: false,
loadingCounter: 0,
pageInfo: {},
};
},
apollo: {
adminVariables: {
query: getAdminVariables,
update(data) {
return data?.ciVariables?.nodes || [];
},
result({ data }) {
this.pageInfo = data?.ciVariables?.pageInfo || this.pageInfo;
this.hasNextPage = this.pageInfo?.hasNextPage || false;
// Because graphQL has a limit of 100 items,
// we batch load all the variables by making successive queries
// to keep the same UX. As a safeguard, we make sure that we cannot go over
// 20 consecutive API calls, which means 2000 variables loaded maximum.
if (!this.hasNextPage) {
this.isLoadingMoreItems = false;
} else if (this.loadingCounter < 20) {
this.hasNextPage = false;
this.fetchMoreVariables();
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
}
},
error() {
this.isLoadingMoreItems = false;
this.hasNextPage = false;
createAlert({ message: variableFetchErrorText });
},
watchLoading(flag) {
if (!flag) {
this.isInitialLoading = false;
}
},
},
},
computed: {
isLoading() {
return (
(this.$apollo.queries.adminVariables.loading && this.isInitialLoading) ||
this.isLoadingMoreItems
);
},
},
methods: {
addVariable(variable) {
this.variableMutation(ADD_MUTATION_ACTION, variable);
},
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
fetchMoreVariables() {
this.isLoadingMoreItems = true;
this.$apollo.queries.adminVariables.fetchMore({
variables: {
after: this.pageInfo.endCursor,
},
});
},
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async variableMutation(mutationAction, variable) {
try {
const currentMutation = this.$options.mutationData[mutationAction];
const { data } = await this.$apollo.mutate({
mutation: currentMutation.action,
variables: {
endpoint: this.endpoint,
variable,
},
});
if (data[currentMutation.name]?.errors?.length) {
const { errors } = data[currentMutation.name];
createAlert({ message: errors[0] });
} else {
// The writing to cache for admin variable is not working
// because there is no ID in the cache at the top level.
// We therefore need to manually refetch.
this.$apollo.queries.adminVariables.refetch();
}
} catch {
createAlert({ message: genericMutationErrorText });
}
},
},
componentName: 'InstanceVariables',
i18n: {
tooManyCallsError: __('Maximum number of variables loaded (2000)'),
CiVariableShared,
},
mutationData: {
[ADD_MUTATION_ACTION]: { action: addAdminVariable, name: 'addAdminVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateAdminVariable, name: 'updateAdminVariable' },
[DELETE_MUTATION_ACTION]: { action: deleteAdminVariable, name: 'deleteAdminVariable' },
[ADD_MUTATION_ACTION]: addAdminVariable,
[UPDATE_MUTATION_ACTION]: updateAdminVariable,
[DELETE_MUTATION_ACTION]: deleteAdminVariable,
},
queryData: {
ciVariables: {
lookup: (data) => data?.ciVariables,
query: getAdminVariables,
},
},
};
</script>
<template>
<ci-variable-settings
<ci-variable-shared
:are-scoped-variables-available="false"
:is-loading="isLoading"
:variables="adminVariables"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@update-variable="updateVariable"
component-name="InstanceVariables"
:mutation-data="$options.mutationData"
:refetch-after-mutation="true"
:query-data="$options.queryData"
/>
</template>

View File

@ -1,143 +1,53 @@
<script>
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportMessageToSentry } from '../utils';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_GROUP_TYPE,
UPDATE_MUTATION_ACTION,
genericMutationErrorText,
variableFetchErrorText,
} from '../constants';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import addGroupVariable from '../graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '../graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '../graphql/mutations/group_update_variable.mutation.graphql';
import CiVariableSettings from './ci_variable_settings.vue';
import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
CiVariableSettings,
CiVariableShared,
},
mixins: [glFeatureFlagsMixin()],
inject: ['endpoint', 'groupPath', 'groupId'],
data() {
return {
groupVariables: [],
hasNextPage: false,
isLoadingMoreItems: false,
loadingCounter: 0,
pageInfo: {},
};
},
apollo: {
groupVariables: {
query: getGroupVariables,
variables() {
return {
fullPath: this.groupPath,
};
},
update(data) {
return data?.group?.ciVariables?.nodes || [];
},
result({ data }) {
this.pageInfo = data?.group?.ciVariables?.pageInfo || this.pageInfo;
this.hasNextPage = this.pageInfo?.hasNextPage || false;
// Because graphQL has a limit of 100 items,
// we batch load all the variables by making successive queries
// to keep the same UX. As a safeguard, we make sure that we cannot go over
// 20 consecutive API calls, which means 2000 variables loaded maximum.
if (!this.hasNextPage) {
this.isLoadingMoreItems = false;
} else if (this.loadingCounter < 20) {
this.hasNextPage = false;
this.fetchMoreVariables();
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
}
},
error() {
this.isLoadingMoreItems = false;
this.hasNextPage = false;
createAlert({ message: variableFetchErrorText });
},
},
},
inject: ['groupPath', 'groupId'],
computed: {
areScopedVariablesAvailable() {
return this.glFeatures.groupScopedCiVariables;
},
isLoading() {
return this.$apollo.queries.groupVariables.loading || this.isLoadingMoreItems;
graphqlId() {
return convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId);
},
},
methods: {
addVariable(variable) {
this.variableMutation(ADD_MUTATION_ACTION, variable);
},
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
fetchMoreVariables() {
this.isLoadingMoreItems = true;
this.$apollo.queries.groupVariables.fetchMore({
variables: {
fullPath: this.groupPath,
after: this.pageInfo.endCursor,
},
});
},
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async variableMutation(mutationAction, variable) {
try {
const currentMutation = this.$options.mutationData[mutationAction];
const { data } = await this.$apollo.mutate({
mutation: currentMutation.action,
variables: {
endpoint: this.endpoint,
fullPath: this.groupPath,
groupId: convertToGraphQLId(GRAPHQL_GROUP_TYPE, this.groupId),
variable,
},
});
if (data[currentMutation.name]?.errors?.length) {
const { errors } = data[currentMutation.name];
createAlert({ message: errors[0] });
}
} catch {
createAlert({ message: genericMutationErrorText });
}
},
},
componentName: 'GroupVariables',
i18n: {
tooManyCallsError: __('Maximum number of variables loaded (2000)'),
},
mutationData: {
[ADD_MUTATION_ACTION]: { action: addGroupVariable, name: 'addGroupVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateGroupVariable, name: 'updateGroupVariable' },
[DELETE_MUTATION_ACTION]: { action: deleteGroupVariable, name: 'deleteGroupVariable' },
[ADD_MUTATION_ACTION]: addGroupVariable,
[UPDATE_MUTATION_ACTION]: updateGroupVariable,
[DELETE_MUTATION_ACTION]: deleteGroupVariable,
},
queryData: {
ciVariables: {
lookup: (data) => data?.group?.ciVariables,
query: getGroupVariables,
},
},
};
</script>
<template>
<ci-variable-settings
<ci-variable-shared
:id="graphqlId"
:are-scoped-variables-available="areScopedVariablesAvailable"
:is-loading="isLoading"
:variables="groupVariables"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@update-variable="updateVariable"
component-name="GroupVariables"
:full-path="groupPath"
:mutation-data="$options.mutationData"
:query-data="$options.queryData"
/>
</template>

View File

@ -1,160 +1,55 @@
<script>
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
GRAPHQL_PROJECT_TYPE,
UPDATE_MUTATION_ACTION,
environmentFetchErrorText,
genericMutationErrorText,
variableFetchErrorText,
} from '../constants';
import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
import addProjectVariable from '../graphql/mutations/project_add_variable.mutation.graphql';
import deleteProjectVariable from '../graphql/mutations/project_delete_variable.mutation.graphql';
import updateProjectVariable from '../graphql/mutations/project_update_variable.mutation.graphql';
import CiVariableSettings from './ci_variable_settings.vue';
import CiVariableShared from './ci_variable_shared.vue';
export default {
components: {
CiVariableSettings,
},
inject: ['endpoint', 'projectFullPath', 'projectId'],
data() {
return {
hasNextPage: false,
isLoadingMoreItems: false,
loadingCounter: 0,
pageInfo: {},
projectEnvironments: [],
projectVariables: [],
};
},
apollo: {
projectEnvironments: {
query: getProjectEnvironments,
variables() {
return {
fullPath: this.projectFullPath,
};
},
update(data) {
return mapEnvironmentNames(data?.project?.environments?.nodes);
},
error() {
createAlert({ message: environmentFetchErrorText });
},
},
projectVariables: {
query: getProjectVariables,
variables() {
return {
after: null,
fullPath: this.projectFullPath,
};
},
update(data) {
return data?.project?.ciVariables?.nodes || [];
},
result({ data }) {
this.pageInfo = data?.project?.ciVariables?.pageInfo || this.pageInfo;
this.hasNextPage = this.pageInfo?.hasNextPage || false;
// Because graphQL has a limit of 100 items,
// we batch load all the variables by making successive queries
// to keep the same UX. As a safeguard, we make sure that we cannot go over
// 20 consecutive API calls, which means 2000 variables loaded maximum.
if (!this.hasNextPage) {
this.isLoadingMoreItems = false;
} else if (this.loadingCounter < 20) {
this.hasNextPage = false;
this.fetchMoreVariables();
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
reportMessageToSentry(this.$options.componentName, this.$options.tooManyCallsError, {});
}
},
error() {
this.isLoadingMoreItems = false;
this.hasNextPage = false;
createAlert({ message: variableFetchErrorText });
},
},
CiVariableShared,
},
mixins: [glFeatureFlagsMixin()],
inject: ['projectFullPath', 'projectId'],
computed: {
isLoading() {
return (
this.$apollo.queries.projectVariables.loading ||
this.$apollo.queries.projectEnvironments.loading ||
this.isLoadingMoreItems
);
graphqlId() {
return convertToGraphQLId(GRAPHQL_PROJECT_TYPE, this.projectId);
},
},
methods: {
addVariable(variable) {
this.variableMutation(ADD_MUTATION_ACTION, variable);
},
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
fetchMoreVariables() {
this.isLoadingMoreItems = true;
this.$apollo.queries.projectVariables.fetchMore({
variables: {
fullPath: this.projectFullPath,
after: this.pageInfo.endCursor,
},
});
},
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async variableMutation(mutationAction, variable) {
try {
const currentMutation = this.$options.mutationData[mutationAction];
const { data } = await this.$apollo.mutate({
mutation: currentMutation.action,
variables: {
endpoint: this.endpoint,
fullPath: this.projectFullPath,
projectId: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, this.projectId),
variable,
},
});
if (data[currentMutation.name]?.errors?.length) {
const { errors } = data[currentMutation.name];
createAlert({ message: errors[0] });
}
} catch {
createAlert({ message: genericMutationErrorText });
}
},
},
componentName: 'ProjectVariables',
i18n: {
tooManyCallsError: __('Maximum number of variables loaded (2000)'),
},
mutationData: {
[ADD_MUTATION_ACTION]: { action: addProjectVariable, name: 'addProjectVariable' },
[UPDATE_MUTATION_ACTION]: { action: updateProjectVariable, name: 'updateProjectVariable' },
[DELETE_MUTATION_ACTION]: { action: deleteProjectVariable, name: 'deleteProjectVariable' },
[ADD_MUTATION_ACTION]: addProjectVariable,
[UPDATE_MUTATION_ACTION]: updateProjectVariable,
[DELETE_MUTATION_ACTION]: deleteProjectVariable,
},
queryData: {
ciVariables: {
lookup: (data) => data?.project?.ciVariables,
query: getProjectVariables,
},
environments: {
lookup: (data) => data?.project?.environments,
query: getProjectEnvironments,
},
},
};
</script>
<template>
<ci-variable-settings
<ci-variable-shared
:id="graphqlId"
:are-scoped-variables-available="true"
:environments="projectEnvironments"
:is-loading="isLoading"
:variables="projectVariables"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@update-variable="updateVariable"
component-name="ProjectVariables"
:full-path="projectFullPath"
:mutation-data="$options.mutationData"
:query-data="$options.queryData"
/>
</template>

View File

@ -0,0 +1,226 @@
<script>
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import { mapEnvironmentNames, reportMessageToSentry } from '../utils';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
UPDATE_MUTATION_ACTION,
environmentFetchErrorText,
genericMutationErrorText,
variableFetchErrorText,
} from '../constants';
import CiVariableSettings from './ci_variable_settings.vue';
export default {
components: {
CiVariableSettings,
},
inject: ['endpoint'],
props: {
areScopedVariablesAvailable: {
required: true,
type: Boolean,
},
componentName: {
required: true,
type: String,
},
fullPath: {
required: false,
type: String,
default: null,
},
id: {
required: false,
type: String,
default: null,
},
mutationData: {
required: true,
type: Object,
validator: (obj) => {
const hasValidKeys = Object.keys(obj).includes(
ADD_MUTATION_ACTION,
UPDATE_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
);
const hasValidValues = Object.values(obj).reduce((acc, val) => {
return acc && typeof val === 'object';
}, true);
return hasValidKeys && hasValidValues;
},
},
refetchAfterMutation: {
required: false,
type: Boolean,
default: false,
},
queryData: {
required: true,
type: Object,
validator: (obj) => {
const { ciVariables, environments } = obj;
const hasCiVariablesKey = Boolean(ciVariables);
let hasCorrectEnvData = true;
const hasCorrectVariablesData =
typeof ciVariables?.lookup === 'function' && typeof ciVariables.query === 'object';
if (environments) {
hasCorrectEnvData =
typeof environments?.lookup === 'function' && typeof environments.query === 'object';
}
return hasCiVariablesKey && hasCorrectVariablesData && hasCorrectEnvData;
},
},
},
data() {
return {
ciVariables: [],
hasNextPage: false,
isInitialLoading: true,
isLoadingMoreItems: false,
loadingCounter: 0,
pageInfo: {},
};
},
apollo: {
ciVariables: {
query() {
return this.queryData.ciVariables.query;
},
variables() {
return {
fullPath: this.fullPath || undefined,
};
},
update(data) {
return this.queryData.ciVariables.lookup(data)?.nodes || [];
},
result({ data }) {
this.pageInfo = this.queryData.ciVariables.lookup(data)?.pageInfo || this.pageInfo;
this.hasNextPage = this.pageInfo?.hasNextPage || false;
// Because graphQL has a limit of 100 items,
// we batch load all the variables by making successive queries
// to keep the same UX. As a safeguard, we make sure that we cannot go over
// 20 consecutive API calls, which means 2000 variables loaded maximum.
if (!this.hasNextPage) {
this.isLoadingMoreItems = false;
} else if (this.loadingCounter < 20) {
this.hasNextPage = false;
this.fetchMoreVariables();
this.loadingCounter += 1;
} else {
createAlert({ message: this.$options.tooManyCallsError });
reportMessageToSentry(this.componentName, this.$options.tooManyCallsError, {});
}
},
error() {
this.isLoadingMoreItems = false;
this.hasNextPage = false;
createAlert({ message: variableFetchErrorText });
},
watchLoading(flag) {
if (!flag) {
this.isInitialLoading = false;
}
},
},
environments: {
query() {
return this.queryData?.environments?.query || {};
},
skip() {
return !this.queryData?.environments?.query;
},
variables() {
return {
fullPath: this.fullPath,
};
},
update(data) {
return mapEnvironmentNames(this.queryData.environments.lookup(data)?.nodes);
},
error() {
createAlert({ message: environmentFetchErrorText });
},
},
},
computed: {
isLoading() {
return (
(this.$apollo.queries.ciVariables.loading && this.isInitialLoading) ||
this.$apollo.queries.environments.loading ||
this.isLoadingMoreItems
);
},
},
methods: {
addVariable(variable) {
this.variableMutation(ADD_MUTATION_ACTION, variable);
},
deleteVariable(variable) {
this.variableMutation(DELETE_MUTATION_ACTION, variable);
},
fetchMoreVariables() {
this.isLoadingMoreItems = true;
this.$apollo.queries.ciVariables.fetchMore({
variables: {
after: this.pageInfo.endCursor,
},
});
},
updateVariable(variable) {
this.variableMutation(UPDATE_MUTATION_ACTION, variable);
},
async variableMutation(mutationAction, variable) {
try {
const currentMutation = this.mutationData[mutationAction];
const { data } = await this.$apollo.mutate({
mutation: currentMutation,
variables: {
endpoint: this.endpoint,
fullPath: this.fullPath || undefined,
id: this.id || undefined,
variable,
},
});
if (data.ciVariableMutation?.errors?.length) {
const { errors } = data.ciVariableMutation;
createAlert({ message: errors[0] });
} else if (this.refetchAfterMutation) {
// The writing to cache for admin variable is not working
// because there is no ID in the cache at the top level.
// We therefore need to manually refetch.
this.$apollo.queries.ciVariables.refetch();
}
} catch (e) {
createAlert({ message: genericMutationErrorText });
}
},
},
i18n: {
tooManyCallsError: __('Maximum number of variables loaded (2000)'),
},
};
</script>
<template>
<ci-variable-settings
:are-scoped-variables-available="areScopedVariablesAvailable"
:is-loading="isLoading"
:variables="ciVariables"
:environments="environments"
@add-variable="addVariable"
@delete-variable="deleteVariable"
@update-variable="updateVariable"
/>
</template>

View File

@ -1,7 +1,7 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addAdminVariable($variable: CiVariable!, $endpoint: String!) {
addAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariableMutation: addAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariables {
nodes {
...BaseCiVariable

View File

@ -1,7 +1,7 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteAdminVariable($variable: CiVariable!, $endpoint: String!) {
deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariableMutation: deleteAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariables {
nodes {
...BaseCiVariable

View File

@ -1,7 +1,7 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateAdminVariable($variable: CiVariable!, $endpoint: String!) {
updateAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariableMutation: updateAdminVariable(variable: $variable, endpoint: $endpoint) @client {
ciVariables {
nodes {
...BaseCiVariable

View File

@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addGroupVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$groupId: ID!
) {
addGroupVariable(
mutation addGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: addGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
groupId: $groupId
id: $id
) @client {
group {
id

View File

@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation deleteGroupVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$groupId: ID!
) {
deleteGroupVariable(
mutation deleteGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: deleteGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
groupId: $groupId
id: $id
) @client {
group {
id

View File

@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation updateGroupVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$groupId: ID!
) {
updateGroupVariable(
mutation updateGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: updateGroupVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
groupId: $groupId
id: $id
) @client {
group {
id

View File

@ -1,16 +1,11 @@
#import "~/ci_variable_list/graphql/fragments/ci_variable.fragment.graphql"
mutation addProjectVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$projectId: ID!
) {
addProjectVariable(
mutation addProjectVariable($variable: CiVariable!, $endpoint: String!, $fullPath: ID!, $id: ID!) {
ciVariableMutation: addProjectVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
projectId: $projectId
id: $id
) @client {
project {
id

View File

@ -4,13 +4,13 @@ mutation deleteProjectVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$projectId: ID!
$id: ID!
) {
deleteProjectVariable(
ciVariableMutation: deleteProjectVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
projectId: $projectId
id: $id
) @client {
project {
id

View File

@ -4,13 +4,13 @@ mutation updateProjectVariable(
$variable: CiVariable!
$endpoint: String!
$fullPath: ID!
$projectId: ID!
$id: ID!
) {
updateProjectVariable(
ciVariableMutation: updateProjectVariable(
variable: $variable
endpoint: $endpoint
fullPath: $fullPath
projectId: $projectId
id: $id
) @client {
project {
id

View File

@ -36,12 +36,12 @@ const mapVariableTypes = (variables = [], kind) => {
});
};
const prepareProjectGraphQLResponse = ({ data, projectId, errors = [] }) => {
const prepareProjectGraphQLResponse = ({ data, id, errors = [] }) => {
return {
errors,
project: {
__typename: GRAPHQL_PROJECT_TYPE,
id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, projectId),
id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, id),
ciVariables: {
__typename: `Ci${GRAPHQL_PROJECT_TYPE}VariableConnection`,
pageInfo: {
@ -57,12 +57,12 @@ const prepareProjectGraphQLResponse = ({ data, projectId, errors = [] }) => {
};
};
const prepareGroupGraphQLResponse = ({ data, groupId, errors = [] }) => {
const prepareGroupGraphQLResponse = ({ data, id, errors = [] }) => {
return {
errors,
group: {
__typename: GRAPHQL_GROUP_TYPE,
id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, groupId),
id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, id),
ciVariables: {
__typename: `Ci${GRAPHQL_GROUP_TYPE}VariableConnection`,
pageInfo: {
@ -95,20 +95,13 @@ const prepareAdminGraphQLResponse = ({ data, errors = [] }) => {
};
};
async function callProjectEndpoint({
endpoint,
fullPath,
variable,
projectId,
cache,
destroy = false,
}) {
async function callProjectEndpoint({ endpoint, fullPath, variable, id, cache, destroy = false }) {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
const graphqlData = prepareProjectGraphQLResponse({ data, projectId });
const graphqlData = prepareProjectGraphQLResponse({ data, id });
cache.writeQuery({
query: getProjectVariables,
@ -122,26 +115,19 @@ async function callProjectEndpoint({
} catch (e) {
return prepareProjectGraphQLResponse({
data: cache.readQuery({ query: getProjectVariables, variables: { fullPath } }),
projectId,
id,
errors: [...e.response.data],
});
}
}
const callGroupEndpoint = async ({
endpoint,
fullPath,
variable,
groupId,
cache,
destroy = false,
}) => {
const callGroupEndpoint = async ({ endpoint, fullPath, variable, id, cache, destroy = false }) => {
try {
const { data } = await axios.patch(endpoint, {
variables_attributes: [prepareVariableForApi({ variable, destroy })],
});
const graphqlData = prepareGroupGraphQLResponse({ data, groupId });
const graphqlData = prepareGroupGraphQLResponse({ data, id });
cache.writeQuery({
query: getGroupVariables,
@ -152,7 +138,7 @@ const callGroupEndpoint = async ({
} catch (e) {
return prepareGroupGraphQLResponse({
data: cache.readQuery({ query: getGroupVariables, variables: { fullPath } }),
groupId,
id,
errors: [...e.response.data],
});
}
@ -182,23 +168,23 @@ const callAdminEndpoint = async ({ endpoint, variable, cache, destroy = false })
export const resolvers = {
Mutation: {
addProjectVariable: async (_, { endpoint, fullPath, variable, projectId }, { cache }) => {
return callProjectEndpoint({ endpoint, fullPath, variable, projectId, cache });
addProjectVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
return callProjectEndpoint({ endpoint, fullPath, variable, id, cache });
},
updateProjectVariable: async (_, { endpoint, fullPath, variable, projectId }, { cache }) => {
return callProjectEndpoint({ endpoint, fullPath, variable, projectId, cache });
updateProjectVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
return callProjectEndpoint({ endpoint, fullPath, variable, id, cache });
},
deleteProjectVariable: async (_, { endpoint, fullPath, variable, projectId }, { cache }) => {
return callProjectEndpoint({ endpoint, fullPath, variable, projectId, cache, destroy: true });
deleteProjectVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
return callProjectEndpoint({ endpoint, fullPath, variable, id, cache, destroy: true });
},
addGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
addGroupVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, id, cache });
},
updateGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache });
updateGroupVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, id, cache });
},
deleteGroupVariable: async (_, { endpoint, fullPath, variable, groupId }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, groupId, cache, destroy: true });
deleteGroupVariable: async (_, { endpoint, fullPath, variable, id }, { cache }) => {
return callGroupEndpoint({ endpoint, fullPath, variable, id, cache, destroy: true });
},
addAdminVariable: async (_, { endpoint, variable }, { cache }) => {
return callAdminEndpoint({ endpoint, variable, cache });
@ -238,7 +224,7 @@ export const cacheConfig = {
Project: {
fields: {
ciVariables: {
keyArgs: ['fullPath', 'endpoint', 'projectId'],
keyArgs: ['fullPath', 'endpoint', 'id'],
merge: mergeVariables,
},
},

View File

@ -5,7 +5,6 @@ import { concatPagination } from '@apollo/client/utilities';
import getIssueStateQuery from '~/issues/show/queries/get_issue_state.query.graphql';
import createDefaultClient from '~/lib/graphql';
import typeDefs from '~/work_items/graphql/typedefs.graphql';
import { WIDGET_TYPE_MILESTONE } from '~/work_items/constants';
export const config = {
typeDefs,
@ -15,10 +14,6 @@ export const config = {
// eslint-disable-next-line no-underscore-dangle
return object.__typename === 'BoardList' ? object.iid : defaultDataIdFromObject(object);
},
possibleTypes: {
LocalWorkItemWidget: ['LocalWorkItemMilestone'],
},
typePolicies: {
Project: {
fields: {
@ -29,28 +24,6 @@ export const config = {
},
WorkItem: {
fields: {
mockWidgets: {
read(widgets) {
return (
widgets || [
{
__typename: 'LocalWorkItemMilestone',
type: WIDGET_TYPE_MILESTONE,
nodes: [
{
dueDate: null,
expired: false,
id: 'gid://gitlab/Milestone/30',
title: 'v4.0',
// eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Milestone',
},
],
},
]
);
},
},
widgets: {
merge(existing = [], incoming) {
if (existing.length === 0) {

View File

@ -117,7 +117,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
createAlert({ message: error });
createAlert({ message: error.message });
}
},
async addProject() {
@ -140,7 +140,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
createAlert({ message: error });
createAlert({ message: error.message });
} finally {
this.clearTargetProjectPath();
this.getProjects();
@ -166,7 +166,7 @@ export default {
throw new Error(errors[0]);
}
} catch (error) {
createAlert({ message: error });
createAlert({ message: error.message });
} finally {
this.getProjects();
}

View File

@ -10,14 +10,21 @@ export default {
{
key: 'project',
label: __('Projects that can be accessed'),
tdClass: 'gl-p-5!',
columnClass: 'gl-w-85p',
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-40p',
},
{
key: 'namespace',
label: __('Namespace'),
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-40p',
},
{
key: 'actions',
label: '',
tdClass: 'gl-p-5! gl-text-right',
columnClass: 'gl-w-15p',
tdClass: 'gl-text-right',
thClass: 'gl-border-t-none!',
columnClass: 'gl-w-10p',
},
],
components: {
@ -57,7 +64,11 @@ export default {
</template>
<template #cell(project)="{ item }">
{{ item.name }}
<span data-testid="token-access-project-name">{{ item.name }}</span>
</template>
<template #cell(namespace)="{ item }">
<span data-testid="token-access-project-namespace">{{ item.namespace.fullPath }}</span>
</template>
<template #cell(actions)="{ item }">

View File

@ -6,6 +6,10 @@ query getProjectsWithCIJobTokenScope($fullPath: ID!) {
nodes {
id
name
namespace {
id
fullPath
}
fullPath
}
}

View File

@ -20,6 +20,11 @@ export default {
required: false,
default: () => ({}),
},
icon: {
type: String,
required: false,
default: 'question-o',
},
},
methods: {
targetFn() {
@ -30,7 +35,7 @@ export default {
</script>
<template>
<span>
<gl-button ref="popoverTrigger" variant="link" icon="question-o" :aria-label="__('Help')" />
<gl-button ref="popoverTrigger" variant="link" :icon="icon" :aria-label="__('Help')" />
<gl-popover :target="targetFn" v-bind="options">
<template v-if="options.title" #title>
<span v-safe-html="options.title"></span>

View File

@ -22,7 +22,7 @@ const format = (node, kind = '') => {
.split(newlineRegex)
.map((newline) => generateHLJSTag(kind, newline, true))
.join('\n');
} else if (node.kind) {
} else if (node.kind || node.sublanguage) {
const { children } = node;
if (children.length && children.length === 1) {
buffer += format(children[0], node.kind);

View File

@ -32,6 +32,7 @@ import {
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
import workItemTitleSubscription from '../graphql/work_item_title.subscription.graphql';
import workItemAssigneesSubscription from '../graphql/work_item_assignees.subscription.graphql';
import workItemMilestoneSubscription from '../graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import { getWorkItemQuery } from '../utils';
@ -170,6 +171,17 @@ export default {
return !this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES) || !this.workItem?.id;
},
},
{
document: workItemMilestoneSubscription,
variables() {
return {
issuableId: this.workItem.id,
};
},
skip() {
return !this.isWidgetPresent(WIDGET_TYPE_MILESTONE) || !this.workItem?.id;
},
},
],
},
},
@ -229,7 +241,7 @@ export default {
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
workItemMilestone() {
return this.workItem?.mockWidgets?.find((widget) => widget.type === WIDGET_TYPE_MILESTONE);
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
},
fetchByIid() {
return this.glFeatures.useIidInWorkItemsPath && parseBoolean(this.$route.query.iid_path);
@ -457,8 +469,10 @@ export default {
<work-item-milestone
v-if="workItemMilestone"
:work-item-id="workItem.id"
:work-item-milestone="workItemMilestone.nodes[0]"
:work-item-milestone="workItemMilestone.milestone"
:work-item-type="workItemType"
:fetch-by-iid="fetchByIid"
:query-variables="queryVariables"
:can-update="canUpdate"
:full-path="fullPath"
@error="updateError = $event"

View File

@ -89,6 +89,9 @@ export default {
issuableIteration() {
return this.parentIssue?.iteration;
},
issuableMilestone() {
return this.parentIssue?.milestone;
},
children() {
return (
this.workItem?.widgets.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY)?.children
@ -309,6 +312,7 @@ export default {
:children-ids="childrenIds"
:parent-confidential="confidential"
:parent-iteration="issuableIteration"
:parent-milestone="issuableMilestone"
@cancel="hideAddForm"
@addWorkItemChild="addChild"
/>

View File

@ -40,6 +40,11 @@ export default {
required: false,
default: () => {},
},
parentMilestone: {
type: Object,
required: false,
default: () => ({}),
},
},
apollo: {
workItemTypes: {
@ -63,6 +68,27 @@ export default {
};
},
computed: {
workItemInput() {
let workItemInput = {
title: this.search?.title || this.search,
projectPath: this.projectPath,
workItemTypeId: this.taskWorkItemType,
hierarchyWidget: {
parentId: this.issuableGid,
},
confidential: this.parentConfidential,
};
if (this.associateMilestone) {
workItemInput = {
...workItemInput,
milestoneWidget: {
milestoneId: this.parentMilestoneId,
},
};
}
return workItemInput;
},
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
@ -93,6 +119,12 @@ export default {
associateIteration() {
return this.parentIterationId && this.hasIterationsFeature && this.workItemsMvc2Enabled;
},
parentMilestoneId() {
return this.parentMilestone?.id;
},
associateMilestone() {
return this.parentMilestoneId && this.workItemsMvc2Enabled;
},
},
methods: {
getIdFromGraphQLId,
@ -132,15 +164,7 @@ export default {
.mutate({
mutation: createWorkItemMutation,
variables: {
input: {
title: this.search?.title || this.search,
projectPath: this.projectPath,
workItemTypeId: this.taskWorkItemType,
hierarchyWidget: {
parentId: this.issuableGid,
},
confidential: this.parentConfidential,
},
input: this.workItemInput,
},
})
.then(({ data }) => {

View File

@ -11,10 +11,10 @@ import {
import * as Sentry from '@sentry/browser';
import { debounce } from 'lodash';
import Tracking from '~/tracking';
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import localUpdateWorkItemMutation from '../graphql/local_update_work_item.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
I18N_WORK_ITEM_ERROR_UPDATING,
sprintfWorkItem,
@ -33,6 +33,7 @@ export default {
MILESTONE_FETCH_ERROR: s__(
'WorkItem|Something went wrong while fetching milestones. Please try again.',
),
EXPIRED_TEXT: __('(expired)'),
},
components: {
GlFormGroup,
@ -68,6 +69,15 @@ export default {
type: String,
required: true,
},
fetchByIid: {
type: Boolean,
required: false,
default: false,
},
queryVariables: {
type: Object,
required: true,
},
},
data() {
return {
@ -90,8 +100,13 @@ export default {
emptyPlaceholder() {
return this.canUpdate ? this.$options.i18n.MILESTONE_PLACEHOLDER : this.$options.i18n.NONE;
},
expired() {
return this.localMilestone?.expired ? ` ${this.$options.i18n.EXPIRED_TEXT}` : '';
},
dropdownText() {
return this.localMilestone?.title || this.emptyPlaceholder;
return this.localMilestone?.title
? `${this.localMilestone?.title}${this.expired}`
: this.emptyPlaceholder;
},
isLoadingMilestones() {
return this.$apollo.queries.milestones.loading;
@ -106,6 +121,14 @@ export default {
};
},
},
watch: {
workItemMilestone: {
handler(newVal) {
this.localMilestone = newVal;
},
deep: true,
},
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
@ -160,12 +183,13 @@ export default {
this.updateInProgress = true;
this.$apollo
.mutate({
mutation: localUpdateWorkItemMutation,
mutation: updateWorkItemMutation,
variables: {
input: {
id: this.workItemId,
milestone: {
milestoneId: this.localMilestone?.id,
milestoneWidget: {
milestoneId:
this.localMilestone?.id === 'no-milestone-id' ? null : this.localMilestone?.id,
},
},
},
@ -240,6 +264,7 @@ export default {
@click="handleMilestoneClick(milestone)"
>
{{ milestone.title }}
<template v-if="milestone.expired">{{ $options.i18n.EXPIRED_TEXT }}</template>
</gl-dropdown-item>
</template>
<gl-dropdown-text v-else>{{ $options.i18n.NO_MATCHING_RESULTS }}</gl-dropdown-text>

View File

@ -4,6 +4,9 @@ query issuableDetails($fullPath: ID!, $iid: String) {
issuable: issue(iid: $iid) {
id
confidential
milestone {
id
}
}
}
}

View File

@ -0,0 +1,5 @@
fragment MilestoneFragment on Milestone {
expired
id
title
}

View File

@ -1,6 +1,5 @@
enum LocalWidgetType {
ASSIGNEES
MILESTONE
}
interface LocalWorkItemWidget {
@ -12,11 +11,6 @@ type LocalWorkItemAssignees implements LocalWorkItemWidget {
nodes: [UserCore]
}
type LocalWorkItemMilestone implements LocalWorkItemWidget {
type: LocalWidgetType!
nodes: [Milestone!]
}
extend type WorkItem {
mockWidgets: [LocalWorkItemWidget]
}
@ -29,14 +23,9 @@ input LocalUserInput {
avatarUrl: String
}
input LocalMilestoneInput {
milestoneId: ID!
}
input LocalUpdateWorkItemInput {
id: WorkItemID!
assignees: [LocalUserInput!]
milestone: LocalMilestoneInput!
}
type LocalWorkItemPayload {

View File

@ -3,16 +3,5 @@
query workItem($id: WorkItemID!) {
workItem(id: $id) {
...WorkItem
mockWidgets @client {
... on LocalWorkItemMilestone {
type
nodes {
id
title
expired
dueDate
}
}
}
}
}

View File

@ -6,17 +6,6 @@ query workItemByIid($fullPath: ID!, $iid: String) {
workItems(iid: $iid) {
nodes {
...WorkItem
mockWidgets @client {
... on LocalWorkItemMilestone {
type
nodes {
id
title
expired
dueDate
}
}
}
}
}
}

View File

@ -0,0 +1,17 @@
#import "~/work_items/graphql/milestone.fragment.graphql"
subscription issuableMilestone($issuableId: IssuableID!) {
issuableMilestoneUpdated(issuableId: $issuableId) {
... on WorkItem {
id
widgets {
... on WorkItemWidgetMilestone {
type
milestone {
...MilestoneFragment
}
}
}
}
}
}

View File

@ -1,5 +1,6 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/work_items/graphql/milestone.fragment.graphql"
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
@ -49,4 +50,10 @@ fragment WorkItemWidgets on WorkItemWidget {
}
}
}
... on WorkItemWidgetMilestone {
type
milestone {
...MilestoneFragment
}
}
}

View File

@ -17,7 +17,7 @@ module AccessTokensActions
respond_to do |format|
format.html
format.json do
render json: @active_resource_access_tokens
render json: @active_access_tokens
end
end
end
@ -30,7 +30,7 @@ module AccessTokensActions
if token_response.success?
@resource_access_token = token_response.payload[:access_token]
render json: { new_token: @resource_access_token.token,
active_access_tokens: active_resource_access_tokens }, status: :ok
active_access_tokens: active_access_tokens }, status: :ok
else
render json: { errors: token_response.errors }, status: :unprocessable_entity
end
@ -69,37 +69,10 @@ module AccessTokensActions
resource.members.load
@scopes = Gitlab::Auth.resource_bot_scopes
@active_resource_access_tokens = active_resource_access_tokens
@active_access_tokens = active_access_tokens
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def active_resource_access_tokens
tokens = finder(state: 'active', sort: 'expires_at_asc_id_desc').execute.preload_users
if Feature.enabled?('access_token_pagination')
tokens = tokens.page(page)
add_pagination_headers(tokens)
end
represent(tokens)
end
def add_pagination_headers(relation)
Gitlab::Pagination::OffsetHeaderBuilder.new(
request_context: self,
per_page: relation.limit_value,
page: relation.current_page,
next_page: relation.next_page,
prev_page: relation.prev_page,
total: relation.total_count,
params: params.permit(:page)
).execute
end
def page
(params[:page] || 1).to_i
end
def finder(options = {})
PersonalAccessTokensFinder.new({ user: bot_users, impersonation: false }.merge(options))
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
module RenderAccessTokens
extend ActiveSupport::Concern
def active_access_tokens
tokens = finder(state: 'active', sort: 'expires_at_asc_id_desc').execute.preload_users
if Feature.enabled?('access_token_pagination')
tokens = tokens.page(page)
add_pagination_headers(tokens)
end
represent(tokens)
end
def add_pagination_headers(relation)
Gitlab::Pagination::OffsetHeaderBuilder.new(
request_context: self,
per_page: relation.limit_value,
page: relation.current_page,
next_page: relation.next_page,
prev_page: relation.prev_page,
total: relation.total_count,
params: params.permit(:page, :per_page)
).execute
end
def page
(params[:page] || 1).to_i
end
end

View File

@ -3,6 +3,7 @@
module Groups
module Settings
class AccessTokensController < Groups::ApplicationController
include RenderAccessTokens
include AccessTokensActions
layout 'group_settings'

View File

@ -1,6 +1,8 @@
# frozen_string_literal: true
class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
include RenderAccessTokens
feature_category :authentication_and_authorization
before_action :check_personal_access_tokens_enabled
@ -16,7 +18,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
respond_to do |format|
format.html
format.json do
render json: @active_personal_access_tokens
render json: @active_access_tokens
end
end
end
@ -30,7 +32,7 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
if result.success?
render json: { new_token: @personal_access_token.token,
active_access_tokens: active_personal_access_tokens }, status: :ok
active_access_tokens: active_access_tokens }, status: :ok
else
render json: { errors: result.errors }, status: :unprocessable_entity
end
@ -56,36 +58,13 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
def set_index_vars
@scopes = Gitlab::Auth.available_scopes_for(current_user)
@active_personal_access_tokens = active_personal_access_tokens
@active_access_tokens = active_access_tokens
end
def active_personal_access_tokens
tokens = finder(state: 'active', sort: 'expires_at_asc_id_desc').execute
if Feature.enabled?('access_token_pagination')
tokens = tokens.page(page)
add_pagination_headers(tokens)
end
def represent(tokens)
::PersonalAccessTokenSerializer.new.represent(tokens)
end
def add_pagination_headers(relation)
Gitlab::Pagination::OffsetHeaderBuilder.new(
request_context: self,
per_page: relation.limit_value,
page: relation.current_page,
next_page: relation.next_page,
prev_page: relation.prev_page,
total: relation.total_count,
params: params.permit(:page)
).execute
end
def page
(params[:page] || 1).to_i
end
def check_personal_access_tokens_enabled
render_404 if Gitlab::CurrentSettings.personal_access_tokens_disabled?
end

View File

@ -3,6 +3,7 @@
module Projects
module Settings
class AccessTokensController < Projects::ApplicationController
include RenderAccessTokens
include AccessTokensActions
layout 'project_settings'

View File

@ -5,7 +5,7 @@ module Resolvers
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Tree::BlobType.connection_type, null: true
authorize :download_code
authorize :read_code
calls_gitaly!
alias_method :repository, :object

View File

@ -4,7 +4,7 @@ module Types
class CommitType < BaseObject
graphql_name 'Commit'
authorize :download_code
authorize :read_code
present_using CommitPresenter

View File

@ -603,7 +603,7 @@ module Types
end
def sast_ci_configuration
return unless Ability.allowed?(current_user, :download_code, object)
return unless Ability.allowed?(current_user, :read_code, object)
::Security::CiConfiguration::SastParserService.new(object).configuration
end

View File

@ -8,6 +8,8 @@ module Types
accepts ::ProtectedBranch
authorize :read_protected_branch
alias_method :branch_rule, :object
field :name,
type: GraphQL::Types::String,
null: false,
@ -20,6 +22,12 @@ module Types
calls_gitaly: true,
description: "Check if this branch rule protects the project's default branch."
field :matching_branches_count,
type: GraphQL::Types::Int,
null: false,
calls_gitaly: true,
description: 'Number of existing branches that match this branch rule.'
field :branch_protection,
type: Types::BranchRules::BranchProtectionType,
null: false,
@ -35,6 +43,10 @@ module Types
Types::TimeType,
null: false,
description: 'Timestamp of when the branch rule was last updated.'
def matching_branches_count
branch_rule.matching(branch_rule.project.repository.branch_names).count
end
end
end
end

View File

@ -14,12 +14,12 @@ module Types
GraphQL::Types::String,
null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=closed`.',
authorize: :download_code
authorize: :read_code
field :closed_merge_requests_url,
GraphQL::Types::String,
null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=closed`.',
authorize: :download_code
authorize: :read_code
field :edit_url, GraphQL::Types::String, null: true,
description: "HTTP URL of the release's edit page.",
authorize: :update_release
@ -27,17 +27,17 @@ module Types
GraphQL::Types::String,
null: true,
description: 'HTTP URL of the merge request page , filtered by this release and `state=merged`.',
authorize: :download_code
authorize: :read_code
field :opened_issues_url,
GraphQL::Types::String,
null: true,
description: 'HTTP URL of the issues page, filtered by this release and `state=open`.',
authorize: :download_code
authorize: :read_code
field :opened_merge_requests_url,
GraphQL::Types::String,
null: true,
description: 'HTTP URL of the merge request page, filtered by this release and `state=open`.',
authorize: :download_code
authorize: :read_code
field :self_url, GraphQL::Types::String, null: true,
description: 'HTTP URL of the release.'
end

View File

@ -5,7 +5,7 @@ module Types
graphql_name 'ReleaseSource'
description 'Represents the source code attached to a release in a particular format'
authorize :download_code
authorize :read_code
field :format, GraphQL::Types::String, null: true,
description: 'Format of the source.'

View File

@ -39,7 +39,7 @@ module Types
description: 'Name of the tag associated with the release.'
field :tag_path, GraphQL::Types::String, null: true,
description: 'Relative web path to the tag associated with the release.',
authorize: :download_code
authorize: :read_code
field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?,
description: 'Indicates the release is an upcoming release.'
field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?,

View File

@ -4,7 +4,7 @@ module Types
class RepositoryType < BaseObject
graphql_name 'Repository'
authorize :download_code
authorize :read_code
field :blobs, Types::Repository::BlobType.connection_type, null: true, resolver: Resolvers::BlobsResolver, calls_gitaly: true,
description: 'Blobs contained within the repository'

View File

@ -172,7 +172,7 @@ module Ci
add_authentication_token_field :token, encrypted: :required
before_save :ensure_token
before_save :ensure_token, unless: :assign_token_on_scheduling?
after_save :stick_build_if_status_changed
@ -247,6 +247,14 @@ module Ci
!build.waiting_for_deployment_approval? # If false is returned, it stops the transition
end
before_transition any => [:pending] do |build, transition|
if build.assign_token_on_scheduling?
build.ensure_token
end
true
end
after_transition created: :scheduled do |build|
build.run_after_commit do
Ci::BuildScheduleWorker.perform_at(build.scheduled_at, build.id)
@ -1128,6 +1136,10 @@ module Ci
end
end
def assign_token_on_scheduling?
::Feature.enabled?(:ci_assign_job_token_on_scheduling, project)
end
protected
def run_status_commit_hooks!

View File

@ -1,31 +1,35 @@
# frozen_string_literal: true
module FileStoreMounter
ALLOWED_FILE_FIELDS = %i[file signed_file].freeze
extend ActiveSupport::Concern
class_methods do
# When `skip_store_file: true` is used, the model MUST explicitly call `store_file_now!`
def mount_file_store_uploader(uploader, skip_store_file: false)
mount_uploader(:file, uploader)
# When `skip_store_file: true` is used, the model MUST explicitly call `store_#{file_field}_now!`
def mount_file_store_uploader(uploader, skip_store_file: false, file_field: :file)
raise ArgumentError, "file_field not allowed: #{file_field}" unless ALLOWED_FILE_FIELDS.include?(file_field)
mount_uploader(file_field, uploader)
define_method("update_#{file_field}_store") do
# The file.object_store is set during `uploader.store!` and `uploader.migrate!`
update_column("#{file_field}_store", public_send(file_field).object_store) # rubocop:disable GitlabSecurity/PublicSend
end
define_method("store_#{file_field}_now!") do
public_send("store_#{file_field}!") # rubocop:disable GitlabSecurity/PublicSend
public_send("update_#{file_field}_store") # rubocop:disable GitlabSecurity/PublicSend
end
if skip_store_file
skip_callback :save, :after, :store_file!
skip_callback :save, :after, "store_#{file_field}!".to_sym
return
end
# This hook is a no-op when the file is uploaded after_commit
after_save :update_file_store, if: :saved_change_to_file?
after_save "update_#{file_field}_store".to_sym, if: "saved_change_to_#{file_field}?".to_sym
end
end
def update_file_store
# The file.object_store is set during `uploader.store!` and `uploader.migrate!`
update_column(:file_store, file.object_store)
end
def store_file_now!
store_file!
update_file_store
end
end

View File

@ -85,8 +85,7 @@ module Packages
scope :with_codename_or_suite, ->(codename_or_suite) { with_codename(codename_or_suite).or(with_suite(codename_or_suite)) }
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader
mount_uploader :signed_file, Packages::Debian::DistributionReleaseFileUploader
after_save :update_signed_file_store, if: :saved_change_to_signed_file?
mount_file_store_uploader Packages::Debian::DistributionReleaseFileUploader, file_field: :signed_file
def component_names
components.pluck(:name).sort
@ -119,12 +118,6 @@ module Packages
self.class.with_container(container).with_codename(suite).exists?
end
def update_signed_file_store
# The signed_file.object_store is set during `uploader.store!`
# which happens after object is inserted/updated
self.update_column(:signed_file_store, signed_file.object_store)
end
end
end
end

View File

@ -2,6 +2,7 @@
class ProjectSetting < ApplicationRecord
include ::Gitlab::Utils::StrongMemoize
include EachBatch
ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze

View File

@ -4,13 +4,13 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
presents ::Release, as: :release
def commit_path
return unless release.commit && can_download_code?
return unless release.commit && can_read_code?
project_commit_path(project, release.commit.id)
end
def tag_path
return unless can_download_code?
return unless can_read_code?
project_tag_path(project, release.tag)
end
@ -47,7 +47,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
delegator_override :assets_count
def assets_count
if can_download_code?
if can_read_code?
release.assets_count
else
release.assets_count(except: [:sources])
@ -67,8 +67,8 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
private
def can_download_code?
can?(current_user, :download_code, project)
def can_read_code?
can?(current_user, :read_code, project)
end
def params_for_issues_and_mrs(state: 'opened')

View File

@ -8,6 +8,15 @@ module Users
def initialize(current_user)
@current_user = current_user
@scheduled_records_gauge = Gitlab::Metrics.gauge(
:gitlab_ghost_user_migration_scheduled_records_total,
'The total number of scheduled ghost user migrations'
)
@lag_gauge = Gitlab::Metrics.gauge(
:gitlab_ghost_user_migration_lag_seconds,
'The waiting time in seconds of the oldest scheduled record for ghost user migration'
)
end
# Asynchronously destroys +user+
@ -64,6 +73,39 @@ module Users
Users::GhostUserMigration.create!(user: user,
initiator_user: current_user,
hard_delete: hard_delete)
update_metrics
end
private
attr_reader :scheduled_records_gauge, :lag_gauge
def update_metrics
update_scheduled_records_gauge
update_lag_gauge
end
def update_scheduled_records_gauge
# We do not want to issue unbounded COUNT() queries, hence we limit the
# query to count 1001 records and then approximate the result.
count = Users::GhostUserMigration.limit(1001).count
if count == 1001
# more than 1000 records, approximate count
min = Users::GhostUserMigration.minimum(:id) || 0
max = Users::GhostUserMigration.maximum(:id) || 0
scheduled_records_gauge.set({}, max - min)
else
# less than 1000 records, count is accurate
scheduled_records_gauge.set({}, count)
end
end
def update_lag_gauge
oldest_job = Users::GhostUserMigration.first
lag_gauge.set({}, Time.current - oldest_job.created_at)
end
end
end

View File

@ -1,6 +1,6 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
- if group.adjourned_deletion?
= render_if_exists 'groups/settings/adjourned_deletion', group: group, remove_form_id: remove_form_id
= render_if_exists 'groups/settings/delayed_deletion', group: group, remove_form_id: remove_form_id
- else
= render 'groups/settings/permanent_deletion', group: group, remove_form_id: remove_form_id

View File

@ -39,6 +39,5 @@
prefix: :resource_access_token,
help_path: help_page_path('user/group/settings/group_access_tokens', anchor: 'scopes-for-a-group-access-token')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_resource_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This group has no active access tokens.'), show_role: true
} }

View File

@ -76,7 +76,7 @@
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-assigned-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:assigned]
%li
= link_to reviewer_mrs_dashboard_path, class: 'gl-display-flex! gl-align-items-center js-prefetch-document' do
= link_to reviewer_mrs_dashboard_path, class: 'dashboard-shortcuts-review_requests gl-display-flex! gl-align-items-center js-prefetch-document' do
= _('Review requests for you')
= gl_badge_tag({ variant: :neutral, size: :sm }, { class: "js-reviewer-mr-count gl-ml-auto" }) do
= user_merge_requests_counts[:review_requested]

View File

@ -25,6 +25,6 @@
scopes: @scopes,
help_path: help_page_path('user/profile/personal_access_tokens.md', anchor: 'personal-access-token-scopes')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_personal_access_tokens.to_json } }
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json } }
#js-tokens-app{ data: { tokens_data: tokens_app_data } }

View File

@ -40,5 +40,5 @@
description_prefix: :project_access_token,
help_path: help_page_path('user/project/settings/project_access_tokens', anchor: 'scopes-for-a-project-access-token')
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_resource_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true
#js-access-token-table-app{ data: { access_token_type: type, access_token_type_plural: type_plural, initial_active_access_tokens: @active_access_tokens.to_json, no_active_tokens_message: _('This project has no active access tokens.'), show_role: true
} }

View File

@ -65,7 +65,7 @@
= gl_loading_icon(inline: true)
- if issuable_sidebar.dig(:features_available, :health_status)
.js-sidebar-health-status-entry-point{ data: sidebar_status_data(issuable_sidebar, @project) }
.js-sidebar-health-status-widget-root{ data: sidebar_status_data(issuable_sidebar, @project) }
- if issuable_sidebar.has_key?(:confidential)
-# haml-lint:disable InlineJavaScript

View File

@ -0,0 +1,8 @@
---
name: ci_assign_job_token_on_scheduling
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/103377
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/382042
milestone: '15.6'
type: development
group: group::pipeline execution
default_enabled: false

View File

@ -770,7 +770,7 @@ Gitlab.ee do
Settings.cron_jobs['iterations_generator_worker']['cron'] ||= '5 0 * * *'
Settings.cron_jobs['iterations_generator_worker']['job_class'] = 'Iterations::Cadences::ScheduleCreateIterationsWorker'
Settings.cron_jobs['vulnerability_statistics_schedule_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['vulnerability_statistics_schedule_worker']['cron'] ||= '15 1 * * *'
Settings.cron_jobs['vulnerability_statistics_schedule_worker']['cron'] ||= '15 1,20 * * *'
Settings.cron_jobs['vulnerability_statistics_schedule_worker']['job_class'] = 'Vulnerabilities::Statistics::ScheduleWorker'
Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['vulnerability_historical_statistics_deletion_worker']['cron'] ||= '15 3 * * *'

View File

@ -25,6 +25,8 @@ metadata:
description: Operations related to clusters
- name: ci_resource_groups
description: Operations to manage job concurrency with resource groups
- name: dependency_proxy
description: Operations to manage dependency proxy for a groups
- name: deploy_keys
description: Operations related to deploy keys
- name: deploy_tokens
@ -67,6 +69,8 @@ metadata:
description: Operations related to release assets (links)
- name: releases
description: Operations related to releases
- name: resource_milestone_events
description: Operations about resource milestone events
- name: suggestions
description: Operations related to suggestions
- name: system_hooks

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
class AddMaxSeatsUsedChangedAtIndexToGitlabSubscriptions < Gitlab::Database::Migration[2.0]
INDEX_NAME = 'index_gitlab_subscriptions_on_max_seats_used_changed_at'
disable_ddl_transaction!
def up
add_concurrent_index :gitlab_subscriptions, [:max_seats_used_changed_at, :namespace_id], name: INDEX_NAME
end
def down
remove_concurrent_index_by_name :gitlab_subscriptions, INDEX_NAME
end
end

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
class AddSupportingIndexForVulnerabilitiesFeedbackMigration < Gitlab::Database::Migration[2.0]
INDEX_NAME = "tmp_idx_for_vulnerability_feedback_migration"
WHERE_CLAUSE = "migrated_to_state_transition = false AND feedback_type = 0"
disable_ddl_transaction!
def up
add_concurrent_index(
:vulnerability_feedback,
%i[migrated_to_state_transition feedback_type],
where: WHERE_CLAUSE,
name: INDEX_NAME
)
end
def down
remove_concurrent_index_by_name(
:vulnerability_feedback,
INDEX_NAME
)
end
end

View File

@ -0,0 +1 @@
2e7e55a23574d45e877712fb67b2c2b50d85905c95fe4ec3990cfd8fe5160122

View File

@ -0,0 +1 @@
4e5deb2f5be081eef7b3dab726b2877bc21a7afad1b6a12aca240f510cada0b3

View File

@ -29137,6 +29137,8 @@ CREATE INDEX index_gitlab_subscriptions_on_end_date_and_namespace_id ON gitlab_s
CREATE INDEX index_gitlab_subscriptions_on_hosted_plan_id ON gitlab_subscriptions USING btree (hosted_plan_id);
CREATE INDEX index_gitlab_subscriptions_on_max_seats_used_changed_at ON gitlab_subscriptions USING btree (max_seats_used_changed_at, namespace_id);
CREATE UNIQUE INDEX index_gitlab_subscriptions_on_namespace_id ON gitlab_subscriptions USING btree (namespace_id);
CREATE UNIQUE INDEX index_gpg_key_subkeys_on_fingerprint ON gpg_key_subkeys USING btree (fingerprint);
@ -31221,6 +31223,8 @@ CREATE UNIQUE INDEX taggings_idx ON taggings USING btree (tag_id, taggable_id, t
CREATE UNIQUE INDEX term_agreements_unique_index ON term_agreements USING btree (user_id, term_id);
CREATE INDEX tmp_idx_for_vulnerability_feedback_migration ON vulnerability_feedback USING btree (migrated_to_state_transition, feedback_type) WHERE ((migrated_to_state_transition = false) AND (feedback_type = 0));
CREATE INDEX tmp_idx_vulnerabilities_on_id_where_report_type_7_99 ON vulnerabilities USING btree (id) WHERE (report_type = ANY (ARRAY[7, 99]));
CREATE INDEX tmp_idx_where_user_details_fields_filled ON users USING btree (id) WHERE (((COALESCE(linkedin, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(twitter, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(skype, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(website_url, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(location, ''::character varying))::text IS DISTINCT FROM ''::text) OR ((COALESCE(organization, ''::character varying))::text IS DISTINCT FROM ''::text));

View File

@ -44,6 +44,8 @@ The following metrics are available:
| `gitlab_ci_pipeline_size_builds` | Histogram | 13.1 | Total number of builds within a pipeline grouped by a pipeline source | `source` |
| `gitlab_ci_runner_authentication_success_total` | Counter | 15.2 | Total number of times that runner authentication has succeeded | `type` |
| `gitlab_ci_runner_authentication_failure_total` | Counter | 15.2 | Total number of times that runner authentication has failed
| `gitlab_ghost_user_migration_lag_seconds` | Gauge | 15.6 | The waiting time in seconds of the oldest scheduled record for ghost user migration | |
| `gitlab_ghost_user_migration_scheduled_records_total` | Gauge | 15.6 | The total number of scheduled ghost user migrations | |
| `job_waiter_started_total` | Counter | 12.9 | Number of batches of jobs started where a web request is waiting for the jobs to complete | `worker` |
| `job_waiter_timeouts_total` | Counter | 12.9 | Number of batches of jobs that timed out where a web request is waiting for the jobs to complete | `worker` |
| `gitlab_ci_active_jobs` | Histogram | 14.2 | Count of active jobs when pipeline is created | |

View File

@ -1853,7 +1853,7 @@ Updates to example must be made at:
gitlab_rails['auto_migrate'] = false
# Sidekiq
sidekiqp['enable'] = true
sidekiq['enable'] = true
sidekiq['listen_address'] = "0.0.0.0"
# Set number of Sidekiq queue processes to the same number as available CPUs

View File

@ -9942,6 +9942,36 @@ Represents the access level of a relationship between a User and object that it
| <a id="accesslevelintegervalue"></a>`integerValue` | [`Int`](#int) | Integer representation of access level. |
| <a id="accesslevelstringvalue"></a>`stringValue` | [`AccessLevelEnum`](#accesslevelenum) | String representation of access level. |
### `AccessLevelGroup`
Representation of a GitLab group.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="accesslevelgroupavatarurl"></a>`avatarUrl` | [`String`](#string) | Avatar URL of the group. |
| <a id="accesslevelgroupid"></a>`id` | [`ID!`](#id) | ID of the group. |
| <a id="accesslevelgroupname"></a>`name` | [`String!`](#string) | Name of the group. |
| <a id="accesslevelgroupparent"></a>`parent` | [`AccessLevelGroup`](#accesslevelgroup) | Parent group. |
| <a id="accesslevelgroupweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the group. |
### `AccessLevelUser`
Representation of a GitLab user.
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="accessleveluseravatarurl"></a>`avatarUrl` | [`String`](#string) | URL of the user's avatar. |
| <a id="accessleveluserid"></a>`id` | [`ID!`](#id) | ID of the user. |
| <a id="accesslevelusername"></a>`name` | [`String!`](#string) | Human-readable name of the user. Returns `****` if the user is a project bot and the requester does not have permission to view the project. |
| <a id="accessleveluserpublicemail"></a>`publicEmail` | [`String`](#string) | User's public email. |
| <a id="accessleveluserusername"></a>`username` | [`String!`](#string) | Username of the user. Unique within this instance of GitLab. |
| <a id="accessleveluserwebpath"></a>`webPath` | [`String!`](#string) | Web path of the user. |
| <a id="accessleveluserweburl"></a>`webUrl` | [`String!`](#string) | Web URL of the user. |
### `AgentConfiguration`
Configuration details for an Agent.
@ -10553,6 +10583,7 @@ List of branch rules for a project, grouped by branch name.
| <a id="branchrulecreatedat"></a>`createdAt` | [`Time!`](#time) | Timestamp of when the branch rule was created. |
| <a id="branchruleexternalstatuschecks"></a>`externalStatusChecks` | [`ExternalStatusCheckConnection`](#externalstatuscheckconnection) | External status checks configured for this branch rule. (see [Connections](#connections)) |
| <a id="branchruleisdefault"></a>`isDefault` | [`Boolean!`](#boolean) | Check if this branch rule protects the project's default branch. |
| <a id="branchrulematchingbranchescount"></a>`matchingBranchesCount` | [`Int!`](#int) | Number of existing branches that match this branch rule. |
| <a id="branchrulename"></a>`name` | [`String!`](#string) | Branch name, with wildcards, for the branch rules. |
| <a id="branchruleupdatedat"></a>`updatedAt` | [`Time!`](#time) | Timestamp of when the branch rule was last updated. |
@ -14499,8 +14530,8 @@ Defines which user roles, users, or groups can merge into a protected branch.
| ---- | ---- | ----------- |
| <a id="mergeaccesslevelaccesslevel"></a>`accessLevel` | [`Int!`](#int) | GitLab::Access level. |
| <a id="mergeaccesslevelaccessleveldescription"></a>`accessLevelDescription` | [`String!`](#string) | Human readable representation for this access level. |
| <a id="mergeaccesslevelgroup"></a>`group` | [`Group`](#group) | Group associated with this access level. |
| <a id="mergeaccessleveluser"></a>`user` | [`UserCore`](#usercore) | User associated with this access level. |
| <a id="mergeaccesslevelgroup"></a>`group` | [`AccessLevelGroup`](#accesslevelgroup) | Group associated with this access level. |
| <a id="mergeaccessleveluser"></a>`user` | [`AccessLevelUser`](#accessleveluser) | User associated with this access level. |
### `MergeRequest`
@ -18072,8 +18103,8 @@ Defines which user roles, users, or groups can push to a protected branch.
| ---- | ---- | ----------- |
| <a id="pushaccesslevelaccesslevel"></a>`accessLevel` | [`Int!`](#int) | GitLab::Access level. |
| <a id="pushaccesslevelaccessleveldescription"></a>`accessLevelDescription` | [`String!`](#string) | Human readable representation for this access level. |
| <a id="pushaccesslevelgroup"></a>`group` | [`Group`](#group) | Group associated with this access level. |
| <a id="pushaccessleveluser"></a>`user` | [`UserCore`](#usercore) | User associated with this access level. |
| <a id="pushaccesslevelgroup"></a>`group` | [`AccessLevelGroup`](#accesslevelgroup) | Group associated with this access level. |
| <a id="pushaccessleveluser"></a>`user` | [`AccessLevelUser`](#accessleveluser) | User associated with this access level. |
### `PushRules`
@ -19327,8 +19358,8 @@ Defines which user roles, users, or groups can unprotect a protected branch.
| ---- | ---- | ----------- |
| <a id="unprotectaccesslevelaccesslevel"></a>`accessLevel` | [`Int!`](#int) | GitLab::Access level. |
| <a id="unprotectaccesslevelaccessleveldescription"></a>`accessLevelDescription` | [`String!`](#string) | Human readable representation for this access level. |
| <a id="unprotectaccesslevelgroup"></a>`group` | [`Group`](#group) | Group associated with this access level. |
| <a id="unprotectaccessleveluser"></a>`user` | [`UserCore`](#usercore) | User associated with this access level. |
| <a id="unprotectaccesslevelgroup"></a>`group` | [`AccessLevelGroup`](#accesslevelgroup) | Group associated with this access level. |
| <a id="unprotectaccessleveluser"></a>`user` | [`AccessLevelUser`](#accessleveluser) | User associated with this access level. |
### `UploadRegistry`

View File

@ -11,7 +11,7 @@ This page describes the API for [suggesting changes](../user/project/merge_reque
Every API call to suggestions must be authenticated.
## Applying suggestions
## Applying a suggestion
Applies a suggested patch in a merge request. Users must have
at least the Developer role to perform such action.
@ -22,7 +22,7 @@ PUT /suggestions/:id/apply
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID of a suggestion |
| `id` | integer | yes | The ID of a suggestion |
| `commit_message` | string | no | A custom commit message to use instead of the default generated message or the project's default message |
```shell
@ -32,13 +32,53 @@ curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab
Example response:
```json
{
"id": 5,
"from_line": 10,
"to_line": 10,
"applicable": true,
"applied": false,
"from_content": "This is an example\n",
"to_content": "This is an example\n"
}
```
## Applying multiple suggestions
```plaintext
PUT /suggestions/batch_apply
```
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `ids` | integer | yes | The ID of a suggestion |
| `commit_message` | string | no | A custom commit message to use instead of the default generated message or the project's default message |
```shell
curl --request PUT --header "PRIVATE-TOKEN: <your_access_token>" --header 'Content-Type: application/json' --data '{"ids": [5, 6]}' "https://gitlab.example.com/api/v4/suggestions/batch_apply"
```
Example response:
```json
[
{
"id": 36,
"id": 5,
"from_line": 10,
"to_line": 10,
"applicable": false,
"applied": true,
"from_content": " \"--talk-name=org.freedesktop.\",\n",
"to_content": " \"--talk-name=org.free.\",\n \"--talk-name=org.desktop.\",\n"
"applicable": true,
"applied": false,
"from_content": "This is an example\n",
"to_content": "This is an example\n"
}
{
"id": 6,
"from_line": 19
"to_line": 19,
"applicable": true,
"applied": false,
"from_content": "This is another eaxmple\n",
"to_content": "This is another example\n"
}
]
```

View File

@ -160,7 +160,7 @@ For example:
```ruby
def initialize
name_of_the_future_flag_activated = false
name_of_the_feature_flag_activated = false
...
end
```

View File

@ -206,6 +206,22 @@ Refer to [`strong_memoize.rb`](https://gitlab.com/gitlab-org/gitlab/-/blob/maste
end
```
There's also `strong_memoize_with` to help memoize methods that take arguments.
This should be used for methods that have a low number of possible values
as arguments or with consistent repeating arguments in a loop.
```ruby
class Find
include Gitlab::Utils::StrongMemoize
def result(basic: true)
strong_memoize_with(:result, basic) do
search(basic)
end
end
end
```
- Clear memoization
```ruby

View File

@ -43,23 +43,19 @@ GitLab supports the following OmniAuth providers.
Before you configure the OmniAuth provider,
configure the settings that are common for all providers.
Setting | Description | Default value
---------------------------|-------------|--------------
`allow_single_sign_on` | Enables you to list the providers that automatically create a GitLab account. The provider names are available in the **OmniAuth provider name** column in the [supported providers table](#supported-providers). | The default is `false`. If `false`, users must be created manually, or they can't sign in using OmniAuth.
`auto_link_ldap_user` | If enabled, creates an LDAP identity in GitLab for users that are created through an OmniAuth provider. You can enable this setting if you have [LDAP integration](../administration/auth/ldap/index.md) enabled. Requires the `uid` of the user to be the same in both LDAP and the OmniAuth provider. | The default is `false`.
`block_auto_created_users` | If enabled, blocks users that are automatically created from signing in until they are approved by an administrator. | The default is `true`. If you set the value to `false`, make sure you only define providers for `allow_single_sign_on` that you can control, like SAML or Google. Otherwise, any user on the internet can sign in to GitLab without an administrator's approval.
Omnibus, Docker, and source | Helm chart | Description | Default value
----------------------------|------------|-------------|-----------
`allow_single_sign_on` | `allowSingleSignOn` | List of providers that automatically create a GitLab account. The provider names are available in the **OmniAuth provider name** column in the [supported providers table](#supported-providers). | `false`, which means that signing in using your OmniAuth provider account without a pre-existing GitLab account is not allowed. You must create a GitLab account first, and then connect it to your OmniAuth provider account through your profile settings.
`auto_link_ldap_user` | `autoLinkLdapUser` | Creates an LDAP identity in GitLab for users that are created through an OmniAuth provider. You can enable this setting if you have [LDAP integration](../administration/auth/ldap/index.md) enabled. Requires the `uid` of the user to be the same in both LDAP and the OmniAuth provider. | `false`
`block_auto_created_users` | `blockAutoCreatedUsers` | Blocks users that are automatically created from signing in until they are approved by an administrator. | `true`. If you set the value to `false`, make sure you define providers that you can control, like SAML or Google. Otherwise, any user on the internet can sign in to GitLab without an administrator's approval.
To change these settings:
- **For Omnibus package**
::Tabs
1. Open the configuration file:
:::TabTitle Omnibus
```shell
sudo editor /etc/gitlab/gitlab.rb
```
1. Update the following section:
1. Edit `/etc/gitlab/gitlab.rb` and update the following section:
```ruby
# CAUTION!
@ -71,13 +67,47 @@ To change these settings:
gitlab_rails['omniauth_block_auto_created_users'] = true
```
- **For installations from source**
1. Reconfigure GitLab:
```shell
sudo gitlab-ctl reconfigure
```
:::TabTitle Helm chart
1. Export the Helm values:
```shell
helm get values gitlab > gitlab_values.yaml
```
1. Edit `gitlab_values.yaml`, and update the `omniauth` section under `globals.appConfig`:
```yaml
global:
appConfig:
omniauth:
enabled: true
allowSingleSignOn: ['saml', 'twitter']
autoLinkLdapUser: false
blockAutoCreatedUsers: true
```
For more details, see the
[globals documentation](https://docs.gitlab.com/charts/charts/globals.html#omniauth).
1. Apply the new values:
```shell
helm upgrade -f gitlab_values.yaml gitlab gitlab/gitlab
```
:::TabTitle Source
1. Open the configuration file:
```shell
cd /home/git/gitlab
sudo -u git -H editor config/gitlab.yml
```
@ -102,6 +132,14 @@ To change these settings:
block_auto_created_users: true
```
1. Restart GitLab:
```shell
sudo service gitlab restart
```
::EndTabs
After configuring these settings, you can configure
your chosen [provider](#supported-providers).

View File

@ -384,13 +384,15 @@ Find where your version sits in the upgrade path below, and upgrade GitLab
accordingly, while also consulting the
[version-specific upgrade instructions](#version-specific-upgrading-instructions):
`8.11.Z` -> `8.12.0` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> [`11.11.8`](#1200) -> `12.0.12` -> [`12.1.17`](#1210) -> [`12.10.14`](#12100) -> `13.0.14` -> [`13.1.11`](#1310) -> [`13.8.8`](#1388) -> [`13.12.15`](#13120) -> [`14.0.12`](#1400) -> [`14.3.6`](#1430) -> [`14.9.5`](#1490) -> [`14.10.Z`](#14100) -> [`15.0.Z`](#1500) -> [`15.1.Z`](#1510)(for GitLab instances with multiple web nodes) -> [`15.4.0`](#1540) -> [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
`8.11.Z` -> `8.12.0` -> `8.17.7` -> `9.5.10` -> `10.8.7` -> [`11.11.8`](#1200) -> `12.0.12` -> [`12.1.17`](#1210) -> [`12.10.14`](#12100) -> `13.0.14` -> [`13.1.11`](#1310) -> [`13.8.8`](#1388) -> [`13.12.15`](#13120) -> [`14.0.12`](#1400) -> [`14.3.6`](#1430) -> [`14.9.5`](#1490) -> [`14.10.Z`](#14100) -> [`15.0.Z`](#1500) -> [`15.1.Z`](#1510) (for GitLab instances with multiple web nodes) -> [`15.4.0`](#1540) -> [latest `15.Y.Z`](https://gitlab.com/gitlab-org/gitlab/-/releases)
NOTE:
When not explicitly specified, upgrade GitLab to the latest available patch
release rather than the first patch release, for example `13.8.8` instead of `13.8.0`.
This includes versions you must stop at on the upgrade path as there may
be fixes for issues relating to the upgrade process.
Specifically around a [major version](#upgrading-to-a-new-major-version),
crucial database schema and migration patches are included in the latest patch releases.
The following table, while not exhaustive, shows some examples of the supported
upgrade paths.

View File

@ -72,7 +72,7 @@ You may have connectivity issues due to the following reasons:
- Check if your GitLab instance has an encrypted connection to `customers.gitlab.com` (with IP addresses 172.64.146.11 and 104.18.41.245) on port 443:
```shell
curl --verbose "telnet://customers.gitlab.com/"
curl --verbose "https://customers.gitlab.com/"
```
- If the curl command returns a failure, either:

View File

@ -52,8 +52,13 @@ To retrieve metrics for deployment frequency, use the [GraphQL](../../api/graphq
## Lead time for changes
Lead time for changes measures the time to deliver a feature once it has been developed,
as described in [Measuring DevOps Performance](https://devops.com/measuring-devops-performance/).
DORA Lead time for changes measures the time to successfully deliver a commit into production.
This metric reflects the efficiency of CI/CD pipelines.
In GitLab, Lead time for changes calculates the median time it takes for a merge request to get merged into production.
We measure **from** code committed **to** code successfully running in production, without adding the `coding_time` to the calculation.
Over time, the lead time for changes should decrease, while your team's performance should increase.
Lead time for changes displays in several charts:
@ -63,6 +68,9 @@ Lead time for changes displays in several charts:
To retrieve metrics for lead time for changes, use the [GraphQL](../../api/graphql/reference/index.md) or the [REST](../../api/dora/metrics.md) APIs.
- The definition of lead time for change can vary widely, which often creates confusion within the industry.
- "Lead time for changes" is not the same as "Lead time". In the value stream, "Lead time" measures the time it takes for work on an issue to move from the moment it's requested (Issue created) to the moment it's fulfilled and delivered (Issue closed).
## Time to restore service
Time to restore service measures how long it takes an organization to recover from a failure in production.

View File

@ -149,7 +149,7 @@ The feature is not ready for production use.
To avoid displaying the changes that are already on target branch in the diff,
we compare the merge request's source branch with HEAD of the target branch.
When there are conflicts between the source and target branch, we show the
conflicts on the merge request diff:
When there are conflicts between the source and target branch, we show an alert
per conflicted file on the merge request diff:
![Example of a conflict shown in a merge request diff](img/conflict_ui_v14_0.png)
![Example of a conflict alert shown in a merge request diff](img/conflict_ui_v15_6.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 444 KiB

View File

@ -55,6 +55,20 @@ NOTE:
The **Set up CI/CD** button does not appear on an empty repository. For the button
to display, add a file to your repository.
## Preview Markdown
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/378966) in GitLab 15.6.
To preview Markdown content in the Web Editor, select the **Preview** tab.
In this tab, you can see a live Markdown preview that updates as you type alongside your content.
![The Markdown Live Preview](img/web_editor_markdown_live_preview.png)
To close the preview panel, do one of the following:
- Select the **Write** tab.
- From the context menu, select **Hide Live Preview**.
## Highlight lines
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56159) in GitLab 13.10 for GitLab SaaS instances.

View File

@ -159,6 +159,30 @@ To set a start date:
The due date must be the same or later than the start date.
If you select a start date to be later than the due date, the due date is then changed to the same day.
## Add a task to a milestone
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/367463) in GitLab 15.5 [with a flag](../administration/feature_flags.md) named `work_items_mvc_2`. Disabled by default.
FLAG:
On self-managed GitLab, by default this feature is not available. To make it available per group, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `work_items_mvc_2`. On GitLab.com, this feature is not available. The feature is not ready for production use.
You can add a task to a [milestone](project/milestones/index.md).
You can see the milestone title when you view a task.
If you create a task for an issue that already belongs to a milestone,
the new task inherits the milestone.
Prerequisites:
- You must have at least the Reporter role for the project.
To add a task to a milestone:
1. In the issue description, in the **Tasks** section, select the title of the task you want to edit.
The task window opens.
1. Next to **Milestone**, select **Add to milestone**.
If a task already belongs to a milestone, the dropdown list shows the current milestone.
1. From the dropdown list, select the milestone to be associated with the task.
## Set task weight **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/362550) in GitLab 15.3.

View File

@ -188,6 +188,7 @@ module API
mount ::API::Clusters::Agents
mount ::API::Commits
mount ::API::CommitStatuses
mount ::API::DependencyProxy
mount ::API::DeployKeys
mount ::API::DeployTokens
mount ::API::Deployments
@ -226,6 +227,7 @@ module API
mount ::API::RemoteMirrors
mount ::API::Repositories
mount ::API::ResourceAccessTokens
mount ::API::ResourceMilestoneEvents
mount ::API::Snippets
mount ::API::SnippetRepositoryStorageMoves
mount ::API::Statistics
@ -264,7 +266,6 @@ module API
mount ::API::ContainerRepositories
mount ::API::DebianGroupPackages
mount ::API::DebianProjectPackages
mount ::API::DependencyProxy
mount ::API::Discussions
mount ::API::ErrorTracking::ClientKeys
mount ::API::ErrorTracking::Collector
@ -314,7 +315,6 @@ module API
mount ::API::ProtectedTags
mount ::API::PypiPackages
mount ::API::ResourceLabelEvents
mount ::API::ResourceMilestoneEvents
mount ::API::ResourceStateEvents
mount ::API::RpmProjectPackages
mount ::API::RubygemPackages

View File

@ -12,11 +12,18 @@ module API
end
params do
requires :id, type: String, desc: 'The ID of a group'
requires :id, types: [String, Integer],
desc: 'The ID or URL-encoded path of the group owned by the authenticated user'
end
resource :groups, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc 'Deletes all dependency_proxy_blobs for a group' do
detail 'This feature was introduced in GitLab 12.10'
desc 'Purge the dependency proxy for a group' do
detail 'Schedules for deletion the cached manifests and blobs for a group.'\
'This endpoint requires the Owner role for the group.'
success code: 202
failure [
{ code: 401, message: 'Unauthorized' }
]
tags %w[dependency_proxy]
end
delete ':id/dependency_proxy/cache' do
not_found! unless user_group.dependency_proxy_feature_available?

View File

@ -3,18 +3,18 @@
module API
module Entities
class ResourceMilestoneEvent < Grape::Entity
expose :id
expose :id, documentation: { type: 'integer', example: 142 }
expose :user, using: Entities::UserBasic
expose :created_at
expose :resource_type do |event, _options|
expose :created_at, documentation: { type: 'dateTime', example: '2018-08-20T13:38:20.077Z' }
expose :resource_type, documentation: { type: 'string', example: 'Issue' } do |event, _options|
event.issuable.class.name
end
expose :resource_id do |event, _options|
expose :resource_id, documentation: { type: 'integer', example: 253 } do |event, _options|
event.issuable.id
end
expose :milestone, using: Entities::Milestone
expose :action
expose :state
expose :action, documentation: { type: 'string', example: 'add' }
expose :state, documentation: { type: 'string', example: 'active' }
end
end
end

View File

@ -5,6 +5,8 @@ module API
include PaginationParams
helpers ::API::Helpers::NotesHelpers
resource_milestone_events_tags = %w[resource_milestone_events]
before { authenticate! }
{
@ -15,17 +17,19 @@ module API
eventables_str = eventable_type.to_s.underscore.pluralize
params do
requires :id, type: String, desc: "The ID of a #{parent_type}"
requires :id, types: [String, Integer], desc: "The ID or URL-encoded path of the #{parent_type}"
end
resource parent_type.pluralize.to_sym, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
desc "Get a list of #{eventable_type.to_s.downcase} resource milestone events" do
desc "List project #{eventable_type.underscore.humanize} milestone events" do
detail "Gets a list of all milestone events for a single #{eventable_type.underscore.humanize}"
success Entities::ResourceMilestoneEvent
is_array true
tags resource_milestone_events_tags
end
params do
requires :eventable_id, types: [Integer, String], desc: 'The ID of the eventable'
use :pagination
end
get ":id/#{eventables_str}/:eventable_id/resource_milestone_events", feature_category: feature_category, urgency: :low do
eventable = find_noteable(eventable_type, params[:eventable_id])
@ -34,8 +38,13 @@ module API
present paginate(events), with: Entities::ResourceMilestoneEvent
end
desc "Get a single #{eventable_type.to_s.downcase} resource milestone event" do
desc "Get single #{eventable_type.underscore.humanize} milestone event" do
detail "Returns a single milestone event for a specific project #{eventable_type.underscore.humanize}"
success Entities::ResourceMilestoneEvent
failure [
{ code: 404, message: 'Not found' }
]
tags resource_milestone_events_tags
end
params do
requires :event_id, type: String, desc: 'The ID of a resource milestone event'

View File

@ -192,7 +192,7 @@ module API
desc 'Download specific version of a module' do
detail 'Download specific version of a module'
success code: 200, model: File
success File
failure [
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }

View File

@ -44,7 +44,7 @@ module API
desc 'Get a Terraform state version' do
detail 'Get a Terraform state version'
success code: 200, model: File
success File
failure [
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }

View File

@ -1108,6 +1108,11 @@ msgstr ""
msgid "%{text} is available"
msgstr ""
msgid "%{thenLabelStart}Then%{thenLabelEnd} Require %{approvalsRequired} approval from %{approverType}%{approvers}"
msgid_plural "%{thenLabelStart}Then%{thenLabelEnd} Require %{approvalsRequired} approvals from %{approverType}%{approvers}"
msgstr[0] ""
msgstr[1] ""
msgid "%{timebox_type} does not support burnup charts"
msgstr ""
@ -18769,6 +18774,9 @@ msgstr ""
msgid "Go to your projects"
msgstr ""
msgid "Go to your review requests"
msgstr ""
msgid "Go to your snippets"
msgstr ""
@ -34987,6 +34995,9 @@ msgstr ""
msgid "Runners|An error has occurred fetching instructions"
msgstr ""
msgid "Runners|An error occurred while deleting. Some runners may not have been deleted."
msgstr ""
msgid "Runners|An upgrade is available for this runner"
msgstr ""
@ -35479,6 +35490,9 @@ msgstr ""
msgid "Runner|Owner"
msgstr ""
msgid "Runner|Runner %{runnerName} failed to delete"
msgstr ""
msgid "Running"
msgstr ""
@ -36290,6 +36304,9 @@ msgstr ""
msgid "SecurityOrchestration|Choose a project"
msgstr ""
msgid "SecurityOrchestration|Choose approver type"
msgstr ""
msgid "SecurityOrchestration|Create more robust vulnerability rules and apply them to all your projects."
msgstr ""
@ -36347,9 +36364,15 @@ msgstr ""
msgid "SecurityOrchestration|Failed to load vulnerability scanners."
msgstr ""
msgid "SecurityOrchestration|Groups"
msgstr ""
msgid "SecurityOrchestration|If any scanner finds a newly detected critical vulnerability in an open merge request targeting the master branch, then require two approvals from any member of App security."
msgstr ""
msgid "SecurityOrchestration|Individual users"
msgstr ""
msgid "SecurityOrchestration|Inherited"
msgstr ""
@ -36488,6 +36511,9 @@ msgstr ""
msgid "SecurityOrchestration|Select security project"
msgstr ""
msgid "SecurityOrchestration|Select users"
msgstr ""
msgid "SecurityOrchestration|Something went wrong, unable to fetch policies"
msgstr ""
@ -41548,7 +41574,7 @@ msgstr ""
msgid "This group"
msgstr ""
msgid "This group and its subgroups and projects will be placed in a 'pending deletion' state for %{deletion_adjourned_period} days, then permanently deleted on %{date}. The group can be fully restored before that date."
msgid "This group and its subgroups and projects will be placed in a 'pending deletion' state for %{deletion_delayed_period} days, then permanently deleted on %{date}. The group can be fully restored before that date."
msgstr ""
msgid "This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group."
@ -48952,6 +48978,9 @@ msgstr ""
msgid "must be unique by status and elapsed time within a policy"
msgstr ""
msgid "must belong to same project of its requirement object."
msgstr ""
msgid "must belong to same project of the work item."
msgstr ""

View File

@ -7,9 +7,6 @@ module QA
it 'creates a new project' do
Page::Project::Show.perform do |project_page|
expect(project_page).to have_content(project_name)
expect(project_page).to have_content(
/Project \S?#{project_name}\S+ was successfully created/
)
expect(project_page).to have_content('The repository for this project is empty')
end
end

View File

@ -367,28 +367,22 @@ EOF
}
function verify_deploy() {
echoinfo "Verifying deployment at ${CI_ENVIRONMENT_URL}"
local namespace="${CI_ENVIRONMENT_SLUG}"
echoinfo "[$(date '+%H:%M:%S')] Verifying deployment at ${CI_ENVIRONMENT_URL}"
if retry "test_url \"${CI_ENVIRONMENT_URL}\""; then
echoinfo "Review app is deployed to ${CI_ENVIRONMENT_URL}"
echoinfo "[$(date '+%H:%M:%S')] Review app is deployed to ${CI_ENVIRONMENT_URL}"
return 0
else
echoerr "Review app is not available at ${CI_ENVIRONMENT_URL}: see the logs from cURL above for more details"
echoerr "State of the pods:"
kubectl get pods
echoerr "[$(date '+%H:%M:%S')] Review app is not available at ${CI_ENVIRONMENT_URL}: see the logs from cURL above for more details"
return 1
fi
}
function display_deployment_debug() {
local namespace="${CI_ENVIRONMENT_SLUG}"
local release="${CI_ENVIRONMENT_SLUG}"
# Get all pods for this release
echoinfo "Pods for release ${release}"
kubectl get pods --namespace "${namespace}" -lrelease=${release}
# Get all non-completed jobs
echoinfo "Unsuccessful Jobs for release ${release}"
kubectl get jobs --namespace "${namespace}" -lrelease=${release} --field-selector=status.successful!=1
echoinfo "Environment debugging data:"
kubectl get svc,pods,jobs --namespace "${namespace}"
}

View File

@ -5,7 +5,7 @@ function retry() {
for i in 2 1; do
sleep 3s
echo "Retrying $i..."
echo "[$(date '+%H:%M:%S')] Retrying $i..."
if eval "$@"; then
return 0
fi

View File

@ -3,11 +3,11 @@
require 'spec_helper'
RSpec.describe Profiles::PersonalAccessTokensController do
let(:user) { create(:user) }
let(:access_token_user) { create(:user) }
let(:token_attributes) { attributes_for(:personal_access_token) }
before do
sign_in(user)
sign_in(access_token_user)
end
describe '#create' do
@ -49,13 +49,27 @@ RSpec.describe Profiles::PersonalAccessTokensController do
end
end
describe 'GET /-/profile/personal_access_tokens' do
let(:get_access_tokens) do
get :index
response
end
subject(:get_access_tokens_with_page) do
get :index, params: { page: 1 }
response
end
it_behaves_like 'GET access tokens are paginated and ordered'
end
describe '#index' do
let!(:active_personal_access_token) { create(:personal_access_token, user: user) }
let!(:active_personal_access_token) { create(:personal_access_token, user: access_token_user) }
before do
# Impersonation and inactive personal tokens are ignored
create(:personal_access_token, :impersonation, user: user)
create(:personal_access_token, :revoked, user: user)
create(:personal_access_token, :impersonation, user: access_token_user)
create(:personal_access_token, :revoked, user: access_token_user)
get :index
end
@ -63,7 +77,7 @@ RSpec.describe Profiles::PersonalAccessTokensController do
active_personal_access_tokens_detail =
::PersonalAccessTokenSerializer.new.represent([active_personal_access_token])
expect(assigns(:active_personal_access_tokens).to_json).to eq(active_personal_access_tokens_detail.to_json)
expect(assigns(:active_access_tokens).to_json).to eq(active_personal_access_tokens_detail.to_json)
end
it "sets PAT name and scopes" do
@ -86,73 +100,10 @@ RSpec.describe Profiles::PersonalAccessTokensController do
expect(response).to have_gitlab_http_status(:not_found)
end
context "access_token_pagination feature flag is enabled" do
before do
stub_feature_flags(access_token_pagination: true)
allow(Kaminari.config).to receive(:default_per_page).and_return(1)
create(:personal_access_token, user: user)
end
it 'returns tokens for json format' do
get :index, params: { format: :json }
it "returns paginated response" do
get :index, params: { page: 1 }
expect(assigns(:active_personal_access_tokens).count).to eq(1)
end
it 'adds appropriate headers' do
get :index, params: { page: 1 }
expect_header('X-Per-Page', '1')
expect_header('X-Page', '1')
expect_header('X-Next-Page', '2')
expect_header('X-Total', '2')
end
expect(json_response.count).to eq(1)
end
context "tokens returned are ordered" do
let(:expires_1_day_from_now) { 1.day.from_now.to_date }
let(:expires_2_day_from_now) { 2.days.from_now.to_date }
before do
create(:personal_access_token, user: user, name: "Token1", expires_at: expires_1_day_from_now)
create(:personal_access_token, user: user, name: "Token2", expires_at: expires_2_day_from_now)
end
it "orders token list ascending on expires_at" do
get :index
first_token = assigns(:active_personal_access_tokens).first.as_json
expect(first_token['name']).to eq("Token1")
expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
end
it "orders tokens on id in case token has same expires_at" do
create(:personal_access_token, user: user, name: "Token3", expires_at: expires_1_day_from_now)
get :index
first_token = assigns(:active_personal_access_tokens).first.as_json
expect(first_token['name']).to eq("Token3")
expect(first_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
second_token = assigns(:active_personal_access_tokens).second.as_json
expect(second_token['name']).to eq("Token1")
expect(second_token['expires_at']).to eq(expires_1_day_from_now.strftime("%Y-%m-%d"))
end
end
context "access_token_pagination feature flag is disabled" do
before do
stub_feature_flags(access_token_pagination: false)
create(:personal_access_token, user: user)
end
it "returns all tokens in system" do
get :index, params: { page: 1 }
expect(assigns(:active_personal_access_tokens).count).to eq(2)
end
end
end
def expect_header(header_name, header_val)
expect(response.headers[header_name]).to eq(header_val)
end
end

View File

@ -24,6 +24,16 @@ FactoryBot.define do
project { pipeline.project }
trait :with_token do
transient do
generate_token { true }
end
after(:build) do |build, evaluator|
build.ensure_token if evaluator.generate_token
end
end
trait :degenerated do
options { nil }
yaml_variables { nil }
@ -93,6 +103,7 @@ FactoryBot.define do
end
trait :pending do
with_token
queued_at { 'Di 29. Okt 09:50:59 CET 2013' }
status { 'pending' }
@ -100,6 +111,7 @@ FactoryBot.define do
trait :created do
status { 'created' }
generate_token { false }
end
trait :preparing do

View File

@ -72,7 +72,6 @@ describe('RunnerBulkDelete', () => {
afterEach(() => {
bulkRunnerDeleteHandler.mockReset();
wrapper.destroy();
});
describe('When no runners are checked', () => {
@ -126,50 +125,61 @@ describe('RunnerBulkDelete', () => {
let evt;
let mockHideModal;
const confirmDeletion = () => {
evt = {
preventDefault: jest.fn(),
};
findModal().vm.$emit('primary', evt);
};
beforeEach(() => {
mockCheckedRunnerIds = [mockId1, mockId2];
createComponent();
jest.spyOn(mockState.localMutations, 'clearChecked').mockImplementation(() => {});
mockHideModal = jest.spyOn(findModal().vm, 'hide');
mockHideModal = jest.spyOn(findModal().vm, 'hide').mockImplementation(() => {});
});
describe('when deletion is successful', () => {
describe('when deletion is confirmed', () => {
beforeEach(() => {
bulkRunnerDeleteHandler.mockResolvedValue({
data: {
bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
},
});
evt = {
preventDefault: jest.fn(),
};
findModal().vm.$emit('primary', evt);
confirmDeletion();
});
it('has loading state', async () => {
it('has loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
expect(findModal().props('actionCancel').attributes.loading).toBe(true);
await waitForPromises();
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
it('modal is not prevented from closing', () => {
expect(evt.preventDefault).toHaveBeenCalledTimes(1);
});
it('mutation is called', async () => {
it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
input: { ids: mockCheckedRunnerIds },
});
});
});
it('user interface is updated', async () => {
describe('when deletion is successful', () => {
beforeEach(async () => {
bulkRunnerDeleteHandler.mockResolvedValue({
data: {
bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
},
});
confirmDeletion();
await waitForPromises();
});
it('removes loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
it('user interface is updated', () => {
const { evict, gc } = apolloCache;
expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
@ -183,54 +193,53 @@ describe('RunnerBulkDelete', () => {
expect(gc).toHaveBeenCalledTimes(1);
});
it('emits deletion confirmation', () => {
expect(wrapper.emitted('deleted')).toEqual([
[{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }],
]);
});
it('modal is hidden', () => {
expect(mockHideModal).toHaveBeenCalledTimes(1);
});
});
describe('when deletion fails', () => {
beforeEach(() => {
bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
describe('when deletion fails partially', () => {
beforeEach(async () => {
bulkRunnerDeleteHandler.mockResolvedValue({
data: {
bulkRunnerDelete: {
deletedIds: [mockId1], // only one runner could be deleted
errors: ['Can only delete up to 1 runners per call. Ignored 1 runner(s).'],
},
},
});
evt = {
preventDefault: jest.fn(),
};
findModal().vm.$emit('primary', evt);
confirmDeletion();
await waitForPromises();
});
it('has loading state', async () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(true);
expect(findModal().props('actionCancel').attributes.loading).toBe(true);
await waitForPromises();
it('removes loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
it('modal is not prevented from closing', () => {
expect(evt.preventDefault).toHaveBeenCalledTimes(1);
});
it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
input: { ids: mockCheckedRunnerIds },
});
});
it('user interface is not updated', async () => {
await waitForPromises();
it('user interface is partially updated', () => {
const { evict, gc } = apolloCache;
expect(evict).not.toHaveBeenCalled();
expect(gc).not.toHaveBeenCalled();
expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled();
expect(evict).toHaveBeenCalledTimes(1);
expect(evict).toHaveBeenCalledWith({
id: expect.stringContaining(mockId1),
});
expect(gc).toHaveBeenCalledTimes(1);
});
it('alert is called', async () => {
await waitForPromises();
it('emits deletion confirmation', () => {
expect(wrapper.emitted('deleted')).toEqual([[{ message: expect.stringContaining('1') }]]);
});
it('alert is called', () => {
expect(createAlert).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalledWith({
message: expect.any(String),
@ -238,6 +247,49 @@ describe('RunnerBulkDelete', () => {
error: expect.any(Error),
});
});
it('modal is hidden', () => {
expect(mockHideModal).toHaveBeenCalledTimes(1);
});
});
describe('when deletion fails', () => {
beforeEach(async () => {
bulkRunnerDeleteHandler.mockRejectedValue(new Error('error!'));
confirmDeletion();
await waitForPromises();
});
it('resolves loading state', () => {
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
expect(findModal().props('actionCancel').attributes.loading).toBe(false);
});
it('user interface is not updated', () => {
const { evict, gc } = apolloCache;
expect(evict).not.toHaveBeenCalled();
expect(gc).not.toHaveBeenCalled();
expect(mockState.localMutations.clearChecked).not.toHaveBeenCalled();
});
it('does not emit deletion confirmation', () => {
expect(wrapper.emitted('deleted')).toBeUndefined();
});
it('alert is called', () => {
expect(createAlert).toHaveBeenCalled();
expect(createAlert).toHaveBeenCalledWith({
message: expect.any(String),
captureError: true,
error: expect.any(Error),
});
});
it('modal is hidden', () => {
expect(mockHideModal).toHaveBeenCalledTimes(1);
});
});
});
});

View File

@ -17,6 +17,7 @@ import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
@ -96,7 +97,7 @@ describe('RunnerDeleteButton', () => {
});
it('Displays a modal with the runner name', () => {
expect(findModal().props('runnerName')).toBe(`#${mockRunnerId} (${mockRunner.shortSha})`);
expect(findModal().props('runnerName')).toBe(mockRunnerName);
});
it('Does not have tabindex when button is enabled', () => {
@ -189,6 +190,10 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
title: expect.stringContaining(mockRunnerName),
message: mockErrorMsg,
});
});
});
@ -217,6 +222,10 @@ describe('RunnerDeleteButton', () => {
it('error is shown to the user', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
title: expect.stringContaining(mockRunnerName),
message: `${mockErrorMsg} ${mockErrorMsg2}`,
});
});
it('does not evict runner from apollo cache', () => {

Some files were not shown because too many files have changed in this diff Show More