Commit d79c0e3a authored by Phil Hughes's avatar Phil Hughes

Merge branch '201889-design-skeleton-loaders-content-for-the-package-stage' into 'master'

Add skeleton loaders to container registry

See merge request gitlab-org/gitlab!25994
parents a0ffe720 077d58fe
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { import {
GlTable, GlTable,
GlFormCheckbox, GlFormCheckbox,
...@@ -8,10 +8,10 @@ import { ...@@ -8,10 +8,10 @@ import {
GlTooltipDirective, GlTooltipDirective,
GlPagination, GlPagination,
GlModal, GlModal,
GlLoadingIcon,
GlSprintf, GlSprintf,
GlEmptyState, GlEmptyState,
GlResizeObserverDirective, GlResizeObserverDirective,
GlSkeletonLoader,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { n__, s__ } from '~/locale'; import { n__, s__ } from '~/locale';
...@@ -42,7 +42,7 @@ export default { ...@@ -42,7 +42,7 @@ export default {
ClipboardButton, ClipboardButton,
GlPagination, GlPagination,
GlModal, GlModal,
GlLoadingIcon, GlSkeletonLoader,
GlSprintf, GlSprintf,
GlEmptyState, GlEmptyState,
}, },
...@@ -51,6 +51,11 @@ export default { ...@@ -51,6 +51,11 @@ export default {
GlResizeObserver: GlResizeObserverDirective, GlResizeObserver: GlResizeObserverDirective,
}, },
mixins: [timeagoMixin, Tracking.mixin()], mixins: [timeagoMixin, Tracking.mixin()],
loader: {
repeat: 10,
width: 1000,
height: 40,
},
data() { data() {
return { return {
selectedItems: [], selectedItems: [],
...@@ -61,15 +66,16 @@ export default { ...@@ -61,15 +66,16 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['tags', 'tagsPagination', 'isLoading', 'config']), ...mapGetters(['tags']),
...mapState(['tagsPagination', 'isLoading', 'config']),
imageName() { imageName() {
const { name } = decodeAndParse(this.$route.params.id); const { name } = decodeAndParse(this.$route.params.id);
return name; return name;
}, },
fields() { fields() {
return [ return [
{ key: LIST_KEY_CHECKBOX, label: '' }, { key: LIST_KEY_CHECKBOX, label: '', class: 'gl-w-16' },
{ key: LIST_KEY_TAG, label: LIST_LABEL_TAG }, { key: LIST_KEY_TAG, label: LIST_LABEL_TAG, class: 'w-25' },
{ key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID }, { key: LIST_KEY_IMAGE_ID, label: LIST_LABEL_IMAGE_ID },
{ key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE }, { key: LIST_KEY_SIZE, label: LIST_LABEL_SIZE },
{ key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED }, { key: LIST_KEY_LAST_UPDATED, label: LIST_LABEL_LAST_UPDATED },
...@@ -209,122 +215,142 @@ export default { ...@@ -209,122 +215,142 @@ export default {
</gl-sprintf> </gl-sprintf>
</h4> </h4>
</div> </div>
<gl-loading-icon v-if="isLoading" />
<template v-else-if="tags.length > 0">
<gl-table :items="tags" :fields="fields" :stacked="!isDesktop">
<template v-if="isDesktop" #head(checkbox)>
<gl-form-checkbox
ref="mainCheckbox"
:checked="selectAllChecked"
@change="onSelectAllChange"
/>
</template>
<template #head(actions)>
<gl-button
ref="bulkDeleteButton"
v-gl-tooltip
:disabled="!selectedItems || selectedItems.length === 0"
class="float-right"
variant="danger"
:title="s__('ContainerRegistry|Remove selected tags')"
:aria-label="s__('ContainerRegistry|Remove selected tags')"
@click="deleteMultipleItems()"
>
<gl-icon name="remove" />
</gl-button>
</template>
<template #cell(checkbox)="{index}"> <gl-table :items="tags" :fields="fields" :stacked="!isDesktop" show-empty>
<gl-form-checkbox <template v-if="isDesktop" #head(checkbox)>
ref="rowCheckbox" <gl-form-checkbox
class="js-row-checkbox" ref="mainCheckbox"
:checked="selectedItems.includes(index)" :checked="selectAllChecked"
@change="updateSelectedItems(index)" @change="onSelectAllChange"
/> />
</template> </template>
<template #cell(name)="{item}"> <template #head(actions)>
<span ref="rowName"> <gl-button
{{ item.name }} ref="bulkDeleteButton"
</span> v-gl-tooltip
<clipboard-button :disabled="!selectedItems || selectedItems.length === 0"
v-if="item.location" class="float-right"
ref="rowClipboardButton" variant="danger"
:title="item.location" :title="s__('ContainerRegistry|Remove selected tags')"
:text="item.location" :aria-label="s__('ContainerRegistry|Remove selected tags')"
css-class="btn-default btn-transparent btn-clipboard" @click="deleteMultipleItems()"
/> >
</template> <gl-icon name="remove" />
<template #cell(short_revision)="{value}"> </gl-button>
<span ref="rowShortRevision"> </template>
{{ value }}
</span> <template #cell(checkbox)="{index}">
</template> <gl-form-checkbox
<template #cell(total_size)="{item}"> ref="rowCheckbox"
<span ref="rowSize"> class="js-row-checkbox"
{{ formatSize(item.total_size) }} :checked="selectedItems.includes(index)"
<template v-if="item.total_size && item.layers"> @change="updateSelectedItems(index)"
&middot; />
</template> </template>
{{ layers(item.layers) }} <template #cell(name)="{item}">
</span> <span ref="rowName">
</template> {{ item.name }}
<template #cell(created_at)="{value}"> </span>
<span ref="rowTime"> <clipboard-button
{{ timeFormatted(value) }} v-if="item.location"
</span> ref="rowClipboardButton"
</template> :title="item.location"
<template #cell(actions)="{index, item}"> :text="item.location"
<gl-button css-class="btn-default btn-transparent btn-clipboard"
ref="singleDeleteButton" />
:title="s__('ContainerRegistry|Remove tag')" </template>
:aria-label="s__('ContainerRegistry|Remove tag')" <template #cell(short_revision)="{value}">
:disabled="!item.destroy_path" <span ref="rowShortRevision">
variant="danger" {{ value }}
:class="['js-delete-registry float-right btn-inverted btn-border-color btn-icon']" </span>
@click="deleteSingleItem(index)" </template>
<template #cell(total_size)="{item}">
<span ref="rowSize">
{{ formatSize(item.total_size) }}
<template v-if="item.total_size && item.layers">
&middot;
</template>
{{ layers(item.layers) }}
</span>
</template>
<template #cell(created_at)="{value}">
<span ref="rowTime">
{{ timeFormatted(value) }}
</span>
</template>
<template #cell(actions)="{index, item}">
<gl-button
ref="singleDeleteButton"
:title="s__('ContainerRegistry|Remove tag')"
:aria-label="s__('ContainerRegistry|Remove tag')"
:disabled="!item.destroy_path"
variant="danger"
class="js-delete-registry float-right btn-inverted btn-border-color btn-icon"
@click="deleteSingleItem(index)"
>
<gl-icon name="remove" />
</gl-button>
</template>
<template #empty>
<template v-if="isLoading">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
:width="$options.loader.width"
:height="$options.loader.height"
preserve-aspect-ratio="xMinYMax meet"
> >
<gl-icon name="remove" /> <rect width="15" x="0" y="12.5" height="15" rx="4" />
</gl-button> <rect width="250" x="25" y="10" height="20" rx="4" />
<circle cx="290" cy="20" r="10" />
<rect width="100" x="315" y="10" height="20" rx="4" />
<rect width="100" x="500" y="10" height="20" rx="4" />
<rect width="100" x="630" y="10" height="20" rx="4" />
<rect x="960" y="0" width="40" height="40" rx="4" />
</gl-skeleton-loader>
</template> </template>
</gl-table> <gl-empty-state
<gl-pagination v-else
ref="pagination" :title="s__('ContainerRegistry|This image has no active tags')"
v-model="currentPage" :svg-path="config.noContainersImage"
:per-page="tagsPagination.perPage" :description="
:total-items="tagsPagination.total" s__(
align="center" `ContainerRegistry|The last tag related to this image was recently removed.
class="w-100"
/>
<gl-modal
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
@ok="onDeletionConfirmed"
@cancel="track('cancel_delete')"
>
<template #modal-title>{{ modalAction }}</template>
<template #modal-ok>{{ modalAction }}</template>
<p v-if="modalDescription">
<gl-sprintf :message="modalDescription.message">
<template #item>
<b>{{ modalDescription.item }}</b>
</template>
</gl-sprintf>
</p>
</gl-modal>
</template>
<gl-empty-state
v-else
:title="s__('ContainerRegistry|This image has no active tags')"
:svg-path="config.noContainersImage"
:description="
s__(
`ContainerRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`, If you have any questions, contact your administrator.`,
) )
" "
class="mx-auto my-0" class="mx-auto my-0"
/>
</template>
</gl-table>
<gl-pagination
ref="pagination"
v-model="currentPage"
:per-page="tagsPagination.perPage"
:total-items="tagsPagination.total"
align="center"
class="w-100"
/> />
<gl-modal
ref="deleteModal"
modal-id="delete-tag-modal"
ok-variant="danger"
@ok="onDeletionConfirmed"
@cancel="track('cancel_delete')"
>
<template #modal-title>{{ modalAction }}</template>
<template #modal-ok>{{ modalAction }}</template>
<p v-if="modalDescription">
<gl-sprintf :message="modalDescription.message">
<template #item>
<b>{{ modalDescription.item }}</b>
</template>
</gl-sprintf>
</p>
</gl-modal>
</div> </div>
</template> </template>
<script> <script>
import { mapState, mapActions } from 'vuex'; import { mapState, mapActions } from 'vuex';
import { import {
GlLoadingIcon,
GlEmptyState, GlEmptyState,
GlPagination, GlPagination,
GlTooltipDirective, GlTooltipDirective,
...@@ -10,6 +9,7 @@ import { ...@@ -10,6 +9,7 @@ import {
GlModal, GlModal,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlSkeletonLoader,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
...@@ -20,7 +20,6 @@ export default { ...@@ -20,7 +20,6 @@ export default {
name: 'RegistryListApp', name: 'RegistryListApp',
components: { components: {
GlEmptyState, GlEmptyState,
GlLoadingIcon,
GlPagination, GlPagination,
ProjectEmptyState, ProjectEmptyState,
GroupEmptyState, GroupEmptyState,
...@@ -30,11 +29,17 @@ export default { ...@@ -30,11 +29,17 @@ export default {
GlModal, GlModal,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlSkeletonLoader,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
}, },
mixins: [Tracking.mixin()], mixins: [Tracking.mixin()],
loader: {
repeat: 10,
width: 1000,
height: 40,
},
data() { data() {
return { return {
itemToDelete: {}, itemToDelete: {},
...@@ -104,74 +109,81 @@ export default { ...@@ -104,74 +109,81 @@ export default {
</gl-empty-state> </gl-empty-state>
<template v-else> <template v-else>
<gl-loading-icon v-if="isLoading" size="md" class="prepend-top-16" /> <div>
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<template v-else> <p>
<div v-if="images.length" ref="imagesList"> <gl-sprintf
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4> :message="
<p> s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every
project can have its own space to store its Docker images. project can have its own space to store its Docker images.
%{docLinkStart}More Information%{docLinkEnd}`) %{docLinkStart}More Information%{docLinkEnd}`)
" "
> >
<template #docLink="{content}"> <template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank"> <gl-link :href="config.helpPagePath" target="_blank">
{{ content }} {{ content }}
</gl-link> </gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
</div>
<div class="d-flex flex-column"> <div v-if="isLoading" class="mt-2">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
:width="$options.loader.width"
:height="$options.loader.height"
preserve-aspect-ratio="xMinYMax meet"
>
<rect width="500" x="10" y="10" height="20" rx="4" />
<circle cx="525" cy="20" r="10" />
<rect x="960" y="0" width="40" height="40" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else>
<div v-if="images.length" ref="imagesList" class="d-flex flex-column">
<div
v-for="(listItem, index) in images"
:key="index"
ref="rowItem"
:class="{ 'border-top': index === 0 }"
class="d-flex justify-content-between align-items-center py-2 border-bottom"
>
<div>
<router-link
ref="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
ref="clipboardButton"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div <div
v-for="(listItem, index) in images" v-gl-tooltip="{ disabled: listItem.destroy_path }"
:key="index" class="d-none d-sm-block"
ref="rowItem" :title="
:class="[ s__('ContainerRegistry|Missing or insufficient permission, delete button disabled')
'd-flex justify-content-between align-items-center py-2 border-bottom', "
{ 'border-top': index === 0 },
]"
> >
<div> <gl-button
<router-link ref="deleteImageButton"
ref="detailsLink" v-gl-tooltip
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }" :disabled="!listItem.destroy_path"
> :title="s__('ContainerRegistry|Remove repository')"
{{ listItem.path }} :aria-label="s__('ContainerRegistry|Remove repository')"
</router-link> class="btn-inverted"
<clipboard-button variant="danger"
v-if="listItem.location" @click="deleteImage(listItem)"
ref="clipboardButton"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
</div>
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="
s__(
'ContainerRegistry|Missing or insufficient permission, delete button disabled',
)
"
> >
<gl-button <gl-icon name="remove" />
ref="deleteImageButton" </gl-button>
v-gl-tooltip
:disabled="!listItem.destroy_path"
:title="s__('ContainerRegistry|Remove repository')"
:aria-label="s__('ContainerRegistry|Remove repository')"
class="btn-inverted"
variant="danger"
@click="deleteImage(listItem)"
>
<gl-icon name="remove" />
</gl-button>
</div>
</div> </div>
</div> </div>
<gl-pagination <gl-pagination
...@@ -182,6 +194,7 @@ export default { ...@@ -182,6 +194,7 @@ export default {
class="w-100 mt-2" class="w-100 mt-2"
/> />
</div> </div>
<template v-else> <template v-else>
<project-empty-state v-if="!config.isGroupPage" /> <project-empty-state v-if="!config.isGroupPage" />
<group-empty-state v-else /> <group-empty-state v-else />
......
...@@ -68,31 +68,28 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) = ...@@ -68,31 +68,28 @@ export const requestDeleteTag = ({ commit, dispatch, state }, { tag, params }) =
.delete(tag.destroy_path) .delete(tag.destroy_path)
.then(() => { .then(() => {
createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success'); createFlash(DELETE_TAG_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, params }); return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .catch(() => {
createFlash(DELETE_TAG_ERROR_MESSAGE); createFlash(DELETE_TAG_ERROR_MESSAGE);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
}); });
}; };
export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => { export const requestDeleteTags = ({ commit, dispatch, state }, { ids, params }) => {
commit(types.SET_MAIN_LOADING, true); commit(types.SET_MAIN_LOADING, true);
const { id } = decodeAndParse(params); const { tags_path } = decodeAndParse(params);
const url = `/${state.config.projectPath}/registry/repository/${id}/tags/bulk_destroy`;
const url = tags_path.replace('?format=json', '/bulk_destroy');
return axios return axios
.delete(url, { params: { ids } }) .delete(url, { params: { ids } })
.then(() => { .then(() => {
createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success'); createFlash(DELETE_TAGS_SUCCESS_MESSAGE, 'success');
dispatch('requestTagsList', { pagination: state.tagsPagination, params }); return dispatch('requestTagsList', { pagination: state.tagsPagination, params });
}) })
.catch(() => { .catch(() => {
createFlash(DELETE_TAGS_ERROR_MESSAGE); createFlash(DELETE_TAGS_ERROR_MESSAGE);
})
.finally(() => {
commit(types.SET_MAIN_LOADING, false); commit(types.SET_MAIN_LOADING, false);
}); });
}; };
......
// eslint-disable-next-line import/prefer-default-export
export const tags = state => {
// to show the loader inside the table we need to pass an empty array to gl-table whenever the table is loading
// this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
return state.isLoading ? [] : state.tags;
};
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
...@@ -9,6 +10,7 @@ Vue.use(Vuex); ...@@ -9,6 +10,7 @@ Vue.use(Vuex);
export const createStore = () => export const createStore = () =>
new Vuex.Store({ new Vuex.Store({
state, state,
getters,
actions, actions,
mutations, mutations,
}); });
......
...@@ -54,7 +54,7 @@ ...@@ -54,7 +54,7 @@
.mh-50vh { max-height: 50vh; } .mh-50vh { max-height: 50vh; }
.font-size-inherit { font-size: inherit; } .font-size-inherit { font-size: inherit; }
.gl-w-16 { width: px-to-rem($grid-size * 2); }
.gl-w-64 { width: px-to-rem($grid-size * 8); } .gl-w-64 { width: px-to-rem($grid-size * 8); }
.gl-h-32 { height: px-to-rem($grid-size * 4); } .gl-h-32 { height: px-to-rem($grid-size * 4); }
.gl-h-64 { height: px-to-rem($grid-size * 8); } .gl-h-64 { height: px-to-rem($grid-size * 8); }
......
...@@ -6,7 +6,6 @@ ...@@ -6,7 +6,6 @@
.col-12 .col-12
- if Feature.enabled?(:vue_container_registry_explorer, @project.group) - if Feature.enabled?(:vue_container_registry_explorer, @project.group)
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
project_path: @project.full_path,
"help_page_path" => help_page_path('user/packages/container_registry/index'), "help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'), "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'), "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
......
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { GlTable, GlPagination, GlLoadingIcon } from '@gitlab/ui'; import { GlTable, GlPagination, GlSkeletonLoader } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import stubChildren from 'helpers/stub_children'; import stubChildren from 'helpers/stub_children';
import component from '~/registry/explorer/pages/details.vue'; import component from '~/registry/explorer/pages/details.vue';
...@@ -14,8 +14,7 @@ describe('Details Page', () => { ...@@ -14,8 +14,7 @@ describe('Details Page', () => {
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(GlModal);
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findTagsTable = () => wrapper.find(GlTable);
const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' }); const findMainCheckbox = () => wrapper.find({ ref: 'mainCheckbox' });
const findFirstRowItem = ref => wrapper.find({ ref }); const findFirstRowItem = ref => wrapper.find({ ref });
const findBulkDeleteButton = () => wrapper.find({ ref: 'bulkDeleteButton' }); const findBulkDeleteButton = () => wrapper.find({ ref: 'bulkDeleteButton' });
...@@ -33,7 +32,7 @@ describe('Details Page', () => { ...@@ -33,7 +32,7 @@ describe('Details Page', () => {
...stubChildren(component), ...stubChildren(component),
GlModal, GlModal,
GlSprintf: false, GlSprintf: false,
GlTable: false, GlTable,
}, },
mocks: { mocks: {
$route: { $route: {
...@@ -53,18 +52,19 @@ describe('Details Page', () => { ...@@ -53,18 +52,19 @@ describe('Details Page', () => {
}); });
describe('when isLoading is true', () => { describe('when isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true)); beforeEach(() => {
store.dispatch('receiveTagsListSuccess', { ...tagsListResponse, data: [] });
store.commit(SET_MAIN_LOADING, true);
});
afterAll(() => store.commit(SET_MAIN_LOADING, false)); afterAll(() => store.commit(SET_MAIN_LOADING, false));
it('has a loading icon', () => { it('has a skeleton loader', () => {
expect(findLoadingIcon().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
}); });
it('does not have a main content', () => { it('does not have list items', () => {
expect(findTagsTable().exists()).toBe(false); expect(findFirstRowItem('rowCheckbox').exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
expect(findDeleteModal().exists()).toBe(false);
}); });
}); });
......
import VueRouter from 'vue-router'; import VueRouter from 'vue-router';
import { shallowMount, createLocalVue } from '@vue/test-utils'; import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
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 store from '~/registry/explorer/stores/'; import store from '~/registry/explorer/stores/';
...@@ -17,7 +17,7 @@ describe('List Page', () => { ...@@ -17,7 +17,7 @@ describe('List Page', () => {
const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' }); const findDeleteBtn = () => wrapper.find({ ref: 'deleteImageButton' });
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(GlModal);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findImagesList = () => wrapper.find({ ref: 'imagesList' }); const findImagesList = () => wrapper.find({ ref: 'imagesList' });
const findRowItems = () => wrapper.findAll({ ref: 'rowItem' }); const findRowItems = () => wrapper.findAll({ ref: 'rowItem' });
const findEmptyState = () => wrapper.find(GlEmptyState); const findEmptyState = () => wrapper.find(GlEmptyState);
...@@ -71,7 +71,7 @@ describe('List Page', () => { ...@@ -71,7 +71,7 @@ describe('List Page', () => {
}); });
it('should not show the loading or default state', () => { it('should not show the loading or default state', () => {
expect(findLoadingIcon().exists()).toBe(false); expect(findSkeletonLoader().exists()).toBe(false);
expect(findImagesList().exists()).toBe(false); expect(findImagesList().exists()).toBe(false);
}); });
}); });
...@@ -81,8 +81,8 @@ describe('List Page', () => { ...@@ -81,8 +81,8 @@ describe('List Page', () => {
afterAll(() => store.commit(SET_MAIN_LOADING, false)); afterAll(() => store.commit(SET_MAIN_LOADING, false));
it('shows the loading icon', () => { it('shows the skeleton loader', () => {
expect(findLoadingIcon().exists()).toBe(true); expect(findSkeletonLoader().exists()).toBe(true);
}); });
it('imagesList is not visible', () => { it('imagesList is not visible', () => {
......
...@@ -180,10 +180,7 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -180,10 +180,7 @@ describe('Actions RegistryExplorer Store', () => {
{ {
tagsPagination: {}, tagsPagination: {},
}, },
[ [{ type: types.SET_MAIN_LOADING, payload: true }],
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[ [
{ {
type: 'requestTagsList', type: 'requestTagsList',
...@@ -220,13 +217,11 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -220,13 +217,11 @@ describe('Actions RegistryExplorer Store', () => {
}); });
describe('request delete multiple tags', () => { describe('request delete multiple tags', () => {
const id = 1; const url = `project-path/registry/repository/foo/tags`;
const params = window.btoa(JSON.stringify({ id })); const params = window.btoa(JSON.stringify({ tags_path: `${url}?format=json` }));
const projectPath = 'project-path';
const url = `${projectPath}/registry/repository/${id}/tags/bulk_destroy`;
it('successfully performs the delete request', done => { it('successfully performs the delete request', done => {
mock.onDelete(url).replyOnce(200); mock.onDelete(`${url}/bulk_destroy`).replyOnce(200);
testAction( testAction(
actions.requestDeleteTags, actions.requestDeleteTags,
...@@ -235,15 +230,9 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -235,15 +230,9 @@ describe('Actions RegistryExplorer Store', () => {
params, params,
}, },
{ {
config: {
projectPath,
},
tagsPagination: {}, tagsPagination: {},
}, },
[ [{ type: types.SET_MAIN_LOADING, payload: true }],
{ type: types.SET_MAIN_LOADING, payload: true },
{ type: types.SET_MAIN_LOADING, payload: false },
],
[ [
{ {
type: 'requestTagsList', type: 'requestTagsList',
...@@ -267,9 +256,6 @@ describe('Actions RegistryExplorer Store', () => { ...@@ -267,9 +256,6 @@ describe('Actions RegistryExplorer Store', () => {
params, params,
}, },
{ {
config: {
projectPath,
},
tagsPagination: {}, tagsPagination: {},
}, },
[ [
......
import * as getters from '~/registry/explorer/stores/getters';
describe('Getters RegistryExplorer store', () => {
let state;
const tags = ['foo', 'bar'];
describe('tags', () => {
describe('when isLoading is false', () => {
beforeEach(() => {
state = {
tags,
isLoading: false,
};
});
it('returns tags', () => {
expect(getters.tags(state)).toEqual(state.tags);
});
});
describe('when isLoading is true', () => {
beforeEach(() => {
state = {
tags,
isLoading: true,
};
});
it('returns empty array', () => {
expect(getters.tags(state)).toEqual([]);
});
});
});
});
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