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> <script>
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui'; 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 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 { export default {
name: 'ProjectEmptyState', name: 'ProjectEmptyState',
...@@ -11,20 +13,24 @@ export default { ...@@ -11,20 +13,24 @@ export default {
GlSprintf, GlSprintf,
GlLink, 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: { computed: {
...mapState(['config']), ...mapState(['config']),
dockerBuildCommand() { ...mapGetters(['dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand']),
// 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}`;
},
}, },
}; };
</script> </script>
...@@ -36,28 +42,15 @@ export default { ...@@ -36,28 +42,15 @@ export default {
> >
<template #description> <template #description>
<p class="js-no-container-images-text"> <p class="js-no-container-images-text">
<gl-sprintf <gl-sprintf :message="$options.i18n.introText">
:message="
s__(`ContainerRegistry|With the Container Registry, every project can have its own space to
store its Docker images. %{docLinkStart}More Information%{docLinkEnd}`)
"
>
<template #docLink="{content}"> <template #docLink="{content}">
<gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link> <gl-link :href="config.helpPagePath" target="_blank">{{ content }}</gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p> </p>
<h5>{{ s__('ContainerRegistry|Quick Start') }}</h5> <h5>{{ $options.i18n.quickStart }}</h5>
<p class="js-not-logged-in-to-registry-text"> <p class="js-not-logged-in-to-registry-text">
<gl-sprintf <gl-sprintf :message="$options.i18n.notLoggedInMessage">
: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.`)
"
>
<template #twofaDocLink="{content}"> <template #twofaDocLink="{content}">
<gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link> <gl-link :href="config.twoFactorAuthHelpLink" target="_blank">{{ content }}</gl-link>
</template> </template>
...@@ -73,18 +66,14 @@ export default { ...@@ -73,18 +66,14 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerLoginCommand" :text="dockerLoginCommand"
:title="s__('ContainerRegistry|Copy login command')" :title="$options.i18n.copyLoginTitle"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
</div> </div>
<p></p> <p></p>
<p> <p>
{{ {{ $options.i18n.addImageText }}
s__(
'ContainerRegistry|You can add an image to this registry with the following commands:',
)
}}
</p> </p>
<div class="input-group append-bottom-10"> <div class="input-group append-bottom-10">
...@@ -92,7 +81,7 @@ export default { ...@@ -92,7 +81,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerBuildCommand" :text="dockerBuildCommand"
:title="s__('ContainerRegistry|Copy build command')" :title="$options.i18n.copyBuildTitle"
class="input-group-text" class="input-group-text"
/> />
</span> </span>
...@@ -103,7 +92,7 @@ export default { ...@@ -103,7 +92,7 @@ export default {
<span class="input-group-append"> <span class="input-group-append">
<clipboard-button <clipboard-button
:text="dockerPushCommand" :text="dockerPushCommand"
:title="s__('ContainerRegistry|Copy push command')" :title="$options.i18n.copyPushTitle"
class="input-group-text" class="input-group-text"
/> />
</span> </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__( ...@@ -47,3 +47,11 @@ export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__(
export const EXPIRATION_POLICY_ALERT_SHORT_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}', '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'; ...@@ -16,6 +16,7 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
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 ProjectPolicyAlert from '../components/project_policy_alert.vue';
import QuickstartDropdown from '../components/quickstart_dropdown.vue';
export default { export default {
name: 'RegistryListApp', name: 'RegistryListApp',
...@@ -26,6 +27,7 @@ export default { ...@@ -26,6 +27,7 @@ export default {
GroupEmptyState, GroupEmptyState,
ProjectPolicyAlert, ProjectPolicyAlert,
ClipboardButton, ClipboardButton,
QuickstartDropdown,
GlButton, GlButton,
GlIcon, GlIcon,
GlModal, GlModal,
...@@ -62,6 +64,9 @@ export default { ...@@ -62,6 +64,9 @@ export default {
this.requestImagesList({ page }); this.requestImagesList({ page });
}, },
}, },
showQuickStartDropdown() {
return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length);
},
}, },
methods: { methods: {
...mapActions(['requestImagesList', 'requestDeleteImage']), ...mapActions(['requestImagesList', 'requestDeleteImage']),
...@@ -114,7 +119,10 @@ export default { ...@@ -114,7 +119,10 @@ export default {
<template v-else> <template v-else>
<div> <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> <p>
<gl-sprintf <gl-sprintf
:message=" :message="
......
// eslint-disable-next-line import/prefer-default-export
export const tags = state => { 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 // 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 // this is to take in account isLoading = true and state.tags =[1,2,3] during pagination and delete
return state.isLoading ? [] : state.tags; 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 "" ...@@ -5274,6 +5274,9 @@ msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept." msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr "" msgstr ""
msgid "ContainerRegistry|Build an image"
msgstr ""
msgid "ContainerRegistry|Container Registry" msgid "ContainerRegistry|Container Registry"
msgstr "" msgstr ""
...@@ -5328,12 +5331,18 @@ msgstr "" ...@@ -5328,12 +5331,18 @@ msgstr ""
msgid "ContainerRegistry|Last Updated" msgid "ContainerRegistry|Last Updated"
msgstr "" msgstr ""
msgid "ContainerRegistry|Login"
msgstr ""
msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled" msgid "ContainerRegistry|Missing or insufficient permission, delete button disabled"
msgstr "" msgstr ""
msgid "ContainerRegistry|Number of tags to retain:" msgid "ContainerRegistry|Number of tags to retain:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Push an image"
msgstr ""
msgid "ContainerRegistry|Quick Start" msgid "ContainerRegistry|Quick Start"
msgstr "" msgstr ""
......
...@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; ...@@ -3,6 +3,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { GlEmptyState } from '../stubs'; import { GlEmptyState } from '../stubs';
import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; import projectEmptyState from '~/registry/explorer/components/project_empty_state.vue';
import * as getters from '~/registry/explorer/stores/getters';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -23,6 +24,7 @@ describe('Registry Project Empty state', () => { ...@@ -23,6 +24,7 @@ describe('Registry Project Empty state', () => {
noContainersImage: 'bazFoo', noContainersImage: 'bazFoo',
}, },
}, },
getters,
}); });
wrapper = shallowMount(projectEmptyState, { wrapper = shallowMount(projectEmptyState, {
localVue, 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'; ...@@ -3,6 +3,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination, GlSkeletonLoader, 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 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 store from '~/registry/explorer/stores/';
import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/';
import { imagesListResponse } from '../mock_data'; import { imagesListResponse } from '../mock_data';
...@@ -24,6 +27,9 @@ describe('List Page', () => { ...@@ -24,6 +27,9 @@ describe('List Page', () => {
const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' }); const findDetailsLink = () => wrapper.find({ ref: 'detailsLink' });
const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' }); const findClipboardButton = () => wrapper.find({ ref: 'clipboardButton' });
const findPagination = () => wrapper.find(GlPagination); const findPagination = () => wrapper.find(GlPagination);
const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
beforeEach(() => { beforeEach(() => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
...@@ -76,7 +82,7 @@ describe('List Page', () => { ...@@ -76,7 +82,7 @@ describe('List Page', () => {
}); });
}); });
describe('when isLoading is true', () => { describe('isLoading is true', () => {
beforeAll(() => store.commit(SET_MAIN_LOADING, true)); beforeAll(() => store.commit(SET_MAIN_LOADING, true));
afterAll(() => store.commit(SET_MAIN_LOADING, false)); afterAll(() => store.commit(SET_MAIN_LOADING, false));
...@@ -88,9 +94,49 @@ describe('List Page', () => { ...@@ -88,9 +94,49 @@ describe('List Page', () => {
it('imagesList is not visible', () => { it('imagesList is not visible', () => {
expect(findImagesList().exists()).toBe(false); 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', () => { describe('listElement', () => {
let listElements; let listElements;
let firstElement; let firstElement;
......
...@@ -31,4 +31,22 @@ describe('Getters RegistryExplorer store', () => { ...@@ -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