diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 11:43:02 +0300 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /app/assets/javascripts/packages_and_registries | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
42 files changed, 1744 insertions, 298 deletions
diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue new file mode 100644 index 00000000000..73fb3656af1 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue @@ -0,0 +1,105 @@ +<script> +import { GlAlert, GlFormGroup, GlFormInputGroup, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; +import { __ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { + DEPENDENCY_PROXY_SETTINGS_DESCRIPTION, + DEPENDENCY_PROXY_DOCS_PATH, +} from '~/packages_and_registries/settings/group/constants'; + +import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql'; + +export default { + components: { + GlFormGroup, + GlAlert, + GlFormInputGroup, + GlSprintf, + ClipboardButton, + TitleArea, + GlSkeletonLoader, + }, + inject: ['groupPath', 'dependencyProxyAvailable'], + i18n: { + proxyNotAvailableText: __('Dependency Proxy feature is limited to public groups for now.'), + proxyDisabledText: __('Dependency Proxy disabled. To enable it, contact the group owner.'), + proxyImagePrefix: __('Dependency Proxy image prefix'), + copyImagePrefixText: __('Copy prefix'), + blobCountAndSize: __('Contains %{count} blobs of images (%{size})'), + }, + data() { + return { + group: {}, + }; + }, + apollo: { + group: { + query: getDependencyProxyDetailsQuery, + skip() { + return !this.dependencyProxyAvailable; + }, + variables() { + return { fullPath: this.groupPath }; + }, + }, + }, + computed: { + infoMessages() { + return [ + { + text: DEPENDENCY_PROXY_SETTINGS_DESCRIPTION, + link: DEPENDENCY_PROXY_DOCS_PATH, + }, + ]; + }, + dependencyProxyEnabled() { + return this.group?.dependencyProxySetting?.enabled; + }, + }, +}; +</script> + +<template> + <div> + <title-area :title="__('Dependency Proxy')" :info-messages="infoMessages" /> + <gl-alert + v-if="!dependencyProxyAvailable" + :dismissible="false" + data-testid="proxy-not-available" + > + {{ $options.i18n.proxyNotAvailableText }} + </gl-alert> + + <gl-skeleton-loader v-else-if="$apollo.queries.group.loading" /> + + <div v-else-if="dependencyProxyEnabled" data-testid="main-area"> + <gl-form-group :label="$options.i18n.proxyImagePrefix"> + <gl-form-input-group + readonly + :value="group.dependencyProxyImagePrefix" + class="gl-layout-w-limited" + data-testid="proxy-url" + > + <template #append> + <clipboard-button + :text="group.dependencyProxyImagePrefix" + :title="$options.i18n.copyImagePrefixText" + /> + </template> + </gl-form-input-group> + <template #description> + <span data-qa-selector="dependency_proxy_count" data-testid="proxy-count"> + <gl-sprintf :message="$options.i18n.blobCountAndSize"> + <template #count>{{ group.dependencyProxyBlobCount }}</template> + <template #size>{{ group.dependencyProxyTotalSize }}</template> + </gl-sprintf> + </span> + </template> + </gl-form-group> + </div> + <gl-alert v-else :dismissible="false" data-testid="proxy-disabled"> + {{ $options.i18n.proxyDisabledText }} + </gl-alert> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js new file mode 100644 index 00000000000..16152eb81f6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/index.js @@ -0,0 +1,14 @@ +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createDefaultClient from '~/lib/graphql'; + +Vue.use(VueApollo); + +export const apolloProvider = new VueApollo({ + defaultClient: createDefaultClient( + {}, + { + assumeImmutableResults: true, + }, + ), +}); diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql new file mode 100644 index 00000000000..9058d349bf3 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql @@ -0,0 +1,10 @@ +query getDependencyProxyDetails($fullPath: ID!) { + group(fullPath: $fullPath) { + dependencyProxyBlobCount + dependencyProxyTotalSize + dependencyProxyImagePrefix + dependencyProxySetting { + enabled + } + } +} diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js new file mode 100644 index 00000000000..dc73470e07d --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js @@ -0,0 +1,26 @@ +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import app from '~/packages_and_registries/dependency_proxy/app.vue'; +import { apolloProvider } from '~/packages_and_registries/dependency_proxy/graphql'; +import Translate from '~/vue_shared/translate'; + +Vue.use(Translate); + +export const initDependencyProxyApp = () => { + const el = document.getElementById('js-dependency-proxy'); + if (!el) { + return null; + } + const { dependencyProxyAvailable, ...dataset } = el.dataset; + return new Vue({ + el, + apolloProvider, + provide: { + dependencyProxyAvailable: parseBoolean(dependencyProxyAvailable), + ...dataset, + }, + render(createElement) { + return createElement(app); + }, + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue new file mode 100644 index 00000000000..6016757c1b9 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue @@ -0,0 +1,240 @@ +<script> +import { + GlButton, + GlModal, + GlModalDirective, + GlTooltipDirective, + GlEmptyState, + GlTab, + GlTabs, + GlSprintf, +} from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { objectToQuery } from '~/lib/utils/url_utility'; +import { s__, __ } from '~/locale'; +import TerraformTitle from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue'; +import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue'; +import Tracking from '~/tracking'; +import PackageListRow from '~/packages/shared/components/package_list_row.vue'; +import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; +import { TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; +import { packageTypeToTrackCategory } from '~/packages/shared/utils'; +import PackageFiles from './package_files.vue'; +import PackageHistory from './package_history.vue'; + +export default { + name: 'PackagesApp', + components: { + GlButton, + GlEmptyState, + GlModal, + GlTab, + GlTabs, + GlSprintf, + TerraformTitle, + PackagesListLoader, + PackageListRow, + PackageHistory, + TerraformInstallation, + PackageFiles, + }, + directives: { + GlTooltip: GlTooltipDirective, + GlModal: GlModalDirective, + }, + mixins: [Tracking.mixin()], + trackingActions: { ...TrackingActions }, + data() { + return { + fileToDelete: null, + }; + }, + computed: { + ...mapState([ + 'projectName', + 'packageEntity', + 'packageFiles', + 'isLoading', + 'canDelete', + 'svgPath', + 'npmPath', + 'npmHelpPath', + 'projectListUrl', + 'groupListUrl', + ]), + isValidPackage() { + return Boolean(this.packageEntity.name); + }, + tracking() { + return { + category: packageTypeToTrackCategory(this.packageEntity.package_type), + }; + }, + hasVersions() { + return this.packageEntity.versions?.length > 0; + }, + }, + methods: { + ...mapActions(['deletePackage', 'fetchPackageVersions', 'deletePackageFile']), + formatSize(size) { + return numberToHumanSize(size); + }, + getPackageVersions() { + if (!this.packageEntity.versions) { + this.fetchPackageVersions(); + } + }, + async confirmPackageDeletion() { + this.track(TrackingActions.DELETE_PACKAGE); + await this.deletePackage(); + const returnTo = + !this.groupListUrl || document.referrer.includes(this.projectName) + ? this.projectListUrl + : this.groupListUrl; // to avoid security issue url are supplied from backend + const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true }); + window.location.replace(`${returnTo}?${modalQuery}`); + }, + handleFileDelete(file) { + this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE); + this.fileToDelete = { ...file }; + this.$refs.deleteFileModal.show(); + }, + confirmFileDelete() { + this.track(TrackingActions.DELETE_PACKAGE_FILE); + this.deletePackageFile(this.fileToDelete.id); + this.fileToDelete = null; + }, + }, + i18n: { + deleteModalTitle: s__(`PackageRegistry|Delete Package Version`), + deleteModalContent: s__( + `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`, + ), + deleteFileModalTitle: s__(`PackageRegistry|Delete Package File`), + deleteFileModalContent: s__( + `PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`, + ), + }, + modal: { + packageDeletePrimaryAction: { + text: __('Delete'), + attributes: [ + { variant: 'danger' }, + { category: 'primary' }, + { 'data-qa-selector': 'delete_modal_button' }, + ], + }, + fileDeletePrimaryAction: { + text: __('Delete'), + attributes: [{ variant: 'danger' }, { category: 'primary' }], + }, + cancelAction: { + text: __('Cancel'), + }, + }, +}; +</script> + +<template> + <gl-empty-state + v-if="!isValidPackage" + :title="s__('PackageRegistry|Unable to load package')" + :description="s__('PackageRegistry|There was a problem fetching the details for this package.')" + :svg-path="svgPath" + /> + + <div v-else class="packages-app"> + <terraform-title> + <template #delete-button> + <gl-button + v-if="canDelete" + v-gl-modal="'delete-modal'" + class="js-delete-button" + variant="danger" + category="primary" + data-qa-selector="delete_button" + > + {{ __('Delete') }} + </gl-button> + </template> + </terraform-title> + + <gl-tabs> + <gl-tab :title="__('Detail')"> + <div data-qa-selector="package_information_content"> + <package-history :package-entity="packageEntity" :project-name="projectName" /> + <terraform-installation /> + </div> + + <package-files + :package-files="packageFiles" + :can-delete="canDelete" + @download-file="track($options.trackingActions.PULL_PACKAGE)" + @delete-file="handleFileDelete" + /> + </gl-tab> + + <gl-tab + :title="__('Other versions')" + title-item-class="js-versions-tab" + @click="getPackageVersions" + > + <template v-if="isLoading && !hasVersions"> + <packages-list-loader /> + </template> + + <template v-else-if="hasVersions"> + <package-list-row + v-for="v in packageEntity.versions" + :key="v.id" + :package-entity="{ name: packageEntity.name, ...v }" + :package-link="v.id.toString()" + :disable-delete="true" + :show-package-type="false" + /> + </template> + + <p v-else class="gl-mt-3" data-testid="no-versions-message"> + {{ s__('PackageRegistry|There are no other versions of this package.') }} + </p> + </gl-tab> + </gl-tabs> + + <gl-modal + ref="deleteModal" + modal-id="delete-modal" + :action-primary="$options.modal.packageDeletePrimaryAction" + :action-cancel="$options.modal.cancelAction" + @primary="confirmPackageDeletion" + @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)" + > + <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template> + <gl-sprintf :message="$options.i18n.deleteModalContent"> + <template #version> + <strong>{{ packageEntity.version }}</strong> + </template> + + <template #name> + <strong>{{ packageEntity.name }}</strong> + </template> + </gl-sprintf> + </gl-modal> + + <gl-modal + ref="deleteFileModal" + modal-id="delete-file-modal" + :action-primary="$options.modal.fileDeletePrimaryAction" + :action-cancel="$options.modal.cancelAction" + @primary="confirmFileDelete" + @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" + > + <template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template> + <gl-sprintf v-if="fileToDelete" :message="$options.i18n.deleteFileModalContent"> + <template #filename> + <strong>{{ fileToDelete.file_name }}</strong> + </template> + </gl-sprintf> + </gl-modal> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/details_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue index 3e551706ed0..3e551706ed0 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/details_title.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/details_title.vue diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/file_sha.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/file_sha.vue new file mode 100644 index 00000000000..a25839be7e1 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/file_sha.vue @@ -0,0 +1,41 @@ +<script> +import { s__ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; + +export default { + name: 'FileSha', + components: { + DetailsRow, + ClipboardButton, + }, + props: { + sha: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + }, + i18n: { + copyButtonTitle: s__('PackageRegistry|Copy SHA'), + }, +}; +</script> + +<template> + <details-row dashed> + <div class="gl-px-4"> + {{ title }}: + {{ sha }} + <clipboard-button + :text="sha" + :title="$options.i18n.copyButtonTitle" + category="tertiary" + size="small" + /> + </div> + </details-row> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue new file mode 100644 index 00000000000..ab4cfccd023 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue @@ -0,0 +1,165 @@ +<script> +import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@gitlab/ui'; +import { last } from 'lodash'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { __ } from '~/locale'; +import Tracking from '~/tracking'; +import FileIcon from '~/vue_shared/components/file_icon.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import FileSha from './file_sha.vue'; + +export default { + name: 'PackageFiles', + components: { + GlLink, + GlTable, + GlIcon, + GlDropdown, + GlDropdownItem, + GlButton, + FileIcon, + TimeAgoTooltip, + FileSha, + }, + mixins: [Tracking.mixin()], + props: { + packageFiles: { + type: Array, + required: false, + default: () => [], + }, + canDelete: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + filesTableRows() { + return this.packageFiles.map((pf) => ({ + ...pf, + size: this.formatSize(pf.size), + pipeline: last(pf.pipelines), + })); + }, + showCommitColumn() { + return this.filesTableRows.some((row) => Boolean(row.pipeline?.id)); + }, + filesTableHeaderFields() { + return [ + { + key: 'name', + label: __('Name'), + }, + { + key: 'commit', + label: __('Commit'), + hide: !this.showCommitColumn, + }, + { + key: 'size', + label: __('Size'), + }, + { + key: 'created', + label: __('Created'), + class: 'gl-text-right', + }, + { + key: 'actions', + label: '', + hide: !this.canDelete, + class: 'gl-text-right', + tdClass: 'gl-w-4', + }, + ].filter((c) => !c.hide); + }, + }, + methods: { + formatSize(size) { + return numberToHumanSize(size); + }, + hasDetails(item) { + return item.file_sha256 || item.file_md5 || item.file_sha1; + }, + }, + i18n: { + deleteFile: __('Delete file'), + }, +}; +</script> + +<template> + <div> + <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> + <gl-table + :fields="filesTableHeaderFields" + :items="filesTableRows" + :tbody-tr-attr="{ 'data-testid': 'file-row' }" + > + <template #cell(name)="{ item, toggleDetails, detailsShowing }"> + <gl-button + v-if="hasDetails(item)" + :icon="detailsShowing ? 'angle-up' : 'angle-down'" + :aria-label="detailsShowing ? __('Collapse') : __('Expand')" + category="tertiary" + size="small" + @click="toggleDetails" + /> + <gl-link + :href="item.download_path" + class="gl-text-gray-500" + data-testid="download-link" + @click="$emit('download-file')" + > + <file-icon + :file-name="item.file_name" + css-classes="gl-relative file-icon" + class="gl-mr-1 gl-relative" + /> + <span>{{ item.file_name }}</span> + </gl-link> + </template> + + <template #cell(commit)="{ item }"> + <gl-link + v-if="item.pipeline && item.pipeline.project" + :href="item.pipeline.project.commit_url" + class="gl-text-gray-500" + data-testid="commit-link" + >{{ item.pipeline.git_commit_message }}</gl-link + > + </template> + + <template #cell(created)="{ item }"> + <time-ago-tooltip :time="item.created_at" /> + </template> + + <template #cell(actions)="{ item }"> + <gl-dropdown category="tertiary" right> + <template #button-content> + <gl-icon name="ellipsis_v" /> + </template> + <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-file', item)"> + {{ $options.i18n.deleteFile }} + </gl-dropdown-item> + </gl-dropdown> + </template> + + <template #row-details="{ item }"> + <div + class="gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-bg-gray-10 gl-rounded-base gl-inset-border-1-gray-100" + > + <file-sha + v-if="item.file_sha256" + data-testid="sha-256" + title="SHA-256" + :sha="item.file_sha256" + /> + <file-sha v-if="item.file_md5" data-testid="md5" title="MD5" :sha="item.file_md5" /> + <file-sha v-if="item.file_sha1" data-testid="sha-1" title="SHA-1" :sha="item.file_sha1" /> + </div> + </template> + </gl-table> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue new file mode 100644 index 00000000000..e5be98b87f7 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_history.vue @@ -0,0 +1,165 @@ +<script> +import { GlLink, GlSprintf } from '@gitlab/ui'; +import { first } from 'lodash'; +import { truncateSha } from '~/lib/utils/text_utility'; +import { s__, n__ } from '~/locale'; +import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants'; +import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + name: 'PackageHistory', + i18n: { + createdOn: s__('PackageRegistry|%{name} version %{version} was first created %{datetime}'), + createdByCommitText: s__('PackageRegistry|Created by commit %{link} on branch %{branch}'), + createdByPipelineText: s__( + 'PackageRegistry|Built by pipeline %{link} triggered %{datetime} by %{author}', + ), + publishText: s__('PackageRegistry|Published to the %{project} Package Registry %{datetime}'), + combinedUpdateText: s__( + 'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}', + ), + }, + components: { + GlLink, + GlSprintf, + HistoryItem, + TimeAgoTooltip, + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + projectName: { + type: String, + required: true, + }, + }, + data() { + return { + showDescription: false, + }; + }, + computed: { + pipelines() { + return this.packageEntity.pipelines || []; + }, + firstPipeline() { + return first(this.pipelines); + }, + lastPipelines() { + return this.pipelines.slice(1).slice(-HISTORY_PIPELINES_LIMIT); + }, + showPipelinesInfo() { + return Boolean(this.firstPipeline?.id); + }, + archivedLines() { + return Math.max(this.pipelines.length - HISTORY_PIPELINES_LIMIT - 1, 0); + }, + archivedPipelineMessage() { + return n__( + 'PackageRegistry|Package has %{updatesCount} archived update', + 'PackageRegistry|Package has %{updatesCount} archived updates', + this.archivedLines, + ); + }, + }, + methods: { + truncate(value) { + return truncateSha(value); + }, + }, +}; +</script> + +<template> + <div class="issuable-discussion"> + <h3 class="gl-font-lg" data-testid="title">{{ __('History') }}</h3> + <ul class="timeline main-notes-list notes gl-mb-4" data-testid="timeline"> + <history-item icon="clock" data-testid="created-on"> + <gl-sprintf :message="$options.i18n.createdOn"> + <template #name> + <strong>{{ packageEntity.name }}</strong> + </template> + <template #version> + <strong>{{ packageEntity.version }}</strong> + </template> + <template #datetime> + <time-ago-tooltip :time="packageEntity.created_at" /> + </template> + </gl-sprintf> + </history-item> + + <template v-if="showPipelinesInfo"> + <!-- FIRST PIPELINE BLOCK --> + <history-item icon="commit" data-testid="first-pipeline-commit"> + <gl-sprintf :message="$options.i18n.createdByCommitText"> + <template #link> + <gl-link :href="firstPipeline.project.commit_url" + >#{{ truncate(firstPipeline.sha) }}</gl-link + > + </template> + <template #branch> + <strong>{{ firstPipeline.ref }}</strong> + </template> + </gl-sprintf> + </history-item> + <history-item icon="pipeline" data-testid="first-pipeline-pipeline"> + <gl-sprintf :message="$options.i18n.createdByPipelineText"> + <template #link> + <gl-link :href="firstPipeline.project.pipeline_url">#{{ firstPipeline.id }}</gl-link> + </template> + <template #datetime> + <time-ago-tooltip :time="firstPipeline.created_at" /> + </template> + <template #author>{{ firstPipeline.user.name }}</template> + </gl-sprintf> + </history-item> + </template> + + <!-- PUBLISHED LINE --> + <history-item icon="package" data-testid="published"> + <gl-sprintf :message="$options.i18n.publishText"> + <template #project> + <strong>{{ projectName }}</strong> + </template> + <template #datetime> + <time-ago-tooltip :time="packageEntity.created_at" /> + </template> + </gl-sprintf> + </history-item> + + <history-item v-if="archivedLines" icon="history" data-testid="archived"> + <gl-sprintf :message="archivedPipelineMessage"> + <template #updatesCount> + <strong>{{ archivedLines }}</strong> + </template> + </gl-sprintf> + </history-item> + + <!-- PIPELINES LIST ENTRIES --> + <history-item + v-for="pipeline in lastPipelines" + :key="pipeline.id" + icon="pencil" + data-testid="pipeline-entry" + > + <gl-sprintf :message="$options.i18n.combinedUpdateText"> + <template #link> + <gl-link :href="pipeline.project.commit_url">#{{ truncate(pipeline.sha) }}</gl-link> + </template> + <template #branch> + <strong>{{ pipeline.ref }}</strong> + </template> + <template #pipeline> + <gl-link :href="pipeline.project.pipeline_url">#{{ pipeline.id }}</gl-link> + </template> + <template #datetime> + <time-ago-tooltip :time="pipeline.created_at" /> + </template> + </gl-sprintf> + </history-item> + </ul> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/terraform_installation.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue index c62bf7fb722..c62bf7fb722 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/terraform_installation.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/constants.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/constants.js new file mode 100644 index 00000000000..c0c67faffba --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/constants.js @@ -0,0 +1,5 @@ +import { s__ } from '~/locale'; + +export const FETCH_PACKAGE_VERSIONS_ERROR = s__( + 'PackageRegistry|Unable to fetch package version information.', +); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js new file mode 100644 index 00000000000..a03fa8d9d63 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js @@ -0,0 +1,59 @@ +import Api from '~/api'; +import createFlash from '~/flash'; +import { + DELETE_PACKAGE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, +} from '~/packages/shared/constants'; +import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants'; +import * as types from './mutation_types'; + +export const fetchPackageVersions = ({ commit, state }) => { + commit(types.SET_LOADING, true); + + const { project_id, id } = state.packageEntity; + + return Api.projectPackage(project_id, id) + .then(({ data }) => { + if (data.versions) { + commit(types.SET_PACKAGE_VERSIONS, data.versions.reverse()); + } + }) + .catch(() => { + createFlash({ message: FETCH_PACKAGE_VERSIONS_ERROR, type: 'warning' }); + }) + .finally(() => { + commit(types.SET_LOADING, false); + }); +}; + +export const deletePackage = ({ + state: { + packageEntity: { project_id, id }, + }, +}) => { + return Api.deleteProjectPackage(project_id, id).catch(() => { + createFlash({ message: DELETE_PACKAGE_ERROR_MESSAGE, type: 'warning' }); + }); +}; + +export const deletePackageFile = ( + { + state: { + packageEntity: { project_id, id }, + packageFiles, + }, + commit, + }, + fileId, +) => { + return Api.deleteProjectPackageFile(project_id, id, fileId) + .then(() => { + const filtered = packageFiles.filter((f) => f.id !== fileId); + commit(types.UPDATE_PACKAGE_FILES, filtered); + createFlash({ message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, type: 'success' }); + }) + .catch(() => { + createFlash({ message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, type: 'warning' }); + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/getters.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/getters.js new file mode 100644 index 00000000000..6a17e7aa6d6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/getters.js @@ -0,0 +1,3 @@ +export const packagePipeline = ({ packageEntity }) => { + return packageEntity?.pipeline || null; +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/index.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/index.js new file mode 100644 index 00000000000..15e17bcfaac --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/index.js @@ -0,0 +1,18 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; +import mutations from './mutations'; + +Vue.use(Vuex); + +export default (initialState = {}) => + new Vuex.Store({ + actions, + getters, + mutations, + state: { + isLoading: false, + ...initialState, + }, + }); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/mutation_types.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/mutation_types.js new file mode 100644 index 00000000000..590f2d9f970 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/mutation_types.js @@ -0,0 +1,3 @@ +export const SET_LOADING = 'SET_LOADING'; +export const SET_PACKAGE_VERSIONS = 'SET_PACKAGE_VERSIONS'; +export const UPDATE_PACKAGE_FILES = 'UPDATE_PACKAGE_FILES'; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/mutations.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/mutations.js new file mode 100644 index 00000000000..762fd5a4040 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/mutations.js @@ -0,0 +1,17 @@ +import * as types from './mutation_types'; + +export default { + [types.SET_LOADING](state, isLoading) { + state.isLoading = isLoading; + }, + + [types.SET_PACKAGE_VERSIONS](state, versions) { + state.packageEntity = { + ...state.packageEntity, + versions, + }; + }, + [types.UPDATE_PACKAGE_FILES](state, files) { + state.packageFiles = files; + }, +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details_app_bundle.js index 98942b1e578..32fbc9382fd 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details_app_bundle.js +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details_app_bundle.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { parseBoolean } from '~/lib/utils/common_utils'; -import PackagesApp from '~/packages/details/components/app.vue'; -import createStore from '~/packages/details/store'; +import PackagesApp from '~/packages_and_registries/infrastructure_registry/details/components/app.vue'; +import createStore from '~/packages_and_registries/infrastructure_registry/details/store'; import Translate from '~/vue_shared/translate'; Vue.use(Translate); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue index f0da7db6c91..1360b03856f 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/nuget.vue @@ -24,7 +24,13 @@ export default { <template> <div> - <details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source"> + <details-row + v-if="packageEntity.metadata.projectUrl" + icon="project" + padding="gl-p-4" + dashed + data-testid="nuget-source" + > <gl-sprintf :message="$options.i18n.sourceText"> <template #link> <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{ @@ -33,7 +39,12 @@ export default { </template> </gl-sprintf> </details-row> - <details-row icon="license" padding="gl-p-4" data-testid="nuget-license"> + <details-row + v-if="packageEntity.metadata.licenseUrl" + icon="license" + padding="gl-p-4" + data-testid="nuget-license" + > <gl-sprintf :message="$options.i18n.licenseText"> <template #link> <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{ diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue index 47081e23318..2448324549e 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue @@ -1,5 +1,5 @@ <script> -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlLink, GlSprintf, GlFormRadioGroup } from '@gitlab/ui'; import { s__ } from '~/locale'; import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; @@ -11,6 +11,8 @@ import { TRACKING_LABEL_CODE_INSTRUCTION, NPM_PACKAGE_MANAGER, YARN_PACKAGE_MANAGER, + PROJECT_PACKAGE_ENDPOINT_TYPE, + INSTANCE_PACKAGE_ENDPOINT_TYPE, } from '~/packages_and_registries/package_registry/constants'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; @@ -21,8 +23,9 @@ export default { CodeInstruction, GlLink, GlSprintf, + GlFormRadioGroup, }, - inject: ['npmHelpPath', 'npmPath'], + inject: ['npmHelpPath', 'npmPath', 'npmProjectPath'], props: { packageEntity: { type: Object, @@ -32,6 +35,7 @@ export default { data() { return { instructionType: NPM_PACKAGE_MANAGER, + packageEndpointType: INSTANCE_PACKAGE_ENDPOINT_TYPE, }; }, computed: { @@ -39,13 +43,13 @@ export default { return this.npmInstallationCommand(NPM_PACKAGE_MANAGER); }, npmSetup() { - return this.npmSetupCommand(NPM_PACKAGE_MANAGER); + return this.npmSetupCommand(NPM_PACKAGE_MANAGER, this.packageEndpointType); }, yarnCommand() { return this.npmInstallationCommand(YARN_PACKAGE_MANAGER); }, yarnSetupCommand() { - return this.npmSetupCommand(YARN_PACKAGE_MANAGER); + return this.npmSetupCommand(YARN_PACKAGE_MANAGER, this.packageEndpointType); }, showNpm() { return this.instructionType === NPM_PACKAGE_MANAGER; @@ -58,14 +62,16 @@ export default { return `${instruction} ${this.packageEntity.name}`; }, - npmSetupCommand(type) { + npmSetupCommand(type, endpointType) { const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/')); + const npmPathForEndpoint = + endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE ? this.npmPath : this.npmProjectPath; if (type === NPM_PACKAGE_MANAGER) { - return `echo ${scope}:registry=${this.npmPath}/ >> .npmrc`; + return `echo ${scope}:registry=${npmPathForEndpoint}/ >> .npmrc`; } - return `echo \\"${scope}:registry\\" \\"${this.npmPath}/\\" >> .yarnrc`; + return `echo \\"${scope}:registry\\" \\"${npmPathForEndpoint}/\\" >> .yarnrc`; }, }, packageManagers: { @@ -87,6 +93,10 @@ export default { { value: NPM_PACKAGE_MANAGER, label: s__('PackageRegistry|Show NPM commands') }, { value: YARN_PACKAGE_MANAGER, label: s__('PackageRegistry|Show Yarn commands') }, ], + packageEndpointTypeOptions: [ + { value: INSTANCE_PACKAGE_ENDPOINT_TYPE, text: s__('PackageRegistry|Instance-level') }, + { value: PROJECT_PACKAGE_ENDPOINT_TYPE, text: s__('PackageRegistry|Project-level') }, + ], }; </script> @@ -116,6 +126,12 @@ export default { <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> + <gl-form-radio-group + :options="$options.packageEndpointTypeOptions" + :checked="packageEndpointType" + @change="packageEndpointType = $event" + /> + <code-instruction v-if="showNpm" :instruction="npmSetup" diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue index 408bd2e3dfe..af6bd7079ba 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_history.vue @@ -1,11 +1,10 @@ <script> -/* eslint-disable @gitlab/require-string-literal-i18n-helpers */ import { GlLink, GlSprintf } from '@gitlab/ui'; import { first } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, n__ } from '~/locale'; -import { HISTORY_PIPELINES_LIMIT } from '~/packages/details/constants'; +import { HISTORY_PIPELINES_LIMIT } from '~/packages_and_registries/shared/constants'; import HistoryItem from '~/vue_shared/components/registry/history_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -21,8 +20,6 @@ export default { combinedUpdateText: s__( 'PackageRegistry|Package updated by commit %{link} on branch %{branch}, built by pipeline %{pipeline}, and published to the registry %{datetime}', ), - archivedPipelineMessageSingular: s__('PackageRegistry|Package has %{number} archived update'), - archivedPipelineMessagePlural: s__('PackageRegistry|Package has %{number} archived updates'), }, components: { GlLink, @@ -58,14 +55,14 @@ export default { showPipelinesInfo() { return Boolean(this.firstPipeline?.id); }, - archiviedLines() { + archivedLines() { return Math.max(this.pipelines.length - HISTORY_PIPELINES_LIMIT - 1, 0); }, archivedPipelineMessage() { return n__( - this.$options.i18n.archivedPipelineMessageSingular, - this.$options.i18n.archivedPipelineMessagePlural, - this.archiviedLines, + 'PackageRegistry|Package has %{updatesCount} archived update', + 'PackageRegistry|Package has %{updatesCount} archived updates', + this.archivedLines, ); }, }, @@ -135,10 +132,10 @@ export default { </gl-sprintf> </history-item> - <history-item v-if="archiviedLines" icon="history" data-testid="archived"> + <history-item v-if="archivedLines" icon="history" data-testid="archived"> <gl-sprintf :message="archivedPipelineMessage"> - <template #number> - <strong>{{ archiviedLines }}</strong> + <template #updatesCount> + <strong>{{ archivedLines }}</strong> </template> </gl-sprintf> </history-item> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue new file mode 100644 index 00000000000..08481ac5655 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue @@ -0,0 +1,134 @@ +<script> +/* + * The following component has several commented lines, this is because we are refactoring them piece by piece on several mrs + * For a complete overview of the plan please check: https://gitlab.com/gitlab-org/gitlab/-/issues/330846 + * This work is behind feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/341136 + */ +// import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; +import createFlash from '~/flash'; +import { historyReplaceState } from '~/lib/utils/common_utils'; +import { s__ } from '~/locale'; +import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants'; +import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; +import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; +import { + PROJECT_RESOURCE_TYPE, + GROUP_RESOURCE_TYPE, + LIST_QUERY_DEBOUNCE_TIME, +} from '~/packages_and_registries/package_registry/constants'; +import PackageTitle from './package_title.vue'; +import PackageSearch from './package_search.vue'; +// import PackageList from './packages_list.vue'; + +export default { + components: { + // GlEmptyState, + // GlLink, + // GlSprintf, + // PackageList, + PackageTitle, + PackageSearch, + }, + inject: [ + 'packageHelpUrl', + 'emptyListIllustration', + 'emptyListHelpUrl', + 'isGroupPage', + 'fullPath', + ], + data() { + return { + packages: {}, + sort: '', + filters: {}, + }; + }, + apollo: { + packages: { + query: getPackagesQuery, + variables() { + return this.queryVariables; + }, + update(data) { + return data[this.graphqlResource].packages; + }, + debounce: LIST_QUERY_DEBOUNCE_TIME, + }, + }, + computed: { + queryVariables() { + return { + isGroupPage: this.isGroupPage, + fullPath: this.fullPath, + sort: this.isGroupPage ? undefined : this.sort, + groupSort: this.isGroupPage ? this.sort : undefined, + packageName: this.filters?.packageName, + packageType: this.filters?.packageType, + }; + }, + graphqlResource() { + return this.isGroupPage ? GROUP_RESOURCE_TYPE : PROJECT_RESOURCE_TYPE; + }, + packagesCount() { + return this.packages?.count; + }, + hasFilters() { + return this.filters.packageName && this.filters.packageType; + }, + emptyStateTitle() { + return this.emptySearch + ? this.$options.i18n.emptyPageTitle + : this.$options.i18n.noResultsTitle; + }, + }, + mounted() { + this.checkDeleteAlert(); + }, + methods: { + checkDeleteAlert() { + const urlParams = new URLSearchParams(window.location.search); + const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT); + if (showAlert) { + // to be refactored to use gl-alert + createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' }); + const cleanUrl = window.location.href.split('?')[0]; + historyReplaceState(cleanUrl); + } + }, + handleSearchUpdate({ sort, filters }) { + this.sort = sort; + this.filters = { ...filters }; + }, + }, + i18n: { + widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), + emptyPageTitle: s__('PackageRegistry|There are no packages yet'), + noResultsTitle: s__('PackageRegistry|Sorry, your filter produced no results'), + noResultsText: s__( + 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', + ), + }, +}; +</script> + +<template> + <div> + <package-title :help-url="packageHelpUrl" :count="packagesCount" /> + <package-search @update="handleSearchUpdate" /> + + <!-- <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> + <template #empty-state> + <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration"> + <template #description> + <gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" /> + <gl-sprintf v-else :message="$options.i18n.noResultsText"> + <template #noPackagesLink="{ content }"> + <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + </gl-empty-state> + </template> + </package-list> --> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue new file mode 100644 index 00000000000..195ff7af583 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue @@ -0,0 +1,151 @@ +<script> +import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import { + PACKAGE_ERROR_STATUS, + PACKAGE_DEFAULT_STATUS, +} from '~/packages_and_registries/package_registry/constants'; +import { getPackageTypeLabel } from '~/packages/shared/utils'; +import PackagePath from '~/packages/shared/components/package_path.vue'; +import PackageTags from '~/packages/shared/components/package_tags.vue'; +import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue'; +import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; + +export default { + name: 'PackageListRow', + components: { + GlButton, + GlLink, + GlSprintf, + GlTruncate, + PackageTags, + PackagePath, + PublishMethod, + ListItem, + PackageIconAndName, + TimeagoTooltip, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: ['isGroupPage'], + props: { + packageEntity: { + type: Object, + required: true, + }, + }, + computed: { + packageType() { + return getPackageTypeLabel(this.packageEntity.packageType.toLowerCase()); + }, + packageLink() { + const { project, id } = this.packageEntity; + return `${project?.webUrl}/-/packages/${getIdFromGraphQLId(id)}`; + }, + pipeline() { + return this.packageEntity?.pipelines?.nodes[0]; + }, + pipelineUser() { + return this.pipeline?.user?.name; + }, + showWarningIcon() { + return this.packageEntity.status === PACKAGE_ERROR_STATUS; + }, + showTags() { + return Boolean(this.packageEntity.tags?.nodes?.length); + }, + disabledRow() { + return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS; + }, + }, + i18n: { + erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'), + }, +}; +</script> + +<template> + <list-item data-qa-selector="package_row" :disabled="disabledRow"> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> + <gl-link + :href="packageLink" + class="gl-text-body gl-min-w-0" + data-qa-selector="package_link" + :disabled="disabledRow" + > + <gl-truncate :text="packageEntity.name" /> + </gl-link> + + <gl-button + v-if="showWarningIcon" + v-gl-tooltip="{ title: $options.i18n.erroredPackageText }" + class="gl-hover-bg-transparent!" + icon="warning" + category="tertiary" + data-testid="warning-icon" + :aria-label="__('Warning')" + /> + + <package-tags + v-if="showTags" + class="gl-ml-3" + :tags="packageEntity.tags.nodes" + hide-label + :tag-display-limit="1" + /> + </div> + </template> + <template #left-secondary> + <div class="gl-display-flex" data-testid="left-secondary-infos"> + <span>{{ packageEntity.version }}</span> + + <div v-if="pipelineUser" class="gl-display-none gl-sm-display-flex gl-ml-2"> + <gl-sprintf :message="s__('PackageRegistry|published by %{author}')"> + <template #author>{{ pipelineUser }}</template> + </gl-sprintf> + </div> + + <package-icon-and-name> + {{ packageType }} + </package-icon-and-name> + + <package-path + v-if="isGroupPage" + :path="packageEntity.project.fullPath" + :disabled="disabledRow" + /> + </div> + </template> + + <template #right-primary> + <publish-method :pipeline="pipeline" /> + </template> + + <template #right-secondary> + <span> + <gl-sprintf :message="__('Created %{timestamp}')"> + <template #timestamp> + <timeago-tooltip :time="packageEntity.createdAt" /> + </template> + </gl-sprintf> + </span> + </template> + + <template v-if="!disabledRow" #right-action> + <gl-button + data-testid="action-delete" + icon="remove" + category="secondary" + variant="danger" + :title="s__('PackageRegistry|Remove package')" + :aria-label="s__('PackageRegistry|Remove package')" + @click="$emit('packageToDelete', packageEntity)" + /> + </template> + </list-item> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue index 280d292ce0b..836df59ca58 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue @@ -1,10 +1,14 @@ <script> -import { mapState, mapActions } from 'vuex'; import { s__ } from '~/locale'; import { sortableFields } from '~/packages/list/utils'; import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue'; +import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils'; +import { + FILTERED_SEARCH_TERM, + FILTERED_SEARCH_TYPE, +} from '~/packages_and_registries/shared/constants'; import PackageTypeToken from './tokens/package_type_token.vue'; export default { @@ -19,21 +23,71 @@ export default { }, ], components: { RegistrySearch, UrlSync }, + inject: ['isGroupPage'], + data() { + return { + filters: [], + sorting: { + orderBy: 'name', + sort: 'desc', + }, + mountRegistrySearch: false, + }; + }, computed: { - ...mapState({ - isGroupPage: (state) => state.config.isGroupPage, - sorting: (state) => state.sorting, - filter: (state) => state.filter, - }), sortableFields() { return sortableFields(this.isGroupPage); }, + parsedSorting() { + const cleanOrderBy = this.sorting?.orderBy.replace('_at', ''); + return `${cleanOrderBy}_${this.sorting?.sort}`.toUpperCase(); + }, + parsedFilters() { + const parsed = { + packageName: '', + packageType: undefined, + }; + + return this.filters.reduce((acc, filter) => { + if (filter.type === FILTERED_SEARCH_TYPE && filter.value?.data) { + return { + ...acc, + packageType: filter.value.data.toUpperCase(), + }; + } + + if (filter.type === FILTERED_SEARCH_TERM) { + return { + ...acc, + packageName: `${acc.packageName} ${filter.value.data}`.trim(), + }; + } + + return acc; + }, parsed); + }, + }, + mounted() { + const queryParams = getQueryParams(window.document.location.search); + const { sorting, filters } = extractFilterAndSorting(queryParams); + this.updateSorting(sorting); + this.updateFilters(filters); + this.mountRegistrySearch = true; + this.emitUpdate(); }, methods: { - ...mapActions(['setSorting', 'setFilter']), + updateFilters(newValue) { + this.filters = newValue; + }, updateSorting(newValue) { - this.setSorting(newValue); - this.$emit('update'); + this.sorting = { ...this.sorting, ...newValue }; + }, + updateSortingAndEmitUpdate(newValue) { + this.updateSorting(newValue); + this.emitUpdate(); + }, + emitUpdate() { + this.$emit('update', { sort: this.parsedSorting, filters: this.parsedFilters }); }, }, }; @@ -43,13 +97,14 @@ export default { <url-sync> <template #default="{ updateQuery }"> <registry-search - :filter="filter" + v-if="mountRegistrySearch" + :filter="filters" :sorting="sorting" :tokens="$options.tokens" :sortable-fields="sortableFields" - @sorting:changed="updateSorting" - @filter:changed="setFilter" - @filter:submit="$emit('update')" + @sorting:changed="updateSortingAndEmitUpdate" + @filter:changed="updateFilters" + @filter:submit="emitUpdate" @query:changed="updateQuery" /> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue deleted file mode 100644 index 75fbdb80192..00000000000 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list_app.vue +++ /dev/null @@ -1,132 +0,0 @@ -<script> -import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; -import { mapActions, mapState } from 'vuex'; -import createFlash from '~/flash'; -import { historyReplaceState } from '~/lib/utils/common_utils'; -import { s__ } from '~/locale'; -import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants'; -import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; -import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; -import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils'; -import PackageList from './packages_list.vue'; - -export default { - components: { - GlEmptyState, - GlLink, - GlSprintf, - PackageList, - PackageTitle: () => - import(/* webpackChunkName: 'package_registry_components' */ './package_title.vue'), - PackageSearch: () => - import(/* webpackChunkName: 'package_registry_components' */ './package_search.vue'), - InfrastructureTitle: () => - import( - /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue' - ), - InfrastructureSearch: () => - import( - /* webpackChunkName: 'infrastructure_registry_components' */ '~/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue' - ), - }, - inject: { - titleComponent: { - from: 'titleComponent', - default: 'PackageTitle', - }, - searchComponent: { - from: 'searchComponent', - default: 'PackageSearch', - }, - emptyPageTitle: { - from: 'emptyPageTitle', - default: s__('PackageRegistry|There are no packages yet'), - }, - noResultsText: { - from: 'noResultsText', - default: s__( - 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', - ), - }, - }, - computed: { - ...mapState({ - emptyListIllustration: (state) => state.config.emptyListIllustration, - emptyListHelpUrl: (state) => state.config.emptyListHelpUrl, - filter: (state) => state.filter, - selectedType: (state) => state.selectedType, - packageHelpUrl: (state) => state.config.packageHelpUrl, - packagesCount: (state) => state.pagination?.total, - }), - emptySearch() { - return ( - this.filter.filter((f) => f.type !== FILTERED_SEARCH_TERM || f.value?.data).length === 0 - ); - }, - - emptyStateTitle() { - return this.emptySearch - ? this.emptyPageTitle - : s__('PackageRegistry|Sorry, your filter produced no results'); - }, - }, - mounted() { - const queryParams = getQueryParams(window.document.location.search); - const { sorting, filters } = extractFilterAndSorting(queryParams); - this.setSorting(sorting); - this.setFilter(filters); - this.requestPackagesList(); - this.checkDeleteAlert(); - }, - methods: { - ...mapActions([ - 'requestPackagesList', - 'requestDeletePackage', - 'setSelectedType', - 'setSorting', - 'setFilter', - ]), - onPageChanged(page) { - return this.requestPackagesList({ page }); - }, - onPackageDeleteRequest(item) { - return this.requestDeletePackage(item); - }, - checkDeleteAlert() { - const urlParams = new URLSearchParams(window.location.search); - const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT); - if (showAlert) { - // to be refactored to use gl-alert - createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' }); - const cleanUrl = window.location.href.split('?')[0]; - historyReplaceState(cleanUrl); - } - }, - }, - i18n: { - widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), - }, -}; -</script> - -<template> - <div> - <component :is="titleComponent" :help-url="packageHelpUrl" :count="packagesCount" /> - <component :is="searchComponent" @update="requestPackagesList" /> - - <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> - <template #empty-state> - <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration"> - <template #description> - <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" /> - <gl-sprintf v-else :message="noResultsText"> - <template #noPackagesLink="{ content }"> - <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> - </template> - </gl-sprintf> - </template> - </gl-empty-state> - </template> - </package-list> - </div> -</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue new file mode 100644 index 00000000000..8ecf433f3ab --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/publish_method.vue @@ -0,0 +1,61 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; +import { __, s__ } from '~/locale'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; + +export default { + name: 'PublishMethod', + components: { + ClipboardButton, + GlIcon, + GlLink, + }, + props: { + pipeline: { + type: Object, + required: false, + default: null, + }, + }, + computed: { + hasPipeline() { + return Boolean(this.pipeline); + }, + packageShaShort() { + return this.pipeline?.sha?.substring(0, 8); + }, + }, + i18n: { + COPY_COMMIT_SHA: __('Copy commit SHA'), + MANUALLY_PUBLISHED: s__('PackageRegistry|Manually Published'), + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <template v-if="hasPipeline"> + <gl-icon name="git-merge" class="gl-mr-2" /> + <span data-testid="pipeline-ref" class="gl-mr-2">{{ pipeline.ref }}</span> + + <gl-icon name="commit" class="gl-mr-2" /> + <gl-link data-testid="pipeline-sha" :href="pipeline.commitPath" class="gl-mr-2">{{ + packageShaShort + }}</gl-link> + + <clipboard-button + :text="pipeline.sha" + :title="$options.i18n.COPY_COMMIT_SHA" + category="tertiary" + size="small" + /> + </template> + + <template v-else> + <gl-icon name="upload" class="gl-mr-2" /> + <span data-testid="manually-published"> + {{ $options.i18n.MANUALLY_PUBLISHED }} + </span> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js index f023b4481a0..6a88880fa90 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -1,5 +1,4 @@ -/* eslint-disable @gitlab/require-string-literal-i18n-helpers */ -import { __, s__ } from '~/locale'; +import { s__ } from '~/locale'; export const PACKAGE_TYPE_CONAN = 'CONAN'; export const PACKAGE_TYPE_MAVEN = 'MAVEN'; @@ -71,7 +70,7 @@ export const DELETE_PACKAGE_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while deleting the package.', ); export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__( - __('PackageRegistry|Something went wrong while deleting the package file.'), + 'PackageRegistry|Something went wrong while deleting the package file.', ); export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__( 'PackageRegistry|Package file deleted successfully', @@ -87,3 +86,10 @@ export const PACKAGE_PROCESSING_STATUS = 'PROCESSING'; export const NPM_PACKAGE_MANAGER = 'npm'; export const YARN_PACKAGE_MANAGER = 'yarn'; + +export const PROJECT_PACKAGE_ENDPOINT_TYPE = 'project'; +export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance'; + +export const PROJECT_RESOURCE_TYPE = 'project'; +export const GROUP_RESOURCE_TYPE = 'group'; +export const LIST_QUERY_DEBOUNCE_TIME = 50; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql new file mode 100644 index 00000000000..aaf0eb54aff --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql @@ -0,0 +1,27 @@ +fragment PackageData on Package { + id + name + version + packageType + createdAt + status + tags { + nodes { + name + } + } + pipelines { + nodes { + sha + ref + commitPath + user { + name + } + } + } + project { + fullPath + webUrl + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql new file mode 100644 index 00000000000..74e6de87866 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql @@ -0,0 +1,27 @@ +#import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql" + +query getPackages( + $fullPath: ID! + $isGroupPage: Boolean! + $sort: PackageSort + $groupSort: PackageGroupSort + $packageName: String + $packageType: PackageTypeEnum +) { + project(fullPath: $fullPath) @skip(if: $isGroupPage) { + packages(sort: $sort, packageName: $packageName, packageType: $packageType) { + count + nodes { + ...PackageData + } + } + } + group(fullPath: $fullPath) @include(if: $isGroupPage) { + packages(sort: $groupSort, packageName: $packageName, packageType: $packageType) { + count + nodes { + ...PackageData + } + } + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js index 1e01b75aabc..d797a0a5327 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js @@ -1,14 +1,22 @@ import Vue from 'vue'; import Translate from '~/vue_shared/translate'; -import PackagesListApp from '../components/list/packages_list_app.vue'; +import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index'; +import PackagesListApp from '../components/list/app.vue'; Vue.use(Translate); export default () => { const el = document.getElementById('js-vue-packages-list'); + const isGroupPage = el.dataset.pageType === 'groups'; + return new Vue({ el, + apolloProvider, + provide: { + ...el.dataset, + isGroupPage, + }, render(createElement) { return createElement(PackagesListApp); }, diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js index 5cd8261ac23..9b5a0d221b8 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js @@ -19,6 +19,7 @@ export default () => { apolloProvider, provide: { defaultExpanded: parseBoolean(el.dataset.defaultExpanded), + dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable), groupPath: el.dataset.groupPath, }, render(createElement) { diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue new file mode 100644 index 00000000000..2dbe36def0e --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue @@ -0,0 +1,110 @@ +<script> +import { GlToggle, GlSprintf, GlLink } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql'; +import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update'; +import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; + +import { + DEPENDENCY_PROXY_HEADER, + DEPENDENCY_PROXY_SETTINGS_DESCRIPTION, + DEPENDENCY_PROXY_DOCS_PATH, +} from '~/packages_and_registries/settings/group/constants'; + +export default { + name: 'DependencyProxySettings', + components: { + GlToggle, + GlSprintf, + GlLink, + SettingsBlock, + }, + i18n: { + DEPENDENCY_PROXY_HEADER, + DEPENDENCY_PROXY_SETTINGS_DESCRIPTION, + label: s__('DependencyProxy|Enable Proxy'), + }, + links: { + DEPENDENCY_PROXY_DOCS_PATH, + }, + inject: ['defaultExpanded', 'groupPath'], + props: { + dependencyProxySettings: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + enabled: { + get() { + return this.dependencyProxySettings.enabled; + }, + set(enabled) { + this.updateSettings({ enabled }); + }, + }, + }, + methods: { + async updateSettings(payload) { + try { + const { data } = await this.$apollo.mutate({ + mutation: updateDependencyProxySettings, + variables: { + input: { + groupPath: this.groupPath, + ...payload, + }, + }, + update: updateGroupPackageSettings(this.groupPath), + optimisticResponse: updateGroupDependencyProxySettingsOptimisticResponse({ + ...this.dependencyProxySettings, + ...payload, + }), + }); + + if (data.updateDependencyProxySettings?.errors?.length > 0) { + throw new Error(); + } else { + this.$emit('success'); + } + } catch { + this.$emit('error'); + } + }, + }, +}; +</script> + +<template> + <settings-block + :default-expanded="defaultExpanded" + data-qa-selector="dependency_proxy_settings_content" + > + <template #title> {{ $options.i18n.DEPENDENCY_PROXY_HEADER }} </template> + <template #description> + <span data-testid="description"> + <gl-sprintf :message="$options.i18n.DEPENDENCY_PROXY_SETTINGS_DESCRIPTION"> + <template #docLink="{ content }"> + <gl-link :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link> + </template> + </gl-sprintf> + </span> + </template> + <template #default> + <div> + <gl-toggle + v-model="enabled" + :disabled="isLoading" + :label="$options.i18n.label" + data-qa-selector="dependency_proxy_setting_toggle" + /> + </div> + </template> + </settings-block> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue index d66a30e7e81..b0088838acc 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue @@ -86,6 +86,7 @@ export default { :label="$options.i18n.DUPLICATES_TOGGLE_LABEL" label-position="hidden" :value="duplicatesAllowed" + :disabled="loading" @change="update(modelNames.allowed, $event)" /> <div class="gl-ml-5"> @@ -108,6 +109,7 @@ export default { > <gl-form-input id="maven-duplicated-settings-regex-input" + :disabled="loading" :value="duplicateExceptionRegex" @change="update(modelNames.exception, $event)" /> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue index ec3be43196c..b45cedcdd66 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue @@ -1,108 +1,66 @@ <script> -import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui'; -import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue'; -import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue'; -import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue'; -import { - PACKAGE_SETTINGS_HEADER, - PACKAGE_SETTINGS_DESCRIPTION, - PACKAGES_DOCS_PATH, - ERROR_UPDATING_SETTINGS, - SUCCESS_UPDATING_SETTINGS, -} from '~/packages_and_registries/settings/group/constants'; -import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql'; +import { GlAlert } from '@gitlab/ui'; +import { n__ } from '~/locale'; +import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue'; +import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue'; + import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql'; -import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update'; -import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; -import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; export default { name: 'GroupSettingsApp', - i18n: { - PACKAGE_SETTINGS_HEADER, - PACKAGE_SETTINGS_DESCRIPTION, - }, - links: { - PACKAGES_DOCS_PATH, - }, components: { GlAlert, - GlSprintf, - GlLink, - SettingsBlock, - MavenSettings, - GenericSettings, - DuplicatesSettings, + PackagesSettings, + DependencyProxySettings, }, - inject: ['defaultExpanded', 'groupPath'], + inject: ['groupPath', 'dependencyProxyAvailable'], apollo: { - packageSettings: { + group: { query: getGroupPackagesSettingsQuery, variables() { return { fullPath: this.groupPath, }; }, - update(data) { - return data.group?.packageSettings; - }, }, }, data() { return { - packageSettings: {}, - errors: {}, + group: {}, alertMessage: null, }; }, computed: { + packageSettings() { + return this.group?.packageSettings || {}; + }, + dependencyProxySettings() { + return this.group?.dependencyProxySetting || {}; + }, isLoading() { - return this.$apollo.queries.packageSettings.loading; + return this.$apollo.queries.group.loading; }, }, methods: { dismissAlert() { this.alertMessage = null; }, - updateSettings(payload) { - this.errors = {}; - return this.$apollo - .mutate({ - mutation: updateNamespacePackageSettings, - variables: { - input: { - namespacePath: this.groupPath, - ...payload, - }, - }, - update: updateGroupPackageSettings(this.groupPath), - optimisticResponse: updateGroupPackagesSettingsOptimisticResponse({ - ...this.packageSettings, - ...payload, - }), - }) - .then(({ data }) => { - if (data.updateNamespacePackageSettings?.errors?.length > 0) { - this.alertMessage = ERROR_UPDATING_SETTINGS; - } else { - this.dismissAlert(); - this.$toast.show(SUCCESS_UPDATING_SETTINGS); - } - }) - .catch((e) => { - if (e.graphQLErrors) { - e.graphQLErrors.forEach((error) => { - const [ - { - path: [key], - message, - }, - ] = error.extensions.problems; - this.errors = { ...this.errors, [key]: message }; - }); - } - this.alertMessage = ERROR_UPDATING_SETTINGS; - }); + handleSuccess(amount = 1) { + const successMessage = n__( + 'Setting saved successfully', + 'Settings saved successfully', + amount, + ); + this.$toast.show(successMessage); + this.dismissAlert(); + }, + handleError(amount = 1) { + const errorMessage = n__( + 'An error occurred while saving the setting', + 'An error occurred while saving the settings', + amount, + ); + this.alertMessage = errorMessage; }, }, }; @@ -114,50 +72,19 @@ export default { {{ alertMessage }} </gl-alert> - <settings-block - :default-expanded="defaultExpanded" - data-qa-selector="package_registry_settings_content" - > - <template #title> {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}</template> - <template #description> - <span data-testid="description"> - <gl-sprintf :message="$options.i18n.PACKAGE_SETTINGS_DESCRIPTION"> - <template #link="{ content }"> - <gl-link :href="$options.links.PACKAGES_DOCS_PATH" target="_blank">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </span> - </template> - <template #default> - <maven-settings data-testid="maven-settings"> - <template #default="{ modelNames }"> - <duplicates-settings - :duplicates-allowed="packageSettings.mavenDuplicatesAllowed" - :duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex" - :duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex" - :model-names="modelNames" - :loading="isLoading" - toggle-qa-selector="allow_duplicates_toggle" - label-qa-selector="allow_duplicates_label" - @update="updateSettings" - /> - </template> - </maven-settings> - <generic-settings class="gl-mt-6" data-testid="generic-settings"> - <template #default="{ modelNames }"> - <duplicates-settings - :duplicates-allowed="packageSettings.genericDuplicatesAllowed" - :duplicate-exception-regex="packageSettings.genericDuplicateExceptionRegex" - :duplicate-exception-regex-error="errors.genericDuplicateExceptionRegex" - :model-names="modelNames" - :loading="isLoading" - @update="updateSettings" - /> - </template> - </generic-settings> - </template> - </settings-block> + <packages-settings + :package-settings="packageSettings" + :is-loading="isLoading" + @success="handleSuccess(2)" + @error="handleError(2)" + /> + + <dependency-proxy-settings + v-if="dependencyProxyAvailable" + :dependency-proxy-settings="dependencyProxySettings" + :is-loading="isLoading" + @success="handleSuccess" + @error="handleError" + /> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue new file mode 100644 index 00000000000..b7e88945dbd --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue @@ -0,0 +1,139 @@ +<script> +import { GlSprintf, GlLink } from '@gitlab/ui'; +import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue'; +import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue'; +import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue'; +import { + PACKAGE_SETTINGS_HEADER, + PACKAGE_SETTINGS_DESCRIPTION, + PACKAGES_DOCS_PATH, +} from '~/packages_and_registries/settings/group/constants'; +import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql'; +import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update'; +import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; +import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; + +export default { + name: 'PackageSettings', + i18n: { + PACKAGE_SETTINGS_HEADER, + PACKAGE_SETTINGS_DESCRIPTION, + }, + links: { + PACKAGES_DOCS_PATH, + }, + components: { + GlSprintf, + GlLink, + SettingsBlock, + MavenSettings, + GenericSettings, + DuplicatesSettings, + }, + inject: ['defaultExpanded', 'groupPath'], + props: { + packageSettings: { + type: Object, + required: true, + }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, + }, + data() { + return { + errors: {}, + }; + }, + methods: { + async updateSettings(payload) { + this.errors = {}; + try { + const { data } = await this.$apollo.mutate({ + mutation: updateNamespacePackageSettings, + variables: { + input: { + namespacePath: this.groupPath, + ...payload, + }, + }, + update: updateGroupPackageSettings(this.groupPath), + optimisticResponse: updateGroupPackagesSettingsOptimisticResponse({ + ...this.packageSettings, + ...payload, + }), + }); + + if (data.updateNamespacePackageSettings?.errors?.length > 0) { + throw new Error(); + } else { + this.$emit('success'); + } + } catch (e) { + if (e.graphQLErrors) { + e.graphQLErrors.forEach((error) => { + const [ + { + path: [key], + message, + }, + ] = error.extensions.problems; + this.errors = { ...this.errors, [key]: message }; + }); + } + this.$emit('error'); + } + }, + }, +}; +</script> + +<template> + <settings-block + :default-expanded="defaultExpanded" + data-qa-selector="package_registry_settings_content" + > + <template #title> {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}</template> + <template #description> + <span data-testid="description"> + <gl-sprintf :message="$options.i18n.PACKAGE_SETTINGS_DESCRIPTION"> + <template #link="{ content }"> + <gl-link :href="$options.links.PACKAGES_DOCS_PATH" target="_blank">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </span> + </template> + <template #default> + <maven-settings data-testid="maven-settings"> + <template #default="{ modelNames }"> + <duplicates-settings + :duplicates-allowed="packageSettings.mavenDuplicatesAllowed" + :duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex" + :duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex" + :model-names="modelNames" + :loading="isLoading" + toggle-qa-selector="allow_duplicates_toggle" + label-qa-selector="allow_duplicates_label" + @update="updateSettings" + /> + </template> + </maven-settings> + <generic-settings class="gl-mt-6" data-testid="generic-settings"> + <template #default="{ modelNames }"> + <duplicates-settings + :duplicates-allowed="packageSettings.genericDuplicatesAllowed" + :duplicate-exception-regex="packageSettings.genericDuplicateExceptionRegex" + :duplicate-exception-regex-error="errors.genericDuplicateExceptionRegex" + :model-names="modelNames" + :loading="isLoading" + @update="updateSettings" + /> + </template> + </generic-settings> + </template> + </settings-block> +</template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js index d29489a0b33..ee922457993 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js @@ -18,9 +18,9 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__( 'PackageRegistry|Publish packages if their name or version matches this regex.', ); -export const SUCCESS_UPDATING_SETTINGS = s__('PackageRegistry|Settings saved successfully'); -export const ERROR_UPDATING_SETTINGS = s__( - 'PackageRegistry|An error occurred while saving the settings', +export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy'); +export const DEPENDENCY_PROXY_SETTINGS_DESCRIPTION = s__( + 'DependencyProxy|Create a local proxy for storing frequently used upstream images. %{docLinkStart}Learn more%{docLinkEnd} about dependency proxies.', ); // Parameters @@ -28,3 +28,5 @@ export const ERROR_UPDATING_SETTINGS = s__( export const PACKAGES_DOCS_PATH = helpPagePath('user/packages'); export const MAVEN_DUPLICATES_ALLOWED = 'mavenDuplicatesAllowed'; export const MAVEN_DUPLICATE_EXCEPTION_REGEX = 'mavenDuplicateExceptionRegex'; + +export const DEPENDENCY_PROXY_DOCS_PATH = helpPagePath('user/packages/dependency_proxy/index'); diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql new file mode 100644 index 00000000000..d24a645fecb --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql @@ -0,0 +1,8 @@ +mutation updateDependencyProxySettings($input: UpdateDependencyProxySettingsInput!) { + updateDependencyProxySettings(input: $input) { + dependencyProxySetting { + enabled + } + errors + } +} diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql index a1c01300893..d3edebfbe20 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql @@ -1,5 +1,8 @@ query getGroupPackagesSettings($fullPath: ID!) { group(fullPath: $fullPath) { + dependencyProxySetting { + enabled + } packageSettings { mavenDuplicatesAllowed mavenDuplicateExceptionRegex diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js index fb06f557d66..fe94203f51b 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js @@ -9,9 +9,16 @@ export const updateGroupPackageSettings = (fullPath) => (client, { data: updated const sourceData = client.readQuery(queryAndParams); const data = produce(sourceData, (draftState) => { - draftState.group.packageSettings = { - ...updatedData.updateNamespacePackageSettings.packageSettings, - }; + if (updatedData.updateNamespacePackageSettings) { + draftState.group.packageSettings = { + ...updatedData.updateNamespacePackageSettings.packageSettings, + }; + } + if (updatedData.updateDependencyProxySettings) { + draftState.group.dependencyProxySetting = { + ...updatedData.updateDependencyProxySettings.dependencyProxySetting, + }; + } }); client.writeQuery({ diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js index f2c8de85bf8..a30d8ca0b81 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js @@ -9,3 +9,15 @@ export const updateGroupPackagesSettingsOptimisticResponse = (changes) => ({ }, }, }); + +export const updateGroupDependencyProxySettingsOptimisticResponse = (changes) => ({ + // eslint-disable-next-line @gitlab/require-i18n-strings + __typename: 'Mutation', + updateDependencyProxySettings: { + __typename: 'UpdateDependencyProxySettingsPayload', + errors: [], + dependencyProxySetting: { + ...changes, + }, + }, +}); 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 bf286c84d5f..7be3bba7cae 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 @@ -95,7 +95,7 @@ export default { <gl-sprintf :message=" __( - 'Save space and find images in the container Registry. remove unneeded tags and keep only the ones you want. %{linkStart}How does cleanup work?%{linkEnd}', + 'Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}', ) " > diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js index 165c4aae3cb..4d477fbd05d 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js @@ -73,6 +73,7 @@ export const OLDER_THAN_OPTIONS = [ { key: 'SEVEN_DAYS', variable: 7, default: false }, { key: 'FOURTEEN_DAYS', variable: 14, default: false }, { key: 'THIRTY_DAYS', variable: 30, default: false }, + { key: 'SIXTY_DAYS', variable: 60, default: false }, { key: 'NINETY_DAYS', variable: 90, default: true }, ]; diff --git a/app/assets/javascripts/packages_and_registries/shared/constants.js b/app/assets/javascripts/packages_and_registries/shared/constants.js index 55b5816cc5a..7d2971bd8c7 100644 --- a/app/assets/javascripts/packages_and_registries/shared/constants.js +++ b/app/assets/javascripts/packages_and_registries/shared/constants.js @@ -1 +1,3 @@ export const FILTERED_SEARCH_TERM = 'filtered-search-term'; +export const FILTERED_SEARCH_TYPE = 'type'; +export const HISTORY_PIPELINES_LIMIT = 5; |