Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
61a82b8ec0
commit
2d80ade702
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
a415ff702cfd0755db5d1a09c63c13ce13b54f58
|
||||
4b3f2921b5f0d659b44aee6323d82fc3698a8ede
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }">
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ query getProjectsWithCIJobTokenScope($fullPath: ID!) {
|
|||
nodes {
|
||||
id
|
||||
name
|
||||
namespace {
|
||||
id
|
||||
fullPath
|
||||
}
|
||||
fullPath
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@ query issuableDetails($fullPath: ID!, $iid: String) {
|
|||
issuable: issue(iid: $iid) {
|
||||
id
|
||||
confidential
|
||||
milestone {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
fragment MilestoneFragment on Milestone {
|
||||
expired
|
||||
id
|
||||
title
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,16 +3,5 @@
|
|||
query workItem($id: WorkItemID!) {
|
||||
workItem(id: $id) {
|
||||
...WorkItem
|
||||
mockWidgets @client {
|
||||
... on LocalWorkItemMilestone {
|
||||
type
|
||||
nodes {
|
||||
id
|
||||
title
|
||||
expired
|
||||
dueDate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
module Groups
|
||||
module Settings
|
||||
class AccessTokensController < Groups::ApplicationController
|
||||
include RenderAccessTokens
|
||||
include AccessTokensActions
|
||||
|
||||
layout 'group_settings'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
module Projects
|
||||
module Settings
|
||||
class AccessTokensController < Projects::ApplicationController
|
||||
include RenderAccessTokens
|
||||
include AccessTokensActions
|
||||
|
||||
layout 'project_settings'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ module Types
|
|||
class CommitType < BaseObject
|
||||
graphql_name 'Commit'
|
||||
|
||||
authorize :download_code
|
||||
authorize :read_code
|
||||
|
||||
present_using CommitPresenter
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.'
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
class ProjectSetting < ApplicationRecord
|
||||
include ::Gitlab::Utils::StrongMemoize
|
||||
include EachBatch
|
||||
|
||||
ALLOWED_TARGET_PLATFORMS = %w(ios osx tvos watchos android).freeze
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
} }
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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 } }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
} }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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 * * *'
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
2e7e55a23574d45e877712fb67b2c2b50d85905c95fe4ec3990cfd8fe5160122
|
||||
|
|
@ -0,0 +1 @@
|
|||
4e5deb2f5be081eef7b3dab726b2877bc21a7afad1b6a12aca240f510cada0b3
|
||||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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 | |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ For example:
|
|||
|
||||
```ruby
|
||||
def initialize
|
||||
name_of_the_future_flag_activated = false
|
||||
name_of_the_feature_flag_activated = false
|
||||
...
|
||||
end
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||

|
||||

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

|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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' }
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue