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
......@@ -35,8 +35,6 @@ export const ASYNC_DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = s__(
'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_MESSAGE = s__(
'ContainerRegistry|To widen your search, change or remove the filters above.',
......@@ -47,3 +45,9 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';
export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED';
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(
$after: String
$before: String
$isGroupPage: Boolean!
$sort: ContainerRepositorySort
) {
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 {
id
tagsCount
......@@ -16,7 +24,14 @@ query getContainerRepositoriesDetails(
}
}
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 {
id
tagsCount
......
......@@ -7,12 +7,12 @@ import {
GlLink,
GlAlert,
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 RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
import DeleteImage from '../components/delete_image.vue';
......@@ -25,12 +25,11 @@ import {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
SORT_FIELDS,
} from '../constants/index';
export default {
......@@ -58,9 +57,9 @@ export default {
GlLink,
GlAlert,
GlSkeletonLoader,
GlSearchBoxByClick,
RegistryHeader,
DeleteImage,
RegistrySearch,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -77,11 +76,10 @@ export default {
CONNECTION_ERROR_MESSAGE,
REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT,
IMAGE_REPOSITORY_LIST_LABEL,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
searchConfig: SORT_FIELDS,
apollo: {
baseImages: {
query: getContainerRepositoriesQuery,
......@@ -123,7 +121,8 @@ export default {
containerRepositoriesCount: 0,
itemToDelete: {},
deleteAlertType: null,
searchValue: null,
filter: [],
sorting: { orderBy: 'UPDATED', sort: 'desc' },
name: null,
mutationLoading: false,
fetchAdditionalDetails: false,
......@@ -142,6 +141,7 @@ export default {
queryVariables() {
return {
name: this.name,
sort: this.sortBy,
fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath,
isGroupPage: this.config.isGroupPage,
first: GRAPHQL_PAGE_SIZE,
......@@ -166,6 +166,10 @@ export default {
? DELETE_IMAGE_SUCCESS_MESSAGE
: DELETE_IMAGE_ERROR_MESSAGE;
},
sortBy() {
const { orderBy, sort } = this.sorting;
return `${orderBy}_${sort}`.toUpperCase();
},
},
mounted() {
// If the two graphql calls - which are not batched - resolve togheter we will have a race
......@@ -231,6 +235,16 @@ export default {
this.track('confirm_delete');
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>
......@@ -283,6 +297,16 @@ export default {
</template>
</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">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
......@@ -298,20 +322,6 @@ export default {
</div>
<template v-else>
<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
v-if="images.length"
:images="images"
......
......@@ -6,11 +6,19 @@ query getProjectContainerRepositories(
$after: String
$before: String
$isGroupPage: Boolean!
$sort: ContainerRepositorySort
) {
project(fullPath: $fullPath) @skip(if: $isGroupPage) {
__typename
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
nodes {
id
......@@ -35,7 +43,14 @@ query getProjectContainerRepositories(
group(fullPath: $fullPath) @include(if: $isGroupPage) {
__typename
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
nodes {
id
......
- page_title _("Container Registry")
- @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
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
......
- page_title _("Container Registry")
- @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
#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 ""
msgid "ContainerRegistry|Expiration policy will run in %{time}"
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."
msgstr ""
msgid "ContainerRegistry|Image Repositories"
msgstr ""
msgid "ContainerRegistry|Image repository deletion failed"
msgstr ""
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
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 waitForPromises from 'helpers/wait_for_promises';
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
import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
IMAGE_REPOSITORY_LIST_LABEL,
SEARCH_PLACEHOLDER_TEXT,
SORT_FIELDS,
} from '~/registry/explorer/constants';
import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql';
......@@ -55,8 +55,7 @@ describe('List Page', () => {
const findDeleteAlert = () => wrapper.find(GlAlert);
const findImageList = () => wrapper.find(ImageList);
const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
const findRegistrySearch = () => wrapper.find(RegistrySearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const findDeleteImage = () => wrapper.find(DeleteImage);
......@@ -229,14 +228,6 @@ describe('List Page', () => {
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', () => {
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', () => {
it('is called on component load', async () => {
const detailsResolver = jest
......@@ -360,10 +341,15 @@ describe('List Page', () => {
});
});
describe('search', () => {
describe('search and sorting', () => {
const doSearch = async () => {
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();
};
......@@ -372,9 +358,26 @@ describe('List Page', () => {
await waitForApolloRequestRender();
const searchBox = findSearchBox();
expect(searchBox.exists()).toBe(true);
expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT);
const registrySearch = findRegistrySearch();
expect(registrySearch.exists()).toBe(true);
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 () => {
......
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