diff options
Diffstat (limited to 'app/assets/javascripts/packages_and_registries/package_registry/components/list')
5 files changed, 414 insertions, 145 deletions
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> |