Commit d3256894 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '280847-reduce-lcp-in-container-registry-index-page' into 'master'

Defer tagsCount & add startup.js to container registry

See merge request gitlab-org/gitlab!50147
parents f20ee83e 836b50eb
...@@ -13,6 +13,11 @@ export default { ...@@ -13,6 +13,11 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
metadataLoading: {
type: Boolean,
default: false,
required: false,
},
pageInfo: { pageInfo: {
type: Object, type: Object,
required: true, required: true,
...@@ -33,6 +38,7 @@ export default { ...@@ -33,6 +38,7 @@ export default {
:key="index" :key="index"
:item="listItem" :item="listItem"
:first="index === 0" :first="index === 0"
:metadata-loading="metadataLoading"
@delete="$emit('delete', $event)" @delete="$emit('delete', $event)"
/> />
<div class="gl-display-flex gl-justify-content-center"> <div class="gl-display-flex gl-justify-content-center">
......
<script> <script>
import { GlTooltipDirective, GlIcon, GlSprintf } from '@gitlab/ui'; import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { n__ } from '~/locale'; import { n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
...@@ -25,6 +25,7 @@ export default { ...@@ -25,6 +25,7 @@ export default {
GlSprintf, GlSprintf,
GlIcon, GlIcon,
ListItem, ListItem,
GlSkeletonLoader,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -34,6 +35,11 @@ export default { ...@@ -34,6 +35,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
metadataLoading: {
type: Boolean,
default: false,
required: false,
},
}, },
i18n: { i18n: {
LIST_DELETE_BUTTON_DISABLED, LIST_DELETE_BUTTON_DISABLED,
...@@ -107,7 +113,11 @@ export default { ...@@ -107,7 +113,11 @@ export default {
/> />
</template> </template>
<template #left-secondary> <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-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText"> <gl-sprintf :message="tagsCountText">
<template #count> <template #count>
...@@ -115,6 +125,13 @@ export default { ...@@ -115,6 +125,13 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</span> </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>
<template #right-action> <template #right-action>
<delete-button <delete-button
......
fragment ContainerRepositoryFields on ContainerRepository {
id
name
path
status
location
canDelete
createdAt
tagsCount
expirationPolicyStartedAt
}
...@@ -8,6 +8,7 @@ export const apolloProvider = new VueApollo({ ...@@ -8,6 +8,7 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient( defaultClient: createDefaultClient(
{}, {},
{ {
batchMax: 1,
assumeImmutableResults: true, assumeImmutableResults: true,
}, },
), ),
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" query getContainerRepositoriesDetails(
#import "../fragments/container_repository.fragment.graphql"
query getGroupContainerRepositories(
$fullPath: ID! $fullPath: ID!
$name: String $name: String
$first: Int $first: Int
$last: Int $last: Int
$after: String $after: String
$before: String $before: String
$isGroupPage: Boolean!
) { ) {
group(fullPath: $fullPath) { project(fullPath: $fullPath) @skip(if: $isGroupPage) {
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
nodes { nodes {
...ContainerRepositoryFields id
tagsCount
} }
pageInfo { }
...PageInfo }
group(fullPath: $fullPath) @include(if: $isGroupPage) {
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
nodes {
id
tagsCount
} }
} }
} }
......
#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
}
}
}
}
...@@ -9,17 +9,13 @@ import { ...@@ -9,17 +9,13 @@ import {
GlSkeletonLoader, GlSkeletonLoader,
GlSearchBoxByClick, GlSearchBoxByClick,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import createFlash from '~/flash'; 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 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 getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
import getGroupContainerRepositoriesQuery from '../graphql/queries/get_group_container_repositories.query.graphql';
import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql'; import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql';
import { import {
...@@ -41,9 +37,22 @@ export default { ...@@ -41,9 +37,22 @@ export default {
name: 'RegistryListPage', name: 'RegistryListPage',
components: { components: {
GlEmptyState, GlEmptyState,
ProjectEmptyState, ProjectEmptyState: () =>
GroupEmptyState, import(
ImageList, /* 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, GlModal,
GlSprintf, GlSprintf,
GlLink, GlLink,
...@@ -51,7 +60,6 @@ export default { ...@@ -51,7 +60,6 @@ export default {
GlSkeletonLoader, GlSkeletonLoader,
GlSearchBoxByClick, GlSearchBoxByClick,
RegistryHeader, RegistryHeader,
CliCommands,
}, },
inject: ['config'], inject: ['config'],
directives: { directives: {
...@@ -74,10 +82,8 @@ export default { ...@@ -74,10 +82,8 @@ export default {
EMPTY_RESULT_MESSAGE, EMPTY_RESULT_MESSAGE,
}, },
apollo: { apollo: {
images: { baseImages: {
query() { query: getContainerRepositoriesQuery,
return this.graphQlQuery;
},
variables() { variables() {
return this.queryVariables; return this.queryVariables;
}, },
...@@ -92,10 +98,26 @@ export default { ...@@ -92,10 +98,26 @@ export default {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); 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() { data() {
return { return {
images: [], baseImages: [],
additionalDetails: [],
pageInfo: {}, pageInfo: {},
containerRepositoriesCount: 0, containerRepositoriesCount: 0,
itemToDelete: {}, itemToDelete: {},
...@@ -103,21 +125,24 @@ export default { ...@@ -103,21 +125,24 @@ export default {
searchValue: null, searchValue: null,
name: null, name: null,
mutationLoading: false, mutationLoading: false,
fetchAdditionalDetails: false,
}; };
}, },
computed: { computed: {
images() {
return this.baseImages.map((image, index) => ({
...image,
...get(this.additionalDetails, index, {}),
}));
},
graphqlResource() { graphqlResource() {
return this.config.isGroupPage ? 'group' : 'project'; return this.config.isGroupPage ? 'group' : 'project';
}, },
graphQlQuery() {
return this.config.isGroupPage
? getGroupContainerRepositoriesQuery
: getProjectContainerRepositoriesQuery;
},
queryVariables() { queryVariables() {
return { return {
name: this.name, name: this.name,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE, first: GRAPHQL_PAGE_SIZE,
}; };
}, },
...@@ -127,7 +152,7 @@ export default { ...@@ -127,7 +152,7 @@ export default {
}; };
}, },
isLoading() { isLoading() {
return this.$apollo.queries.images.loading || this.mutationLoading; return this.$apollo.queries.baseImages.loading || this.mutationLoading;
}, },
showCommands() { showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
...@@ -141,6 +166,13 @@ export default { ...@@ -141,6 +166,13 @@ export default {
: DELETE_IMAGE_ERROR_MESSAGE; : 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: { methods: {
deleteImage(item) { deleteImage(item) {
this.track('click_button'); this.track('click_button');
...@@ -175,30 +207,46 @@ export default { ...@@ -175,30 +207,46 @@ export default {
this.deleteAlertType = null; this.deleteAlertType = null;
this.itemToDelete = {}; this.itemToDelete = {};
}, },
fetchNextPage() { updateQuery(_, { fetchMoreResult }) {
return fetchMoreResult;
},
async fetchNextPage() {
if (this.pageInfo?.hasNextPage) { if (this.pageInfo?.hasNextPage) {
this.$apollo.queries.images.fetchMore({ const variables = {
variables: { after: this.pageInfo?.endCursor,
after: this.pageInfo?.endCursor, first: GRAPHQL_PAGE_SIZE,
first: GRAPHQL_PAGE_SIZE, };
},
updateQuery(previousResult, { fetchMoreResult }) { this.$apollo.queries.baseImages.fetchMore({
return fetchMoreResult; variables,
}, updateQuery: this.updateQuery,
});
await this.$nextTick();
this.$apollo.queries.additionalDetails.fetchMore({
variables,
updateQuery: this.updateQuery,
}); });
} }
}, },
fetchPreviousPage() { async fetchPreviousPage() {
if (this.pageInfo?.hasPreviousPage) { if (this.pageInfo?.hasPreviousPage) {
this.$apollo.queries.images.fetchMore({ const variables = {
variables: { first: null,
first: null, before: this.pageInfo?.startCursor,
before: this.pageInfo?.startCursor, last: GRAPHQL_PAGE_SIZE,
last: GRAPHQL_PAGE_SIZE, };
}, this.$apollo.queries.baseImages.fetchMore({
updateQuery(previousResult, { fetchMoreResult }) { variables,
return fetchMoreResult; updateQuery: this.updateQuery,
}, });
await this.$nextTick();
this.$apollo.queries.additionalDetails.fetchMore({
variables,
updateQuery: this.updateQuery,
}); });
} }
}, },
...@@ -286,6 +334,7 @@ export default { ...@@ -286,6 +334,7 @@ export default {
<image-list <image-list
v-if="images.length" v-if="images.length"
:images="images" :images="images"
:metadata-loading="$apollo.queries.additionalDetails.loading"
:page-info="pageInfo" :page-info="pageInfo"
@delete="deleteImage" @delete="deleteImage"
@prev-page="fetchPreviousPage" @prev-page="fetchPreviousPage"
......
query getProjectContainerRepositories(
$fullPath: ID!
$name: String
$first: Int
$last: Int
$after: String
$before: String
$isGroupPage: Boolean!
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
__typename
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
__typename
nodes {
id
name
path
status
location
canDelete
createdAt
expirationPolicyStartedAt
__typename
}
pageInfo {
__typename
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
group(fullPath: $fullPath) @include(if: $isGroupPage) {
__typename
containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) {
__typename
nodes {
id
name
path
status
location
canDelete
createdAt
expirationPolicyStartedAt
__typename
}
pageInfo {
__typename
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
}
- page_title _("Container Registry") - page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true} )
%section %section
#js-container-registry{ data: { endpoint: group_container_registries_path(@group), #js-container-registry{ data: { endpoint: group_container_registries_path(@group),
......
- page_title _("Container Registry") - page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false} )
%section %section
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
......
---
title: Defer tagsCount & add startup.js to container registry
merge_request: 50147
author:
type: changed
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui'; import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
...@@ -23,10 +23,11 @@ describe('Image List Row', () => { ...@@ -23,10 +23,11 @@ describe('Image List Row', () => {
const [item] = imagesListResponse; const [item] = imagesListResponse;
const findDetailsLink = () => wrapper.find('[data-testid="details-link"]'); const findDetailsLink = () => wrapper.find('[data-testid="details-link"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]'); const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
const findDeleteBtn = () => wrapper.find(DeleteButton); const findDeleteBtn = () => wrapper.find(DeleteButton);
const findClipboardButton = () => wrapper.find(ClipboardButton); const findClipboardButton = () => wrapper.find(ClipboardButton);
const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]');
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const mountComponent = (props) => { const mountComponent = (props) => {
wrapper = shallowMount(Component, { wrapper = shallowMount(Component, {
...@@ -164,6 +165,20 @@ describe('Image List Row', () => { ...@@ -164,6 +165,20 @@ describe('Image List Row', () => {
expect(icon.props('name')).toBe('tag'); expect(icon.props('name')).toBe('tag');
}); });
describe('loading state', () => {
it('shows a loader when metadataLoading is true', () => {
mountComponent({ metadataLoading: true });
expect(findSkeletonLoader().exists()).toBe(true);
});
it('hides the tags count while loading', () => {
mountComponent({ metadataLoading: true });
expect(findTagsCount().exists()).toBe(false);
});
});
describe('tags count text', () => { describe('tags count text', () => {
it('with one tag in the image', () => { it('with one tag in the image', () => {
mountComponent({ item: { ...item, tagsCount: 1 } }); mountComponent({ item: { ...item, tagsCount: 1 } });
......
...@@ -11,11 +11,12 @@ describe('Image List', () => { ...@@ -11,11 +11,12 @@ describe('Image List', () => {
const findRow = () => wrapper.findAll(ImageListRow); const findRow = () => wrapper.findAll(ImageListRow);
const findPagination = () => wrapper.find(GlKeysetPagination); const findPagination = () => wrapper.find(GlKeysetPagination);
const mountComponent = (pageInfo = defaultPageInfo) => { const mountComponent = (props) => {
wrapper = shallowMount(Component, { wrapper = shallowMount(Component, {
propsData: { propsData: {
images: imagesListResponse, images: imagesListResponse,
pageInfo, pageInfo: defaultPageInfo,
...props,
}, },
}); });
}; };
...@@ -38,6 +39,11 @@ describe('Image List', () => { ...@@ -38,6 +39,11 @@ describe('Image List', () => {
findRow().at(0).vm.$emit('delete', 'foo'); findRow().at(0).vm.$emit('delete', 'foo');
expect(wrapper.emitted('delete')).toEqual([['foo']]); expect(wrapper.emitted('delete')).toEqual([['foo']]);
}); });
it('passes down the metadataLoading prop', () => {
mountComponent({ metadataLoading: true });
expect(findRow().at(0).props('metadataLoading')).toBe(true);
});
}); });
describe('pagination', () => { describe('pagination', () => {
...@@ -55,7 +61,7 @@ describe('Image List', () => { ...@@ -55,7 +61,7 @@ describe('Image List', () => {
`( `(
'when hasNextPage is $hasNextPage and hasPreviousPage is $hasPreviousPage: is $isVisible that the component is visible', 'when hasNextPage is $hasNextPage and hasPreviousPage is $hasPreviousPage: is $isVisible that the component is visible',
({ hasNextPage, hasPreviousPage, isVisible }) => { ({ hasNextPage, hasPreviousPage, isVisible }) => {
mountComponent({ hasNextPage, hasPreviousPage }); mountComponent({ pageInfo: { ...defaultPageInfo, hasNextPage, hasPreviousPage } });
expect(findPagination().exists()).toBe(isVisible); expect(findPagination().exists()).toBe(isVisible);
expect(findPagination().props('hasPreviousPage')).toBe(hasPreviousPage); expect(findPagination().props('hasPreviousPage')).toBe(hasPreviousPage);
...@@ -64,7 +70,7 @@ describe('Image List', () => { ...@@ -64,7 +70,7 @@ describe('Image List', () => {
); );
it('emits "prev-page" when the user clicks the back page button', () => { it('emits "prev-page" when the user clicks the back page button', () => {
mountComponent({ hasPreviousPage: true }); mountComponent();
findPagination().vm.$emit('prev'); findPagination().vm.$emit('prev');
...@@ -72,7 +78,7 @@ describe('Image List', () => { ...@@ -72,7 +78,7 @@ describe('Image List', () => {
}); });
it('emits "next-page" when the user clicks the forward page button', () => { it('emits "next-page" when the user clicks the forward page button', () => {
mountComponent({ hasNextPage: true }); mountComponent();
findPagination().vm.$emit('next'); findPagination().vm.$emit('next');
......
...@@ -8,7 +8,6 @@ export const imagesListResponse = [ ...@@ -8,7 +8,6 @@ export const imagesListResponse = [
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009', location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009',
canDelete: true, canDelete: true,
createdAt: '2020-11-03T13:29:21Z', createdAt: '2020-11-03T13:29:21Z',
tagsCount: 18,
expirationPolicyStartedAt: null, expirationPolicyStartedAt: null,
}, },
{ {
...@@ -20,7 +19,6 @@ export const imagesListResponse = [ ...@@ -20,7 +19,6 @@ export const imagesListResponse = [
location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572', location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572',
canDelete: true, canDelete: true,
createdAt: '2020-09-21T06:57:43Z', createdAt: '2020-09-21T06:57:43Z',
tagsCount: 1,
expirationPolicyStartedAt: null, expirationPolicyStartedAt: null,
}, },
]; ];
...@@ -209,3 +207,26 @@ export const dockerCommands = { ...@@ -209,3 +207,26 @@ export const dockerCommands = {
dockerPushCommand: 'barbar', dockerPushCommand: 'barbar',
dockerLoginCommand: 'bazbaz', dockerLoginCommand: 'bazbaz',
}; };
export const graphQLProjectImageRepositoriesDetailsMock = {
data: {
project: {
containerRepositories: {
nodes: [
{
id: 'gid://gitlab/ContainerRepository/26',
tagsCount: 4,
__typename: 'ContainerRepository',
},
{
id: 'gid://gitlab/ContainerRepository/11',
tagsCount: 1,
__typename: 'ContainerRepository',
},
],
__typename: 'ContainerRepositoryConnection',
},
__typename: 'Project',
},
},
};
...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo'; ...@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui'; import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
import createMockApollo from 'jest/helpers/mock_apollo_helper'; import createMockApollo from 'jest/helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue'; import component from '~/registry/explorer/pages/list.vue';
import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue'; import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
...@@ -19,8 +20,7 @@ import { ...@@ -19,8 +20,7 @@ import {
SEARCH_PLACEHOLDER_TEXT, SEARCH_PLACEHOLDER_TEXT,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
import getProjectContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql'; import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
import getGroupContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql';
import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql'; import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
import { import {
...@@ -31,6 +31,8 @@ import { ...@@ -31,6 +31,8 @@ import {
graphQLEmptyImageListMock, graphQLEmptyImageListMock,
graphQLEmptyGroupImageListMock, graphQLEmptyGroupImageListMock,
pageInfo, pageInfo,
graphQLProjectImageRepositoriesDetailsMock,
dockerCommands,
} from '../mock_data'; } from '../mock_data';
import { GlModal, GlEmptyState } from '../stubs'; import { GlModal, GlEmptyState } from '../stubs';
import { $toast } from '../../shared/mocks'; import { $toast } from '../../shared/mocks';
...@@ -58,6 +60,7 @@ describe('List Page', () => { ...@@ -58,6 +60,7 @@ describe('List Page', () => {
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const waitForApolloRequestRender = async () => { const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers();
await waitForPromises(); await waitForPromises();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}; };
...@@ -65,15 +68,15 @@ describe('List Page', () => { ...@@ -65,15 +68,15 @@ describe('List Page', () => {
const mountComponent = ({ const mountComponent = ({
mocks, mocks,
resolver = jest.fn().mockResolvedValue(graphQLImageListMock), resolver = jest.fn().mockResolvedValue(graphQLImageListMock),
groupResolver = jest.fn().mockResolvedValue(graphQLImageListMock), detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock),
mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock), mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock),
config = {}, config = { isGroupPage: false },
} = {}) => { } = {}) => {
localVue.use(VueApollo); localVue.use(VueApollo);
const requestHandlers = [ const requestHandlers = [
[getProjectContainerRepositoriesQuery, resolver], [getContainerRepositoriesQuery, resolver],
[getGroupContainerRepositoriesQuery, groupResolver], [getContainerRepositoriesDetails, detailsResolver],
[deleteContainerRepositoryMutation, mutationResolver], [deleteContainerRepositoryMutation, mutationResolver],
]; ];
...@@ -99,6 +102,7 @@ describe('List Page', () => { ...@@ -99,6 +102,7 @@ describe('List Page', () => {
provide() { provide() {
return { return {
config, config,
...dockerCommands,
}; };
}, },
}); });
...@@ -125,6 +129,7 @@ describe('List Page', () => { ...@@ -125,6 +129,7 @@ describe('List Page', () => {
characterError: true, characterError: true,
containersErrorImage: 'foo', containersErrorImage: 'foo',
helpPagePath: 'bar', helpPagePath: 'bar',
isGroupPage: false,
}; };
it('should show an empty state', () => { it('should show an empty state', () => {
...@@ -199,15 +204,16 @@ describe('List Page', () => { ...@@ -199,15 +204,16 @@ describe('List Page', () => {
expect(findProjectEmptyState().exists()).toBe(true); expect(findProjectEmptyState().exists()).toBe(true);
}); });
}); });
describe('group page', () => { describe('group page', () => {
const groupResolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock); const resolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock);
const config = { const config = {
isGroupPage: true, isGroupPage: true,
}; };
it('group empty state is visible', async () => { it('group empty state is visible', async () => {
mountComponent({ groupResolver, config }); mountComponent({ resolver, config });
await waitForApolloRequestRender(); await waitForApolloRequestRender();
...@@ -215,7 +221,7 @@ describe('List Page', () => { ...@@ -215,7 +221,7 @@ describe('List Page', () => {
}); });
it('cli commands is not visible', async () => { it('cli commands is not visible', async () => {
mountComponent({ groupResolver, config }); mountComponent({ resolver, config });
await waitForApolloRequestRender(); await waitForApolloRequestRender();
...@@ -223,7 +229,7 @@ describe('List Page', () => { ...@@ -223,7 +229,7 @@ describe('List Page', () => {
}); });
it('list header is not visible', async () => { it('list header is not visible', async () => {
mountComponent({ groupResolver, config }); mountComponent({ resolver, config });
await waitForApolloRequestRender(); await waitForApolloRequestRender();
...@@ -260,6 +266,39 @@ describe('List Page', () => { ...@@ -260,6 +266,39 @@ describe('List Page', () => {
expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL); expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
}); });
describe('additional metadata', () => {
it('is called on component load', async () => {
const detailsResolver = jest
.fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ detailsResolver });
jest.runOnlyPendingTimers();
await waitForPromises();
expect(detailsResolver).toHaveBeenCalled();
});
it('does not block the list ui to show', async () => {
const detailsResolver = jest.fn().mockRejectedValue();
mountComponent({ detailsResolver });
await waitForApolloRequestRender();
expect(findImageList().exists()).toBe(true);
});
it('loading state is passed to list component', async () => {
// this is a promise that never resolves, to trick apollo to think that this request is still loading
const detailsResolver = jest.fn().mockImplementation(() => new Promise(() => {}));
mountComponent({ detailsResolver });
await waitForApolloRequestRender();
expect(findImageList().props('metadataLoading')).toBe(true);
});
});
describe('delete image', () => { describe('delete image', () => {
const deleteImage = async () => { const deleteImage = async () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -357,9 +396,15 @@ describe('List Page', () => { ...@@ -357,9 +396,15 @@ describe('List Page', () => {
it('when search result is empty displays an empty search message', async () => { it('when search result is empty displays an empty search message', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver }); const detailsResolver = jest
.fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ resolver, detailsResolver });
await waitForApolloRequestRender();
resolver.mockResolvedValue(graphQLEmptyImageListMock); resolver.mockResolvedValue(graphQLEmptyImageListMock);
detailsResolver.mockResolvedValue(graphQLEmptyImageListMock);
await doSearch(); await doSearch();
...@@ -370,28 +415,42 @@ describe('List Page', () => { ...@@ -370,28 +415,42 @@ describe('List Page', () => {
describe('pagination', () => { describe('pagination', () => {
it('prev-page event triggers a fetchMore request', async () => { it('prev-page event triggers a fetchMore request', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver }); const detailsResolver = jest
.fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ resolver, detailsResolver });
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('prev-page'); findImageList().vm.$emit('prev-page');
await wrapper.vm.$nextTick();
expect(resolver).toHaveBeenCalledWith( expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ first: null, before: pageInfo.startCursor }), expect.objectContaining({ before: pageInfo.startCursor }),
);
expect(detailsResolver).toHaveBeenCalledWith(
expect.objectContaining({ before: pageInfo.startCursor }),
); );
}); });
it('next-page event triggers a fetchMore request', async () => { it('next-page event triggers a fetchMore request', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver }); const detailsResolver = jest
.fn()
.mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock);
mountComponent({ resolver, detailsResolver });
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findImageList().vm.$emit('next-page'); findImageList().vm.$emit('next-page');
await wrapper.vm.$nextTick();
expect(resolver).toHaveBeenCalledWith( expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pageInfo.endCursor }), expect.objectContaining({ after: pageInfo.endCursor }),
); );
expect(detailsResolver).toHaveBeenCalledWith(
expect.objectContaining({ after: pageInfo.endCursor }),
);
}); });
}); });
}); });
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment