Commit a9f5971a authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch 'jivanvl-add-clear-cache-button-dependency-proxy' into 'master'

Add clear cache button to dependency proxy

See merge request gitlab-org/gitlab!83786
parents da506038 b5753a52
...@@ -93,6 +93,7 @@ const Api = { ...@@ -93,6 +93,7 @@ const Api = {
notificationSettingsPath: '/api/:version/notification_settings', notificationSettingsPath: '/api/:version/notification_settings',
deployKeysPath: '/api/:version/deploy_keys', deployKeysPath: '/api/:version/deploy_keys',
secureFilesPath: '/api/:version/projects/:project_id/secure_files', secureFilesPath: '/api/:version/projects/:project_id/secure_files',
dependencyProxyPath: '/api/:version/groups/:id/dependency_proxy/cache',
group(groupId, callback = () => {}) { group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -999,6 +1000,12 @@ const Api = { ...@@ -999,6 +1000,12 @@ const Api = {
return result; return result;
}, },
deleteDependencyProxyCacheList(groupId, options = {}) {
const url = Api.buildUrl(this.dependencyProxyPath).replace(':id', groupId);
return axios.delete(url, { params: { ...options } });
},
}; };
export default Api; export default Api;
<script> <script>
import { import {
GlAlert, GlAlert,
GlDropdown,
GlDropdownItem,
GlEmptyState, GlEmptyState,
GlFormGroup, GlFormGroup,
GlFormInputGroup, GlFormInputGroup,
GlModal,
GlModalDirective,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import { s__ } from '~/locale'; import { __, s__, n__, sprintf } from '~/locale';
import Api from '~/api';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue'; import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
...@@ -22,16 +27,22 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency ...@@ -22,16 +27,22 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency
export default { export default {
components: { components: {
GlAlert, GlAlert,
GlDropdown,
GlDropdownItem,
GlEmptyState, GlEmptyState,
GlFormGroup, GlFormGroup,
GlFormInputGroup, GlFormInputGroup,
GlModal,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
ClipboardButton, ClipboardButton,
TitleArea, TitleArea,
ManifestsList, ManifestsList,
}, },
inject: ['groupPath', 'dependencyProxyAvailable', 'noManifestsIllustration'], directives: {
GlModalDirective,
},
inject: ['groupPath', 'groupId', 'dependencyProxyAvailable', 'noManifestsIllustration'],
i18n: { i18n: {
proxyNotAvailableText: s__( proxyNotAvailableText: s__(
'DependencyProxy|Dependency Proxy feature is limited to public groups for now.', 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.',
...@@ -41,6 +52,19 @@ export default { ...@@ -41,6 +52,19 @@ export default {
blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'), blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'),
pageTitle: s__('DependencyProxy|Dependency Proxy'), pageTitle: s__('DependencyProxy|Dependency Proxy'),
noManifestTitle: s__('DependencyProxy|There are no images in the cache'), noManifestTitle: s__('DependencyProxy|There are no images in the cache'),
deleteCacheAlertMessageSuccess: s__(
'DependencyProxy|All items in the cache are scheduled for removal.',
),
},
confirmClearCacheModal: 'confirm-clear-cache-modal',
modalButtons: {
primary: {
text: s__('DependencyProxy|Clear cache'),
attributes: [{ variant: 'danger' }],
},
secondary: {
text: __('Cancel'),
},
}, },
links: { links: {
DEPENDENCY_PROXY_DOCS_PATH, DEPENDENCY_PROXY_DOCS_PATH,
...@@ -48,6 +72,8 @@ export default { ...@@ -48,6 +72,8 @@ export default {
data() { data() {
return { return {
group: {}, group: {},
showDeleteCacheAlert: false,
deleteCacheAlertMessage: '',
}; };
}, },
apollo: { apollo: {
...@@ -80,6 +106,33 @@ export default { ...@@ -80,6 +106,33 @@ export default {
manifests() { manifests() {
return this.group.dependencyProxyManifests.nodes; return this.group.dependencyProxyManifests.nodes;
}, },
modalTitleWithCount() {
return sprintf(
n__(
'Clear %{count} image from cache?',
'Clear %{count} images from cache?',
this.group.dependencyProxyBlobCount,
),
{
count: this.group.dependencyProxyBlobCount,
},
);
},
modalConfirmationMessageWithCount() {
return sprintf(
n__(
'You are about to clear %{count} image from the cache. Once you confirm, the next time a pipeline runs it must pull an image or tag from Docker Hub. Are you sure?',
'You are about to clear %{count} images from the cache. Once you confirm, the next time a pipeline runs it must pull an image or tag from Docker Hub. Are you sure?',
this.group.dependencyProxyBlobCount,
),
{
count: this.group.dependencyProxyBlobCount,
},
);
},
showDeleteDropdown() {
return this.group.dependencyProxyBlobCount > 0;
},
}, },
methods: { methods: {
fetchNextPage() { fetchNextPage() {
...@@ -103,13 +156,47 @@ export default { ...@@ -103,13 +156,47 @@ export default {
}, },
}); });
}, },
async submit() {
try {
await Api.deleteDependencyProxyCacheList(this.groupId);
this.deleteCacheAlertMessage = this.$options.i18n.deleteCacheAlertMessageSuccess;
this.showDeleteCacheAlert = true;
} catch (err) {
this.deleteCacheAlertMessage = err;
this.showDeleteCacheAlert = true;
}
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages" /> <gl-alert
v-if="showDeleteCacheAlert"
data-testid="delete-cache-alert"
@dismiss="showDeleteCacheAlert = false"
>
{{ deleteCacheAlertMessage }}
</gl-alert>
<title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages">
<template v-if="showDeleteDropdown" #right-actions>
<gl-dropdown
icon="ellipsis_v"
text="More actions"
:text-sr-only="true"
category="tertiary"
no-caret
>
<gl-dropdown-item
v-gl-modal-directive="$options.confirmClearCacheModal"
variant="danger"
>{{ $options.i18n.clearCache }}</gl-dropdown-item
>
</gl-dropdown>
</template>
</title-area>
<gl-alert <gl-alert
v-if="!dependencyProxyAvailable" v-if="!dependencyProxyAvailable"
:dismissible="false" :dismissible="false"
...@@ -159,5 +246,15 @@ export default { ...@@ -159,5 +246,15 @@ export default {
:title="$options.i18n.noManifestTitle" :title="$options.i18n.noManifestTitle"
/> />
</div> </div>
<gl-modal
:modal-id="$options.confirmClearCacheModal"
:title="modalTitleWithCount"
:action-primary="$options.modalButtons.primary"
:action-secondary="$options.modalButtons.secondary"
@primary="submit"
>
{{ modalConfirmationMessageWithCount }}
</gl-modal>
</div> </div>
</template> </template>
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
#js-dependency-proxy{ data: { group_path: @group.full_path, #js-dependency-proxy{ data: { group_path: @group.full_path,
dependency_proxy_available: dependency_proxy_available.to_s, dependency_proxy_available: dependency_proxy_available.to_s,
no_manifests_illustration: image_path('illustrations/docker-empty-state.svg') } } no_manifests_illustration: image_path('illustrations/docker-empty-state.svg'), group_id: @group.id } }
...@@ -7621,6 +7621,11 @@ msgstr "" ...@@ -7621,6 +7621,11 @@ msgstr ""
msgid "Clear" msgid "Clear"
msgstr "" msgstr ""
msgid "Clear %{count} image from cache?"
msgid_plural "Clear %{count} images from cache?"
msgstr[0] ""
msgstr[1] ""
msgid "Clear all repository checks" msgid "Clear all repository checks"
msgstr "" msgstr ""
...@@ -12155,9 +12160,15 @@ msgstr "" ...@@ -12155,9 +12160,15 @@ msgstr ""
msgid "Dependency list" msgid "Dependency list"
msgstr "" msgstr ""
msgid "DependencyProxy|All items in the cache are scheduled for removal."
msgstr ""
msgid "DependencyProxy|Cached %{time}" msgid "DependencyProxy|Cached %{time}"
msgstr "" msgstr ""
msgid "DependencyProxy|Clear cache"
msgstr ""
msgid "DependencyProxy|Clear the Dependency Proxy cache automatically" msgid "DependencyProxy|Clear the Dependency Proxy cache automatically"
msgstr "" msgstr ""
...@@ -42727,6 +42738,11 @@ msgstr "" ...@@ -42727,6 +42738,11 @@ msgstr ""
msgid "You are about to add %{usersTag} people to the discussion. They will all receive a notification." msgid "You are about to add %{usersTag} people to the discussion. They will all receive a notification."
msgstr "" msgstr ""
msgid "You are about to clear %{count} image from the cache. Once you confirm, the next time a pipeline runs it must pull an image or tag from Docker Hub. Are you sure?"
msgid_plural "You are about to clear %{count} images from the cache. Once you confirm, the next time a pipeline runs it must pull an image or tag from Docker Hub. Are you sure?"
msgstr[0] ""
msgstr[1] ""
msgid "You are about to delete this forked project containing:" msgid "You are about to delete this forked project containing:"
msgstr "" msgstr ""
......
...@@ -1671,6 +1671,18 @@ describe('Api', () => { ...@@ -1671,6 +1671,18 @@ describe('Api', () => {
}); });
}); });
describe('dependency proxy cache', () => {
it('schedules the cache list for deletion', async () => {
const groupId = 1;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/dependency_proxy/cache`;
mock.onDelete(expectedUrl).reply(httpStatus.ACCEPTED);
const { status } = await Api.deleteDependencyProxyCacheList(groupId, {});
expect(status).toBe(httpStatus.ACCEPTED);
});
});
describe('Feature Flag User List', () => { describe('Feature Flag User List', () => {
let expectedUrl; let expectedUrl;
let projectId; let projectId;
......
import { import {
GlAlert,
GlDropdown,
GlDropdownItem,
GlFormInputGroup, GlFormInputGroup,
GlFormGroup, GlFormGroup,
GlModal,
GlSkeletonLoader, GlSkeletonLoader,
GlSprintf, GlSprintf,
GlEmptyState, GlEmptyState,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Vue from 'vue'; import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stripTypenames } from 'helpers/graphql_helpers'; import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants'; import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import axios from '~/lib/utils/axios_utils';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue'; import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue'; import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
...@@ -21,13 +28,25 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency ...@@ -21,13 +28,25 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency
import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data'; import { proxyDetailsQuery, proxyData, pagination, proxyManifests } from './mock_data';
const dummyApiVersion = 'v3000';
const dummyGrouptId = 1;
const dummyUrlRoot = '/gitlab';
const dummyGon = {
api_version: dummyApiVersion,
relative_url_root: dummyUrlRoot,
};
let originalGon;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${dummyGrouptId}/dependency_proxy/cache`;
describe('DependencyProxyApp', () => { describe('DependencyProxyApp', () => {
let wrapper; let wrapper;
let apolloProvider; let apolloProvider;
let resolver; let resolver;
let mock;
const provideDefaults = { const provideDefaults = {
groupPath: 'gitlab-org', groupPath: 'gitlab-org',
groupId: dummyGrouptId,
dependencyProxyAvailable: true, dependencyProxyAvailable: true,
noManifestsIllustration: 'noManifestsIllustration', noManifestsIllustration: 'noManifestsIllustration',
}; };
...@@ -43,9 +62,14 @@ describe('DependencyProxyApp', () => { ...@@ -43,9 +62,14 @@ describe('DependencyProxyApp', () => {
apolloProvider, apolloProvider,
provide, provide,
stubs: { stubs: {
GlAlert,
GlDropdown,
GlDropdownItem,
GlFormInputGroup, GlFormInputGroup,
GlFormGroup, GlFormGroup,
GlModal,
GlSprintf, GlSprintf,
TitleArea,
}, },
}); });
} }
...@@ -59,13 +83,24 @@ describe('DependencyProxyApp', () => { ...@@ -59,13 +83,24 @@ describe('DependencyProxyApp', () => {
const findProxyCountText = () => wrapper.findByTestId('proxy-count'); const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList); const findManifestList = () => wrapper.findComponent(ManifestsList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findClearCacheDropdownList = () => wrapper.findComponent(GlDropdown);
const findClearCacheModal = () => wrapper.findComponent(GlModal);
const findClearCacheAlert = () => wrapper.findComponent(GlAlert);
beforeEach(() => { beforeEach(() => {
resolver = jest.fn().mockResolvedValue(proxyDetailsQuery()); resolver = jest.fn().mockResolvedValue(proxyDetailsQuery());
originalGon = window.gon;
window.gon = { ...dummyGon };
mock = new MockAdapter(axios);
mock.onDelete(expectedUrl).reply(202, {});
}); });
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
window.gon = originalGon;
mock.restore();
}); });
describe('when the dependency proxy is not available', () => { describe('when the dependency proxy is not available', () => {
...@@ -95,6 +130,12 @@ describe('DependencyProxyApp', () => { ...@@ -95,6 +130,12 @@ describe('DependencyProxyApp', () => {
expect(resolver).not.toHaveBeenCalled(); expect(resolver).not.toHaveBeenCalled();
}); });
it('hides the clear cache dropdown list', () => {
createComponent(createComponentArguments);
expect(findClearCacheDropdownList().exists()).toBe(false);
});
}); });
describe('when the dependency proxy is available', () => { describe('when the dependency proxy is available', () => {
...@@ -165,6 +206,7 @@ describe('DependencyProxyApp', () => { ...@@ -165,6 +206,7 @@ describe('DependencyProxyApp', () => {
}), }),
); );
createComponent(); createComponent();
return waitForPromises(); return waitForPromises();
}); });
...@@ -214,6 +256,28 @@ describe('DependencyProxyApp', () => { ...@@ -214,6 +256,28 @@ describe('DependencyProxyApp', () => {
fullPath: provideDefaults.groupPath, fullPath: provideDefaults.groupPath,
}); });
}); });
it('shows the clear cache dropdown list', () => {
expect(findClearCacheDropdownList().exists()).toBe(true);
});
it('shows the clear cache confirmation modal', () => {
const modal = findClearCacheModal();
expect(modal.find('.modal-title').text()).toContain('Clear 2 images from cache?');
expect(modal.props('actionPrimary').text).toBe('Clear cache');
});
it('submits the clear cache request', async () => {
findClearCacheModal().vm.$emit('primary', { preventDefault: jest.fn() });
await waitForPromises();
expect(findClearCacheAlert().exists()).toBe(true);
expect(findClearCacheAlert().text()).toBe(
'All items in the cache are scheduled for removal.',
);
});
}); });
}); });
}); });
......
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