diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-02 15:07:36 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-07-02 15:07:36 +0000 |
commit | e61f798b74e8e18fca7239fd01802182479bfcfc (patch) | |
tree | 4a49b062d8df2ffe3e6e8a07d2aadc034acb9092 | |
parent | afbfbfc87abfa006f1d369fdf9c740eb1c826808 (diff) | |
download | gitlab-ce-e61f798b74e8e18fca7239fd01802182479bfcfc.tar.gz |
Add latest changes from gitlab-org/gitlab@master
47 files changed, 1253 insertions, 280 deletions
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js index fa781ff7f0e..bab3cce39ac 100644 --- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js +++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js @@ -5,7 +5,7 @@ export const HELPER_TEXT_USAGE_PING_DISABLED = __( ); export const HELPER_TEXT_USAGE_PING_ENABLED = __( - 'You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service.', + 'You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service.', ); function setHelperText(usagePingCheckbox) { diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js index db7b3bad6ed..e88dbf20e1b 100644 --- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js +++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js @@ -8,6 +8,7 @@ import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deploy import { initInstallRunner } from '~/pages/shared/mount_runner_instructions'; import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle'; import initSettingsPanels from '~/settings_panels'; +import { initTokenAccess } from '~/token_access'; document.addEventListener('DOMContentLoaded', () => { // Initialize expandable settings panels @@ -40,4 +41,5 @@ document.addEventListener('DOMContentLoaded', () => { initSharedRunnersToggle(); initInstallRunner(); initRunnerAwsDeployments(); + initTokenAccess(); }); diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue index 284ff57870d..85d14547efd 100644 --- a/app/assets/javascripts/runner/components/runner_update_form.vue +++ b/app/assets/javascripts/runner/components/runner_update_form.vue @@ -7,36 +7,16 @@ import { GlFormInputGroup, GlTooltipDirective, } from '@gitlab/ui'; +import { + modelToUpdateMutationVariables, + runnerToModel, +} from 'ee_else_ce/runner/runner_details/runner_update_form_utils'; import createFlash, { FLASH_TYPES } from '~/flash'; import { __ } from '~/locale'; import { captureException } from '~/runner/sentry_utils'; import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants'; import runnerUpdateMutation from '../graphql/runner_update.mutation.graphql'; -const runnerToModel = (runner) => { - const { - id, - description, - maximumTimeout, - accessLevel, - active, - locked, - runUntagged, - tagList = [], - } = runner || {}; - - return { - id, - description, - maximumTimeout, - accessLevel, - active, - locked, - runUntagged, - tagList: tagList.join(', '), - }; -}; - export default { name: 'RunnerUpdateForm', components: { @@ -45,6 +25,8 @@ export default { GlFormCheckbox, GlFormGroup, GlFormInputGroup, + RunnerUpdateCostFactorFields: () => + import('ee_component/runner/components/runner_update_cost_factor_fields.vue'), }, directives: { GlTooltip: GlTooltipDirective, @@ -69,18 +51,6 @@ export default { readonlyIpAddress() { return this.runner?.ipAddress; }, - updateMutationInput() { - const { maximumTimeout, tagList } = this.model; - - return { - ...this.model, - maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null, - tagList: tagList - .split(',') - .map((tag) => tag.trim()) - .filter((tag) => Boolean(tag)), - }; - }, }, watch: { runner(newVal, oldVal) { @@ -100,9 +70,7 @@ export default { }, } = await this.$apollo.mutate({ mutation: runnerUpdateMutation, - variables: { - input: this.updateMutationInput, - }, + variables: modelToUpdateMutationVariables(this.model), }); if (errors?.length) { @@ -218,6 +186,8 @@ export default { <gl-form-input-group v-model="model.tagList" /> </gl-form-group> + <runner-update-cost-factor-fields v-model="model" /> + <div class="form-actions"> <gl-button type="submit" diff --git a/app/assets/javascripts/runner/graphql/get_runner.query.graphql b/app/assets/javascripts/runner/graphql/get_runner.query.graphql index 84e0d6cc95c..c294cb9bf22 100644 --- a/app/assets/javascripts/runner/graphql/get_runner.query.graphql +++ b/app/assets/javascripts/runner/graphql/get_runner.query.graphql @@ -1,4 +1,4 @@ -#import "~/runner/graphql/runner_details.fragment.graphql" +#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql" query getRunner($id: CiRunnerID!) { runner(id: $id) { diff --git a/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql index 6d7dc1e2798..2449ee0fc0f 100644 --- a/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql +++ b/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql @@ -1,12 +1,5 @@ +#import "./runner_details_shared.fragment.graphql" + fragment RunnerDetails on CiRunner { - id - runnerType - active - accessLevel - runUntagged - locked - ipAddress - description - maximumTimeout - tagList + ...RunnerDetailsShared } diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql new file mode 100644 index 00000000000..8c50cba7de3 --- /dev/null +++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql @@ -0,0 +1,12 @@ +fragment RunnerDetailsShared on CiRunner { + id + runnerType + active + accessLevel + runUntagged + locked + ipAddress + description + maximumTimeout + tagList +} diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql index d50c1880d77..dcc7fdf24f1 100644 --- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql +++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql @@ -1,4 +1,4 @@ -#import "~/runner/graphql/runner_details.fragment.graphql" +#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql" mutation runnerUpdate($input: RunnerUpdateInput!) { runnerUpdate(input: $input) { diff --git a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js b/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js new file mode 100644 index 00000000000..3b519fa7d71 --- /dev/null +++ b/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js @@ -0,0 +1,38 @@ +export const runnerToModel = (runner) => { + const { + id, + description, + maximumTimeout, + accessLevel, + active, + locked, + runUntagged, + tagList = [], + } = runner || {}; + + return { + id, + description, + maximumTimeout, + accessLevel, + active, + locked, + runUntagged, + tagList: tagList.join(', '), + }; +}; + +export const modelToUpdateMutationVariables = (model) => { + const { maximumTimeout, tagList } = model; + + return { + input: { + ...model, + maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null, + tagList: tagList + ?.split(',') + .map((tag) => tag.trim()) + .filter((tag) => Boolean(tag)), + }, + }; +}; diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue new file mode 100644 index 00000000000..271c90921f0 --- /dev/null +++ b/app/assets/javascripts/token_access/components/token_access.vue @@ -0,0 +1,207 @@ +<script> +import { GlButton, GlCard, GlFormGroup, GlFormInput, GlLoadingIcon, GlToggle } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { __, s__ } from '~/locale'; +import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql'; +import removeProjectCIJobTokenScopeMutation from '../graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql'; +import updateCIJobTokenScopeMutation from '../graphql/mutations/update_ci_job_token_scope.mutation.graphql'; +import getCIJobTokenScopeQuery from '../graphql/queries/get_ci_job_token_scope.query.graphql'; +import getProjectsWithCIJobTokenScopeQuery from '../graphql/queries/get_projects_with_ci_job_token_scope.query.graphql'; +import TokenProjectsTable from './token_projects_table.vue'; + +export default { + i18n: { + toggleLabelTitle: s__('CICD|Limit CI_JOB_TOKEN access'), + toggleHelpText: s__( + `CICD|Manage which projects can use this project's CI_JOB_TOKEN CI/CD variable for API access`, + ), + cardHeaderTitle: s__('CICD|Add an existing project to the scope'), + formGroupLabel: __('Search for project'), + addProject: __('Add project'), + cancel: __('Cancel'), + addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'), + projectsFetchError: __('There was a problem fetching the projects'), + scopeFetchError: __('There was a problem fetching the job token scope value'), + }, + components: { + GlButton, + GlCard, + GlFormGroup, + GlFormInput, + GlLoadingIcon, + GlToggle, + TokenProjectsTable, + }, + inject: { + fullPath: { + default: '', + }, + }, + apollo: { + jobTokenScopeEnabled: { + query: getCIJobTokenScopeQuery, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update(data) { + return data.project.ciCdSettings.jobTokenScopeEnabled; + }, + error() { + createFlash({ message: this.$options.i18n.scopeFetchError }); + }, + }, + projects: { + query: getProjectsWithCIJobTokenScopeQuery, + variables() { + return { + fullPath: this.fullPath, + }; + }, + update(data) { + return data.project?.ciJobTokenScope?.projects?.nodes ?? []; + }, + error() { + createFlash({ message: this.$options.i18n.projectsFetchError }); + }, + }, + }, + data() { + return { + jobTokenScopeEnabled: null, + targetProjectPath: '', + projects: [], + }; + }, + computed: { + isProjectPathEmpty() { + return this.targetProjectPath === ''; + }, + }, + methods: { + async updateCIJobTokenScope() { + try { + const { + data: { + ciCdSettingsUpdate: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: updateCIJobTokenScopeMutation, + variables: { + input: { + fullPath: this.fullPath, + jobTokenScopeEnabled: this.jobTokenScopeEnabled, + }, + }, + }); + + if (errors.length) { + throw new Error(errors[0]); + } + } catch (error) { + createFlash({ message: error }); + } + }, + async addProject() { + try { + const { + data: { + ciJobTokenScopeAddProject: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: addProjectCIJobTokenScopeMutation, + variables: { + input: { + projectPath: this.fullPath, + targetProjectPath: this.targetProjectPath, + }, + }, + }); + + if (errors.length) { + throw new Error(errors[0]); + } + } catch (error) { + createFlash({ message: error }); + } finally { + this.clearTargetProjectPath(); + this.getProjects(); + } + }, + async removeProject(removeTargetPath) { + try { + const { + data: { + ciJobTokenScopeRemoveProject: { errors }, + }, + } = await this.$apollo.mutate({ + mutation: removeProjectCIJobTokenScopeMutation, + variables: { + input: { + projectPath: this.fullPath, + targetProjectPath: removeTargetPath, + }, + }, + }); + + if (errors.length) { + throw new Error(errors[0]); + } + } catch (error) { + createFlash({ message: error }); + } finally { + this.getProjects(); + } + }, + clearTargetProjectPath() { + this.targetProjectPath = ''; + }, + getProjects() { + this.$apollo.queries.projects.refetch(); + }, + }, +}; +</script> +<template> + <div> + <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" /> + <template v-else> + <gl-toggle + v-model="jobTokenScopeEnabled" + :label="$options.i18n.toggleLabelTitle" + :help="$options.i18n.toggleHelpText" + @change="updateCIJobTokenScope" + /> + <div v-if="jobTokenScopeEnabled" data-testid="token-section"> + <gl-card class="gl-mt-5"> + <template #header> + <h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5> + </template> + <template #default> + <gl-form-group :label="$options.i18n.formGroupLabel" label-for="token-project-search"> + <gl-form-input + id="token-project-search" + v-model="targetProjectPath" + :placeholder="$options.i18n.addProjectPlaceholder" + /> + </gl-form-group> + </template> + <template #footer> + <gl-button + variant="confirm" + :disabled="isProjectPathEmpty" + data-testid="add-project-button" + @click="addProject" + > + {{ $options.i18n.addProject }} + </gl-button> + <gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button> + </template> + </gl-card> + + <token-projects-table :projects="projects" @removeProject="removeProject" /> + </div> + </template> + </div> +</template> diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue new file mode 100644 index 00000000000..c89ac79abb9 --- /dev/null +++ b/app/assets/javascripts/token_access/components/token_projects_table.vue @@ -0,0 +1,81 @@ +<script> +import { GlButton, GlTable } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; + +const defaultTableClasses = { + thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!', +}; + +export default { + i18n: { + emptyText: s__('CI/CD|No projects have been added to the scope'), + }, + fields: [ + { + key: 'project', + label: __('Projects with access'), + tdClass: 'gl-p-5!', + ...defaultTableClasses, + columnClass: 'gl-w-85p', + }, + { + key: 'actions', + label: '', + tdClass: 'gl-p-5! gl-text-right', + ...defaultTableClasses, + columnClass: 'gl-w-15p', + }, + ], + components: { + GlButton, + GlTable, + }, + inject: { + fullPath: { + default: '', + }, + }, + props: { + projects: { + type: Array, + required: true, + }, + }, + methods: { + removeProject(project) { + this.$emit('removeProject', project); + }, + }, +}; +</script> +<template> + <gl-table + :items="projects" + :fields="$options.fields" + :tbody-tr-attr="{ 'data-testid': 'projects-token-table-row' }" + :empty-text="$options.i18n.emptyText" + show-empty + stacked="sm" + fixed + > + <template #table-colgroup="{ fields }"> + <col v-for="field in fields" :key="field.key" :class="field.columnClass" /> + </template> + + <template #cell(project)="{ item }"> + {{ item.name }} + </template> + + <template #cell(actions)="{ item }"> + <gl-button + v-if="item.fullPath !== fullPath" + category="primary" + variant="danger" + icon="remove" + :aria-label="__('Remove access')" + data-testid="remove-project-button" + @click="removeProject(item.fullPath)" + /> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql new file mode 100644 index 00000000000..0a7c76dd580 --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql @@ -0,0 +1,5 @@ +mutation addProjectCIJobTokenScope($input: CiJobTokenScopeAddProjectInput!) { + ciJobTokenScopeAddProject(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql new file mode 100644 index 00000000000..5107ea30cd1 --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql @@ -0,0 +1,5 @@ +mutation removeProjectCIJobTokenScope($input: CiJobTokenScopeRemoveProjectInput!) { + ciJobTokenScopeRemoveProject(input: $input) { + errors + } +} diff --git a/app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql new file mode 100644 index 00000000000..d99f2e3597d --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql @@ -0,0 +1,8 @@ +mutation updateCIJobTokenScope($input: CiCdSettingsUpdateInput!) { + ciCdSettingsUpdate(input: $input) { + ciCdSettings { + jobTokenScopeEnabled + } + errors + } +} diff --git a/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql new file mode 100644 index 00000000000..d4f559c3701 --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql @@ -0,0 +1,7 @@ +query getCIJobTokenScope($fullPath: ID!) { + project(fullPath: $fullPath) { + ciCdSettings { + jobTokenScopeEnabled + } + } +} diff --git a/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql new file mode 100644 index 00000000000..bec0710a1dd --- /dev/null +++ b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql @@ -0,0 +1,12 @@ +query getProjectsWithCIJobTokenScope($fullPath: ID!) { + project(fullPath: $fullPath) { + ciJobTokenScope { + projects { + nodes { + name + fullPath + } + } + } + } +} diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js new file mode 100644 index 00000000000..6a29883290a --- /dev/null +++ b/app/assets/javascripts/token_access/index.js @@ -0,0 +1,31 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; +import TokenAccess from './components/token_access.vue'; + +Vue.use(VueApollo); + +const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient(), +}); + +export const initTokenAccess = (containerId = 'js-ci-token-access-app') => { + const containerEl = document.getElementById(containerId); + + if (!containerEl) { + return false; + } + + const { fullPath } = containerEl.dataset; + + return new Vue({ + el: containerEl, + apolloProvider, + provide: { + fullPath, + }, + render(createElement) { + return createElement(TokenAccess); + }, + }); +}; diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss index c8f69bfdbaf..3885134e276 100644 --- a/app/assets/stylesheets/components/avatar.scss +++ b/app/assets/stylesheets/components/avatar.scss @@ -67,15 +67,12 @@ $avatar-sizes: ( ) ); -$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal, - $identicon-orange, $identicon-gray; - .avatar, .avatar-container { float: left; margin-right: $gl-padding; border-radius: $avatar-radius; - border: 1px solid $gray-normal; + border: 1px solid $t-gray-a-08; @each $size, $size-config in $avatar-sizes { &.s#{$size} { @@ -125,8 +122,8 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i .identicon { text-align: center; vertical-align: top; - color: $identicon-text-color; - background-color: $identicon-gray; + color: $gray-900; + background-color: $gray-50; // Sizes @each $size, $size-config in $avatar-sizes { @@ -143,9 +140,9 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i } // Background colors - @for $i from 1 through length($identicon-backgrounds) { + @for $i from 1 through length($gl-avatar-identicon-bgs) { &.bg#{$i} { - background-color: nth($identicon-backgrounds, $i); + background-color: nth($gl-avatar-identicon-bgs, $i); } } } diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss index d3976cfa8c7..726f8e28efe 100644 --- a/app/assets/stylesheets/framework/variables.scss +++ b/app/assets/stylesheets/framework/variables.scss @@ -634,18 +634,6 @@ $note-targe3-outside: #fffff0; $note-targe3-inside: #ffffd3; /* -* Identicon -*/ -$identicon-text-color: #525252 !default; -$identicon-red: #ffebee !default; -$identicon-purple: #f3e5f5 !default; -$identicon-indigo: #e8eaf6 !default; -$identicon-blue: #e3f2fd !default; -$identicon-teal: #e0f2f1 !default; -$identicon-orange: #fbe9e7 !default; -$identicon-gray: #eee !default; - -/* * Calendar */ $calendar-hover-bg: #ecf3fe; diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss index 766f185ced2..f4a76601913 100644 --- a/app/assets/stylesheets/startup/startup-dark.scss +++ b/app/assets/stylesheets/startup/startup-dark.scss @@ -2066,7 +2066,7 @@ svg.s16 { float: left; margin-right: 16px; border-radius: 50%; - border: 1px solid #333; + border: 1px solid rgba(255, 255, 255, 0.08); } .avatar.s16, .avatar-container.s16 { @@ -2108,8 +2108,8 @@ svg.s16 { .identicon { text-align: center; vertical-align: top; - color: #525252; - background-color: #eee; + color: #fafafa; + background-color: #303030; } .identicon.s16 { font-size: 10px; @@ -2124,25 +2124,25 @@ svg.s16 { line-height: 38px; } .identicon.bg1 { - background-color: #ffebee; + background-color: #660e00; } .identicon.bg2 { - background-color: #f3e5f5; + background-color: #f4f0ff; } .identicon.bg3 { - background-color: #e8eaf6; + background-color: #f1f1ff; } .identicon.bg4 { - background-color: #e3f2fd; + background-color: #033464; } .identicon.bg5 { - background-color: #e0f2f1; + background-color: #0a4020; } .identicon.bg6 { - background-color: #fbe9e7; + background-color: #5c2900; } .identicon.bg7 { - background-color: #eee; + background-color: #303030; } .avatar-container { overflow: hidden; diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss index 47d8b44230e..08175c2e99b 100644 --- a/app/assets/stylesheets/startup/startup-general.scss +++ b/app/assets/stylesheets/startup/startup-general.scss @@ -1934,7 +1934,7 @@ svg.s16 { float: left; margin-right: 16px; border-radius: 50%; - border: 1px solid #f5f5f5; + border: 1px solid rgba(0, 0, 0, 0.08); } .avatar.s16, .avatar-container.s16 { @@ -1976,8 +1976,8 @@ svg.s16 { .identicon { text-align: center; vertical-align: top; - color: #525252; - background-color: #eee; + color: #303030; + background-color: #f0f0f0; } .identicon.s16 { font-size: 10px; @@ -1992,25 +1992,25 @@ svg.s16 { line-height: 38px; } .identicon.bg1 { - background-color: #ffebee; + background-color: #fcf1ef; } .identicon.bg2 { - background-color: #f3e5f5; + background-color: #f4f0ff; } .identicon.bg3 { - background-color: #e8eaf6; + background-color: #f1f1ff; } .identicon.bg4 { - background-color: #e3f2fd; + background-color: #e9f3fc; } .identicon.bg5 { - background-color: #e0f2f1; + background-color: #ecf4ee; } .identicon.bg6 { - background-color: #fbe9e7; + background-color: #fdf1dd; } .identicon.bg7 { - background-color: #eee; + background-color: #f0f0f0; } .avatar-container { overflow: hidden; diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb index 3254d4129d3..960c0beb244 100644 --- a/app/controllers/projects/settings/ci_cd_controller.rb +++ b/app/controllers/projects/settings/ci_cd_controller.rb @@ -12,6 +12,7 @@ module Projects before_action :define_variables before_action do push_frontend_feature_flag(:ajax_new_deploy_token, @project) + push_frontend_feature_flag(:ci_scoped_job_token, @project, default_enabled: :yaml) end helper_method :highlight_badge diff --git a/app/views/admin/application_settings/_gitaly.html.haml b/app/views/admin/application_settings/_gitaly.html.haml index b28a53d8bf6..ade6dac606a 100644 --- a/app/views/admin/application_settings/_gitaly.html.haml +++ b/app/views/admin/application_settings/_gitaly.html.haml @@ -3,25 +3,19 @@ %fieldset .form-group - = f.label :gitaly_timeout_default, _('Default Timeout Period'), class: 'label-bold' + = f.label :gitaly_timeout_default, _('Default timeout'), class: 'label-bold' = f.number_field :gitaly_timeout_default, class: 'form-control gl-form-input' .form-text.text-muted - Timeout for Gitaly calls from the GitLab application (in seconds). This timeout is not enforced - for git fetch/push operations or Sidekiq jobs. - This timeout should be less than the worker timeout. If a Gitaly call timeout would exceed the - worker timeout, the remaining time from the worker timeout would be used to avoid having to terminate - the worker. + = _('Timeout for most Gitaly operations (in seconds).') .form-group - = f.label :gitaly_timeout_fast, _('Fast Timeout Period'), class: 'label-bold' + = f.label :gitaly_timeout_fast, _('Fast timeout'), class: 'label-bold' = f.number_field :gitaly_timeout_fast, class: 'form-control gl-form-input' .form-text.text-muted - Fast operation timeout (in seconds). Some Gitaly operations are expected to be fast. - If they exceed this threshold, there may be a problem with a storage shard and 'failing fast' - can help maintain the stability of the GitLab instance. + = _('Timeout for the fastest Gitaly operations (in seconds).') .form-group - = f.label :gitaly_timeout_medium, _('Medium Timeout Period'), class: 'label-bold' + = f.label :gitaly_timeout_medium, _('Medium timeout'), class: 'label-bold' = f.number_field :gitaly_timeout_medium, class: 'form-control gl-form-input' .form-text.text-muted - Medium operation timeout (in seconds). This should be a value between the Fast and the Default timeout. + = _('Timeout for moderately fast Gitaly operations (in seconds). Provide a value between Default timeout and Fast timeout.') = f.submit _('Save changes'), class: "gl-button btn btn-confirm" diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml index e307fc3a33a..fee2a98bf44 100644 --- a/app/views/admin/application_settings/_usage.html.haml +++ b/app/views/admin/application_settings/_usage.html.haml @@ -44,7 +44,7 @@ = link_to sprite_icon('question-o'), help_page_path('development/usage_ping/index.md', anchor: 'registration-features-program') .form-text.text-muted - if usage_ping_enabled - %p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service.') + %p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service.') - else %p.gl-mb-3.text-muted{ id: 'usage_ping_features_helper_text' }= _('To enable Registration Features, make sure "Enable service ping" is checked.') diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml index 17bf9ba84a2..bb584818f25 100644 --- a/app/views/admin/application_settings/preferences.html.haml +++ b/app/views/admin/application_settings/preferences.html.haml @@ -60,11 +60,13 @@ %section.settings.as-gitaly.no-animate#js-gitaly-settings{ class: ('expanded' if expanded_by_default?) } .settings-header %h4 - = _('Gitaly') + = _('Gitaly timeouts') %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } = expanded_by_default? ? _('Collapse') : _('Expand') %p = _('Configure Gitaly timeouts.') + %span + = link_to _('Learn more.'), help_page_path('user/admin_area/settings/gitaly_timeouts.md'), target: '_blank' .settings-content = render 'gitaly' diff --git a/app/views/ci/token_access/_index.html.haml b/app/views/ci/token_access/_index.html.haml new file mode 100644 index 00000000000..e6f21fc4ea4 --- /dev/null +++ b/app/views/ci/token_access/_index.html.haml @@ -0,0 +1 @@ +#js-ci-token-access-app{ data: { full_path: @project.full_path } } diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml index b9a8bddda91..15752287fc3 100644 --- a/app/views/projects/settings/ci_cd/show.html.haml +++ b/app/views/projects/settings/ci_cd/show.html.haml @@ -95,3 +95,16 @@ .settings-content = render 'ci/deploy_freeze/index' + +- if Feature.enabled?(:ci_scoped_job_token, @project, default_enabled: :yaml) + %section.settings.no-animate#js-token-access{ class: ('expanded' if expanded) } + .settings-header + %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only + = _("Token Access") + %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' } + = expanded ? _('Collapse') : _('Expand') + %p + = _("Control which projects can use the CI_JOB_TOKEN CI/CD variable for API access to this project. It is a security risk to disable this feature, because unauthorized projects may attempt to retrieve an active token and access the API.") + = link_to _('Learn more'), help_page_path('api/index', anchor: 'gitlab-cicd-job-token-scope'), target: '_blank', rel: 'noopener noreferrer' + .settings-content + = render 'ci/token_access/index' diff --git a/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml b/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml new file mode 100644 index 00000000000..66597f95e8c --- /dev/null +++ b/config/feature_flags/development/api_caching_rate_limit_repository_compare.yml @@ -0,0 +1,8 @@ +--- +name: api_caching_rate_limit_repository_compare +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64407 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334264 +milestone: '14.1' +type: development +group: group::source code +default_enabled: false diff --git a/doc/api/index.md b/doc/api/index.md index e4b7cc56aab..76d31c35b96 100644 --- a/doc/api/index.md +++ b/doc/api/index.md @@ -245,6 +245,68 @@ your [runners](../ci/runners/README.md) to be secure. Avoid: If you have an insecure GitLab Runner configuration, you increase the risk that someone tries to steal tokens from other jobs. +#### GitLab CI/CD job token scope + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/328553) in GitLab 14.1. + +- [Deployed behind a feature flag](../user/feature_flags.md), disabled by default. +- Disabled on GitLab.com. +- Not recommended for production use. +- To use in GitLab self-managed instances, ask a GitLab administrator to [enable it](#enable-or-disable-ci-job-token-scope). **(FREE SELF)** + +This in-development feature might not be available for your use. There can be +[risks when enabling features still in development](../user/feature_flags.md#risks-when-enabling-features-still-in-development). +Refer to this feature's version history for more details. + +CI job token can access only projects that are defined in its scope. +You can configure the scope via project settings. + +The CI job token scope consists in a allowlist of projects that are authorized by maintainers to be +accessible via a CI job token. By default a scope only contains the same project where the token +comes from. Other projects can be added and removed by maintainers. + +You can configure the scope via project settings. + +Since GitLab 14.1 this setting is enabled by default for new projects. Existing projects are +recommended to enable this feature and configure which projects are authorized to be accessed +by a job token. + +The CI job token scope limits the risks that a leaked token is used to access private data that +the user associated to the job can access to. + +When the job token scope feature is enabled in the project settings, only the projects in scope +will be allowed to be accessed by a job token. If the job token scope feature is disabled, any +projects can be accessed, as long as the user associated to the job has permissions. + +For example. If a project `A` has a running job with a `CI_JOB_TOKEN`, its scope is defined by +project `A`. If the job wants to use the `CI_JOB_TOKEN` to access data from project `B` or +trigger some actions in that project, then project `B` must be in the job token scope for `A`. + +A job token might give extra permissions that aren't necessary to access specific resources. +There is [a proposal](https://gitlab.com/groups/gitlab-org/-/epics/3559) to redesign the feature +for more strategic control of the access permissions. + +<!-- Add this at the end of the file --> + +#### Enable or disable CI Job Token Scope **(FREE SELF)** + +This is under development and not ready for production use. It is +deployed behind a feature flag that is **disabled by default**. +[GitLab administrators with access to the GitLab Rails console](../administration/feature_flags.md) +can enable it. + +To enable it: + +```ruby +Feature.enable(:ci_scoped_job_token) +``` + +To disable it: + +```ruby +Feature.disable(:ci_scoped_job_token) +``` + ### Impersonation tokens Impersonation tokens are a type of [personal access token](../user/profile/personal_access_tokens.md). diff --git a/doc/development/changelog.md b/doc/development/changelog.md index c9d2d820c54..c96fe2c18c1 100644 --- a/doc/development/changelog.md +++ b/doc/development/changelog.md @@ -98,6 +98,7 @@ EE: true database records created during Cycle Analytics model spec." - _Any_ contribution from a community member, no matter how small, **may** have a changelog entry regardless of these guidelines if the contributor wants one. +- [Removing](feature_flags/#changelog) a feature flag, when the new code is retained. ## Writing good changelog entries diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md index 9449fd52839..4fe3458b909 100644 --- a/doc/development/documentation/structure.md +++ b/doc/development/documentation/structure.md @@ -7,37 +7,40 @@ description: What to include in GitLab documentation pages. # Documentation topic types -At GitLab, we have not traditionally used topic types. However, we are starting to -move in this direction, and we now use four topic types: +At GitLab, we have not traditionally used types for our content. However, we are starting to +move in this direction, and we now use four primary topic types: - [Concept](#concept) - [Task](#task) - [Reference](#reference) - [Troubleshooting](#troubleshooting) -Each page contains multiple topic types. For example, -a page with the title `Pipelines`, which is generated from a file called `index.md`, -can include a concept and multiple task and reference topics. +In general, each page in our docset contains multiple topics. (Each heading indicates a new topic.) +Each topic on a page should be a specific topic type. For example, +a page with the title `Pipelines` can include topics that are concepts and tasks. + +A page might also contain only one type of information. These pages are generally one of our +[other content types](#other-types-of-content). ## Concept -A concept topic introduces a single feature or concept. +A concept introduces a single feature or concept. A concept should answer the questions: - What is this? - Why would I use it? -Think of everything someone might want to know if they've never heard of this topic before. +Think of everything someone might want to know if they've never heard of this concept before. Don't tell them **how** to do this thing. Tell them **what it is**. -If you start describing another topic, start a new concept and link to it. +If you start describing another concept, start a new concept and link to it. -Also, do not use "Overview" or "Introduction" for the topic title. Instead, +Also, do not use **Overview** or **Introduction** for the title. Instead, use a noun or phrase that someone would search for. -Concept topics should be in this format: +Concepts should be in this format: ```markdown # Title (a noun, like "Widgets") @@ -47,14 +50,14 @@ A paragraph that explains what this thing is. Another paragraph that explains what this thing is. Remember, if you start to describe about another concept, stop yourself. -Each concept topic should be about one concept only. +Each concept should be about one concept only. ``` ## Task -A task topic gives instructions for how to complete a procedure. +A task gives instructions for how to complete a procedure. -Task topics should be in this format: +Tasks should be in this format: ```markdown # Title (starts with an active verb, like "Create a widget" or "Delete a widget") @@ -89,20 +92,21 @@ Prerequisites: To create an issue: -1. Go to **Issues > List**. -1. In the top right, select **New issue**. -1. Complete the fields. (If you have a reference topic that lists each field, link to it here.) +1. On the top bar, select **Menu > Projects** and find your project. +1. On the left sidebar, select **Issues > List**. +1. In the top right corner, select **New issue**. +1. Complete the fields. (If you have reference content that lists each field, link to it here.) 1. Select **Create issue**. The issue is created. You can view it by going to **Issues > List**. ``` -If you have several tasks on a page that share prerequisites, you can make a -reference topic with the title **Prerequisites**, and link to it. +If you have several tasks on a page that share prerequisites, you can use the title +**Prerequisites**, and link to it. ## Reference -A reference topic provides information in an easily-scannable format, +Reference information should be in an easily-scannable format, like a table or list. It's similar to a dictionary or encyclopedia entry. ```markdown @@ -115,18 +119,18 @@ Introductory sentence. | **Name** | Descriptive sentence about the setting. | ``` -If a feature or concept has its own prerequisites, you can use the reference -topic type to create a **Prerequisites** header for the information. +If a feature or concept has its own prerequisites, you can use reference +content to create a **Prerequisites** header for the information. ## Troubleshooting -Troubleshooting topics can be one of two categories: +Troubleshooting can be one of two categories: -- **Troubleshooting task.** This topic is written the same as a [standard task topic](#task). +- **Troubleshooting task.** This information is written the same way as a [standard task](#task). For example, "Run debug tools" or "Verify syntax." -- **Troubleshooting reference.** This topic has a specific format. +- **Troubleshooting reference.** This information has a specific format. -Troubleshooting reference topics should be in this format: +Troubleshooting reference information should be in this format: ```markdown # Title (the error message or a description of it) @@ -138,104 +142,24 @@ This issue occurs when... The workaround is... ``` -For the topic title: +For the heading: -- Consider including at least a partial error message in the title. +- Consider including at least a partial error message. - Use fewer than 70 characters. -Remember to include the complete error message in the topics content if it is -not complete in the title. - -## Other information on a topic - -Topics include other information. - -For example: - -- Each topic must have a [tier badge](styleguide/index.md#product-tier-badges). -- New topics must have information about the - [GitLab version where the feature was introduced](styleguide/index.md#where-to-put-version-text). - -### Help and feedback section - -This section ([introduced](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/319) in GitLab 11.4) -is displayed at the end of each document and can be omitted by adding a key into -the front matter: - -```yaml ---- -feedback: false ---- -``` - -The default is to leave it there. If you want to omit it from a document, you -must check with a technical writer before doing so. - -#### Disqus - -We also have integrated the docs site with Disqus (introduced by -[!151](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/151)), -allowing our users to post comments. - -To omit only the comments from the feedback section, use the following key in -the front matter: - -```yaml ---- -comments: false ---- -``` - -We're hiding comments only in main index pages, such as [the main documentation index](../../index.md), -since its content is too broad to comment on. Before omitting Disqus, you must -check with a technical writer. - -Note that after adding `feedback: false` to the front matter, it will omit -Disqus, therefore, don't add both keys to the same document. - -The click events in the feedback section are tracked with Google Tag Manager. -The conversions can be viewed on Google Analytics by navigating to -**Behavior > Events > Top events > docs**. - -### Guidelines for good practices - -> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36576/) in GitLab 13.2 as GitLab Development documentation. - -*Good practice* examples demonstrate encouraged ways of writing code while -comparing with examples of practices to avoid. These examples are labeled as -*Bad* or *Good*. In GitLab development guidelines, when presenting the cases, -it's recommended to follow a *first-bad-then-good* strategy. First demonstrate -the *Bad* practice (how things *could* be done, which is often still working -code), and then how things *should* be done better, using a *Good* example. This -is typically an improved example of the same code. - -Consider the following guidelines when offering examples: - -- First, offer the *Bad* example, and then the *Good* one. -- When only one bad case and one good case is given, use the same code block. -- When more than one bad case or one good case is offered, use separated code - blocks for each. With many examples being presented, a clear separation helps - the reader to go directly to the good part. Consider offering an explanation - (for example, a comment, or a link to a resource) on why something is bad - practice. -- Better and best cases can be considered part of the good case(s) code block. - In the same code block, precede each with comments: `# Better` and `# Best`. - -Although the bad-then-good approach is acceptable for the GitLab development -guidelines, do not use it for user documentation. For user documentation, use -*Do* and *Don't*. For examples, see the [Pajamas Design System](https://design.gitlab.com/content/punctuation/). +If you do not put the full error in the title, include it in the body text. ## Other types of content There are other types of content in the GitLab documentation that don't -classify as one of the four [topic types](#documentation-topic-types). +classify as one of the four primary [topic types](#documentation-topic-types). These include: - [Tutorials](#tutorials) - [Get started pages](#get-started) - [Topics and resources pages](#topics-and-resources-pages) -In most cases, these content types are on their own standalone page. +In most cases, these pages are standalone. ### Tutorials @@ -315,10 +239,78 @@ or you have to create one, use this format: Brief sentence to describe the feature. -The following topics and resources can help you understand and work with this feature: +Refer to these resources for more information about <this feature>: - Link 1 - Link 2 - Link 3 +``` + +## Help and feedback section + +This section ([introduced](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/319) in GitLab 11.4) +is displayed at the end of each document and can be omitted by adding a key into +the front matter: + +```yaml +--- +feedback: false +--- +``` + +The default is to leave it there. If you want to omit it from a document, you +must check with a technical writer before doing so. + +### Disqus + +We also have integrated the docs site with Disqus (introduced by +[!151](https://gitlab.com/gitlab-org/gitlab-docs/-/merge_requests/151)), +allowing our users to post comments. + +To omit only the comments from the feedback section, use the following key in +the front matter: +```yaml +--- +comments: false +--- ``` + +We're hiding comments only in main index pages, such as [the main documentation index](../../index.md), +since its content is too broad to comment on. Before omitting Disqus, you must +check with a technical writer. + +Note that after adding `feedback: false` to the front matter, it will omit +Disqus, therefore, don't add both keys to the same document. + +The click events in the feedback section are tracked with Google Tag Manager. +The conversions can be viewed on Google Analytics by navigating to +**Behavior > Events > Top events > docs**. + +## Guidelines for good practices + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36576/) in GitLab 13.2 as GitLab Development documentation. + +*Good practice* examples demonstrate encouraged ways of writing code while +comparing with examples of practices to avoid. These examples are labeled as +*Bad* or *Good*. In GitLab development guidelines, when presenting the cases, +it's recommended to follow a *first-bad-then-good* strategy. First demonstrate +the *Bad* practice (how things *could* be done, which is often still working +code), and then how things *should* be done better, using a *Good* example. This +is typically an improved example of the same code. + +Consider the following guidelines when offering examples: + +- First, offer the *Bad* example, and then the *Good* one. +- When only one bad case and one good case is given, use the same code block. +- When more than one bad case or one good case is offered, use separated code + blocks for each. With many examples being presented, a clear separation helps + the reader to go directly to the good part. Consider offering an explanation + (for example, a comment, or a link to a resource) on why something is bad + practice. +- Better and best cases can be considered part of the good case(s) code block. + In the same code block, precede each with comments: `# Better` and `# Best`. + +Although the bad-then-good approach is acceptable for the GitLab development +guidelines, do not use it for user documentation. For user documentation, use +*Do* and *Don't*. For examples, see the [Pajamas Design System](https://design.gitlab.com/content/punctuation/). diff --git a/doc/development/feature_flags/index.md b/doc/development/feature_flags/index.md index ac5a7268c54..397153a0abb 100644 --- a/doc/development/feature_flags/index.md +++ b/doc/development/feature_flags/index.md @@ -21,6 +21,10 @@ All newly-introduced feature flags should be [disabled by default](https://about NOTE: This document is the subject of continued work as part of an epic to [improve internal usage of Feature Flags](https://gitlab.com/groups/gitlab-org/-/epics/3551). Raise any suggestions as new issues and attach them to the epic. +## When to use feature flags + +Moved to the ["When to use feature flags"](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags) section in the handbook. + ## Feature flags in GitLab development The following highlights should be considered when deciding if feature flags diff --git a/doc/development/index.md b/doc/development/index.md index bc996fdff21..e26dd6e105c 100644 --- a/doc/development/index.md +++ b/doc/development/index.md @@ -143,6 +143,13 @@ In these cases, use the following workflow: for final content review and merge. The Technical Writer may ask for additional approvals as previously suggested before merging the MR. +### Reviewer values + +> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57293) in GitLab 14.1. + +As a reviewer or as a reviewee, make sure to familiarize yourself with +the [reviewer values](https://about.gitlab.com/handbook/engineering/workflow/reviewer-values/) we strive for at GitLab. + ## UX and Frontend guides - [GitLab Design System](https://design.gitlab.com/), for building GitLab with diff --git a/doc/development/multi_version_compatibility.md b/doc/development/multi_version_compatibility.md index acdf8194cb1..8e49afda579 100644 --- a/doc/development/multi_version_compatibility.md +++ b/doc/development/multi_version_compatibility.md @@ -43,6 +43,10 @@ Is it ok if all GitLab nodes have been updated, but the post-deployment migratio Is it ok if all nodes have been updated, and then the post-deployment migrations get executed a couple days later, and then the background migrations take a week to finish? +### When upgrading a dependency like Rails + +Is it ok that some nodes have the new Rails version, but some nodes have the old Rails version? + ## A walkthrough of an update Backwards compatibility problems during updates are often very subtle. This is why it is worth familiarizing yourself with [update instructions](../update/index.md), [reference architectures](../administration/reference_architectures/index.md), and [GitLab.com's architecture](https://about.gitlab.com/handbook/engineering/infrastructure/production/architecture/). But to illustrate how these problems arise, take a look at this example of a simple update. @@ -102,6 +106,13 @@ Yes! We have specific instructions for [zero-downtime updates](../update/index.m ## I've identified a potential backwards compatibility problem, what can I do about it? +### Coordinate + +For major or minor version updates of Rails or Puma: + +- Engage the Quality team to thoroughly test the MR. +- Notify the `@gitlab-org/release/managers` on the MR prior to merging. + ### Feature flags One way to handle this is to use a feature flag that is disabled by diff --git a/doc/development/sql.md b/doc/development/sql.md index a98645cfcae..ddca88cb9bb 100644 --- a/doc/development/sql.md +++ b/doc/development/sql.md @@ -70,7 +70,7 @@ WHERE title ILIKE '%Draft:%'; Because the value for `ILIKE` starts with a wildcard the database is not able to use an index as it doesn't know where to start scanning the indexes. -Luckily, PostgreSQL _does_ provide a solution: trigram GIN indexes. These +Luckily, PostgreSQL _does_ provide a solution: trigram Generalized Inverted Index (GIN) indexes. These indexes can be created as follows: ```sql @@ -261,9 +261,9 @@ from `ActiveRecord::Base`. ## Use UNIONs -UNIONs aren't very commonly used in most Rails applications but they're very -powerful and useful. In most applications queries tend to use a lot of JOINs to -get related data or data based on certain criteria, but JOIN performance can +`UNION`s aren't very commonly used in most Rails applications but they're very +powerful and useful. Queries tend to use a lot of `JOIN`s to +get related data or data based on certain criteria, but `JOIN` performance can quickly deteriorate as the data involved grows. For example, if you want to get a list of projects where the name contains a @@ -279,7 +279,7 @@ OR namespaces.name ILIKE '%gitlab%'; ``` Using a large database this query can easily take around 800 milliseconds to -run. Using a UNION we'd write the following instead: +run. Using a `UNION` we'd write the following instead: ```sql SELECT projects.* @@ -301,7 +301,7 @@ This doesn't mean you should start using UNIONs everywhere, but it's something to keep in mind when using lots of JOINs in a query and filtering out records based on the joined data. -GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a UNION +GitLab comes with a `Gitlab::SQL::Union` class that can be used to build a `UNION` of multiple `ActiveRecord::Relation` objects. You can use this class as follows: diff --git a/doc/user/admin_area/analytics/dev_ops_report.md b/doc/user/admin_area/analytics/dev_ops_report.md index cb94bc8d35b..3c5c646f86c 100644 --- a/doc/user/admin_area/analytics/dev_ops_report.md +++ b/doc/user/admin_area/analytics/dev_ops_report.md @@ -39,25 +39,28 @@ collected before this feature is available. ## DevOps Adoption **(ULTIMATE SELF)** > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/247112) in GitLab 13.7 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta). -> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1. > - [Deployed behind a feature flag](../../../user/feature_flags.md), disabled by default. > - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59267) in GitLab 14.0. > - Enabled on GitLab.com. > - For GitLab self-managed instances, GitLab administrators can opt to [disable it](#disable-or-enable-devops-adoption). **(ULTIMATE SELF)** +> - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1. +> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1. DevOps Adoption shows you which groups within your organization are using the most essential features of GitLab: - Dev - - Issues - - Merge Requests - Approvals - Code owners + - Issues + - Merge requests - Sec + - DAST + - SAST - Scans - Ops - - Runners - - Pipelines - Deployments + - Pipelines + - Runners When managing groups in the UI, you can add your groups with the **Add group to table** button, in the top right hand section the page. diff --git a/doc/user/admin_area/settings/gitaly_timeouts.md b/doc/user/admin_area/settings/gitaly_timeouts.md index 6f488efee11..04887906c91 100644 --- a/doc/user/admin_area/settings/gitaly_timeouts.md +++ b/doc/user/admin_area/settings/gitaly_timeouts.md @@ -8,23 +8,20 @@ type: reference # Gitaly timeouts **(FREE SELF)** [Gitaly](../../../administration/gitaly/index.md) timeouts are configurable. The timeouts can be -configured to make sure that long running Gitaly calls don't needlessly take up resources. +configured to make sure that long-running Gitaly calls don't needlessly take up resources. To access Gitaly timeout settings: 1. On the top bar, select **Menu >** **{admin}** **Admin**. 1. On the left sidebar, select **Settings > Preferences**. -1. Expand the **Gitaly** section. +1. Expand the **Gitaly timeouts** section. ## Available timeouts -The following timeouts can be modified: +The following timeouts are available. -- **Default Timeout Period**. This timeout is the default for most Gitaly calls. It should be shorter than the - worker timeout that can be configured for [Puma](https://docs.gitlab.com/omnibus/settings/puma.html#puma-settings). - Used to make sure that Gitaly calls made within a web request cannot exceed the entire request timeout. - Defaults to 55 seconds. - -- **Fast Timeout Period**. This is the timeout for very short Gitaly calls. Defaults to 10 seconds. -- **Medium Timeout Period**. This timeout should be between the default and the fast timeout. - Defaults to 30 seconds. +| Timeout | Default | Description | +|:--------|:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Default | 55 seconds | Timeout for most Gitaly calls (not enforced for `git` `fetch` and `push` operations, or Sidekiq jobs). For example, checking if a repository exists on disk. Makes sure that Gitaly calls made within a web request cannot exceed the entire request timeout. It should be shorter than the worker timeout that can be configured for [Puma](https://docs.gitlab.com/omnibus/settings/puma.html#puma-settings). If a Gitaly call timeout exceeds the worker timeout, the remaining time from the worker timeout is used to avoid having to terminate the worker. | +| Fast | 10 seconds | Timeout for fast Gitaly operations used within requests, sometimes multiple times. For example, checking if a repository exists on disk. If fast operations exceed this threshold, there may be a problem with a storage shard. Failing fast can help maintain the stability of the GitLab instance. | +| Medium | 30 seconds | Timeout for Gitaly operations that should be fast (possibly within requests) but preferably not used multiple times within a request. For example, loading blobs. Timeout that should be set between Default and Fast. | diff --git a/doc/user/group/devops_adoption/index.md b/doc/user/group/devops_adoption/index.md index 6056af1cb80..e6c9a492c2c 100644 --- a/doc/user/group/devops_adoption/index.md +++ b/doc/user/group/devops_adoption/index.md @@ -9,6 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w > - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321083) in GitLab 13.11 as a [Beta feature](https://about.gitlab.com/handbook/product/gitlab-the-product/#beta). > - [Feature flag removed](https://gitlab.com/gitlab-org/gitlab/-/issues/333556) in GitLab 14.1. > - The Overview tab [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/330401) in GitLab 14.1. +> - DAST and SAST metrics [added](https://gitlab.com/gitlab-org/gitlab/-/issues/328033) in GitLab 14.1. Prerequisites: @@ -19,16 +20,18 @@ To access Group DevOps Adoption, go to your group and select **Analytics > DevOp Group DevOps Adoption shows you how individual groups and sub-groups within your organization use the following features: - Dev - - Issues - - Merge Requests - Approvals - Code owners + - Issues + - Merge requests - Sec + - DAST + - SAST - Scans - Ops - - Runners - - Pipelines - Deployments + - Pipelines + - Runners When managing groups in the UI, you can add your sub-groups with the **Add sub-group to table** button, in the top right hand section of your Groups pages. diff --git a/doc/user/markdown.md b/doc/user/markdown.md index db2d6846b7c..809eaf1b8fd 100644 --- a/doc/user/markdown.md +++ b/doc/user/markdown.md @@ -507,7 +507,8 @@ This example links to `<wiki_root>/miscellaneous.md`: GitLab Flavored Markdown renders GitLab-specific references. For example, you can reference an issue, a commit, a team member, or even an entire project team. GitLab Flavored Markdown turns -that reference into a link so you can navigate between them. +that reference into a link so you can navigate between them. All references to namespaces or +projects must use lower-case characters. Additionally, GitLab Flavored Markdown recognizes certain cross-project references and also has a shorthand version to reference other projects from the same namespace. diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb index a5234828de3..145863e3158 100644 --- a/lib/api/repositories.rb +++ b/lib/api/repositories.rb @@ -120,24 +120,28 @@ module API optional :straight, type: Boolean, desc: 'Comparison method, `true` for direct comparison between `from` and `to` (`from`..`to`), `false` to compare using merge base (`from`...`to`)', default: false end get ':id/repository/compare' do - if params[:from_project_id].present? - target_project = MergeRequestTargetProjectFinder - .new(current_user: current_user, source_project: user_project, project_feature: :repository) - .execute(include_routes: true).find_by_id(params[:from_project_id]) - - if target_project.blank? - render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400) + ff_enabled = Feature.enabled?(:api_caching_rate_limit_repository_compare, user_project, default_enabled: :yaml) + + cache_action_if(ff_enabled, [user_project, :repository_compare, current_user, declared_params], expires_in: 30.seconds) do + if params[:from_project_id].present? + target_project = MergeRequestTargetProjectFinder + .new(current_user: current_user, source_project: user_project, project_feature: :repository) + .execute(include_routes: true).find_by_id(params[:from_project_id]) + + if target_project.blank? + render_api_error!("Target project id:#{params[:from_project_id]} is not a fork of project id:#{params[:id]}", 400) + end + else + target_project = user_project end - else - target_project = user_project - end - compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight]) + compare = CompareService.new(user_project, params[:to]).execute(target_project, params[:from], straight: params[:straight]) - if compare - present compare, with: Entities::Compare - else - not_found!("Ref") + if compare + present compare, with: Entities::Compare + else + not_found!("Ref") + end end end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ddf2a37f450..4ed6f7c057a 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2050,6 +2050,9 @@ msgstr "" msgid "Add previously merged commits" msgstr "" +msgid "Add project" +msgstr "" + msgid "Add projects" msgstr "" @@ -5762,6 +5765,9 @@ msgstr "" msgid "CI/CD configuration file" msgstr "" +msgid "CI/CD|No projects have been added to the scope" +msgstr "" + msgid "CICDAnalytics|%{percent}%{percentSymbol}" msgstr "" @@ -5797,6 +5803,9 @@ msgstr "" msgid "CICD|Add a %{kubernetes_cluster_link_start}Kubernetes cluster integration%{link_end} with a domain, or create an AUTO_DEVOPS_PLATFORM_TARGET CI variable." msgstr "" +msgid "CICD|Add an existing project to the scope" +msgstr "" + msgid "CICD|Auto DevOps" msgstr "" @@ -5821,6 +5830,12 @@ msgstr "" msgid "CICD|Jobs" msgstr "" +msgid "CICD|Limit CI_JOB_TOKEN access" +msgstr "" + +msgid "CICD|Manage which projects can use this project's CI_JOB_TOKEN CI/CD variable for API access" +msgstr "" + msgid "CICD|The Auto DevOps pipeline runs by default in all projects with no CI/CD configuration file." msgstr "" @@ -8784,6 +8799,9 @@ msgstr "" msgid "Control whether to display third-party offers in GitLab." msgstr "" +msgid "Control which projects can use the CI_JOB_TOKEN CI/CD variable for API access to this project. It is a security risk to disable this feature, because unauthorized projects may attempt to retrieve an active token and access the API." +msgstr "" + msgid "Cookie domain" msgstr "" @@ -10314,9 +10332,6 @@ msgstr "" msgid "Default CI/CD configuration file" msgstr "" -msgid "Default Timeout Period" -msgstr "" - msgid "Default artifacts expiration" msgstr "" @@ -10353,6 +10368,9 @@ msgstr "" msgid "Default projects limit" msgstr "" +msgid "Default timeout" +msgstr "" + msgid "Default: Map a FogBugz account ID to a full name" msgstr "" @@ -11150,10 +11168,7 @@ msgstr "" msgid "DevopsAdoption|Are you sure that you would like to remove %{name} from the table?" msgstr "" -msgid "DevopsAdoption|At least one MR opened" -msgstr "" - -msgid "DevopsAdoption|At least one approval on an MR" +msgid "DevopsAdoption|At least one approval on a merge request" msgstr "" msgid "DevopsAdoption|At least one deploy" @@ -11162,6 +11177,9 @@ msgstr "" msgid "DevopsAdoption|At least one issue opened" msgstr "" +msgid "DevopsAdoption|At least one merge request opened" +msgstr "" + msgid "DevopsAdoption|At least one pipeline successfully run" msgstr "" @@ -11177,6 +11195,12 @@ msgstr "" msgid "DevopsAdoption|Confirm remove Group" msgstr "" +msgid "DevopsAdoption|DAST" +msgstr "" + +msgid "DevopsAdoption|DAST enabled for at least one project" +msgstr "" + msgid "DevopsAdoption|Deploys" msgstr "" @@ -11231,6 +11255,12 @@ msgstr "" msgid "DevopsAdoption|Runners" msgstr "" +msgid "DevopsAdoption|SAST" +msgstr "" + +msgid "DevopsAdoption|SAST enabled for at least one project" +msgstr "" + msgid "DevopsAdoption|Save changes" msgstr "" @@ -13563,7 +13593,7 @@ msgstr "" msgid "False positive" msgstr "" -msgid "Fast Timeout Period" +msgid "Fast timeout" msgstr "" msgid "Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged." @@ -15025,9 +15055,6 @@ msgstr "" msgid "GitLabPages|Your pages are served under:" msgstr "" -msgid "Gitaly" -msgstr "" - msgid "Gitaly Servers" msgstr "" @@ -15037,6 +15064,9 @@ msgstr "" msgid "Gitaly storage name:" msgstr "" +msgid "Gitaly timeouts" +msgstr "" + msgid "Gitaly|Address" msgstr "" @@ -20201,7 +20231,7 @@ msgstr "" msgid "Measured in bytes of code. Excludes generated and vendored code." msgstr "" -msgid "Medium Timeout Period" +msgid "Medium timeout" msgstr "" msgid "Medium vulnerabilities present" @@ -23612,6 +23642,9 @@ msgstr "" msgid "Paste issue link" msgstr "" +msgid "Paste project path (i.e. gitlab-org/gitlab)" +msgstr "" + msgid "Paste your public SSH key, which is usually contained in the file '~/.ssh/id_ed25519.pub' or '~/.ssh/id_rsa.pub' and begins with 'ssh-ed25519' or 'ssh-rsa'. Do not paste your private SSH key, as that can compromise your identity." msgstr "" @@ -25958,6 +25991,9 @@ msgstr "" msgid "Projects will be permanently deleted immediately." msgstr "" +msgid "Projects with access" +msgstr "" + msgid "Projects with critical vulnerabilities" msgstr "" @@ -27036,6 +27072,9 @@ msgstr "" msgid "Remove Zoom meeting" msgstr "" +msgid "Remove access" +msgstr "" + msgid "Remove all approvals in a merge request when new commits are pushed to its source branch." msgstr "" @@ -28492,6 +28531,9 @@ msgstr "" msgid "Search for a user" msgstr "" +msgid "Search for project" +msgstr "" + msgid "Search for projects, issues, etc." msgstr "" @@ -32808,9 +32850,15 @@ msgstr "" msgid "There was a problem fetching project users." msgstr "" +msgid "There was a problem fetching the job token scope value" +msgstr "" + msgid "There was a problem fetching the keep latest artifacts setting." msgstr "" +msgid "There was a problem fetching the projects" +msgstr "" + msgid "There was a problem fetching users." msgstr "" @@ -33894,6 +33942,15 @@ msgstr "" msgid "Timeout connecting to the Google API. Please try again." msgstr "" +msgid "Timeout for moderately fast Gitaly operations (in seconds). Provide a value between Default timeout and Fast timeout." +msgstr "" + +msgid "Timeout for most Gitaly operations (in seconds)." +msgstr "" + +msgid "Timeout for the fastest Gitaly operations (in seconds)." +msgstr "" + msgid "Timezone" msgstr "" @@ -34165,6 +34222,9 @@ msgstr "" msgid "Token" msgstr "" +msgid "Token Access" +msgstr "" + msgid "Token name" msgstr "" @@ -37179,7 +37239,7 @@ msgstr "" msgid "You can easily contribute to them by requesting to join these groups." msgstr "" -msgid "You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in future, you will also need to register with GitLab via a new cloud licensing service." +msgid "You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service." msgstr "" msgid "You can enable project access token creation in %{link_start}group settings%{link_end}." diff --git a/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb b/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb index fd161cdb4a6..734ff160937 100644 --- a/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb @@ -94,7 +94,7 @@ module QA end def run_jenkins_server - Service::DockerRun::Jenkins.init do |runner| + Service::DockerRun::Jenkins.new.tap do |runner| runner.pull runner.register! end diff --git a/spec/frontend/runner/components/runner_update_form_spec.js b/spec/frontend/runner/components/runner_update_form_spec.js index dad041072ce..15029d7a911 100644 --- a/spec/frontend/runner/components/runner_update_form_spec.js +++ b/spec/frontend/runner/components/runner_update_form_spec.js @@ -207,13 +207,11 @@ describe('RunnerUpdateForm', () => { }); it.each` - value | submitted - ${''} | ${{ tagList: [] }} - ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }} - ${'with spaces'} | ${{ tagList: ['with spaces'] }} - ${',,,,, commas'} | ${{ tagList: ['commas'] }} - ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }} - ${' trimmed , trimmed2 '} | ${{ tagList: ['trimmed', 'trimmed2'] }} + value | submitted + ${''} | ${{ tagList: [] }} + ${'tag1, tag2'} | ${{ tagList: ['tag1', 'tag2'] }} + ${'with spaces'} | ${{ tagList: ['with spaces'] }} + ${'more ,,,,, commas'} | ${{ tagList: ['more', 'commas'] }} `('Field updates runner\'s tags for "$value"', async ({ value, submitted }) => { const runner = { ...mockRunner, tagList: ['tag1'] }; createComponent({ props: { runner } }); diff --git a/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js b/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js new file mode 100644 index 00000000000..510b4e604ac --- /dev/null +++ b/spec/frontend/runner/runner_detail/runner_update_form_utils_spec.js @@ -0,0 +1,96 @@ +import { ACCESS_LEVEL_NOT_PROTECTED } from '~/runner/constants'; +import { + modelToUpdateMutationVariables, + runnerToModel, +} from '~/runner/runner_details/runner_update_form_utils'; + +const mockId = 'gid://gitlab/Ci::Runner/1'; +const mockDescription = 'Runner Desc.'; + +const mockRunner = { + id: mockId, + description: mockDescription, + maximumTimeout: 100, + accessLevel: ACCESS_LEVEL_NOT_PROTECTED, + active: true, + locked: true, + runUntagged: true, + tagList: ['tag-1', 'tag-2'], +}; + +const mockModel = { + ...mockRunner, + tagList: 'tag-1, tag-2', +}; + +describe('~/runner/runner_details/runner_update_form_utils', () => { + describe('runnerToModel', () => { + it('collects all model data', () => { + expect(runnerToModel(mockRunner)).toEqual(mockModel); + }); + + it('does not collect other data', () => { + const model = runnerToModel({ + ...mockRunner, + unrelated: 'unrelatedValue', + }); + + expect(model.unrelated).toEqual(undefined); + }); + + it('tag list defaults to an empty string', () => { + const model = runnerToModel({ + ...mockRunner, + tagList: undefined, + }); + + expect(model.tagList).toEqual(''); + }); + }); + + describe('modelToUpdateMutationVariables', () => { + it('collects all model data', () => { + expect(modelToUpdateMutationVariables(mockModel)).toEqual({ + input: { + ...mockRunner, + }, + }); + }); + + it('collects a nullable timeout from the model', () => { + const variables = modelToUpdateMutationVariables({ + ...mockModel, + maximumTimeout: '', + }); + + expect(variables).toEqual({ + input: { + ...mockRunner, + maximumTimeout: null, + }, + }); + }); + + it.each` + tagList | tagListInput + ${''} | ${[]} + ${'tag1, tag2'} | ${['tag1', 'tag2']} + ${'with spaces'} | ${['with spaces']} + ${',,,,, commas'} | ${['commas']} + ${'more ,,,,, commas'} | ${['more', 'commas']} + ${' trimmed , trimmed2 '} | ${['trimmed', 'trimmed2']} + `('collect tags separated by commas for "$value"', ({ tagList, tagListInput }) => { + const variables = modelToUpdateMutationVariables({ + ...mockModel, + tagList, + }); + + expect(variables).toEqual({ + input: { + ...mockRunner, + tagList: tagListInput, + }, + }); + }); + }); +}); diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js new file mode 100644 index 00000000000..14d7b00cb6d --- /dev/null +++ b/spec/frontend/token_access/mock_data.js @@ -0,0 +1,84 @@ +export const enabledJobTokenScope = { + data: { + project: { + ciCdSettings: { + jobTokenScopeEnabled: true, + __typename: 'ProjectCiCdSetting', + }, + __typename: 'Project', + }, + }, +}; + +export const disabledJobTokenScope = { + data: { + project: { + ciCdSettings: { + jobTokenScopeEnabled: false, + __typename: 'ProjectCiCdSetting', + }, + __typename: 'Project', + }, + }, +}; + +export const updateJobTokenScope = { + data: { + ciCdSettingsUpdate: { + ciCdSettings: { + jobTokenScopeEnabled: true, + __typename: 'ProjectCiCdSetting', + }, + errors: [], + __typename: 'CiCdSettingsUpdatePayload', + }, + }, +}; + +export const projectsWithScope = { + data: { + project: { + __typename: 'Project', + ciJobTokenScope: { + __typename: 'CiJobTokenScopeType', + projects: { + __typename: 'ProjectConnection', + nodes: [ + { + fullPath: 'root/332268-test', + name: 'root/332268-test', + }, + ], + }, + }, + }, + }, +}; + +export const addProjectSuccess = { + data: { + ciJobTokenScopeAddProject: { + errors: [], + __typename: 'CiJobTokenScopeAddProjectPayload', + }, + }, +}; + +export const removeProjectSuccess = { + data: { + ciJobTokenScopeRemoveProject: { + errors: [], + __typename: 'CiJobTokenScopeRemoveProjectPayload', + }, + }, +}; + +export const mockProjects = [ + { + name: 'merge-train-stuff', + fullPath: 'root/merge-train-stuff', + isLocked: false, + __typename: 'Project', + }, + { name: 'ci-project', fullPath: 'root/ci-project', isLocked: true, __typename: 'Project' }, +]; diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/token_access_spec.js new file mode 100644 index 00000000000..244a7f00cb9 --- /dev/null +++ b/spec/frontend/token_access/token_access_spec.js @@ -0,0 +1,213 @@ +import { GlToggle, GlLoadingIcon } from '@gitlab/ui'; +import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import TokenAccess from '~/token_access/components/token_access.vue'; +import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql'; +import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql'; +import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql'; +import getCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_ci_job_token_scope.query.graphql'; +import getProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql'; +import { + enabledJobTokenScope, + disabledJobTokenScope, + updateJobTokenScope, + projectsWithScope, + addProjectSuccess, + removeProjectSuccess, +} from './mock_data'; + +const projectPath = 'root/my-repo'; +const error = new Error('Error'); +const localVue = createLocalVue(); + +localVue.use(VueApollo); + +jest.mock('~/flash'); + +describe('TokenAccess component', () => { + let wrapper; + + const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope); + const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope); + const updateJobTokenScopeHandler = jest.fn().mockResolvedValue(updateJobTokenScope); + const getProjectsWithScope = jest.fn().mockResolvedValue(projectsWithScope); + const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess); + const addProjectFailureHandler = jest.fn().mockRejectedValue(error); + const removeProjectSuccessHandler = jest.fn().mockResolvedValue(removeProjectSuccess); + const removeProjectFailureHandler = jest.fn().mockRejectedValue(error); + + const findToggle = () => wrapper.findComponent(GlToggle); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findAddProjectBtn = () => wrapper.find('[data-testid="add-project-button"]'); + const findRemoveProjectBtn = () => wrapper.find('[data-testid="remove-project-button"]'); + const findTokenSection = () => wrapper.find('[data-testid="token-section"]'); + + const createMockApolloProvider = (requestHandlers) => { + return createMockApollo(requestHandlers); + }; + + const createComponent = (requestHandlers, mountFn = shallowMount) => { + wrapper = mountFn(TokenAccess, { + localVue, + provide: { + fullPath: projectPath, + }, + apolloProvider: createMockApolloProvider(requestHandlers), + data() { + return { + targetProjectPath: 'root/test', + }; + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('loading state', () => { + it('shows loading state while waiting on query to resolve', async () => { + createComponent([ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + ]); + + expect(findLoadingIcon().exists()).toBe(true); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + }); + }); + + describe('toggle', () => { + it('the toggle should be enabled and the token section should show', async () => { + createComponent([ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + ]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(true); + expect(findTokenSection().exists()).toBe(true); + }); + + it('the toggle should be disabled and the token section should not show', async () => { + createComponent([ + [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + ]); + + await waitForPromises(); + + expect(findToggle().props('value')).toBe(false); + expect(findTokenSection().exists()).toBe(false); + }); + + it('switching the toggle calls the mutation', async () => { + createComponent([ + [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler], + [updateCIJobTokenScopeMutation, updateJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + ]); + + await waitForPromises(); + + findToggle().vm.$emit('change', true); + + expect(updateJobTokenScopeHandler).toHaveBeenCalledWith({ + input: { fullPath: projectPath, jobTokenScopeEnabled: true }, + }); + }); + }); + + describe('add project', () => { + it('calls add project mutation', async () => { + createComponent( + [ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + [addProjectCIJobTokenScopeMutation, addProjectSuccessHandler], + ], + mount, + ); + + await waitForPromises(); + + findAddProjectBtn().trigger('click'); + + expect(addProjectSuccessHandler).toHaveBeenCalledWith({ + input: { + projectPath, + targetProjectPath: 'root/test', + }, + }); + }); + + it('add project handles error correctly', async () => { + createComponent( + [ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + [addProjectCIJobTokenScopeMutation, addProjectFailureHandler], + ], + mount, + ); + + await waitForPromises(); + + findAddProjectBtn().trigger('click'); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalled(); + }); + }); + + describe('remove project', () => { + it('calls remove project mutation', async () => { + createComponent( + [ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + [removeProjectCIJobTokenScopeMutation, removeProjectSuccessHandler], + ], + mount, + ); + + await waitForPromises(); + + findRemoveProjectBtn().trigger('click'); + + expect(removeProjectSuccessHandler).toHaveBeenCalledWith({ + input: { + projectPath, + targetProjectPath: 'root/332268-test', + }, + }); + }); + + it('remove project handles error correctly', async () => { + createComponent( + [ + [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler], + [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScope], + [removeProjectCIJobTokenScopeMutation, removeProjectFailureHandler], + ], + mount, + ); + + await waitForPromises(); + + findRemoveProjectBtn().trigger('click'); + + await waitForPromises(); + + expect(createFlash).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js new file mode 100644 index 00000000000..3bda0d0b530 --- /dev/null +++ b/spec/frontend/token_access/token_projects_table_spec.js @@ -0,0 +1,51 @@ +import { GlTable, GlButton } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import TokenProjectsTable from '~/token_access/components/token_projects_table.vue'; +import { mockProjects } from './mock_data'; + +describe('Token projects table', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount(TokenProjectsTable, { + provide: { + fullPath: 'root/ci-project', + }, + propsData: { + projects: mockProjects, + }, + }); + }; + + const findTable = () => wrapper.findComponent(GlTable); + const findAllTableRows = () => wrapper.findAll('[data-testid="projects-token-table-row"]'); + const findDeleteProjectBtn = () => wrapper.findComponent(GlButton); + const findAllDeleteProjectBtn = () => wrapper.findAllComponents(GlButton); + + beforeEach(() => { + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('displays a table', () => { + expect(findTable().exists()).toBe(true); + }); + + it('displays the correct amount of table rows', () => { + expect(findAllTableRows()).toHaveLength(mockProjects.length); + }); + + it('delete project button emits event with correct project to delete', async () => { + await findDeleteProjectBtn().trigger('click'); + + expect(wrapper.emitted('removeProject')).toEqual([[mockProjects[0].fullPath]]); + }); + + it('does not show the remove icon if the project is locked', () => { + // currently two mock projects with one being a locked project + expect(findAllDeleteProjectBtn()).toHaveLength(1); + }); +}); diff --git a/spec/requests/api/repositories_spec.rb b/spec/requests/api/repositories_spec.rb index 1b96efeca22..d019e89e0b4 100644 --- a/spec/requests/api/repositories_spec.rb +++ b/spec/requests/api/repositories_spec.rb @@ -477,6 +477,17 @@ RSpec.describe API::Repositories do let(:request) { get api(route, guest) } end end + + context 'api_caching_rate_limit_repository_compare is disabled' do + before do + stub_feature_flags(api_caching_rate_limit_repository_compare: false) + end + + it_behaves_like 'repository compare' do + let(:project) { create(:project, :public, :repository) } + let(:current_user) { nil } + end + end end describe 'GET /projects/:id/repository/contributors' do |