Commit cdc80507 authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch '235733-mlunoe-clean-up-page-loading-computed-in-insights-analytics' into 'master'

Resolve "Clean up `pageLoading` computed in `insights.vue`"

Closes #235733

See merge request gitlab-org/gitlab!40096
parents 9d3d71b4 4ece674e
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapState } from 'vuex';
import { GlAlert, GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlLoadingIcon } from '@gitlab/ui'; import {
GlAlert,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlEmptyState,
GlLoadingIcon,
} from '@gitlab/ui';
import { EMPTY_STATE_TITLE, EMPTY_STATE_DESCRIPTION, EMPTY_STATE_SVG_PATH } from '../constants';
import InsightsPage from './insights_page.vue'; import InsightsPage from './insights_page.vue';
import InsightsConfigWarning from './insights_config_warning.vue';
export default { export default {
components: { components: {
GlAlert, GlAlert,
GlLoadingIcon, GlLoadingIcon,
InsightsPage, InsightsPage,
InsightsConfigWarning, GlEmptyState,
GlDeprecatedDropdown, GlDeprecatedDropdown,
GlDeprecatedDropdownItem, GlDeprecatedDropdownItem,
}, },
...@@ -36,15 +42,22 @@ export default { ...@@ -36,15 +42,22 @@ export default {
'activePage', 'activePage',
'chartData', 'chartData',
]), ]),
pageLoading() { emptyState() {
return {
title: EMPTY_STATE_TITLE,
description: EMPTY_STATE_DESCRIPTION,
svgPath: EMPTY_STATE_SVG_PATH,
};
},
hasAllChartsLoaded() {
const requestedChartKeys = this.activePage?.charts?.map(chart => chart.title) || []; const requestedChartKeys = this.activePage?.charts?.map(chart => chart.title) || [];
const storeChartKeys = Object.keys(this.chartData); return requestedChartKeys.every(key => this.chartData[key]?.loaded);
const loadedCharts = storeChartKeys.filter(key => this.chartData[key].loaded); },
const chartsLoaded = hasChartsError() {
Boolean(requestedChartKeys.length) && return Object.values(this.chartData).some(data => data.error);
requestedChartKeys.every(key => loadedCharts.includes(key)); },
const chartsErrored = storeChartKeys.some(key => this.chartData[key].error); pageLoading() {
return !chartsLoaded && !chartsErrored; return !this.hasChartsError && !this.hasAllChartsLoaded;
}, },
pages() { pages() {
const { configData, activeTab } = this; const { configData, activeTab } = this;
...@@ -138,15 +151,11 @@ export default { ...@@ -138,15 +151,11 @@ export default {
</gl-alert> </gl-alert>
<insights-page :query-endpoint="queryEndpoint" :page-config="activePage" /> <insights-page :query-endpoint="queryEndpoint" :page-config="activePage" />
</div> </div>
<insights-config-warning <gl-empty-state
v-else v-else
:title="__('Invalid Insights config file detected')" :title="emptyState.title"
:summary=" :description="emptyState.description"
__( :svg-path="emptyState.svgPath"
'Please check the configuration file to ensure that it is available and the YAML is valid',
)
"
image="illustrations/monitoring/getting_started.svg"
/> />
</div> </div>
</template> </template>
<script>
import { imagePath } from '~/lib/utils/common_utils';
export default {
props: {
image: {
type: String,
required: true,
},
title: {
type: String,
required: true,
},
summary: {
type: String,
required: true,
},
},
computed: {
imageSrc() {
return imagePath(this.image);
},
},
};
</script>
<template>
<div class="row js-empty-state empty-state">
<div class="col-12">
<div class="svg-content"><img class="content-image" :src="imageSrc" /></div>
</div>
<div class="col-12">
<div class="text-content">
<h4 class="content-title text-center">{{ title }}</h4>
<p class="content-summary">{{ summary }}</p>
</div>
</div>
</div>
</template>
import { __ } from '~/locale';
export const CHART_TYPES = { export const CHART_TYPES = {
BAR: 'bar', BAR: 'bar',
LINE: 'line', LINE: 'line',
...@@ -6,4 +8,8 @@ export const CHART_TYPES = { ...@@ -6,4 +8,8 @@ export const CHART_TYPES = {
PIE: 'pie', PIE: 'pie',
}; };
export default { CHART_TYPES }; export const EMPTY_STATE_TITLE = __('Invalid Insights config file detected');
export const EMPTY_STATE_DESCRIPTION = __(
'Please check the configuration file to ensure that it is available and the YAML is valid',
);
export const EMPTY_STATE_SVG_PATH = '/assets/illustrations/monitoring/getting_started.svg';
---
title: Fix issue where the select page dropdown would be disabled on the Insights
Analytics page when no charts were loaded.
merge_request: 40096
author:
type: fixed
import InsightsConfigWarning from 'ee/insights/components/insights_config_warning.vue';
import { shallowMount } from '@vue/test-utils';
describe('Insights config warning component', () => {
const image = 'illustrations/monitoring/getting_started.svg';
const title = 'There are no charts configured for this page';
const summary =
'Please check the configuration file to ensure that a collection of charts has been declared.';
let wrapper;
beforeEach(() => {
wrapper = shallowMount(InsightsConfigWarning, {
propsData: { image, title, summary },
});
});
afterEach(() => {
wrapper.destroy();
});
it('renders the component', () => {
expect(
wrapper
.findAll('.content-image')
.at(0)
.attributes('src'),
).toContain(image);
expect(wrapper.find('.content-title').text()).toBe(title);
expect(wrapper.find('.content-summary').text()).toBe(summary);
});
});
import Vue from 'vue'; import Vuex from 'vuex';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import Insights from 'ee/insights/components/insights.vue'; import Insights from 'ee/insights/components/insights.vue';
import { createStore } from 'ee/insights/stores'; import { createStore } from 'ee/insights/stores';
import createRouter from 'ee/insights/insights_router';
import { pageInfo } from 'ee_jest/insights/mock_data'; import { pageInfo } from 'ee_jest/insights/mock_data';
import { GlAlert, GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlEmptyState } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
const defaultMocks = {
$route: {
params: {},
},
$router: {
replace() {},
push() {},
},
};
const createComponent = (store, options = {}) => {
const { mocks = defaultMocks } = options;
return shallowMount(Insights, {
localVue,
store,
propsData: {
endpoint: TEST_HOST,
queryEndpoint: `${TEST_HOST}/query`,
},
stubs: ['router-link', 'router-view'],
mocks,
});
};
describe('Insights component', () => { describe('Insights component', () => {
let vm; let mock;
let store; let wrapper;
let mountComponent; let vuexStore;
const Component = Vue.extend(Insights);
const router = createRouter('');
beforeEach(() => { beforeEach(() => {
store = createStore(); mock = new MockAdapter(axios);
jest.spyOn(store, 'dispatch').mockImplementation(() => {}); vuexStore = createStore();
jest.spyOn(vuexStore, 'dispatch').mockImplementation(() => {});
mountComponent = data => { wrapper = createComponent(vuexStore);
const el = null;
const props = data || {
endpoint: TEST_HOST,
queryEndpoint: `${TEST_HOST}/query`,
};
return new Component({
store,
router,
propsData: props || {},
}).$mount(el);
};
vm = mountComponent();
}); });
afterEach(() => { afterEach(() => {
store.dispatch.mockReset(); mock.restore();
vm.$destroy(); vuexStore.dispatch.mockReset();
wrapper.destroy();
}); });
it('fetches config data when mounted', () => { it('fetches config data when mounted', () => {
expect(store.dispatch).toHaveBeenCalledWith('insights/fetchConfigData', TEST_HOST); expect(vuexStore.dispatch).toHaveBeenCalledWith('insights/fetchConfigData', TEST_HOST);
}); });
describe('when loading config', () => { describe('when loading config', () => {
it('renders config loading state', () => { it('renders config loading state', async () => {
vm.$store.state.insights.configLoading = true; vuexStore.state.insights.configLoading = true;
return vm.$nextTick(() => { expect(wrapper.contains('.insights-config-loading')).toBe(true);
expect(vm.$el.querySelector('.insights-config-loading')).not.toBe(null); expect(wrapper.contains('.insights-wrapper')).toBe(false);
expect(vm.$el.querySelector('.insights-wrapper')).toBe(null);
});
}); });
}); });
...@@ -59,35 +70,65 @@ describe('Insights component', () => { ...@@ -59,35 +70,65 @@ describe('Insights component', () => {
const chart1 = { title: 'foo' }; const chart1 = { title: 'foo' };
const chart2 = { title: 'bar' }; const chart2 = { title: 'bar' };
describe('when charts have not been initialized', () => { describe('when no charts have been requested', () => {
const page = { const page = {
title, title,
charts: [], charts: [],
}; };
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page; vuexStore.state.insights.activePage = page;
vm.$store.state.insights.configData = { vuexStore.state.insights.configData = {
bugsPerTeam: page, bugsPerTeam: page,
}; };
}); });
it('has the correct nav tabs', () => { it('has the correct nav tabs', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(vm.$el.querySelector('.js-insights-dropdown')).not.toBe(null); expect(wrapper.contains(GlDeprecatedDropdown)).toBe(true);
expect( expect(
vm.$el.querySelector('.js-insights-dropdown .dropdown-item').innerText.trim(), wrapper
).toBe(title); .find(GlDeprecatedDropdown)
}); .find(GlDeprecatedDropdownItem)
.text(),
).toBe(title);
}); });
it('disables the tab selector', () => { it('should not disable the tab selector', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect( expect(wrapper.find(GlDeprecatedDropdown).attributes().disabled).toBeUndefined();
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'), });
).toBe('disabled'); });
});
describe('when charts have not been initialized', () => {
const page = {
title,
charts: [chart1, chart2],
};
beforeEach(() => {
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.activePage = page;
vuexStore.state.insights.configData = {
bugsPerTeam: page,
};
});
it('has the correct nav tabs', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.contains(GlDeprecatedDropdown)).toBe(true);
expect(
wrapper
.find(GlDeprecatedDropdown)
.find(GlDeprecatedDropdownItem)
.text(),
).toBe(title);
});
it('disables the tab selector', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).attributes()).toMatchObject({ disabled: 'true' });
}); });
}); });
...@@ -98,23 +139,20 @@ describe('Insights component', () => { ...@@ -98,23 +139,20 @@ describe('Insights component', () => {
}; };
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page; vuexStore.state.insights.activePage = page;
vm.$store.state.insights.configData = { vuexStore.state.insights.configData = {
bugsPerTeam: page, bugsPerTeam: page,
}; };
vm.$store.state.insights.chartData = { vuexStore.state.insights.chartData = {
[chart1.title]: {}, [chart1.title]: {},
[chart2.title]: {}, [chart2.title]: {},
}; };
}); });
it('enables the tab selector', () => { it('enables the tab selector', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect( expect(wrapper.find(GlDeprecatedDropdown).attributes()).toMatchObject({ disabled: 'true' });
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe('disabled');
});
}); });
}); });
...@@ -125,22 +163,19 @@ describe('Insights component', () => { ...@@ -125,22 +163,19 @@ describe('Insights component', () => {
}; };
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page; vuexStore.state.insights.activePage = page;
vm.$store.state.insights.configData = { vuexStore.state.insights.configData = {
bugsPerTeam: page, bugsPerTeam: page,
}; };
vm.$store.state.insights.chartData = { vuexStore.state.insights.chartData = {
[chart2.title]: { loaded: true }, [chart2.title]: { loaded: true },
}; };
}); });
it('disables the tab selector', () => { it('disables the tab selector', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect( expect(wrapper.find(GlDeprecatedDropdown).attributes()).toMatchObject({ disabled: 'true' });
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe('disabled');
});
}); });
}); });
...@@ -151,23 +186,20 @@ describe('Insights component', () => { ...@@ -151,23 +186,20 @@ describe('Insights component', () => {
}; };
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page; vuexStore.state.insights.activePage = page;
vm.$store.state.insights.configData = { vuexStore.state.insights.configData = {
bugsPerTeam: page, bugsPerTeam: page,
}; };
vm.$store.state.insights.chartData = { vuexStore.state.insights.chartData = {
[chart1.title]: { loaded: true }, [chart1.title]: { loaded: true },
[chart2.title]: { loaded: true }, [chart2.title]: { loaded: true },
}; };
}); });
it('enables the tab selector', () => { it('enables the tab selector', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect( expect(wrapper.find(GlDeprecatedDropdown).attributes().disabled).toBeUndefined();
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe(null);
});
}); });
}); });
...@@ -178,66 +210,59 @@ describe('Insights component', () => { ...@@ -178,66 +210,59 @@ describe('Insights component', () => {
}; };
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page; vuexStore.state.insights.activePage = page;
vm.$store.state.insights.configData = { vuexStore.state.insights.configData = {
bugsPerTeam: page, bugsPerTeam: page,
}; };
vm.$store.state.insights.chartData = { vuexStore.state.insights.chartData = {
[chart1.title]: { error: 'Baz' }, [chart1.title]: { error: 'Baz' },
[chart2.title]: { loaded: true }, [chart2.title]: { loaded: true },
}; };
}); });
it('enables the tab selector', () => { it('enables the tab selector', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect( expect(wrapper.find(GlDeprecatedDropdown).attributes().disabled).toBeUndefined();
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe(null);
});
}); });
}); });
}); });
describe('empty config', () => { describe('empty config', () => {
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.configData = null; vuexStore.state.insights.configData = null;
}); });
it('it displays a warning', () => { it('it displays a warning', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(vm.$el.querySelector('.js-empty-state').innerText.trim()).toContain( expect(wrapper.find(GlEmptyState).attributes()).toMatchObject({
'Invalid Insights config file detected', title: 'Invalid Insights config file detected',
);
}); });
}); });
it('does not display dropdown', () => { it('does not display dropdown', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(vm.$el.querySelector('.js-insights-dropdown > button')).toBe(null); expect(wrapper.find(GlDeprecatedDropdown).exists()).toBe(false);
});
}); });
}); });
describe('filtered out items', () => { describe('filtered out items', () => {
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.configData = {}; vuexStore.state.insights.configData = {};
}); });
it('it displays a warning', () => { it('it displays a warning', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(vm.$el.querySelector('.gl-alert-body').innerText.trim()).toContain( expect(wrapper.find(GlAlert).text()).toContain(
'This project is filtered out in the insights.yml file', 'This project is filtered out in the insights.yml file',
); );
});
}); });
it('does not display dropdown', () => { it('does not display dropdown', async () => {
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(vm.$el.querySelector('.js-insights-dropdown > button')).toBe(null); expect(wrapper.find(GlDeprecatedDropdown).exists()).toBe(false);
});
}); });
}); });
...@@ -250,33 +275,41 @@ describe('Insights component', () => { ...@@ -250,33 +275,41 @@ describe('Insights component', () => {
configData[selectedKey] = {}; configData[selectedKey] = {};
beforeEach(() => { beforeEach(() => {
vm.$store.state.insights.configLoading = false; vuexStore.state.insights.configLoading = false;
vm.$store.state.insights.configData = configData; vuexStore.state.insights.configData = configData;
vm.$store.state.insights.activePage = pageInfo; vuexStore.state.insights.activePage = pageInfo;
});
afterEach(() => {
window.location.hash = '';
}); });
it('selects the first tab if invalid', () => { it('selects the first tab if invalid', async () => {
window.location.hash = '#/invalid'; const mocks = {
$route: {
params: {
tabId: 'invalid',
},
},
};
wrapper = createComponent(vuexStore, { mocks: { ...defaultMocks, ...mocks } });
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('insights/setActiveTab', defaultKey); expect(vuexStore.dispatch).toHaveBeenCalledWith('insights/setActiveTab', defaultKey);
});
}); });
it('selects the specified tab if valid', () => { it('selects the specified tab if valid', async () => {
window.location.hash = `#/${selectedKey}`; const mocks = {
$route: {
params: {
tabId: selectedKey,
},
},
};
wrapper = createComponent(vuexStore, { mocks: { ...defaultMocks, ...mocks } });
jest.runOnlyPendingTimers(); jest.runOnlyPendingTimers();
return vm.$nextTick(() => { await wrapper.vm.$nextTick();
expect(store.dispatch).toHaveBeenCalledWith('insights/setActiveTab', selectedKey); expect(vuexStore.dispatch).toHaveBeenCalledWith('insights/setActiveTab', selectedKey);
});
}); });
}); });
}); });
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