Commit 40dcce91 authored by Savas Vedova's avatar Savas Vedova

Merge branch...

Merge branch '327380-fe-generic-report-schema-render-table-type-on-vulnerability-details-page' into 'master'

Generic Report Schema: Render 'table' type on vulnerability details page

See merge request gitlab-org/gitlab!62546
parents e7fe5027 2bf3b324
...@@ -61,7 +61,7 @@ export default { ...@@ -61,7 +61,7 @@ export default {
<div class="generic-report-container" data-testid="reports"> <div class="generic-report-container" data-testid="reports">
<template v-for="[label, item] in detailsEntries"> <template v-for="[label, item] in detailsEntries">
<div :key="label" class="generic-report-row" :data-testid="`report-row-${label}`"> <div :key="label" class="generic-report-row" :data-testid="`report-row-${label}`">
<strong class="generic-report-column">{{ item.name }}</strong> <strong class="generic-report-column">{{ item.name || label }}</strong>
<div class="generic-report-column" data-testid="reportContent"> <div class="generic-report-column" data-testid="reportContent">
<report-item :item="item" :data-testid="`report-item-${label}`" /> <report-item :item="item" :data-testid="`report-item-${label}`" />
</div> </div>
......
...@@ -9,6 +9,7 @@ export const REPORT_TYPES = { ...@@ -9,6 +9,7 @@ export const REPORT_TYPES = {
value: 'value', value: 'value',
moduleLocation: 'module-location', moduleLocation: 'module-location',
fileLocation: 'file-location', fileLocation: 'file-location',
table: 'table',
}; };
const REPORT_TYPE_TO_COMPONENT_MAP = { const REPORT_TYPE_TO_COMPONENT_MAP = {
...@@ -20,6 +21,7 @@ const REPORT_TYPE_TO_COMPONENT_MAP = { ...@@ -20,6 +21,7 @@ const REPORT_TYPE_TO_COMPONENT_MAP = {
[REPORT_TYPES.value]: () => import('./value.vue'), [REPORT_TYPES.value]: () => import('./value.vue'),
[REPORT_TYPES.moduleLocation]: () => import('./module_location.vue'), [REPORT_TYPES.moduleLocation]: () => import('./module_location.vue'),
[REPORT_TYPES.fileLocation]: () => import('./file_location.vue'), [REPORT_TYPES.fileLocation]: () => import('./file_location.vue'),
[REPORT_TYPES.table]: () => import('./table.vue'),
}; };
export const getComponentNameForType = (reportType) => export const getComponentNameForType = (reportType) =>
......
<script>
import { GlTable } from '@gitlab/ui';
export default {
components: {
GlTable,
ReportItem: () => import('../report_item.vue'),
},
inheritAttrs: false,
props: {
header: {
type: Array,
required: true,
},
rows: {
type: Array,
required: true,
},
},
};
</script>
<template>
<gl-table
:fields="header"
:items="rows"
bordered
borderless
thead-class="gl-border-t-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"
>
<template #head()="data">
<report-item :item="data.field" />
</template>
<template #cell()="data">
<report-item :item="data.value" />
</template>
</gl-table>
</template>
...@@ -18,13 +18,21 @@ const isSupportedType = ({ type }) => Object.values(REPORT_TYPES).includes(type) ...@@ -18,13 +18,21 @@ const isSupportedType = ({ type }) => Object.values(REPORT_TYPES).includes(type)
const isOfType = (typeToCheck) => ({ type }) => type === typeToCheck; const isOfType = (typeToCheck) => ({ type }) => type === typeToCheck;
/** /**
* Check if the given report is of type list * Check if the given report is of type 'list'
* *
* @param {{ type: string } } reportItem * @param {{ type: string } } reportItem
* @returns boolean * @returns boolean
*/ */
export const isOfTypeList = isOfType(REPORT_TYPES.list); export const isOfTypeList = isOfType(REPORT_TYPES.list);
/**
* Check if the given report is of type 'table'
*
* @param {{ type: string } } reportItem
* @returns boolean
*/
const isOfTypeTable = isOfType(REPORT_TYPES.table);
/** /**
* Check if the given report is of type named-list * Check if the given report is of type named-list
* *
...@@ -122,6 +130,40 @@ const transformItemsIntoArray = (items) => { ...@@ -122,6 +130,40 @@ const transformItemsIntoArray = (items) => {
return Object.entries(items).map(([label, value]) => ({ ...value, label })); return Object.entries(items).map(([label, value]) => ({ ...value, label }));
}; };
/**
* Takes a report item's entry and transforms each item of type `table` in the following ways:
*
* 1. Adds a index-based key to each header-item (eg.: ` { key: column_0, ...headerData }`)
* 2. Transforms each item within the `rows` array into an object where each item's key corresponds
* to it's header's key
* (e.g: `rows: [
* [{ column_0: {...cellData }}]
* ]`)
*
* This prepares the data to be rendered into a table.
*
* @param [String, {*}] report entry
* @returns [String, {*}]
*/
const transformTableItems = ([label, item]) => {
const newItem = isOfTypeTable(item)
? {
...item,
header: item.header.map((headerItem, index) => ({
...headerItem,
key: `column_${index}`,
})),
rows: item.rows.map((row) => {
const getCellEntry = (cell, index) => [`column_${index}`, cell];
// transforms the array into an object with `column_N` as keys
return Object.fromEntries(row.map(getCellEntry));
}),
}
: item;
return [label, newItem];
};
/** /**
* Takes a vulnerabilities details object - containing generic report data * Takes a vulnerabilities details object - containing generic report data
* Returns a copy of the report data with the following items being filtered: * Returns a copy of the report data with the following items being filtered:
...@@ -143,6 +185,7 @@ export const filterTypesAndLimitListDepth = (data, { maxDepth = 5 } = {}) => { ...@@ -143,6 +185,7 @@ export const filterTypesAndLimitListDepth = (data, { maxDepth = 5 } = {}) => {
flow([ flow([
filterNestedListsItems(filterCriteria), filterNestedListsItems(filterCriteria),
overEveryNamedListItem(flow([filterTypesAndLimitListDepth, transformItemsIntoArray])), overEveryNamedListItem(flow([filterTypesAndLimitListDepth, transformItemsIntoArray])),
transformTableItems,
]), ]),
); );
......
...@@ -16,10 +16,15 @@ const TEST_DATA = { ...@@ -16,10 +16,15 @@ const TEST_DATA = {
type: REPORT_TYPES.url, type: REPORT_TYPES.url,
href: 'http://bar.com', href: 'http://bar.com',
}, },
three: {
type: REPORT_TYPES.table,
header: [],
rows: [],
},
}, },
unsupportedTypes: { unsupportedTypes: {
three: { four: {
name: 'three', name: 'four',
type: 'not-supported', type: 'not-supported',
}, },
}, },
...@@ -32,7 +37,7 @@ describe('ee/vulnerabilities/components/generic_report/report_section.vue', () = ...@@ -32,7 +37,7 @@ describe('ee/vulnerabilities/components/generic_report/report_section.vue', () =
extendedWrapper( extendedWrapper(
mount(ReportSection, { mount(ReportSection, {
propsData: { propsData: {
details: { ...TEST_DATA.supportedTypes }, details: { ...TEST_DATA.supportedTypes, ...TEST_DATA.unsupportedTypes },
}, },
...options, ...options,
}), }),
......
import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import ReportItem from 'ee/vulnerabilities/components/generic_report/report_item.vue';
import { REPORT_TYPES } from 'ee/vulnerabilities/components/generic_report/types/constants';
import Table from 'ee/vulnerabilities/components/generic_report/types/table.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const TEST_DATA = {
header: [{ key: 'column_1', type: REPORT_TYPES.text, value: 'foo ' }],
rows: [
{
column_1: { type: REPORT_TYPES.url, href: 'bar' },
},
],
};
describe('ee/vulnerabilities/components/generic_report/types/table.vue', () => {
let wrapper;
const createWrapper = () => {
return extendedWrapper(
mount(Table, {
propsData: {
...TEST_DATA,
},
stubs: {
'report-item': ReportItem,
},
}),
);
};
const findTable = () => wrapper.findComponent(GlTable);
const findTableHead = () => wrapper.find('thead');
const findTableBody = () => wrapper.find('tbody');
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a table', () => {
expect(findTable().exists()).toBe(true);
});
it('renders a table header containing the given report type', () => {
expect(findTableHead().findComponent(ReportItem).props('item')).toMatchObject(
TEST_DATA.header[0],
);
});
it('renders a table cell containing the given report type', () => {
expect(findTableBody().findComponent(ReportItem).props('item')).toMatchObject(
TEST_DATA.rows[0].column_1,
);
});
});
...@@ -49,6 +49,19 @@ const TEST_DATA = { ...@@ -49,6 +49,19 @@ const TEST_DATA = {
unsupported: { type: MOCK_REPORT_TYPE_UNSUPPORTED }, unsupported: { type: MOCK_REPORT_TYPE_UNSUPPORTED },
}, },
}, },
table: {
type: REPORT_TYPES.table,
header: [
{ type: REPORT_TYPES.text, value: 'foo ' },
{ type: REPORT_TYPES.text, value: 'bar ' },
],
rows: [
[
{ type: REPORT_TYPES.text, value: 'foo' },
{ type: REPORT_TYPES.text, value: 'bar' },
],
],
},
}; };
describe('ee/vulnerabilities/components/generic_report/types/utils', () => { describe('ee/vulnerabilities/components/generic_report/types/utils', () => {
...@@ -94,5 +107,20 @@ describe('ee/vulnerabilities/components/generic_report/types/utils', () => { ...@@ -94,5 +107,20 @@ describe('ee/vulnerabilities/components/generic_report/types/utils', () => {
]); ]);
}); });
}); });
describe('with tables', () => {
const filteredData = filterTypesAndLimitListDepth(TEST_DATA);
it('adds a key to each header item', () => {
expect(filteredData.table.header).toMatchObject([{ key: 'column_0' }, { key: 'column_1' }]);
});
it(`transforms the "rows" array into an object with it's keys corresponding to the header keys`, () => {
expect(filteredData.table.rows[0]).toMatchObject({
column_0: TEST_DATA.table.rows[0][0],
column_1: TEST_DATA.table.rows[0][1],
});
});
});
}); });
}); });
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