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';
import { METRICS_REQUESTS } from '../constants';
import DurationChart from './duration_chart.vue';
import TypeOfWorkCharts from './type_of_work_charts.vue';
import ValueStreamAggregationStatus from './value_stream_aggregation_status.vue';
import ValueStreamSelect from './value_stream_select.vue';
export default {
......@@ -20,6 +21,7 @@ export default {
TypeOfWorkCharts,
StageTable,
PathNavigation,
ValueStreamAggregationStatus,
ValueStreamFilters,
ValueStreamMetrics,
ValueStreamSelect,
......@@ -55,6 +57,7 @@ export default {
'selectedStageError',
'selectedValueStream',
'pagination',
'aggregation',
]),
...mapGetters([
'hasNoAccessError',
......@@ -81,6 +84,9 @@ export default {
hasDateRangeSet() {
return this.createdAfter && this.createdBefore;
},
isAggregationEnabled() {
return this.aggregation?.enabled;
},
query() {
const { project_ids, created_after, created_before } = this.cycleAnalyticsRequestParams;
const paginationUrlParams = !this.isOverviewStageSelected
......@@ -140,6 +146,10 @@ export default {
},
},
METRICS_REQUESTS,
aggregationPopoverOptions: {
triggers: 'hover',
placement: 'left',
},
};
</script>
<template>
......@@ -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"
>
<h3>{{ __('Value Stream Analytics') }}</h3>
<value-stream-select
v-if="shouldDisplayCreateMultipleValueStreams"
class="gl-align-self-start gl-sm-align-self-start gl-mt-0 gl-sm-mt-5"
/>
<div class="gl-display-flex gl-flex-direction-row gl-align-items-center gl-mt-0 gl-sm-mt-5">
<value-stream-aggregation-status v-if="isAggregationEnabled" :data="aggregation" />
<value-stream-select v-if="shouldDisplayCreateMultipleValueStreams" />
</div>
</div>
<gl-empty-state
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 {
selectedValueStream = {},
defaultStageConfig = [],
pagination = {},
aggregation = {},
} = {},
) {
state.isLoading = true;
......@@ -119,6 +120,8 @@ export default {
state.createdAfter = createdAfter;
state.defaultStageConfig = defaultStageConfig;
Vue.set(state, 'aggregation', aggregation);
Vue.set(state, 'pagination', {
page: pagination.page ?? state.pagination.page,
sort: pagination.sort ?? state.pagination.sort,
......
......@@ -46,4 +46,9 @@ export default () => ({
direction: PAGINATION_SORT_DIRECTION_DESC,
},
stageCounts: {},
aggregation: {
enabled: false,
lastRunAt: null,
nextRunAt: null,
},
});
import dateFormat from 'dateformat';
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';
export const formattedDate = (d) => dateFormat(d, dateFormats.defaultDate);
......@@ -101,6 +101,9 @@ export const buildCycleAnalyticsInitialData = ({
milestonesPath = '',
defaultStages = null,
stage = null,
aggregationEnabled = false,
aggregationLastRunAt = null,
aggregationNextRunAt = null,
} = {}) => ({
selectedValueStream: buildValueStreamFromJson(valueStream),
group: groupId
......@@ -128,4 +131,9 @@ export const buildCycleAnalyticsInitialData = ({
}))
: [],
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';
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 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 waitForPromises from 'helpers/wait_for_promises';
import {
......@@ -42,6 +43,7 @@ import {
issueEvents,
groupLabels,
tasksByTypeData,
aggregationData,
} from '../mock_data';
const noDataSvgPath = 'path/to/no/data';
......@@ -155,6 +157,7 @@ describe('EE Value Stream Analytics component', () => {
return comp;
}
const findAggregationStatus = () => wrapper.findComponent(ValueStreamAggregationStatus);
const findPathNavigation = () => wrapper.findComponent(PathNavigation);
const findStageTable = () => wrapper.findComponent(StageTable);
......@@ -330,6 +333,46 @@ describe('EE Value Stream Analytics component', () => {
it('displays the duration chart', () => {
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 = [
];
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
aggregation = ::Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
{
enabled: aggregation.enabled.to_s,
last_run_at: aggregation.last_incremental_run_at,
next_run_at: aggregation.estimated_next_run_at
last_run_at: aggregation.last_incremental_run_at&.iso8601,
next_run_at: aggregation.estimated_next_run_at&.iso8601
}
end
......
......@@ -11448,6 +11448,9 @@ msgstr ""
msgid "Data is still calculating..."
msgstr ""
msgid "Data refresh"
msgstr ""
msgid "Data type"
msgstr ""
......@@ -21580,6 +21583,9 @@ msgstr ""
msgid "Last updated"
msgstr ""
msgid "Last updated %{time} ago"
msgstr ""
msgid "Last used"
msgstr ""
......@@ -24643,6 +24649,9 @@ msgstr ""
msgid "Next unresolved discussion"
msgstr ""
msgid "Next update"
msgstr ""
msgid "Nickname"
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