Commit c5177835 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents dc815607 1f9cce7f
...@@ -182,12 +182,6 @@ Lint/SelfAssignment: ...@@ -182,12 +182,6 @@ Lint/SelfAssignment:
Exclude: Exclude:
- 'spec/lib/gitlab/search_context/builder_spec.rb' - 'spec/lib/gitlab/search_context/builder_spec.rb'
# Offense count: 1
# Cop supports --auto-correct.
Lint/SendWithMixinArgument:
Exclude:
- 'config/initializers/trusted_proxies.rb'
# Offense count: 3 # Offense count: 3
Lint/StructNewOverride: Lint/StructNewOverride:
Exclude: Exclude:
......
17251f57d798c70e6adec8ab3be649bbef66dc27 01e940cac5cdeaf1b14a18f3f71e0a3c50d1c60c
<script> <script>
import { GlSprintf } from '@gitlab/ui'; import { GlSprintf } from '@gitlab/ui';
import { sprintf } from '~/locale'; import { sprintf, n__ } from '~/locale';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
import { DETAILS_PAGE_TITLE, UPDATED_AT } from '../../constants/index'; import {
DETAILS_PAGE_TITLE,
UPDATED_AT,
CLEANUP_UNSCHEDULED_TEXT,
CLEANUP_SCHEDULED_TEXT,
CLEANUP_ONGOING_TEXT,
CLEANUP_UNFINISHED_TEXT,
CLEANUP_DISABLED_TEXT,
CLEANUP_SCHEDULED_TOOLTIP,
CLEANUP_ONGOING_TOOLTIP,
CLEANUP_UNFINISHED_TOOLTIP,
CLEANUP_DISABLED_TOOLTIP,
UNFINISHED_STATUS,
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
} from '../../constants/index';
export default { export default {
name: 'DetailsHeader', name: 'DetailsHeader',
...@@ -15,6 +31,11 @@ export default { ...@@ -15,6 +31,11 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
metadataLoading: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
visibilityIcon() { visibilityIcon() {
...@@ -26,6 +47,24 @@ export default { ...@@ -26,6 +47,24 @@ export default {
updatedText() { updatedText() {
return sprintf(UPDATED_AT, { time: this.timeAgo }); return sprintf(UPDATED_AT, { time: this.timeAgo });
}, },
tagCountText() {
return n__('%d tag', '%d tags', this.image.tagsCount);
},
cleanupTextAndTooltip() {
if (!this.image.project.containerExpirationPolicy?.enabled) {
return { text: CLEANUP_DISABLED_TEXT, tooltip: CLEANUP_DISABLED_TOOLTIP };
}
return {
[UNSCHEDULED_STATUS]: {
text: sprintf(CLEANUP_UNSCHEDULED_TEXT, {
time: this.timeFormatted(this.image.project.containerExpirationPolicy.nextRunAt),
}),
},
[SCHEDULED_STATUS]: { text: CLEANUP_SCHEDULED_TEXT, tooltip: CLEANUP_SCHEDULED_TOOLTIP },
[ONGOING_STATUS]: { text: CLEANUP_ONGOING_TEXT, tooltip: CLEANUP_ONGOING_TOOLTIP },
[UNFINISHED_STATUS]: { text: CLEANUP_UNFINISHED_TEXT, tooltip: CLEANUP_UNFINISHED_TOOLTIP },
}[this.image?.expirationPolicyCleanupStatus];
},
}, },
i18n: { i18n: {
DETAILS_PAGE_TITLE, DETAILS_PAGE_TITLE,
...@@ -34,7 +73,7 @@ export default { ...@@ -34,7 +73,7 @@ export default {
</script> </script>
<template> <template>
<title-area> <title-area :metadata-loading="metadataLoading">
<template #title> <template #title>
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE"> <gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName> <template #imageName>
...@@ -42,6 +81,20 @@ export default { ...@@ -42,6 +81,20 @@ export default {
</template> </template>
</gl-sprintf> </gl-sprintf>
</template> </template>
<template #metadata-tags-count>
<metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />
</template>
<template #metadata-cleanup>
<metadata-item
icon="expire"
:text="cleanupTextAndTooltip.text"
:text-tooltip="cleanupTextAndTooltip.tooltip"
size="xl"
data-testid="cleanup"
/>
</template>
<template #metadata-updated> <template #metadata-updated>
<metadata-item <metadata-item
:icon="visibilityIcon" :icon="visibilityIcon"
......
...@@ -60,6 +60,22 @@ export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}'); ...@@ -60,6 +60,22 @@ export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}');
export const NOT_AVAILABLE_TEXT = __('N/A'); export const NOT_AVAILABLE_TEXT = __('N/A');
export const NOT_AVAILABLE_SIZE = __('0 bytes'); export const NOT_AVAILABLE_SIZE = __('0 bytes');
export const CLEANUP_UNSCHEDULED_TEXT = s__('ContainerRegistry|Cleanup will run %{time}');
export const CLEANUP_SCHEDULED_TEXT = s__('ContainerRegistry|Cleanup pending');
export const CLEANUP_ONGOING_TEXT = s__('ContainerRegistry|Cleanup in progress');
export const CLEANUP_UNFINISHED_TEXT = s__('ContainerRegistry|Cleanup incomplete');
export const CLEANUP_DISABLED_TEXT = s__('ContainerRegistry|Cleanup disabled');
export const CLEANUP_SCHEDULED_TOOLTIP = s__('ContainerRegistry|Cleanup will run soon');
export const CLEANUP_ONGOING_TOOLTIP = s__('ContainerRegistry|Cleanup is currently removing tags');
export const CLEANUP_UNFINISHED_TOOLTIP = s__(
'ContainerRegistry|Cleanup ran but some tags were not removed',
);
export const CLEANUP_DISABLED_TOOLTIP = s__(
'ContainerRegistry|Cleanup is disabled for this project',
);
// Parameters // Parameters
export const DEFAULT_PAGE = 1; export const DEFAULT_PAGE = 1;
...@@ -76,3 +92,8 @@ export const ALERT_MESSAGES = { ...@@ -76,3 +92,8 @@ export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE, [ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE, [ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
}; };
export const UNFINISHED_STATUS = 'UNFINISHED';
export const UNSCHEDULED_STATUS = 'UNSCHEDULED';
export const SCHEDULED_STATUS = 'SCHEDULED';
export const ONGOING_STATUS = 'ONGOING';
...@@ -18,6 +18,7 @@ query getContainerRepositoryDetails( ...@@ -18,6 +18,7 @@ query getContainerRepositoryDetails(
updatedAt updatedAt
tagsCount tagsCount
expirationPolicyStartedAt expirationPolicyStartedAt
expirationPolicyCleanupStatus
tags(after: $after, before: $before, first: $first, last: $last) { tags(after: $after, before: $before, first: $first, last: $last) {
nodes { nodes {
digest digest
...@@ -36,6 +37,10 @@ query getContainerRepositoryDetails( ...@@ -36,6 +37,10 @@ query getContainerRepositoryDetails(
} }
project { project {
visibility visibility
containerExpirationPolicy {
enabled
nextRunAt
}
} }
} }
} }
...@@ -22,6 +22,7 @@ import { ...@@ -22,6 +22,7 @@ import {
ALERT_DANGER_TAGS, ALERT_DANGER_TAGS,
GRAPHQL_PAGE_SIZE, GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE, FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
} from '../constants/index'; } from '../constants/index';
export default { export default {
...@@ -84,7 +85,10 @@ export default { ...@@ -84,7 +85,10 @@ export default {
return this.image?.tags?.nodes || []; return this.image?.tags?.nodes || [];
}, },
showPartialCleanupWarning() { showPartialCleanupWarning() {
return this.image?.expirationPolicyStartedAt && !this.dismissPartialCleanupWarning; return (
this.image?.expirationPolicyCleanupStatus === UNFINISHED_STATUS &&
!this.dismissPartialCleanupWarning
);
}, },
tracking() { tracking() {
return { return {
...@@ -184,7 +188,7 @@ export default { ...@@ -184,7 +188,7 @@ export default {
@dismiss="dismissPartialCleanupWarning = true" @dismiss="dismissPartialCleanupWarning = true"
/> />
<details-header :image="image" /> <details-header :image="image" :metadata-loading="isLoading" />
<tags-loader v-if="isLoading" /> <tags-loader v-if="isLoading" />
<template v-else> <template v-else>
......
<script> <script>
import { GlIcon, GlLink } from '@gitlab/ui'; import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default { export default {
...@@ -9,6 +9,9 @@ export default { ...@@ -9,6 +9,9 @@ export default {
GlLink, GlLink,
TooltipOnTruncate, TooltipOnTruncate,
}, },
directives: {
GlTooltip: GlTooltipDirective,
},
props: { props: {
icon: { icon: {
type: String, type: String,
...@@ -32,6 +35,11 @@ export default { ...@@ -32,6 +35,11 @@ export default {
return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value); return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value);
}, },
}, },
textTooltip: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
sizeClass() { sizeClass() {
...@@ -55,9 +63,12 @@ export default { ...@@ -55,9 +63,12 @@ export default {
class="gl-font-weight-bold gl-display-inline-flex" class="gl-font-weight-bold gl-display-inline-flex"
:class="sizeClass" :class="sizeClass"
> >
<tooltip-on-truncate :title="text" class="gl-text-truncate"> <tooltip-on-truncate v-if="!textTooltip" :title="text" class="gl-text-truncate">
{{ text }} {{ text }}
</tooltip-on-truncate> </tooltip-on-truncate>
<span v-else v-gl-tooltip="{ title: textTooltip }" data-testid="text-tooltip-container">
{{ text }}</span
>
</div> </div>
</div> </div>
</template> </template>
...@@ -50,7 +50,7 @@ export default { ...@@ -50,7 +50,7 @@ export default {
<template> <template>
<div class="gl-display-flex gl-flex-direction-column"> <div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-justify-content-space-between gl-py-3"> <div class="gl-display-flex gl-justify-content-space-between gl-py-3">
<div class="gl-flex-direction-column"> <div class="gl-flex-direction-column gl-flex-grow-1">
<div class="gl-display-flex"> <div class="gl-display-flex">
<gl-avatar <gl-avatar
v-if="avatar" v-if="avatar"
...@@ -85,7 +85,7 @@ export default { ...@@ -85,7 +85,7 @@ export default {
</template> </template>
<template v-else> <template v-else>
<div class="gl-w-full"> <div class="gl-w-full">
<gl-skeleton-loader :width="200" :height="16" preserve-aspect-ratio="xMinYMax meet"> <gl-skeleton-loader :width="960" :height="16" preserve-aspect-ratio="xMinYMax meet">
<circle cx="6" cy="8" r="6" /> <circle cx="6" cy="8" r="6" />
<rect x="16" y="4" width="200" height="8" rx="4" /> <rect x="16" y="4" width="200" height="8" rx="4" />
</gl-skeleton-loader> </gl-skeleton-loader>
......
---
title: Add tags count and cleanup status to registry details
merge_request: 50756
author:
type: changed
---
title: Drop group_id column from compliance_management_frameworks table
merge_request: 50829
author:
type: removed
...@@ -30,4 +30,4 @@ module TrustedProxyMonkeyPatch ...@@ -30,4 +30,4 @@ module TrustedProxyMonkeyPatch
end end
end end
ActionDispatch::Request.send(:include, TrustedProxyMonkeyPatch) ActionDispatch::Request.include TrustedProxyMonkeyPatch
# frozen_string_literal: true
class DeleteColumnGroupIdOnComplianceFramework < ActiveRecord::Migration[6.0]
DOWNTIME = false
def change
remove_column :compliance_management_frameworks, :group_id, :bigint
end
end
0a318fbcf54860d9fe8b3e8372e10331d2b52df738e621f4b0eec5fd4f255739
\ No newline at end of file
...@@ -11418,7 +11418,6 @@ ALTER SEQUENCE commit_user_mentions_id_seq OWNED BY commit_user_mentions.id; ...@@ -11418,7 +11418,6 @@ ALTER SEQUENCE commit_user_mentions_id_seq OWNED BY commit_user_mentions.id;
CREATE TABLE compliance_management_frameworks ( CREATE TABLE compliance_management_frameworks (
id bigint NOT NULL, id bigint NOT NULL,
group_id bigint,
name text NOT NULL, name text NOT NULL,
description text NOT NULL, description text NOT NULL,
color text NOT NULL, color text NOT NULL,
......
...@@ -322,7 +322,7 @@ project and should only have access to that project. ...@@ -322,7 +322,7 @@ project and should only have access to that project.
External users: External users:
- Cannot create groups, projects, or personal snippets. - Cannot create projects (including forks), groups, or personal snippets.
- Can only access public projects and projects to which they are explicitly granted access, - Can only access public projects and projects to which they are explicitly granted access,
thus hiding all other internal or private ones from them (like being thus hiding all other internal or private ones from them (like being
logged out). logged out).
......
...@@ -112,7 +112,7 @@ token = PersonalAccessToken.find_by_token('token-string-here123') ...@@ -112,7 +112,7 @@ token = PersonalAccessToken.find_by_token('token-string-here123')
token.revoke! token.revoke!
``` ```
This can be shorted into a single-line shell command using the This can be shortened into a single-line shell command using the
[Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner): [Rails runner](../../administration/troubleshooting/debug.md#using-the-rails-runner):
```shell ```shell
......
...@@ -2,20 +2,18 @@ ...@@ -2,20 +2,18 @@
- expanded = expanded_by_default? - expanded = expanded_by_default?
%section.settings.issues-feature.no-animate#js-issue-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:issues_access_level) == 0)] } %section.settings.issues-feature.no-animate#js-issue-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:issues_access_level) == 0)] }
.settings-header .settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Default issue template') %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only= _('Default description template for issues')
%button.gl-button.btn.btn-default.js-settings-toggle= expanded ? _('Collapse') : _('Expand') %button.gl-button.btn.btn-default.js-settings-toggle= expanded ? _('Collapse') : _('Expand')
%p= _('Set a default template for issue descriptions.') - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/project/description_templates', anchor: 'setting-a-default-template-for-merge-requests-and-issues') }
%p#issue-settings-default-template-label= _('Set a default description template to be used for new issues. %{link_start}What are description templates?%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
.settings-content .settings-content
= form_for @project, remote: true, html: { multipart: true, class: "issue-settings-form" }, authenticity_token: true do |f| = form_for @project, remote: true, html: { multipart: true, class: "issue-settings-form" }, authenticity_token: true do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-issue-settings' } %input{ type: 'hidden', name: 'update_section', value: 'js-issue-settings' }
.row .row
.form-group.col-md-9 .form-group.col-md-9
= f.label :issues_template, class: 'label-bold' do = f.text_area :issues_template, class: "form-control", rows: 3, aria: { labelledby: 'issue-settings-default-template-label'}
= _('Default description template for issues')
= link_to sprite_icon('question-o'), help_page_path('user/project/description_templates'), target: '_blank'
= f.text_area :issues_template, class: "form-control", rows: 3
.text-secondary .text-secondary
- link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') } - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/markdown') }
= _('Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}').html_safe % { link_start: link_start, link_end: '</a>'.html_safe } = _('Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}.').html_safe % { link_start: link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-success" = f.submit _('Save changes'), class: "gl-button btn btn-success"
---
title: Update issue description templates UI settings
merge_request: 50421
author:
type: other
...@@ -19,14 +19,14 @@ RSpec.describe 'Project settings > Issues', :js do ...@@ -19,14 +19,14 @@ RSpec.describe 'Project settings > Issues', :js do
end end
it 'shows the Issues settings' do it 'shows the Issues settings' do
expect(page).to have_content('Set a default template for issue descriptions.') expect(page).to have_content('Set a default description template to be used for new issues.')
within('.sharing-permissions-form') do within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click
click_on('Save changes') click_on('Save changes')
end end
expect(page).not_to have_content('Set a default template for issue descriptions.') expect(page).not_to have_content('Set a default description template to be used for new issues.')
end end
end end
end end
...@@ -38,14 +38,14 @@ RSpec.describe 'Project settings > Issues', :js do ...@@ -38,14 +38,14 @@ RSpec.describe 'Project settings > Issues', :js do
end end
it 'does not show the Issues settings' do it 'does not show the Issues settings' do
expect(page).not_to have_content('Set a default template for issue descriptions.') expect(page).not_to have_content('Set a default description template to be used for new issues.')
within('.sharing-permissions-form') do within('.sharing-permissions-form') do
find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click find('.project-feature-controls[data-for="project[project_feature_attributes][issues_access_level]"] .project-feature-toggle').click
click_on('Save changes') click_on('Save changes')
end end
expect(page).to have_content('Set a default template for issue descriptions.') expect(page).to have_content('Set a default description template to be used for new issues.')
end end
end end
......
...@@ -7471,15 +7471,42 @@ msgstr "" ...@@ -7471,15 +7471,42 @@ msgstr ""
msgid "ContainerRegistry|CLI Commands" msgid "ContainerRegistry|CLI Commands"
msgstr "" msgstr ""
msgid "ContainerRegistry|Cleanup disabled"
msgstr ""
msgid "ContainerRegistry|Cleanup in progress"
msgstr ""
msgid "ContainerRegistry|Cleanup incomplete"
msgstr ""
msgid "ContainerRegistry|Cleanup is currently removing tags"
msgstr ""
msgid "ContainerRegistry|Cleanup is disabled for this project"
msgstr ""
msgid "ContainerRegistry|Cleanup pending"
msgstr ""
msgid "ContainerRegistry|Cleanup policy for tags is disabled" msgid "ContainerRegistry|Cleanup policy for tags is disabled"
msgstr "" msgstr ""
msgid "ContainerRegistry|Cleanup policy successfully saved." msgid "ContainerRegistry|Cleanup policy successfully saved."
msgstr "" msgstr ""
msgid "ContainerRegistry|Cleanup ran but some tags were not removed"
msgstr ""
msgid "ContainerRegistry|Cleanup timed out before it could delete all tags" msgid "ContainerRegistry|Cleanup timed out before it could delete all tags"
msgstr "" msgstr ""
msgid "ContainerRegistry|Cleanup will run %{time}"
msgstr ""
msgid "ContainerRegistry|Cleanup will run soon"
msgstr ""
msgid "ContainerRegistry|Configuration digest: %{digest}" msgid "ContainerRegistry|Configuration digest: %{digest}"
msgstr "" msgstr ""
...@@ -9040,9 +9067,6 @@ msgstr "" ...@@ -9040,9 +9067,6 @@ msgstr ""
msgid "Default initial branch name" msgid "Default initial branch name"
msgstr "" msgstr ""
msgid "Default issue template"
msgstr ""
msgid "Default project deletion protection" msgid "Default project deletion protection"
msgstr "" msgstr ""
...@@ -9586,6 +9610,9 @@ msgstr "" ...@@ -9586,6 +9610,9 @@ msgstr ""
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}" msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}"
msgstr "" msgstr ""
msgid "Description parsed with %{link_start}GitLab Flavored Markdown%{link_end}."
msgstr ""
msgid "Description template" msgid "Description template"
msgstr "" msgstr ""
...@@ -25402,7 +25429,7 @@ msgstr "" ...@@ -25402,7 +25429,7 @@ msgstr ""
msgid "Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings." msgid "Set .gitlab-ci.yml to enable or configure SAST security scanning using the GitLab managed template. You can [add variable overrides](https://docs.gitlab.com/ee/user/application_security/sast/#customizing-the-sast-settings) to customize SAST settings."
msgstr "" msgstr ""
msgid "Set a default template for issue descriptions." msgid "Set a default description template to be used for new issues. %{link_start}What are description templates?%{link_end}"
msgstr "" msgstr ""
msgid "Set a password on your account to pull or push via %{protocol}." msgid "Set a password on your account to pull or push via %{protocol}."
......
...@@ -9,7 +9,7 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -9,7 +9,7 @@ exports[`PackageTitle renders with tags 1`] = `
class="gl-display-flex gl-justify-content-space-between gl-py-3" class="gl-display-flex gl-justify-content-space-between gl-py-3"
> >
<div <div
class="gl-flex-direction-column" class="gl-flex-direction-column gl-flex-grow-1"
> >
<div <div
class="gl-display-flex" class="gl-display-flex"
...@@ -54,6 +54,7 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -54,6 +54,7 @@ exports[`PackageTitle renders with tags 1`] = `
link="" link=""
size="s" size="s"
text="maven" text="maven"
texttooltip=""
/> />
</div> </div>
<div <div
...@@ -65,6 +66,7 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -65,6 +66,7 @@ exports[`PackageTitle renders with tags 1`] = `
link="" link=""
size="s" size="s"
text="300 bytes" text="300 bytes"
texttooltip=""
/> />
</div> </div>
<div <div
...@@ -95,7 +97,7 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -95,7 +97,7 @@ exports[`PackageTitle renders without tags 1`] = `
class="gl-display-flex gl-justify-content-space-between gl-py-3" class="gl-display-flex gl-justify-content-space-between gl-py-3"
> >
<div <div
class="gl-flex-direction-column" class="gl-flex-direction-column gl-flex-grow-1"
> >
<div <div
class="gl-display-flex" class="gl-display-flex"
...@@ -140,6 +142,7 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -140,6 +142,7 @@ exports[`PackageTitle renders without tags 1`] = `
link="" link=""
size="s" size="s"
text="maven" text="maven"
texttooltip=""
/> />
</div> </div>
<div <div
...@@ -151,6 +154,7 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -151,6 +154,7 @@ exports[`PackageTitle renders without tags 1`] = `
link="" link=""
size="s" size="s"
text="300 bytes" text="300 bytes"
texttooltip=""
/> />
</div> </div>
</div> </div>
......
...@@ -3,7 +3,18 @@ import { GlSprintf } from '@gitlab/ui'; ...@@ -3,7 +3,18 @@ import { GlSprintf } from '@gitlab/ui';
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import component from '~/registry/explorer/components/details_page/details_header.vue'; import component from '~/registry/explorer/components/details_page/details_header.vue';
import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants'; import {
DETAILS_PAGE_TITLE,
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
UNFINISHED_STATUS,
CLEANUP_DISABLED_TEXT,
CLEANUP_DISABLED_TOOLTIP,
CLEANUP_SCHEDULED_TOOLTIP,
CLEANUP_ONGOING_TOOLTIP,
CLEANUP_UNFINISHED_TOOLTIP,
} from '~/registry/explorer/constants';
describe('Details Header', () => { describe('Details Header', () => {
let wrapper; let wrapper;
...@@ -11,15 +22,22 @@ describe('Details Header', () => { ...@@ -11,15 +22,22 @@ describe('Details Header', () => {
const defaultImage = { const defaultImage = {
name: 'foo', name: 'foo',
updatedAt: '2020-11-03T13:29:21Z', updatedAt: '2020-11-03T13:29:21Z',
tagsCount: 10,
project: { project: {
visibility: 'public', visibility: 'public',
containerExpirationPolicy: {
enabled: false,
},
}, },
}; };
// set the date to Dec 4, 2020 // set the date to Dec 4, 2020
useFakeDate(2020, 11, 4); useFakeDate(2020, 11, 4);
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findLastUpdatedAndVisibility = () => wrapper.find('[data-testid="updated-and-visibility"]'); const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility');
const findTagsCount = () => findByTestId('tags-count');
const findCleanup = () => findByTestId('cleanup');
const waitForMetadataItems = async () => { const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
...@@ -54,7 +72,76 @@ describe('Details Header', () => { ...@@ -54,7 +72,76 @@ describe('Details Header', () => {
expect(wrapper.text()).toContain('foo'); expect(wrapper.text()).toContain('foo');
}); });
it('has a metadata item with last updated text', async () => { describe('metadata items', () => {
describe('tags count', () => {
it('when there is more than one tag has the correct text', async () => {
mountComponent();
await waitForMetadataItems();
expect(findTagsCount().props('text')).toBe('10 tags');
});
it('when there is one tag has the correct text', async () => {
mountComponent({ ...defaultImage, tagsCount: 1 });
await waitForMetadataItems();
expect(findTagsCount().props('text')).toBe('1 tag');
});
it('has the correct icon', async () => {
mountComponent();
await waitForMetadataItems();
expect(findTagsCount().props('icon')).toBe('tag');
});
});
describe('cleanup metadata item', () => {
it('has the correct icon', async () => {
mountComponent();
await waitForMetadataItems();
expect(findCleanup().props('icon')).toBe('expire');
});
it('when the expiration policy is disabled', async () => {
mountComponent();
await waitForMetadataItems();
expect(findCleanup().props()).toMatchObject({
text: CLEANUP_DISABLED_TEXT,
textTooltip: CLEANUP_DISABLED_TOOLTIP,
});
});
it.each`
status | text | tooltip
${UNSCHEDULED_STATUS} | ${'Cleanup will run in 1 month'} | ${''}
${SCHEDULED_STATUS} | ${'Cleanup pending'} | ${CLEANUP_SCHEDULED_TOOLTIP}
${ONGOING_STATUS} | ${'Cleanup in progress'} | ${CLEANUP_ONGOING_TOOLTIP}
${UNFINISHED_STATUS} | ${'Cleanup incomplete'} | ${CLEANUP_UNFINISHED_TOOLTIP}
`(
'when the status is $status the text is $text and the tooltip is $tooltip',
async ({ status, text, tooltip }) => {
mountComponent({
...defaultImage,
expirationPolicyCleanupStatus: status,
project: {
containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' },
},
});
await waitForMetadataItems();
expect(findCleanup().props()).toMatchObject({
text,
textTooltip: tooltip,
});
},
);
});
describe('visibility and updated at ', () => {
it('has last updated text', async () => {
mountComponent(); mountComponent();
await waitForMetadataItems(); await waitForMetadataItems();
...@@ -75,4 +162,6 @@ describe('Details Header', () => { ...@@ -75,4 +162,6 @@ describe('Details Header', () => {
expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash'); expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash');
}); });
}); });
});
});
}); });
...@@ -115,8 +115,13 @@ export const containerRepositoryMock = { ...@@ -115,8 +115,13 @@ export const containerRepositoryMock = {
updatedAt: '2020-11-03T13:29:21Z', updatedAt: '2020-11-03T13:29:21Z',
tagsCount: 13, tagsCount: 13,
expirationPolicyStartedAt: null, expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: { project: {
visibility: 'public', visibility: 'public',
containerExpirationPolicy: {
enabled: false,
nextRunAt: '2020-11-27T08:59:27Z',
},
__typename: 'Project', __typename: 'Project',
}, },
}; };
......
...@@ -15,6 +15,8 @@ import EmptyTagsState from '~/registry/explorer/components/details_page/empty_ta ...@@ -15,6 +15,8 @@ import EmptyTagsState from '~/registry/explorer/components/details_page/empty_ta
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql'; import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import { UNFINISHED_STATUS } from '~/registry/explorer/constants/index';
import { import {
graphQLImageDetailsMock, graphQLImageDetailsMock,
graphQLImageDetailsEmptyTagsMock, graphQLImageDetailsEmptyTagsMock,
...@@ -353,11 +355,14 @@ describe('Details Page', () => { ...@@ -353,11 +355,14 @@ describe('Details Page', () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
expect(findDetailsHeader().props('image')).toMatchObject({ expect(findDetailsHeader().props()).toMatchObject({
metadataLoading: false,
image: {
name: containerRepositoryMock.name, name: containerRepositoryMock.name,
project: { project: {
visibility: containerRepositoryMock.project.visibility, visibility: containerRepositoryMock.project.visibility,
}, },
},
}); });
}); });
}); });
...@@ -398,13 +403,13 @@ describe('Details Page', () => { ...@@ -398,13 +403,13 @@ describe('Details Page', () => {
cleanupPoliciesHelpPagePath: 'bar', cleanupPoliciesHelpPagePath: 'bar',
}; };
describe('when expiration_policy_started is not null', () => { describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => {
let resolver; let resolver;
beforeEach(() => { beforeEach(() => {
resolver = jest.fn().mockResolvedValue( resolver = jest.fn().mockResolvedValue(
graphQLImageDetailsMock({ graphQLImageDetailsMock({
expirationPolicyStartedAt: Date.now().toString(), expirationPolicyCleanupStatus: UNFINISHED_STATUS,
}), }),
); );
}); });
...@@ -439,7 +444,7 @@ describe('Details Page', () => { ...@@ -439,7 +444,7 @@ describe('Details Page', () => {
}); });
}); });
describe('when expiration_policy_started is null', () => { describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => {
it('the component is hidden', async () => { it('the component is hidden', async () => {
mountComponent(); mountComponent();
await waitForApolloRequestRender(); await waitForApolloRequestRender();
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui'; import { GlIcon, GlLink } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/vue_shared/components/registry/metadata_item.vue'; import component from '~/vue_shared/components/registry/metadata_item.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue'; import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
...@@ -12,6 +13,9 @@ describe('Metadata Item', () => { ...@@ -12,6 +13,9 @@ describe('Metadata Item', () => {
const mountComponent = (propsData = defaultProps) => { const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
propsData, propsData,
directives: {
GlTooltip: createMockDirective(),
},
}); });
}; };
...@@ -24,6 +28,7 @@ describe('Metadata Item', () => { ...@@ -24,6 +28,7 @@ describe('Metadata Item', () => {
const findLink = (w = wrapper) => w.find(GlLink); const findLink = (w = wrapper) => w.find(GlLink);
const findText = () => wrapper.find('[data-testid="metadata-item-text"]'); const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate); const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate);
const findTextTooltip = () => wrapper.find('[data-testid="text-tooltip-container"]');
describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', (size) => { describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', (size) => {
const className = `mw-${size}`; const className = `mw-${size}`;
...@@ -55,6 +60,22 @@ describe('Metadata Item', () => { ...@@ -55,6 +60,22 @@ describe('Metadata Item', () => {
expect(tooltip.exists()).toBe(true); expect(tooltip.exists()).toBe(true);
expect(tooltip.attributes('title')).toBe(defaultProps.text); expect(tooltip.attributes('title')).toBe(defaultProps.text);
}); });
describe('with tooltip prop set to something', () => {
const textTooltip = 'foo';
it('hides tooltip_on_truncate', () => {
mountComponent({ ...defaultProps, textTooltip });
expect(findTooltipOnTruncate(findText()).exists()).toBe(false);
});
it('set the tooltip on the text', () => {
mountComponent({ ...defaultProps, textTooltip });
const tooltip = getBinding(findTextTooltip().element, 'gl-tooltip');
expect(tooltip.value.title).toBe(textTooltip);
});
});
}); });
describe('link', () => { describe('link', () => {
......
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