Commit a1124f44 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Add path navigation to project VSA

Cleanup styling for stage table

Minor update feature specs

Added getter specs for filterStagesByHiddenStatus
and some additional getters
parent 700e1ab5
<script>
import { GlIcon, GlEmptyState, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import Cookies from 'js-cookie';
import { mapActions, mapState } from 'vuex';
import { mapActions, mapState, mapGetters } from 'vuex';
import PathNavigation from 'ee/analytics/cycle_analytics/components/path_navigation.vue';
import { __ } from '~/locale';
import banner from './banner.vue';
import stageCodeComponent from './stage_code_component.vue';
......@@ -29,6 +30,7 @@ export default {
'stage-staging-component': stageStagingComponent,
'stage-production-component': stageComponent,
'stage-nav-item': stageNavItem,
PathNavigation,
},
props: {
noDataSvgPath: {
......@@ -56,6 +58,7 @@ export default {
'summary',
'startDate',
]),
...mapGetters(['pathNavigationData']),
displayStageEvents() {
const { selectedStageEvents, isLoadingStage, isEmptyStage } = this;
return selectedStageEvents.length && !isLoadingStage && !isEmptyStage;
......@@ -68,6 +71,12 @@ export default {
const { selectedStage } = this;
return selectedStage && !selectedStage.isUserAllowed;
},
selectedStageReady() {
return !this.hasNoAccessError && this.selectedStage;
},
shouldDisplayPathNavigation() {
return this.selectedStage;
},
},
methods: {
...mapActions([
......@@ -83,8 +92,8 @@ export default {
isActiveStage(stage) {
return stage.slug === this.selectedStage.slug;
},
selectStage(stage) {
if (this.selectedStage === stage) return;
onSelectStage(stage) {
if (this.isLoadingStage || this.selectedStage?.slug === stage?.slug) return;
this.setSelectedStage(stage);
if (!stage.isUserAllowed) {
......@@ -108,6 +117,15 @@ export default {
<div class="cycle-analytics">
<gl-loading-icon v-if="isLoading" size="lg" />
<div v-else class="wrapper">
<path-navigation
v-if="shouldDisplayPathNavigation"
:key="`path_navigation_key_${pathNavigationData.length}`"
class="js-path-navigation gl-w-full gl-pb-2"
:loading="isLoading"
:stages="pathNavigationData"
:selected-stage="selectedStage"
@selected="(ev) => onSelectStage(ev)"
/>
<div class="card">
<div class="card-header">{{ __('Recent Project Activity') }}</div>
<div class="d-flex justify-content-between">
......@@ -140,39 +158,11 @@ export default {
</div>
</div>
<div class="stage-panel-container">
<div class="card stage-panel">
<div class="card stage-panel gl-px-5">
<div class="card-header border-bottom-0">
<nav class="col-headers">
<ul>
<li class="stage-header pl-5">
<span class="stage-name font-weight-bold">{{
s__('ProjectLifecycle|Stage')
}}</span>
<span
class="has-tooltip"
data-placement="top"
:title="__('The phase of the development lifecycle.')"
aria-hidden="true"
>
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
<li class="median-header">
<span class="stage-name font-weight-bold">{{ __('Median') }}</span>
<span
class="has-tooltip"
data-placement="top"
:title="
__(
'The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.',
)
"
aria-hidden="true"
>
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
<li class="event-header pl-3">
<ul class="gl-display-flex gl-justify-content-space-between gl-list-style-none">
<li>
<span v-if="selectedStage" class="stage-name font-weight-bold">{{
selectedStage.legend ? __(selectedStage.legend) : __('Related Issues')
}}</span>
......@@ -187,7 +177,7 @@ export default {
<gl-icon name="question-o" class="gl-text-gray-500" />
</span>
</li>
<li class="total-time-header pr-5 text-right">
<li>
<span class="stage-name font-weight-bold">{{ __('Time') }}</span>
<span
class="has-tooltip"
......@@ -201,22 +191,8 @@ export default {
</ul>
</nav>
</div>
<div class="stage-panel-body">
<nav class="stage-nav">
<ul>
<stage-nav-item
v-for="stage in stages"
:key="stage.title"
:title="stage.title"
:is-user-allowed="stage.isUserAllowed"
:value="stage.value"
:is-active="isActiveStage(stage)"
@select="selectStage(stage)"
/>
</ul>
</nav>
<section class="stage-events overflow-auto">
<section class="stage-events overflow-auto gl-w-full">
<gl-loading-icon v-show="isLoadingStage" size="lg" />
<template v-if="displayNoAccess">
<gl-empty-state
......
import { transformStagesForPathNavigation } from '../utils';
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
console.log('medians', medians);
return transformStagesForPathNavigation({
stages: filterStagesByHiddenStatus(stages, false),
medians,
stageCounts,
selectedStage,
});
};
......@@ -8,6 +8,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import state from './state';
......@@ -16,6 +17,7 @@ Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
getters,
mutations,
state,
});
import { decorateData, decorateEvents } from '../utils';
import { decorateData, decorateEvents, formatMedianValues } from '../utils';
import * as types from './mutation_types';
export default {
......@@ -20,9 +20,12 @@ export default {
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
state.isLoading = false;
const { stages, summary } = decorateData(data);
const { stages, summary, medians } = decorateData(data);
console.log('medians', medians);
console.log('formatMedianValues::medians', formatMedianValues(medians));
state.stages = stages;
state.summary = summary;
state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
......
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { dasherize } from '~/lib/utils/text_utility';
import { __ } from '../locale';
import { unescape } from 'lodash';
import { sanitize } from '~/lib/dompurify';
import {
newDate,
dayAfter,
secondsToDays,
getDatesInRange,
parseSeconds,
} from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
import { OVERVIEW_STAGE_ID } from './constants';
const EMPTY_STAGE_TEXTS = {
issue: __(
......@@ -53,11 +63,96 @@ const mapToStage = (permissions, item) => {
};
const mapToSummary = ({ value, ...rest }) => ({ ...rest, value: value || '-' });
const mapToMedians = ({ id, name, value }) => ({ id, name, value });
export const decorateData = (data = {}) => {
const { permissions, stats, summary } = data;
return {
stages: stats?.map((item) => mapToStage(permissions, item)) || [],
summary: summary?.map((item) => mapToSummary(item)) || [],
medians: stats?.map((item) => mapToMedians(item)) || [],
};
};
/**
* Takes the stages and median data, combined with the selected stage, to build an
* array which is formatted to proivde the data required for the path navigation.
*
* @param {Array} stages - The stages available to the group / project
* @param {Object} medians - The median values for the stages available to the group / project
* @param {Object} stageCounts - The total item count for the stages available
* @param {Object} selectedStage - The currently selected stage
* @returns {Array} An array of stages formatted with data required for the path navigation
*/
export const transformStagesForPathNavigation = ({
stages,
medians,
stageCounts = {},
selectedStage,
}) => {
// TODO: do we need popovers for the project path
// - medians, start / end events + descriptions
const formattedStages = stages.map((stage) => {
return {
metric: medians[stage?.id],
selected: stage.id === selectedStage.id,
stageCount: stageCounts && stageCounts[stage?.id],
icon: null,
...stage,
};
});
return formattedStages;
};
export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, weeks, months }) => {
if (months) {
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
value: roundToNearestHalf(months),
});
} else if (weeks) {
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
value: roundToNearestHalf(weeks),
});
} else if (days) {
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
value: roundToNearestHalf(days),
});
} else if (hours) {
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
} else if (minutes) {
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
} else if (seconds) {
return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
}
return '-';
};
/**
* Takes a raw median value in seconds and converts it to a string representation
* ie. converts 172800 => 2d (2 days)
*
* @param {Number} Median - The number of seconds for the median calculation
* @returns {String} String representation ie 2w
*/
export const medianTimeToParsedSeconds = (value) =>
timeSummaryForPathNavigation({
...parseSeconds(value, { daysPerWeek: 7, hoursPerDay: 24 }),
seconds: value,
});
/**
* Takes the raw median value arrays and converts them into a useful object
* containing the string for display in the path navigation
* ie. converts [{ id: 'test', value: 172800 }] => { 'test': '2d' }
*
* @param {Array} Medians - Array of stage median objects, each contains a `id`, `value` and `error`
* @returns {Object} Returns key value pair with the stage name and its display median value
*/
export const formatMedianValues = (medians = []) =>
medians.reduce((acc, { id, value = 0 }) => {
return {
...acc,
[id]: value ? medianTimeToParsedSeconds(value) : '-',
};
}, {});
......@@ -30,32 +30,12 @@
.col-headers {
ul {
@include clearfix;
margin: 0;
padding: 0;
}
li {
display: inline-block;
float: left;
line-height: 50px;
width: 20%;
}
.stage-header {
width: 20.5%;
}
.median-header {
width: 19.5%;
}
.event-header {
width: 45%;
}
.total-time-header {
width: 15%;
}
}
......@@ -120,7 +100,6 @@
}
li {
@include clearfix;
list-style-type: none;
}
......@@ -169,7 +148,6 @@
.events-description {
line-height: 65px;
padding: 0 $gl-padding;
}
.events-info {
......@@ -178,7 +156,6 @@
}
.stage-events {
width: 60%;
min-height: 467px;
}
......@@ -190,8 +167,8 @@
.stage-event-item {
@include clearfix;
list-style-type: none;
padding: 0 0 $gl-padding;
margin: 0 $gl-padding $gl-padding;
padding-bottom: $gl-padding;
margin-bottom: $gl-padding;
border-bottom: 1px solid var(--gray-50, $gray-50);
&:last-child {
......
......@@ -57,7 +57,6 @@ export default {
'errorCode',
'startDate',
'endDate',
'medians',
'isLoadingValueStreams',
'selectedStageError',
'selectedValueStream',
......
import dateFormat from 'dateformat';
import { isNumber } from 'lodash';
import {
filterStagesByHiddenStatus,
pathNavigationData as basePathNavigationData,
} from '~/cycle_analytics/store/getters';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import httpStatus from '~/lib/utils/http_status';
import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
......@@ -10,7 +14,6 @@ import {
PAGINATION_TYPE,
OVERVIEW_STAGE_ID,
} from '../constants';
import { transformStagesForPathNavigation } from '../utils';
export const hasNoAccessError = (state) => state.errorCode === httpStatus.FORBIDDEN;
......@@ -56,9 +59,6 @@ export const paginationParams = ({ pagination: { page, sort, direction } }) => (
page,
});
const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
export const hiddenStages = ({ stages }) => filterStagesByHiddenStatus(stages);
export const activeStages = ({ stages }) => filterStagesByHiddenStatus(stages, false);
......@@ -78,8 +78,8 @@ export const isOverviewStageSelected = ({ selectedStage }) =>
* https://gitlab.com/gitlab-org/gitlab/-/issues/216227
*/
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) =>
transformStagesForPathNavigation({
stages: [OVERVIEW_STAGE_CONFIG, ...filterStagesByHiddenStatus(stages, false)],
basePathNavigationData({
stages: [OVERVIEW_STAGE_CONFIG, ...stages],
medians,
stageCounts,
selectedStage,
......
import dateFormat from 'dateformat';
import { unescape, isNumber } from 'lodash';
import { isNumber } from 'lodash';
import { medianTimeToParsedSeconds } from '~/cycle_analytics/utils';
import createFlash, { hideFlash } from '~/flash';
import { sanitize } from '~/lib/dompurify';
import { convertObjectPropsToCamelCase, roundToNearestHalf } from '~/lib/utils/common_utils';
import {
newDate,
dayAfter,
secondsToDays,
getDatesInRange,
parseSeconds,
} from '~/lib/utils/datetime_utility';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { newDate, dayAfter, secondsToDays, getDatesInRange } from '~/lib/utils/datetime_utility';
import httpStatus from '~/lib/utils/http_status';
import { convertToSnakeCase, slugify } from '~/lib/utils/text_utility';
import { s__, sprintf } from '~/locale';
import { dateFormats } from '../shared/constants';
import { toYmd } from '../shared/utils';
import { OVERVIEW_STAGE_ID } from './constants';
......@@ -357,42 +350,6 @@ export const throwIfUserForbidden = (error) => {
}
};
export const timeSummaryForPathNavigation = ({ seconds, hours, days, minutes, weeks, months }) => {
if (months) {
return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
value: roundToNearestHalf(months),
});
} else if (weeks) {
return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
value: roundToNearestHalf(weeks),
});
} else if (days) {
return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
value: roundToNearestHalf(days),
});
} else if (hours) {
return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
} else if (minutes) {
return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
} else if (seconds) {
return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
}
return '-';
};
/**
* Takes a raw median value in seconds and converts it to a string representation
* ie. converts 172800 => 2d (2 days)
*
* @param {Number} Median - The number of seconds for the median calculation
* @returns {String} String representation ie 2w
*/
export const medianTimeToParsedSeconds = (value) =>
timeSummaryForPathNavigation({
...parseSeconds(value, { daysPerWeek: 7, hoursPerDay: 24 }),
seconds: value,
});
/**
* Takes the raw median value arrays and converts them into a useful object
* containing the string for display in the path navigation, additionally
......@@ -422,35 +379,6 @@ export const formatMedianValuesWithOverview = (medians = []) => {
};
};
/**
* Takes the stages and median data, combined with the selected stage, to build an
* array which is formatted to proivde the data required for the path navigation.
*
* @param {Array} stages - The stages available to the group / project
* @param {Object} medians - The median values for the stages available to the group / project
* @param {Object} stageCounts - The total item count for the stages available
* @param {Object} selectedStage - The currently selected stage
* @returns {Array} An array of stages formatted with data required for the path navigation
*/
export const transformStagesForPathNavigation = ({
stages,
medians,
stageCounts,
selectedStage,
}) => {
const formattedStages = stages.map((stage) => {
return {
metric: medians[stage?.id],
selected: stage.id === selectedStage.id,
stageCount: stageCounts[stage?.id],
icon: null,
...stage,
};
});
return formattedStages;
};
/**
* @typedef {Object} MetricData
* @property {String} title - Title of the metric measured
......
......@@ -61,6 +61,7 @@ describe('PathNavigation', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(false);
});
// TODO: make this test more granular
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
......
......@@ -12,24 +12,20 @@ import mutations from 'ee/analytics/cycle_analytics/store/mutations';
import {
getTasksByTypeData,
transformRawTasksByTypeData,
transformStagesForPathNavigation,
} from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import {
getStageByTitle,
defaultStages,
rawStageMedians,
fixtureEndpoints,
} from 'jest/cycle_analytics/mock_data';
import { transformStagesForPathNavigation } from '~/cycle_analytics/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast, getDatesInRange } from '~/lib/utils/datetime_utility';
const fixtureEndpoints = {
customizableCycleAnalyticsStagesAndEvents: 'analytics/value_stream_analytics/stages.json', // customizable stages and events endpoint
stageEvents: (stage) => `analytics/value_stream_analytics/stages/${stage}/records.json`,
stageMedian: (stage) => `analytics/value_stream_analytics/stages/${stage}/median.json`,
stageCount: (stage) => `analytics/value_stream_analytics/stages/${stage}/count.json`,
recentActivityData: 'analytics/metrics/value_stream_analytics/summary.json',
timeMetricsData: 'analytics/metrics/value_stream_analytics/time_summary.json',
groupLabels: 'api/group_labels.json',
};
export const endpoints = {
groupLabels: /groups\/[A-Z|a-z|\d|\-|_]+\/-\/labels.json/,
recentActivityData: /analytics\/value_stream_analytics\/summary/,
......@@ -63,9 +59,6 @@ export const group = {
export const currentGroup = convertObjectPropsToCamelCase(group, { deep: true });
const getStageByTitle = (stages, title) =>
stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
export const recentActivityData = getJSONFixture(fixtureEndpoints.recentActivityData);
export const timeMetricsData = getJSONFixture(fixtureEndpoints.timeMetricsData);
......@@ -113,8 +106,6 @@ export const allowedStages = [issueStage, planStage, codeStage];
const deepCamelCase = (obj) => convertObjectPropsToCamelCase(obj, { deep: true });
export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging'];
const stageFixtures = defaultStages.reduce((acc, stage) => {
const events = getJSONFixture(fixtureEndpoints.stageEvents(stage));
return {
......@@ -123,11 +114,6 @@ const stageFixtures = defaultStages.reduce((acc, stage) => {
};
}, {});
export const rawStageMedians = defaultStages.map((id) => ({
id,
...getJSONFixture(fixtureEndpoints.stageMedian(id)),
}));
export const stageMedians = rawStageMedians.reduce(
(acc, { id, value }) => ({
...acc,
......@@ -323,8 +309,6 @@ export const selectedProjects = [
},
];
export const pathNavIssueMetric = 172800;
export const initialPaginationState = {
page: null,
hasNextPage: false,
......
......@@ -15,7 +15,6 @@ import {
flattenTaskByTypeSeries,
orderByDate,
toggleSelectedLabel,
transformStagesForPathNavigation,
prepareTimeMetricsData,
prepareStageErrors,
timeSummaryForPathNavigation,
......@@ -23,6 +22,7 @@ import {
medianTimeToParsedSeconds,
} from 'ee/analytics/cycle_analytics/utils';
import { toYmd } from 'ee/analytics/shared/utils';
import { rawStageMedians } from 'jest/cycle_analytics/mock_data';
import { getDatesInRange } from '~/lib/utils/datetime_utility';
import { slugify } from '~/lib/utils/text_utility';
import {
......@@ -38,12 +38,7 @@ import {
issueStage,
rawCustomStage,
rawTasksByTypeData,
allowedStages,
stageMediansWithNumericIds,
pathNavIssueMetric,
timeMetricsData,
rawStageMedians,
stageCounts,
} from './mock_data';
const labelEventIds = labelEvents.map((ev) => ev.identifier);
......@@ -341,35 +336,6 @@ describe('Value Stream Analytics utils', () => {
});
});
describe('transformStagesForPathNavigation', () => {
const stages = allowedStages;
const response = transformStagesForPathNavigation({
stages,
medians: stageMediansWithNumericIds,
selectedStage: issueStage,
stageCounts,
});
describe('transforms the data as expected', () => {
it('returns an array of stages', () => {
expect(Array.isArray(response)).toBe(true);
expect(response.length).toEqual(stages.length);
});
it('selects the correct stage', () => {
const selected = response.filter((stage) => stage.selected === true)[0];
expect(selected.title).toEqual(issueStage.title);
});
it('includes the correct metric for the associated stage', () => {
const issue = response.filter((stage) => stage.name === 'Issue')[0];
expect(issue.metric).toEqual(pathNavIssueMetric);
});
});
});
describe('prepareTimeMetricsData', () => {
let prepared;
const [{ title: firstTitle }, { title: secondTitle }] = timeMetricsData;
......
......@@ -32,7 +32,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
it 'shows active stage with empty message' do
expect(page).to have_selector('.stage-nav-item.active', text: 'Issue')
expect(page).to have_selector('.gl-path-active-item-indigo', text: 'Issue')
expect(page).to have_content("We don't have enough data to show this stage.")
end
end
......@@ -171,7 +171,7 @@ RSpec.describe 'Value Stream Analytics', :js do
end
def click_stage(stage_name)
find('.stage-nav li', text: stage_name).click
find('.gl-path-nav-list-item', text: stage_name).click
wait_for_requests
end
end
import { getJSONFixture } from 'helpers/fixtures';
import { transformStagesForPathNavigation } from '~/cycle_analytics/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export const fixtureEndpoints = {
customizableCycleAnalyticsStagesAndEvents: 'analytics/value_stream_analytics/stages.json', // customizable stages and events endpoint
stageEvents: (stage) => `analytics/value_stream_analytics/stages/${stage}/records.json`,
stageMedian: (stage) => `analytics/value_stream_analytics/stages/${stage}/median.json`,
stageCount: (stage) => `analytics/value_stream_analytics/stages/${stage}/count.json`,
recentActivityData: 'analytics/metrics/value_stream_analytics/summary.json',
timeMetricsData: 'analytics/metrics/value_stream_analytics/time_summary.json',
groupLabels: 'api/group_labels.json',
};
export const getStageByTitle = (stages, title) =>
stages.find((stage) => stage.title && stage.title.toLowerCase().trim() === title) || {};
export const defaultStages = ['issue', 'plan', 'review', 'code', 'test', 'staging'];
export const rawStageMedians = defaultStages.map((id) => ({
id,
...getJSONFixture(fixtureEndpoints.stageMedian(id)),
}));
export const summary = [
{ value: '20', title: 'New Issues' },
{ value: null, title: 'Commits' },
......@@ -184,3 +205,29 @@ export const rawEvents = [
export const convertedEvents = rawEvents.map((ev) =>
convertObjectPropsToCamelCase(ev, { deep: true }),
);
export const pathNavIssueMetric = 388800;
export const stageMediansWithNumericIds = rawStageMedians.reduce((acc, { id, value }) => {
const { id: stageId } = getStageByTitle(convertedData.stages, id);
return {
...acc,
[stageId]: value,
};
}, {});
export const stageMedians = rawStageMedians.reduce(
(acc, { id, value }) => ({
...acc,
[id]: value,
}),
{},
);
export const allowedStages = [issueStage, planStage, codeStage];
export const transformedProjectStagePathData = transformStagesForPathNavigation({
stages: allowedStages,
medians: stageMedians,
selectedStage: issueStage,
});
import * as getters from '~/cycle_analytics/store/getters';
import {
allowedStages,
stageMedians,
transformedProjectStagePathData,
selectedStage,
} from '../mock_data';
// TODO: move path navigation component to CE ee/spec/frontend/analytics/cycle_analytics/components/path_navigation_spec.js
describe('Value stream analytics getters', () => {
describe('pathNavigationData', () => {
it('returns the transformed data', () => {
const state = { stages: allowedStages, medians: stageMedians, selectedStage };
expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData);
});
});
describe('filterStagesByHiddenStatus', () => {
const hiddenStages = [{ title: 'three', hidden: true }];
const visibleStages = [
{ title: 'one', hidden: false },
{ title: 'two', hidden: false },
];
const mockStages = [...visibleStages, ...hiddenStages];
it.each`
isHidden | result
${false} | ${visibleStages}
${undefined} | ${hiddenStages}
${true} | ${hiddenStages}
`('with isHidden=$isHidden returns matching stages', ({ isHidden, result }) => {
expect(getters.filterStagesByHiddenStatus(mockStages, isHidden)).toEqual(result);
});
});
});
import { decorateEvents, decorateData } from '~/cycle_analytics/utils';
import { selectedStage, rawData, convertedData, rawEvents } from './mock_data';
import {
decorateEvents,
decorateData,
transformStagesForPathNavigation,
} from '~/cycle_analytics/utils';
import {
selectedStage,
rawData,
convertedData,
rawEvents,
allowedStages,
stageMediansWithNumericIds,
pathNavIssueMetric,
stageCounts,
} from './mock_data';
describe('Value stream analytics utils', () => {
describe('decorateEvents', () => {
......@@ -74,4 +87,33 @@ describe('Value stream analytics utils', () => {
});
});
});
describe('transformStagesForPathNavigation', () => {
const stages = allowedStages;
const response = transformStagesForPathNavigation({
stages,
medians: stageMediansWithNumericIds,
selectedStage,
stageCounts,
});
describe('transforms the data as expected', () => {
it('returns an array of stages', () => {
expect(Array.isArray(response)).toBe(true);
expect(response.length).toEqual(stages.length);
});
it('selects the correct stage', () => {
const selected = response.filter((stage) => stage.selected === true)[0];
expect(selected.title).toEqual(selectedStage.title);
});
it('includes the correct metric for the associated stage', () => {
const issue = response.filter((stage) => stage.name === 'issue')[0];
expect(issue.metric).toEqual(pathNavIssueMetric);
});
});
});
});
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