Commit fa4e0d5d authored by David Pisek's avatar David Pisek Committed by Savas Vedova

Add named-list type go generic reports

parent d9468939
......@@ -4,12 +4,14 @@ export const REPORT_TYPES = {
list: 'list',
url: 'url',
diff: 'diff',
namedList: 'named-list',
};
const REPORT_TYPE_TO_COMPONENT_MAP = {
[REPORT_TYPES.list]: () => import('./list.vue'),
[REPORT_TYPES.url]: () => import('./url.vue'),
[REPORT_TYPES.diff]: () => import('./diff.vue'),
[REPORT_TYPES.namedList]: () => import('./named_list.vue'),
};
export const getComponentNameForType = (reportType) =>
......
<script>
import { isListType } from './utils';
import { isOfTypeList } from './utils';
export default {
isListType,
isOfTypeList,
components: {
ReportItem: () => import('../report_item.vue'),
},
......@@ -15,7 +15,7 @@ export default {
},
computed: {
hasNestedListItems() {
return this.items.some(isListType);
return this.items.some(isOfTypeList);
},
},
};
......@@ -25,7 +25,7 @@ export default {
<li
v-for="item in items"
:key="item.name"
:class="{ 'gl-list-style-none!': $options.isListType(item) }"
:class="{ 'gl-list-style-none!': $options.isOfTypeList(item) }"
>
<report-item :item="item" data-testid="reportItem" />
</li>
......
<script>
export default {
components: {
ReportItem: () => import('../report_item.vue'),
},
inheritAttrs: false,
props: {
items: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
hasItems() {
return this.items.length > 0;
},
},
};
</script>
<template>
<div class="gl-display-table">
<ul v-if="hasItems" class="generic-report-named-list">
<li
v-for="{ label, ...values } in items"
:key="label"
class="generic-report-named-list-item"
data-testid="listItem"
>
<strong class="generic-report-named-list-label">{{ label }}</strong>
<span class="generic-report-named-list-value">
<report-item :item="values" :data-testid="`listValue${label}`" />
</span>
</li>
</ul>
</div>
</template>
import { overEvery } from 'lodash';
import { overEvery, flow } from 'lodash';
import { REPORT_TYPES } from './constants';
/**
......@@ -9,13 +9,29 @@ import { REPORT_TYPES } from './constants';
*/
const isSupportedType = ({ type }) => Object.values(REPORT_TYPES).includes(type);
/**
* Higher order function that accepts a type and returns a function that returns true if the passed in report type matches
*
* @param {*} typeToCheck
* @returns
*/
const isOfType = (typeToCheck) => ({ type }) => type === typeToCheck;
/**
* Check if the given report is of type list
*
* @param {{ type: string } } reportItem
* @returns boolean
*/
export const isListType = ({ type }) => type === REPORT_TYPES.list;
export const isOfTypeList = isOfType(REPORT_TYPES.list);
/**
* Check if the given report is of type named-list
*
* @param {{ type: string } } reportItem
* @returns boolean
*/
export const isOfTypeNamedList = isOfType(REPORT_TYPES.namedList);
/**
* Check if the current report item is of that list and is not nested deeper than the maximum depth
......@@ -23,8 +39,8 @@ export const isListType = ({ type }) => type === REPORT_TYPES.list;
* @param {number} maxDepth
* @returns {function}
*/
const isNotListTypeDeeperThan = (maxDepth) => (item, currentDepth) => {
return !isListType(item) || maxDepth > currentDepth + 1;
const isNotOfTypeListDeeperThan = (maxDepth) => (item, currentDepth) => {
return !isOfTypeList(item) || maxDepth > currentDepth + 1;
};
/**
......@@ -44,7 +60,7 @@ const deepFilterListItems = (items, { condition, currentDepth = 0 }) =>
const nextItem = { ...currentItem };
if (isListType(nextItem)) {
if (isOfTypeList(nextItem)) {
nextItem.items = deepFilterListItems(currentItem.items, {
condition,
currentDepth: currentDepth + 1,
......@@ -61,7 +77,7 @@ const deepFilterListItems = (items, { condition, currentDepth = 0 }) =>
* @returns {{*}}
*/
const filterNestedListsItems = (condition) => ([label, reportItem]) => {
const filtered = isListType(reportItem)
const filtered = isOfTypeList(reportItem)
? {
...reportItem,
items: deepFilterListItems(reportItem.items, { condition }),
......@@ -71,6 +87,41 @@ const filterNestedListsItems = (condition) => ([label, reportItem]) => {
return [label, filtered];
};
/**
* Takes an entry from the vulnerability's details object and removes unsupported
* report types from `named-list` types
*
* @param {function} filterFn
* @param {number} maxDepth
* @returns
*/
const overEveryNamedListItem = (fn) => ([label, reportItem]) => {
const filtered = isOfTypeNamedList(reportItem)
? {
...reportItem,
items: fn(reportItem.items),
}
: reportItem;
return [label, filtered];
};
/**
* Takes an object of the shape
* {
* label1: { ... }
* label2: { ... }
* }
* and returns an array of the shape
* [{ label: 'label1', ... }, { label: 'label2', ...}]
*
* @param {*} items
* @returns
*/
const transformItemsIntoArray = (items) => {
return Object.entries(items).map(([label, value]) => ({ ...value, label }));
};
/**
* Takes a vulnerabilities details object - containing generic report data
* Returns a copy of the report data with the following items being filtered:
......@@ -84,11 +135,16 @@ const filterNestedListsItems = (condition) => ([label, reportItem]) => {
*/
export const filterTypesAndLimitListDepth = (data, { maxDepth = 5 } = {}) => {
const entries = Object.entries(data);
const filterCriteria = overEvery([isSupportedType, isNotListTypeDeeperThan(maxDepth)]);
const filterCriteria = overEvery([isSupportedType, isNotOfTypeListDeeperThan(maxDepth)]);
const filteredEntries = entries
.filter(([, reportItem]) => isSupportedType(reportItem))
.map(filterNestedListsItems(filterCriteria));
.map(
flow([
filterNestedListsItems(filterCriteria),
overEveryNamedListItem(flow([filterTypesAndLimitListDepth, transformItemsIntoArray])),
]),
);
return Object.fromEntries(filteredEntries);
};
......@@ -147,6 +147,8 @@ $selection-summary-with-error-height: 118px;
}
.generic-report-list {
margin-bottom: 0 !important;
li {
@include gl-ml-0;
@include gl-list-style-none;
......@@ -157,3 +159,44 @@ $selection-summary-with-error-height: 118px;
list-style-type: disc;
}
}
.generic-report-named-list {
@include gl-display-grid;
grid-template-columns: max-content auto;
margin: 0 !important;
> .generic-report-named-list-item {
display: contents;
}
}
.generic-report-named-list-item {
@include gl-list-style-none;
margin: 0 !important;
.generic-report-named-list-label,
.generic-report-named-list-value {
@include gl-m-0;
@include gl-display-flex;
@include gl-pr-5;
@include gl-py-3;
@include gl-border-b-1;
@include gl-border-b-solid;
@include gl-border-gray-100;
}
&:first-child {
> .generic-report-named-list-label,
> .generic-report-named-list-value {
@include gl-pt-0;
}
}
&:last-child {
> .generic-report-named-list-label,
> .generic-report-named-list-value {
@include gl-pb-0;
@include gl-border-b-0;
}
}
}
---
title: Add support for rendering "named-list" types on generic vulnerability reports
merge_request: 60370
author:
type: added
import { screen } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import { REPORT_TYPES } from 'ee/vulnerabilities/components/generic_report/types/constants';
import List from 'ee/vulnerabilities/components/generic_report/types/list.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const TEST_DATA = {
items: [
{ type: 'url', href: 'http://foo.bar' },
{ type: 'url', href: 'http://bar.baz' },
{ type: REPORT_TYPES.url, href: 'http://foo.bar' },
{ type: REPORT_TYPES.url, href: 'http://bar.baz' },
],
};
......
import { shallowMount } from '@vue/test-utils';
import { REPORT_TYPES } from 'ee/vulnerabilities/components/generic_report/types/constants';
import NamedList from 'ee/vulnerabilities/components/generic_report/types/named_list.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const TEST_DATA = {
items: [
{ label: 'url1', type: REPORT_TYPES.url, href: 'http://foo.bar' },
{ label: 'url2', type: REPORT_TYPES.url, href: 'http://bar.baz' },
],
};
describe('ee/vulnerabilities/components/generic_report/types/named_list.vue', () => {
let wrapper;
const createWrapper = () =>
extendedWrapper(
shallowMount(NamedList, {
propsData: {
...TEST_DATA,
},
// manual stubbing is needed because the component is dynamically imported
stubs: {
ReportItem: true,
},
}),
);
const findList = () => wrapper.findByRole('list');
const findAllListItems = () => wrapper.findAllByTestId('listItem');
const findItemValueWithLabel = (label) => wrapper.findByTestId(`listValue${label}`);
beforeEach(() => {
wrapper = createWrapper();
});
afterEach(() => {
wrapper.destroy();
});
it('renders a list element', () => {
expect(findList().exists()).toBe(true);
});
it('renders all list items', () => {
expect(findAllListItems()).toHaveLength(Object.values(TEST_DATA.items).length);
});
describe.each(TEST_DATA.items)('list item: %s', (item) => {
it(`renders the item's label`, () => {
expect(wrapper.findByText(item.label).exists()).toBe(true);
});
it('renders a report-item', () => {
expect(findItemValueWithLabel(item.label).exists()).toBe(true);
});
});
});
......@@ -40,6 +40,15 @@ const TEST_DATA = {
{ type: MOCK_REPORT_TYPE_UNSUPPORTED },
],
},
namedList: {
type: REPORT_TYPES.namedList,
name: 'rootNamedList',
items: {
url1: { type: REPORT_TYPES.url, name: 'foo' },
url2: { type: REPORT_TYPES.url, name: 'bar' },
unsupported: { type: MOCK_REPORT_TYPE_UNSUPPORTED },
},
},
};
describe('ee/vulnerabilities/components/generic_report/types/utils', () => {
......@@ -70,5 +79,20 @@ describe('ee/vulnerabilities/components/generic_report/types/utils', () => {
expect(includesUnsupportedType(getListAtCurrentDepth(filteredData).items)).toBe(false);
});
});
describe('with named lists', () => {
const filteredData = filterTypesAndLimitListDepth(TEST_DATA);
it('filters items with types that are not supported', () => {
expect(includesUnsupportedType(filteredData.namedList.items)).toBe(false);
});
it('transforms the items object into an array of report-items with labels', () => {
expect(filteredData.namedList.items).toEqual([
{ label: 'url1', type: REPORT_TYPES.url, name: 'foo' },
{ label: 'url2', type: REPORT_TYPES.url, name: 'bar' },
]);
});
});
});
});
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