Commit 4f9714c1 authored by Brandon Labuschagne's avatar Brandon Labuschagne

Merge branch 'mw-metric-tile' into 'master'

Add metric tile component

See merge request gitlab-org/gitlab!79153
parents 8257fde2 38b7f1ea
<script>
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { redirectTo } from '~/lib/utils/url_utility';
import MetricPopover from './metric_popover.vue';
export default {
name: 'MetricTile',
components: {
GlSingleStat,
MetricPopover,
},
props: {
metric: {
type: Object,
required: true,
},
},
computed: {
decimalPlaces() {
const parsedFloat = parseFloat(this.metric.value);
return Number.isNaN(parsedFloat) || Number.isInteger(parsedFloat) ? 0 : 1;
},
hasLinks() {
return this.metric.links?.length && this.metric.links[0].url;
},
},
methods: {
clickHandler({ links }) {
if (this.hasLinks) {
redirectTo(links[0].url);
}
},
},
};
</script>
<template>
<div v-bind="$attrs">
<gl-single-stat
:id="metric.identifier"
:value="`${metric.value}`"
:title="metric.label"
:unit="metric.unit || ''"
:should-animate="true"
:animation-decimal-places="decimalPlaces"
:class="{ 'gl-hover-cursor-pointer': hasLinks }"
tabindex="0"
@click="clickHandler(metric)"
/>
<metric-popover :metric="metric" :target="metric.identifier" />
</div>
</template>
<script>
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { flatten } from 'lodash';
import createFlash from '~/flash';
import { sprintf, s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
import { METRICS_POPOVER_CONTENT } from '../constants';
import { removeFlash, prepareTimeMetricsData } from '../utils';
import MetricPopover from './metric_popover.vue';
import MetricTile from './metric_tile.vue';
const requestData = ({ request, endpoint, path, params, name }) => {
return request({ endpoint, params, requestPath: path })
......@@ -33,9 +32,8 @@ const fetchMetricsData = (reqs = [], path, params) => {
export default {
name: 'ValueStreamMetrics',
components: {
GlSingleStat,
GlSkeletonLoading,
MetricPopover,
MetricTile,
},
props: {
requestPath: {
......@@ -94,26 +92,14 @@ export default {
};
</script>
<template>
<div class="gl-display-flex gl-flex-wrap" data-testid="vsa-time-metrics">
<div class="gl-display-flex gl-flex-wrap" data-testid="vsa-metrics">
<gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3 gl-pr-9 gl-my-6" />
<div
<metric-tile
v-for="metric in metrics"
v-show="!isLoading"
:key="metric.identifier"
:metric="metric"
class="gl-my-6 gl-pr-9"
>
<gl-single-stat
:id="metric.identifier"
:value="`${metric.value}`"
:title="metric.label"
:unit="metric.unit || ''"
:should-animate="true"
:animation-decimal-places="getDecimalPlaces(metric.value)"
:class="{ 'gl-hover-cursor-pointer': hasLinks(metric.links) }"
tabindex="0"
@click="clickHandler(metric)"
/>
<metric-popover :metric="metric" :target="metric.identifier" />
</div>
</div>
</template>
......@@ -19,7 +19,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
path_nav_selector = '[data-testid="vsa-path-navigation"]'
filter_bar_selector = '[data-testid="vsa-filter-bar"]'
card_metric_selector = '[data-testid="vsa-time-metrics"] .gl-single-stat'
card_metric_selector = '[data-testid="vsa-metrics"] .gl-single-stat'
new_issues_count = 3
new_issues_count.times do |i|
......
......@@ -11,7 +11,7 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:stage_table_event_title_selector) { '[data-testid="vsa-stage-event-title"]' }
let_it_be(:stage_table_pagination_selector) { '[data-testid="vsa-stage-pagination"]' }
let_it_be(:stage_table_duration_column_header_selector) { '[data-testid="vsa-stage-header-duration"]' }
let_it_be(:metrics_selector) { "[data-testid='vsa-time-metrics']" }
let_it_be(:metrics_selector) { "[data-testid='vsa-metrics']" }
let_it_be(:metric_value_selector) { "[data-testid='displayValue']" }
let(:stage_table) { find(stage_table_selector) }
......
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import MetricTile from '~/cycle_analytics/components/metric_tile.vue';
import MetricPopover from '~/cycle_analytics/components/metric_popover.vue';
import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
describe('MetricTile', () => {
let wrapper;
const createComponent = (props = {}) => {
return shallowMount(MetricTile, {
propsData: {
metric: {},
...props,
},
});
};
const findSingleStat = () => wrapper.findComponent(GlSingleStat);
const findPopover = () => wrapper.findComponent(MetricPopover);
afterEach(() => {
wrapper.destroy();
});
describe('template', () => {
describe('links', () => {
it('when the metric has links, it redirects the user on click', () => {
const metric = {
identifier: 'deploys',
value: '10',
label: 'Deploys',
links: [{ url: 'foo/bar' }],
};
wrapper = createComponent({ metric });
const singleStat = findSingleStat();
singleStat.vm.$emit('click');
expect(redirectTo).toHaveBeenCalledWith('foo/bar');
});
it("when the metric doesn't have links, it won't the user on click", () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
const singleStat = findSingleStat();
singleStat.vm.$emit('click');
expect(redirectTo).not.toHaveBeenCalled();
});
});
describe('decimal places', () => {
it(`will render 0 decimal places for an integer value`, () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
const singleStat = findSingleStat();
expect(singleStat.props('animationDecimalPlaces')).toBe(0);
});
it(`will render 1 decimal place for a float value`, () => {
const metric = { identifier: 'deploys', value: '10.5', label: 'Deploys' };
wrapper = createComponent({ metric });
const singleStat = findSingleStat();
expect(singleStat.props('animationDecimalPlaces')).toBe(1);
});
});
it('renders a metric popover', () => {
const metric = { identifier: 'deploys', value: '10', label: 'Deploys' };
wrapper = createComponent({ metric });
const popover = findPopover();
expect(popover.exists()).toBe(true);
expect(popover.props()).toMatchObject({ metric, target: metric.identifier });
});
});
});
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import metricsData from 'test_fixtures/projects/analytics/value_stream_analytics/summary.json';
import waitForPromises from 'helpers/wait_for_promises';
import { METRIC_TYPE_SUMMARY } from '~/api/analytics_api';
import ValueStreamMetrics from '~/cycle_analytics/components/value_stream_metrics.vue';
import MetricTile from '~/cycle_analytics/components/metric_tile.vue';
import createFlash from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
import { group } from './mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
describe('ValueStreamMetrics', () => {
let wrapper;
......@@ -35,7 +33,7 @@ describe('ValueStreamMetrics', () => {
});
};
const findMetrics = () => wrapper.findAllComponents(GlSingleStat);
const findMetrics = () => wrapper.findAllComponents(MetricTile);
const expectToHaveRequest = (fields) => {
expect(mockGetValueStreamSummaryMetrics).toHaveBeenCalledWith({
......@@ -61,7 +59,7 @@ describe('ValueStreamMetrics', () => {
expect(wrapper.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
it('renders hidden GlSingleStat components for each metric', async () => {
it('renders hidden MetricTile components for each metric', async () => {
await waitForPromises();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
......@@ -89,34 +87,17 @@ describe('ValueStreamMetrics', () => {
});
describe.each`
index | value | title | unit | animationDecimalPlaces | clickable
${0} | ${metricsData[0].value} | ${metricsData[0].title} | ${metricsData[0].unit} | ${0} | ${false}
${1} | ${metricsData[1].value} | ${metricsData[1].title} | ${metricsData[1].unit} | ${0} | ${false}
${2} | ${metricsData[2].value} | ${metricsData[2].title} | ${metricsData[2].unit} | ${0} | ${false}
${3} | ${metricsData[3].value} | ${metricsData[3].title} | ${metricsData[3].unit} | ${1} | ${true}
`('metric tiles', ({ index, value, title, unit, animationDecimalPlaces, clickable }) => {
it(`renders a single stat component for "${title}" with value and unit`, () => {
index | identifier | value | label
${0} | ${metricsData[0].identifier} | ${metricsData[0].value} | ${metricsData[0].title}
${1} | ${metricsData[1].identifier} | ${metricsData[1].value} | ${metricsData[1].title}
${2} | ${metricsData[2].identifier} | ${metricsData[2].value} | ${metricsData[2].title}
${3} | ${metricsData[3].identifier} | ${metricsData[3].value} | ${metricsData[3].title}
`('metric tiles', ({ identifier, index, value, label }) => {
it(`renders a metric tile component for "${label}"`, () => {
const metric = findMetrics().at(index);
expect(metric.props()).toMatchObject({ value, title, unit: unit ?? '' });
expect(metric.props('metric')).toMatchObject({ identifier, value, label });
expect(metric.isVisible()).toBe(true);
});
it(`${
clickable ? 'redirects' : "doesn't redirect"
} when the user clicks the "${title}" metric`, () => {
const metric = findMetrics().at(index);
metric.vm.$emit('click');
if (clickable) {
expect(redirectTo).toHaveBeenCalledWith(metricsData[index].links[0].url);
} else {
expect(redirectTo).not.toHaveBeenCalled();
}
});
it(`will render ${animationDecimalPlaces} decimal places for the ${title} metric with the value "${value}"`, () => {
const metric = findMetrics().at(index);
expect(metric.props('animationDecimalPlaces')).toBe(animationDecimalPlaces);
});
});
it('will not display a loading icon', () => {
......
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