Commit 438f0aff authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '198387-always-show-copyable-container-registry-build-push-commands' into 'master'

Always show copyable container registry build / push commands

See merge request gitlab-org/gitlab!27492
parents 51848311 148c854b
<script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { mapState } from 'vuex';
import { COPY_LOGIN_TITLE, COPY_BUILD_TITLE, COPY_PUSH_TITLE, QUICK_START } from '../constants';
export default {
name: 'ProjectEmptyState',
......@@ -11,20 +13,24 @@ export default {
GlSprintf,
GlLink,
},
i18n: {
quickStart: QUICK_START,
copyLoginTitle: COPY_LOGIN_TITLE,
copyBuildTitle: COPY_BUILD_TITLE,
copyPushTitle: COPY_PUSH_TITLE,
introText: s__(
`ContainerRegistry|With the Container Registry, every project can have its own space to store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`,
),
notLoggedInMessage: s__(
`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.`,
),
addImageText: s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
),
},
computed: {
...mapState(['config']),
dockerBuildCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `docker build -t ${this.config.repositoryUrl} .`;
},
dockerPushCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `docker push ${this.config.repositoryUrl}`;
},
dockerLoginCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `docker login ${this.config.registryHostUrlWithPort}`;
},
...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
},
};
</script>
......@@ -36,28 +42,15 @@ export default {
>
<template #description>
<p class="js-no-container-images-text">
<gl-sprintf
:message="
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
"
>
<gl-sprintf :message="$options.i18n.introText">
<template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</p>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5>
<h5>{{ $options.i18n.quickStart }}</h5>
<p class="js-not-logged-in-to-registry-text">
<gl-sprintf
:message="
s__(`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.`)
"
>
<gl-sprintf :message="$options.i18n.notLoggedInMessage">
<template #twofaDocLink="{content}">
<gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
</template>
......@@ -73,18 +66,14 @@ export default {
<span class="input-group-append">
<clipboard-button
:text="dockerLoginCommand"
:title="s__('ContainerRegistry|Copy login command')"
:title="$options.i18n.copyLoginTitle"
class="input-group-text"
/>
</span>
</div>
<p></p>
<p>
{{
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
{{ $options.i18n.addImageText }}
</p>
<div class="input-group append-bottom-10">
......@@ -92,7 +81,7 @@ export default {
<span class="input-group-append">
<clipboard-button
:text="dockerBuildCommand"
:title="s__('ContainerRegistry|Copy build command')"
:title="$options.i18n.copyBuildTitle"
class="input-group-text"
/>
</span>
......@@ -103,7 +92,7 @@ export default {
<span class="input-group-append">
<clipboard-button
:text="dockerPushCommand"
:title="s__('ContainerRegistry|Copy push command')"
:title="$options.i18n.copyPushTitle"
class="input-group-text"
/>
</span>
......
<script>
import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
COPY_LOGIN_TITLE,
BUILD_COMMAND_LABEL,
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
} from '../constants';
export default {
components: {
GlDropdown,
GlFormGroup,
GlFormInputGroup,
ClipboardButton,
},
i18n: {
dropdownTitle: QUICK_START,
loginCommandLabel: LOGIN_COMMAND_LABEL,
copyLoginTitle: COPY_LOGIN_TITLE,
buildCommandLabel: BUILD_COMMAND_LABEL,
copyBuildTitle: COPY_BUILD_TITLE,
pushCommandLabel: PUSH_COMMAND_LABEL,
copyPushTitle: COPY_PUSH_TITLE,
},
computed: {
...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
},
};
</script>
<template>
<gl-dropdown :text="$options.i18n.dropdownTitle" variant="primary" size="sm" right>
<!-- This li is used as a container since gl-dropdown produces a root ul, this mimics the functionality exposed by b-dropdown-form -->
<li role="presentation" class="px-2 py-1 dropdown-menu-large">
<form>
<gl-form-group
label-size="sm"
label-for="docker-login-btn"
:label="$options.i18n.loginCommandLabel"
>
<gl-form-input-group id="docker-login-btn" :value="dockerLoginCommand" readonly>
<template #append>
<clipboard-button
class="border"
:text="dockerLoginCommand"
:title="$options.i18n.copyLoginTitle"
/>
</template>
</gl-form-input-group>
</gl-form-group>
<gl-form-group
label-size="sm"
label-for="docker-build-btn"
:label="$options.i18n.buildCommandLabel"
>
<gl-form-input-group id="docker-build-btn" :value="dockerBuildCommand" readonly>
<template #append>
<clipboard-button
class="border"
:text="dockerBuildCommand"
:title="$options.i18n.copyBuildTitle"
/>
</template>
</gl-form-input-group>
</gl-form-group>
<gl-form-group
class="mb-0"
label-size="sm"
label-for="docker-push-btn"
:label="$options.i18n.pushCommandLabel"
>
<gl-form-input-group id="docker-push-btn" :value="dockerPushCommand" readonly>
<template #append>
<clipboard-button
class="border"
:text="dockerPushCommand"
:title="$options.i18n.copyPushTitle"
/>
</template>
</gl-form-input-group>
</gl-form-group>
</form>
</li>
</gl-dropdown>
</template>
......@@ -47,3 +47,11 @@ export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__(
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 QUICK_START = s__('ContainerRegistry|Quick Start');
export const LOGIN_COMMAND_LABEL = s__('ContainerRegistry|Login');
export const COPY_LOGIN_TITLE = s__('ContainerRegistry|Copy login command');
export const BUILD_COMMAND_LABEL = s__('ContainerRegistry|Build an image');
export const COPY_BUILD_TITLE = s__('ContainerRegistry|Copy build command');
export const PUSH_COMMAND_LABEL = s__('ContainerRegistry|Push an image');
export const COPY_PUSH_TITLE = s__('ContainerRegistry|Copy push command');
......@@ -16,6 +16,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
export default {
name: 'RegistryListApp',
......@@ -26,6 +27,7 @@ export default {
GroupEmptyState,
ProjectPolicyAlert,
ClipboardButton,
QuickstartDropdown,
GlButton,
GlIcon,
GlModal,
......@@ -62,6 +64,9 @@ export default {
this.requestImagesList({ page });
},
},
showQuickStartDropdown() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
},
methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']),
......@@ -114,7 +119,10 @@ export default {
<template v-else>
<div>
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<div class="d-flex justify-content-between align-items-center">
<h4>{{ s__('ContainerRegistry|Container Registry') }}</h4>
<quickstart-dropdown v-if="showQuickStartDropdown" class="d-none d-sm-block" />
</div>
<p>
<gl-sprintf
:message="
......
// 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;
};
export const dockerBuildCommand = state => {
/* eslint-disable @gitlab/require-i18n-strings */
return `docker build -t ${state.config.repositoryUrl} .`;
};
export const dockerPushCommand = state => {
/* eslint-disable @gitlab/require-i18n-strings */
return `docker push ${state.config.repositoryUrl}`;
};
export const dockerLoginCommand = state => {
/* eslint-disable @gitlab/require-i18n-strings */
return `docker login ${state.config.registryHostUrlWithPort}`;
};
......@@ -5274,6 +5274,9 @@ msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
msgid "ContainerRegistry|Build an image"
msgstr ""
msgid "ContainerRegistry|Container Registry"
msgstr ""
......@@ -5328,12 +5331,18 @@ msgstr ""
msgid "ContainerRegistry|Last Updated"
msgstr ""
msgid "ContainerRegistry|Login"
msgstr ""
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr ""
msgid "ContainerRegistry|Number of tags to retain:"
msgstr ""
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start"
msgstr ""
......
......@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs';
import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import * as getters from '~/registry/explorer/stores/getters';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -23,6 +24,7 @@ describe('Registry Project Empty state', () => {
noContainersImage: 'bazFoo',
},
},
getters,
});
wrapper = shallowMount(projectEmptyState, {
localVue,
......
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
import * as getters from '~/registry/explorer/stores/getters';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
QUICK_START,
LOGIN_COMMAND_LABEL,
COPY_LOGIN_TITLE,
BUILD_COMMAND_LABEL,
COPY_BUILD_TITLE,
PUSH_COMMAND_LABEL,
COPY_PUSH_TITLE,
} from '~/registry/explorer//constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('quickstart_dropdown', () => {
let wrapper;
let store;
const findDropdownButton = () => wrapper.find(GlDropdown);
const findFormGroups = () => wrapper.findAll(GlFormGroup);
const mountComponent = () => {
store = new Vuex.Store({
state: {
config: {
repositoryUrl: 'foo',
registryHostUrlWithPort: 'bar',
},
},
getters,
});
wrapper = mount(QuickstartDropdown, {
localVue,
store,
});
};
beforeEach(() => {
mountComponent();
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
store = null;
});
it('shows the correct text on the button', () => {
expect(findDropdownButton().text()).toContain(QUICK_START);
});
describe.each`
index | id | labelText | titleText | getter
${0} | ${'docker-login-btn'} | ${LOGIN_COMMAND_LABEL} | ${COPY_LOGIN_TITLE} | ${'dockerLoginCommand'}
${1} | ${'docker-build-btn'} | ${BUILD_COMMAND_LABEL} | ${COPY_BUILD_TITLE} | ${'dockerBuildCommand'}
${2} | ${'docker-push-btn'} | ${PUSH_COMMAND_LABEL} | ${COPY_PUSH_TITLE} | ${'dockerPushCommand'}
`('form group at $index', ({ index, id, labelText, titleText, getter }) => {
let formGroup;
const findFormInputGroup = parent => parent.find(GlFormInputGroup);
const findClipboardButton = parent => parent.find(ClipboardButton);
beforeEach(() => {
formGroup = findFormGroups().at(index);
});
it('exists', () => {
expect(formGroup.exists()).toBe(true);
});
it(`has a label ${labelText}`, () => {
expect(formGroup.text()).toBe(labelText);
});
it(`contains a form input group with ${id} id and with value equal to ${getter} getter`, () => {
const formInputGroup = findFormInputGroup(formGroup);
expect(formInputGroup.exists()).toBe(true);
expect(formInputGroup.attributes('id')).toBe(id);
expect(formInputGroup.props('value')).toBe(store.getters[getter]);
});
it(`contains a clipboard button with title of ${titleText} and text equal to ${getter} getter`, () => {
const clipBoardButton = findClipboardButton(formGroup);
expect(clipBoardButton.exists()).toBe(true);
expect(clipBoardButton.props('title')).toBe(titleText);
expect(clipBoardButton.props('text')).toBe(store.getters[getter]);
});
});
});
......@@ -3,6 +3,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue';
import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue';
import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import { imagesListResponse } from '../mock_data';
......@@ -24,6 +27,9 @@ describe('List Page', () => {
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
const findPagination = () => wrapper.find(GlPagination);
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
beforeEach(() => {
wrapper = shallowMount(component, {
......@@ -76,7 +82,7 @@ describe('List Page', () => {
});
});
describe('when isLoading is true', () => {
describe('isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true));
afterAll(() => store.commit(SET_MAIN_LOADING, false));
......@@ -88,9 +94,49 @@ describe('List Page', () => {
it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false);
});
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
});
});
describe('list is empty', () => {
beforeEach(() => {
store.dispatch('receiveImagesListSuccess', { data: [] });
});
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
});
it('project empty state is visible', () => {
expect(findProjectEmptyState().exists()).toBe(true);
});
describe('is group page is true', () => {
beforeAll(() => {
store.dispatch('setInitialState', { isGroupPage: true });
});
afterAll(() => {
store.dispatch('setInitialState', { isGroupPage: undefined });
});
it('group empty state is visible', () => {
expect(findGroupEmptyState().exists()).toBe(true);
});
it('quick start is not visible', () => {
expect(findQuickStartDropdown().exists()).toBe(false);
});
});
});
describe('list', () => {
describe('list is not empty', () => {
it('quick start is visible', () => {
expect(findQuickStartDropdown().exists()).toBe(true);
});
describe('listElement', () => {
let listElements;
let firstElement;
......
......@@ -31,4 +31,22 @@ describe('Getters RegistryExplorer store', () => {
});
});
});
describe.each`
getter | prefix | configParameter | suffix
${'dockerBuildCommand'} | ${'docker build -t'} | ${'repositoryUrl'} | ${'.'}
${'dockerPushCommand'} | ${'docker push'} | ${'repositoryUrl'} | ${null}
${'dockerLoginCommand'} | ${'docker login'} | ${'registryHostUrlWithPort'} | ${null}
`('$getter', ({ getter, prefix, configParameter, suffix }) => {
beforeEach(() => {
state = {
config: { repositoryUrl: 'foo', registryHostUrlWithPort: 'bar' },
};
});
it(`returns ${prefix} concatenated with ${configParameter} and optionally suffixed with ${suffix}`, () => {
const expectedPieces = [prefix, state.config[configParameter], suffix].filter(p => p);
expect(getters[getter](state)).toBe(expectedPieces.join(' '));
});
});
});
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