Commit 2d3ebd1a authored by Ezekiel Kigbo's avatar Ezekiel Kigbo Committed by Jose Ivan Vargas

Add aggregation vuex state

Adds vuex state for the aggregation
data and builds the object from the
data attributes on load.
parent 0ef34d11
......@@ -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',
......@@ -82,6 +85,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
......@@ -141,6 +147,10 @@ export default {
},
},
METRICS_REQUESTS,
aggregationPopoverOptions: {
triggers: 'hover',
placement: 'left',
},
};
</script>
<template>
......@@ -149,10 +159,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 ""
......@@ -21577,6 +21580,9 @@ msgstr ""
msgid "Last updated"
msgstr ""
msgid "Last updated %{time} ago"
msgstr ""
msgid "Last used"
msgstr ""
......@@ -24640,6 +24646,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