Commit 7f6f70fc authored by Savas Vedova's avatar Savas Vedova

Merge branch...

Merge branch '335974-vsa-fe-add-sorting-and-pagination-to-the-project-level-stage-table' into 'master'

[VSA][FE] Add sorting and pagination to the project level stage table

See merge request gitlab-org/gitlab!71875
parents 2c99025f 76e23a12
...@@ -51,6 +51,7 @@ export default { ...@@ -51,6 +51,7 @@ export default {
'features', 'features',
'createdBefore', 'createdBefore',
'createdAfter', 'createdAfter',
'pagination',
]), ]),
...mapGetters(['pathNavigationData', 'filterParams']), ...mapGetters(['pathNavigationData', 'filterParams']),
displayStageEvents() { displayStageEvents() {
...@@ -99,7 +100,12 @@ export default { ...@@ -99,7 +100,12 @@ export default {
}, },
}, },
methods: { methods: {
...mapActions(['fetchStageData', 'setSelectedStage', 'setDateRange']), ...mapActions([
'fetchStageData',
'setSelectedStage',
'setDateRange',
'updateStageTablePagination',
]),
onSetDateRange({ startDate, endDate }) { onSetDateRange({ startDate, endDate }) {
this.setDateRange({ this.setDateRange({
createdAfter: new Date(startDate), createdAfter: new Date(startDate),
...@@ -108,6 +114,7 @@ export default { ...@@ -108,6 +114,7 @@ export default {
}, },
onSelectStage(stage) { onSelectStage(stage) {
this.setSelectedStage(stage); this.setSelectedStage(stage);
this.updateStageTablePagination({ ...this.pagination, page: 1 });
}, },
dismissOverviewDialog() { dismissOverviewDialog() {
this.isOverviewDialogDismissed = true; this.isOverviewDialogDismissed = true;
...@@ -117,6 +124,9 @@ export default { ...@@ -117,6 +124,9 @@ export default {
const { permissions } = this; const { permissions } = this;
return Boolean(permissions?.[id]); return Boolean(permissions?.[id]);
}, },
onHandleUpdatePagination(data) {
this.updateStageTablePagination(data);
},
}, },
dayRangeOptions: [7, 30, 90], dayRangeOptions: [7, 30, 90],
i18n: { i18n: {
...@@ -163,8 +173,8 @@ export default { ...@@ -163,8 +173,8 @@ export default {
:empty-state-title="emptyStageTitle" :empty-state-title="emptyStageTitle"
:empty-state-message="emptyStageText" :empty-state-message="emptyStageText"
:no-data-svg-path="noDataSvgPath" :no-data-svg-path="noDataSvgPath"
:pagination="null" :pagination="pagination"
:sortable="false" @handleUpdatePagination="onHandleUpdatePagination"
/> />
</div> </div>
</template> </template>
...@@ -194,6 +194,9 @@ export default { ...@@ -194,6 +194,9 @@ export default {
><formatted-stage-count :stage-count="stageCount" ><formatted-stage-count :stage-count="stageCount"
/></gl-badge> /></gl-badge>
</template> </template>
<template #head(duration)="data">
<span data-testid="vsa-stage-header-duration">{{ data.label }}</span>
</template>
<template #cell(end_event)="{ item }"> <template #cell(end_event)="{ item }">
<div data-testid="vsa-stage-event"> <div data-testid="vsa-stage-event">
<div v-if="item.id" data-testid="vsa-stage-content"> <div v-if="item.id" data-testid="vsa-stage-content">
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
getValueStreamStageRecords, getValueStreamStageRecords,
getValueStreamStageCounts, getValueStreamStageCounts,
} from '~/api/analytics_api'; } from '~/api/analytics_api';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants'; import { DEFAULT_VALUE_STREAM, I18N_VSA_ERROR_STAGE_MEDIAN } from '../constants';
...@@ -72,16 +73,21 @@ export const fetchCycleAnalyticsData = ({ ...@@ -72,16 +73,21 @@ export const fetchCycleAnalyticsData = ({
}); });
}; };
export const fetchStageData = ({ getters: { requestParams, filterParams }, commit }) => { export const fetchStageData = ({
getters: { requestParams, filterParams, paginationParams },
commit,
}) => {
commit(types.REQUEST_STAGE_DATA); commit(types.REQUEST_STAGE_DATA);
return getValueStreamStageRecords(requestParams, filterParams) return getValueStreamStageRecords(requestParams, { ...filterParams, ...paginationParams })
.then(({ data }) => { .then(({ data, headers }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data // when there's a query timeout, the request succeeds but the error is encoded in the response data
if (data?.error) { if (data?.error) {
commit(types.RECEIVE_STAGE_DATA_ERROR, data.error); commit(types.RECEIVE_STAGE_DATA_ERROR, data.error);
} else { } else {
commit(types.RECEIVE_STAGE_DATA_SUCCESS, data); commit(types.RECEIVE_STAGE_DATA_SUCCESS, data);
const { page = null, nextPage = null } = parseIntPagination(normalizeHeaders(headers));
commit(types.SET_PAGINATION, { ...paginationParams, page, hasNextPage: Boolean(nextPage) });
} }
}) })
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR)); .catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
...@@ -176,6 +182,14 @@ export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore ...@@ -176,6 +182,14 @@ export const setDateRange = ({ dispatch, commit }, { createdAfter, createdBefore
return refetchStageData(dispatch); return refetchStageData(dispatch);
}; };
export const updateStageTablePagination = (
{ commit, dispatch, state: { selectedStage } },
paginationParams,
) => {
commit(types.SET_PAGINATION, paginationParams);
return dispatch('fetchStageData', selectedStage.id);
};
export const initializeVsa = ({ commit, dispatch }, initialData = {}) => { export const initializeVsa = ({ commit, dispatch }, initialData = {}) => {
commit(types.INITIALIZE_VSA, initialData); commit(types.INITIALIZE_VSA, initialData);
......
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils'; import { filterToQueryObject } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import { PAGINATION_TYPE } from '../constants';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils'; import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => { export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
...@@ -21,6 +22,13 @@ export const requestParams = (state) => { ...@@ -21,6 +22,13 @@ export const requestParams = (state) => {
return { requestPath: fullPath, valueStreamId, stageId }; return { requestPath: fullPath, valueStreamId, stageId };
}; };
export const paginationParams = ({ pagination: { page, sort, direction } }) => ({
pagination: PAGINATION_TYPE,
sort,
direction,
page,
});
const filterBarParams = ({ filters }) => { const filterBarParams = ({ filters }) => {
const { const {
authors: { selected: selectedAuthor }, authors: { selected: selectedAuthor },
......
...@@ -4,6 +4,7 @@ export const SET_LOADING = 'SET_LOADING'; ...@@ -4,6 +4,7 @@ export const SET_LOADING = 'SET_LOADING';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM'; export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE'; export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const SET_DATE_RANGE = 'SET_DATE_RANGE'; export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const SET_PAGINATION = 'SET_PAGINATION';
export const REQUEST_VALUE_STREAMS = 'REQUEST_VALUE_STREAMS'; export const REQUEST_VALUE_STREAMS = 'REQUEST_VALUE_STREAMS';
export const RECEIVE_VALUE_STREAMS_SUCCESS = 'RECEIVE_VALUE_STREAMS_SUCCESS'; export const RECEIVE_VALUE_STREAMS_SUCCESS = 'RECEIVE_VALUE_STREAMS_SUCCESS';
......
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { PAGINATION_SORT_FIELD_END_EVENT, PAGINATION_SORT_DIRECTION_DESC } from '../constants';
import { formatMedianValues } from '../utils'; import { formatMedianValues } from '../utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.INITIALIZE_VSA](state, { endpoints, features, createdBefore, createdAfter }) { [types.INITIALIZE_VSA](
state,
{ endpoints, features, createdBefore, createdAfter, pagination = {} },
) {
state.endpoints = endpoints; state.endpoints = endpoints;
state.createdBefore = createdBefore; state.createdBefore = createdBefore;
state.createdAfter = createdAfter; state.createdAfter = createdAfter;
state.features = features; state.features = features;
Vue.set(state, 'pagination', {
page: pagination.page ?? state.pagination.page,
sort: pagination.sort ?? state.pagination.sort,
direction: pagination.direction ?? state.pagination.direction,
});
}, },
[types.SET_LOADING](state, loadingState) { [types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState; state.isLoading = loadingState;
...@@ -22,6 +33,14 @@ export default { ...@@ -22,6 +33,14 @@ export default {
state.createdBefore = createdBefore; state.createdBefore = createdBefore;
state.createdAfter = createdAfter; state.createdAfter = createdAfter;
}, },
[types.SET_PAGINATION](state, { page, hasNextPage, sort, direction }) {
Vue.set(state, 'pagination', {
page,
hasNextPage,
sort: sort || PAGINATION_SORT_FIELD_END_EVENT,
direction: direction || PAGINATION_SORT_DIRECTION_DESC,
});
},
[types.REQUEST_VALUE_STREAMS](state) { [types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = []; state.valueStreams = [];
}, },
......
import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/cycle_analytics/constants';
export default () => ({ export default () => ({
id: null, id: null,
features: {}, features: {},
...@@ -20,4 +25,10 @@ export default () => ({ ...@@ -20,4 +25,10 @@ export default () => ({
isLoadingStage: false, isLoadingStage: false,
isEmptyStage: false, isEmptyStage: false,
permissions: {}, permissions: {},
pagination: {
page: null,
hasNextPage: false,
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
},
}); });
...@@ -68,6 +68,34 @@ To filter analytics results based on a date range, ...@@ -68,6 +68,34 @@ To filter analytics results based on a date range,
select different **From** and **To** days select different **From** and **To** days
from the date picker (default: last 30 days). from the date picker (default: last 30 days).
### Stage table
> Sorting the stage table [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/335974) in GitLab 14.4.
![Value Stream Analytics Stage table](img/project_vsa_stage_table_v14_4.png "Project VSA stage table")
The stage table shows a list of related workflow items for the selected stage. This can include:
- CI/CD jobs
- Issues
- Merge requests
- Pipelines
A little badge next to the workflow items table header shows the number of workflow items that
completed the selected stage.
The stage table also includes the **Time** column, which shows how long it takes each item to pass
through the selected value stream stage.
To sort the stage table by a table column, select the table header.
You can sort in ascending or descending order. To find items that spent the most time in a stage,
potentially causing bottlenecks in your value stream, sort the table by the **Time** column.
From there, select individual items to drill in and investigate how delays are happening.
To see which items most recently exited the stage, sort by the work item column on the left.
The table displays 20 items per page. If there are more than 20 items, you can use the
**Prev** and **Next** buttons to navigate through the pages.
## How Time metrics are measured ## How Time metrics are measured
The **Time** metrics near the top of the page are measured as follows: The **Time** metrics near the top of the page are measured as follows:
......
...@@ -284,9 +284,9 @@ To sort the stage table by a table column, select the table header. ...@@ -284,9 +284,9 @@ To sort the stage table by a table column, select the table header.
You can sort in ascending or descending order. To find items that spent the most time in a stage, You can sort in ascending or descending order. To find items that spent the most time in a stage,
potentially causing bottlenecks in your value stream, sort the table by the **Time** column. potentially causing bottlenecks in your value stream, sort the table by the **Time** column.
From there, select individual items to drill in and investigate how delays are happening. From there, select individual items to drill in and investigate how delays are happening.
To see which items the stage most recently, sort by the work item column on the left. To see which items most recently exited the stage, sort by the work item column on the left.
The table displays up to 20 items at a time. If there are more than 20 items, you can use the The table displays 20 items per page. If there are more than 20 items, you can use the
**Prev** and **Next** buttons to navigate through the pages. **Prev** and **Next** buttons to navigate through the pages.
### Creating a value stream ### Creating a value stream
......
import dateFormat from 'dateformat'; import dateFormat from 'dateformat';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { dateFormats } from '~/analytics/shared/constants'; import { dateFormats } from '~/analytics/shared/constants';
import { OVERVIEW_STAGE_ID, PAGINATION_TYPE } from '~/cycle_analytics/constants'; import { OVERVIEW_STAGE_ID } from '~/cycle_analytics/constants';
import { pathNavigationData as basePathNavigationData } from '~/cycle_analytics/store/getters'; import {
pathNavigationData as basePathNavigationData,
paginationParams as basePaginationParams,
} from '~/cycle_analytics/store/getters';
import { filterStagesByHiddenStatus } from '~/cycle_analytics/utils'; import { filterStagesByHiddenStatus } from '~/cycle_analytics/utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import httpStatus from '~/lib/utils/http_status'; import httpStatus from '~/lib/utils/http_status';
...@@ -46,12 +49,7 @@ export const cycleAnalyticsRequestParams = (state, getters) => { ...@@ -46,12 +49,7 @@ export const cycleAnalyticsRequestParams = (state, getters) => {
}; };
}; };
export const paginationParams = ({ pagination: { page, sort, direction } }) => ({ export const paginationParams = basePaginationParams;
pagination: PAGINATION_TYPE,
sort,
direction,
page,
});
export const hiddenStages = ({ stages }) => filterStagesByHiddenStatus(stages); export const hiddenStages = ({ stages }) => filterStagesByHiddenStatus(stages);
export const activeStages = ({ stages }) => filterStagesByHiddenStatus(stages, false); export const activeStages = ({ stages }) => filterStagesByHiddenStatus(stages, false);
......
...@@ -14,6 +14,7 @@ import { ...@@ -14,6 +14,7 @@ import {
createdBefore, createdBefore,
createdAfter, createdAfter,
selectedProjects, selectedProjects,
initialPaginationQuery,
} from 'jest/cycle_analytics/mock_data'; } from 'jest/cycle_analytics/mock_data';
import { toYmd } from '~/analytics/shared/utils'; import { toYmd } from '~/analytics/shared/utils';
import PathNavigation from '~/cycle_analytics/components/path_navigation.vue'; import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
...@@ -33,7 +34,6 @@ import httpStatusCodes from '~/lib/utils/http_status'; ...@@ -33,7 +34,6 @@ import httpStatusCodes from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility'; import * as urlUtils from '~/lib/utils/url_utility';
import UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
import { import {
initialPaginationQuery,
valueStreams, valueStreams,
endpoints, endpoints,
customizableStagesAndEvents, customizableStagesAndEvents,
......
import tasksByType from 'test_fixtures/analytics/charts/type_of_work/tasks_by_type.json';
import { uniq } from 'lodash'; import { uniq } from 'lodash';
import tasksByType from 'test_fixtures/analytics/charts/type_of_work/tasks_by_type.json';
import { import {
TASKS_BY_TYPE_SUBJECT_ISSUE, TASKS_BY_TYPE_SUBJECT_ISSUE,
OVERVIEW_STAGE_CONFIG, OVERVIEW_STAGE_CONFIG,
...@@ -20,11 +20,6 @@ import { ...@@ -20,11 +20,6 @@ import {
deepCamelCase, deepCamelCase,
} from 'jest/cycle_analytics/mock_data'; } from 'jest/cycle_analytics/mock_data';
import { toYmd } from '~/analytics/shared/utils'; import { toYmd } from '~/analytics/shared/utils';
import {
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
} from '~/cycle_analytics/constants';
import { transformStagesForPathNavigation, formatMedianValues } from '~/cycle_analytics/utils'; import { transformStagesForPathNavigation, formatMedianValues } from '~/cycle_analytics/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDatesInRange } from '~/lib/utils/datetime_utility'; import { getDatesInRange } from '~/lib/utils/datetime_utility';
...@@ -270,22 +265,3 @@ export const rawDurationMedianData = [ ...@@ -270,22 +265,3 @@ export const rawDurationMedianData = [
]; ];
export const pathNavIssueMetric = 172800; export const pathNavIssueMetric = 172800;
export const initialPaginationQuery = {
page: 15,
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
};
export const initialPaginationState = {
...initialPaginationQuery,
page: null,
hasNextPage: false,
};
export const basePaginationResult = {
pagination: PAGINATION_TYPE,
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
page: null,
};
...@@ -14,8 +14,6 @@ import { ...@@ -14,8 +14,6 @@ import {
issueStage, issueStage,
stageMediansWithNumericIds, stageMediansWithNumericIds,
stageCounts, stageCounts,
basePaginationResult,
initialPaginationState,
transformedStagePathData, transformedStagePathData,
} from '../mock_data'; } from '../mock_data';
...@@ -221,26 +219,6 @@ describe('Value Stream Analytics getters', () => { ...@@ -221,26 +219,6 @@ describe('Value Stream Analytics getters', () => {
}); });
}); });
describe('paginationParams', () => {
beforeEach(() => {
state = { pagination: initialPaginationState };
});
it('returns the `pagination` type', () => {
expect(getters.paginationParams(state)).toEqual(basePaginationResult);
});
it('returns the `sort` type', () => {
expect(getters.paginationParams(state)).toEqual(basePaginationResult);
});
it('with page=10, sets the `page` property', () => {
const page = 10;
state = { pagination: { ...initialPaginationState, page } };
expect(getters.paginationParams(state)).toEqual({ ...basePaginationResult, page });
});
});
describe('selectedStageCount', () => { describe('selectedStageCount', () => {
it('returns the count when a value exist for the given stage', () => { it('returns the count when a value exist for the given stage', () => {
state = { selectedStage: { id: 1 }, stageCounts: { 1: 10, 2: 20 } }; state = { selectedStage: { id: 1 }, stageCounts: { 1: 10, 2: 20 } };
......
...@@ -7,10 +7,13 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -7,10 +7,13 @@ RSpec.describe 'Value Stream Analytics', :js do
let_it_be(:guest) { create(:user) } let_it_be(:guest) { create(:user) }
let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' } let_it_be(:stage_table_selector) { '[data-testid="vsa-stage-table"]' }
let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' } let_it_be(:stage_table_event_selector) { '[data-testid="vsa-stage-event"]' }
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-time-metrics']" }
let_it_be(:metric_value_selector) { "[data-testid='displayValue']" } let_it_be(:metric_value_selector) { "[data-testid='displayValue']" }
let(:stage_table) { page.find(stage_table_selector) } let(:stage_table) { find(stage_table_selector) }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) } let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
...@@ -53,6 +56,7 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -53,6 +56,7 @@ RSpec.describe 'Value Stream Analytics', :js do
# So setting the date range to be the last 2 days should skip past the existing data # So setting the date range to be the last 2 days should skip past the existing data
from = 2.days.ago.strftime("%Y-%m-%d") from = 2.days.ago.strftime("%Y-%m-%d")
to = 1.day.ago.strftime("%Y-%m-%d") to = 1.day.ago.strftime("%Y-%m-%d")
max_items_per_page = 20
around do |example| around do |example|
travel_to(5.days.ago) { example.run } travel_to(5.days.ago) { example.run }
...@@ -60,9 +64,8 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -60,9 +64,8 @@ RSpec.describe 'Value Stream Analytics', :js do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
create_list(:issue, 2, project: project, created_at: 2.weeks.ago, milestone: milestone)
create_cycle(user, project, issue, mr, milestone, pipeline) create_cycle(user, project, issue, mr, milestone, pipeline)
create_list(:issue, max_items_per_page, project: project, created_at: 2.weeks.ago, milestone: milestone)
deploy_master(user, project) deploy_master(user, project)
issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.hour) issue.metrics.update!(first_mentioned_in_commit_at: issue.metrics.first_associated_with_milestone_at + 1.hour)
...@@ -81,6 +84,8 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -81,6 +84,8 @@ RSpec.describe 'Value Stream Analytics', :js do
wait_for_requests wait_for_requests
end end
let(:stage_table_events) { stage_table.all(stage_table_event_selector) }
it 'displays metrics' do it 'displays metrics' do
metrics_tiles = page.find(metrics_selector) metrics_tiles = page.find(metrics_selector)
...@@ -112,20 +117,62 @@ RSpec.describe 'Value Stream Analytics', :js do ...@@ -112,20 +117,62 @@ RSpec.describe 'Value Stream Analytics', :js do
end end
it 'can filter the issues by date' do it 'can filter the issues by date' do
expect(stage_table.all(stage_table_event_selector).length).to eq(3) expect(page).to have_selector(stage_table_event_selector)
set_daterange(from, to) set_daterange(from, to)
expect(stage_table.all(stage_table_event_selector).length).to eq(0) expect(page).not_to have_selector(stage_table_event_selector)
expect(page).not_to have_selector(stage_table_pagination_selector)
end end
it 'can filter the metrics by date' do it 'can filter the metrics by date' do
expect(metrics_values).to eq(["3.0", "2.0", "1.0", "0.0"]) expect(metrics_values).to match_array(["21.0", "2.0", "1.0", "0.0"])
set_daterange(from, to) set_daterange(from, to)
expect(metrics_values).to eq(['-'] * 4) expect(metrics_values).to eq(['-'] * 4)
end end
it 'can sort records' do
# NOTE: checking that the string changes should suffice
# depending on the order the tests are run we might run into problems with hard coded strings
original_first_title = first_stage_title
stage_time_column.click
expect_to_be_sorted "descending"
expect(first_stage_title).not_to have_text(original_first_title, exact: true)
stage_time_column.click
expect_to_be_sorted "ascending"
expect(first_stage_title).to have_text(original_first_title, exact: true)
end
it 'paginates the results' do
original_first_title = first_stage_title
expect(page).to have_selector(stage_table_pagination_selector)
go_to_next_page
expect(page).not_to have_text(original_first_title, exact: true)
end
def stage_time_column
stage_table.find(stage_table_duration_column_header_selector).ancestor("th")
end
def first_stage_title
stage_table.all(stage_table_event_title_selector).first.text
end
def expect_to_be_sorted(direction)
expect(stage_time_column['aria-sort']).to eq(direction)
end
def go_to_next_page
page.find(stage_table_pagination_selector).find_link("Next").click
end
end end
end end
......
...@@ -19,6 +19,7 @@ import { ...@@ -19,6 +19,7 @@ import {
createdAfter, createdAfter,
currentGroup, currentGroup,
stageCounts, stageCounts,
initialPaginationState as pagination,
} from './mock_data'; } from './mock_data';
const selectedStageEvents = issueEvents.events; const selectedStageEvents = issueEvents.events;
...@@ -81,6 +82,7 @@ const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics); ...@@ -81,6 +82,7 @@ const findOverviewMetrics = () => wrapper.findComponent(ValueStreamMetrics);
const findStageTable = () => wrapper.findComponent(StageTable); const findStageTable = () => wrapper.findComponent(StageTable);
const findStageEvents = () => findStageTable().props('stageEvents'); const findStageEvents = () => findStageTable().props('stageEvents');
const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title'); const findEmptyStageTitle = () => wrapper.findComponent(GlEmptyState).props('title');
const findPagination = () => wrapper.findByTestId('vsa-stage-pagination');
const hasMetricsRequests = (reqs) => { const hasMetricsRequests = (reqs) => {
const foundReqs = findOverviewMetrics().props('requests'); const foundReqs = findOverviewMetrics().props('requests');
...@@ -90,7 +92,7 @@ const hasMetricsRequests = (reqs) => { ...@@ -90,7 +92,7 @@ const hasMetricsRequests = (reqs) => {
describe('Value stream analytics component', () => { describe('Value stream analytics component', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents } }); wrapper = createComponent({ initialState: { selectedStage, selectedStageEvents, pagination } });
}); });
afterEach(() => { afterEach(() => {
...@@ -153,6 +155,10 @@ describe('Value stream analytics component', () => { ...@@ -153,6 +155,10 @@ describe('Value stream analytics component', () => {
expect(findLoadingIcon().exists()).toBe(false); expect(findLoadingIcon().exists()).toBe(false);
}); });
it('renders pagination', () => {
expect(findPagination().exists()).toBe(true);
});
describe('with `cycleAnalyticsForGroups=true` license', () => { describe('with `cycleAnalyticsForGroups=true` license', () => {
beforeEach(() => { beforeEach(() => {
wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } }); wrapper = createComponent({ initialState: { features: { cycleAnalyticsForGroups: true } } });
......
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { DEFAULT_VALUE_STREAM, DEFAULT_DAYS_IN_PAST } from '~/cycle_analytics/constants'; import {
DEFAULT_VALUE_STREAM,
DEFAULT_DAYS_IN_PAST,
PAGINATION_TYPE,
PAGINATION_SORT_DIRECTION_DESC,
PAGINATION_SORT_FIELD_END_EVENT,
} from '~/cycle_analytics/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { getDateInPast } from '~/lib/utils/datetime_utility'; import { getDateInPast } from '~/lib/utils/datetime_utility';
...@@ -256,3 +262,22 @@ export const rawValueStreamStages = customizableStagesAndEvents.stages; ...@@ -256,3 +262,22 @@ export const rawValueStreamStages = customizableStagesAndEvents.stages;
export const valueStreamStages = rawValueStreamStages.map((s) => export const valueStreamStages = rawValueStreamStages.map((s) =>
convertObjectPropsToCamelCase(s, { deep: true }), convertObjectPropsToCamelCase(s, { deep: true }),
); );
export const initialPaginationQuery = {
page: 15,
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
};
export const initialPaginationState = {
...initialPaginationQuery,
page: null,
hasNextPage: false,
};
export const basePaginationResult = {
pagination: PAGINATION_TYPE,
sort: PAGINATION_SORT_FIELD_END_EVENT,
direction: PAGINATION_SORT_DIRECTION_DESC,
page: null,
};
...@@ -11,6 +11,8 @@ import { ...@@ -11,6 +11,8 @@ import {
currentGroup, currentGroup,
createdAfter, createdAfter,
createdBefore, createdBefore,
initialPaginationState,
reviewEvents,
} from '../mock_data'; } from '../mock_data';
const { id: groupId, path: groupPath } = currentGroup; const { id: groupId, path: groupPath } = currentGroup;
...@@ -31,7 +33,13 @@ const mockSetDateActionCommit = { ...@@ -31,7 +33,13 @@ const mockSetDateActionCommit = {
type: 'SET_DATE_RANGE', type: 'SET_DATE_RANGE',
}; };
const defaultState = { ...getters, selectedValueStream, createdAfter, createdBefore }; const defaultState = {
...getters,
selectedValueStream,
createdAfter,
createdBefore,
pagination: initialPaginationState,
};
describe('Project Value Stream Analytics actions', () => { describe('Project Value Stream Analytics actions', () => {
let state; let state;
...@@ -112,6 +120,21 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -112,6 +120,21 @@ describe('Project Value Stream Analytics actions', () => {
}); });
}); });
describe('updateStageTablePagination', () => {
beforeEach(() => {
state = { ...state, selectedStage };
});
it(`will dispatch the "fetchStageData" action and commit the 'SET_PAGINATION' mutation`, () => {
return testAction({
action: actions.updateStageTablePagination,
state,
expectedMutations: [{ type: 'SET_PAGINATION' }],
expectedActions: [{ type: 'fetchStageData', payload: selectedStage.id }],
});
});
});
describe('fetchCycleAnalyticsData', () => { describe('fetchCycleAnalyticsData', () => {
beforeEach(() => { beforeEach(() => {
state = { ...defaultState, endpoints: mockEndpoints }; state = { ...defaultState, endpoints: mockEndpoints };
...@@ -154,6 +177,10 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -154,6 +177,10 @@ describe('Project Value Stream Analytics actions', () => {
describe('fetchStageData', () => { describe('fetchStageData', () => {
const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/; const mockStagePath = /value_streams\/\w+\/stages\/\w+\/records/;
const headers = {
'X-Next-Page': 2,
'X-Page': 1,
};
beforeEach(() => { beforeEach(() => {
state = { state = {
...@@ -162,7 +189,7 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -162,7 +189,7 @@ describe('Project Value Stream Analytics actions', () => {
selectedStage, selectedStage,
}; };
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
mock.onGet(mockStagePath).reply(httpStatusCodes.OK); mock.onGet(mockStagePath).reply(httpStatusCodes.OK, reviewEvents, headers);
}); });
it(`commits the 'RECEIVE_STAGE_DATA_SUCCESS' mutation`, () => it(`commits the 'RECEIVE_STAGE_DATA_SUCCESS' mutation`, () =>
...@@ -170,7 +197,11 @@ describe('Project Value Stream Analytics actions', () => { ...@@ -170,7 +197,11 @@ describe('Project Value Stream Analytics actions', () => {
action: actions.fetchStageData, action: actions.fetchStageData,
state, state,
payload: {}, payload: {},
expectedMutations: [{ type: 'REQUEST_STAGE_DATA' }, { type: 'RECEIVE_STAGE_DATA_SUCCESS' }], expectedMutations: [
{ type: 'REQUEST_STAGE_DATA' },
{ type: 'RECEIVE_STAGE_DATA_SUCCESS', payload: reviewEvents },
{ type: 'SET_PAGINATION', payload: { hasNextPage: true, page: 1 } },
],
expectedActions: [], expectedActions: [],
})); }));
......
import * as getters from '~/cycle_analytics/store/getters'; import * as getters from '~/cycle_analytics/store/getters';
import { import {
allowedStages, allowedStages,
stageMedians, stageMedians,
transformedProjectStagePathData, transformedProjectStagePathData,
selectedStage, selectedStage,
stageCounts, stageCounts,
basePaginationResult,
initialPaginationState,
} from '../mock_data'; } from '../mock_data';
describe('Value stream analytics getters', () => { describe('Value stream analytics getters', () => {
let state = {};
describe('pathNavigationData', () => { describe('pathNavigationData', () => {
it('returns the transformed data', () => { it('returns the transformed data', () => {
const state = { stages: allowedStages, medians: stageMedians, selectedStage, stageCounts }; state = { stages: allowedStages, medians: stageMedians, selectedStage, stageCounts };
expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData); expect(getters.pathNavigationData(state)).toEqual(transformedProjectStagePathData);
}); });
}); });
describe('paginationParams', () => {
beforeEach(() => {
state = { pagination: initialPaginationState };
});
it('returns the `pagination` type', () => {
expect(getters.paginationParams(state)).toEqual(basePaginationResult);
});
it('returns the `sort` type', () => {
expect(getters.paginationParams(state)).toEqual(basePaginationResult);
});
it('with page=10, sets the `page` property', () => {
const page = 10;
state = { pagination: { ...initialPaginationState, page } };
expect(getters.paginationParams(state)).toEqual({ ...basePaginationResult, page });
});
});
}); });
import { useFakeDate } from 'helpers/fake_date'; import { useFakeDate } from 'helpers/fake_date';
import * as types from '~/cycle_analytics/store/mutation_types'; import * as types from '~/cycle_analytics/store/mutation_types';
import mutations from '~/cycle_analytics/store/mutations'; import mutations from '~/cycle_analytics/store/mutations';
import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
} from '~/cycle_analytics/constants';
import { import {
selectedStage, selectedStage,
rawIssueEvents, rawIssueEvents,
...@@ -12,6 +16,7 @@ import { ...@@ -12,6 +16,7 @@ import {
formattedStageMedians, formattedStageMedians,
rawStageCounts, rawStageCounts,
stageCounts, stageCounts,
initialPaginationState as pagination,
} from '../mock_data'; } from '../mock_data';
let state; let state;
...@@ -25,7 +30,7 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -25,7 +30,7 @@ describe('Project Value Stream Analytics mutations', () => {
useFakeDate(2020, 6, 18); useFakeDate(2020, 6, 18);
beforeEach(() => { beforeEach(() => {
state = {}; state = { pagination };
}); });
afterEach(() => { afterEach(() => {
...@@ -88,16 +93,18 @@ describe('Project Value Stream Analytics mutations', () => { ...@@ -88,16 +93,18 @@ describe('Project Value Stream Analytics mutations', () => {
}); });
it.each` it.each`
mutation | payload | stateKey | value mutation | payload | stateKey | value
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter} ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore} ${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true} ${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false} ${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream} ${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]} ${types.SET_PAGINATION} | ${pagination} | ${'pagination'} | ${{ ...pagination, sort: PAGINATION_SORT_FIELD_END_EVENT, direction: PAGINATION_SORT_DIRECTION_DESC }}
${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages} ${types.SET_PAGINATION} | ${{ ...pagination, sort: 'duration', direction: 'asc' }} | ${'pagination'} | ${{ ...pagination, sort: 'duration', direction: 'asc' }}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians} ${types.RECEIVE_VALUE_STREAMS_SUCCESS} | ${[selectedValueStream]} | ${'valueStreams'} | ${[selectedValueStream]}
${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts} ${types.RECEIVE_VALUE_STREAM_STAGES_SUCCESS} | ${{ stages: rawValueStreamStages }} | ${'stages'} | ${valueStreamStages}
${types.RECEIVE_STAGE_MEDIANS_SUCCESS} | ${rawStageMedians} | ${'medians'} | ${formattedStageMedians}
${types.RECEIVE_STAGE_COUNTS_SUCCESS} | ${rawStageCounts} | ${'stageCounts'} | ${stageCounts}
`( `(
'$mutation with $payload will set $stateKey to $value', '$mutation with $payload will set $stateKey to $value',
({ mutation, payload, stateKey, value }) => { ({ mutation, payload, stateKey, value }) => {
......
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