diff options
Diffstat (limited to 'app/assets/javascripts/registry/explorer')
22 files changed, 325 insertions, 179 deletions
diff --git a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue index 8bdf043a106..56d2ff86fb7 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/delete_alert.vue @@ -60,7 +60,7 @@ export default { @dismiss="$emit('change', null)" > <gl-sprintf :message="deleteAlertConfig.message"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="garbageCollectionHelpPagePath" target="_blank"> {{ content }} </gl-link> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue index 3eeb7b29386..ed02aa264ed 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/details_header.vue @@ -1,12 +1,29 @@ <script> import { GlSprintf } from '@gitlab/ui'; -import { sprintf } from '~/locale'; +import { sprintf, n__ } from '~/locale'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; -import { DETAILS_PAGE_TITLE, UPDATED_AT } from '../../constants/index'; +import { + DETAILS_PAGE_TITLE, + UPDATED_AT, + CLEANUP_UNSCHEDULED_TEXT, + CLEANUP_SCHEDULED_TEXT, + CLEANUP_ONGOING_TEXT, + CLEANUP_UNFINISHED_TEXT, + CLEANUP_DISABLED_TEXT, + CLEANUP_SCHEDULED_TOOLTIP, + CLEANUP_ONGOING_TOOLTIP, + CLEANUP_UNFINISHED_TOOLTIP, + CLEANUP_DISABLED_TOOLTIP, + UNFINISHED_STATUS, + UNSCHEDULED_STATUS, + SCHEDULED_STATUS, + ONGOING_STATUS, +} from '../../constants/index'; export default { + name: 'DetailsHeader', components: { GlSprintf, TitleArea, MetadataItem }, mixins: [timeagoMixin], props: { @@ -14,6 +31,11 @@ export default { type: Object, required: true, }, + metadataLoading: { + type: Boolean, + required: false, + default: false, + }, }, computed: { visibilityIcon() { @@ -25,6 +47,24 @@ export default { updatedText() { return sprintf(UPDATED_AT, { time: this.timeAgo }); }, + tagCountText() { + return n__('%d tag', '%d tags', this.image.tagsCount); + }, + cleanupTextAndTooltip() { + if (!this.image.project.containerExpirationPolicy?.enabled) { + return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP }; + } + return { + [UNSCHEDULED_STATUS]: { + text: sprintf(CLEANUP_UNSCHEDULED_TEXT, { + time: this.timeFormatted(this.image.project.containerExpirationPolicy.nextRunAt), + }), + }, + [SCHEDULED_STATUS]: { text: CLEANUP_SCHEDULED_TEXT, tooltip: CLEANUP_SCHEDULED_TOOLTIP }, + [ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP }, + [UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP }, + }[this.image?.expirationPolicyCleanupStatus]; + }, }, i18n: { DETAILS_PAGE_TITLE, @@ -33,7 +73,7 @@ export default { </script> <template> - <title-area> + <title-area :metadata-loading="metadataLoading"> <template #title> <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE"> <template #imageName> @@ -41,6 +81,20 @@ export default { </template> </gl-sprintf> </template> + <template #metadata-tags-count> + <metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" /> + </template> + + <template #metadata-cleanup> + <metadata-item + icon="expire" + :text="cleanupTextAndTooltip.text" + :text-tooltip="cleanupTextAndTooltip.tooltip" + size="xl" + data-testid="cleanup" + /> + </template> + <template #metadata-updated> <metadata-item :icon="visibilityIcon" diff --git a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue b/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue index d13d815a59e..12095655126 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/partial_cleanup_alert.vue @@ -23,12 +23,12 @@ export default { <template> <gl-alert variant="warning" :title="$options.i18n.DELETE_ALERT_TITLE" @dismiss="$emit('dismiss')"> <gl-sprintf :message="$options.i18n.DELETE_ALERT_LINK_TEXT"> - <template #adminLink="{content}"> + <template #adminLink="{ content }"> <gl-link data-testid="run-link" :href="runCleanupPoliciesHelpPagePath" target="_blank">{{ content }}</gl-link> </template> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link data-testid="help-link" :href="cleanupPoliciesHelpPagePath" target="_blank">{{ content }}</gl-link> diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue index ad39a898e7b..1e0736c4a53 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list.vue @@ -4,6 +4,7 @@ import TagsListRow from './tags_list_row.vue'; import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index'; export default { + name: 'TagsList', components: { GlButton, TagsListRow, @@ -31,10 +32,10 @@ export default { }, computed: { hasSelectedItems() { - return this.tags.some(tag => this.selectedItems[tag.name]); + return this.tags.some((tag) => this.selectedItems[tag.name]); }, showMultiDeleteButton() { - return this.tags.some(tag => tag.canDelete) && !this.isMobile; + return this.tags.some((tag) => tag.canDelete) && !this.isMobile; }, }, methods: { diff --git a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue index 3a5cccc7d08..2e4a489f2cb 100644 --- a/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue @@ -140,9 +140,7 @@ export default { <template #left-secondary> <span data-testid="size"> {{ formattedSize }} - <template v-if="formattedSize && layers" - >·</template - > + <template v-if="formattedSize && layers">·</template> {{ layers }} </span> </template> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue index 319666210d6..07ee3c6083b 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/cli_commands.vue @@ -19,8 +19,8 @@ export default { GlDropdown, CodeInstruction, }, - inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'], mixins: [Tracking.mixin({ label: trackingLabel })], + inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'], trackingLabel, i18n: { QUICK_START, diff --git a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue index 26e9fee63af..a68c4de5aa6 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/group_empty_state.vue @@ -3,12 +3,12 @@ import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; export default { name: 'GroupEmptyState', - inject: ['config'], components: { GlEmptyState, GlSprintf, GlLink, }, + inject: ['config'], }; </script> <template> @@ -25,7 +25,7 @@ export default { ) " > - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue index f8b3233438f..10ad99d5956 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list.vue @@ -13,6 +13,11 @@ export default { type: Array, required: true, }, + metadataLoading: { + type: Boolean, + default: false, + required: false, + }, pageInfo: { type: Object, required: true, @@ -33,6 +38,7 @@ export default { :key="index" :item="listItem" :first="index === 0" + :metadata-loading="metadataLoading" @delete="$emit('delete', $event)" /> <div class="gl-display-flex gl-justify-content-center"> diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue index 3fe61dc231a..264a3c27cde 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui'; +import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; import { n__ } from '~/locale'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; @@ -18,13 +18,14 @@ import { } from '../../constants/index'; export default { - name: 'ImageListrow', + name: 'ImageListRow', components: { ClipboardButton, DeleteButton, GlSprintf, GlIcon, ListItem, + GlSkeletonLoader, }, directives: { GlTooltip: GlTooltipDirective, @@ -34,6 +35,11 @@ export default { type: Object, required: true, }, + metadataLoading: { + type: Boolean, + default: false, + required: false, + }, }, i18n: { LIST_DELETE_BUTTON_DISABLED, @@ -107,7 +113,11 @@ export default { /> </template> <template #left-secondary> - <span class="gl-display-flex gl-align-items-center" data-testid="tagsCount"> + <span + v-if="!metadataLoading" + class="gl-display-flex gl-align-items-center" + data-testid="tags-count" + > <gl-icon name="tag" class="gl-mr-2" /> <gl-sprintf :message="tagsCountText"> <template #count> @@ -115,6 +125,13 @@ export default { </template> </gl-sprintf> </span> + + <div v-else class="gl-w-full"> + <gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet"> + <circle cx="6" cy="8" r="6" /> + <rect x="16" y="4" width="100" height="8" rx="4" /> + </gl-skeleton-loader> + </div> </template> <template #right-action> <delete-button diff --git a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue index 5308b025cc0..5aa04419ca0 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/project_empty_state.vue @@ -45,7 +45,7 @@ export default { <template #description> <p> <gl-sprintf :message="$options.i18n.introText"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> </template> </gl-sprintf> @@ -53,10 +53,10 @@ export default { <h5>{{ $options.i18n.quickStart }}</h5> <p> <gl-sprintf :message="$options.i18n.notLoggedInMessage"> - <template #twofaDocLink="{content}"> + <template #twofaDocLink="{ content }"> <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link> </template> - <template #personalAccessTokensDocLink="{content}"> + <template #personalAccessTokensDocLink="{ content }"> <gl-link :href="config.personalAccessTokensHelpLink" target="_blank">{{ content }}</gl-link> @@ -81,7 +81,7 @@ export default { <p class="gl-mb-4"> {{ $options.i18n.addImageText }} </p> - <gl-form-input-group class="gl-mb-4 "> + <gl-form-input-group class="gl-mb-4"> <gl-form-input :value="dockerBuildCommand" readonly diff --git a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue index c2bd01701df..f01e3c9d24a 100644 --- a/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue +++ b/app/assets/javascripts/registry/explorer/components/list_page/registry_header.vue @@ -13,6 +13,7 @@ import { } from '../../constants/index'; export default { + name: 'ListHeader', components: { TitleArea, MetadataItem, @@ -43,6 +44,11 @@ export default { required: false, default: false, }, + metadataLoading: { + type: Boolean, + required: false, + default: false, + }, }, loader: { repeat: 10, @@ -92,7 +98,11 @@ export default { </script> <template> - <title-area :title="$options.i18n.CONTAINER_REGISTRY_TITLE" :info-messages="infoMessages"> + <title-area + :title="$options.i18n.CONTAINER_REGISTRY_TITLE" + :info-messages="infoMessages" + :metadata-loading="metadataLoading" + > <template #right-actions> <slot name="commands"></slot> </template> diff --git a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue index 1cedcc41b2b..e77eda31596 100644 --- a/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue +++ b/app/assets/javascripts/registry/explorer/components/registry_breadcrumb.vue @@ -1,66 +1,51 @@ <script> -/* eslint-disable vue/no-v-html */ -// We are forced to use `v-html` untill this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 -// then we can re-write this to use gl-breadcrumb -import { initial, first, last } from 'lodash'; -import { sanitize } from '~/lib/dompurify'; +// We are using gl-breadcrumb only at the last child of the handwritten breadcrumb +// until this gitlab-ui issue is resolved: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1079 +// +// See the CSS workaround in app/assets/stylesheets/pages/registry.scss when this file is changed. +import { GlBreadcrumb, GlIcon } from '@gitlab/ui'; export default { - props: { - crumbs: { - type: Array, - required: true, - }, + components: { + GlBreadcrumb, + GlIcon, }, computed: { - parsedCrumbs() { - return this.crumbs.map(c => ({ ...c, innerHTML: sanitize(c.innerHTML) })); - }, rootRoute() { - return this.$router.options.routes.find(r => r.meta.root); + return this.$router.options.routes.find((r) => r.meta.root); + }, + detailsRoute() { + return this.$router.options.routes.find((r) => r.name === 'details'); }, isRootRoute() { return this.$route.name === this.rootRoute.name; }, - rootCrumbs() { - return initial(this.parsedCrumbs); - }, - divider() { - const { classList, tagName, innerHTML } = first(this.crumbs).querySelector('svg'); - return { classList: [...classList], tagName, innerHTML: sanitize(innerHTML) }; + isLoaded() { + return this.isRootRoute || this.$store?.state.imageDetails?.name; }, - lastCrumb() { - const { children } = last(this.crumbs); - const { tagName, className } = first(children); - return { - tagName, - className, - text: this.$route.meta.nameGenerator(), - path: { to: this.$route.name }, - }; + allCrumbs() { + const crumbs = [ + { + text: this.rootRoute.meta.nameGenerator(), + to: this.rootRoute.path, + }, + ]; + if (!this.isRootRoute) { + crumbs.push({ + text: this.detailsRoute.meta.nameGenerator(), + href: this.detailsRoute.meta.path, + }); + } + return crumbs; }, }, }; </script> <template> - <ul> - <li - v-for="(crumb, index) in rootCrumbs" - :key="index" - :class="crumb.className" - v-html="crumb.innerHTML" - ></li> - <li v-if="!isRootRoute"> - <router-link ref="rootRouteLink" :to="rootRoute.path"> - {{ rootRoute.meta.nameGenerator() }} - </router-link> - <component :is="divider.tagName" :class="divider.classList" v-html="divider.innerHTML" /> - </li> - <li> - <component :is="lastCrumb.tagName" ref="lastCrumb" :class="lastCrumb.className"> - <router-link ref="childRouteLink" :to="lastCrumb.path">{{ lastCrumb.text }}</router-link> - </component> - </li> - </ul> + <gl-breadcrumb :key="isLoaded" :items="allCrumbs"> + <template #separator> + <gl-icon name="angle-right" :size="8" /> + </template> + </gl-breadcrumb> </template> diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js index 1babaaa93da..b5627352857 100644 --- a/app/assets/javascripts/registry/explorer/constants/details.js +++ b/app/assets/javascripts/registry/explorer/constants/details.js @@ -60,6 +60,22 @@ export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}'); export const NOT_AVAILABLE_TEXT = __('N/A'); export const NOT_AVAILABLE_SIZE = __('0 bytes'); + +export const CLEANUP_UNSCHEDULED_TEXT = s__('ContainerRegistry|Cleanup will run %{time}'); +export const CLEANUP_SCHEDULED_TEXT = s__('ContainerRegistry|Cleanup pending'); +export const CLEANUP_ONGOING_TEXT = s__('ContainerRegistry|Cleanup in progress'); +export const CLEANUP_UNFINISHED_TEXT = s__('ContainerRegistry|Cleanup incomplete'); +export const CLEANUP_DISABLED_TEXT = s__('ContainerRegistry|Cleanup disabled'); + +export const CLEANUP_SCHEDULED_TOOLTIP = s__('ContainerRegistry|Cleanup will run soon'); +export const CLEANUP_ONGOING_TOOLTIP = s__('ContainerRegistry|Cleanup is currently removing tags'); +export const CLEANUP_UNFINISHED_TOOLTIP = s__( + 'ContainerRegistry|Cleanup ran but some tags were not removed', +); +export const CLEANUP_DISABLED_TOOLTIP = s__( + 'ContainerRegistry|Cleanup is disabled for this project', +); + // Parameters export const DEFAULT_PAGE = 1; @@ -76,3 +92,8 @@ export const ALERT_MESSAGES = { [ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE, [ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE, }; + +export const UNFINISHED_STATUS = 'UNFINISHED'; +export const UNSCHEDULED_STATUS = 'UNSCHEDULED'; +export const SCHEDULED_STATUS = 'SCHEDULED'; +export const ONGOING_STATUS = 'ONGOING'; diff --git a/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql b/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql deleted file mode 100644 index 9a3579ee8e0..00000000000 --- a/app/assets/javascripts/registry/explorer/graphql/fragments/container_repository.fragment.graphql +++ /dev/null @@ -1,11 +0,0 @@ -fragment ContainerRepositoryFields on ContainerRepository { - id - name - path - status - location - canDelete - createdAt - tagsCount - expirationPolicyStartedAt -} diff --git a/app/assets/javascripts/registry/explorer/graphql/index.js b/app/assets/javascripts/registry/explorer/graphql/index.js index 16152eb81f6..d934bcc7419 100644 --- a/app/assets/javascripts/registry/explorer/graphql/index.js +++ b/app/assets/javascripts/registry/explorer/graphql/index.js @@ -8,6 +8,7 @@ export const apolloProvider = new VueApollo({ defaultClient: createDefaultClient( {}, { + batchMax: 1, assumeImmutableResults: true, }, ), diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql new file mode 100644 index 00000000000..8b6d778c655 --- /dev/null +++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql @@ -0,0 +1,26 @@ +query getContainerRepositoriesDetails( + $fullPath: ID! + $name: String + $first: Int + $last: Int + $after: String + $before: String + $isGroupPage: Boolean! +) { + project(fullPath: $fullPath) @skip(if: $isGroupPage) { + containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { + nodes { + id + tagsCount + } + } + } + group(fullPath: $fullPath) @include(if: $isGroupPage) { + containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { + nodes { + id + tagsCount + } + } + } +} diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql index b40200e020b..3fd019467ac 100644 --- a/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql +++ b/app/assets/javascripts/registry/explorer/graphql/queries/get_container_repository_details.query.graphql @@ -18,6 +18,7 @@ query getContainerRepositoryDetails( updatedAt tagsCount expirationPolicyStartedAt + expirationPolicyCleanupStatus tags(after: $after, before: $before, first: $first, last: $last) { nodes { digest @@ -36,6 +37,10 @@ query getContainerRepositoryDetails( } project { visibility + containerExpirationPolicy { + enabled + nextRunAt + } } } } diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql deleted file mode 100644 index 348eda97ea7..00000000000 --- a/app/assets/javascripts/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql +++ /dev/null @@ -1,23 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" -#import "../fragments/container_repository.fragment.graphql" - -query getGroupContainerRepositories( - $fullPath: ID! - $name: String - $first: Int - $last: Int - $after: String - $before: String -) { - group(fullPath: $fullPath) { - containerRepositoriesCount - containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { - nodes { - ...ContainerRepositoryFields - } - pageInfo { - ...PageInfo - } - } - } -} diff --git a/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql b/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql deleted file mode 100644 index 338e27745f7..00000000000 --- a/app/assets/javascripts/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql +++ /dev/null @@ -1,23 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" -#import "../fragments/container_repository.fragment.graphql" - -query getProjectContainerRepositories( - $fullPath: ID! - $name: String - $first: Int - $last: Int - $after: String - $before: String -) { - project(fullPath: $fullPath) { - containerRepositoriesCount - containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { - nodes { - ...ContainerRepositoryFields - } - pageInfo { - ...PageInfo - } - } - } -} diff --git a/app/assets/javascripts/registry/explorer/index.js b/app/assets/javascripts/registry/explorer/index.js index d887b6a1b15..a3890ab5c42 100644 --- a/app/assets/javascripts/registry/explorer/index.js +++ b/app/assets/javascripts/registry/explorer/index.js @@ -2,6 +2,7 @@ import Vue from 'vue'; import { GlToast } from '@gitlab/ui'; import Translate from '~/vue_shared/translate'; import { parseBoolean } from '~/lib/utils/common_utils'; +import PerformancePlugin from '~/performance/vue_performance_plugin'; import RegistryExplorer from './pages/index.vue'; import RegistryBreadcrumb from './components/registry_breadcrumb.vue'; import createRouter from './router'; @@ -10,6 +11,17 @@ import { apolloProvider } from './graphql/index'; Vue.use(Translate); Vue.use(GlToast); +Vue.use(PerformancePlugin, { + components: [ + 'RegistryListPage', + 'ListHeader', + 'ImageListRow', + 'RegistryDetailsPage', + 'DetailsHeader', + 'TagsList', + ], +}); + export default () => { const el = document.getElementById('js-container-registry'); @@ -59,16 +71,28 @@ export default () => { }); const attachBreadcrumb = () => { - const breadCrumbEl = document.querySelector('nav .js-breadcrumbs-list'); - const crumbs = [...document.querySelectorAll('.js-breadcrumbs-list li')]; + const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li'); + const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1]; + const crumbs = [breadCrumbEl.querySelector('h2')]; + const nestedBreadcrumbEl = document.createElement('div'); + breadCrumbEl.replaceChild(nestedBreadcrumbEl, breadCrumbEl.querySelector('h2')); return new Vue({ - el: breadCrumbEl, + el: nestedBreadcrumbEl, router, apolloProvider, components: { RegistryBreadcrumb, }, render(createElement) { + // FIXME(@tnir): this is a workaround until the MR gets merged: + // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115 + const parentEl = breadCrumbEl.parentElement.parentElement; + if (parentEl) { + parentEl.classList.remove('breadcrumbs-container'); + parentEl.classList.add('gl-display-flex'); + parentEl.classList.add('w-100'); + } + // End of FIXME(@tnir) return createElement('registry-breadcrumb', { class: breadCrumbEl.className, props: { diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index 540f02d58d4..0894fd6fcfa 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -22,9 +22,11 @@ import { ALERT_DANGER_TAGS, GRAPHQL_PAGE_SIZE, FETCH_IMAGES_LIST_ERROR_MESSAGE, + UNFINISHED_STATUS, } from '../constants/index'; export default { + name: 'RegistryDetailsPage', components: { DeleteAlert, PartialCleanupAlert, @@ -35,11 +37,11 @@ export default { TagsLoader, EmptyTagsState, }, - inject: ['breadCrumbState', 'config'], directives: { GlResizeObserver: GlResizeObserverDirective, }, mixins: [Tracking.mixin()], + inject: ['breadCrumbState', 'config'], apollo: { image: { query: getContainerRepositoryDetailsQuery, @@ -83,7 +85,10 @@ export default { return this.image?.tags?.nodes || []; }, showPartialCleanupWarning() { - return this.image?.expirationPolicyStartedAt && !this.dismissPartialCleanupWarning; + return ( + this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS && + !this.dismissPartialCleanupWarning + ); }, tracking() { return { @@ -97,7 +102,7 @@ export default { }, methods: { deleteTags(toBeDeleted) { - this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]); + this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]); this.track('click_button'); this.$refs.deleteModal.show(); }, @@ -111,7 +116,7 @@ export default { mutation: deleteContainerRepositoryTagsMutation, variables: { id: this.queryVariables.id, - tagNames: itemsToBeDeleted.map(i => i.name), + tagNames: itemsToBeDeleted.map((i) => i.name), }, awaitRefetchQueries: true, refetchQueries: [ @@ -183,7 +188,7 @@ export default { @dismiss="dismissPartialCleanupWarning = true" /> - <details-header :image="image" /> + <details-header :image="image" :metadata-loading="isLoading" /> <tags-loader v-if="isLoading" /> <template v-else> diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index 3192ba82db8..336a997d629 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -9,17 +9,13 @@ import { GlSkeletonLoader, GlSearchBoxByClick, } from '@gitlab/ui'; +import { get } from 'lodash'; +import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; import Tracking from '~/tracking'; import createFlash from '~/flash'; - -import ProjectEmptyState from '../components/list_page/project_empty_state.vue'; -import GroupEmptyState from '../components/list_page/group_empty_state.vue'; import RegistryHeader from '../components/list_page/registry_header.vue'; -import ImageList from '../components/list_page/image_list.vue'; -import CliCommands from '../components/list_page/cli_commands.vue'; -import getProjectContainerRepositoriesQuery from '../graphql/queries/get_project_container_repositories.query.graphql'; -import getGroupContainerRepositoriesQuery from '../graphql/queries/get_group_container_repositories.query.graphql'; +import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql'; import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql'; import { @@ -38,12 +34,25 @@ import { } from '../constants/index'; export default { - name: 'RegistryListApp', + name: 'RegistryListPage', components: { GlEmptyState, - ProjectEmptyState, - GroupEmptyState, - ImageList, + ProjectEmptyState: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/project_empty_state.vue' + ), + GroupEmptyState: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/group_empty_state.vue' + ), + ImageList: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/image_list.vue' + ), + CliCommands: () => + import( + /* webpackChunkName: 'container_registry_components' */ '../components/list_page/cli_commands.vue' + ), GlModal, GlSprintf, GlLink, @@ -51,13 +60,12 @@ export default { GlSkeletonLoader, GlSearchBoxByClick, RegistryHeader, - CliCommands, }, - inject: ['config'], directives: { GlTooltip: GlTooltipDirective, }, mixins: [Tracking.mixin()], + inject: ['config'], loader: { repeat: 10, width: 1000, @@ -74,10 +82,8 @@ export default { EMPTY_RESULT_MESSAGE, }, apollo: { - images: { - query() { - return this.graphQlQuery; - }, + baseImages: { + query: getContainerRepositoriesQuery, variables() { return this.queryVariables; }, @@ -92,10 +98,26 @@ export default { createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); }, }, + additionalDetails: { + skip() { + return !this.fetchAdditionalDetails; + }, + query: getContainerRepositoriesDetails, + variables() { + return this.queryVariables; + }, + update(data) { + return data[this.graphqlResource]?.containerRepositories.nodes; + }, + error() { + createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); + }, + }, }, data() { return { - images: [], + baseImages: [], + additionalDetails: [], pageInfo: {}, containerRepositoriesCount: 0, itemToDelete: {}, @@ -103,21 +125,24 @@ export default { searchValue: null, name: null, mutationLoading: false, + fetchAdditionalDetails: false, }; }, computed: { + images() { + return this.baseImages.map((image, index) => ({ + ...image, + ...get(this.additionalDetails, index, {}), + })); + }, graphqlResource() { return this.config.isGroupPage ? 'group' : 'project'; }, - graphQlQuery() { - return this.config.isGroupPage - ? getGroupContainerRepositoriesQuery - : getProjectContainerRepositoriesQuery; - }, queryVariables() { return { name: this.name, fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, + isGroupPage: this.config.isGroupPage, first: GRAPHQL_PAGE_SIZE, }; }, @@ -127,7 +152,7 @@ export default { }; }, isLoading() { - return this.$apollo.queries.images.loading || this.mutationLoading; + return this.$apollo.queries.baseImages.loading || this.mutationLoading; }, showCommands() { return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); @@ -141,6 +166,13 @@ export default { : DELETE_IMAGE_ERROR_MESSAGE; }, }, + mounted() { + // If the two graphql calls - which are not batched - resolve togheter we will have a race + // condition when apollo sets the cache, with this we give the 'base' call an headstart + setTimeout(() => { + this.fetchAdditionalDetails = true; + }, 200); + }, methods: { deleteImage(item) { this.track('click_button'); @@ -175,30 +207,46 @@ export default { this.deleteAlertType = null; this.itemToDelete = {}; }, - fetchNextPage() { + updateQuery(_, { fetchMoreResult }) { + return fetchMoreResult; + }, + async fetchNextPage() { if (this.pageInfo?.hasNextPage) { - this.$apollo.queries.images.fetchMore({ - variables: { - after: this.pageInfo?.endCursor, - first: GRAPHQL_PAGE_SIZE, - }, - updateQuery(previousResult, { fetchMoreResult }) { - return fetchMoreResult; - }, + const variables = { + after: this.pageInfo?.endCursor, + first: GRAPHQL_PAGE_SIZE, + }; + + this.$apollo.queries.baseImages.fetchMore({ + variables, + updateQuery: this.updateQuery, + }); + + await this.$nextTick(); + + this.$apollo.queries.additionalDetails.fetchMore({ + variables, + updateQuery: this.updateQuery, }); } }, - fetchPreviousPage() { + async fetchPreviousPage() { if (this.pageInfo?.hasPreviousPage) { - this.$apollo.queries.images.fetchMore({ - variables: { - first: null, - before: this.pageInfo?.startCursor, - last: GRAPHQL_PAGE_SIZE, - }, - updateQuery(previousResult, { fetchMoreResult }) { - return fetchMoreResult; - }, + const variables = { + first: null, + before: this.pageInfo?.startCursor, + last: GRAPHQL_PAGE_SIZE, + }; + this.$apollo.queries.baseImages.fetchMore({ + variables, + updateQuery: this.updateQuery, + }); + + await this.$nextTick(); + + this.$apollo.queries.additionalDetails.fetchMore({ + variables, + updateQuery: this.updateQuery, }); } }, @@ -230,7 +278,7 @@ export default { <template #description> <p> <gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE"> - <template #docLink="{content}"> + <template #docLink="{ content }"> <gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank"> {{ content }} </gl-link> @@ -242,6 +290,7 @@ export default { <template v-else> <registry-header + :metadata-loading="isLoading" :images-count="containerRepositoriesCount" :expiration-policy="config.expirationPolicy" :help-page-path="config.helpPagePath" @@ -285,6 +334,7 @@ export default { <image-list v-if="images.length" :images="images" + :metadata-loading="$apollo.queries.additionalDetails.loading" :page-info="pageInfo" @delete="deleteImage" @prev-page="fetchPreviousPage" |