Commit 17dda274 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Alex Kalderimis

Alert project admin that their project's cleanup policy is turned on

parent f3841248
......@@ -9,17 +9,28 @@ import {
UNAVAILABLE_ADMIN_FEATURE_TEXT,
} from '~/packages_and_registries/settings/project/constants';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import SettingsForm from './settings_form.vue';
export default {
components: {
SettingsBlock,
SettingsForm,
CleanupPolicyEnabledAlert,
GlAlert,
GlSprintf,
GlLink,
},
inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries'],
inject: [
'projectPath',
'isAdmin',
'adminSettingsPath',
'enableHistoricEntries',
'helpPagePath',
'showCleanupPolicyOnAlert',
],
i18n: {
UNAVAILABLE_FEATURE_TITLE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
......@@ -75,32 +86,53 @@ export default {
</script>
<template>
<div>
<settings-form
v-if="!isDisabled"
v-model="workingCopy"
:is-loading="$apollo.queries.containerExpirationPolicy.loading"
:is-edited="isEdited"
@reset="restoreOriginal"
/>
<template v-else>
<gl-alert
v-if="showDisabledFormMessage"
:dismissible="false"
:title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
variant="tip"
>
{{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
<section data-testid="registry-settings-app">
<cleanup-policy-enabled-alert v-if="showCleanupPolicyOnAlert" :project-path="projectPath" />
<settings-block default-expanded>
<template #title> {{ __('Clean up image tags') }}</template>
<template #description>
<span data-testid="description">
<gl-sprintf
:message="
__(
'Save space and find images in the container Registry. remove unneeded tags and keep only the ones you want. %{linkStart}How does cleanup work?%{linkEnd}',
)
"
>
<template #link="{ content }">
<gl-link :href="helpPagePath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
</template>
<template #default>
<settings-form
v-if="!isDisabled"
v-model="workingCopy"
:is-loading="$apollo.queries.containerExpirationPolicy.loading"
:is-edited="isEdited"
@reset="restoreOriginal"
/>
<template v-else>
<gl-alert
v-if="showDisabledFormMessage"
:dismissible="false"
:title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
variant="tip"
>
{{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
<gl-sprintf :message="unavailableFeatureMessage">
<template #link="{ content }">
<gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
<gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
</gl-alert>
</template>
</div>
<gl-sprintf :message="unavailableFeatureMessage">
<template #link="{ content }">
<gl-link :href="adminSettingsPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
<gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
</gl-alert>
</template>
</template>
</settings-block>
</section>
</template>
......@@ -19,6 +19,8 @@ export default () => {
projectPath,
adminSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
showCleanupPolicyOnAlert,
} = el.dataset;
return new Vue({
el,
......@@ -32,6 +34,8 @@ export default () => {
projectPath,
adminSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert),
},
render(createElement) {
return createElement('registry-settings-app', {});
......
<script>
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
export default {
components: {
GlAlert,
GlLink,
GlSprintf,
LocalStorageSync,
},
props: {
projectPath: {
type: String,
required: true,
},
cleanupPoliciesSettingsPath: {
type: String,
required: false,
default: '',
},
},
data() {
return {
dismissed: false,
};
},
computed: {
storageKey() {
return `cleanup_policy_enabled_for_project_${this.projectPath}`;
},
},
i18n: {
message: s__(
'ContainerRegistry|Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}',
),
},
};
</script>
<template>
<local-storage-sync v-model="dismissed" :storage-key="storageKey">
<gl-alert v-if="!dismissed" class="gl-mt-2" dismissible @dismiss="dismissed = true">
<gl-sprintf :message="$options.i18n.message">
<template #link="{ content }">
<gl-link v-if="cleanupPoliciesSettingsPath" :href="cleanupPoliciesSettingsPath">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</local-storage-sync>
</template>
......@@ -34,6 +34,7 @@ export default () => {
expirationPolicy,
isGroupPage,
isAdmin,
showCleanupPolicyOnAlert,
showUnfinishedTagCleanupCallout,
...config
} = el.dataset;
......@@ -64,6 +65,7 @@ export default () => {
expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined,
isGroupPage: parseBoolean(isGroupPage),
isAdmin: parseBoolean(isAdmin),
showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert),
showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout),
},
/* eslint-disable @gitlab/require-i18n-strings */
......
......@@ -11,6 +11,7 @@ import {
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import createFlash from '~/flash';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import Tracking from '~/tracking';
......@@ -61,6 +62,7 @@ export default {
RegistryHeader,
DeleteImage,
RegistrySearch,
CleanupPolicyEnabledAlert,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -283,6 +285,12 @@ export default {
</gl-sprintf>
</gl-alert>
<cleanup-policy-enabled-alert
v-if="config.showCleanupPolicyOnAlert"
:project-path="config.projectPath"
:cleanup-policies-settings-path="config.cleanupPoliciesSettingsPath"
/>
<gl-empty-state
v-if="config.characterError"
:title="$options.i18n.CONNECTION_ERROR_TITLE"
......
......@@ -53,4 +53,14 @@ module PackagesHelper
category = args.delete(:category) || self.class.name
::Gitlab::Tracking.event(category, event_name.to_s, **args)
end
def show_cleanup_policy_on_alert(project)
Gitlab.com? &&
Gitlab.config.registry.enabled &&
project.container_registry_enabled &&
!Gitlab::CurrentSettings.container_expiration_policies_enable_historic_entries &&
Feature.enabled?(:container_expiration_policies_historic_entry, project) &&
project.container_expiration_policy.nil? &&
project.container_repositories.exists?
end
end
......@@ -18,6 +18,8 @@
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,
"show_cleanup_policy_on_alert": show_cleanup_policy_on_alert(@project).to_s,
"cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project),
character_error: @character_error.to_s,
user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
......
......@@ -76,17 +76,7 @@
= render 'projects/triggers/index'
- if settings_container_registry_expiration_policy_available?(@project)
%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Clean up image tags")
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.")
= link_to _('How does cleanup work?'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'projects/registry/settings/index'
= render 'projects/registry/settings/index'
= render_if_exists 'projects/settings/ci_cd/auto_rollback', expanded: expanded
......
- breadcrumb_title _('Packages & Registries')
- page_title _('Packages & Registries')
- @content_class = 'limit-container-width' unless fluid_layout
- expanded = true
%section.settings.no-animate#js-registry-policies{ class: ('expanded' if expanded) }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _("Clean up image tags")
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _("Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want.")
= link_to _('How does cleanup work?'), help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'projects/registry/settings/index'
#js-registry-settings{ data: { project_id: @project.id,
project_path: @project.full_path,
cadence_options: cadence_options.to_json,
keep_n_options: keep_n_options.to_json,
older_than_options: older_than_options.to_json,
is_admin: current_user&.admin.to_s,
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
enable_historic_entries: container_expiration_policies_historic_entry_enabled?(@project).to_s,
help_page_path: help_page_path('user/packages/container_registry/index', anchor: 'cleanup-policy'),
show_cleanup_policy_on_alert: show_cleanup_policy_on_alert(@project).to_s,
tags_regex_help_page_path: help_page_path('user/packages/container_registry/index', anchor: 'regex-pattern-examples') } }
......@@ -8563,6 +8563,9 @@ msgstr ""
msgid "ContainerRegistry|Cleanup pending"
msgstr ""
msgid "ContainerRegistry|Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|Cleanup policy for tags is disabled"
msgstr ""
......@@ -16361,9 +16364,6 @@ msgstr ""
msgid "How do I set up this service?"
msgstr ""
msgid "How does cleanup work?"
msgstr ""
msgid "How it works"
msgstr ""
......@@ -28399,7 +28399,7 @@ msgstr ""
msgid "Save pipeline schedule"
msgstr ""
msgid "Save space and find images in the Container Registry. Remove unneeded tags and keep only the ones you want."
msgid "Save space and find images in the container Registry. remove unneeded tags and keep only the ones you want. %{linkStart}How does cleanup work?%{linkEnd}"
msgstr ""
msgid "Saved scan settings and target site settings which are reusable."
......
......@@ -24,14 +24,14 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'shows available section' do
subject
settings_block = find('#js-registry-policies')
settings_block = find('[data-testid="registry-settings-app"]')
expect(settings_block).to have_text 'Clean up image tags'
end
it 'saves cleanup policy submit the form' do
subject
within '#js-registry-policies' do
within '[data-testid="registry-settings-app"]' do
select('Every day', from: 'Run cleanup')
select('50 tags per image name', from: 'Keep the most recent:')
fill_in('Keep tags matching:', with: 'stable')
......@@ -49,7 +49,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not save cleanup policy submit form with invalid regex' do
subject
within '#js-registry-policies' do
within '[data-testid="registry-settings-app"]' do
fill_in('Remove tags matching:', with: '*-production')
submit_button = find('[data-testid="save-button"')
......@@ -80,7 +80,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'displays the expected result' do
subject
within '#js-registry-policies' do
within '[data-testid="registry-settings-app"]' do
case result
when :available_section
expect(find('[data-testid="enable-toggle"]')).to have_content('Disabled - Tags will not be automatically deleted.')
......@@ -98,7 +98,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not exists' do
subject
expect(page).not_to have_selector('#js-registry-policies')
expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
end
end
......@@ -108,7 +108,7 @@ RSpec.describe 'Project > Settings > CI/CD > Container registry tag expiration p
it 'does not exists' do
subject
expect(page).not_to have_selector('#js-registry-policies')
expect(page).not_to have_selector('[data-testid="registry-settings-app"]')
end
end
end
......
......@@ -10,6 +10,8 @@ import {
UNAVAILABLE_USER_FEATURE_TEXT,
} from '~/packages_and_registries/settings/project/constants';
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import {
expirationPolicyPayload,
......@@ -28,15 +30,19 @@ describe('Registry Settings App', () => {
isAdmin: false,
adminSettingsPath: 'settingsPath',
enableHistoricEntries: false,
helpPagePath: 'helpPagePath',
showCleanupPolicyOnAlert: false,
};
const findSettingsComponent = () => wrapper.find(SettingsForm);
const findAlert = () => wrapper.find(GlAlert);
const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const mountComponent = (provide = defaultProvidedValues, config) => {
wrapper = shallowMount(component, {
stubs: {
GlSprintf,
SettingsBlock,
},
mocks: {
$toast: {
......@@ -66,6 +72,26 @@ describe('Registry Settings App', () => {
wrapper.destroy();
});
describe('cleanup is on alert', () => {
it('exist when showCleanupPolicyOnAlert is true and has the correct props', () => {
mountComponent({
...defaultProvidedValues,
showCleanupPolicyOnAlert: true,
});
expect(findCleanupAlert().exists()).toBe(true);
expect(findCleanupAlert().props()).toMatchObject({
projectPath: 'path',
});
});
it('is hidden when showCleanupPolicyOnAlert is false', async () => {
mountComponent();
expect(findCleanupAlert().exists()).toBe(false);
});
});
describe('isEdited status', () => {
it.each`
description | apiResponse | workingCopy | result
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CleanupPolicyEnabledAlert renders 1`] = `
<gl-alert-stub
class="gl-mt-2"
dismissible="true"
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink=""
secondarybuttontext=""
title=""
variant="info"
>
<gl-sprintf-stub
message="Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}"
/>
</gl-alert-stub>
`;
import { GlAlert } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import component from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
describe('CleanupPolicyEnabledAlert', () => {
let wrapper;
const defaultProps = {
projectPath: 'foo',
cleanupPoliciesSettingsPath: 'label-bar',
};
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = (props) => {
wrapper = shallowMount(component, {
stubs: {
LocalStorageSync,
},
propsData: {
...defaultProps,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
});
it('when dismissed is not visible', async () => {
mountComponent();
expect(findAlert().exists()).toBe(true);
findAlert().vm.$emit('dismiss');
await nextTick();
expect(findAlert().exists()).toBe(false);
});
});
......@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
......@@ -43,21 +44,22 @@ describe('List Page', () => {
let wrapper;
let apolloProvider;
const findDeleteModal = () => wrapper.find(GlModal);
const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
const findDeleteModal = () => wrapper.findComponent(GlModal);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findEmptyState = () => wrapper.find(GlEmptyState);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findCliCommands = () => wrapper.find(CliCommands);
const findProjectEmptyState = () => wrapper.find(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.find(GroupEmptyState);
const findRegistryHeader = () => wrapper.find(RegistryHeader);
const findCliCommands = () => wrapper.findComponent(CliCommands);
const findProjectEmptyState = () => wrapper.findComponent(ProjectEmptyState);
const findGroupEmptyState = () => wrapper.findComponent(GroupEmptyState);
const findRegistryHeader = () => wrapper.findComponent(RegistryHeader);
const findDeleteAlert = () => wrapper.find(GlAlert);
const findImageList = () => wrapper.find(ImageList);
const findRegistrySearch = () => wrapper.find(RegistrySearch);
const findDeleteAlert = () => wrapper.findComponent(GlAlert);
const findImageList = () => wrapper.findComponent(ImageList);
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
const findDeleteImage = () => wrapper.find(DeleteImage);
const findDeleteImage = () => wrapper.findComponent(DeleteImage);
const findCleanupAlert = () => wrapper.findComponent(CleanupPolicyEnabledAlert);
const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers();
......@@ -560,4 +562,33 @@ describe('List Page', () => {
},
);
});
describe('cleanup is on alert', () => {
it('exist when showCleanupPolicyOnAlert is true and has the correct props', async () => {
mountComponent({
config: {
showCleanupPolicyOnAlert: true,
projectPath: 'foo',
isGroupPage: false,
cleanupPoliciesSettingsPath: 'bar',
},
});
await waitForApolloRequestRender();
expect(findCleanupAlert().exists()).toBe(true);
expect(findCleanupAlert().props()).toMatchObject({
projectPath: 'foo',
cleanupPoliciesSettingsPath: 'bar',
});
});
it('is hidden when showCleanupPolicyOnAlert is false', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findCleanupAlert().exists()).toBe(false);
});
});
});
This diff is collapsed.
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