Commit 6125f174 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Vitaly Slobodin

Add metadata item

- source
- tests
parent 09788355
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import PackageTags from '../../shared/components/package_tags.vue'; import PackageTags from '../../shared/components/package_tags.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils'; import { numberToHumanSize } from '~/lib/utils/number_utils';
import timeagoMixin from '~/vue_shared/mixins/timeago'; import timeagoMixin from '~/vue_shared/mixins/timeago';
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 { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
...@@ -12,9 +13,9 @@ export default { ...@@ -12,9 +13,9 @@ export default {
components: { components: {
TitleArea, TitleArea,
GlIcon, GlIcon,
GlLink,
GlSprintf, GlSprintf,
PackageTags, PackageTags,
MetadataItem,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -54,35 +55,24 @@ export default { ...@@ -54,35 +55,24 @@ export default {
</template> </template>
<template v-if="packageTypeDisplay" #metadata_type> <template v-if="packageTypeDisplay" #metadata_type>
<gl-icon name="package" class="gl-text-gray-500 gl-mr-3" /> <metadata-item data-testid="package-type" icon="package" :text="packageTypeDisplay" />
<span data-testid="package-type" class="gl-font-weight-bold">{{ packageTypeDisplay }}</span>
</template> </template>
<template #metadata_size> <template #metadata_size>
<gl-icon name="disk" class="gl-text-gray-500 gl-mr-3" /> <metadata-item data-testid="package-size" icon="disk" :text="totalSize" />
<span data-testid="package-size" class="gl-font-weight-bold">{{ totalSize }}</span>
</template> </template>
<template v-if="packagePipeline" #metadata_pipeline> <template v-if="packagePipeline" #metadata_pipeline>
<gl-icon name="review-list" class="gl-text-gray-500 gl-mr-3" /> <metadata-item
<gl-link
data-testid="pipeline-project" data-testid="pipeline-project"
:href="packagePipeline.project.web_url" icon="review-list"
class="gl-font-weight-bold gl-str-truncated" :text="packagePipeline.project.name"
> :link="packagePipeline.project.web_url"
{{ packagePipeline.project.name }} />
</gl-link>
</template> </template>
<template v-if="packagePipeline" #metadata_ref> <template v-if="packagePipeline" #metadata_ref>
<gl-icon name="branch" data-testid="package-ref-icon" class="gl-text-gray-500 gl-mr-3" /> <metadata-item data-testid="package-ref" icon="branch" :text="packagePipeline.ref" />
<span
v-gl-tooltip
data-testid="package-ref"
class="gl-font-weight-bold gl-str-truncated mw-xs"
:title="packagePipeline.ref"
>{{ packagePipeline.ref }}</span
>
</template> </template>
<template v-if="hasTagsToDisplay" #metadata_tags> <template v-if="hasTagsToDisplay" #metadata_tags>
......
<script> <script>
import { GlSprintf, GlLink, GlIcon } from '@gitlab/ui'; import { GlSprintf, GlLink } from '@gitlab/ui';
import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { n__ } from '~/locale'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { n__, sprintf } from '~/locale';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility'; import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import { import {
...@@ -14,10 +15,10 @@ import { ...@@ -14,10 +15,10 @@ import {
export default { export default {
components: { components: {
GlIcon,
GlSprintf, GlSprintf,
GlLink, GlLink,
TitleArea, TitleArea,
MetadataItem,
}, },
props: { props: {
expirationPolicy: { expirationPolicy: {
...@@ -58,11 +59,12 @@ export default { ...@@ -58,11 +59,12 @@ export default {
}, },
computed: { computed: {
imagesCountText() { imagesCountText() {
return n__( const pluralisedString = n__(
'ContainerRegistry|%{count} Image repository', 'ContainerRegistry|%{count} Image repository',
'ContainerRegistry|%{count} Image repositories', 'ContainerRegistry|%{count} Image repositories',
this.imagesCount, this.imagesCount,
); );
return sprintf(pluralisedString, { count: this.imagesCount });
}, },
timeTillRun() { timeTillRun() {
const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at); const difference = calculateRemainingMilliseconds(this.expirationPolicy?.next_run_at);
...@@ -73,7 +75,7 @@ export default { ...@@ -73,7 +75,7 @@ export default {
}, },
expirationPolicyText() { expirationPolicyText() {
return this.expirationPolicyEnabled return this.expirationPolicyEnabled
? EXPIRATION_POLICY_WILL_RUN_IN ? sprintf(EXPIRATION_POLICY_WILL_RUN_IN, { time: this.timeTillRun })
: EXPIRATION_POLICY_DISABLED_TEXT; : EXPIRATION_POLICY_DISABLED_TEXT;
}, },
showExpirationPolicyTip() { showExpirationPolicyTip() {
...@@ -92,24 +94,21 @@ export default { ...@@ -92,24 +94,21 @@ export default {
<slot name="commands"></slot> <slot name="commands"></slot>
</template> </template>
<template #metadata_count> <template #metadata_count>
<span v-if="imagesCount" data-testid="images-count"> <metadata-item
<gl-icon class="gl-mr-1" name="container-image" /> v-if="imagesCount"
<gl-sprintf :message="imagesCountText"> data-testid="images-count"
<template #count> icon="container-image"
{{ imagesCount }} :text="imagesCountText"
</template> />
</gl-sprintf>
</span>
</template> </template>
<template #metadata_exp_policies> <template #metadata_exp_policies>
<span v-if="!hideExpirationPolicyData" data-testid="expiration-policy"> <metadata-item
<gl-icon class="gl-mr-1" name="expire" /> v-if="!hideExpirationPolicyData"
<gl-sprintf :message="expirationPolicyText"> data-testid="expiration-policy"
<template #time> icon="expire"
{{ timeTillRun }} :text="expirationPolicyText"
</template> size="xl"
</gl-sprintf> />
</span>
</template> </template>
</title-area> </title-area>
......
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
name: 'MetadataItem',
components: {
GlIcon,
GlLink,
TooltipOnTruncate,
},
props: {
icon: {
type: String,
required: false,
default: null,
},
text: {
type: String,
required: true,
},
link: {
type: String,
required: false,
default: '',
},
size: {
type: String,
required: false,
default: 's',
validator(value) {
return !value || ['xs', 's', 'm', 'l', 'xl'].includes(value);
},
},
},
computed: {
sizeClass() {
return `mw-${this.size}`;
},
},
};
</script>
<template>
<div class="gl-display-inline-flex gl-align-items-center">
<gl-icon v-if="icon" :name="icon" class="gl-text-gray-500 gl-mr-3" />
<tooltip-on-truncate v-if="link" :title="text" class="gl-text-truncate" :class="sizeClass">
<gl-link :href="link" class="gl-font-weight-bold">
{{ text }}
</gl-link>
</tooltip-on-truncate>
<div
v-else
data-testid="metadata-item-text"
class="gl-font-weight-bold gl-display-inline-flex"
:class="sizeClass"
>
<tooltip-on-truncate :title="text" class="gl-text-truncate">
{{ text }}
</tooltip-on-truncate>
</div>
</div>
</template>
---
title: Update visual styling of container registry metadata
merge_request: 42202
author:
type: changed
...@@ -45,34 +45,24 @@ exports[`PackageTitle renders with tags 1`] = ` ...@@ -45,34 +45,24 @@ exports[`PackageTitle renders with tags 1`] = `
<div <div
class="gl-display-flex gl-align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon-stub <metadata-item-stub
class="gl-text-gray-500 gl-mr-3"
name="package"
size="16"
/>
<span
class="gl-font-weight-bold"
data-testid="package-type" data-testid="package-type"
> icon="package"
maven link=""
</span> size="s"
text="maven"
/>
</div> </div>
<div <div
class="gl-display-flex gl-align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon-stub <metadata-item-stub
class="gl-text-gray-500 gl-mr-3"
name="disk"
size="16"
/>
<span
class="gl-font-weight-bold"
data-testid="package-size" data-testid="package-size"
> icon="disk"
300 bytes link=""
</span> size="s"
text="300 bytes"
/>
</div> </div>
<div <div
class="gl-display-flex gl-align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
...@@ -135,34 +125,24 @@ exports[`PackageTitle renders without tags 1`] = ` ...@@ -135,34 +125,24 @@ exports[`PackageTitle renders without tags 1`] = `
<div <div
class="gl-display-flex gl-align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon-stub <metadata-item-stub
class="gl-text-gray-500 gl-mr-3"
name="package"
size="16"
/>
<span
class="gl-font-weight-bold"
data-testid="package-type" data-testid="package-type"
> icon="package"
maven link=""
</span> size="s"
text="maven"
/>
</div> </div>
<div <div
class="gl-display-flex gl-align-items-center gl-mr-5" class="gl-display-flex gl-align-items-center gl-mr-5"
> >
<gl-icon-stub <metadata-item-stub
class="gl-text-gray-500 gl-mr-3"
name="disk"
size="16"
/>
<span
class="gl-font-weight-bold"
data-testid="package-size" data-testid="package-size"
> icon="disk"
300 bytes link=""
</span> size="s"
text="300 bytes"
/>
</div> </div>
</div> </div>
</div> </div>
......
...@@ -52,7 +52,6 @@ describe('PackageTitle', () => { ...@@ -52,7 +52,6 @@ describe('PackageTitle', () => {
const packageSize = () => wrapper.find('[data-testid="package-size"]'); const packageSize = () => wrapper.find('[data-testid="package-size"]');
const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]'); const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
const packageRef = () => wrapper.find('[data-testid="package-ref"]'); const packageRef = () => wrapper.find('[data-testid="package-ref"]');
const packageRefIcon = () => wrapper.find('[data-testid="package-ref-icon"]');
const packageTags = () => wrapper.find(PackageTags); const packageTags = () => wrapper.find(PackageTags);
afterEach(() => { afterEach(() => {
...@@ -98,16 +97,16 @@ describe('PackageTitle', () => { ...@@ -98,16 +97,16 @@ describe('PackageTitle', () => {
}); });
describe.each` describe.each`
packageEntity | expectedResult packageEntity | text
${conanPackage} | ${'conan'} ${conanPackage} | ${'conan'}
${mavenPackage} | ${'maven'} ${mavenPackage} | ${'maven'}
${npmPackage} | ${'npm'} ${npmPackage} | ${'npm'}
${nugetPackage} | ${'nuget'} ${nugetPackage} | ${'nuget'}
`(`package type`, ({ packageEntity, expectedResult }) => { `(`package type`, ({ packageEntity, text }) => {
beforeEach(() => createComponent({ packageEntity })); beforeEach(() => createComponent({ packageEntity }));
it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => { it(`${packageEntity.package_type} should render from Vuex getters ${text}`, () => {
expect(packageType().text()).toBe(expectedResult); expect(packageType().props()).toEqual(expect.objectContaining({ text, icon: 'package' }));
}); });
}); });
...@@ -115,13 +114,13 @@ describe('PackageTitle', () => { ...@@ -115,13 +114,13 @@ describe('PackageTitle', () => {
it('correctly calculates when there is only 1 file', async () => { it('correctly calculates when there is only 1 file', async () => {
await createComponent({ packageEntity: npmPackage, packageFiles: npmFiles }); await createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
expect(packageSize().text()).toBe('200 bytes'); expect(packageSize().props()).toMatchObject({ text: '200 bytes', icon: 'disk' });
}); });
it('correctly calulates when there are multiple files', async () => { it('correctly calulates when there are multiple files', async () => {
await createComponent(); await createComponent();
expect(packageSize().text()).toBe('300 bytes'); expect(packageSize().props('text')).toBe('300 bytes');
}); });
}); });
...@@ -153,8 +152,10 @@ describe('PackageTitle', () => { ...@@ -153,8 +152,10 @@ describe('PackageTitle', () => {
it('correctly shows the package ref if there is one', async () => { it('correctly shows the package ref if there is one', async () => {
await createComponent({ packageEntity: npmPackage }); await createComponent({ packageEntity: npmPackage });
expect(packageRefIcon().exists()).toBe(true); expect(packageRef().props()).toMatchObject({
expect(packageRef().text()).toBe(npmPackage.pipeline.ref); text: npmPackage.pipeline.ref,
icon: 'branch',
});
}); });
}); });
...@@ -168,8 +169,11 @@ describe('PackageTitle', () => { ...@@ -168,8 +169,11 @@ describe('PackageTitle', () => {
it('correctly shows the pipeline project if there is one', async () => { it('correctly shows the pipeline project if there is one', async () => {
await createComponent({ packageEntity: npmPackage }); await createComponent({ packageEntity: npmPackage });
expect(pipelineProject().text()).toBe(npmPackage.pipeline.project.name); expect(pipelineProject().props()).toMatchObject({
expect(pipelineProject().attributes('href')).toBe(npmPackage.pipeline.project.web_url); text: npmPackage.pipeline.project.name,
icon: 'review-list',
link: npmPackage.pipeline.project.web_url,
});
}); });
}); });
}); });
...@@ -7,7 +7,6 @@ import { ...@@ -7,7 +7,6 @@ import {
LIST_INTRO_TEXT, LIST_INTRO_TEXT,
EXPIRATION_POLICY_DISABLED_MESSAGE, EXPIRATION_POLICY_DISABLED_MESSAGE,
EXPIRATION_POLICY_DISABLED_TEXT, EXPIRATION_POLICY_DISABLED_TEXT,
EXPIRATION_POLICY_WILL_RUN_IN,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
jest.mock('~/lib/utils/datetime_utility', () => ({ jest.mock('~/lib/utils/datetime_utility', () => ({
...@@ -68,13 +67,16 @@ describe('registry_header', () => { ...@@ -68,13 +67,16 @@ describe('registry_header', () => {
it('when there is one image', async () => { it('when there is one image', async () => {
await mountComponent({ imagesCount: 1 }); await mountComponent({ imagesCount: 1 });
expect(findImagesCountSubHeader().text()).toMatchInterpolatedText('1 Image repository'); expect(findImagesCountSubHeader().props()).toMatchObject({
text: '1 Image repository',
icon: 'container-image',
});
}); });
it('when there is more than one image', async () => { it('when there is more than one image', async () => {
await mountComponent({ imagesCount: 3 }); await mountComponent({ imagesCount: 3 });
expect(findImagesCountSubHeader().text()).toMatchInterpolatedText('3 Image repositories'); expect(findImagesCountSubHeader().props('text')).toBe('3 Image repositories');
}); });
}); });
...@@ -88,7 +90,11 @@ describe('registry_header', () => { ...@@ -88,7 +90,11 @@ describe('registry_header', () => {
const text = findExpirationPolicySubHeader(); const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(true); expect(text.exists()).toBe(true);
expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_DISABLED_TEXT); expect(text.props()).toMatchObject({
text: EXPIRATION_POLICY_DISABLED_TEXT,
icon: 'expire',
size: 'xl',
});
}); });
it('when is enabled', async () => { it('when is enabled', async () => {
...@@ -100,7 +106,7 @@ describe('registry_header', () => { ...@@ -100,7 +106,7 @@ describe('registry_header', () => {
const text = findExpirationPolicySubHeader(); const text = findExpirationPolicySubHeader();
expect(text.exists()).toBe(true); expect(text.exists()).toBe(true);
expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_WILL_RUN_IN); expect(text.props('text')).toBe('Expiration policy will run in ');
}); });
it('when the expiration policy is completely disabled', async () => { it('when the expiration policy is completely disabled', async () => {
await mountComponent({ await mountComponent({
......
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlLink } from '@gitlab/ui';
import component from '~/vue_shared/components/registry/metadata_item.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
describe('Metadata Item', () => {
let wrapper;
const defaultProps = {
text: 'foo',
};
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findIcon = () => wrapper.find(GlIcon);
const findLink = (w = wrapper) => w.find(GlLink);
const findText = () => wrapper.find('[data-testid="metadata-item-text"]');
const findTooltipOnTruncate = (w = wrapper) => w.find(TooltipOnTruncate);
describe.each(['xs', 's', 'm', 'l', 'xl'])('size class', size => {
const className = `mw-${size}`;
it(`${size} is assigned correctly to text`, () => {
mountComponent({ ...defaultProps, size });
expect(findText().classes()).toContain(className);
});
it(`${size} is assigned correctly to link`, () => {
mountComponent({ ...defaultProps, link: 'foo', size });
expect(findTooltipOnTruncate().classes()).toContain(className);
});
});
describe('text', () => {
it('display a proper text', () => {
mountComponent();
expect(findText().text()).toBe(defaultProps.text);
});
it('uses tooltip_on_truncate', () => {
mountComponent();
const tooltip = findTooltipOnTruncate(findText());
expect(tooltip.exists()).toBe(true);
expect(tooltip.attributes('title')).toBe(defaultProps.text);
});
});
describe('link', () => {
it('if a link prop is passed shows a link and hides the text', () => {
mountComponent({ ...defaultProps, link: 'bar' });
expect(findLink().exists()).toBe(true);
expect(findText().exists()).toBe(false);
expect(findLink().attributes('href')).toBe('bar');
});
it('uses tooltip_on_truncate', () => {
mountComponent({ ...defaultProps, link: 'bar' });
const tooltip = findTooltipOnTruncate();
expect(tooltip.exists()).toBe(true);
expect(tooltip.attributes('title')).toBe(defaultProps.text);
expect(findLink(tooltip).exists()).toBe(true);
});
it('hides the link and shows the test if a link prop is not passed', () => {
mountComponent();
expect(findText().exists()).toBe(true);
expect(findLink().exists()).toBe(false);
});
});
describe('icon', () => {
it('if a icon prop is passed shows a icon', () => {
mountComponent({ ...defaultProps, icon: 'pencil' });
expect(findIcon().exists()).toBe(true);
expect(findIcon().props('name')).toBe('pencil');
});
it('if a icon prop is not passed hides the icon', () => {
mountComponent();
expect(findIcon().exists()).toBe(false);
});
});
});
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