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>
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 InsightsConfigWarning from './insights_config_warning.vue';
export default {
components: {
GlAlert,
GlLoadingIcon,
InsightsPage,
InsightsConfigWarning,
GlEmptyState,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
},
......@@ -36,15 +42,22 @@ export default {
'activePage',
'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 storeChartKeys = Object.keys(this.chartData);
const loadedCharts = storeChartKeys.filter(key => this.chartData[key].loaded);
const chartsLoaded =
Boolean(requestedChartKeys.length) &&
requestedChartKeys.every(key => loadedCharts.includes(key));
const chartsErrored = storeChartKeys.some(key => this.chartData[key].error);
return !chartsLoaded && !chartsErrored;
return requestedChartKeys.every(key => this.chartData[key]?.loaded);
},
hasChartsError() {
return Object.values(this.chartData).some(data => data.error);
},
pageLoading() {
return !this.hasChartsError && !this.hasAllChartsLoaded;
},
pages() {
const { configData, activeTab } = this;
......@@ -138,15 +151,11 @@ export default {
</gl-alert>
<insights-page :query-endpoint="queryEndpoint" :page-config="activePage" />
</div>
<insights-config-warning
<gl-empty-state
v-else
:title="__('Invalid Insights config file detected')"
:summary="
__(
'Please check the configuration file to ensure that it is available and the YAML is valid',
)
"
image="illustrations/monitoring/getting_started.svg"
:title="emptyState.title"
:description="emptyState.description"
:svg-path="emptyState.svgPath"
/>
</div>
</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 = {
BAR: 'bar',
LINE: 'line',
......@@ -6,4 +8,8 @@ export const CHART_TYPES = {
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 Insights from 'ee/insights/components/insights.vue';
import { createStore } from 'ee/insights/stores';
import createRouter from 'ee/insights/insights_router';
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', () => {
let vm;
let store;
let mountComponent;
const Component = Vue.extend(Insights);
const router = createRouter('');
let mock;
let wrapper;
let vuexStore;
beforeEach(() => {
store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation(() => {});
mountComponent = data => {
const el = null;
const props = data || {
endpoint: TEST_HOST,
queryEndpoint: `${TEST_HOST}/query`,
};
return new Component({
store,
router,
propsData: props || {},
}).$mount(el);
};
vm = mountComponent();
mock = new MockAdapter(axios);
vuexStore = createStore();
jest.spyOn(vuexStore, 'dispatch').mockImplementation(() => {});
wrapper = createComponent(vuexStore);
});
afterEach(() => {
store.dispatch.mockReset();
vm.$destroy();
mock.restore();
vuexStore.dispatch.mockReset();
wrapper.destroy();
});
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', () => {
it('renders config loading state', () => {
vm.$store.state.insights.configLoading = true;
it('renders config loading state', async () => {
vuexStore.state.insights.configLoading = true;
return vm.$nextTick(() => {
expect(vm.$el.querySelector('.insights-config-loading')).not.toBe(null);
expect(vm.$el.querySelector('.insights-wrapper')).toBe(null);
});
expect(wrapper.contains('.insights-config-loading')).toBe(true);
expect(wrapper.contains('.insights-wrapper')).toBe(false);
});
});
......@@ -59,35 +70,65 @@ describe('Insights component', () => {
const chart1 = { title: 'foo' };
const chart2 = { title: 'bar' };
describe('when charts have not been initialized', () => {
describe('when no charts have been requested', () => {
const page = {
title,
charts: [],
};
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page;
vm.$store.state.insights.configData = {
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.activePage = page;
vuexStore.state.insights.configData = {
bugsPerTeam: page,
};
});
it('has the correct nav tabs', () => {
return vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-insights-dropdown')).not.toBe(null);
expect(
vm.$el.querySelector('.js-insights-dropdown .dropdown-item').innerText.trim(),
).toBe(title);
});
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', () => {
return vm.$nextTick(() => {
expect(
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe('disabled');
});
it('should not disable the tab selector', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).attributes().disabled).toBeUndefined();
});
});
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', () => {
};
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page;
vm.$store.state.insights.configData = {
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.activePage = page;
vuexStore.state.insights.configData = {
bugsPerTeam: page,
};
vm.$store.state.insights.chartData = {
vuexStore.state.insights.chartData = {
[chart1.title]: {},
[chart2.title]: {},
};
});
it('enables the tab selector', () => {
return vm.$nextTick(() => {
expect(
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe('disabled');
});
it('enables the tab selector', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).attributes()).toMatchObject({ disabled: 'true' });
});
});
......@@ -125,22 +163,19 @@ describe('Insights component', () => {
};
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page;
vm.$store.state.insights.configData = {
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.activePage = page;
vuexStore.state.insights.configData = {
bugsPerTeam: page,
};
vm.$store.state.insights.chartData = {
vuexStore.state.insights.chartData = {
[chart2.title]: { loaded: true },
};
});
it('disables the tab selector', () => {
return vm.$nextTick(() => {
expect(
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe('disabled');
});
it('disables the tab selector', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).attributes()).toMatchObject({ disabled: 'true' });
});
});
......@@ -151,23 +186,20 @@ describe('Insights component', () => {
};
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page;
vm.$store.state.insights.configData = {
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.activePage = page;
vuexStore.state.insights.configData = {
bugsPerTeam: page,
};
vm.$store.state.insights.chartData = {
vuexStore.state.insights.chartData = {
[chart1.title]: { loaded: true },
[chart2.title]: { loaded: true },
};
});
it('enables the tab selector', () => {
return vm.$nextTick(() => {
expect(
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe(null);
});
it('enables the tab selector', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).attributes().disabled).toBeUndefined();
});
});
......@@ -178,66 +210,59 @@ describe('Insights component', () => {
};
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.activePage = page;
vm.$store.state.insights.configData = {
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.activePage = page;
vuexStore.state.insights.configData = {
bugsPerTeam: page,
};
vm.$store.state.insights.chartData = {
vuexStore.state.insights.chartData = {
[chart1.title]: { error: 'Baz' },
[chart2.title]: { loaded: true },
};
});
it('enables the tab selector', () => {
return vm.$nextTick(() => {
expect(
vm.$el.querySelector('.js-insights-dropdown > button').getAttribute('disabled'),
).toBe(null);
});
it('enables the tab selector', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).attributes().disabled).toBeUndefined();
});
});
});
describe('empty config', () => {
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.configData = null;
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.configData = null;
});
it('it displays a warning', () => {
return vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-empty-state').innerText.trim()).toContain(
'Invalid Insights config file detected',
);
it('it displays a warning', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlEmptyState).attributes()).toMatchObject({
title: 'Invalid Insights config file detected',
});
});
it('does not display dropdown', () => {
return vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-insights-dropdown > button')).toBe(null);
});
it('does not display dropdown', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).exists()).toBe(false);
});
});
describe('filtered out items', () => {
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.configData = {};
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.configData = {};
});
it('it displays a warning', () => {
return vm.$nextTick(() => {
expect(vm.$el.querySelector('.gl-alert-body').innerText.trim()).toContain(
'This project is filtered out in the insights.yml file',
);
});
it('it displays a warning', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlAlert).text()).toContain(
'This project is filtered out in the insights.yml file',
);
});
it('does not display dropdown', () => {
return vm.$nextTick(() => {
expect(vm.$el.querySelector('.js-insights-dropdown > button')).toBe(null);
});
it('does not display dropdown', async () => {
await wrapper.vm.$nextTick();
expect(wrapper.find(GlDeprecatedDropdown).exists()).toBe(false);
});
});
......@@ -250,33 +275,41 @@ describe('Insights component', () => {
configData[selectedKey] = {};
beforeEach(() => {
vm.$store.state.insights.configLoading = false;
vm.$store.state.insights.configData = configData;
vm.$store.state.insights.activePage = pageInfo;
});
afterEach(() => {
window.location.hash = '';
vuexStore.state.insights.configLoading = false;
vuexStore.state.insights.configData = configData;
vuexStore.state.insights.activePage = pageInfo;
});
it('selects the first tab if invalid', () => {
window.location.hash = '#/invalid';
it('selects the first tab if invalid', async () => {
const mocks = {
$route: {
params: {
tabId: 'invalid',
},
},
};
wrapper = createComponent(vuexStore, { mocks: { ...defaultMocks, ...mocks } });
jest.runOnlyPendingTimers();
return vm.$nextTick(() => {
expect(store.dispatch).toHaveBeenCalledWith('insights/setActiveTab', defaultKey);
});
await wrapper.vm.$nextTick();
expect(vuexStore.dispatch).toHaveBeenCalledWith('insights/setActiveTab', defaultKey);
});
it('selects the specified tab if valid', () => {
window.location.hash = `#/${selectedKey}`;
it('selects the specified tab if valid', async () => {
const mocks = {
$route: {
params: {
tabId: selectedKey,
},
},
};
wrapper = createComponent(vuexStore, { mocks: { ...defaultMocks, ...mocks } });
jest.runOnlyPendingTimers();
return vm.$nextTick(() => {
expect(store.dispatch).toHaveBeenCalledWith('insights/setActiveTab', selectedKey);
});
await wrapper.vm.$nextTick();
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