Commit de25a9ed authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Add aggregating warning component

Introduces a alert message for when a newly
created value stream is aggregating data.
parent 5fc82e3e
......@@ -291,6 +291,9 @@ To create a value stream:
![New value stream](img/new_value_stream_v13_12.png "Creating a new value stream")
NOTE:
If you have recently upgraded to GitLab Premium, it can take up to 30 minutes for data to collect and display.
### Create a value stream with stages
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50229) in GitLab 13.7.
......
......@@ -15,6 +15,7 @@ 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 ValueStreamAggregatingWarning from './value_stream_aggregating_warning.vue';
import ValueStreamEmptyState from './value_stream_empty_state.vue';
import ValueStreamSelect from './value_stream_select.vue';
......@@ -27,6 +28,7 @@ export default {
StageTable,
PathNavigation,
ValueStreamAggregationStatus,
ValueStreamAggregatingWarning,
ValueStreamEmptyState,
ValueStreamFilters,
ValueStreamMetrics,
......@@ -66,6 +68,7 @@ export default {
'pagination',
'aggregation',
'isUpdatingAggregation',
'isCreatingAggregation',
]),
...mapGetters([
'hasNoAccessError',
......@@ -78,8 +81,16 @@ export default {
'selectedStageCount',
'hasValueStreams',
]),
isWaitingForNextAggregation() {
return Boolean(
this.selectedValueStream && this.isAggregationEnabled && !this.aggregation.lastRunAt,
);
},
shouldRenderEmptyState() {
return this.isLoadingValueStreams || !this.hasValueStreams;
return this.isLoadingValueStreams || (!this.isCreatingAggregation && !this.hasValueStreams);
},
shouldRenderAggregationWarning() {
return this.isCreatingAggregation || this.isWaitingForNextAggregation;
},
shouldDisplayFilters() {
return !this.errorCode && !this.hasNoAccessError;
......@@ -88,7 +99,9 @@ export default {
return !this.hasNoAccessError && this.selectedStage;
},
shouldDisplayCreateMultipleValueStreams() {
return Boolean(!this.shouldRenderEmptyState && !this.isLoadingValueStreams);
return Boolean(
!this.shouldRenderEmptyState && !this.isLoadingValueStreams && !this.isCreatingAggregation,
);
},
hasDateRangeSet() {
return this.createdAfter && this.createdBefore;
......@@ -102,6 +115,9 @@ export default {
isAggregationStatusAvailable() {
return this.isAggregationEnabled && this.aggregation.lastRunAt;
},
selectedValueStreamName() {
return this.selectedValueStream?.name;
},
query() {
const { project_ids, created_after, created_before } = this.cycleAnalyticsRequestParams;
const paginationUrlParams = !this.isOverviewStageSelected
......@@ -160,6 +176,9 @@ export default {
onHandleUpdatePagination(data) {
this.updateStageTablePagination(data);
},
onHandleReloadPage() {
refreshCurrentPage();
},
onToggleAggregation(value) {
this.updateAggregation(value)
.then(() => {
......@@ -229,55 +248,67 @@ export default {
:selected-stage="selectedStage"
@selected="onStageSelect"
/>
<value-stream-filters
:group-id="currentGroup.id"
:group-path="currentGroupPath"
:selected-projects="selectedProjects"
:start-date="createdAfter"
:end-date="createdBefore"
:can-toggle-aggregation="canToggleAggregation"
:is-aggregation-enabled="isAggregationEnabled"
:is-updating-aggregation-data="isLoading || isUpdatingAggregation"
@toggleAggregation="onToggleAggregation"
@selectProject="onProjectsSelect"
@setDateRange="onSetDateRange"
/>
<gl-empty-state
v-if="hasNoAccessError"
class="js-empty-state gl-mt-2"
:title="__('You don’t have access to Value Stream Analytics for this group')"
:svg-path="noAccessSvgPath"
:description="
__(
'Only \'Reporter\' roles and above on tiers Premium and above can see Value Stream Analytics.',
)
"
<value-stream-aggregating-warning
v-if="shouldRenderAggregationWarning"
class="gl-my-6"
:value-stream-title="selectedValueStreamName"
@reload="onHandleReloadPage"
/>
<template v-else>
<div :class="[isOverviewStageSelected ? 'gl-mt-2' : 'gl-mt-6']">
<value-stream-metrics
v-if="isOverviewStageSelected"
:request-path="currentGroupPath"
:request-params="cycleAnalyticsRequestParams"
:requests="$options.METRICS_REQUESTS"
/>
<duration-chart class="gl-mt-3" :stages="activeStages" :selected-stage="selectedStage" />
<type-of-work-charts v-if="isOverviewStageSelected" />
<stage-table
v-if="!isOverviewStageSelected"
class="gl-mt-5"
:is-loading="isLoading || isLoadingStage"
:stage-events="selectedStageEvents"
:selected-stage="selectedStage"
:stage-count="selectedStageCount"
:empty-state-message="selectedStageError"
:no-data-svg-path="noDataSvgPath"
:pagination="pagination"
include-project-name
@handleUpdatePagination="onHandleUpdatePagination"
/>
</div>
<url-sync v-if="selectedStageReady" :query="query" />
<value-stream-filters
:group-id="currentGroup.id"
:group-path="currentGroupPath"
:selected-projects="selectedProjects"
:start-date="createdAfter"
:end-date="createdBefore"
:can-toggle-aggregation="canToggleAggregation"
:is-aggregation-enabled="isAggregationEnabled"
:is-updating-aggregation-data="isLoading || isUpdatingAggregation"
@toggleAggregation="onToggleAggregation"
@selectProject="onProjectsSelect"
@setDateRange="onSetDateRange"
/>
<gl-empty-state
v-if="hasNoAccessError"
class="js-empty-state gl-mt-2"
:title="__('You don’t have access to Value Stream Analytics for this group')"
:svg-path="noAccessSvgPath"
:description="
__(
'Only \'Reporter\' roles and above on tiers Premium and above can see Value Stream Analytics.',
)
"
/>
<template v-else>
<div :class="[isOverviewStageSelected ? 'gl-mt-2' : 'gl-mt-6']">
<value-stream-metrics
v-if="isOverviewStageSelected"
:request-path="currentGroupPath"
:request-params="cycleAnalyticsRequestParams"
:requests="$options.METRICS_REQUESTS"
/>
<duration-chart
class="gl-mt-3"
:stages="activeStages"
:selected-stage="selectedStage"
/>
<type-of-work-charts v-if="isOverviewStageSelected" />
<stage-table
v-if="!isOverviewStageSelected"
class="gl-mt-5"
:is-loading="isLoading || isLoadingStage"
:stage-events="selectedStageEvents"
:selected-stage="selectedStage"
:stage-count="selectedStageCount"
:empty-state-message="selectedStageError"
:no-data-svg-path="noDataSvgPath"
:pagination="pagination"
include-project-name
@handleUpdatePagination="onHandleUpdatePagination"
/>
</div>
<url-sync v-if="selectedStageReady" :query="query" />
</template>
</template>
</div>
</div>
......
<script>
import { GlAlert } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
AGGREGATING_DATA_WARNING_TITLE,
AGGREGATING_DATA_WARNING_MESSAGE,
AGGREGATING_DATA_WARNING_NEXT_UPDATE,
AGGREGATING_DATA_PRIMARY_ACTION_TEXT,
AGGREGATING_DATA_SECONDARY_ACTION_TEXT,
} from '../constants';
export default {
name: 'ValueStreamAggregatingWarning',
components: {
GlAlert,
},
props: {
valueStreamTitle: {
type: String,
required: true,
},
},
computed: {
message() {
return sprintf(AGGREGATING_DATA_WARNING_MESSAGE, { name: this.valueStreamTitle });
},
},
i18n: {
title: AGGREGATING_DATA_WARNING_TITLE,
nextUpdate: AGGREGATING_DATA_WARNING_NEXT_UPDATE,
primaryText: AGGREGATING_DATA_PRIMARY_ACTION_TEXT,
secondaryText: AGGREGATING_DATA_SECONDARY_ACTION_TEXT,
},
docsPath: helpPagePath('user/group/value_stream_analytics', {
anchor: 'create-a-value-stream',
}),
};
</script>
<template>
<gl-alert
:title="$options.i18n.title"
:dismissible="false"
:primary-button-text="$options.i18n.primaryText"
:secondary-button-text="$options.i18n.secondaryText"
:secondary-button-link="$options.docsPath"
@primaryAction="$emit('reload')"
>
<p>{{ message }}</p>
<p>{{ $options.i18n.nextUpdate }}</p>
</gl-alert>
</template>
......@@ -70,3 +70,13 @@ export const EMPTY_STATE_FILTER_ERROR_TITLE = __(
export const EMPTY_STATE_FILTER_ERROR_DESCRIPTION = __(
'Filter parameters are not valid. Make sure that the end date is after the start date.',
);
export const AGGREGATING_DATA_WARNING_TITLE = s__('CycleAnalytics|Data is collecting and loading.');
export const AGGREGATING_DATA_WARNING_MESSAGE = s__(
"CycleAnalytics|'%{name}' is collecting the data. This can take a few minutes.",
);
export const AGGREGATING_DATA_WARNING_NEXT_UPDATE = s__(
'CycleAnalytics|If you have recently upgraded to GitLab Premium, it can take up to 30 minutes for data to collect and display.',
);
export const AGGREGATING_DATA_PRIMARY_ACTION_TEXT = __('Reload page');
export const AGGREGATING_DATA_SECONDARY_ACTION_TEXT = __('Learn more');
......@@ -38,6 +38,9 @@ export default () => {
selectedAssigneeList,
selectedLabelList,
pagination,
featureFlags: {
useVsaAggregatedTables: gon.features.useVsaAggregatedTables,
},
});
return new Vue({
......
......@@ -14,9 +14,14 @@ export const updateAggregation = ({ commit, getters }, status) => {
});
};
export const receiveCreateValueStreamSuccess = ({ commit, dispatch }, valueStream = {}) => {
export const receiveCreateValueStreamSuccess = (
{ commit, dispatch, state: { featureFlags } },
valueStream = {},
) => {
commit(types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, valueStream);
return dispatch('fetchCycleAnalyticsData');
return featureFlags.useVsaAggregatedTables
? commit(types.SET_CREATING_AGGREGATION, true)
: dispatch('fetchCycleAnalyticsData');
};
export const createValueStream = ({ commit, dispatch, getters }, data) => {
......
......@@ -6,6 +6,7 @@ export const SET_DATE_RANGE = 'SET_DATE_RANGE';
export const SET_SELECTED_VALUE_STREAM = 'SET_SELECTED_VALUE_STREAM';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_STAGE_EVENTS = 'SET_STAGE_EVENTS';
export const SET_CREATING_AGGREGATION = 'SET_CREATING_AGGREGATION';
export const REQUEST_VALUE_STREAM_DATA = 'REQUEST_VALUE_STREAM_DATA';
export const RECEIVE_VALUE_STREAM_DATA_SUCCESS = 'RECEIVE_VALUE_STREAM_DATA_SUCCESS';
......
......@@ -140,10 +140,13 @@ export default {
state.createValueStreamErrors = { ...rest, stages: prepareStageErrors(stages, stageErrors) };
state.isCreatingValueStream = false;
},
[types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS](state, valueStream) {
[types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS](state, valueStream = {}) {
state.isCreatingValueStream = false;
state.createValueStreamErrors = {};
state.selectedValueStream = convertObjectPropsToCamelCase(valueStream, { deep: true });
const { stages = [] } = valueStream;
state.stages = transformRawStages(stages);
},
[types.REQUEST_UPDATE_VALUE_STREAM](state) {
state.isEditingValueStream = true;
......@@ -209,4 +212,7 @@ export default {
[types.RECEIVE_UPDATE_AGGREGATION_ERROR](state) {
state.isUpdatingAggregation = false;
},
[types.SET_CREATING_AGGREGATION](state, value) {
state.isCreatingAggregation = value;
},
};
......@@ -29,6 +29,7 @@ export default () => ({
isDeletingValueStream: false,
isFetchingGroupLabels: false,
isUpdatingAggregation: false,
isCreatingAggregation: false,
createValueStreamErrors: {},
deleteValueStreamError: null,
......
......@@ -44,8 +44,7 @@ RSpec.describe 'Value stream analytics charts', :js do
context 'Duration chart' do
before do
select_group(group)
select_value_stream(custom_value_stream_name)
select_group_and_custom_value_stream(group, custom_value_stream_name)
end
it 'displays data for all stages on the overview' do
......@@ -87,8 +86,7 @@ RSpec.describe 'Value stream analytics charts', :js do
create(:labeled_issue, created_at: i.days.ago, project: create(:project, group: group), labels: [group_label2])
end
select_group(group)
select_value_stream(custom_value_stream_name)
select_group_and_custom_value_stream(group, custom_value_stream_name)
end
it 'displays the chart' do
......@@ -125,7 +123,7 @@ RSpec.describe 'Value stream analytics charts', :js do
context 'no data available' do
before do
select_group(group)
select_group_and_custom_value_stream(group, custom_value_stream_name)
end
it 'shows the no data available message' do
......
......@@ -12,6 +12,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
let_it_be(:sub_group_project) { create(:project, :repository, namespace: group, group: sub_group, name: 'Cool sub group project') }
let_it_be(:group_label1) { create(:group_label, group: group) }
let_it_be(:group_label2) { create(:group_label, group: group) }
let_it_be(:custom_value_stream_name) { "First custom value stream" }
let(:milestone) { create(:milestone, project: project) }
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
......@@ -191,10 +192,15 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
end
let_it_be(:issue) { create(:issue, project: project) }
let_it_be(:value_stream) { create(:cycle_analytics_group_value_stream, group: group, name: 'First value stream', stages: vsa_stages(group)) }
let_it_be(:value_stream) { create(:cycle_analytics_group_value_stream, group: group, name: custom_value_stream_name, stages: vsa_stages(group)) }
let_it_be(:subgroup_value_stream) { create(:cycle_analytics_group_value_stream, group: sub_group, name: 'First subgroup value stream', stages: vsa_stages(sub_group)) }
context 'without valid query parameters set' do
before do
create_value_stream_group_aggregation(group)
end
context 'with created_after date > created_before date' do
before do
visit "#{group_analytics_cycle_analytics_path(group)}?created_after=2019-12-31&created_before=2019-11-01"
......@@ -207,7 +213,8 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
before do
visit "#{group_analytics_cycle_analytics_path(group)}?beans=not-cool"
select_value_stream('First value stream')
select_value_stream(custom_value_stream_name)
select_stage("Issue")
end
......@@ -218,6 +225,10 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
context 'with valid query parameters set' do
projects_dropdown = '.js-projects-dropdown-filter'
before do
create_value_stream_group_aggregation(group)
end
context 'with project_ids set' do
before do
visit "#{group_analytics_cycle_analytics_path(group)}?project_ids[]=#{project.id}"
......@@ -250,8 +261,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
let(:selected_group) { group }
before do
select_group(group)
select_value_stream('First value stream')
select_group_and_custom_value_stream(group, custom_value_stream_name)
end
it_behaves_like 'group value stream analytics'
......@@ -265,8 +275,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
let(:selected_group) { sub_group }
before do
select_group(sub_group)
select_value_stream('First subgroup value stream')
select_group_and_custom_value_stream(sub_group, 'First subgroup value stream')
end
it_behaves_like 'group value stream analytics'
......@@ -293,11 +302,7 @@ RSpec.describe 'Group value stream analytics filters and data', :js do
deploy_master(user, project, environment: 'staging')
deploy_master(user, project)
aggregation = Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
Analytics::CycleAnalytics::AggregatorService.new(aggregation: aggregation).execute
select_group(group)
select_value_stream('First value stream')
select_group_and_custom_value_stream(group, custom_value_stream_name)
end
stages_with_data = [
......
......@@ -30,13 +30,6 @@ RSpec.describe 'Multiple value streams', :js do
sign_in(user)
end
def select_value_stream(value_stream_name)
toggle_value_stream_dropdown
page.find('[data-testid="dropdown-value-streams"]').all('li button').find { |item| item.text == value_stream_name.to_s }.click
wait_for_requests
end
def path_nav_elem
page.find('[data-testid="vsa-path-navigation"]')
end
......@@ -45,6 +38,19 @@ RSpec.describe 'Multiple value streams', :js do
page.find("[data-testid='stage-action-#{action}-#{index}']").click
end
def reload_value_stream
click_button 'Reload page'
end
def create_and_select_value_stream(name, with_aggregation = true)
create_custom_value_stream(name)
return unless with_aggregation
reload_value_stream
select_value_stream(name)
end
shared_examples 'create a value stream' do |custom_value_stream_name|
before do
toggle_value_stream_dropdown
......@@ -86,11 +92,11 @@ RSpec.describe 'Multiple value streams', :js do
end
end
shared_examples 'update a value stream' do |custom_value_stream_name|
shared_examples 'update a value stream' do |custom_value_stream_name, with_aggregation|
before do
select_group(group)
create_custom_value_stream(custom_value_stream_name)
create_and_select_value_stream(custom_value_stream_name, with_aggregation)
end
it 'can reorder stages' do
......@@ -198,7 +204,7 @@ RSpec.describe 'Multiple value streams', :js do
end
end
shared_examples 'create group value streams' do
shared_examples 'create group value streams' do |with_aggregation|
name = 'group value stream'
before do
......@@ -206,11 +212,11 @@ RSpec.describe 'Multiple value streams', :js do
end
it_behaves_like 'create a value stream', name
it_behaves_like 'update a value stream', name
it_behaves_like 'update a value stream', name, with_aggregation
it_behaves_like 'delete a value stream', name
end
shared_examples 'create sub group value streams' do
shared_examples 'create sub group value streams' do |with_aggregation|
name = 'sub group value stream'
before do
......@@ -218,7 +224,7 @@ RSpec.describe 'Multiple value streams', :js do
end
it_behaves_like 'create a value stream', name
it_behaves_like 'update a value stream', name
it_behaves_like 'update a value stream', name, with_aggregation
it_behaves_like 'delete a value stream', name
end
......@@ -227,8 +233,8 @@ RSpec.describe 'Multiple value streams', :js do
stub_feature_flags(use_vsa_aggregated_tables: false)
end
it_behaves_like 'create group value streams'
it_behaves_like 'create sub group value streams'
it_behaves_like 'create group value streams', false
it_behaves_like 'create sub group value streams', false
end
context 'use_vsa_aggregated_tables feature flag on' do
......@@ -249,14 +255,39 @@ RSpec.describe 'Multiple value streams', :js do
end
context 'with a value stream' do
before do
# ensure we have a value stream already available
create(:cycle_analytics_group_value_stream, group: group, name: 'default')
create(:cycle_analytics_group_value_stream, group: sub_group, name: 'default')
context 'without an aggregation created' do
before do
create(:cycle_analytics_group_value_stream, group: group, name: 'default')
select_group(group)
end
it 'renders the aggregating status banner' do
expect(page).to have_text(s_('CycleAnalytics|Data is collecting and loading.'))
end
it 'displays the value stream once an aggregation is run' do
create_value_stream_group_aggregation(group)
reload_value_stream
expect(page).not_to have_button(_('Reload page'))
expect(page).to have_text('Last updated less than a minute ago')
end
end
it_behaves_like 'create group value streams'
it_behaves_like 'create sub group value streams'
context 'with an aggregation created' do
before do
create_value_stream_group_aggregation(group)
create_value_stream_group_aggregation(sub_group)
# ensure we have a value stream already available
create(:cycle_analytics_group_value_stream, group: group, name: 'default')
create(:cycle_analytics_group_value_stream, group: sub_group, name: 'default')
end
it_behaves_like 'create group value streams', true
it_behaves_like 'create sub group value streams', true
end
end
end
end
......@@ -37,7 +37,9 @@ describe('Value Stream Analytics actions / value streams', () => {
beforeEach(() => {
state = {
stages: [],
featureFlags: {},
featureFlags: {
useVsaAggregatedTables: true,
},
activeStages,
selectedValueStream,
...mockGetters,
......@@ -50,6 +52,35 @@ describe('Value Stream Analytics actions / value streams', () => {
state = { ...state, currentGroup: null };
});
describe('useVsaAggregatedTables = false', () => {
beforeEach(() => {
state = {
...state,
featureFlags: {
useVsaAggregatedTables: false,
},
};
});
describe('receiveCreateValueStreamSuccess', () => {
beforeEach(() => {
state = { ...state, valueStream: {} };
});
it(`will dispatch the "fetchCycleAnalyticsData" action and commit the ${types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS} mutation`, () => {
return testAction({
action: actions.receiveCreateValueStreamSuccess,
payload: selectedValueStream,
state,
expectedMutations: [
{ type: types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, payload: selectedValueStream },
],
expectedActions: [{ type: 'fetchCycleAnalyticsData' }],
});
});
});
});
describe('setSelectedValueStream', () => {
const vs = { id: 'vs-1', name: 'Value stream 1' };
......@@ -141,8 +172,8 @@ describe('Value Stream Analytics actions / value streams', () => {
state,
expectedMutations: [
{ type: types.RECEIVE_CREATE_VALUE_STREAM_SUCCESS, payload: selectedValueStream },
{ type: types.SET_CREATING_AGGREGATION, payload: true },
],
expectedActions: [{ type: 'fetchCycleAnalyticsData' }],
});
});
});
......
......@@ -11152,6 +11152,9 @@ msgstr ""
msgid "CycleAnalytics|%{selectedLabelsCount} selected (%{maxLabels} max)"
msgstr ""
msgid "CycleAnalytics|'%{name}' is collecting the data. This can take a few minutes."
msgstr ""
msgid "CycleAnalytics|Aggregation disabled"
msgstr ""
......@@ -11170,6 +11173,9 @@ msgstr ""
msgid "CycleAnalytics|Custom value streams to measure your DevSecOps lifecycle"
msgstr ""
msgid "CycleAnalytics|Data is collecting and loading."
msgstr ""
msgid "CycleAnalytics|Date"
msgstr ""
......@@ -11179,6 +11185,9 @@ msgstr ""
msgid "CycleAnalytics|Filter by stop date"
msgstr ""
msgid "CycleAnalytics|If you have recently upgraded to GitLab Premium, it can take up to 30 minutes for data to collect and display."
msgstr ""
msgid "CycleAnalytics|Lead Time for Changes"
msgstr ""
......
......@@ -93,6 +93,18 @@ module CycleAnalyticsHelpers
wait_for_requests
end
def create_value_stream_group_aggregation(group)
aggregation = Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group)
Analytics::CycleAnalytics::AggregatorService.new(aggregation: aggregation).execute
end
def select_group_and_custom_value_stream(group, custom_value_stream_name)
create_value_stream_group_aggregation(group)
select_group(group)
select_value_stream(custom_value_stream_name)
end
def toggle_dropdown(field)
page.within("[data-testid*='#{field}']") do
find('.dropdown-toggle').click
......
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