Commit 6d499a19 authored by David O'Regan's avatar David O'Regan

Merge branch '341370-dast-view-scans-truncate-values-gl-truncate' into 'master'

Truncate long table values in on-demand scans page

See merge request gitlab-org/gitlab!73641
parents 9bf98911 8f1a83be
...@@ -12,6 +12,7 @@ export default { ...@@ -12,6 +12,7 @@ export default {
{ {
label: __('Status'), label: __('Status'),
key: 'detailedStatus', key: 'detailedStatus',
columnClass: 'gl-w-15',
}, },
{ {
label: __('Name'), label: __('Name'),
...@@ -20,6 +21,7 @@ export default { ...@@ -20,6 +21,7 @@ export default {
{ {
label: s__('OnDemandScans|Scan type'), label: s__('OnDemandScans|Scan type'),
key: 'scanType', key: 'scanType',
columnClass: 'gl-w-13',
}, },
{ {
label: s__('OnDemandScans|Target'), label: s__('OnDemandScans|Target'),
...@@ -28,10 +30,12 @@ export default { ...@@ -28,10 +30,12 @@ export default {
{ {
label: __('Start date'), label: __('Start date'),
key: 'createdAt', key: 'createdAt',
columnClass: 'gl-w-15',
}, },
{ {
label: __('Pipeline'), label: __('Pipeline'),
key: 'id', key: 'id',
columnClass: 'gl-w-13',
}, },
], ],
i18n: { i18n: {
......
...@@ -7,6 +7,7 @@ import { ...@@ -7,6 +7,7 @@ import {
GlKeysetPagination, GlKeysetPagination,
GlAlert, GlAlert,
GlSkeletonLoader, GlSkeletonLoader,
GlTruncate,
} from '@gitlab/ui'; } from '@gitlab/ui';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
...@@ -36,6 +37,7 @@ export default { ...@@ -36,6 +37,7 @@ export default {
GlKeysetPagination, GlKeysetPagination,
GlAlert, GlAlert,
GlSkeletonLoader, GlSkeletonLoader,
GlTruncate,
CiBadgeLink, CiBadgeLink,
TimeAgoTooltip, TimeAgoTooltip,
EmptyState, EmptyState,
...@@ -122,9 +124,8 @@ export default { ...@@ -122,9 +124,8 @@ export default {
return this.pipelines?.pageInfo; return this.pipelines?.pageInfo;
}, },
tableFields() { tableFields() {
return this.fields.map(({ key, label }) => ({ return this.fields.map((field) => ({
key, ...field,
label,
class: ['gl-text-black-normal'], class: ['gl-text-black-normal'],
thClass: ['gl-bg-transparent!', 'gl-white-space-nowrap'], thClass: ['gl-bg-transparent!', 'gl-white-space-nowrap'],
})); }));
...@@ -188,7 +189,12 @@ export default { ...@@ -188,7 +189,12 @@ export default {
:items="pipelineNodes" :items="pipelineNodes"
:busy="$apollo.queries.pipelines.loading" :busy="$apollo.queries.pipelines.loading"
stacked="md" stacked="md"
fixed
> >
<template #table-colgroup="scope">
<col v-for="field in scope.fields" :key="field.key" :class="field.columnClass" />
</template>
<template #table-busy> <template #table-busy>
<gl-skeleton-loader v-for="i in 20" :key="i" :width="1000" :height="45"> <gl-skeleton-loader v-for="i in 20" :key="i" :width="1000" :height="45">
<rect width="85" height="20" x="0" y="5" rx="4" /> <rect width="85" height="20" x="0" y="5" rx="4" />
...@@ -205,12 +211,30 @@ export default { ...@@ -205,12 +211,30 @@ export default {
</div> </div>
</template> </template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastProfile.name)="{ item }">
<gl-truncate v-if="item.dastProfile" :text="item.dastProfile.name" with-tooltip />
</template>
<template #cell(scanType)> <template #cell(scanType)>
{{ $options.DAST_SHORT_NAME }} {{ $options.DAST_SHORT_NAME }}
</template> </template>
<!-- eslint-disable-next-line vue/valid-v-slot -->
<template #cell(dastProfile.dastSiteProfile.targetUrl)="{ item }">
<gl-truncate
v-if="item.dastProfile"
:text="item.dastProfile.dastSiteProfile.targetUrl"
with-tooltip
/>
</template>
<template #cell(createdAt)="{ item }"> <template #cell(createdAt)="{ item }">
<time-ago-tooltip v-if="item.createdAt" :time="item.createdAt" tooltip-placement="left" /> <time-ago-tooltip
v-if="item.createdAt"
class="gl-white-space-nowrap"
:time="item.createdAt"
/>
</template> </template>
<template #cell(id)="{ item }"> <template #cell(id)="{ item }">
......
...@@ -15,6 +15,7 @@ RSpec.describe 'On-demand DAST scans (GraphQL fixtures)' do ...@@ -15,6 +15,7 @@ RSpec.describe 'On-demand DAST scans (GraphQL fixtures)' do
path = 'on_demand_scans/graphql/on_demand_scans.query.graphql' path = 'on_demand_scans/graphql/on_demand_scans.query.graphql'
before do before do
stub_licensed_features(security_on_demand_scans: true)
project.add_developer(current_user) project.add_developer(current_user)
end end
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
exports[`AllTab renders the base tab with the correct props 1`] = ` exports[`AllTab renders the base tab with the correct props 1`] = `
Array [ Array [
Object { Object {
"columnClass": "gl-w-15",
"key": "detailedStatus", "key": "detailedStatus",
"label": "Status", "label": "Status",
}, },
...@@ -11,6 +12,7 @@ Array [ ...@@ -11,6 +12,7 @@ Array [
"label": "Name", "label": "Name",
}, },
Object { Object {
"columnClass": "gl-w-13",
"key": "scanType", "key": "scanType",
"label": "Scan type", "label": "Scan type",
}, },
...@@ -19,10 +21,12 @@ Array [ ...@@ -19,10 +21,12 @@ Array [
"label": "Target", "label": "Target",
}, },
Object { Object {
"columnClass": "gl-w-15",
"key": "createdAt", "key": "createdAt",
"label": "Start date", "label": "Start date",
}, },
Object { Object {
"columnClass": "gl-w-13",
"key": "id", "key": "id",
"label": "Pipeline", "label": "Pipeline",
}, },
......
import { GlTab, GlTable, GlAlert } from '@gitlab/ui'; import { GlTab, GlTable, GlAlert } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils'; import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import { merge } from 'lodash';
import allPipelinesWithPipelinesMock from 'test_fixtures/graphql/on_demand_scans/graphql/on_demand_scans.query.graphql.with_pipelines.json'; import allPipelinesWithPipelinesMock from 'test_fixtures/graphql/on_demand_scans/graphql/on_demand_scans.query.graphql.with_pipelines.json';
import allPipelinesWithoutPipelinesMock from 'test_fixtures/graphql/on_demand_scans/graphql/on_demand_scans.query.graphql.without_pipelines.json'; import allPipelinesWithoutPipelinesMock from 'test_fixtures/graphql/on_demand_scans/graphql/on_demand_scans.query.graphql.without_pipelines.json';
import { stubComponent } from 'helpers/stub_component'; import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BaseTab from 'ee/on_demand_scans/components/tabs/base_tab.vue'; import BaseTab from 'ee/on_demand_scans/components/tabs/base_tab.vue';
import EmptyState from 'ee/on_demand_scans/components/empty_state.vue'; import EmptyState from 'ee/on_demand_scans/components/empty_state.vue';
import createMockApollo from 'helpers/mock_apollo_helper'; import createMockApollo from 'helpers/mock_apollo_helper';
...@@ -13,6 +14,7 @@ import { createRouter } from 'ee/on_demand_scans/router'; ...@@ -13,6 +14,7 @@ import { createRouter } from 'ee/on_demand_scans/router';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { scrollToElement } from '~/lib/utils/common_utils'; import { scrollToElement } from '~/lib/utils/common_utils';
import setWindowLocation from 'helpers/set_window_location_helper'; import setWindowLocation from 'helpers/set_window_location_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
jest.mock('~/lib/utils/common_utils'); jest.mock('~/lib/utils/common_utils');
...@@ -44,9 +46,12 @@ describe('BaseTab', () => { ...@@ -44,9 +46,12 @@ describe('BaseTab', () => {
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}; };
const createComponent = (propsData) => { const createComponentFactory = (mountFn = shallowMountExtended) => (options = {}) => {
router = createRouter(); router = createRouter();
wrapper = shallowMountExtended(BaseTab, { wrapper = mountFn(
BaseTab,
merge(
{
localVue, localVue,
apolloProvider: createMockApolloProvider(), apolloProvider: createMockApolloProvider(),
router, router,
...@@ -54,8 +59,32 @@ describe('BaseTab', () => { ...@@ -54,8 +59,32 @@ describe('BaseTab', () => {
title: 'All', title: 'All',
query: onDemandScansQuery, query: onDemandScansQuery,
itemsCount: 0, itemsCount: 0,
fields: [{ name: 'ID', key: 'id' }], fields: [
...propsData, {
label: 'Status',
key: 'detailedStatus',
},
{
label: 'Name',
key: 'dastProfile.name',
},
{
label: 'OnDemandScans|Scan type',
key: 'scanType',
},
{
label: 'OnDemandScans|Target',
key: 'dastProfile.dastSiteProfile.targetUrl',
},
{
label: 'Start date',
key: 'createdAt',
},
{
label: 'Pipeline',
key: 'id',
},
],
}, },
provide: { provide: {
projectPath, projectPath,
...@@ -75,9 +104,15 @@ describe('BaseTab', () => { ...@@ -75,9 +104,15 @@ describe('BaseTab', () => {
props: ['items', 'busy'], props: ['items', 'busy'],
}), }),
}, },
}); },
options,
),
);
}; };
const createComponent = createComponentFactory();
const createFullComponent = createComponentFactory(mountExtended);
beforeEach(() => { beforeEach(() => {
requestHandler = jest.fn().mockResolvedValue(allPipelinesWithPipelinesMock); requestHandler = jest.fn().mockResolvedValue(allPipelinesWithPipelinesMock);
}); });
...@@ -127,11 +162,13 @@ describe('BaseTab', () => { ...@@ -127,11 +162,13 @@ describe('BaseTab', () => {
describe('when there are pipelines', () => { describe('when there are pipelines', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
propsData: {
itemsCount: 30, itemsCount: 30,
},
}); });
}); });
it('renders the title with the item count', () => { it('renders the title with the item count', async () => {
expect(findTitle().text()).toMatchInterpolatedText('All 30'); expect(findTitle().text()).toMatchInterpolatedText('All 30');
}); });
...@@ -171,6 +208,66 @@ describe('BaseTab', () => { ...@@ -171,6 +208,66 @@ describe('BaseTab', () => {
}); });
}); });
describe('rendered cells', () => {
const [firstPipeline] = allPipelinesWithPipelinesMock.data.project.pipelines.nodes;
const findFirstRow = () => wrapper.find('tbody > tr');
const findCellAt = (index) => findFirstRow().findAll('td').at(index);
beforeEach(() => {
createFullComponent({
propsData: {
itemsCount: 30,
},
stubs: {
GlTable: false,
},
});
});
it('renders the status badge', () => {
const statusCell = findCellAt(0);
expect(statusCell.text()).toBe(firstPipeline.detailedStatus.text);
});
it('renders the name with GlTruncate', () => {
const nameCell = findCellAt(1);
const truncateContainer = nameCell.find('[data-testid="truncate-end-container"]');
expect(truncateContainer.exists()).toBe(true);
expect(truncateContainer.text()).toBe(firstPipeline.dastProfile.name);
});
it('renders the scan type', () => {
const scanTypeCell = findCellAt(2);
expect(scanTypeCell.text()).toBe('DAST');
});
it('renders the target URL with GlTruncate', () => {
const targetUrlCell = findCellAt(3);
const truncateContainer = targetUrlCell.find('[data-testid="truncate-end-container"]');
expect(truncateContainer.exists()).toBe(true);
expect(truncateContainer.text()).toBe(firstPipeline.dastProfile.dastSiteProfile.targetUrl);
});
it('renders the start date as a timeElement', () => {
const startDateCell = findCellAt(4);
const timeElement = startDateCell.find('time');
expect(timeElement.exists()).toBe(true);
expect(timeElement.attributes('datetime')).toBe(firstPipeline.createdAt);
});
it('renders the pipeline ID', () => {
const pipelineIdCell = findCellAt(5);
expect(pipelineIdCell.text()).toBe(`#${getIdFromGraphQLId(firstPipeline.id)}`);
});
});
describe('when there are no pipelines', () => { describe('when there are no pipelines', () => {
beforeEach(() => { beforeEach(() => {
requestHandler = jest.fn().mockResolvedValue(allPipelinesWithoutPipelinesMock); requestHandler = jest.fn().mockResolvedValue(allPipelinesWithoutPipelinesMock);
......
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