Commit cdfddc41 authored by Enrique Alcántara's avatar Enrique Alcántara

Merge branch...

Merge branch '324891-fe-generic-report-schema-render-named-list-list-url-types-on-vulnerability-details-page' into 'master'

Add recursive list component to generic reports section on vulnerability details page

See merge request gitlab-org/gitlab!58206
parents 503fa3b5 33051a37
<script>
import List from './types/list.vue';
import Url from './types/url.vue';
export default {
components: {
List,
Url,
},
props: {
......
......@@ -3,7 +3,9 @@ import { GlCollapse, GlIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import ReportItem from './report_item.vue';
import ReportRow from './report_row.vue';
import { isValidReportType } from './types/utils';
import { filterTypesAndLimitListDepth } from './types/utils';
const NESTED_LISTS_MAX_DEPTH = 4;
export default {
i18n: {
......@@ -27,8 +29,13 @@ export default {
};
},
computed: {
filteredDetails() {
return filterTypesAndLimitListDepth(this.details, {
maxDepth: NESTED_LISTS_MAX_DEPTH,
});
},
detailsEntries() {
return Object.entries(this.details).filter(([, item]) => isValidReportType(item.type));
return Object.entries(this.filteredDetails);
},
hasDetails() {
return this.detailsEntries.length > 0;
......
export const REPORT_TYPE_LIST = 'list';
export const REPORT_TYPE_URL = 'url';
export const REPORT_TYPES = [REPORT_TYPE_URL];
export const REPORT_TYPES = [REPORT_TYPE_LIST, REPORT_TYPE_URL];
<script>
import { isListType } from './utils';
export default {
isListType,
components: {
ReportItem: () => import('../report_item.vue'),
},
inheritAttrs: false,
props: {
items: {
type: Array,
required: true,
},
},
computed: {
hasNestedListItems() {
return this.items.some(isListType);
},
},
};
</script>
<template>
<ul class="generic-report-list" :class="{ 'generic-report-list-nested': hasNestedListItems }">
<li
v-for="item in items"
:key="item.name"
:class="{ 'gl-list-style-none!': $options.isListType(item) }"
>
<report-item :item="item" />
</li>
</ul>
</template>
......@@ -5,6 +5,7 @@ export default {
components: {
GlLink,
},
inheritAttrs: false,
props: {
href: {
type: String,
......
import { REPORT_TYPES } from './constants';
import { overEvery } from 'lodash';
import { REPORT_TYPES, REPORT_TYPE_LIST } from './constants';
/**
* Check if a given type is supported (i.e, is mapped to a component and can be rendered)
* Check if the given report is of a type that can be rendered (i.e, is mapped to a component and can be rendered)
*
* @param string type
* @param {{ type: string }} reportItem
* @returns boolean
*/
export const isValidReportType = (type) => REPORT_TYPES.includes(type);
const isSupportedType = ({ type }) => REPORT_TYPES.includes(type);
/**
* Check if the given report is of type list
*
* @param {{ type: string } } reportItem
* @returns boolean
*/
export const isListType = ({ type }) => type === REPORT_TYPE_LIST;
/**
* Check if the current report item is of that list and is not nested deeper than the maximum depth
*
* @param {number} maxDepth
* @returns {function}
*/
const isNotListTypeDeeperThan = (maxDepth) => (item, currentDepth) => {
return !isListType(item) || maxDepth > currentDepth + 1;
};
/**
* Takes an array of report items and recursively filters out items not matching the given condition
*
* @param {array} items
* @param {{condition: function, currentDepth? : number }} options
* @returns {array}
*/
const deepFilterListItems = (items, { condition, currentDepth = 0 }) =>
items.reduce((filteredItems, currentItem) => {
const shouldInsertItem = condition(currentItem, currentDepth);
if (!shouldInsertItem) {
return filteredItems;
}
const nextItem = { ...currentItem };
if (isListType(nextItem)) {
nextItem.items = deepFilterListItems(currentItem.items, {
condition,
currentDepth: currentDepth + 1,
});
}
return [...filteredItems, nextItem];
}, []);
/**
* If the given entry is a list it will deep filter it's child items based on the given condition
*
* @param {function} condition
* @returns {{*}}
*/
const filterNestedListsItems = (condition) => ([label, reportItem]) => {
const filtered = isListType(reportItem)
? {
...reportItem,
items: deepFilterListItems(reportItem.items, { condition }),
}
: reportItem;
return [label, filtered];
};
/**
* Takes a vulnerabilities details object - containing generic report data
* Returns a copy of the report data with the following items being filtered:
*
* 1.) Report items which have a type that is not supported for rendering
* 2.) Nested list items, which are nested beyond the given maximum depth
*
* @param {object} entries
* @param {{ maxDepth?: number }} options
* @returns {object}
*/
export const filterTypesAndLimitListDepth = (data, { maxDepth = 5 } = {}) => {
const entries = Object.entries(data);
const filterCriteria = overEvery([isSupportedType, isNotListTypeDeeperThan(maxDepth)]);
const filteredEntries = entries
.filter(([, reportItem]) => isSupportedType(reportItem))
.map(filterNestedListsItems(filterCriteria));
return Object.fromEntries(filteredEntries);
};
......@@ -128,3 +128,16 @@ $selection-summary-with-error-height: 118px;
@include gl-border-b-0;
}
}
.generic-report-list {
li {
@include gl-ml-0;
@include gl-list-style-none;
}
&.generic-report-list-nested li {
@include gl-ml-5;
list-style-type: disc;
}
}
---
title: Add recursive list rendering to generic vulnerability reports
merge_request: 58206
author:
type: added
......@@ -3,6 +3,7 @@ import ReportItem from 'ee/vulnerabilities/components/generic_report/report_item
import {
REPORT_TYPES,
REPORT_TYPE_URL,
REPORT_TYPE_LIST,
} from 'ee/vulnerabilities/components/generic_report/types/constants';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
......@@ -10,6 +11,9 @@ const TEST_DATA = {
[REPORT_TYPE_URL]: {
href: 'http://foo.com',
},
[REPORT_TYPE_LIST]: {
items: [{ type: 'foo' }],
},
};
describe('ee/vulnerabilities/components/generic_report/report_item.vue', () => {
......
import { screen } from '@testing-library/dom';
import { shallowMount } from '@vue/test-utils';
import ReportItem from 'ee/vulnerabilities/components/generic_report/report_item.vue';
import List from 'ee/vulnerabilities/components/generic_report/types/list.vue';
const TEST_DATA = {
items: [
{ type: 'url', href: 'http://foo.bar' },
{ type: 'url', href: 'http://bar.baz' },
],
};
describe('ee/vulnerabilities/components/generic_report/types/list.vue', () => {
let wrapper;
const createWrapper = () => {
return shallowMount(List, {
propsData: {
items: TEST_DATA.items,
},
attachTo: document.body,
});
};
const findReportItems = () => wrapper.findAllComponents(ReportItem);
beforeEach(() => {
wrapper = createWrapper();
});
it('renders a list', () => {
expect(screen.getByRole('list')).toBeInstanceOf(HTMLElement);
});
it('renders a report-item for each item', () => {
expect(findReportItems()).toHaveLength(TEST_DATA.items.length);
});
});
import { REPORT_TYPES } from 'ee/vulnerabilities/components/generic_report/types/constants';
import { isValidReportType } from 'ee/vulnerabilities/components/generic_report/types/utils';
import {
REPORT_TYPE_LIST,
REPORT_TYPE_URL,
} from 'ee/vulnerabilities/components/generic_report/types/constants';
import { filterTypesAndLimitListDepth } from 'ee/vulnerabilities/components/generic_report/types/utils';
const MOCK_REPORT_TYPE_UNSUPPORTED = 'MOCK_REPORT_TYPE_UNSUPPORTED';
const TEST_DATA = {
url: {
type: REPORT_TYPE_URL,
name: 'url1',
},
list: {
type: REPORT_TYPE_LIST,
name: 'rootList',
items: [
{ type: REPORT_TYPE_URL, name: 'url2' },
{
type: REPORT_TYPE_LIST,
name: 'listDepthOne',
items: [
{ type: REPORT_TYPE_URL, name: 'url3' },
{
type: REPORT_TYPE_LIST,
name: 'listDepthTwo',
items: [
{ type: REPORT_TYPE_URL, name: 'url4' },
{
type: REPORT_TYPE_LIST,
name: 'listDepthThree',
items: [
{ type: REPORT_TYPE_URL, name: 'url5' },
{ type: MOCK_REPORT_TYPE_UNSUPPORTED },
],
},
{ type: MOCK_REPORT_TYPE_UNSUPPORTED },
],
},
{ type: MOCK_REPORT_TYPE_UNSUPPORTED },
],
},
{ type: MOCK_REPORT_TYPE_UNSUPPORTED },
],
},
};
describe('ee/vulnerabilities/components/generic_report/types/utils', () => {
describe('isValidReportType', () => {
it.each(REPORT_TYPES)('returns "true" if the given type is a "%s"', (reportType) => {
expect(isValidReportType(reportType)).toBe(true);
});
describe('filterTypesAndLimitListDepth', () => {
const getRootList = (reportsData) => reportsData.list;
const getListWithDepthOne = (reportsData) => reportsData.list.items[1];
const getListWithDepthTwo = (reportsData) => reportsData.list.items[1].items[1];
const includesType = (type) => (items) =>
items.find(({ type: currentType }) => currentType === type) !== undefined;
const includesListItem = includesType(REPORT_TYPE_LIST);
const includesUnsupportedType = includesType(MOCK_REPORT_TYPE_UNSUPPORTED);
describe.each`
depth | getListAtCurrentDepth
${1} | ${getRootList}
${2} | ${getListWithDepthOne}
${3} | ${getListWithDepthTwo}
`('with nested lists at depth: "$depth"', ({ depth, getListAtCurrentDepth }) => {
const filteredData = filterTypesAndLimitListDepth(TEST_DATA, { maxDepth: depth });
it('filters list items', () => {
expect(includesListItem(getListAtCurrentDepth(TEST_DATA).items)).toBe(true);
expect(includesListItem(getListAtCurrentDepth(filteredData).items)).toBe(false);
});
it('returns "false" if the given type is not supported', () => {
expect(isValidReportType('this-type-does-not-exist')).toBe(false);
it('filters items with types that are not supported', () => {
expect(includesUnsupportedType(getListAtCurrentDepth(TEST_DATA).items)).toBe(true);
expect(includesUnsupportedType(getListAtCurrentDepth(filteredData).items)).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