Commit d19a6f68 authored by Phil Hughes's avatar Phil Hughes

Merge branch...

Merge branch '7737-ci-pipeline-view-slowed-down-massivly-if-security-tabs-has-many-entries' into 'master'

Improve performance of rendering large reports

See merge request gitlab-org/gitlab-ce!22835
parents 7a726160 26ab92d3
<script> <script>
import IssuesBlock from '~/reports/components/report_issues.vue'; import ReportItem from '~/reports/components/report_item.vue';
import { STATUS_SUCCESS, STATUS_FAILED, STATUS_NEUTRAL } from '~/reports/constants'; import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
const wrapIssueWithState = (status, isNew = false) => issue => ({
status: issue.status || status,
isNew,
issue,
});
/** /**
* Renders block of issues * Renders block of issues
*/ */
export default { export default {
components: { components: {
IssuesBlock, SmartVirtualList,
ReportItem,
}, },
success: STATUS_SUCCESS, // Typical height of a report item in px
failed: STATUS_FAILED, typicalReportItemHeight: 32,
neutral: STATUS_NEUTRAL, /*
The maximum amount of shown issues. This is calculated by
( max-height of report-block-list / typicalReportItemHeight ) + some safety margin
We will use VirtualList if we have more items than this number.
For entries lower than this number, the virtual scroll list calculates the total height of the element wrongly.
*/
maxShownReportItems: 20,
props: { props: {
newIssues: { newIssues: {
type: Array, type: Array,
...@@ -40,42 +53,34 @@ export default { ...@@ -40,42 +53,34 @@ export default {
default: '', default: '',
}, },
}, },
computed: {
issuesWithState() {
return [
...this.newIssues.map(wrapIssueWithState(STATUS_FAILED, true)),
...this.unresolvedIssues.map(wrapIssueWithState(STATUS_FAILED)),
...this.neutralIssues.map(wrapIssueWithState(STATUS_NEUTRAL)),
...this.resolvedIssues.map(wrapIssueWithState(STATUS_SUCCESS)),
];
},
},
}; };
</script> </script>
<template> <template>
<div class="report-block-container"> <smart-virtual-list
:length="issuesWithState.length"
<issues-block :remain="$options.maxShownReportItems"
v-if="newIssues.length" :size="$options.typicalReportItemHeight"
:component="component" class="report-block-container"
:issues="newIssues" wtag="ul"
class="js-mr-code-new-issues" wclass="report-block-list"
status="failed" >
is-new <report-item
/> v-for="(wrapped, index) in issuesWithState"
:key="index"
<issues-block :issue="wrapped.issue"
v-if="unresolvedIssues.length" :status="wrapped.status"
:component="component"
:issues="unresolvedIssues"
:status="$options.failed"
class="js-mr-code-new-issues"
/>
<issues-block
v-if="neutralIssues.length"
:component="component"
:issues="neutralIssues"
:status="$options.neutral"
class="js-mr-code-non-issues"
/>
<issues-block
v-if="resolvedIssues.length"
:component="component" :component="component"
:issues="resolvedIssues" :is-new="wrapped.isNew"
:status="$options.success"
class="js-mr-code-resolved-issues"
/> />
</div> </smart-virtual-list>
</template> </template>
...@@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue'; ...@@ -3,14 +3,14 @@ import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import { components, componentNames } from '~/reports/components/issue_body'; import { components, componentNames } from '~/reports/components/issue_body';
export default { export default {
name: 'ReportIssues', name: 'ReportItem',
components: { components: {
IssueStatusIcon, IssueStatusIcon,
...components, ...components,
}, },
props: { props: {
issues: { issue: {
type: Array, type: Object,
required: true, required: true,
}, },
component: { component: {
...@@ -33,27 +33,21 @@ export default { ...@@ -33,27 +33,21 @@ export default {
}; };
</script> </script>
<template> <template>
<div> <li
<ul class="report-block-list"> :class="{ 'is-dismissed': issue.isDismissed }"
<li class="report-block-list-issue"
v-for="(issue, index) in issues" >
:key="index" <issue-status-icon
:class="{ 'is-dismissed': issue.isDismissed }" :status="status"
class="report-block-list-issue" class="append-right-5"
> />
<issue-status-icon
:status="issue.status || status"
class="append-right-5"
/>
<component <component
:is="component" :is="component"
v-if="component" v-if="component"
:issue="issue" :issue="issue"
:status="issue.status || status" :status="status"
:is-new="isNew" :is-new="isNew"
/> />
</li> </li>
</ul>
</div>
</template> </template>
<script>
import VirtualList from 'vue-virtual-scroll-list';
export default {
name: 'SmartVirtualList',
components: { VirtualList },
props: {
size: { type: Number, required: true },
length: { type: Number, required: true },
remain: { type: Number, required: true },
rtag: { type: String, default: 'div' },
wtag: { type: String, default: 'div' },
wclass: { type: String, default: null },
},
};
</script>
<template>
<virtual-list
v-if="length > remain"
v-bind="$attrs"
:size="remain"
:remain="remain"
:rtag="rtag"
:wtag="wtag"
:wclass="wclass"
class="js-virtual-list"
>
<slot></slot>
</virtual-list>
<component
:is="rtag"
v-else
class="js-plain-element"
>
<component
:is="wtag"
:class="wclass"
>
<slot></slot>
</component>
</component>
</template>
---
title: Improve performance of rendering large reports
merge_request: 22835
author:
type: performance
...@@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => { ...@@ -151,11 +151,11 @@ describe('Grouped Test Reports App', () => {
it('renders resolved failures', done => { it('renders resolved failures', done => {
setTimeout(() => { setTimeout(() => {
expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
resolvedFailures.suites[0].resolved_failures[0].name, resolvedFailures.suites[0].resolved_failures[0].name,
); );
expect(vm.$el.querySelector('.js-mr-code-resolved-issues').textContent).toContain( expect(vm.$el.querySelector('.report-block-container').textContent).toContain(
resolvedFailures.suites[0].resolved_failures[1].name, resolvedFailures.suites[0].resolved_failures[1].name,
); );
done(); done();
......
...@@ -120,7 +120,7 @@ describe('Report section', () => { ...@@ -120,7 +120,7 @@ describe('Report section', () => {
'Code quality improved on 1 point and degraded on 1 point', 'Code quality improved on 1 point and degraded on 1 point',
); );
expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual( expect(vm.$el.querySelectorAll('.report-block-container li').length).toEqual(
resolvedIssues.length, resolvedIssues.length,
); );
}); });
......
import Vue from 'vue';
import SmartVirtualScrollList from '~/vue_shared/components/smart_virtual_list.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Toggle Button', () => {
let vm;
const createComponent = ({ length, remain }) => {
const smartListProperties = {
rtag: 'section',
wtag: 'ul',
wclass: 'test-class',
// Size in pixels does not matter for our tests here
size: 35,
length,
remain,
};
const Component = Vue.extend({
components: {
SmartVirtualScrollList,
},
smartListProperties,
items: Array(length).fill(1),
template: `
<smart-virtual-scroll-list v-bind="$options.smartListProperties">
<li v-for="(val, key) in $options.items" :key="key">{{ key + 1 }}</li>
</smart-virtual-scroll-list>`,
});
return mountComponent(Component);
};
afterEach(() => {
vm.$destroy();
});
describe('if the list is shorter than the maximum shown elements', () => {
const listLength = 10;
beforeEach(() => {
vm = createComponent({ length: listLength, remain: 20 });
});
it('renders without the vue-virtual-scroll-list component', () => {
expect(vm.$el.classList).not.toContain('js-virtual-list');
expect(vm.$el.classList).toContain('js-plain-element');
});
it('renders list with provided tags and classes for the wrapper elements', () => {
expect(vm.$el.tagName).toEqual('SECTION');
expect(vm.$el.firstChild.tagName).toEqual('UL');
expect(vm.$el.firstChild.classList).toContain('test-class');
});
it('renders all children list elements', () => {
expect(vm.$el.querySelectorAll('li').length).toEqual(listLength);
});
});
describe('if the list is longer than the maximum shown elements', () => {
const maxItemsShown = 20;
beforeEach(() => {
vm = createComponent({ length: 1000, remain: maxItemsShown });
});
it('uses the vue-virtual-scroll-list component', () => {
expect(vm.$el.classList).toContain('js-virtual-list');
expect(vm.$el.classList).not.toContain('js-plain-element');
});
it('renders list with provided tags and classes for the wrapper elements', () => {
expect(vm.$el.tagName).toEqual('SECTION');
expect(vm.$el.firstChild.tagName).toEqual('UL');
expect(vm.$el.firstChild.classList).toContain('test-class');
});
it('renders at max twice the maximum shown elements', () => {
expect(vm.$el.querySelectorAll('li').length).toBeLessThanOrEqual(2 * maxItemsShown);
});
});
});
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