diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue index 7bc4b05546fb72cdd9d39ae20c4eb41d4bc50130..3832896261512a2129332f1e4d8df5112aff34d8 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/components/base.vue @@ -1,7 +1,7 @@ <script> // eslint-disable-next-line no-restricted-imports import { mapActions, mapState, mapGetters } from 'vuex'; -import { GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; import { VSA_METRICS_GROUPS, FLOW_METRICS_QUERY_TYPE } from '~/analytics/shared/constants'; import { @@ -28,6 +28,7 @@ export default { PageHeading, DurationChartLoader, GlEmptyState, + GlLoadingIcon, TypeOfWorkChartsLoader, StageTable, PathNavigation, @@ -98,9 +99,6 @@ export default { isWaitingForNextAggregation() { return Boolean(this.selectedValueStream && !this.aggregation.lastRunAt); }, - shouldRenderEmptyState() { - return this.isLoadingValueStreams || !this.hasValueStreams; - }, shouldRenderAggregationWarning() { return this.isWaitingForNextAggregation; }, @@ -110,13 +108,6 @@ export default { selectedStageReady() { return !this.hasNoAccessError && this.selectedStage; }, - shouldDisplayCreateMultipleValueStreams() { - return Boolean( - this.enableCustomizableStages && - !this.shouldRenderEmptyState && - !this.isLoadingValueStreams, - ); - }, hasDateRangeSet() { return this.createdAfter && this.createdBefore; }, @@ -212,9 +203,11 @@ export default { </script> <template> <div> + <div v-if="isLoadingValueStreams" class="gl-p-7 gl-text-center"> + <gl-loading-icon size="lg" /> + </div> <value-stream-empty-state - v-if="shouldRenderEmptyState" - :is-loading="isLoadingValueStreams" + v-else-if="!hasValueStreams" :empty-state-svg-path="emptyStateSvgPath" :has-date-range-error="!hasDateRangeSet" :can-edit="canEdit" @@ -224,7 +217,7 @@ export default { <div class="gl-mb-6 gl-flex gl-flex-col gl-justify-between gl-gap-3 sm:gl-flex-row sm:gl-items-center" > - <value-stream-select v-if="shouldDisplayCreateMultipleValueStreams" :can-edit="canEdit" /> + <value-stream-select v-if="enableCustomizableStages" :can-edit="canEdit" /> <value-stream-aggregation-status v-if="isAggregationStatusAvailable" :data="aggregation" /> </div> <value-stream-filters diff --git a/ee/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_empty_state.vue b/ee/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_empty_state.vue index acd390919e87dbabc19124dc02a304cb345685c2..fa4e02efed95d3e6bddf02d0b3506e51cbdb3329 100644 --- a/ee/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_empty_state.vue +++ b/ee/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_empty_state.vue @@ -1,5 +1,5 @@ <script> -import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlEmptyState } from '@gitlab/ui'; import { helpPagePath } from '~/helpers/help_page_helper'; import Tracking from '~/tracking'; import { @@ -16,16 +16,10 @@ export default { components: { GlButton, GlEmptyState, - GlLoadingIcon, }, mixins: [Tracking.mixin()], inject: ['newValueStreamPath'], props: { - isLoading: { - type: Boolean, - required: true, - default: false, - }, hasDateRangeError: { type: Boolean, required: true, @@ -67,31 +61,25 @@ export default { }; </script> <template> - <div> - <div v-if="isLoading" class="gl-p-7 gl-text-center"> - <gl-loading-icon size="lg" /> - </div> - <gl-empty-state - v-else - :svg-path="emptyStateSvgPath" - :title="title" - :description="description" - data-testid="vsa-empty-state" - > - <template v-if="!hasDateRangeError && canEdit" #actions> - <gl-button - :href="newValueStreamPath" - class="gl-mx-2 gl-mb-3" - variant="confirm" - data-testid="create-value-stream-button" - data-track-action="click_button" - data-track-label="empty_state_create_value_stream_form_open" - >{{ $options.i18n.EMPTY_STATE_ACTION_TEXT }}</gl-button - > - <gl-button class="gl-mx-2 gl-mb-3" data-testid="learn-more-link" :href="$options.docsPath" - >{{ $options.i18n.EMPTY_STATE_SECONDARY_TEXT }} - </gl-button> - </template> - </gl-empty-state> - </div> + <gl-empty-state + :svg-path="emptyStateSvgPath" + :title="title" + :description="description" + data-testid="vsa-empty-state" + > + <template v-if="!hasDateRangeError && canEdit" #actions> + <gl-button + :href="newValueStreamPath" + class="gl-mx-2 gl-mb-3" + variant="confirm" + data-testid="create-value-stream-button" + data-track-action="click_button" + data-track-label="empty_state_create_value_stream_form_open" + >{{ $options.i18n.EMPTY_STATE_ACTION_TEXT }}</gl-button + > + <gl-button class="gl-mx-2 gl-mb-3" data-testid="learn-more-link" :href="$options.docsPath" + >{{ $options.i18n.EMPTY_STATE_SECONDARY_TEXT }} + </gl-button> + </template> + </gl-empty-state> </template> diff --git a/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js b/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js index 5eefb42209b6a0e0a96ed5971c521ab1d61e589c..ccc897ef68dbf9402808d1d467bd7cc847c52989 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/components/base_spec.js @@ -1,4 +1,4 @@ -import { GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount, mount } from '@vue/test-utils'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; @@ -11,7 +11,10 @@ import TypeOfWorkChartsLoader from 'ee/analytics/cycle_analytics/components/task 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 ValueStreamEmptyState from 'ee/analytics/cycle_analytics/components/value_stream_empty_state.vue'; -import createStore from 'ee/analytics/cycle_analytics/store'; +import * as actions from 'ee/analytics/cycle_analytics/store/actions'; +import * as getters from 'ee/analytics/cycle_analytics/store/getters'; +import mutations from 'ee/analytics/cycle_analytics/store/mutations'; +import state from 'ee/analytics/cycle_analytics/store/state'; import waitForPromises from 'helpers/wait_for_promises'; import { currentGroup, @@ -22,6 +25,7 @@ import { initialPaginationQuery, selectedProjects as rawSelectedProjects, } from 'jest/analytics/cycle_analytics/mock_data'; +import filters from '~/vue_shared/components/filtered_search_bar/store/modules/filters'; import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue'; import { toYmd } from '~/analytics/shared/utils'; import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue'; @@ -137,18 +141,32 @@ describe('EE Value Stream Analytics component', () => { withStageSelected = false, features = {}, initialState = initialCycleAnalyticsState, + initializeStore = true, + stateOverrides = {}, props = {}, selectedStage = null, } = options; - store = createStore(); - await store.dispatch('initializeCycleAnalytics', { - ...initialState, - features: { - ...features, + store = new Vuex.Store({ + actions, + getters, + mutations, + state: { + ...state(), + ...stateOverrides, }, + modules: { filters }, }); + if (initializeStore) { + await store.dispatch('initializeCycleAnalytics', { + ...initialState, + features: { + ...features, + }, + }); + } + const func = shallow ? shallowMount : mount; const comp = func(Component, { store, @@ -190,16 +208,32 @@ describe('EE Value Stream Analytics component', () => { const findValueStreamSelect = () => wrapper.findComponent(ValueStreamSelect); const findUrlSync = () => wrapper.findComponent(UrlSync); - describe('with no value streams', () => { + beforeEach(() => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('when loading', () => { beforeEach(async () => { - mock = new MockAdapter(axios); wrapper = await createComponent({ - initialState: { ...initialCycleAnalyticsState, valueStreams: [] }, + initializeStore: false, + stateOverrides: { isLoadingValueStreams: true }, }); }); - afterEach(() => { - mock.restore(); + it('displays the loading icon', () => { + expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); + }); + }); + + describe('with no value streams', () => { + beforeEach(async () => { + wrapper = await createComponent({ + initialState: { ...initialCycleAnalyticsState, valueStreams: [] }, + }); }); it('displays an empty state', () => { @@ -225,7 +259,6 @@ describe('EE Value Stream Analytics component', () => { describe('the user does not have access to the group', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ @@ -260,15 +293,10 @@ describe('EE Value Stream Analytics component', () => { describe('the user has access to the group', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ withStageSelected: true }); }); - afterEach(() => { - mock.restore(); - }); - it('hides the empty state', () => { expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false); }); @@ -301,7 +329,6 @@ describe('EE Value Stream Analytics component', () => { describe('without the overview stage selected', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock, { selectedStageEvents: [{}] }); wrapper = await createComponent({ selectedStage: issueStage }); }); @@ -331,7 +358,6 @@ describe('EE Value Stream Analytics component', () => { describe('without issue events', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock, { selectedStageEvents: [] }); wrapper = await createComponent({ selectedStage: issueStage }); }); @@ -378,14 +404,9 @@ describe('EE Value Stream Analytics component', () => { describe('with failed requests while loading', () => { beforeEach(() => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); }); - afterEach(() => { - mock.restore(); - }); - it('will display an error if the fetchGroupStagesAndEvents request fails', async () => { expect(createAlert).not.toHaveBeenCalled(); @@ -425,14 +446,9 @@ describe('EE Value Stream Analytics component', () => { const overviewStage = { id: OVERVIEW_STAGE_ID, title: 'Overview' }; beforeEach(() => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); }); - afterEach(() => { - mock.restore(); - }); - it('when a stage is selected', async () => { wrapper = await createComponent(); @@ -477,14 +493,9 @@ describe('EE Value Stream Analytics component', () => { commonUtils.historyPushState = jest.fn(); urlUtils.mergeUrlParams = jest.fn(); - mock = new MockAdapter(axios); mockRequiredRoutes(mock); }); - afterEach(() => { - mock.restore(); - }); - describe('with minimal parameters set', () => { beforeEach(async () => { wrapper = await createComponent(); @@ -558,14 +569,9 @@ describe('EE Value Stream Analytics component', () => { describe('with`groupLevelAnalyticsDashboard=true`', () => { beforeEach(() => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); }); - afterEach(() => { - mock.restore(); - }); - it('renders a link to the value streams dashboard', async () => { wrapper = await createComponent({ withStageSelected: true, @@ -584,7 +590,6 @@ describe('EE Value Stream Analytics component', () => { describe('with `enableTasksByTypeChart=false`', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ withStageSelected: true, @@ -594,10 +599,6 @@ describe('EE Value Stream Analytics component', () => { }); }); - afterEach(() => { - mock.restore(); - }); - it('does not display the tasks by type chart', () => { expect(findTypeOfWorkCharts().exists()).toBe(false); }); @@ -605,7 +606,6 @@ describe('EE Value Stream Analytics component', () => { describe('with `enableCustomizableStages=false`', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ withStageSelected: true, @@ -617,10 +617,6 @@ describe('EE Value Stream Analytics component', () => { }); }); - afterEach(() => { - mock.restore(); - }); - it('does not display the value stream selector', () => { expect(findValueStreamSelect().exists()).toBe(false); }); @@ -628,7 +624,6 @@ describe('EE Value Stream Analytics component', () => { describe('with `enableProjectsFilter=false`', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ withStageSelected: true, @@ -640,10 +635,6 @@ describe('EE Value Stream Analytics component', () => { }); }); - afterEach(() => { - mock.restore(); - }); - it('does not display the project filter', () => { expect(findFilterBar().props('hasProjectFilter')).toBe(false); }); @@ -651,7 +642,6 @@ describe('EE Value Stream Analytics component', () => { describe('with a project namespace', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ withStageSelected: true, @@ -666,10 +656,6 @@ describe('EE Value Stream Analytics component', () => { }); }); - afterEach(() => { - mock.restore(); - }); - it('renders a link to the value streams dashboard', () => { expect(findOverviewMetrics().props('dashboardsPath')).toBe( '/some/cool/path/-/analytics/dashboards/value_streams_dashboard', @@ -679,7 +665,6 @@ describe('EE Value Stream Analytics component', () => { describe('when dashboard link is disabled for the project namespace`', () => { beforeEach(async () => { - mock = new MockAdapter(axios); mockRequiredRoutes(mock); wrapper = await createComponent({ withStageSelected: true, @@ -694,10 +679,6 @@ describe('EE Value Stream Analytics component', () => { }); }); - afterEach(() => { - mock.restore(); - }); - it('does not render a link to the value streams dashboard', () => { expect(findOverviewMetrics().props('dashboardsPath')).toBeNull(); }); diff --git a/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_empty_state_spec.js b/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_empty_state_spec.js index 5942a768513c31b0dfbad46637e714ba0f8ddb0a..0f558ba534758904463161c498bcb40aba3d86da 100644 --- a/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_empty_state_spec.js +++ b/ee/spec/frontend/analytics/cycle_analytics/components/value_stream_empty_state_spec.js @@ -1,4 +1,4 @@ -import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import ValueStreamEmptyState from 'ee/analytics/cycle_analytics/components/value_stream_empty_state.vue'; import { @@ -20,7 +20,6 @@ describe('ValueStreamEmptyState', () => { wrapper = shallowMountExtended(ValueStreamEmptyState, { propsData: { emptyStateSvgPath, - isLoading: false, hasDateRangeError: false, canEdit: true, ...props, @@ -44,10 +43,6 @@ describe('ValueStreamEmptyState', () => { createComponent(); }); - it('does not render the loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); - }); - it('renders the empty state title message', () => { expect(findTitle()).toEqual(EMPTY_STATE_TITLE); }); @@ -89,20 +84,6 @@ describe('ValueStreamEmptyState', () => { }); }); - describe('isLoading = true', () => { - beforeEach(() => { - createComponent({ - props: { - isLoading: true, - }, - }); - }); - - it('renders the loading icon', () => { - expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true); - }); - }); - describe('hasDateRangeError = true', () => { beforeEach(() => { createComponent({