Commit a87edf49 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '341739-display-information-about-data-aggregation' into 'master'

VSA display aggregated VSA metadata

See merge request gitlab-org/gitlab!82384
parents 51be5ed8 2d3ebd1a
...@@ -10,6 +10,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue'; ...@@ -10,6 +10,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
import { METRICS_REQUESTS } from '../constants'; import { METRICS_REQUESTS } from '../constants';
import DurationChart from './duration_chart.vue'; import DurationChart from './duration_chart.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue'; import TypeOfWorkCharts from './type_of_work_charts.vue';
import ValueStreamAggregationStatus from './value_stream_aggregation_status.vue';
import ValueStreamSelect from './value_stream_select.vue'; import ValueStreamSelect from './value_stream_select.vue';
export default { export default {
...@@ -20,6 +21,7 @@ export default { ...@@ -20,6 +21,7 @@ export default {
TypeOfWorkCharts, TypeOfWorkCharts,
StageTable, StageTable,
PathNavigation, PathNavigation,
ValueStreamAggregationStatus,
ValueStreamFilters, ValueStreamFilters,
ValueStreamMetrics, ValueStreamMetrics,
ValueStreamSelect, ValueStreamSelect,
...@@ -55,6 +57,7 @@ export default { ...@@ -55,6 +57,7 @@ export default {
'selectedStageError', 'selectedStageError',
'selectedValueStream', 'selectedValueStream',
'pagination', 'pagination',
'aggregation',
]), ]),
...mapGetters([ ...mapGetters([
'hasNoAccessError', 'hasNoAccessError',
...@@ -81,6 +84,9 @@ export default { ...@@ -81,6 +84,9 @@ export default {
hasDateRangeSet() { hasDateRangeSet() {
return this.createdAfter && this.createdBefore; return this.createdAfter && this.createdBefore;
}, },
isAggregationEnabled() {
return this.aggregation?.enabled;
},
query() { query() {
const { project_ids, created_after, created_before } = this.cycleAnalyticsRequestParams; const { project_ids, created_after, created_before } = this.cycleAnalyticsRequestParams;
const paginationUrlParams = !this.isOverviewStageSelected const paginationUrlParams = !this.isOverviewStageSelected
...@@ -140,6 +146,10 @@ export default { ...@@ -140,6 +146,10 @@ export default {
}, },
}, },
METRICS_REQUESTS, METRICS_REQUESTS,
aggregationPopoverOptions: {
triggers: 'hover',
placement: 'left',
},
}; };
</script> </script>
<template> <template>
...@@ -148,10 +158,10 @@ export default { ...@@ -148,10 +158,10 @@ export default {
class="gl-mb-3 gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between" class="gl-mb-3 gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between"
> >
<h3>{{ __('Value Stream Analytics') }}</h3> <h3>{{ __('Value Stream Analytics') }}</h3>
<value-stream-select <div class="gl-display-flex gl-flex-direction-row gl-align-items-center gl-mt-0 gl-sm-mt-5">
v-if="shouldDisplayCreateMultipleValueStreams" <value-stream-aggregation-status v-if="isAggregationEnabled" :data="aggregation" />
class="gl-align-self-start gl-sm-align-self-start gl-mt-0 gl-sm-mt-5" <value-stream-select v-if="shouldDisplayCreateMultipleValueStreams" />
/> </div>
</div> </div>
<gl-empty-state <gl-empty-state
v-if="shouldRenderEmptyState" v-if="shouldRenderEmptyState"
......
<script>
import dateFormat from 'dateformat';
import { GlBadge, GlPopover } from '@gitlab/ui';
import {
approximateDuration,
differenceInMilliseconds,
} from '~/lib/utils/datetime/date_calculation_utility';
import { __, sprintf } from '~/locale';
export const LAST_UPDATED_TEXT = __('Last updated');
export const LAST_UPDATED_AGO_TEXT = __('Last updated %{time} ago');
export const NEXT_UPDATE_TEXT = __('Next update');
export const POPOVER_TITLE = __('Data refresh');
export const toYmdhs = (date) => dateFormat(date, 'yyyy-mm-dd HH:MM');
export default {
name: 'ValueStreamAggregationStatus',
components: { GlBadge, GlPopover },
props: {
data: {
type: Object,
required: true,
},
},
computed: {
elapsedTimeParsedSeconds() {
return differenceInMilliseconds(this.lastUpdated, this.nextUpdate) / 1000;
},
elapsedTimeText() {
return sprintf(this.$options.i18n.LAST_UPDATED_AGO_TEXT, {
time: approximateDuration(this.elapsedTimeParsedSeconds),
});
},
lastUpdated() {
return Date.parse(this.data.lastRunAt);
},
nextUpdate() {
return Date.parse(this.data.nextRunAt);
},
formattedLastUpdated() {
return toYmdhs(this.lastUpdated);
},
formattedNextUpdate() {
return toYmdhs(this.nextUpdate);
},
},
i18n: {
LAST_UPDATED_AGO_TEXT,
LAST_UPDATED_TEXT,
NEXT_UPDATE_TEXT,
POPOVER_TITLE,
},
};
</script>
<template>
<div class="gl-mr-2 gl-text-align-center">
<gl-badge id="vsa-data-refresh" variant="neutral" icon="information-o">{{
elapsedTimeText
}}</gl-badge>
<gl-popover
v-bind="$options.aggregationPopoverOptions"
target="vsa-data-refresh"
:title="$options.i18n.POPOVER_TITLE"
:css-classes="['stage-item-popover']"
data-testid="vsa-data-refresh-popover"
>
<div class="gl-px-4">
<div
data-testid="vsa-data-refresh-last"
class="gl-display-flex gl-justify-content-space-between"
>
<div class="gl-pr-4 gl-pb-4">
{{ $options.i18n.LAST_UPDATED_TEXT }}
</div>
<div class="gl-pb-4 gl-font-weight-bold">
{{ formattedLastUpdated }}
</div>
</div>
<div
data-testid="vsa-data-refresh-next"
class="gl-display-flex gl-justify-content-space-between"
>
<div class="gl-pr-4 gl-pb-4">
{{ $options.i18n.NEXT_UPDATE_TEXT }}
</div>
<div class="gl-pb-4 gl-font-weight-bold">
{{ formattedNextUpdate }}
</div>
</div>
</div>
</gl-popover>
</div>
</template>
...@@ -109,6 +109,7 @@ export default { ...@@ -109,6 +109,7 @@ export default {
selectedValueStream = {}, selectedValueStream = {},
defaultStageConfig = [], defaultStageConfig = [],
pagination = {}, pagination = {},
aggregation = {},
} = {}, } = {},
) { ) {
state.isLoading = true; state.isLoading = true;
...@@ -119,6 +120,8 @@ export default { ...@@ -119,6 +120,8 @@ export default {
state.createdAfter = createdAfter; state.createdAfter = createdAfter;
state.defaultStageConfig = defaultStageConfig; state.defaultStageConfig = defaultStageConfig;
Vue.set(state, 'aggregation', aggregation);
Vue.set(state, 'pagination', { Vue.set(state, 'pagination', {
page: pagination.page ?? state.pagination.page, page: pagination.page ?? state.pagination.page,
sort: pagination.sort ?? state.pagination.sort, sort: pagination.sort ?? state.pagination.sort,
......
...@@ -46,4 +46,9 @@ export default () => ({ ...@@ -46,4 +46,9 @@ export default () => ({
direction: PAGINATION_SORT_DIRECTION_DESC, direction: PAGINATION_SORT_DIRECTION_DESC,
}, },
stageCounts: {}, stageCounts: {},
aggregation: {
enabled: false,
lastRunAt: null,
nextRunAt: null,
},
}); });
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
export const formattedDate = (d) => dateFormat(d, dateFormats.defaultDate); export const formattedDate = (d) => dateFormat(d, dateFormats.defaultDate);
...@@ -101,6 +101,9 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -101,6 +101,9 @@ export const buildCycleAnalyticsInitialData = ({
milestonesPath = '', milestonesPath = '',
defaultStages = null, defaultStages = null,
stage = null, stage = null,
aggregationEnabled = false,
aggregationLastRunAt = null,
aggregationNextRunAt = null,
} = {}) => ({ } = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream), selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId group: groupId
...@@ -128,4 +131,9 @@ export const buildCycleAnalyticsInitialData = ({ ...@@ -128,4 +131,9 @@ export const buildCycleAnalyticsInitialData = ({
})) }))
: [], : [],
stage: JSON.parse(stage), stage: JSON.parse(stage),
aggregation: {
enabled: parseBoolean(aggregationEnabled),
lastRunAt: aggregationLastRunAt,
nextRunAt: aggregationNextRunAt,
},
}); });
...@@ -8,6 +8,7 @@ import Component from 'ee/analytics/cycle_analytics/components/base.vue'; ...@@ -8,6 +8,7 @@ import Component from 'ee/analytics/cycle_analytics/components/base.vue';
import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue'; import DurationChart from 'ee/analytics/cycle_analytics/components/duration_chart.vue';
import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue'; import TypeOfWorkCharts from 'ee/analytics/cycle_analytics/components/type_of_work_charts.vue';
import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue'; import ValueStreamSelect from 'ee/analytics/cycle_analytics/components/value_stream_select.vue';
import ValueStreamAggregationStatus from 'ee/analytics/cycle_analytics/components/value_stream_aggregation_status.vue';
import createStore from 'ee/analytics/cycle_analytics/store'; import createStore from 'ee/analytics/cycle_analytics/store';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import { import {
...@@ -42,6 +43,7 @@ import { ...@@ -42,6 +43,7 @@ import {
issueEvents, issueEvents,
groupLabels, groupLabels,
tasksByTypeData, tasksByTypeData,
aggregationData,
} from '../mock_data'; } from '../mock_data';
const noDataSvgPath = 'path/to/no/data'; const noDataSvgPath = 'path/to/no/data';
...@@ -155,6 +157,7 @@ describe('EE Value Stream Analytics component', () => { ...@@ -155,6 +157,7 @@ describe('EE Value Stream Analytics component', () => {
return comp; return comp;
} }
const findAggregationStatus = () => wrapper.findComponent(ValueStreamAggregationStatus);
const findPathNavigation = () => wrapper.findComponent(PathNavigation); const findPathNavigation = () => wrapper.findComponent(PathNavigation);
const findStageTable = () => wrapper.findComponent(StageTable); const findStageTable = () => wrapper.findComponent(StageTable);
...@@ -330,6 +333,46 @@ describe('EE Value Stream Analytics component', () => { ...@@ -330,6 +333,46 @@ describe('EE Value Stream Analytics component', () => {
it('displays the duration chart', () => { it('displays the duration chart', () => {
displaysDurationChart(true); displaysDurationChart(true);
}); });
it('does not render the aggregation status', () => {
expect(findAggregationStatus().exists()).toBe(false);
});
});
});
describe('with aggregation data', () => {
beforeEach(async () => {
wrapper = await createComponent({
initialState: {
...initialCycleAnalyticsState,
aggregation: {
...aggregationData,
},
},
});
});
it('renders the aggregation status', () => {
expect(findAggregationStatus().exists()).toBe(true);
expect(findAggregationStatus().props('data')).toEqual(aggregationData);
});
describe('enabled=false', () => {
beforeEach(async () => {
wrapper = await createComponent({
initialState: {
...initialCycleAnalyticsState,
aggregation: {
...aggregationData,
enabled: false,
},
},
});
});
it('does not render the aggregation status', () => {
expect(findAggregationStatus().exists()).toBe(false);
});
}); });
}); });
......
import { shallowMount } from '@vue/test-utils';
import { GlBadge, GlPopover } from '@gitlab/ui';
import ValueStreamAggregationStatus, {
LAST_UPDATED_TEXT,
NEXT_UPDATE_TEXT,
POPOVER_TITLE,
toYmdhs,
} from 'ee/analytics/cycle_analytics/components/value_stream_aggregation_status.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { aggregationData } from '../mock_data';
const createComponent = (props = {}) =>
extendedWrapper(
shallowMount(ValueStreamAggregationStatus, {
propsData: {
data: aggregationData,
...props,
},
}),
);
describe('ValueStreamAggregationStatus', () => {
let wrapper = null;
const findBadge = () => wrapper.findComponent(GlBadge);
const findPopover = () => wrapper.findComponent(GlPopover);
const findLastUpdated = () => wrapper.findByTestId('vsa-data-refresh-last');
const findNextUpdate = () => wrapper.findByTestId('vsa-data-refresh-next');
afterEach(() => {
wrapper.destroy();
});
describe('default state', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('renders the elapsed time badge', () => {
expect(findBadge().exists()).toBe(true);
expect(findBadge().text()).toContain('Last updated about 1 hour ago');
});
it('renders the data refresh popover', () => {
expect(findPopover().exists()).toBe(true);
expect(findPopover().attributes('title')).toBe(POPOVER_TITLE);
});
it('renders the last updated date in the popover', () => {
const txt = findLastUpdated().text();
expect(txt).toContain(LAST_UPDATED_TEXT);
expect(txt).toContain(toYmdhs(aggregationData.lastRunAt));
});
it('renders the next update date in the popover', () => {
const txt = findNextUpdate().text();
expect(txt).toContain(NEXT_UPDATE_TEXT);
expect(txt).toContain(toYmdhs(aggregationData.nextRunAt));
});
});
});
...@@ -309,3 +309,9 @@ export const durationChartPlottableData = [ ...@@ -309,3 +309,9 @@ export const durationChartPlottableData = [
]; ];
export const pathNavIssueMetric = 172800; export const pathNavIssueMetric = 172800;
export const aggregationData = {
enabled: true,
lastRunAt: '2022-03-11T04:34:59Z',
nextRunAt: '2022-03-11T05:21:01Z',
};
...@@ -108,8 +108,8 @@ module Gitlab ...@@ -108,8 +108,8 @@ module Gitlab
aggregation = ::Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group) aggregation = ::Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
{ {
enabled: aggregation.enabled.to_s, enabled: aggregation.enabled.to_s,
last_run_at: aggregation.last_incremental_run_at, last_run_at: aggregation.last_incremental_run_at&.iso8601,
next_run_at: aggregation.estimated_next_run_at next_run_at: aggregation.estimated_next_run_at&.iso8601
} }
end end
......
...@@ -11448,6 +11448,9 @@ msgstr "" ...@@ -11448,6 +11448,9 @@ msgstr ""
msgid "Data is still calculating..." msgid "Data is still calculating..."
msgstr "" msgstr ""
msgid "Data refresh"
msgstr ""
msgid "Data type" msgid "Data type"
msgstr "" msgstr ""
...@@ -21580,6 +21583,9 @@ msgstr "" ...@@ -21580,6 +21583,9 @@ msgstr ""
msgid "Last updated" msgid "Last updated"
msgstr "" msgstr ""
msgid "Last updated %{time} ago"
msgstr ""
msgid "Last used" msgid "Last used"
msgstr "" msgstr ""
...@@ -24643,6 +24649,9 @@ msgstr "" ...@@ -24643,6 +24649,9 @@ msgstr ""
msgid "Next unresolved discussion" msgid "Next unresolved discussion"
msgstr "" msgstr ""
msgid "Next update"
msgstr ""
msgid "Nickname" msgid "Nickname"
msgstr "" msgstr ""
......
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