Commit db2a50fa authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '290302-update-the-default-sort-order-of-the-image-repository-list' into 'master'

Add registry_search to container registry list page

See merge request gitlab-org/gitlab!53820
parents af1ff616 58a9a516
import { s__ } from '~/locale'; import { s__, __ } from '~/locale';
// Translations strings // Translations strings
...@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__( ...@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__( export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'ContainerRegistry|%{title} was successfully scheduled for deletion', 'ContainerRegistry|%{title} was successfully scheduled for deletion',
); );
export const IMAGE_REPOSITORY_LIST_LABEL = s__('ContainerRegistry|Image Repositories');
export const SEARCH_PLACEHOLDER_TEXT = s__('ContainerRegistry|Filter by name');
export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.'); export const EMPTY_RESULT_TITLE = s__('ContainerRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__( export const EMPTY_RESULT_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.', 'ContainerRegistry|To widen your search, change or remove the filters above.',
...@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__( ...@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED'; export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED'; export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
export const GRAPHQL_PAGE_SIZE = 10; export const GRAPHQL_PAGE_SIZE = 10;
export const SORT_FIELDS = [
{ orderBy: 'UPDATED', label: __('Updated') },
{ orderBy: 'CREATED', label: __('Created') },
{ orderBy: 'NAME', label: __('Name') },
];
...@@ -6,9 +6,17 @@ query getContainerRepositoriesDetails( ...@@ -6,9 +6,17 @@ query getContainerRepositoriesDetails(
$after: String $after: String
$before: String $before: String
$isGroupPage: Boolean! $isGroupPage: Boolean!
$sort: ContainerRepositorySort
) { ) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) { project(fullPath: $fullPath) @skip(if: $isGroupPage) {
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { containerRepositories(
name: $name
after: $after
before: $before
first: $first
last: $last
sort: $sort
) {
nodes { nodes {
id id
tagsCount tagsCount
...@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails( ...@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails(
} }
} }
group(fullPath: $fullPath) @include(if: $isGroupPage) { group(fullPath: $fullPath) @include(if: $isGroupPage) {
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { containerRepositories(
name: $name
after: $after
before: $before
first: $first
last: $last
sort: $sort
) {
nodes { nodes {
id id
tagsCount tagsCount
......
...@@ -7,12 +7,12 @@ import { ...@@ -7,12 +7,12 @@ import {
GlLink, GlLink,
GlAlert, GlAlert,
GlSkeletonLoader, GlSkeletonLoader,
GlSearchBoxByClick,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { get } from 'lodash'; import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; 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 RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import RegistryHeader from '../components/list_page/registry_header.vue'; import RegistryHeader from '../components/list_page/registry_header.vue';
import DeleteImage from '../components/delete_image.vue'; import DeleteImage from '../components/delete_image.vue';
...@@ -25,12 +25,11 @@ import { ...@@ -25,12 +25,11 @@ import {
CONNECTION_ERROR_MESSAGE, CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT, REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE, EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE, EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE, GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE, FETCH_IMAGES_LIST_ERROR_MESSAGE,
SORT_FIELDS,
} from '../constants/index'; } from '../constants/index';
export default { export default {
...@@ -58,9 +57,9 @@ export default { ...@@ -58,9 +57,9 @@ export default {
GlLink, GlLink,
GlAlert, GlAlert,
GlSkeletonLoader, GlSkeletonLoader,
GlSearchBoxByClick,
RegistryHeader, RegistryHeader,
DeleteImage, DeleteImage,
RegistrySearch,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -77,11 +76,10 @@ export default { ...@@ -77,11 +76,10 @@ export default {
CONNECTION_ERROR_MESSAGE, CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT, REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE, EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE, EMPTY_RESULT_MESSAGE,
}, },
searchConfig: SORT_FIELDS,
apollo: { apollo: {
baseImages: { baseImages: {
query: getContainerRepositoriesQuery, query: getContainerRepositoriesQuery,
...@@ -123,7 +121,8 @@ export default { ...@@ -123,7 +121,8 @@ export default {
containerRepositoriesCount: 0, containerRepositoriesCount: 0,
itemToDelete: {}, itemToDelete: {},
deleteAlertType: null, deleteAlertType: null,
searchValue: null, filter: [],
sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null, name: null,
mutationLoading: false, mutationLoading: false,
fetchAdditionalDetails: false, fetchAdditionalDetails: false,
...@@ -142,6 +141,7 @@ export default { ...@@ -142,6 +141,7 @@ export default {
queryVariables() { queryVariables() {
return { return {
name: this.name, name: this.name,
sort: this.sortBy,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage, isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE, first: GRAPHQL_PAGE_SIZE,
...@@ -166,6 +166,10 @@ export default { ...@@ -166,6 +166,10 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE ? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE; : DELETE_IMAGE_ERROR_MESSAGE;
}, },
sortBy() {
const { orderBy, sort } = this.sorting;
return `${orderBy}_${sort}`.toUpperCase();
},
}, },
mounted() { mounted() {
// If the two graphql calls - which are not batched - resolve togheter we will have a race // If the two graphql calls - which are not batched - resolve togheter we will have a race
...@@ -231,6 +235,16 @@ export default { ...@@ -231,6 +235,16 @@ export default {
this.track('confirm_delete'); this.track('confirm_delete');
this.mutationLoading = true; this.mutationLoading = true;
}, },
updateSorting(value) {
this.sorting = {
...this.sorting,
...value,
};
},
doFilter() {
const search = this.filter.find((i) => i.type === 'filtered-search-term');
this.name = search?.value?.data;
},
}, },
}; };
</script> </script>
...@@ -283,6 +297,16 @@ export default { ...@@ -283,6 +297,16 @@ export default {
</template> </template>
</registry-header> </registry-header>
<registry-search
:filter="filter"
:sorting="sorting"
:tokens="[]"
:sortable-fields="$options.searchConfig"
@sorting:changed="updateSorting"
@filter:changed="filter = $event"
@filter:submit="doFilter"
/>
<div v-if="isLoading" class="gl-mt-5"> <div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader <gl-skeleton-loader
v-for="index in $options.loader.repeat" v-for="index in $options.loader.repeat"
...@@ -298,20 +322,6 @@ export default { ...@@ -298,20 +322,6 @@ export default {
</div> </div>
<template v-else> <template v-else>
<template v-if="images.length > 0 || name"> <template v-if="images.length > 0 || name">
<div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
<div class="gl-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div>
<div>
<gl-search-box-by-click
v-model="searchValue"
:placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT"
@clear="name = null"
@submit="name = $event"
/>
</div>
</div>
<image-list <image-list
v-if="images.length" v-if="images.length"
:images="images" :images="images"
......
...@@ -6,11 +6,19 @@ query getProjectContainerRepositories( ...@@ -6,11 +6,19 @@ query getProjectContainerRepositories(
$after: String $after: String
$before: String $before: String
$isGroupPage: Boolean! $isGroupPage: Boolean!
$sort: ContainerRepositorySort
) { ) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) { project(fullPath: $fullPath) @skip(if: $isGroupPage) {
__typename __typename
containerRepositoriesCount containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { containerRepositories(
name: $name
after: $after
before: $before
first: $first
last: $last
sort: $sort
) {
__typename __typename
nodes { nodes {
id id
...@@ -35,7 +43,14 @@ query getProjectContainerRepositories( ...@@ -35,7 +43,14 @@ query getProjectContainerRepositories(
group(fullPath: $fullPath) @include(if: $isGroupPage) { group(fullPath: $fullPath) @include(if: $isGroupPage) {
__typename __typename
containerRepositoriesCount containerRepositoriesCount
containerRepositories(name: $name, after: $after, before: $before, first: $first, last: $last) { containerRepositories(
name: $name
after: $after
before: $before
first: $first
last: $last
sort: $sort
) {
__typename __typename
nodes { nodes {
id id
......
- 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} ) - add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil} )
%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} ) - add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil} )
%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: Add sort to container registry list page
merge_request: 53820
author:
type: changed
...@@ -7829,15 +7829,9 @@ msgstr "" ...@@ -7829,15 +7829,9 @@ msgstr ""
msgid "ContainerRegistry|Expiration policy will run in %{time}" msgid "ContainerRegistry|Expiration policy will run in %{time}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Filter by name"
msgstr ""
msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password." msgid "ContainerRegistry|If you are not already logged in, you need to authenticate to the Container Registry by using your GitLab username and password. If you have %{twofaDocLinkStart}Two-Factor Authentication%{twofaDocLinkEnd} enabled, use a %{personalAccessTokensDocLinkStart}Personal Access Token%{personalAccessTokensDocLinkEnd} instead of a password."
msgstr "" msgstr ""
msgid "ContainerRegistry|Image Repositories"
msgstr ""
msgid "ContainerRegistry|Image repository deletion failed" msgid "ContainerRegistry|Image repository deletion failed"
msgstr "" msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui'; import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from '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 getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
...@@ -13,12 +13,12 @@ import RegistryHeader from '~/registry/explorer/components/list_page/registry_he ...@@ -13,12 +13,12 @@ import RegistryHeader from '~/registry/explorer/components/list_page/registry_he
import ImageList from '~/registry/explorer/components/list_page/image_list.vue'; import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import DeleteImage from '~/registry/explorer/components/delete_image.vue'; import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import { import {
DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE,
IMAGE_REPOSITORY_LIST_LABEL, SORT_FIELDS,
SEARCH_PLACEHOLDER_TEXT,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
...@@ -55,8 +55,7 @@ describe('List Page', () => { ...@@ -55,8 +55,7 @@ describe('List Page', () => {
const findDeleteAlert = () => wrapper.find(GlAlert); const findDeleteAlert = () => wrapper.find(GlAlert);
const findImageList = () => wrapper.find(ImageList); const findImageList = () => wrapper.find(ImageList);
const findListHeader = () => wrapper.find('[data-testid="listHeader"]'); const findRegistrySearch = () => wrapper.find(RegistrySearch);
const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const findDeleteImage = () => wrapper.find(DeleteImage); const findDeleteImage = () => wrapper.find(DeleteImage);
...@@ -229,14 +228,6 @@ describe('List Page', () => { ...@@ -229,14 +228,6 @@ describe('List Page', () => {
expect(findCliCommands().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
it('list header is not visible', async () => {
mountComponent({ resolver, config });
await waitForApolloRequestRender();
expect(findListHeader().exists()).toBe(false);
});
}); });
}); });
...@@ -258,16 +249,6 @@ describe('List Page', () => { ...@@ -258,16 +249,6 @@ describe('List Page', () => {
expect(findImageList().exists()).toBe(true); expect(findImageList().exists()).toBe(true);
}); });
it('list header is visible', async () => {
mountComponent();
await waitForApolloRequestRender();
const header = findListHeader();
expect(header.exists()).toBe(true);
expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL);
});
describe('additional metadata', () => { describe('additional metadata', () => {
it('is called on component load', async () => { it('is called on component load', async () => {
const detailsResolver = jest const detailsResolver = jest
...@@ -360,10 +341,15 @@ describe('List Page', () => { ...@@ -360,10 +341,15 @@ describe('List Page', () => {
}); });
}); });
describe('search', () => { describe('search and sorting', () => {
const doSearch = async () => { const doSearch = async () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findSearchBox().vm.$emit('submit', 'centos6'); findRegistrySearch().vm.$emit('filter:changed', [
{ type: 'filtered-search-term', value: { data: 'centos6' } },
]);
findRegistrySearch().vm.$emit('filter:submit');
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}; };
...@@ -372,9 +358,26 @@ describe('List Page', () => { ...@@ -372,9 +358,26 @@ describe('List Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
const searchBox = findSearchBox(); const registrySearch = findRegistrySearch();
expect(searchBox.exists()).toBe(true); expect(registrySearch.exists()).toBe(true);
expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT); expect(registrySearch.props()).toMatchObject({
filter: [],
sorting: { orderBy: 'UPDATED', sort: 'desc' },
sortableFields: SORT_FIELDS,
tokens: [],
});
});
it('performs sorting', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageListMock);
mountComponent({ resolver });
await waitForApolloRequestRender();
findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' });
await wrapper.vm.$nextTick();
expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' }));
}); });
it('performs a search', async () => { it('performs a search', async () => {
......
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