Commit c335a26b authored by Quang-Minh Nguyen's avatar Quang-Minh Nguyen

Allow to sort the details chronologically

Issue https://gitlab.com/gitlab-org/gitlab/-/issues/324649
parent 28f1bf87
<script>
import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { GlButton, GlModal, GlModalDirective, GlSegmentedControl } from '@gitlab/ui';
import { s__ } from '~/locale';
import RequestWarning from './request_warning.vue';
export const SortOrderDuration = 'SortOrderDuration';
export const SortOrderChronological = 'SortOrderChronological';
export default {
components: {
RequestWarning,
GlButton,
GlModal,
GlSegmentedControl,
},
directives: {
'gl-modal': GlModalDirective,
......@@ -39,6 +45,7 @@ export default {
data() {
return {
openedBacktraces: [],
sortOrder: SortOrderDuration,
};
},
computed: {
......@@ -60,12 +67,32 @@ export default {
return summary;
},
metricDetailsLabel() {
return this.metricDetails.duration
? `${this.metricDetails.duration} / ${this.metricDetails.calls}`
: this.metricDetails.calls;
if (this.metricDetails.duration && this.metricDetails.calls) {
return `${this.metricDetails.duration} / ${this.metricDetails.calls}`;
} else if (this.metricDetails.calls) {
return this.metricDetails.calls;
}
return '0';
},
displaySortOrder() {
return (
this.metricDetails.details.length !== 0 &&
this.metricDetails.details.every((item) => item.start)
);
},
detailsList() {
return this.metricDetails.details;
const list = this.metricDetails.details
.slice()
.map((item, index) => ({ ...item, id: index }));
if (this.sortOrder === SortOrderDuration) {
return list.sort((a, b) => (a.duration < b.duration ? 1 : -1));
} else if (this.sortOrder === SortOrderChronological) {
return list.sort((a, b) => (a.start < b.start ? -1 : 1));
}
return list;
},
warnings() {
return this.metricDetails.warnings || [];
......@@ -93,7 +120,14 @@ export default {
itemHasOpenedBacktrace(toggledIndex) {
return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0;
},
changeSortOrder(order) {
this.sortOrder = order;
},
},
sortOrders: [
{ value: SortOrderDuration, text: s__('PerformanceBar|Sort by duration') },
{ value: SortOrderChronological, text: s__('PerformanceBar|Sort chronologically') },
],
};
</script>
<template>
......@@ -104,7 +138,12 @@ export default {
data-qa-selector="detailed_metric_content"
>
<gl-button v-gl-modal="modalId" class="gl-mr-2" type="button" variant="link">
<span class="gl-text-blue-300 gl-font-weight-bold">{{ metricDetailsLabel }}</span>
<span
class="gl-text-blue-300 gl-font-weight-bold"
data-testid="performance-bar-details-label"
>
{{ metricDetailsLabel }}
</span>
</gl-button>
<gl-modal :modal-id="modalId" :title="header" size="lg" footer-class="d-none" scrollable>
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
......@@ -120,17 +159,24 @@ export default {
<div class="gl-font-size-h1 gl-font-weight-bold">{{ value }}</div>
</div>
</div>
<gl-segmented-control
v-if="displaySortOrder"
data-testid="performance-bar-sort-order"
:options="$options.sortOrders"
:checked="sortOrder"
@input="changeSortOrder"
/>
</div>
<hr />
<table class="table gl-table">
<template v-if="detailsList.length">
<tr v-for="(item, index) in detailsList" :key="index">
<td>
<tr v-for="item in detailsList" :key="item.id">
<td data-testid="performance-item-duration">
<span v-if="item.duration">{{
sprintf(__('%{duration}ms'), { duration: item.duration })
}}</span>
</td>
<td>
<td data-testid="performance-item-content">
<div>
<div
v-for="(key, keyIndex) in keys"
......@@ -147,12 +193,12 @@ export default {
variant="default"
icon="ellipsis_h"
size="small"
:selected="itemHasOpenedBacktrace(index)"
:selected="itemHasOpenedBacktrace(item.id)"
:aria-label="__('Toggle backtrace')"
@click="toggleBacktrace(index)"
@click="toggleBacktrace(item.id)"
/>
</div>
<pre v-if="itemHasOpenedBacktrace(index)" class="backtrace-row mt-2">{{
<pre v-if="itemHasOpenedBacktrace(item.id)" class="backtrace-row mt-2">{{
item.backtrace
}}</pre>
</div>
......
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import DetailedMetric, {
SortOrderDuration,
SortOrderChronological,
} from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
describe('detailedMetric', () => {
let wrapper;
const createComponent = (props) => {
wrapper = shallowMount(DetailedMetric, {
propsData: {
...props,
},
});
wrapper = extendedWrapper(
shallowMount(DetailedMetric, {
propsData: {
...props,
},
}),
);
};
const findAllTraceBlocks = () => wrapper.findAll('pre');
const findTraceBlockAtIndex = (index) => findAllTraceBlocks().at(index);
const findExpandBacktraceBtns = () => wrapper.findAll('[data-testid="backtrace-expand-btn"]');
const findExpandedBacktraceBtnAtIndex = (index) => findExpandBacktraceBtns().at(index);
const findDetailsLabel = () => wrapper.findByTestId('performance-bar-details-label');
const findSortOrderSwitcher = () => wrapper.findByTestId('performance-bar-sort-order');
const findAllDetailDurations = () =>
wrapper.findAll('[data-testid="performance-item-duration"]').wrappers.map((w) => w.text());
const findAllSummaryItems = () =>
wrapper.findAll('[data-testid="performance-bar-summary-item"]').wrappers.map((w) => w.text());
afterEach(() => {
wrapper.destroy();
......@@ -42,25 +54,61 @@ describe('detailedMetric', () => {
describe('when the current request has details', () => {
const requestDetails = [
{ duration: '100', feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
{
duration: '23',
duration: 23,
feature: 'rebase_in_progress',
request: '',
backtrace: ['other', 'example'],
},
{ duration: 100, feature: 'find_commit', request: 'abcdef', backtrace: ['hello', 'world'] },
];
describe('with a default metric name', () => {
describe('with an empty detail', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '0ms',
calls: 0,
details: [],
warnings: [],
},
},
},
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
it('displays an empty title', () => {
expect(findDetailsLabel().text()).toContain('0');
});
it('displays an empty modal', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain('No gitaly calls for this request');
});
it('does not display sort by switcher', () => {
expect(findSortOrderSwitcher().exists()).toBe(false);
});
});
describe('when the details have a summary field', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: '456',
calls: 456,
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
summary: {
'In controllers': 100,
'In middlewares': 20,
},
},
},
},
......@@ -70,8 +118,53 @@ describe('detailedMetric', () => {
});
});
it('displays details', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain('123ms / 456');
it('displays a summary section', () => {
expect(findAllSummaryItems()).toEqual([
'Total 456',
'Total duration 123ms',
'In controllers 100',
'In middlewares 20',
]);
});
});
describe("when the details don't have a start field", () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: 456,
details: requestDetails,
warnings: ['gitaly calls: 456 over 30'],
},
},
},
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
it('displays details header', () => {
expect(findDetailsLabel().text()).toContain('123ms / 456');
});
it('displays a basic summary section', () => {
expect(findAllSummaryItems()).toEqual(['Total 456', 'Total duration 123ms']);
});
it('sorts the details by descending duration order', () => {
expect(
wrapper
.findAll('[data-testid="performance-item-duration"]')
.wrappers.map((w) => w.text()),
).toEqual(['100ms', '23ms']);
});
it('does not display sort by switcher', () => {
expect(findSortOrderSwitcher().exists()).toBe(false);
});
it('adds a modal with a table of the details', () => {
......@@ -119,17 +212,75 @@ describe('detailedMetric', () => {
findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
await nextTick();
expect(findAllTraceBlocks()).toHaveLength(1);
expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]);
expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]);
secondExpandButton.vm.$emit('click');
await nextTick();
expect(findAllTraceBlocks()).toHaveLength(2);
expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[1].backtrace[0]);
expect(findTraceBlockAtIndex(1).text()).toContain(requestDetails[0].backtrace[0]);
secondExpandButton.vm.$emit('click');
await nextTick();
expect(findAllTraceBlocks()).toHaveLength(1);
expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[0].backtrace[0]);
expect(findTraceBlockAtIndex(0).text()).toContain(requestDetails[1].backtrace[0]);
});
});
describe('when the details have a start field', () => {
const requestDetailsWithStart = [
{
start: '2021-03-18 11:41:49.846356 +0700',
duration: 23,
feature: 'rebase_in_progress',
request: '',
},
{
start: '2021-03-18 11:42:11.645711 +0700',
duration: 75,
feature: 'find_commit',
request: 'abcdef',
},
{
start: '2021-03-18 11:42:10.645711 +0700',
duration: 100,
feature: 'find_commit',
request: 'abcdef',
},
];
beforeEach(() => {
createComponent({
currentRequest: {
details: {
gitaly: {
duration: '123ms',
calls: 456,
details: requestDetailsWithStart,
warnings: ['gitaly calls: 456 over 30'],
},
},
},
metric: 'gitaly',
header: 'Gitaly calls',
keys: ['feature', 'request'],
});
});
it('sorts the details by descending duration order', () => {
expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
});
it('displays sort by switcher', () => {
expect(findSortOrderSwitcher().exists()).toBe(true);
});
it('allows switch sorting orders', async () => {
findSortOrderSwitcher().vm.$emit('input', SortOrderChronological);
await nextTick();
expect(findAllDetailDurations()).toEqual(['23ms', '100ms', '75ms']);
findSortOrderSwitcher().vm.$emit('input', SortOrderDuration);
await nextTick();
expect(findAllDetailDurations()).toEqual(['100ms', '75ms', '23ms']);
});
});
......@@ -156,31 +307,39 @@ describe('detailedMetric', () => {
expect(wrapper.text()).toContain('custom');
});
});
});
describe('when the details has no duration', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
bullet: {
calls: '456',
details: [{ notification: 'notification', backtrace: 'backtrace' }],
describe('when the details has no duration', () => {
beforeEach(() => {
createComponent({
currentRequest: {
details: {
bullet: {
calls: '456',
details: [{ notification: 'notification', backtrace: 'backtrace' }],
},
},
},
},
metric: 'bullet',
header: 'Bullet notifications',
keys: ['notification'],
metric: 'bullet',
header: 'Bullet notifications',
keys: ['notification'],
});
});
it('displays calls in the label', () => {
expect(findDetailsLabel().text()).toContain('456');
});
it('displays a basic summary section', () => {
expect(findAllSummaryItems()).toEqual(['Total 456']);
});
});
it('renders only the number of calls', async () => {
expect(trimText(wrapper.text())).toEqual('456 notification bullet');
it('renders only the number of calls', async () => {
expect(trimText(wrapper.text())).toContain('notification bullet');
findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
await nextTick();
expect(trimText(wrapper.text())).toEqual('456 notification backtrace bullet');
findExpandedBacktraceBtnAtIndex(0).vm.$emit('click');
await nextTick();
expect(trimText(wrapper.text())).toContain('notification backtrace bullet');
});
});
});
});
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