Commit 5c15e31e authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '330846-convert-package-list-page-to-use-apollo-graphql' into 'master'

Move and refactor package list shared components

See merge request gitlab-org/gitlab!71717
parents 21133fee 8c9e551d
<script>
import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
import { s__ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
PACKAGE_ERROR_STATUS,
PACKAGE_DEFAULT_STATUS,
} from '~/packages_and_registries/package_registry/constants';
import { getPackageTypeLabel } from '~/packages/shared/utils';
import PackagePath from '~/packages/shared/components/package_path.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
name: 'PackageListRow',
components: {
GlButton,
GlLink,
GlSprintf,
GlTruncate,
PackageTags,
PackagePath,
PublishMethod,
ListItem,
PackageIconAndName,
TimeagoTooltip,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: ['isGroupPage'],
props: {
packageEntity: {
type: Object,
required: true,
},
},
computed: {
packageType() {
return getPackageTypeLabel(this.packageEntity.packageType.toLowerCase());
},
packageLink() {
const { project, id } = this.packageEntity;
return `${project?.webUrl}/-/packages/${getIdFromGraphQLId(id)}`;
},
pipeline() {
return this.packageEntity?.pipelines?.nodes[0];
},
pipelineUser() {
return this.pipeline?.user?.name;
},
showWarningIcon() {
return this.packageEntity.status === PACKAGE_ERROR_STATUS;
},
showTags() {
return Boolean(this.packageEntity.tags?.nodes?.length);
},
disabledRow() {
return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
},
},
i18n: {
erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'),
},
};
</script>
<template>
<list-item data-qa-selector="package_row" :disabled="disabledRow">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link
:href="packageLink"
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
:disabled="disabledRow"
>
<gl-truncate :text="packageEntity.name" />
</gl-link>
<gl-button
v-if="showWarningIcon"
v-gl-tooltip="{ title: $options.i18n.erroredPackageText }"
class="gl-hover-bg-transparent!"
icon="warning"
category="tertiary"
data-testid="warning-icon"
:aria-label="__('Warning')"
/>
<package-tags
v-if="showTags"
class="gl-ml-3"
:tags="packageEntity.tags.nodes"
hide-label
:tag-display-limit="1"
/>
</div>
</template>
<template #left-secondary>
<div class="gl-display-flex" data-testid="left-secondary-infos">
<span>{{ packageEntity.version }}</span>
<div v-if="pipelineUser" class="gl-display-none gl-sm-display-flex gl-ml-2">
<gl-sprintf :message="s__('PackageRegistry|published by %{author}')">
<template #author>{{ pipelineUser }}</template>
</gl-sprintf>
</div>
<package-icon-and-name>
{{ packageType }}
</package-icon-and-name>
<package-path
v-if="isGroupPage"
:path="packageEntity.project.fullPath"
:disabled="disabledRow"
/>
</div>
</template>
<template #right-primary>
<publish-method :pipeline="pipeline" />
</template>
<template #right-secondary>
<span>
<gl-sprintf :message="__('Created %{timestamp}')">
<template #timestamp>
<timeago-tooltip :time="packageEntity.createdAt" />
</template>
</gl-sprintf>
</span>
</template>
<template v-if="!disabledRow" #right-action>
<gl-button
data-testid="action-delete"
icon="remove"
category="secondary"
variant="danger"
:title="s__('PackageRegistry|Remove package')"
:aria-label="s__('PackageRegistry|Remove package')"
@click="$emit('packageToDelete', packageEntity)"
/>
</template>
</list-item>
</template>
<script>
import { GlIcon, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'PublishMethod',
components: {
ClipboardButton,
GlIcon,
GlLink,
},
props: {
pipeline: {
type: Object,
required: false,
default: null,
},
},
computed: {
hasPipeline() {
return Boolean(this.pipeline);
},
packageShaShort() {
return this.pipeline?.sha?.substring(0, 8);
},
},
i18n: {
COPY_COMMIT_SHA: __('Copy commit SHA'),
MANUALLY_PUBLISHED: s__('PackageRegistry|Manually Published'),
},
};
</script>
<template>
<div class="gl-display-flex gl-align-items-center">
<template v-if="hasPipeline">
<gl-icon name="git-merge" class="gl-mr-2" />
<span data-testid="pipeline-ref" class="gl-mr-2">{{ pipeline.ref }}</span>
<gl-icon name="commit" class="gl-mr-2" />
<gl-link data-testid="pipeline-sha" :href="pipeline.commitPath" class="gl-mr-2">{{
packageShaShort
}}</gl-link>
<clipboard-button
:text="pipeline.sha"
:title="$options.i18n.COPY_COMMIT_SHA"
category="tertiary"
size="small"
/>
</template>
<template v-else>
<gl-icon name="upload" class="gl-mr-2" />
<span data-testid="manually-published">
{{ $options.i18n.MANUALLY_PUBLISHED }}
</span>
</template>
</div>
</template>
fragment PackageData on Package { fragment PackageData on Package {
id id
name name
version
packageType
createdAt
status
tags {
nodes {
name
}
}
pipelines {
nodes {
sha
ref
commitPath
user {
name
}
}
}
project {
fullPath
webUrl
}
} }
import { GlLink } from '@gitlab/ui'; import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import PackagesListRow from '~/packages/shared/components/package_list_row.vue'; import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
...@@ -19,14 +19,14 @@ describe('packages_list_row', () => { ...@@ -19,14 +19,14 @@ describe('packages_list_row', () => {
const InfrastructureIconAndName = { name: 'InfrastructureIconAndName', template: '<div></div>' }; const InfrastructureIconAndName = { name: 'InfrastructureIconAndName', template: '<div></div>' };
const PackageIconAndName = { name: 'PackageIconAndName', template: '<div></div>' }; const PackageIconAndName = { name: 'PackageIconAndName', template: '<div></div>' };
const findPackageTags = () => wrapper.find(PackageTags); const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPackagePath = () => wrapper.find(PackagePath); const findPackagePath = () => wrapper.findComponent(PackagePath);
const findDeleteButton = () => wrapper.find('[data-testid="action-delete"]'); const findDeleteButton = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName); const findPackageIconAndName = () => wrapper.findComponent(PackageIconAndName);
const findInfrastructureIconAndName = () => wrapper.findComponent(InfrastructureIconAndName); const findInfrastructureIconAndName = () => wrapper.findComponent(InfrastructureIconAndName);
const findListItem = () => wrapper.findComponent(ListItem); const findListItem = () => wrapper.findComponent(ListItem);
const findPackageLink = () => wrapper.findComponent(GlLink); const findPackageLink = () => wrapper.findComponent(GlLink);
const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const mountComponent = ({ const mountComponent = ({
isGroup = false, isGroup = false,
...@@ -35,7 +35,7 @@ describe('packages_list_row', () => { ...@@ -35,7 +35,7 @@ describe('packages_list_row', () => {
disableDelete = false, disableDelete = false,
provide, provide,
} = {}) => { } = {}) => {
wrapper = shallowMount(PackagesListRow, { wrapper = shallowMountExtended(PackagesListRow, {
store, store,
provide, provide,
stubs: { stubs: {
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`packages_list_row renders 1`] = `
<div
class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
data-qa-selector="package_row"
>
<div
class="gl-display-flex gl-align-items-center gl-py-3 gl-px-5"
>
<!---->
<div
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
>
<div
class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
>
<div
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
>
<div
class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
>
<gl-link-stub
class="gl-text-body gl-min-w-0"
data-qa-selector="package_link"
href="http://gdk.test:3000/gitlab-org/gitlab-test/-/packages/111"
>
<gl-truncate-stub
position="end"
text="@gitlab-org/package-15"
/>
</gl-link-stub>
<!---->
<!---->
</div>
<!---->
</div>
<div
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
>
<div
class="gl-display-flex"
data-testid="left-secondary-infos"
>
<span>
1.0.0
</span>
<!---->
<package-icon-and-name-stub>
npm
</package-icon-and-name-stub>
<!---->
</div>
</div>
</div>
<div
class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
>
<div
class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
>
<publish-method-stub />
</div>
<div
class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<span>
Created
<timeago-tooltip-stub
cssclass=""
time="2020-08-17T14:23:32Z"
tooltipplacement="top"
/>
</span>
</div>
</div>
</div>
<div
class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
>
<gl-button-stub
aria-label="Remove package"
buttontextclasses=""
category="secondary"
data-testid="action-delete"
icon="remove"
size="medium"
title="Remove package"
variant="danger"
/>
</div>
</div>
<div
class="gl-display-flex"
>
<div
class="gl-w-7"
/>
<!---->
<div
class="gl-w-9"
/>
</div>
</div>
`;
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`publish_method renders 1`] = `
<div
class="gl-display-flex gl-align-items-center"
>
<gl-icon-stub
class="gl-mr-2"
name="git-merge"
size="16"
/>
<span
class="gl-mr-2"
data-testid="pipeline-ref"
>
master
</span>
<gl-icon-stub
class="gl-mr-2"
name="commit"
size="16"
/>
<gl-link-stub
class="gl-mr-2"
data-testid="pipeline-sha"
href="/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0"
>
b83d6e39
</gl-link-stub>
<clipboard-button-stub
category="tertiary"
size="small"
text="b83d6e391c22777fca1ed3012fce84f633d7fed0"
title="Copy commit SHA"
tooltipplacement="top"
/>
</div>
`;
import { GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagePath from '~/packages/shared/components/package_path.vue';
import PackageTags from '~/packages/shared/components/package_tags.vue';
import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue';
import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { packageData, packagePipelines, packageProject, packageTags } from '../../mock_data';
describe('packages_list_row', () => {
let wrapper;
const defaultProvide = {
isGroupPage: false,
};
const packageWithoutTags = { ...packageData(), project: packageProject() };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
const findPackageTags = () => wrapper.find(PackageTags);
const findPackagePath = () => wrapper.find(PackagePath);
const findDeleteButton = () => wrapper.findByTestId('action-delete');
const findPackageIconAndName = () => wrapper.find(PackageIconAndName);
const findListItem = () => wrapper.findComponent(ListItem);
const findPackageLink = () => wrapper.findComponent(GlLink);
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const mountComponent = ({
packageEntity = packageWithoutTags,
provide = defaultProvide,
} = {}) => {
wrapper = shallowMountExtended(PackagesListRow, {
provide,
stubs: {
ListItem,
GlSprintf,
},
propsData: {
packageEntity,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
});
describe('tags', () => {
it('renders package tags when a package has tags', () => {
mountComponent({ packageEntity: packageWithTags });
expect(findPackageTags().exists()).toBe(true);
});
it('does not render when there are no tags', () => {
mountComponent();
expect(findPackageTags().exists()).toBe(false);
});
});
describe('when it is group', () => {
it('has a package path component', () => {
mountComponent({ provide: { isGroupPage: true } });
expect(findPackagePath().exists()).toBe(true);
expect(findPackagePath().props()).toMatchObject({ path: 'gitlab-org/gitlab-test' });
});
});
describe('delete button', () => {
it('exists and has the correct props', () => {
mountComponent({ packageEntity: packageWithoutTags });
expect(findDeleteButton().exists()).toBe(true);
expect(findDeleteButton().attributes()).toMatchObject({
icon: 'remove',
category: 'secondary',
variant: 'danger',
title: 'Remove package',
});
});
it('emits the packageToDelete event when the delete button is clicked', async () => {
mountComponent({ packageEntity: packageWithoutTags });
findDeleteButton().vm.$emit('click');
await wrapper.vm.$nextTick();
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
});
});
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } });
});
it('list item has a disabled prop', () => {
expect(findListItem().props('disabled')).toBe(true);
});
it('details link is disabled', () => {
expect(findPackageLink().attributes('disabled')).toBe('true');
});
it('has a warning icon', () => {
const icon = findWarningIcon();
const tooltip = getBinding(icon.element, 'gl-tooltip');
expect(icon.props('icon')).toBe('warning');
expect(tooltip.value).toMatchObject({
title: 'Invalid Package: failed metadata extraction',
});
});
it('delete button does not exist', () => {
expect(findDeleteButton().exists()).toBe(false);
});
});
describe('secondary left info', () => {
it('has the package version', () => {
mountComponent();
expect(findLeftSecondaryInfos().text()).toContain(packageWithoutTags.version);
});
it('if the pipeline exists show the author message', () => {
mountComponent({
packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
});
expect(findLeftSecondaryInfos().text()).toContain('published by Administrator');
});
it('has icon and name component', () => {
mountComponent();
expect(findPackageIconAndName().text()).toBe(packageWithoutTags.packageType.toLowerCase());
});
});
});
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
import { packagePipelines } from '../../mock_data';
const [pipelineData] = packagePipelines();
describe('publish_method', () => {
let wrapper;
const findPipelineRef = () => wrapper.findByTestId('pipeline-ref');
const findPipelineSha = () => wrapper.findByTestId('pipeline-sha');
const findManualPublish = () => wrapper.findByTestId('manually-published');
const mountComponent = (pipeline = pipelineData) => {
wrapper = shallowMountExtended(PublishMethod, {
propsData: {
pipeline,
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('renders', () => {
mountComponent();
expect(wrapper.element).toMatchSnapshot();
});
describe('pipeline information', () => {
it('displays branch and commit when pipeline info exists', () => {
mountComponent();
expect(findPipelineRef().exists()).toBe(true);
expect(findPipelineSha().exists()).toBe(true);
});
it('does not show any pipeline details when no information exists', () => {
mountComponent(null);
expect(findPipelineRef().exists()).toBe(false);
expect(findPipelineSha().exists()).toBe(false);
expect(findManualPublish().text()).toBe(PublishMethod.i18n.MANUALLY_PUBLISHED);
});
});
});
...@@ -86,6 +86,12 @@ export const dependencyLinks = () => [ ...@@ -86,6 +86,12 @@ export const dependencyLinks = () => [
}, },
]; ];
export const packageProject = () => ({
fullPath: 'gitlab-org/gitlab-test',
webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-test',
__typename: 'Project',
});
export const packageVersions = () => [ export const packageVersions = () => [
{ {
createdAt: '2021-08-10T09:33:54Z', createdAt: '2021-08-10T09:33:54Z',
...@@ -257,14 +263,18 @@ export const packagesListQuery = (type = 'group') => ({ ...@@ -257,14 +263,18 @@ export const packagesListQuery = (type = 'group') => ({
count: 2, count: 2,
nodes: [ nodes: [
{ {
__typename: 'Package', ...packageData(),
id: 'gid://gitlab/Packages::Package/247', project: packageProject(),
name: 'version_test1', tags: { nodes: packageTags() },
pipelines: {
nodes: packagePipelines(),
},
}, },
{ {
__typename: 'Package', ...packageData(),
id: 'gid://gitlab/Packages::Package/246', project: packageProject(),
name: 'version_test1', tags: { nodes: [] },
pipelines: { nodes: [] },
}, },
], ],
__typename: 'PackageConnection', __typename: 'PackageConnection',
......
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