diff options
Diffstat (limited to 'app/assets/javascripts/token_access')
8 files changed, 355 insertions, 0 deletions
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..24565c441d8 --- /dev/null +++ b/app/assets/javascripts/token_access/components/token_access.vue @@ -0,0 +1,206 @@ +<script> +import { GlButton, GlCard, 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|Select projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable.`, + ), + cardHeaderTitle: s__('CICD|Add an existing project to the scope'), + 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, + 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 }); + } finally { + if (this.jobTokenScopeEnabled) { + this.getProjects(); + } + } + }, + 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-input + v-model="targetProjectPath" + :placeholder="$options.i18n.addProjectPlaceholder" + /> + </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..777eda1c4d7 --- /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 that can be accessed'), + 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); + }, + }); +}; |