Commit 236f031d authored by Miguel Rincon's avatar Miguel Rincon

Add a link in dashboard to go to the logs page

- Add time range to the dashboard stores to allow inner components to
create a link to the current time range.
- Have the logs page read time parameters from the URL.
- Add link in panel type component to the logs page.
- Update embed function so logs path is checked for
- Improve and extend specs for panel type
parent bbcd604f
...@@ -21,7 +21,6 @@ import createFlash from '~/flash'; ...@@ -21,7 +21,6 @@ import createFlash from '~/flash';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility'; import { mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import Icon from '~/vue_shared/components/icon.vue'; import Icon from '~/vue_shared/components/icon.vue';
import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
...@@ -102,6 +101,11 @@ export default { ...@@ -102,6 +101,11 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
logsPath: {
type: String,
required: false,
default: invalidUrl,
},
defaultBranch: { defaultBranch: {
type: String, type: String,
required: true, required: true,
...@@ -247,22 +251,20 @@ export default { ...@@ -247,22 +251,20 @@ export default {
dashboardsEndpoint: this.dashboardsEndpoint, dashboardsEndpoint: this.dashboardsEndpoint,
currentDashboard: this.currentDashboard, currentDashboard: this.currentDashboard,
projectPath: this.projectPath, projectPath: this.projectPath,
logsPath: this.logsPath,
}); });
}, },
mounted() { mounted() {
if (!this.hasMetrics) { if (!this.hasMetrics) {
this.setGettingStartedEmptyState(); this.setGettingStartedEmptyState();
} else { } else {
const { start, end } = convertToFixedRange(this.selectedTimeRange); this.setTimeRange(this.selectedTimeRange);
this.fetchData();
this.fetchData({
start,
end,
});
} }
}, },
methods: { methods: {
...mapActions('monitoringDashboard', [ ...mapActions('monitoringDashboard', [
'setTimeRange',
'fetchData', 'fetchData',
'setGettingStartedEmptyState', 'setGettingStartedEmptyState',
'setEndpoints', 'setEndpoints',
......
...@@ -19,14 +19,8 @@ export default { ...@@ -19,14 +19,8 @@ export default {
}, },
data() { data() {
const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange; const timeRange = timeRangeFromUrl(this.dashboardUrl) || defaultTimeRange;
const { start, end } = convertToFixedRange(timeRange);
const params = {
start,
end,
};
return { return {
params, timeRange: convertToFixedRange(timeRange),
elWidth: 0, elWidth: 0,
}; };
}, },
...@@ -49,7 +43,9 @@ export default { ...@@ -49,7 +43,9 @@ export default {
}, },
mounted() { mounted() {
this.setInitialState(); this.setInitialState();
this.fetchMetricsData(this.params); this.setTimeRange(this.timeRange);
this.fetchDashboard();
sidebarMutationObserver = new MutationObserver(this.onSidebarMutation); sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
sidebarMutationObserver.observe(document.querySelector('.layout-page'), { sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
attributes: true, attributes: true,
...@@ -64,7 +60,8 @@ export default { ...@@ -64,7 +60,8 @@ export default {
}, },
methods: { methods: {
...mapActions('monitoringDashboard', [ ...mapActions('monitoringDashboard', [
'fetchMetricsData', 'setTimeRange',
'fetchDashboard',
'setEndpoints', 'setEndpoints',
'setFeatureFlags', 'setFeatureFlags',
'setShowErrorBanner', 'setShowErrorBanner',
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import { import {
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
...@@ -18,7 +19,7 @@ import MonitorColumnChart from './charts/column.vue'; ...@@ -18,7 +19,7 @@ import MonitorColumnChart from './charts/column.vue';
import MonitorStackedColumnChart from './charts/stacked_column.vue'; import MonitorStackedColumnChart from './charts/stacked_column.vue';
import MonitorEmptyChart from './charts/empty_chart.vue'; import MonitorEmptyChart from './charts/empty_chart.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import { downloadCSVOptions, generateLinkToChartOptions } from '../utils'; import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
export default { export default {
components: { components: {
...@@ -59,7 +60,7 @@ export default { ...@@ -59,7 +60,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState('monitoringDashboard', ['deploymentData', 'projectPath']), ...mapState('monitoringDashboard', ['deploymentData', 'projectPath', 'logsPath', 'timeRange']),
alertWidgetAvailable() { alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData; return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint && this.graphData;
}, },
...@@ -70,6 +71,12 @@ export default { ...@@ -70,6 +71,12 @@ export default {
this.graphData.metrics[0].result.length > 0 this.graphData.metrics[0].result.length > 0
); );
}, },
logsPathWithTimeRange() {
if (this.logsPath && this.logsPath !== invalidUrl && this.timeRange) {
return timeRangeToUrl(this.timeRange, this.logsPath);
}
return null;
},
csvText() { csvText() {
const chartData = this.graphData.metrics[0].result[0].values; const chartData = this.graphData.metrics[0].result[0].values;
const yLabel = this.graphData.y_label; const yLabel = this.graphData.y_label;
...@@ -157,6 +164,15 @@ export default { ...@@ -157,6 +164,15 @@ export default {
<template slot="button-content"> <template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" /> <icon name="ellipsis_v" class="text-secondary" />
</template> </template>
<gl-dropdown-item
v-if="logsPathWithTimeRange"
ref="viewLogsLink"
:href="logsPathWithTimeRange"
>
{{ s__('Metrics|View logs') }}
</gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)" v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv" :href="downloadCsv"
......
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils'; import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper'; import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql'; import getEnvironments from '../queries/getEnvironments.query.graphql';
...@@ -32,6 +33,10 @@ export const setEndpoints = ({ commit }, endpoints) => { ...@@ -32,6 +33,10 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints); commit(types.SET_ENDPOINTS, endpoints);
}; };
export const setTimeRange = ({ commit }, timeRange) => {
commit(types.SET_TIME_RANGE, timeRange);
};
export const filterEnvironments = ({ commit, dispatch }, searchTerm) => { export const filterEnvironments = ({ commit, dispatch }, searchTerm) => {
commit(types.SET_ENVIRONMENTS_FILTER, searchTerm); commit(types.SET_ENVIRONMENTS_FILTER, searchTerm);
dispatch('fetchEnvironmentsData'); dispatch('fetchEnvironmentsData');
...@@ -63,19 +68,24 @@ export const receiveEnvironmentsDataSuccess = ({ commit }, data) => ...@@ -63,19 +68,24 @@ export const receiveEnvironmentsDataSuccess = ({ commit }, data) =>
export const receiveEnvironmentsDataFailure = ({ commit }) => export const receiveEnvironmentsDataFailure = ({ commit }) =>
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE); commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
export const fetchData = ({ dispatch }, params) => { export const fetchData = ({ dispatch }) => {
dispatch('fetchMetricsData', params); dispatch('fetchDashboard');
dispatch('fetchDeploymentsData'); dispatch('fetchDeploymentsData');
dispatch('fetchEnvironmentsData'); dispatch('fetchEnvironmentsData');
}; };
export const fetchMetricsData = ({ dispatch }, params) => dispatch('fetchDashboard', params); export const fetchDashboard = ({ state, dispatch }) => {
export const fetchDashboard = ({ state, dispatch }, params) => {
dispatch('requestMetricsDashboard'); dispatch('requestMetricsDashboard');
const params = {};
if (state.timeRange) {
const { start, end } = convertToFixedRange(state.timeRange);
params.start = start;
params.end = end;
}
if (state.currentDashboard) { if (state.currentDashboard) {
// eslint-disable-next-line no-param-reassign
params.dashboard = state.currentDashboard; params.dashboard = state.currentDashboard;
} }
......
...@@ -14,7 +14,7 @@ export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT'; ...@@ -14,7 +14,7 @@ export const REQUEST_METRIC_RESULT = 'REQUEST_METRIC_RESULT';
export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS'; export const RECEIVE_METRIC_RESULT_SUCCESS = 'RECEIVE_METRIC_RESULT_SUCCESS';
export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE'; export const RECEIVE_METRIC_RESULT_FAILURE = 'RECEIVE_METRIC_RESULT_FAILURE';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW'; export const SET_TIME_RANGE = 'SET_TIME_RANGE';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS'; export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS'; export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE'; export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
......
...@@ -182,6 +182,10 @@ export default { ...@@ -182,6 +182,10 @@ export default {
state.dashboardsEndpoint = endpoints.dashboardsEndpoint; state.dashboardsEndpoint = endpoints.dashboardsEndpoint;
state.currentDashboard = endpoints.currentDashboard; state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath; state.projectPath = endpoints.projectPath;
state.logsPath = endpoints.logsPath || state.logsPath;
},
[types.SET_TIME_RANGE](state, timeRange) {
state.timeRange = timeRange;
}, },
[types.SET_GETTING_STARTED_EMPTY_STATE](state) { [types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted'; state.emptyState = 'gettingStarted';
......
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
export default () => ({ export default () => ({
// API endpoints
metricsEndpoint: null, metricsEndpoint: null,
deploymentsEndpoint: null, deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl, dashboardEndpoint: invalidUrl,
// Dashboard request parameters
timeRange: null,
currentDashboard: null,
// Dashboard data
emptyState: 'gettingStarted', emptyState: 'gettingStarted',
showEmptyState: true, showEmptyState: true,
showErrorBanner: true, showErrorBanner: true,
dashboard: { dashboard: {
panel_groups: [], panel_groups: [],
}, },
allDashboards: [],
// Other project data
deploymentData: [], deploymentData: [],
environments: [], environments: [],
environmentsSearchTerm: '', environmentsSearchTerm: '',
environmentsLoading: false, environmentsLoading: false,
allDashboards: [],
currentDashboard: null, // GitLab paths to other pages
projectPath: null, projectPath: null,
logsPath: invalidUrl,
}); });
...@@ -103,8 +103,9 @@ export const graphDataValidatorForAnomalyValues = graphData => { ...@@ -103,8 +103,9 @@ export const graphDataValidatorForAnomalyValues = graphData => {
/** /**
* Returns a time range from the current URL params * Returns a time range from the current URL params
* *
* @returns {Object} The time range defined by the * @returns {Object|null} The time range defined by the
* current URL, reading from `window.location.search` * current URL, reading from search query or `window.location.search`.
* Returns `null` if no parameters form a time range.
*/ */
export const timeRangeFromUrl = (search = window.location.search) => { export const timeRangeFromUrl = (search = window.location.search) => {
const params = queryToObject(search); const params = queryToObject(search);
......
...@@ -6,6 +6,7 @@ import { scrollDown } from '~/lib/utils/scroll_utils'; ...@@ -6,6 +6,7 @@ import { scrollDown } from '~/lib/utils/scroll_utils';
import LogControlButtons from './log_control_buttons.vue'; import LogControlButtons from './log_control_buttons.vue';
import { timeRanges, defaultTimeRange } from '~/monitoring/constants'; import { timeRanges, defaultTimeRange } from '~/monitoring/constants';
import { timeRangeFromUrl } from '~/monitoring/utils';
export default { export default {
components: { components: {
...@@ -41,7 +42,6 @@ export default { ...@@ -41,7 +42,6 @@ export default {
data() { data() {
return { return {
searchQuery: '', searchQuery: '',
selectedTimeRange: defaultTimeRange,
timeRanges, timeRanges,
isElasticStackCalloutDismissed: false, isElasticStackCalloutDismissed: false,
}; };
...@@ -91,6 +91,7 @@ export default { ...@@ -91,6 +91,7 @@ export default {
}, },
mounted() { mounted() {
this.setInitData({ this.setInitData({
timeRange: timeRangeFromUrl() || defaultTimeRange,
environmentName: this.environmentName, environmentName: this.environmentName,
podName: this.currentPodName, podName: this.currentPodName,
}); });
......
...@@ -31,7 +31,10 @@ const requestLogsUntilData = params => ...@@ -31,7 +31,10 @@ const requestLogsUntilData = params =>
}); });
}); });
export const setInitData = ({ commit }, { environmentName, podName }) => { export const setInitData = ({ commit }, { timeRange, environmentName, podName }) => {
if (timeRange) {
commit(types.SET_TIME_RANGE, timeRange);
}
commit(types.SET_PROJECT_ENVIRONMENT, environmentName); commit(types.SET_PROJECT_ENVIRONMENT, environmentName);
commit(types.SET_CURRENT_POD_NAME, podName); commit(types.SET_CURRENT_POD_NAME, podName);
}; };
......
---
title: Add a link in dashboard to allow users to go to the logs page
merge_request: 24240
author:
type: added
...@@ -131,6 +131,9 @@ describe('EnvironmentLogs', () => { ...@@ -131,6 +131,9 @@ describe('EnvironmentLogs', () => {
expect(actionMocks.setInitData).toHaveBeenCalledTimes(1); expect(actionMocks.setInitData).toHaveBeenCalledTimes(1);
expect(actionMocks.setInitData).toHaveBeenLastCalledWith({ expect(actionMocks.setInitData).toHaveBeenLastCalledWith({
timeRange: expect.objectContaining({
default: true,
}),
environmentName: mockEnvName, environmentName: mockEnvName,
podName: null, podName: null,
}); });
......
...@@ -17,7 +17,6 @@ describe('Panel Type', () => { ...@@ -17,7 +17,6 @@ describe('Panel Type', () => {
let axiosMock; let axiosMock;
let panelType; let panelType;
let store; let store;
const dashboardWidth = 100;
const exampleText = 'example_text'; const exampleText = 'example_text';
const createWrapper = propsData => { const createWrapper = propsData => {
...@@ -46,7 +45,6 @@ describe('Panel Type', () => { ...@@ -46,7 +45,6 @@ describe('Panel Type', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ createWrapper({
clipboardText: exampleText, clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange, graphData: graphDataPrometheusQueryRange,
alertsEndpoint: '/endpoint', alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: true, prometheusAlertsAvailable: true,
...@@ -89,7 +87,6 @@ describe('Panel Type', () => { ...@@ -89,7 +87,6 @@ describe('Panel Type', () => {
beforeEach(() => { beforeEach(() => {
createWrapper({ createWrapper({
clipboardText: exampleText, clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange, graphData: graphDataPrometheusQueryRange,
alertsEndpoint: '/endpoint', alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: false, prometheusAlertsAvailable: false,
......
...@@ -12118,6 +12118,9 @@ msgstr "" ...@@ -12118,6 +12118,9 @@ msgstr ""
msgid "Metrics|Validating query" msgid "Metrics|Validating query"
msgstr "" msgstr ""
msgid "Metrics|View logs"
msgstr ""
msgid "Metrics|Y-axis label" msgid "Metrics|Y-axis label"
msgstr "" msgstr ""
......
...@@ -73,12 +73,20 @@ describe('Dashboard', () => { ...@@ -73,12 +73,20 @@ describe('Dashboard', () => {
describe('no metrics are available yet', () => { describe('no metrics are available yet', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(store, 'dispatch');
createShallowWrapper(); createShallowWrapper();
}); });
it('shows the environment selector', () => { it('shows the environment selector', () => {
expect(findEnvironmentsDropdown().exists()).toBe(true); expect(findEnvironmentsDropdown().exists()).toBe(true);
}); });
it('sets endpoints: logs path', () => {
expect(store.dispatch).toHaveBeenCalledWith(
'monitoringDashboard/setEndpoints',
expect.objectContaining({ logsPath: propsData.logsPath }),
);
});
}); });
describe('no data found', () => { describe('no data found', () => {
...@@ -94,6 +102,21 @@ describe('Dashboard', () => { ...@@ -94,6 +102,21 @@ describe('Dashboard', () => {
}); });
describe('request information to the server', () => { describe('request information to the server', () => {
it('calls to set time range and fetch data', () => {
jest.spyOn(store, 'dispatch');
createShallowWrapper({ hasMetrics: true }, { methods: {} });
return wrapper.vm.$nextTick().then(() => {
expect(store.dispatch).toHaveBeenCalledWith(
'monitoringDashboard/setTimeRange',
expect.any(Object),
);
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
});
});
it('shows up a loading state', done => { it('shows up a loading state', done => {
createShallowWrapper({ hasMetrics: true }, { methods: {} }); createShallowWrapper({ hasMetrics: true }, { methods: {} });
...@@ -126,7 +149,7 @@ describe('Dashboard', () => { ...@@ -126,7 +149,7 @@ describe('Dashboard', () => {
.catch(done.fail); .catch(done.fail);
}); });
it('fetches the metrics data with proper time window', done => { it('fetches the metrics data with proper time window', () => {
jest.spyOn(store, 'dispatch'); jest.spyOn(store, 'dispatch');
createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] });
...@@ -136,14 +159,9 @@ describe('Dashboard', () => { ...@@ -136,14 +159,9 @@ describe('Dashboard', () => {
environmentData, environmentData,
); );
wrapper.vm return wrapper.vm.$nextTick().then(() => {
.$nextTick() expect(store.dispatch).toHaveBeenCalled();
.then(() => { });
expect(store.dispatch).toHaveBeenCalled();
done();
})
.catch(done.fail);
}); });
}); });
...@@ -263,10 +281,6 @@ describe('Dashboard', () => { ...@@ -263,10 +281,6 @@ describe('Dashboard', () => {
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
afterEach(() => {
wrapper.destroy();
});
it('renders a search input', () => { it('renders a search input', () => {
expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownSearch' }).exists()).toBe(true); expect(wrapper.find({ ref: 'monitorEnvironmentsDropdownSearch' }).exists()).toBe(true);
}); });
......
...@@ -7,6 +7,7 @@ import { mockProjectDir } from '../mock_data'; ...@@ -7,6 +7,7 @@ import { mockProjectDir } from '../mock_data';
import Dashboard from '~/monitoring/components/dashboard.vue'; import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores'; import { createStore } from '~/monitoring/stores';
import { defaultTimeRange } from '~/monitoring/constants';
import { propsData } from '../init_utils'; import { propsData } from '../init_utils';
jest.mock('~/flash'); jest.mock('~/flash');
...@@ -17,16 +18,11 @@ describe('dashboard invalid url parameters', () => { ...@@ -17,16 +18,11 @@ describe('dashboard invalid url parameters', () => {
let wrapper; let wrapper;
let mock; let mock;
const fetchDataMock = jest.fn();
const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => { const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
wrapper = mount(Dashboard, { wrapper = mount(Dashboard, {
propsData: { ...propsData, ...props }, propsData: { ...propsData, ...props },
store, store,
stubs: ['graph-group', 'panel-type'], stubs: ['graph-group', 'panel-type'],
methods: {
fetchData: fetchDataMock,
},
...options, ...options,
}); });
}; };
...@@ -35,6 +31,8 @@ describe('dashboard invalid url parameters', () => { ...@@ -35,6 +31,8 @@ describe('dashboard invalid url parameters', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); store = createStore();
jest.spyOn(store, 'dispatch');
mock = new MockAdapter(axios); mock = new MockAdapter(axios);
}); });
...@@ -43,7 +41,6 @@ describe('dashboard invalid url parameters', () => { ...@@ -43,7 +41,6 @@ describe('dashboard invalid url parameters', () => {
wrapper.destroy(); wrapper.destroy();
} }
mock.restore(); mock.restore();
fetchDataMock.mockReset();
queryToObject.mockReset(); queryToObject.mockReset();
}); });
...@@ -53,15 +50,13 @@ describe('dashboard invalid url parameters', () => { ...@@ -53,15 +50,13 @@ describe('dashboard invalid url parameters', () => {
createMountedWrapper(); createMountedWrapper();
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toMatchObject({ expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
duration: { seconds: 28800 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledWith(
expect(fetchDataMock).toHaveBeenCalledWith({ 'monitoringDashboard/setTimeRange',
start: expect.any(String), expect.any(Object),
end: expect.any(String), );
}); expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
}); });
}); });
...@@ -78,8 +73,8 @@ describe('dashboard invalid url parameters', () => { ...@@ -78,8 +73,8 @@ describe('dashboard invalid url parameters', () => {
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toEqual(params); expect(findDateTimePicker().props('value')).toEqual(params);
expect(fetchDataMock).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setTimeRange', params);
expect(fetchDataMock).toHaveBeenCalledWith(params); expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
}); });
}); });
...@@ -91,15 +86,17 @@ describe('dashboard invalid url parameters', () => { ...@@ -91,15 +86,17 @@ describe('dashboard invalid url parameters', () => {
createMountedWrapper(); createMountedWrapper();
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(findDateTimePicker().props('value')).toMatchObject({ const expectedTimeRange = {
duration: { seconds: 60 * 2 }, duration: { seconds: 60 * 2 },
}); };
expect(fetchDataMock).toHaveBeenCalledTimes(1); expect(findDateTimePicker().props('value')).toMatchObject(expectedTimeRange);
expect(fetchDataMock).toHaveBeenCalledWith({
start: expect.any(String), expect(store.dispatch).toHaveBeenCalledWith(
end: expect.any(String), 'monitoringDashboard/setTimeRange',
}); expectedTimeRange,
);
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
}); });
}); });
...@@ -114,15 +111,13 @@ describe('dashboard invalid url parameters', () => { ...@@ -114,15 +111,13 @@ describe('dashboard invalid url parameters', () => {
return wrapper.vm.$nextTick().then(() => { return wrapper.vm.$nextTick().then(() => {
expect(createFlash).toHaveBeenCalled(); expect(createFlash).toHaveBeenCalled();
expect(findDateTimePicker().props('value')).toMatchObject({ expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
duration: { seconds: 28800 },
});
expect(fetchDataMock).toHaveBeenCalledTimes(1); expect(store.dispatch).toHaveBeenCalledWith(
expect(fetchDataMock).toHaveBeenCalledWith({ 'monitoringDashboard/setTimeRange',
start: expect.any(String), defaultTimeRange,
end: expect.any(String), );
}); expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
}); });
}); });
...@@ -137,7 +132,7 @@ describe('dashboard invalid url parameters', () => { ...@@ -137,7 +132,7 @@ describe('dashboard invalid url parameters', () => {
duration: { seconds: 120 }, duration: { seconds: 120 },
}); });
// redirect to plus + new parameters // redirect to with new parameters
expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl); expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
expect(redirectTo).toHaveBeenCalledTimes(1); expect(redirectTo).toHaveBeenCalledTimes(1);
}); });
......
...@@ -26,10 +26,11 @@ describe('Embed', () => { ...@@ -26,10 +26,11 @@ describe('Embed', () => {
beforeEach(() => { beforeEach(() => {
actions = { actions = {
setFeatureFlags: () => {}, setFeatureFlags: jest.fn(),
setShowErrorBanner: () => {}, setShowErrorBanner: jest.fn(),
setEndpoints: () => {}, setEndpoints: jest.fn(),
fetchMetricsData: () => {}, setTimeRange: jest.fn(),
fetchDashboard: jest.fn(),
}; };
metricsWithDataGetter = jest.fn(); metricsWithDataGetter = jest.fn();
...@@ -76,6 +77,18 @@ describe('Embed', () => { ...@@ -76,6 +77,18 @@ describe('Embed', () => {
mountComponent(); mountComponent();
}); });
it('calls actions to fetch data', () => {
const expectedTimeRangePayload = expect.objectContaining({
start: expect.any(String),
end: expect.any(String),
});
expect(actions.setTimeRange).toHaveBeenCalledTimes(1);
expect(actions.setTimeRange.mock.calls[0][1]).toEqual(expectedTimeRangePayload);
expect(actions.fetchDashboard).toHaveBeenCalled();
});
it('shows a chart when metrics are present', () => { it('shows a chart when metrics are present', () => {
expect(wrapper.find('.metrics-embed').exists()).toBe(true); expect(wrapper.find('.metrics-embed').exists()).toBe(true);
expect(wrapper.find(PanelType).exists()).toBe(true); expect(wrapper.find(PanelType).exists()).toBe(true);
......
...@@ -15,6 +15,7 @@ export const propsData = { ...@@ -15,6 +15,7 @@ export const propsData = {
clustersPath: '/path/to/clusters', clustersPath: '/path/to/clusters',
tagsPath: '/path/to/tags', tagsPath: '/path/to/tags',
projectPath: '/path/to/project', projectPath: '/path/to/project',
logsPath: '/path/to/logs',
defaultBranch: 'master', defaultBranch: 'master',
metricsEndpoint: mockApiEndpoint, metricsEndpoint: mockApiEndpoint,
deploymentsEndpoint: null, deploymentsEndpoint: null,
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout'; import { setTestTimeout } from 'helpers/timeout';
import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import PanelType from '~/monitoring/components/panel_type.vue'; import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue'; import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
...@@ -16,20 +17,25 @@ global.URL.createObjectURL = jest.fn(); ...@@ -16,20 +17,25 @@ global.URL.createObjectURL = jest.fn();
describe('Panel Type component', () => { describe('Panel Type component', () => {
let axiosMock; let axiosMock;
let store; let store;
let panelType; let state;
const dashboardWidth = 100; let wrapper;
const exampleText = 'example_text'; const exampleText = 'example_text';
const createWrapper = props => const createWrapper = props => {
shallowMount(PanelType, { wrapper = shallowMount(PanelType, {
propsData: { propsData: {
...props, ...props,
}, },
store, store,
}); });
};
beforeEach(() => { beforeEach(() => {
setTestTimeout(1000); setTestTimeout(1000);
store = createStore();
state = store.state.monitoringDashboard;
axiosMock = new AxiosMockAdapter(axios); axiosMock = new AxiosMockAdapter(axios);
}); });
...@@ -44,19 +50,18 @@ describe('Panel Type component', () => { ...@@ -44,19 +50,18 @@ describe('Panel Type component', () => {
graphDataNoResult.metrics[0].result = []; graphDataNoResult.metrics[0].result = [];
beforeEach(() => { beforeEach(() => {
panelType = createWrapper({ createWrapper({
dashboardWidth,
graphData: graphDataNoResult, graphData: graphDataNoResult,
}); });
}); });
afterEach(() => { afterEach(() => {
panelType.destroy(); wrapper.destroy();
}); });
describe('Empty Chart component', () => { describe('Empty Chart component', () => {
beforeEach(() => { beforeEach(() => {
glEmptyChart = panelType.find(EmptyChart); glEmptyChart = wrapper.find(EmptyChart);
}); });
it('is a Vue instance', () => { it('is a Vue instance', () => {
...@@ -66,51 +71,105 @@ describe('Panel Type component', () => { ...@@ -66,51 +71,105 @@ describe('Panel Type component', () => {
it('it receives a graph title', () => { it('it receives a graph title', () => {
const props = glEmptyChart.props(); const props = glEmptyChart.props();
expect(props.graphTitle).toBe(panelType.vm.graphData.title); expect(props.graphTitle).toBe(wrapper.vm.graphData.title);
}); });
}); });
}); });
describe('when graph data is available', () => { describe('when graph data is available', () => {
beforeEach(() => { beforeEach(() => {
store = createStore(); createWrapper({
panelType = createWrapper({
dashboardWidth,
graphData: graphDataPrometheusQueryRange, graphData: graphDataPrometheusQueryRange,
}); });
}); });
afterEach(() => { afterEach(() => {
panelType.destroy(); wrapper.destroy();
}); });
it('sets no clipboard copy link on dropdown by default', () => { it('sets no clipboard copy link on dropdown by default', () => {
const link = () => panelType.find('.js-chart-link'); const link = () => wrapper.find('.js-chart-link');
expect(link().exists()).toBe(false); expect(link().exists()).toBe(false);
}); });
describe('Time Series Chart panel type', () => { describe('Time Series Chart panel type', () => {
it('is rendered', () => { it('is rendered', () => {
expect(panelType.find(TimeSeriesChart).isVueInstance()).toBe(true); expect(wrapper.find(TimeSeriesChart).isVueInstance()).toBe(true);
expect(panelType.find(TimeSeriesChart).exists()).toBe(true); expect(wrapper.find(TimeSeriesChart).exists()).toBe(true);
}); });
it('includes a default group id', () => { it('includes a default group id', () => {
expect(panelType.vm.groupId).toBe('panel-type-chart'); expect(wrapper.vm.groupId).toBe('panel-type-chart');
}); });
}); });
describe('Anomaly Chart panel type', () => { describe('Anomaly Chart panel type', () => {
beforeEach(done => { beforeEach(() => {
panelType.setProps({ wrapper.setProps({
graphData: anomalyMockGraphData, graphData: anomalyMockGraphData,
}); });
panelType.vm.$nextTick(done); return wrapper.vm.$nextTick();
}); });
it('is rendered with an anomaly chart', () => { it('is rendered with an anomaly chart', () => {
expect(panelType.find(AnomalyChart).isVueInstance()).toBe(true); expect(wrapper.find(AnomalyChart).isVueInstance()).toBe(true);
expect(panelType.find(AnomalyChart).exists()).toBe(true); expect(wrapper.find(AnomalyChart).exists()).toBe(true);
});
});
});
describe('View Logs dropdown item', () => {
const mockLogsPath = '/path/to/logs';
const mockTimeRange = { duration: { seconds: 120 } };
const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' });
beforeEach(() => {
createWrapper({
graphData: graphDataPrometheusQueryRange,
});
return wrapper.vm.$nextTick();
});
it('is not present by default', () =>
wrapper.vm.$nextTick(() => {
expect(findViewLogsLink().exists()).toBe(false);
}));
it('is not present if a time range is not set', () => {
state.logsPath = mockLogsPath;
state.timeRange = null;
return wrapper.vm.$nextTick(() => {
expect(findViewLogsLink().exists()).toBe(false);
});
});
it('is not present if the logs path is default', () => {
state.logsPath = invalidUrl;
state.timeRange = mockTimeRange;
return wrapper.vm.$nextTick(() => {
expect(findViewLogsLink().exists()).toBe(false);
});
});
it('is not present if the logs path is not set', () => {
state.logsPath = null;
state.timeRange = mockTimeRange;
return wrapper.vm.$nextTick(() => {
expect(findViewLogsLink().exists()).toBe(false);
});
});
it('is present when logs path and time a range is present', () => {
state.logsPath = mockLogsPath;
state.timeRange = mockTimeRange;
return wrapper.vm.$nextTick(() => {
expect(findViewLogsLink().exists()).toBe(true);
expect(findViewLogsLink().attributes('href')).toEqual('/path/to/logs?duration_seconds=120');
}); });
}); });
}); });
...@@ -119,20 +178,18 @@ describe('Panel Type component', () => { ...@@ -119,20 +178,18 @@ describe('Panel Type component', () => {
const clipboardText = 'A value to copy.'; const clipboardText = 'A value to copy.';
beforeEach(() => { beforeEach(() => {
store = createStore(); createWrapper({
panelType = createWrapper({
clipboardText, clipboardText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange, graphData: graphDataPrometheusQueryRange,
}); });
}); });
afterEach(() => { afterEach(() => {
panelType.destroy(); wrapper.destroy();
}); });
it('sets clipboard text on the dropdown', () => { it('sets clipboard text on the dropdown', () => {
const link = () => panelType.find('.js-chart-link'); const link = () => wrapper.find('.js-chart-link');
expect(link().exists()).toBe(true); expect(link().exists()).toBe(true);
expect(link().element.dataset.clipboardText).toBe(clipboardText); expect(link().element.dataset.clipboardText).toBe(clipboardText);
...@@ -140,22 +197,20 @@ describe('Panel Type component', () => { ...@@ -140,22 +197,20 @@ describe('Panel Type component', () => {
}); });
describe('when downloading metrics data as CSV', () => { describe('when downloading metrics data as CSV', () => {
beforeEach(done => { beforeEach(() => {
graphDataPrometheusQueryRange.y_label = 'metric'; graphDataPrometheusQueryRange.y_label = 'metric';
store = createStore(); wrapper = shallowMount(PanelType, {
panelType = shallowMount(PanelType, {
propsData: { propsData: {
clipboardText: exampleText, clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange, graphData: graphDataPrometheusQueryRange,
}, },
store, store,
}); });
panelType.vm.$nextTick(done); return wrapper.vm.$nextTick();
}); });
afterEach(() => { afterEach(() => {
panelType.destroy(); wrapper.destroy();
}); });
describe('csvText', () => { describe('csvText', () => {
...@@ -165,7 +220,7 @@ describe('Panel Type component', () => { ...@@ -165,7 +220,7 @@ describe('Panel Type component', () => {
const firstRow = `${data[0][0]},${data[0][1]}`; const firstRow = `${data[0][0]},${data[0][1]}`;
const secondRow = `${data[1][0]},${data[1][1]}`; const secondRow = `${data[1][0]},${data[1][1]}`;
expect(panelType.vm.csvText).toBe(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`); expect(wrapper.vm.csvText).toBe(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
}); });
}); });
...@@ -174,7 +229,7 @@ describe('Panel Type component', () => { ...@@ -174,7 +229,7 @@ describe('Panel Type component', () => {
expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob)); expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
expect(global.URL.createObjectURL).toHaveBeenLastCalledWith( expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
expect.objectContaining({ expect.objectContaining({
size: panelType.vm.csvText.length, size: wrapper.vm.csvText.length,
type: 'text/plain', type: 'text/plain',
}), }),
); );
......
...@@ -90,6 +90,16 @@ describe('Monitoring mutations', () => { ...@@ -90,6 +90,16 @@ describe('Monitoring mutations', () => {
expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json'); expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss'); expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
}); });
it('should not remove default value of logsPath', () => {
const defaultLogsPath = stateCopy.logsPath;
mutations[types.SET_ENDPOINTS](stateCopy, {
dashboardEndpoint: 'dashboard.json',
});
expect(stateCopy.logsPath).toBe(defaultLogsPath);
});
}); });
describe('Individual panel/metric results', () => { describe('Individual panel/metric results', () => {
const metricId = '12_system_metrics_kubernetes_container_memory_total'; const metricId = '12_system_metrics_kubernetes_container_memory_total';
......
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