diff options
Diffstat (limited to 'app')
14 files changed, 429 insertions, 252 deletions
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue index 7bc1eb5d652..4a2da487e9b 100644 --- a/app/assets/javascripts/deploy_keys/components/action_btn.vue +++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue @@ -1,6 +1,5 @@ <script> import { GlButton } from '@gitlab/ui'; -import eventHub from '../eventhub'; export default { components: { @@ -11,10 +10,6 @@ export default { type: Object, required: true, }, - type: { - type: String, - required: true, - }, category: { type: String, required: false, @@ -30,6 +25,10 @@ export default { required: false, default: '', }, + mutation: { + type: Object, + required: true, + }, }, data() { return { @@ -39,10 +38,15 @@ export default { methods: { doAction() { this.isLoading = true; - - eventHub.$emit(`${this.type}.key`, this.deployKey, () => { - this.isLoading = false; - }); + this.$apollo + .mutate({ + mutation: this.mutation, + variables: { id: this.deployKey.id }, + }) + .catch((error) => this.$emit('error', error)) + .finally(() => { + this.isLoading = false; + }); }, }, }; @@ -50,6 +54,7 @@ export default { <template> <gl-button + v-bind="$attrs" :category="category" :variant="variant" :icon="icon" diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue index ec17bbea48f..7168a209b52 100644 --- a/app/assets/javascripts/deploy_keys/components/app.vue +++ b/app/assets/javascripts/deploy_keys/components/app.vue @@ -1,11 +1,18 @@ <script> -import { GlButton, GlIcon, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlIcon, GlLoadingIcon, GlPagination } from '@gitlab/ui'; import { createAlert } from '~/alert'; -import { s__ } from '~/locale'; +import { s__, __, sprintf } from '~/locale'; +import { captureException } from '~/sentry/sentry_browser_wrapper'; +import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql'; import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue'; -import eventHub from '../eventhub'; -import DeployKeysService from '../service'; -import DeployKeysStore from '../store'; +import deployKeysQuery from '../graphql/queries/deploy_keys.query.graphql'; +import currentPageQuery from '../graphql/queries/current_page.query.graphql'; +import currentScopeQuery from '../graphql/queries/current_scope.query.graphql'; +import confirmRemoveKeyQuery from '../graphql/queries/confirm_remove_key.query.graphql'; +import updateCurrentScopeMutation from '../graphql/mutations/update_current_scope.mutation.graphql'; +import updateCurrentPageMutation from '../graphql/mutations/update_current_page.mutation.graphql'; +import confirmDisableMutation from '../graphql/mutations/confirm_action.mutation.graphql'; +import disableKeyMutation from '../graphql/mutations/disable_key.mutation.graphql'; import ConfirmModal from './confirm_modal.vue'; import KeysPanel from './keys_panel.vue'; @@ -17,120 +24,147 @@ export default { GlButton, GlIcon, GlLoadingIcon, + GlPagination, }, props: { - endpoint: { + projectId: { type: String, required: true, }, - projectId: { + projectPath: { type: String, required: true, }, }, + apollo: { + deployKeys: { + query: deployKeysQuery, + variables() { + return { + projectPath: this.projectPath, + scope: this.currentScope, + page: this.currentPage, + }; + }, + update(data) { + return data?.project?.deployKeys || []; + }, + error(error) { + createAlert({ + message: s__('DeployKeys|Error getting deploy keys'), + captureError: true, + error, + }); + }, + }, + pageInfo: { + query: pageInfoQuery, + variables() { + return { input: { page: this.currentPage, scope: this.currentScope } }; + }, + update({ pageInfo }) { + return pageInfo || {}; + }, + }, + currentPage: { + query: currentPageQuery, + }, + currentScope: { + query: currentScopeQuery, + }, + deployKeyToRemove: { + query: confirmRemoveKeyQuery, + }, + }, data() { return { - currentTab: 'enabled_keys', - isLoading: false, - store: new DeployKeysStore(), - removeKey: () => {}, - cancel: () => {}, - confirmModalVisible: false, + deployKeys: [], + pageInfo: {}, + deployKeyToRemove: null, }; }, scopes: { - enabled_keys: s__('DeployKeys|Enabled deploy keys'), - available_project_keys: s__('DeployKeys|Privately accessible deploy keys'), - public_keys: s__('DeployKeys|Publicly accessible deploy keys'), + enabledKeys: s__('DeployKeys|Enabled deploy keys'), + availableProjectKeys: s__('DeployKeys|Privately accessible deploy keys'), + availablePublicKeys: s__('DeployKeys|Publicly accessible deploy keys'), }, i18n: { loading: s__('DeployKeys|Loading deploy keys'), addButton: s__('DeployKeys|Add new key'), + prevPage: __('Go to previous page'), + nextPage: __('Go to next page'), + next: __('Next'), + prev: __('Prev'), + goto: (page) => sprintf(__('Go to page %{page}'), { page }), }, computed: { tabs() { - return Object.keys(this.$options.scopes).map((scope) => { - const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null; - + return Object.entries(this.$options.scopes).map(([scope, name]) => { return { - name: this.$options.scopes[scope], + name, scope, - isActive: scope === this.currentTab, - count, + isActive: scope === this.currentScope, }; }); }, - hasKeys() { - return Object.keys(this.keys).length; - }, - keys() { - return this.store.keys; + confirmModalVisible() { + return Boolean(this.deployKeyToRemove); }, }, - created() { - this.service = new DeployKeysService(this.endpoint); - - eventHub.$on('enable.key', this.enableKey); - eventHub.$on('remove.key', this.confirmRemoveKey); - eventHub.$on('disable.key', this.confirmRemoveKey); - }, - mounted() { - this.fetchKeys(); - }, - beforeDestroy() { - eventHub.$off('enable.key', this.enableKey); - eventHub.$off('remove.key', this.confirmRemoveKey); - eventHub.$off('disable.key', this.confirmRemoveKey); - }, methods: { - onChangeTab(tab) { - this.currentTab = tab; + onChangeTab(scope) { + return this.$apollo + .mutate({ + mutation: updateCurrentScopeMutation, + variables: { scope }, + }) + .then(() => { + this.$apollo.queries.deployKeys.refetch(); + }) + .catch((error) => { + captureException(error, { + tags: { + deployKeyScope: scope, + }, + }); + }); }, - fetchKeys() { - this.isLoading = true; - - return this.service - .getKeys() - .then((data) => { - this.isLoading = false; - this.store.keys = data; + moveNext() { + return this.movePage('next'); + }, + movePrevious() { + return this.movePage('previous'); + }, + movePage(direction) { + return this.moveToPage(this.pageInfo[`${direction}Page`]); + }, + moveToPage(page) { + return this.$apollo.mutate({ mutation: updateCurrentPageMutation, variables: { page } }); + }, + removeKey() { + this.$apollo + .mutate({ + mutation: disableKeyMutation, + variables: { id: this.deployKeyToRemove.id }, + }) + .then(() => { + if (!this.deployKeys.length) { + return this.movePage('previous'); + } + return null; }) + .then(() => this.$apollo.queries.deployKeys.refetch()) .catch(() => { - this.isLoading = false; - this.store.keys = {}; - return createAlert({ - message: s__('DeployKeys|Error getting deploy keys'), + createAlert({ + message: s__('DeployKeys|Error removing deploy key'), }); }); }, - enableKey(deployKey) { - this.service - .enableKey(deployKey.id) - .then(this.fetchKeys) - .catch(() => - createAlert({ - message: s__('DeployKeys|Error enabling deploy key'), - }), - ); - }, - confirmRemoveKey(deployKey, callback) { - const hideModal = () => { - this.confirmModalVisible = false; - callback?.(); - }; - this.removeKey = () => { - this.service - .disableKey(deployKey.id) - .then(this.fetchKeys) - .then(hideModal) - .catch(() => - createAlert({ - message: s__('DeployKeys|Error removing deploy key'), - }), - ); - }; - this.cancel = hideModal; - this.confirmModalVisible = true; + cancel() { + this.$apollo.mutate({ + mutation: confirmDisableMutation, + variables: { id: null }, + }); }, }, }; @@ -139,47 +173,59 @@ export default { <template> <div class="deploy-keys"> <confirm-modal :visible="confirmModalVisible" @remove="removeKey" @cancel="cancel" /> + <div class="gl-new-card-header gl-align-items-center gl-py-0 gl-pl-0"> + <div class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-b-0"> + <div class="fade-left"> + <gl-icon name="chevron-lg-left" :size="12" /> + </div> + <div class="fade-right"> + <gl-icon name="chevron-lg-right" :size="12" /> + </div> + + <navigation-tabs + :tabs="tabs" + scope="deployKeys" + class="gl-rounded-lg" + @onChangeTab="onChangeTab" + /> + </div> + + <div class="gl-new-card-actions"> + <gl-button + size="small" + class="js-toggle-button js-toggle-content" + data-testid="add-new-deploy-key-button" + > + {{ $options.i18n.addButton }} + </gl-button> + </div> + </div> <gl-loading-icon - v-if="isLoading && !hasKeys" + v-if="$apollo.queries.deployKeys.loading" :label="$options.i18n.loading" - size="sm" + size="md" class="gl-m-5" /> - <template v-else-if="hasKeys"> - <div class="gl-new-card-header gl-align-items-center gl-pt-0 gl-pb-0 gl-pl-0"> - <div class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-b-0"> - <div class="fade-left"> - <gl-icon name="chevron-lg-left" :size="12" /> - </div> - <div class="fade-right"> - <gl-icon name="chevron-lg-right" :size="12" /> - </div> - - <navigation-tabs - :tabs="tabs" - scope="deployKeys" - class="gl-rounded-lg" - @onChangeTab="onChangeTab" - /> - </div> - - <div class="gl-new-card-actions"> - <gl-button - size="small" - class="js-toggle-button js-toggle-content" - data-testid="add-new-deploy-key-button" - > - {{ $options.i18n.addButton }} - </gl-button> - </div> - </div> + <template v-else> <keys-panel :project-id="projectId" - :keys="keys[currentTab]" - :store="store" - :endpoint="endpoint" + :keys="deployKeys" data-testid="project-deploy-keys-container" /> + <gl-pagination + align="center" + :total-items="pageInfo.total" + :per-page="pageInfo.perPage" + :value="currentPage" + :next="$options.i18n.next" + :prev="$options.i18n.prev" + :label-previous-page="$options.i18n.prevPage" + :label-next-page="$options.i18n.nextPage" + :label-page="$options.i18n.goto" + @next="moveNext()" + @previous="movePrevious()" + @input="moveToPage" + /> </template> </div> </template> diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue index 16c745d8cff..d4b140f1adb 100644 --- a/app/assets/javascripts/deploy_keys/components/key.vue +++ b/app/assets/javascripts/deploy_keys/components/key.vue @@ -2,8 +2,12 @@ <script> import { GlBadge, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui'; import { head, tail } from 'lodash'; +import { createAlert } from '~/alert'; import { s__, sprintf } from '~/locale'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import currentScopeQuery from '../graphql/queries/current_scope.query.graphql'; +import enableKeyMutation from '../graphql/mutations/enable_key.mutation.graphql'; +import confirmDisableMutation from '../graphql/mutations/confirm_action.mutation.graphql'; import ActionBtn from './action_btn.vue'; @@ -23,48 +27,25 @@ export default { type: Object, required: true, }, - store: { - type: Object, - required: true, - }, - endpoint: { - type: String, - required: true, - }, projectId: { type: String, required: false, default: null, }, }, + apollo: { + currentScope: { + query: currentScopeQuery, + }, + }, data() { return { projectsExpanded: false, }; }, computed: { - editDeployKeyPath() { - return `${this.endpoint}/${this.deployKey.id}/edit`; - }, projects() { - const projects = [...this.deployKey.deploy_keys_projects]; - - if (this.projectId !== null) { - const indexOfCurrentProject = projects.findIndex( - (project) => - project && - project.project && - project.project.id && - project.project.id.toString() === this.projectId, - ); - - if (indexOfCurrentProject > -1) { - const currentProject = projects.splice(indexOfCurrentProject, 1); - currentProject[0].project.full_name = s__('DeployKeys|Current project'); - return currentProject.concat(projects); - } - } - return projects; + return this.deployKey.deployKeysProjects; }, firstProject() { return head(this.projects); @@ -81,13 +62,11 @@ export default { return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length }); }, isEnabled() { - return this.store.isEnabled(this.deployKey.id); + return this.currentScope === 'enabledKeys'; }, isRemovable() { return ( - this.store.isEnabled(this.deployKey.id) && - this.deployKey.destroyed_when_orphaned && - this.deployKey.almost_orphaned + this.isEnabled && this.deployKey.destroyedWhenOrphaned && this.deployKey.almostOrphaned ); }, isExpandable() { @@ -99,14 +78,37 @@ export default { }, methods: { projectTooltipTitle(project) { - return project.can_push + return project.canPush ? s__('DeployKeys|Grant write permissions to this key') : s__('DeployKeys|Read access only'); }, toggleExpanded() { this.projectsExpanded = !this.projectsExpanded; }, + isCurrentProject({ project } = {}) { + if (this.projectId !== null) { + return Boolean(project?.id?.toString() === this.projectId); + } + + return false; + }, + projectName(project) { + if (this.isCurrentProject(project)) { + return s__('DeployKeys|Current project'); + } + + return project?.project?.fullName; + }, + onEnableError(error) { + createAlert({ + message: s__('DeployKeys|Error enabling deploy key'), + captureError: true, + error, + }); + }, }, + enableKeyMutation, + confirmDisableMutation, }; </script> @@ -128,7 +130,7 @@ export default { <dl class="gl-font-sm gl-mb-0"> <dt>{{ __('SHA256') }}</dt> <dd class="fingerprint" data-testid="key-sha256-fingerprint-content"> - {{ deployKey.fingerprint_sha256 }} + {{ deployKey.fingerprintSha256 }} </dd> <template v-if="deployKey.fingerprint"> <dt> @@ -150,10 +152,10 @@ export default { <gl-badge v-gl-tooltip :title="projectTooltipTitle(firstProject)" - :icon="firstProject.can_push ? 'lock-open' : 'lock'" + :icon="firstProject.canPush ? 'lock-open' : 'lock'" class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate" > - <span class="gl-text-truncate">{{ firstProject.project.full_name }}</span> + <span class="gl-text-truncate">{{ projectName(firstProject) }}</span> </gl-badge> <gl-badge @@ -170,14 +172,14 @@ export default { <gl-badge v-for="deployKeysProject in restProjects" v-else-if="isExpanded" - :key="deployKeysProject.project.full_path" + :key="deployKeysProject.project.fullPath" v-gl-tooltip - :href="deployKeysProject.project.full_path" + :href="deployKeysProject.project.fullPath" :title="projectTooltipTitle(deployKeysProject)" - :icon="deployKeysProject.can_push ? 'lock-open' : 'lock'" + :icon="deployKeysProject.canPush ? 'lock-open' : 'lock'" class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate" > - <span class="gl-text-truncate">{{ deployKeysProject.project.full_name }}</span> + <span class="gl-text-truncate">{{ projectName(deployKeysProject) }}</span> </gl-badge> </template> <span v-else class="gl-text-secondary">{{ __('None') }}</span> @@ -188,8 +190,8 @@ export default { {{ __('Created') }} </div> <div class="table-mobile-content gl-text-gray-700 key-created-at"> - <span v-gl-tooltip :title="tooltipTitle(deployKey.created_at)"> - <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span> + <span v-gl-tooltip :title="tooltipTitle(deployKey.createdAt)"> + <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.createdAt) }}</span> </span> </div> </div> @@ -199,12 +201,12 @@ export default { </div> <div class="table-mobile-content gl-text-gray-700 key-expires-at"> <span - v-if="deployKey.expires_at" + v-if="deployKey.expiresAt" v-gl-tooltip - :title="tooltipTitle(deployKey.expires_at)" + :title="tooltipTitle(deployKey.expiresAt)" data-testid="expires-at-tooltip" > - <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.expires_at) }}</span> + <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.expiresAt) }}</span> </span> <span v-else> <span data-testid="expires-never">{{ __('Never') }}</span> @@ -213,13 +215,19 @@ export default { </div> <div class="table-section section-10 table-button-footer deploy-key-actions"> <div class="btn-group table-action-buttons"> - <action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable" category="secondary"> + <action-btn + v-if="!isEnabled" + :deploy-key="deployKey" + :mutation="$options.enableKeyMutation" + category="secondary" + @error="onEnableError" + > {{ __('Enable') }} </action-btn> <gl-button - v-if="deployKey.can_edit" + v-if="deployKey.editPath" v-gl-tooltip - :href="editDeployKeyPath" + :href="deployKey.editPath" :title="__('Edit')" :aria-label="__('Edit')" data-container="body" @@ -232,10 +240,10 @@ export default { :deploy-key="deployKey" :title="__('Remove')" :aria-label="__('Remove')" + :mutation="$options.confirmDisableMutation" category="secondary" variant="danger" icon="remove" - type="remove" data-container="body" /> <action-btn @@ -244,7 +252,7 @@ export default { :deploy-key="deployKey" :title="__('Disable')" :aria-label="__('Disable')" - type="disable" + :mutation="$options.confirmDisableMutation" data-container="body" icon="cancel" category="secondary" diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue index dac63188aa5..088b85e6093 100644 --- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue +++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue @@ -10,14 +10,6 @@ export default { type: Array, required: true, }, - store: { - type: Object, - required: true, - }, - endpoint: { - type: String, - required: true, - }, projectId: { type: String, required: false, @@ -48,8 +40,6 @@ export default { v-for="deployKey in keys" :key="deployKey.id" :deploy-key="deployKey" - :store="store" - :endpoint="endpoint" :project-id="projectId" /> </template> diff --git a/app/assets/javascripts/deploy_keys/graphql/resolvers.js b/app/assets/javascripts/deploy_keys/graphql/resolvers.js index 1993801636e..a8693665b90 100644 --- a/app/assets/javascripts/deploy_keys/graphql/resolvers.js +++ b/app/assets/javascripts/deploy_keys/graphql/resolvers.js @@ -15,6 +15,8 @@ export const mapDeployKey = (deployKey) => ({ __typename: 'LocalDeployKey', }); +const DEFAULT_PAGE_SIZE = 5; + export const resolvers = (endpoints) => ({ Project: { deployKeys(_, { scope, page }, { client }) { @@ -25,19 +27,21 @@ export const resolvers = (endpoints) => ({ endpoint = endpoints.enabledKeysEndpoint; } - return axios.get(endpoint, { params: { page } }).then(({ headers, data }) => { - const normalizedHeaders = normalizeHeaders(headers); - const pageInfo = { - ...parseIntPagination(normalizedHeaders), - __typename: 'LocalPageInfo', - }; - client.writeQuery({ - query: pageInfoQuery, - variables: { input: { page, scope } }, - data: { pageInfo }, + return axios + .get(endpoint, { params: { page, per_page: DEFAULT_PAGE_SIZE } }) + .then(({ headers, data }) => { + const normalizedHeaders = normalizeHeaders(headers); + const pageInfo = { + ...parseIntPagination(normalizedHeaders), + __typename: 'LocalPageInfo', + }; + client.writeQuery({ + query: pageInfoQuery, + variables: { input: { page, scope } }, + data: { pageInfo }, + }); + return data?.keys?.map(mapDeployKey) || []; }); - return data?.keys?.map(mapDeployKey) || []; - }); }, }, Mutation: { @@ -48,6 +52,13 @@ export const resolvers = (endpoints) => ({ }); }, currentScope(_, { scope }, { client }) { + const key = `${scope}Endpoint`; + const { [key]: endpoint } = endpoints; + + if (!endpoint) { + throw new Error(`invalid deploy key scope selected: ${scope}`); + } + client.writeQuery({ query: currentPageQuery, data: { currentPage: 1 }, diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js index 83601d5b2e3..673462073f0 100644 --- a/app/assets/javascripts/deploy_keys/index.js +++ b/app/assets/javascripts/deploy_keys/index.js @@ -1,24 +1,26 @@ import Vue from 'vue'; +import VueApollo from 'vue-apollo'; import DeployKeysApp from './components/app.vue'; +import { createApolloProvider } from './graphql/client'; -export default () => - new Vue({ - el: document.getElementById('js-deploy-keys'), - components: { - DeployKeysApp, - }, - data() { - return { - endpoint: this.$options.el.dataset.endpoint, - projectId: this.$options.el.dataset.projectId, - }; - }, +Vue.use(VueApollo); + +export default () => { + const el = document.getElementById('js-deploy-keys'); + return new Vue({ + el, + apolloProvider: createApolloProvider({ + enabledKeysEndpoint: el.dataset.enabledEndpoint, + availableProjectKeysEndpoint: el.dataset.availableProjectEndpoint, + availablePublicKeysEndpoint: el.dataset.availablePublicEndpoint, + }), render(createElement) { - return createElement('deploy-keys-app', { + return createElement(DeployKeysApp, { props: { - endpoint: this.endpoint, - projectId: this.projectId, + projectId: el.dataset.projectId, + projectPath: el.dataset.projectPath, }, }); }, }); +}; diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js deleted file mode 100644 index 2837fc8ed88..00000000000 --- a/app/assets/javascripts/deploy_keys/service/index.js +++ /dev/null @@ -1,19 +0,0 @@ -import axios from '~/lib/utils/axios_utils'; - -export default class DeployKeysService { - constructor(endpoint) { - this.endpoint = endpoint; - } - - getKeys() { - return axios.get(this.endpoint).then((response) => response.data); - } - - enableKey(id) { - return axios.put(`${this.endpoint}/${id}/enable`).then((response) => response.data); - } - - disableKey(id) { - return axios.put(`${this.endpoint}/${id}/disable`).then((response) => response.data); - } -} diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js deleted file mode 100644 index dcd77e921cd..00000000000 --- a/app/assets/javascripts/deploy_keys/store/index.js +++ /dev/null @@ -1,9 +0,0 @@ -export default class DeployKeysStore { - constructor() { - this.keys = {}; - } - - isEnabled(id) { - return this.keys.enabled_keys.some((key) => key.id === id); - } -} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue new file mode 100644 index 00000000000..32f05d0e298 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue @@ -0,0 +1,111 @@ +<script> +import { GlCard, GlTable, GlLoadingIcon } from '@gitlab/ui'; +import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql'; +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; +import { s__ } from '~/locale'; + +const PAGINATION_DEFAULT_PER_PAGE = 10; + +export default { + components: { + SettingsBlock, + GlCard, + GlTable, + GlLoadingIcon, + }, + inject: ['projectPath'], + i18n: { + settingBlockTitle: s__('PackageRegistry|Protected packages'), + settingBlockDescription: s__( + 'PackageRegistry|When a package is protected then only certain user roles are able to update and delete the protected package. This helps to avoid tampering with the package.', + ), + }, + data() { + return { + fetchSettingsError: false, + packageProtectionRules: [], + }; + }, + computed: { + tableItems() { + return this.packageProtectionRules.map((packagesProtectionRule) => { + return { + col_1_package_name_pattern: packagesProtectionRule.packageNamePattern, + col_2_package_type: packagesProtectionRule.packageType, + col_3_push_protected_up_to_access_level: + packagesProtectionRule.pushProtectedUpToAccessLevel, + }; + }); + }, + totalItems() { + return this.packageProtectionRules.length; + }, + }, + apollo: { + packageProtectionRules: { + query: packagesProtectionRuleQuery, + variables() { + return { + projectPath: this.projectPath, + first: PAGINATION_DEFAULT_PER_PAGE, + }; + }, + update: (data) => { + return data.project?.packagesProtectionRules?.nodes || []; + }, + error(e) { + this.fetchSettingsError = e; + }, + }, + }, + fields: [ + { + key: 'col_1_package_name_pattern', + label: s__('PackageRegistry|Package name pattern'), + }, + { key: 'col_2_package_type', label: s__('PackageRegistry|Package type') }, + { + key: 'col_3_push_protected_up_to_access_level', + label: s__('PackageRegistry|Push protected up to access level'), + }, + ], +}; +</script> + +<template> + <settings-block> + <template #title>{{ $options.i18n.settingBlockTitle }}</template> + + <template #description> + {{ $options.i18n.settingBlockDescription }} + </template> + + <template #default> + <gl-card + class="gl-new-card" + header-class="gl-new-card-header" + body-class="gl-new-card-body gl-px-0" + > + <template #header> + <div class="gl-new-card-title-wrapper gl-justify-content-space-between"> + <h3 class="gl-new-card-title">{{ $options.i18n.settingBlockTitle }}</h3> + </div> + </template> + + <template #default> + <gl-table + :items="tableItems" + :fields="$options.fields" + show-empty + stacked="md" + class="mb-3" + > + <template #table-busy> + <gl-loading-icon size="sm" class="gl-my-5" /> + </template> + </gl-table> + </template> + </gl-card> + </template> + </settings-block> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index 06af69ff250..8e4c50b199b 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -8,6 +8,7 @@ import { } from '~/packages_and_registries/settings/project/constants'; import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue'; import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue'; +import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; export default { components: { @@ -18,7 +19,10 @@ export default { ), GlAlert, PackagesCleanupPolicy, + PackagesProtectionRules: () => + import('~/packages_and_registries/settings/project/components/packages_protection_rules.vue'), }, + mixins: [glFeatureFlagsMixin()], inject: [ 'showContainerRegistrySettings', 'showPackageRegistrySettings', @@ -32,6 +36,11 @@ export default { showAlert: false, }; }, + computed: { + showProtectedPackagesSettings() { + return this.showPackageRegistrySettings && this.glFeatures.packagesProtectedPackages; + }, + }, mounted() { this.checkAlert(); }, @@ -60,6 +69,7 @@ export default { > {{ $options.i18n.UPDATE_SETTINGS_SUCCESS_MESSAGE }} </gl-alert> + <packages-protection-rules v-if="showProtectedPackagesSettings" /> <packages-cleanup-policy v-if="showPackageRegistrySettings" /> <container-expiration-policy v-if="showContainerRegistrySettings" /> <dependency-proxy-packages-settings v-if="showDependencyProxySettings" /> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql new file mode 100644 index 00000000000..e0a072b93e4 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql @@ -0,0 +1,13 @@ +query getProjectPackageProtectionRules($projectPath: ID!, $first: Int) { + project(fullPath: $projectPath) { + id + packagesProtectionRules(first: $first) { + nodes { + id + packageNamePattern + packageType + pushProtectedUpToAccessLevel + } + } + } +} diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue index 27de858fe4e..6feae8dd94e 100644 --- a/app/assets/javascripts/work_items/components/widget_wrapper.vue +++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue @@ -57,28 +57,31 @@ export default { </script> <template> - <div :id="widgetName" class="gl-new-card" :aria-expanded="isOpenString"> + <div :id="widgetName" class="gl-new-card"> <div class="gl-new-card-header"> <div class="gl-new-card-title-wrapper"> - <h3 class="gl-new-card-title"> - <gl-link - :id="anchorLinkId" - class="gl-text-decoration-none" - :href="anchorLink" - aria-hidden="true" - /> + <h2 class="gl-new-card-title"> + <div aria-hidden="true"> + <gl-link + :id="anchorLinkId" + class="gl-text-decoration-none gl-display-none" + :href="anchorLink" + /> + </div> <slot name="header"></slot> - </h3> + </h2> <slot name="header-suffix"></slot> </div> <slot name="header-right"></slot> <div class="gl-new-card-toggle"> + <!-- https://www.w3.org/TR/wai-aria-1.2/#aria-expanded --> <gl-button category="tertiary" size="small" :icon="toggleIcon" :aria-label="toggleLabel" data-testid="widget-toggle" + :aria-expanded="isOpenString" @click="toggle" /> </div> diff --git a/app/controllers/projects/settings/packages_and_registries_controller.rb b/app/controllers/projects/settings/packages_and_registries_controller.rb index 76c9cead360..fd4dbdab95f 100644 --- a/app/controllers/projects/settings/packages_and_registries_controller.rb +++ b/app/controllers/projects/settings/packages_and_registries_controller.rb @@ -12,6 +12,7 @@ module Projects urgency :low def show + push_frontend_feature_flag(:packages_protected_packages, project) end def cleanup_tags diff --git a/app/views/shared/deploy_keys/_index.html.haml b/app/views/shared/deploy_keys/_index.html.haml index 5188c530672..95c99f20380 100644 --- a/app/views/shared/deploy_keys/_index.html.haml +++ b/app/views/shared/deploy_keys/_index.html.haml @@ -13,4 +13,9 @@ .gl-new-card-add-form.gl-m-3.gl-display-none.js-toggle-content = render @deploy_keys.form_partial_path - #js-deploy-keys{ data: { endpoint: project_deploy_keys_path(@project), project_id: @project.id } } + #js-deploy-keys{ data: { project_id: @project.id, + project_path: @project.full_path, + enabled_endpoint: enabled_keys_project_deploy_keys_path(@project), + available_project_endpoint: available_project_keys_project_deploy_keys_path(@project), + available_public_endpoint: available_public_keys_project_deploy_keys_path(@project) + } } |