diff options
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
21 files changed, 356 insertions, 119 deletions
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue index bfa99c01c3f..ce221a274c9 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue @@ -60,7 +60,7 @@ export default { return this.$options.i18n[`CLEANUP_STATUS_${this.status}`]; }, calculatedTimeTilNextRun() { - return timeTilRun(this.expirationPolicy?.next_run); + return timeTilRun(this.expirationPolicy?.next_run_at); }, expireIconName() { return this.failedDelete ? 'expire' : 'clock'; @@ -90,9 +90,9 @@ export default { {{ statusText }} </span> <gl-icon - v-if="failedDelete" + v-if="failedDelete && calculatedTimeTilNextRun" :id="iconId" - :size="14" + :size="16" class="gl-text-gray-500" data-testid="extra-info" name="information-o" diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue index aecc0bf92ea..80bca536b7c 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue @@ -95,7 +95,7 @@ export default { if (this.showFullPath) { return this.item.path; } - const projectPath = this.item?.project?.path ?? ''; + const projectPath = this.item?.project?.path?.toLowerCase() ?? ''; if (this.item.name) { return joinPaths(projectPath, this.item.name); } diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue index 1faff1ff4de..45dc217b9e3 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue @@ -39,7 +39,7 @@ export default { directives: { GlModalDirective, }, - inject: ['groupPath', 'groupId', 'noManifestsIllustration'], + inject: ['groupPath', 'groupId', 'noManifestsIllustration', 'canClearCache'], i18n: { proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'), copyImagePrefixText: s__('DependencyProxy|Copy prefix'), @@ -114,7 +114,7 @@ export default { ); }, showDeleteDropdown() { - return this.group.dependencyProxyManifests?.nodes.length > 0; + return this.group.dependencyProxyManifests?.nodes.length > 0 && this.canClearCache; }, showDependencyProxyImagePrefix() { return this.group.dependencyProxyImagePrefix?.length > 0; diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js index 14789aafdb7..428d6d6cd75 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js @@ -1,4 +1,5 @@ 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'; @@ -10,12 +11,15 @@ export const initDependencyProxyApp = () => { if (!el) { return null; } - const { ...dataset } = el.dataset; + const { groupPath, groupId, noManifestsIllustration, canClearCache } = el.dataset; return new Vue({ el, apolloProvider, provide: { - ...dataset, + groupPath, + groupId, + noManifestsIllustration, + canClearCache: parseBoolean(canClearCache), }, render(createElement) { return createElement(app); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js index 408d34fbe93..51a38c434cb 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js @@ -26,8 +26,7 @@ export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { export const requestPackagesList = ({ dispatch, state }, params = {}) => { dispatch('setLoading', true); - // eslint-disable-next-line camelcase - const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params; + const { page = DEFAULT_PAGE, per_page: perPage = DEFAULT_PAGE_SIZE } = params; const { sort, orderBy } = state.sorting; const type = state.config.forceTerraform ? TERRAFORM_SEARCH_TYPE @@ -38,7 +37,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => { const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages'; return Api[apiMethod](state.config.resourceId, { - params: { page, per_page, sort, order_by: orderBy, ...packageFilters }, + params: { page, per_page: perPage, sort, order_by: orderBy, ...packageFilters }, }) .then(({ data, headers }) => { dispatch('receivePackagesListSuccess', { data, headers }); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue index a049b0eff8d..b872294d2cf 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue @@ -1,14 +1,16 @@ <script> -import { GlLink, GlTableLite, GlDropdownItem, GlDropdown, GlIcon, GlButton } from '@gitlab/ui'; +import { GlLink, GlTable, GlDropdownItem, GlDropdown, GlButton, GlFormCheckbox } from '@gitlab/ui'; import { last } from 'lodash'; import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { __ } from '~/locale'; +import { __, s__ } from '~/locale'; import FileSha from '~/packages_and_registries/package_registry/components/details/file_sha.vue'; import Tracking from '~/tracking'; import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { + REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION, + SELECT_PACKAGE_FILE_TRACKING_ACTION, TRACKING_LABEL_PACKAGE_ASSET, TRACKING_ACTION_EXPAND_PACKAGE_ASSET, } from '~/packages_and_registries/package_registry/constants'; @@ -17,10 +19,10 @@ export default { name: 'PackageFiles', components: { GlLink, - GlTableLite, - GlIcon, + GlTable, GlDropdown, GlDropdownItem, + GlFormCheckbox, GlButton, FileIcon, TimeAgoTooltip, @@ -33,13 +35,29 @@ export default { required: false, default: false, }, + isLoading: { + type: Boolean, + required: false, + default: false, + }, packageFiles: { type: Array, required: false, default: () => [], }, }, + data() { + return { + selectedReferences: [], + }; + }, computed: { + areFilesSelected() { + return this.selectedReferences.length > 0; + }, + areAllFilesSelected() { + return this.packageFiles.every(this.isSelected); + }, filesTableRows() { return this.packageFiles.map((pf) => ({ ...pf, @@ -47,6 +65,9 @@ export default { pipeline: last(pf.pipelines), })); }, + hasSelectedSomeFiles() { + return this.areFilesSelected && !this.areAllFilesSelected; + }, showCommitColumn() { // note that this is always false for now since we do not return // pipelines associated to files for performance concerns @@ -55,6 +76,12 @@ export default { filesTableHeaderFields() { return [ { + key: 'checkbox', + label: __('Select all'), + class: 'gl-w-4', + hide: !this.canDelete, + }, + { key: 'name', label: __('Name'), }, @@ -77,7 +104,7 @@ export default { label: '', hide: !this.canDelete, class: 'gl-text-right', - tdClass: 'gl-w-4', + tdClass: 'gl-w-4 gl-pt-3!', }, ].filter((c) => !c.hide); }, @@ -99,21 +126,71 @@ export default { this.track(TRACKING_ACTION_EXPAND_PACKAGE_ASSET, { label: TRACKING_LABEL_PACKAGE_ASSET }); } }, + updateSelectedReferences(selection) { + this.track(SELECT_PACKAGE_FILE_TRACKING_ACTION); + this.selectedReferences = selection; + }, + isSelected(packageFile) { + return this.selectedReferences.find((reference) => reference.id === packageFile.id); + }, + handleFileDeleteSelected() { + this.track(REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION); + this.$emit('delete-files', this.selectedReferences); + }, }, i18n: { deleteFile: __('Delete file'), + deleteSelected: s__('PackageRegistry|Delete selected'), + moreActionsText: __('More actions'), }, }; </script> <template> - <div> - <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> - <gl-table-lite + <div class="gl-pt-6"> + <div class="gl-display-flex gl-align-items-center gl-justify-content-space-between"> + <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3> + <gl-button + v-if="canDelete" + :disabled="isLoading || !areFilesSelected" + category="secondary" + variant="danger" + data-testid="delete-selected" + @click="handleFileDeleteSelected" + > + {{ $options.i18n.deleteSelected }} + </gl-button> + </div> + <gl-table :fields="filesTableHeaderFields" :items="filesTableRows" + show-empty + selectable + select-mode="multi" + selected-variant="primary" :tbody-tr-attr="{ 'data-testid': 'file-row' }" + @row-selected="updateSelectedReferences" > + <template #head(checkbox)="{ selectAllRows, clearSelected }"> + <gl-form-checkbox + v-if="canDelete" + data-testid="package-files-checkbox-all" + :checked="areAllFilesSelected" + :indeterminate="hasSelectedSomeFiles" + @change="areAllFilesSelected ? clearSelected() : selectAllRows()" + /> + </template> + + <template #cell(checkbox)="{ rowSelected, selectRow, unselectRow }"> + <gl-form-checkbox + v-if="canDelete" + class="gl-mt-1" + :checked="rowSelected" + data-testid="package-files-checkbox" + @change="rowSelected ? unselectRow() : selectRow()" + /> + </template> + <template #cell(name)="{ item, toggleDetails, detailsShowing }"> <gl-button v-if="hasDetails(item)" @@ -156,11 +233,15 @@ export default { </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)"> + <gl-dropdown + category="tertiary" + icon="ellipsis_v" + :text-sr-only="true" + :text="$options.i18n.moreActionsText" + no-caret + right + > + <gl-dropdown-item data-testid="delete-file" @click="$emit('delete-files', [item])"> {{ $options.i18n.deleteFile }} </gl-dropdown-item> </gl-dropdown> @@ -180,6 +261,6 @@ export default { <file-sha v-if="item.fileSha1" data-testid="sha-1" title="SHA-1" :sha="item.fileSha1" /> </div> </template> - </gl-table-lite> + </gl-table> </div> </template> 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 96b82a20364..a1fc7563de1 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 @@ -5,12 +5,17 @@ import { first } from 'lodash'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { truncateSha } from '~/lib/utils/text_utility'; import { s__, n__ } from '~/locale'; +import Tracking from '~/tracking'; +import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils'; 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'; import { GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE, FETCH_PACKAGE_PIPELINES_ERROR_MESSAGE, + TRACKING_ACTION_CLICK_PIPELINE_LINK, + TRACKING_ACTION_CLICK_COMMIT_LINK, + TRACKING_LABEL_PACKAGE_HISTORY, } from '../../constants'; import getPackagePipelinesQuery from '../../graphql/queries/get_package_pipelines.query.graphql'; import PackageHistoryLoader from './package_history_loader.vue'; @@ -37,6 +42,9 @@ export default { PackageHistoryLoader, TimeAgoTooltip, }, + mixins: [Tracking.mixin()], + TRACKING_ACTION_CLICK_PIPELINE_LINK, + TRACKING_ACTION_CLICK_COMMIT_LINK, props: { packageEntity: { type: Object, @@ -97,6 +105,11 @@ export default { first: GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE, }; }, + tracking() { + return { + category: packageTypeToTrackCategory(this.packageType), + }; + }, }, methods: { truncate(value) { @@ -105,6 +118,12 @@ export default { convertToBaseId(value) { return getIdFromGraphQLId(value); }, + trackPipelineClick() { + this.track(TRACKING_ACTION_CLICK_PIPELINE_LINK, { label: TRACKING_LABEL_PACKAGE_HISTORY }); + }, + trackCommitClick() { + this.track(TRACKING_ACTION_CLICK_COMMIT_LINK, { label: TRACKING_LABEL_PACKAGE_HISTORY }); + }, }, }; </script> @@ -140,7 +159,9 @@ export default { <history-item icon="commit" data-testid="first-pipeline-commit"> <gl-sprintf :message="$options.i18n.createdByCommitText"> <template #link> - <gl-link :href="firstPipeline.commitPath">#{{ truncate(firstPipeline.sha) }}</gl-link> + <gl-link :href="firstPipeline.commitPath" @click="trackCommitClick" + >#{{ truncate(firstPipeline.sha) }}</gl-link + > </template> <template #branch> <strong>{{ firstPipeline.ref }}</strong> @@ -150,7 +171,9 @@ export default { <history-item icon="pipeline" data-testid="first-pipeline-pipeline"> <gl-sprintf :message="$options.i18n.createdByPipelineText"> <template #link> - <gl-link :href="firstPipeline.path">#{{ convertToBaseId(firstPipeline.id) }}</gl-link> + <gl-link :href="firstPipeline.path" @click="trackPipelineClick" + >#{{ convertToBaseId(firstPipeline.id) }}</gl-link + > </template> <template #datetime> <time-ago-tooltip :time="firstPipeline.createdAt" /> @@ -189,13 +212,17 @@ export default { > <gl-sprintf :message="$options.i18n.combinedUpdateText"> <template #link> - <gl-link :href="pipeline.commitPath">#{{ truncate(pipeline.sha) }}</gl-link> + <gl-link :href="pipeline.commitPath" @click="trackCommitClick" + >#{{ truncate(pipeline.sha) }}</gl-link + > </template> <template #branch> <strong>{{ pipeline.ref }}</strong> </template> <template #pipeline> - <gl-link :href="pipeline.path">#{{ convertToBaseId(pipeline.id) }}</gl-link> + <gl-link :href="pipeline.path" @click="trackPipelineClick" + >#{{ convertToBaseId(pipeline.id) }}</gl-link + > </template> <template #datetime> <time-ago-tooltip :time="pipeline.createdAt" /> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue index f5946797626..11fd0db3106 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue @@ -23,6 +23,7 @@ export default { directives: { GlResizeObserver: GlResizeObserverDirective, }, + inject: ['isGroupPage'], i18n: { packageInfo: __('v%{version} published %{timeAgo}'), }, @@ -65,9 +66,6 @@ export default { this.checkBreakpoints(); }, methods: { - dynamicSlotName(index) { - return `metadata-tag${index}`; - }, checkBreakpoints() { this.isDesktop = GlBreakpointInstance.isDesktop(); }, @@ -83,21 +81,38 @@ export default { data-qa-selector="package_title" > <template #sub-header> - <span data-testid="sub-header"> + <div data-testid="sub-header" class="gl-display-flex gl-gap-3"> <gl-sprintf :message="$options.i18n.packageInfo"> <template #version> {{ packageEntity.version }} </template> <template #timeAgo> - <time-ago-tooltip - v-if="packageEntity.createdAt" - class="gl-ml-2" - :time="packageEntity.createdAt" - /> + <time-ago-tooltip v-if="packageEntity.createdAt" :time="packageEntity.createdAt" /> </template> </gl-sprintf> - </span> + + <package-tags + v-if="isDesktop && hasTagsToDisplay" + :tag-display-limit="2" + :tags="packageEntity.tags.nodes" + hide-label + /> + + <!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap --> + <template v-else-if="hasTagsToDisplay"> + <gl-badge + v-for="(tag, index) in packageEntity.tags.nodes" + :key="index" + class="gl-my-1" + data-testid="tag-badge" + variant="info" + size="sm" + > + {{ tag.name }} + </gl-badge> + </template> + </div> </template> <template v-if="packageTypeDisplay" #metadata-type> @@ -108,7 +123,7 @@ export default { <metadata-item data-testid="package-size" icon="disk" :text="totalSize" /> </template> - <template v-if="packagePipeline" #metadata-pipeline> + <template v-if="isGroupPage && packagePipeline" #metadata-pipeline> <metadata-item data-testid="pipeline-project" icon="review-list" @@ -121,21 +136,6 @@ export default { <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" /> </template> - <template v-if="isDesktop && hasTagsToDisplay" #metadata-tags> - <package-tags :tag-display-limit="2" :tags="packageEntity.tags.nodes" hide-label /> - </template> - - <!-- we need to duplicate the package tags on mobile to ensure proper styling inside the flex wrap --> - <template - v-for="(tag, index) in packageEntity.tags.nodes" - v-else-if="hasTagsToDisplay" - #[dynamicSlotName(index)] - > - <gl-badge :key="index" class="gl-my-1" data-testid="tag-badge" variant="info" size="sm"> - {{ tag.name }} - </gl-badge> - </template> - <template #right-actions> <slot name="delete-button"></slot> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue index a126d30f1ec..dd58f28a262 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue @@ -1,9 +1,10 @@ <script> -import { GlLink, GlSprintf } from '@gitlab/ui'; +import { GlFormGroup, GlLink, GlSprintf } from '@gitlab/ui'; import { s__ } from '~/locale'; import InstallationTitle from '~/packages_and_registries/package_registry/components/details/installation_title.vue'; import { + PERSONAL_ACCESS_TOKEN_HELP_URL, TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND, TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND, TRACKING_LABEL_CODE_INSTRUCTION, @@ -16,6 +17,7 @@ export default { components: { InstallationTitle, CodeInstruction, + GlFormGroup, GlLink, GlSprintf, }, @@ -43,6 +45,7 @@ password = <your personal access token>`; TRACKING_LABEL_CODE_INSTRUCTION, }, i18n: { + tokenText: s__(`PackageRegistry|You will need a %{linkStart}personal access token%{linkEnd}.`), setupText: s__( `PackageRegistry|If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file.`, ), @@ -50,7 +53,10 @@ password = <your personal access token>`; 'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.', ), }, - links: { PYPI_HELP_PATH }, + links: { + PERSONAL_ACCESS_TOKEN_HELP_URL, + PYPI_HELP_PATH, + }, installOptions: [{ value: 'pypi', label: s__('PackageRegistry|Show PyPi commands') }], }; </script> @@ -59,14 +65,28 @@ password = <your personal access token>`; <div> <installation-title package-type="pypi" :options="$options.installOptions" /> - <code-instruction - :label="s__('PackageRegistry|Pip Command')" - :instruction="pypiPipCommand" - :copy-text="s__('PackageRegistry|Copy Pip command')" - data-testid="pip-command" - :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND" - :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" - /> + <gl-form-group id="installation-pip-command-group"> + <code-instruction + id="installation-pip-command" + :label="s__('PackageRegistry|Pip Command')" + :instruction="pypiPipCommand" + :copy-text="s__('PackageRegistry|Copy Pip command')" + data-testid="pip-command" + :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND" + :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" + /> + <template #description> + <gl-sprintf :message="$options.i18n.tokenText"> + <template #link="{ content }"> + <gl-link + :href="$options.links.PERSONAL_ACCESS_TOKEN_HELP_URL" + data-testid="access-token-link" + >{{ content }}</gl-link + > + </template> + </gl-sprintf> + </template> + </gl-form-group> <h3 class="gl-font-lg">{{ __('Registry setup') }}</h3> <p> @@ -87,7 +107,12 @@ password = <your personal access token>`; /> <gl-sprintf :message="$options.i18n.helpText"> <template #link="{ content }"> - <gl-link :href="$options.links.PYPI_HELP_PATH" target="_blank">{{ content }}</gl-link> + <gl-link + :href="$options.links.PYPI_HELP_PATH" + target="_blank" + data-testid="pypi-docs-link" + >{{ content }}</gl-link + > </template> </gl-sprintf> </div> 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 cea053992f8..5b2a347a4ee 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -7,9 +7,12 @@ export { CANCEL_DELETE_PACKAGE_TRACKING_ACTION, PULL_PACKAGE_TRACKING_ACTION, DELETE_PACKAGE_FILE_TRACKING_ACTION, + DELETE_PACKAGE_FILES_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, + SELECT_PACKAGE_FILE_TRACKING_ACTION, } from '~/packages_and_registries/shared/constants'; export const PACKAGE_TYPE_CONAN = 'CONAN'; @@ -69,6 +72,11 @@ export const TRACKING_ACTION_DOWNLOAD_PACKAGE_ASSET = 'download_package_asset'; export const TRACKING_ACTION_EXPAND_PACKAGE_ASSET = 'expand_package_asset'; export const TRACKING_ACTION_COPY_PACKAGE_ASSET_SHA = 'copy_package_asset_sha'; +export const TRACKING_ACTION_CLICK_PIPELINE_LINK = 'click_pipeline_link_from_package'; +export const TRACKING_ACTION_CLICK_COMMIT_LINK = 'click_commit_link_from_package'; + +export const TRACKING_LABEL_PACKAGE_HISTORY = 'package_history'; + export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert'; export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while deleting the package file.', @@ -76,6 +84,12 @@ export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__( export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__( 'PackageRegistry|Package file deleted successfully', ); +export const DELETE_PACKAGE_FILES_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while deleting the package assets.', +); +export const DELETE_PACKAGE_FILES_SUCCESS_MESSAGE = s__( + 'PackageRegistry|Package assets deleted successfully', +); export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__( 'PackageRegistry|Failed to load the package data', ); @@ -162,5 +176,6 @@ export const CONAN_HELP_PATH = helpPagePath('user/packages/conan_repository/inde export const NUGET_HELP_PATH = helpPagePath('user/packages/nuget_repository/index'); export const PYPI_HELP_PATH = helpPagePath('user/packages/pypi_repository/index'); export const COMPOSER_HELP_PATH = helpPagePath('user/packages/composer_repository/index'); +export const PERSONAL_ACCESS_TOKEN_HELP_URL = helpPagePath('user/profile/personal_access_tokens'); export const GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE = 10; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql deleted file mode 100644 index f016640f57d..00000000000 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql +++ /dev/null @@ -1,5 +0,0 @@ -mutation destroyPackageFile($id: PackagesPackageFileID!) { - destroyPackageFile(input: { id: $id }) { - errors - } -} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql new file mode 100644 index 00000000000..8f9a3156492 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql @@ -0,0 +1,5 @@ +mutation destroyPackageFiles($projectPath: ID!, $ids: [PackagesPackageFileID!]!) { + destroyPackageFiles(input: { projectPath: $projectPath, ids: $ids }) { + errors + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 5574020c9e4..f3f0d096d10 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -20,6 +20,7 @@ query getPackageDetails($id: PackagesPackageID!) { id path name + fullPath } tags(first: 10) { nodes { @@ -39,6 +40,9 @@ query getPackageDetails($id: PackagesPackageID!) { } } packageFiles(first: 100) { + pageInfo { + hasNextPage + } nodes { id fileMd5 diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue index 29438fba86b..e83962bb608 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue @@ -34,16 +34,19 @@ import { REQUEST_DELETE_PACKAGE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_TRACKING_ACTION, DELETE_PACKAGE_FILE_TRACKING_ACTION, + DELETE_PACKAGE_FILES_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, SHOW_DELETE_SUCCESS_ALERT, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILES_ERROR_MESSAGE, + DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, } from '~/packages_and_registries/package_registry/constants'; -import destroyPackageFileMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_file.mutation.graphql'; +import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import Tracking from '~/tracking'; @@ -83,7 +86,8 @@ export default { }, data() { return { - fileToDelete: null, + filesToDelete: [], + mutationLoading: false, packageEntity: {}, }; }, @@ -114,6 +118,9 @@ export default { projectName() { return this.packageEntity.project?.name; }, + projectPath() { + return this.packageEntity.project?.fullPath; + }, packageId() { return this.$route.params.id; }, @@ -131,6 +138,9 @@ export default { isLoading() { return this.$apollo.queries.packageEntity.loading; }, + packageFilesLoading() { + return this.isLoading || this.mutationLoading; + }, isValidPackage() { return this.isLoading || Boolean(this.packageEntity.name); }, @@ -175,12 +185,14 @@ export default { window.location.replace(`${returnTo}?${modalQuery}`); }, - async deletePackageFile(id) { + async deletePackageFiles(ids) { + this.mutationLoading = true; try { const { data } = await this.$apollo.mutate({ - mutation: destroyPackageFileMutation, + mutation: destroyPackageFilesMutation, variables: { - id, + projectPath: this.projectPath, + ids, }, awaitRefetchQueries: true, refetchQueries: [ @@ -190,31 +202,53 @@ export default { }, ], }); - if (data?.destroyPackageFile?.errors[0]) { - throw data.destroyPackageFile.errors[0]; + if (data?.destroyPackageFiles?.errors[0]) { + throw data.destroyPackageFiles.errors[0]; } createFlash({ - message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + message: + ids.length === 1 + ? DELETE_PACKAGE_FILE_SUCCESS_MESSAGE + : DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, type: 'success', }); } catch (error) { createFlash({ - message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + message: + ids.length === 1 + ? DELETE_PACKAGE_FILE_ERROR_MESSAGE + : DELETE_PACKAGE_FILES_ERROR_MESSAGE, type: 'warning', captureError: true, error, }); } + this.mutationLoading = false; }, - handleFileDelete(file) { + handleFileDelete(files) { this.track(REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION); - this.fileToDelete = { ...file }; - this.$refs.deleteFileModal.show(); + if ( + files.length === this.packageFiles.length && + !this.packageEntity.packageFiles?.pageInfo?.hasNextPage + ) { + this.$refs.deleteModal.show(); + } else { + this.filesToDelete = files; + if (files.length === 1) { + this.$refs.deleteFileModal.show(); + } else if (files.length > 1) { + this.$refs.deleteFilesModal.show(); + } + } }, - confirmFileDelete() { - this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); - this.deletePackageFile(this.fileToDelete.id); - this.fileToDelete = null; + confirmFilesDelete() { + if (this.filesToDelete.length === 1) { + this.track(DELETE_PACKAGE_FILE_TRACKING_ACTION); + } else { + this.track(DELETE_PACKAGE_FILES_TRACKING_ACTION); + } + this.deletePackageFiles(this.filesToDelete.map((file) => file.id)); + this.filesToDelete = []; }, }, i18n: { @@ -240,6 +274,10 @@ export default { text: __('Delete'), attributes: [{ variant: 'danger' }, { category: 'primary' }], }, + filesDeletePrimaryAction: { + text: s__('PackageRegistry|Permanently delete assets'), + attributes: [{ variant: 'danger' }, { category: 'primary' }], + }, cancelAction: { text: __('Cancel'), }, @@ -287,9 +325,10 @@ export default { <package-files v-if="showFiles" :can-delete="packageEntity.canDestroy" + :is-loading="packageFilesLoading" :package-files="packageFiles" @download-file="track($options.trackingActions.DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION)" - @delete-file="handleFileDelete" + @delete-files="handleFileDelete" /> </gl-tab> @@ -355,15 +394,43 @@ export default { :action-primary="$options.modal.fileDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" data-testid="delete-file-modal" - @primary="confirmFileDelete" + @primary="confirmFilesDelete" @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" > <template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template> - <gl-sprintf v-if="fileToDelete" :message="$options.i18n.deleteFileModalContent"> + <gl-sprintf v-if="filesToDelete.length === 1" :message="$options.i18n.deleteFileModalContent"> <template #filename> - <strong>{{ fileToDelete.file_name }}</strong> + <strong>{{ filesToDelete[0].fileName }}</strong> </template> </gl-sprintf> </gl-modal> + + <gl-modal + ref="deleteFilesModal" + size="sm" + modal-id="delete-files-modal" + :action-primary="$options.modal.filesDeletePrimaryAction" + :action-cancel="$options.modal.cancelAction" + data-testid="delete-files-modal" + @primary="confirmFilesDelete" + @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)" + > + <template #modal-title>{{ + n__( + `PackageRegistry|Delete 1 asset`, + `PackageRegistry|Delete %d assets`, + filesToDelete.length, + ) + }}</template> + <span v-if="filesToDelete.length > 0"> + {{ + n__( + `PackageRegistry|You are about to delete 1 asset. This operation is irreversible.`, + `PackageRegistry|You are about to delete %d assets. This operation is irreversible.`, + filesToDelete.length, + ) + }} + </span> + </gl-modal> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue index 90a18d5cf5a..1c44d2bc38b 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue @@ -11,7 +11,7 @@ import { UNAVAILABLE_ADMIN_FEATURE_TEXT, } from '~/packages_and_registries/settings/project/constants'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; -import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue'; diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue index 7682754fdcb..f06e3a41bd0 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue @@ -35,22 +35,34 @@ export default { required: false, default: '', }, + dropdownClass: { + type: String, + required: false, + default: '', + }, }, }; </script> <template> <gl-form-group :id="`${name}-form-group`" :label-for="name" :label="label"> - <gl-form-select :id="name" :value="value" :disabled="disabled" @input="$emit('input', $event)"> - <option - v-for="option in formOptions" - :key="option.key" - :value="option.key" - data-testid="option" + <div :class="dropdownClass"> + <gl-form-select + :id="name" + :value="value" + :disabled="disabled" + @input="$emit('input', $event)" > - {{ option.label }} - </option> - </gl-form-select> + <option + v-for="option in formOptions" + :key="option.key" + :value="option.key" + data-testid="option" + > + {{ option.label }} + </option> + </gl-form-select> + </div> <template v-if="description" #description> <span data-testid="description" class="gl-text-gray-400"> {{ description }} diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue index 1170407a349..2f4bc35e5f7 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy.vue @@ -6,7 +6,7 @@ import { PACKAGES_CLEANUP_POLICY_DESCRIPTION, } from '~/packages_and_registries/settings/project/constants'; import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql'; -import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue'; import PackagesCleanupPolicyForm from './packages_cleanup_policy_form.vue'; diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue index b1751d5174a..f1f0b970b15 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_cleanup_policy_form.vue @@ -3,10 +3,10 @@ import { GlButton } from '@gitlab/ui'; import { UPDATE_SETTINGS_ERROR_MESSAGE, UPDATE_SETTINGS_SUCCESS_MESSAGE, - SET_CLEANUP_POLICY_BUTTON, KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION, KEEP_N_DUPLICATED_PACKAGE_FILES_FIELDNAME, KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL, + SET_CLEANUP_POLICY_BUTTON, } from '~/packages_and_registries/settings/project/constants'; import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql'; import { formOptionsGenerator } from '~/packages_and_registries/settings/project/utils'; @@ -108,18 +108,17 @@ export default { <template> <form ref="form-element" @submit.prevent="submit"> - <div class="gl-md-max-w-50p"> - <expiration-dropdown - v-model="prefilledForm.keepNDuplicatedPackageFiles" - :disabled="isFieldDisabled" - :form-options="$options.formOptions.keepNDuplicatedPackageFiles" - :label="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL" - :description="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION" - name="keep-n-duplicated-package-files" - data-testid="keep-n-duplicated-package-files-dropdown" - @input="onModelChange($event, 'keepNDuplicatedPackageFiles')" - /> - </div> + <expiration-dropdown + :value="prefilledForm.keepNDuplicatedPackageFiles" + :disabled="isFieldDisabled" + :form-options="$options.formOptions.keepNDuplicatedPackageFiles" + :label="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL" + :description="$options.i18n.KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION" + dropdown-class="gl-md-max-w-50p gl-sm-pr-5" + name="keep-n-duplicated-package-files" + data-testid="keep-n-duplicated-package-files-dropdown" + @input="onModelChange($event, 'keepNDuplicatedPackageFiles')" + /> <div class="gl-mt-7 gl-display-flex gl-align-items-center"> <gl-button data-testid="save-button" 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 948520151ce..fcb4a8ee297 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js @@ -4,7 +4,7 @@ export const CONTAINER_CLEANUP_POLICY_TITLE = s__(`ContainerRegistry|Clean up im export const CONTAINER_CLEANUP_POLICY_DESCRIPTION = s__( `ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}`, ); -export const SET_CLEANUP_POLICY_BUTTON = __('Save'); +export const SET_CLEANUP_POLICY_BUTTON = __('Save changes'); export const UNAVAILABLE_FEATURE_TITLE = s__( `ContainerRegistry|Cleanup policy for tags is disabled`, ); diff --git a/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue b/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue index 5caf95cd050..0458b914b58 100644 --- a/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue +++ b/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue @@ -1,7 +1,7 @@ <template> <section class="settings gl-py-7"> - <div class="gl-lg-display-flex"> - <div class="gl-lg-w-half gl-pr-10"> + <div class="gl-lg-display-flex gl-gap-6"> + <div class="gl-lg-w-40p gl-pr-10 gl-flex-shrink-0"> <h4> <slot name="title"></slot> </h4> @@ -9,7 +9,7 @@ <slot name="description"></slot> </p> </div> - <div class="gl-lg-w-half gl-pt-3"> + <div class="gl-pt-3 gl-flex-grow-1"> <slot></slot> </div> </div> diff --git a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js index 5505205cf33..6744e821565 100644 --- a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js +++ b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js @@ -9,7 +9,11 @@ export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package'; export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package'; export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package'; export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file'; +export const DELETE_PACKAGE_FILES_TRACKING_ACTION = 'delete_package_files'; +export const SELECT_PACKAGE_FILE_TRACKING_ACTION = 'select_package_file'; export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file'; +export const REQUEST_DELETE_SELECTED_PACKAGE_FILE_TRACKING_ACTION = + 'request_delete_selected_package_file'; export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file'; export const DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION = 'download_package_asset'; |