Commit 162d4ec8 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch...

Merge branch '216749-improve-the-container-registry-ui-header-section-with-relevant-metadata' into 'master'

Improve Container Registry UI Header

See merge request gitlab-org/gitlab!32424
parents 076d4f7d 5722ca93
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
EXPIRATION_POLICY_ALERT_TITLE,
EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
} from '../constants';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config', 'images', 'isLoading']),
isEmpty() {
return !this.images || this.images.length === 0;
},
showAlert() {
return this.config.expirationPolicy?.enabled;
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.config.expirationPolicy?.next_run_at);
return approximateDuration(difference / 1000);
},
alertConfiguration() {
if (this.isEmpty || this.isLoading) {
return {
title: null,
primaryButton: null,
message: EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
};
}
return {
title: EXPIRATION_POLICY_ALERT_TITLE,
primaryButton: EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
message: EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
};
},
},
};
</script>
<template>
<gl-alert
v-if="showAlert"
:dismissible="false"
:primary-button-text="alertConfiguration.primaryButton"
:primary-button-link="config.settingsPath"
:title="alertConfiguration.title"
>
<gl-sprintf :message="alertConfiguration.message">
<template #days>
<strong>{{ timeTillRun }}</strong>
</template>
<template #link="{content}">
<gl-link :href="config.expirationPolicyHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>
<script>
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
} from '../constants';
export default {
components: {
GlIcon,
GlSprintf,
GlLink,
},
props: {
expirationPolicy: {
type: Object,
default: () => ({}),
required: false,
},
imagesCount: {
type: Number,
default: 0,
required: false,
},
helpPagePath: {
type: String,
default: '',
required: false,
},
expirationPolicyHelpPagePath: {
type: String,
default: '',
required: false,
},
hideExpirationPolicyData: {
type: Boolean,
required: false,
default: false,
},
},
loader: {
repeat: 10,
width: 1000,
height: 40,
},
i18n: {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
},
computed: {
imagesCountText() {
return n__(
'ContainerRegistry|%{count} Image repository',
'ContainerRegistry|%{count} Image repositories',
this.imagesCount,
);
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
return approximateDuration(difference / 1000);
},
expirationPolicyEnabled() {
return this.expirationPolicy?.enabled;
},
expirationPolicyText() {
return this.expirationPolicyEnabled
? EXPIRATION_POLICY_WILL_RUN_IN
: EXPIRATION_POLICY_DISABLED_TEXT;
},
showExpirationPolicyTip() {
return (
!this.expirationPolicyEnabled && this.imagesCount > 0 && !this.hideExpirationPolicyData
);
},
},
};
</script>
<template>
<div>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center"
data-testid="header"
>
<h4 data-testid="title">{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4>
<div class="gl-display-none d-sm-block" data-testid="commands-slot">
<slot name="commands"></slot>
</div>
</div>
<div
v-if="imagesCount"
class="gl-display-flex gl-align-items-center gl-mt-1 gl-mb-3 gl-text-gray-700"
data-testid="subheader"
>
<span class="gl-mr-3" data-testid="images-count">
<gl-icon class="gl-mr-1" name="container-image" />
<gl-sprintf :message="imagesCountText">
<template #count>
{{ imagesCount }}
</template>
</gl-sprintf>
</span>
<span v-if="!hideExpirationPolicyData" data-testid="expiration-policy">
<gl-icon class="gl-mr-1" name="expire" />
<gl-sprintf :message="expirationPolicyText">
<template #time>
{{ timeTillRun }}
</template>
</gl-sprintf>
</span>
</div>
<div data-testid="info-area">
<p>
<span data-testid="default-intro">
<gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT">
<template #docLink="{content}">
<gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
<span v-if="showExpirationPolicyTip" data-testid="expiration-disabled-message">
<gl-sprintf :message="$options.i18n.EXPIRATION_POLICY_DISABLED_MESSAGE">
<template #docLink="{content}">
<gl-link :href="expirationPolicyHelpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</p>
</div>
</div>
</template>
...@@ -5,10 +5,10 @@ import { s__ } from '~/locale'; ...@@ -5,10 +5,10 @@ import { s__ } from '~/locale';
export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry'); export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry');
export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error'); export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error');
export const CONNECTION_ERROR_MESSAGE = s__( export const CONNECTION_ERROR_MESSAGE = s__(
`ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}`, `ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}`,
); );
export const LIST_INTRO_TEXT = s__( export const LIST_INTRO_TEXT = s__(
`ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`, `ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
); );
export const LIST_DELETE_BUTTON_DISABLED = s__( export const LIST_DELETE_BUTTON_DISABLED = s__(
...@@ -103,20 +103,21 @@ export const ADMIN_GARBAGE_COLLECTION_TIP = s__( ...@@ -103,20 +103,21 @@ export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
// Expiration policies // Expiration policies
export const EXPIRATION_POLICY_ALERT_TITLE = s__( export const EXPIRATION_POLICY_WILL_RUN_IN = s__(
'ContainerRegistry|Retention policy has been Enabled', 'ContainerRegistry|Expiration policy will run in %{time}',
); );
export const EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON = s__('ContainerRegistry|Edit Settings');
export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__( export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}', 'ContainerRegistry|Expiration policy is disabled',
); );
export const EXPIRATION_POLICY_ALERT_SHORT_MESSAGE = s__(
'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}', export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
); );
// Quick Start // Quick Start
export const QUICK_START = s__('ContainerRegistry|Quick Start'); export const QUICK_START = s__('ContainerRegistry|CLI Commands');
export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login'); export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command'); export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image'); export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');
......
...@@ -14,17 +14,15 @@ import Tracking from '~/tracking'; ...@@ -14,17 +14,15 @@ import Tracking from '~/tracking';
import ProjectEmptyState from '../components/project_empty_state.vue'; import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue'; import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue'; import RegistryHeader from '../components/registry_header.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
import ImageList from '../components/image_list.vue'; import ImageList from '../components/image_list.vue';
import CliCommands from '../components/cli_commands.vue';
import { import {
DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE,
CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE, CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE, CONNECTION_ERROR_MESSAGE,
LIST_INTRO_TEXT,
REMOVE_REPOSITORY_MODAL_TEXT, REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT, SEARCH_PLACEHOLDER_TEXT,
...@@ -39,8 +37,6 @@ export default { ...@@ -39,8 +37,6 @@ export default {
GlEmptyState, GlEmptyState,
ProjectEmptyState, ProjectEmptyState,
GroupEmptyState, GroupEmptyState,
ProjectPolicyAlert,
QuickstartDropdown,
ImageList, ImageList,
GlModal, GlModal,
GlSprintf, GlSprintf,
...@@ -48,6 +44,8 @@ export default { ...@@ -48,6 +44,8 @@ export default {
GlAlert, GlAlert,
GlSkeletonLoader, GlSkeletonLoader,
GlSearchBoxByClick, GlSearchBoxByClick,
RegistryHeader,
CliCommands,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -59,10 +57,8 @@ export default { ...@@ -59,10 +57,8 @@ export default {
height: 40, height: 40,
}, },
i18n: { i18n: {
CONTAINER_REGISTRY_TITLE,
CONNECTION_ERROR_TITLE, CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE, CONNECTION_ERROR_MESSAGE,
LIST_INTRO_TEXT,
REMOVE_REPOSITORY_MODAL_TEXT, REMOVE_REPOSITORY_MODAL_TEXT,
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
SEARCH_PLACEHOLDER_TEXT, SEARCH_PLACEHOLDER_TEXT,
...@@ -85,7 +81,7 @@ export default { ...@@ -85,7 +81,7 @@ export default {
label: 'registry_repository_delete', label: 'registry_repository_delete',
}; };
}, },
showQuickStartDropdown() { showCommands() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
}, },
showDeleteAlert() { showDeleteAlert() {
...@@ -149,8 +145,6 @@ export default { ...@@ -149,8 +145,6 @@ export default {
</gl-sprintf> </gl-sprintf>
</gl-alert> </gl-alert>
<project-policy-alert v-if="!config.isGroupPage" class="mt-2" />
<gl-empty-state <gl-empty-state
v-if="config.characterError" v-if="config.characterError"
:title="$options.i18n.CONNECTION_ERROR_TITLE" :title="$options.i18n.CONNECTION_ERROR_TITLE"
...@@ -170,21 +164,17 @@ export default { ...@@ -170,21 +164,17 @@ export default {
</gl-empty-state> </gl-empty-state>
<template v-else> <template v-else>
<div> <registry-header
<div class="d-flex justify-content-between align-items-center"> :images-count="pagination.total"
<h4>{{ $options.i18n.CONTAINER_REGISTRY_TITLE }}</h4> :expiration-policy="config.expirationPolicy"
<quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" /> :help-page-path="config.helpPagePath"
</div> :expiration-policy-help-page-path="config.expirationPolicyHelpPagePath"
<p> :hide-expiration-policy-data="config.isGroupPage"
<gl-sprintf :message="$options.i18n.LIST_INTRO_TEXT"> >
<template #docLink="{content}"> <template #commands>
<gl-link :href="config.helpPagePath" target="_blank"> <cli-commands v-if="showCommands" />
{{ content }} </template>
</gl-link> </registry-header>
</template>
</gl-sprintf>
</p>
</div>
<div v-if="isLoading" class="mt-2"> <div v-if="isLoading" class="mt-2">
<gl-skeleton-loader <gl-skeleton-loader
...@@ -201,7 +191,7 @@ export default { ...@@ -201,7 +191,7 @@ export default {
</div> </div>
<template v-else> <template v-else>
<template v-if="!isEmpty"> <template v-if="!isEmpty">
<div class="gl-display-flex gl-p-1" data-testid="listHeader"> <div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader">
<div class="gl-flex-fill-1"> <div class="gl-flex-fill-1">
<h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5> <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5>
</div> </div>
......
...@@ -5,7 +5,6 @@ ...@@ -5,7 +5,6 @@
.row.registry-placeholder.prepend-bottom-10 .row.registry-placeholder.prepend-bottom-10
.col-12 .col-12
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
settings_path: project_settings_ci_cd_path(@project),
expiration_policy: @project.container_expiration_policy.to_json, expiration_policy: @project.container_expiration_policy.to_json,
"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'),
......
---
title: Improve Container Registry UI header
merge_request: 32424
author:
type: changed
...@@ -26,7 +26,7 @@ have its own space to store its Docker images. ...@@ -26,7 +26,7 @@ have its own space to store its Docker images.
You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>. You can read more about Docker Registry at <https://docs.docker.com/registry/introduction/>.
![Container Registry repositories](img/container_registry_repositories_v13_0.png) ![Container Registry repositories](img/container_registry_repositories_v13_1.png)
## Enable the Container Registry for your project ## Enable the Container Registry for your project
...@@ -62,7 +62,7 @@ for both projects and groups. ...@@ -62,7 +62,7 @@ for both projects and groups.
Navigate to your project's **{package}** **Packages & Registries > Container Registry**. Navigate to your project's **{package}** **Packages & Registries > Container Registry**.
![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_0.png) ![Container Registry project repositories](img/container_registry_repositories_with_quickstart_v13_1.png)
This view will: This view will:
...@@ -77,7 +77,7 @@ This view will: ...@@ -77,7 +77,7 @@ This view will:
Navigate to your groups's **{package}** **Packages & Registries > Container Registry**. Navigate to your groups's **{package}** **Packages & Registries > Container Registry**.
![Container Registry group repositories](img/container_registry_group_repositories_v13_0.png) ![Container Registry group repositories](img/container_registry_group_repositories_v13_1.png)
This view will: This view will:
......
...@@ -5839,6 +5839,11 @@ msgstr "" ...@@ -5839,6 +5839,11 @@ msgstr ""
msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature." msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
msgstr "" msgstr ""
msgid "ContainerRegistry|%{count} Image repository"
msgid_plural "ContainerRegistry|%{count} Image repositories"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags" msgid "ContainerRegistry|%{imageName} tags"
msgstr "" msgstr ""
...@@ -5851,6 +5856,9 @@ msgstr "" ...@@ -5851,6 +5856,9 @@ msgstr ""
msgid "ContainerRegistry|Build an image" msgid "ContainerRegistry|Build an image"
msgstr "" msgstr ""
msgid "ContainerRegistry|CLI Commands"
msgstr ""
msgid "ContainerRegistry|Compressed Size" msgid "ContainerRegistry|Compressed Size"
msgstr "" msgstr ""
...@@ -5875,15 +5883,21 @@ msgstr "" ...@@ -5875,15 +5883,21 @@ msgstr ""
msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}" msgid "ContainerRegistry|Docker tag expiration policy is %{toggleStatus}"
msgstr "" msgstr ""
msgid "ContainerRegistry|Edit Settings" msgid "ContainerRegistry|Expiration interval:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Expiration interval:" msgid "ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|Expiration policy is disabled"
msgstr "" msgstr ""
msgid "ContainerRegistry|Expiration policy successfully saved." msgid "ContainerRegistry|Expiration policy successfully saved."
msgstr "" msgstr ""
msgid "ContainerRegistry|Expiration policy will run in %{time}"
msgstr ""
msgid "ContainerRegistry|Expiration policy:" msgid "ContainerRegistry|Expiration policy:"
msgstr "" msgstr ""
...@@ -5923,9 +5937,6 @@ msgstr "" ...@@ -5923,9 +5937,6 @@ msgstr ""
msgid "ContainerRegistry|Push an image" msgid "ContainerRegistry|Push an image"
msgstr "" msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported" msgid "ContainerRegistry|Regular expressions such as %{codeStart}.*-master%{codeEnd} or %{codeStart}release-.*%{codeEnd} are supported"
msgstr "" msgstr ""
...@@ -5946,9 +5957,6 @@ msgid_plural "ContainerRegistry|Remove tags" ...@@ -5946,9 +5957,6 @@ msgid_plural "ContainerRegistry|Remove tags"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the expiration policy." msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr "" msgstr ""
...@@ -6000,12 +6008,6 @@ msgstr "" ...@@ -6000,12 +6008,6 @@ msgstr ""
msgid "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. If you have any questions, contact your administrator." msgid "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. If you have any questions, contact your administrator."
msgstr "" msgstr ""
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The value of this input should be less than 255 characters" msgid "ContainerRegistry|The value of this input should be less than 255 characters"
msgstr "" msgstr ""
...@@ -6027,7 +6029,7 @@ msgstr "" ...@@ -6027,7 +6029,7 @@ msgstr ""
msgid "ContainerRegistry|To widen your search, change or remove the filters above." msgid "ContainerRegistry|To widen your search, change or remove the filters above."
msgstr "" msgstr ""
msgid "ContainerRegistry|We are having trouble connecting to Docker, which could be due to an issue with your project name or path. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|We are having trouble connecting to the Registry, which could be due to an issue with your project name or path. %{docLinkStart}More information%{docLinkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}"
...@@ -6036,7 +6038,7 @@ msgstr "" ...@@ -6036,7 +6038,7 @@ msgstr ""
msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. Push at least one Docker image in one of this group's projects in order to show up here. %{docLinkStart}More Information%{docLinkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|With the Docker Container Registry integrated into GitLab, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}" msgid "ContainerRegistry|With the GitLab Container Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?" msgid "ContainerRegistry|You are about to remove %{item} tags. Are you sure?"
......
...@@ -30,10 +30,10 @@ describe 'Container Registry', :js do ...@@ -30,10 +30,10 @@ describe 'Container Registry', :js do
expect(page).to have_content _('There are no container images stored for this project') expect(page).to have_content _('There are no container images stored for this project')
end end
it 'list page has quickstart' do it 'list page has cli commands' do
visit_container_registry visit_container_registry
expect(page).to have_content _('Quick Start') expect(page).to have_content _('CLI Commands')
end end
end end
......
...@@ -19,7 +19,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` ...@@ -19,7 +19,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = `
</p> </p>
<h5> <h5>
Quick Start CLI Commands
</h5> </h5>
<p <p
......
...@@ -3,7 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; ...@@ -3,7 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui'; import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import * as getters from '~/registry/explorer/stores/getters'; import * as getters from '~/registry/explorer/stores/getters';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; import QuickstartDropdown from '~/registry/explorer/components/cli_commands.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { import {
...@@ -19,7 +19,7 @@ import { ...@@ -19,7 +19,7 @@ import {
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
describe('quickstart_dropdown', () => { describe('cli_commands', () => {
let wrapper; let wrapper;
let store; let store;
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import * as dateTimeUtils from '~/lib/utils/datetime_utility';
import component from '~/registry/explorer/components/project_policy_alert.vue';
import {
EXPIRATION_POLICY_ALERT_TITLE,
EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
} from '~/registry/explorer/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Project Policy Alert', () => {
let wrapper;
let store;
const defaultState = {
config: {
expirationPolicy: {
enabled: true,
},
settingsPath: 'foo',
expirationPolicyHelpPagePath: 'bar',
},
images: [],
isLoading: false,
};
const findAlert = () => wrapper.find(GlAlert);
const findLink = () => wrapper.find(GlLink);
const createComponent = (state = defaultState) => {
store = new Vuex.Store({
state,
});
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlSprintf,
},
});
};
const documentationExpectation = () => {
it('contain a documentation link', () => {
createComponent();
expect(findLink().attributes('href')).toBe(defaultState.config.expirationPolicyHelpPagePath);
expect(findLink().text()).toBe('documentation');
});
};
beforeEach(() => {
jest.spyOn(dateTimeUtils, 'approximateDuration').mockReturnValue('1 day');
});
afterEach(() => {
wrapper.destroy();
});
describe('is hidden', () => {
it('when expiration policy does not exist', () => {
createComponent({ config: {} });
expect(findAlert().exists()).toBe(false);
});
it('when expiration policy exist but is disabled', () => {
createComponent({
...defaultState,
config: {
expirationPolicy: {
enabled: false,
},
},
});
expect(findAlert().exists()).toBe(false);
});
});
describe('is visible', () => {
it('when expiration policy exists and is enabled', () => {
createComponent();
expect(findAlert().exists()).toBe(true);
});
});
describe('full info alert', () => {
beforeEach(() => {
createComponent({ ...defaultState, images: [1] });
});
it('has a primary button', () => {
const alert = findAlert();
expect(alert.props('primaryButtonText')).toBe(EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON);
expect(alert.props('primaryButtonLink')).toBe(defaultState.config.settingsPath);
});
it('has a title', () => {
const alert = findAlert();
expect(alert.props('title')).toBe(EXPIRATION_POLICY_ALERT_TITLE);
});
it('has the full message', () => {
expect(findAlert().html()).toContain('<strong>1 day</strong>');
});
documentationExpectation();
});
describe('compact info alert', () => {
beforeEach(() => {
createComponent({ ...defaultState, images: [] });
});
it('does not have a button', () => {
const alert = findAlert();
expect(alert.props('primaryButtonText')).toBe(null);
});
it('does not have a title', () => {
const alert = findAlert();
expect(alert.props('title')).toBe(null);
});
it('has the short message', () => {
expect(findAlert().html()).not.toContain('<strong>1 day</strong>');
});
documentationExpectation();
});
});
import { shallowMount } from '@vue/test-utils';
import { GlSprintf, GlLink } from '@gitlab/ui';
import Component from '~/registry/explorer/components/registry_header.vue';
import {
CONTAINER_REGISTRY_TITLE,
LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE,
EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
} from '~/registry/explorer/constants';
jest.mock('~/lib/utils/datetime_utility', () => ({
approximateDuration: jest.fn(),
calculateRemainingMilliseconds: jest.fn(),
}));
describe('registry_header', () => {
let wrapper;
const findHeader = () => wrapper.find('[data-testid="header"]');
const findTitle = () => wrapper.find('[data-testid="title"]');
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findInfoArea = () => wrapper.find('[data-testid="info-area"]');
const findIntroText = () => wrapper.find('[data-testid="default-intro"]');
const findSubHeader = () => wrapper.find('[data-testid="subheader"]');
const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]');
const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]');
const findDisabledExpirationPolicyMessage = () =>
wrapper.find('[data-testid="expiration-disabled-message"]');
const mountComponent = (propsData, slots) => {
wrapper = shallowMount(Component, {
stubs: {
GlSprintf,
},
propsData,
slots,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('header', () => {
it('exists', () => {
mountComponent();
expect(findHeader().exists()).toBe(true);
});
it('contains the title of the page', () => {
mountComponent();
const title = findTitle();
expect(title.exists()).toBe(true);
expect(title.text()).toBe(CONTAINER_REGISTRY_TITLE);
});
it('has a commands slot', () => {
mountComponent(null, { commands: 'baz' });
expect(findCommandsSlot().text()).toBe('baz');
});
});
describe('subheader', () => {
describe('when there are no images', () => {
it('is hidden ', () => {
mountComponent();
expect(findSubHeader().exists()).toBe(false);
});
});
describe('when there are images', () => {
it('is visible', () => {
mountComponent({ imagesCount: 1 });
expect(findSubHeader().exists()).toBe(true);
});
describe('sub header parts', () => {
describe('images count', () => {
it('exists', () => {
mountComponent({ imagesCount: 1 });
expect(findImagesCountSubHeader().exists()).toBe(true);
});
it('when there is one image', () => {
mountComponent({ imagesCount: 1 });
expect(findImagesCountSubHeader().text()).toMatchInterpolatedText('1 Image repository');
});
it('when there is more than one image', () => {
mountComponent({ imagesCount: 3 });
expect(findImagesCountSubHeader().text()).toMatchInterpolatedText(
'3 Image repositories',
);
});
});
describe('expiration policy', () => {
it('when is disabled', () => {
mountComponent({
expirationPolicy: { enabled: false },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(true);
expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_DISABLED_TEXT);
});
it('when is enabled', () => {
mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(true);
expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_WILL_RUN_IN);
});
it('when the expiration policy is completely disabled', () => {
mountComponent({
expirationPolicy: { enabled: true },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
hideExpirationPolicyData: true,
});
const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(false);
});
});
});
});
});
describe('info area', () => {
it('exists', () => {
mountComponent();
expect(findInfoArea().exists()).toBe(true);
});
describe('default message', () => {
beforeEach(() => {
mountComponent({ helpPagePath: 'bar' });
});
it('exists', () => {
expect(findIntroText().exists()).toBe(true);
});
it('has the correct copy', () => {
expect(findIntroText().text()).toMatchInterpolatedText(LIST_INTRO_TEXT);
});
it('has the correct link', () => {
expect(
findIntroText()
.find(GlLink)
.attributes('href'),
).toBe('bar');
});
});
describe('expiration policy info message', () => {
describe('when there are no images', () => {
it('is hidden', () => {
mountComponent();
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
describe('when there are images', () => {
describe('when expiration policy is disabled', () => {
beforeEach(() => {
mountComponent({
expirationPolicy: { enabled: false },
expirationPolicyHelpPagePath: 'foo',
imagesCount: 1,
});
});
it('message exist', () => {
expect(findDisabledExpirationPolicyMessage().exists()).toBe(true);
});
it('has the correct copy', () => {
expect(findDisabledExpirationPolicyMessage().text()).toMatchInterpolatedText(
EXPIRATION_POLICY_DISABLED_MESSAGE,
);
});
it('has the correct link', () => {
expect(
findDisabledExpirationPolicyMessage()
.find(GlLink)
.attributes('href'),
).toBe('foo');
});
});
describe('when expiration policy is enabled', () => {
it('message does not exist', () => {
mountComponent({
expirationPolicy: { enabled: true },
imagesCount: 1,
});
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
describe('when the expiration policy is completely disabled', () => {
it('message does not exist', () => {
mountComponent({
expirationPolicy: { enabled: true },
imagesCount: 1,
hideExpirationPolicyData: true,
});
expect(findDisabledExpirationPolicyMessage().exists()).toBe(false);
});
});
});
});
});
});
...@@ -3,10 +3,10 @@ import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitla ...@@ -3,10 +3,10 @@ import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitla
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import component from '~/registry/explorer/pages/list.vue'; import component from '~/registry/explorer/pages/list.vue';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; import CliCommands from '~/registry/explorer/components/cli_commands.vue';
import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue'; import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue'; import RegistryHeader from '~/registry/explorer/components/registry_header.vue';
import ImageList from '~/registry/explorer/components/image_list.vue'; import ImageList from '~/registry/explorer/components/image_list.vue';
import { createStore } from '~/registry/explorer/stores/'; import { createStore } from '~/registry/explorer/stores/';
import { import {
...@@ -32,14 +32,14 @@ describe('List Page', () => { ...@@ -32,14 +32,14 @@ describe('List Page', () => {
const findDeleteModal = () => wrapper.find(GlModal); const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findImagesList = () => wrapper.find({ ref: 'imagesList' });
const findEmptyState = () => wrapper.find(GlEmptyState); const findEmptyState = () => wrapper.find(GlEmptyState);
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown); const findCliCommands = () => wrapper.find(CliCommands);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState); const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState); const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert); const findRegistryHeader = () => wrapper.find(RegistryHeader);
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 findListHeader = () => wrapper.find('[data-testid="listHeader"]');
...@@ -53,6 +53,7 @@ describe('List Page', () => { ...@@ -53,6 +53,7 @@ describe('List Page', () => {
GlModal, GlModal,
GlEmptyState, GlEmptyState,
GlSprintf, GlSprintf,
RegistryHeader,
}, },
mocks: { mocks: {
$toast, $toast,
...@@ -76,21 +77,6 @@ describe('List Page', () => { ...@@ -76,21 +77,6 @@ describe('List Page', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('Expiration policy notification', () => {
beforeEach(() => {
mountComponent();
});
it('shows up on project page', () => {
expect(findProjectPolicyAlert().exists()).toBe(true);
});
it('does show up on group page', () => {
store.commit(SET_INITIAL_STATE, { isGroupPage: true });
return wrapper.vm.$nextTick().then(() => {
expect(findProjectPolicyAlert().exists()).toBe(false);
});
});
});
describe('API calls', () => { describe('API calls', () => {
it.each` it.each`
imageList | name | called imageList | name | called
...@@ -109,6 +95,11 @@ describe('List Page', () => { ...@@ -109,6 +95,11 @@ describe('List Page', () => {
); );
}); });
it('contains registry header', () => {
mountComponent();
expect(findRegistryHeader().exists()).toBe(true);
});
describe('connection error', () => { describe('connection error', () => {
const config = { const config = {
characterError: true, characterError: true,
...@@ -139,7 +130,7 @@ describe('List Page', () => { ...@@ -139,7 +130,7 @@ describe('List Page', () => {
it('should not show the loading or default state', () => { it('should not show the loading or default state', () => {
expect(findSkeletonLoader().exists()).toBe(false); expect(findSkeletonLoader().exists()).toBe(false);
expect(findImagesList().exists()).toBe(false); expect(findImageList().exists()).toBe(false);
}); });
}); });
...@@ -156,11 +147,11 @@ describe('List Page', () => { ...@@ -156,11 +147,11 @@ describe('List Page', () => {
}); });
it('imagesList is not visible', () => { it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false); expect(findImageList().exists()).toBe(false);
}); });
it('quick start is not visible', () => { it('cli commands is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
}); });
...@@ -171,8 +162,8 @@ describe('List Page', () => { ...@@ -171,8 +162,8 @@ describe('List Page', () => {
return waitForPromises(); return waitForPromises();
}); });
it('quick start is not visible', () => { it('cli commands is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
it('project empty state is visible', () => { it('project empty state is visible', () => {
...@@ -193,8 +184,8 @@ describe('List Page', () => { ...@@ -193,8 +184,8 @@ describe('List Page', () => {
expect(findGroupEmptyState().exists()).toBe(true); expect(findGroupEmptyState().exists()).toBe(true);
}); });
it('quick start is not visible', () => { it('cli commands is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false); expect(findCliCommands().exists()).toBe(false);
}); });
it('list header is not visible', () => { it('list header is not visible', () => {
...@@ -210,7 +201,7 @@ describe('List Page', () => { ...@@ -210,7 +201,7 @@ describe('List Page', () => {
}); });
it('quick start is visible', () => { it('quick start is visible', () => {
expect(findQuickStartDropdown().exists()).toBe(true); expect(findCliCommands().exists()).toBe(true);
}); });
it('list component is visible', () => { it('list component is visible', () => {
......
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