Commit 20e4a5c5 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas Committed by Fatih Acet

Remove the `additional_panel_types` feature flag

This removes the `additional_panel_types` feature flag
allowing to utilize al currently supported chart types
for all users
parent b0732480
......@@ -11,7 +11,7 @@ import {
GlModalDirective,
GlTooltipDirective,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { s__ } from '~/locale';
import createFlash from '~/flash';
import Icon from '~/vue_shared/components/icon.vue';
import { getParameterValues, mergeUrlParams, redirectTo } from '~/lib/utils/url_utility';
......@@ -167,6 +167,7 @@ export default {
formIsValid: null,
selectedTimeWindow: {},
isRearrangingPanels: false,
hasValidDates: true,
};
},
computed: {
......@@ -185,7 +186,9 @@ export default {
'additionalPanelTypesEnabled',
]),
firstDashboard() {
return this.allDashboards[0] || {};
return this.environmentsEndpoint.length > 0 && this.allDashboards.length > 0
? this.allDashboards[0]
: {};
},
selectedDashboard() {
return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
......@@ -199,9 +202,6 @@ export default {
addingMetricsAvailable() {
return IS_EE && this.canAddMetrics && !this.showEmptyState;
},
alertWidgetAvailable() {
return IS_EE && this.prometheusAlertsAvailable && this.alertsEndpoint;
},
hasHeaderButtons() {
return (
this.addingMetricsAvailable ||
......@@ -237,8 +237,10 @@ export default {
this.selectedTimeWindow = range;
if (!isValidDate(start) || !isValidDate(end)) {
this.hasValidDates = false;
this.showInvalidDateError();
} else {
this.hasValidDates = true;
this.fetchData(range);
}
}
......@@ -248,44 +250,13 @@ export default {
'fetchData',
'setGettingStartedEmptyState',
'setEndpoints',
'setDashboardEnabled',
'setPanelGroupMetrics',
]),
chartsWithData(charts) {
if (!this.useDashboardEndpoint) {
return charts;
}
return charts.filter(chart =>
chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
);
},
csvText(graphData) {
const chartData = graphData.queries[0].result[0].values;
const yLabel = graphData.y_label;
const header = `timestamp,${yLabel}\r\n`; // eslint-disable-line @gitlab/i18n/no-non-i18n-strings
return chartData.reduce((csv, data) => {
const row = data.join(',');
return `${csv}${row}\r\n`;
}, header);
},
downloadCsv(graphData) {
const data = new Blob([this.csvText(graphData)], { type: 'text/plain' });
return window.URL.createObjectURL(data);
},
// TODO: BEGIN, Duplicated code with panel_type until feature flag is removed
// Issue number: https://gitlab.com/gitlab-org/gitlab-foss/issues/63845
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map(q => q.metricId);
return _.pick(this.allAlerts, alert => metricIdsForChart.includes(alert.metricId));
},
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
showToast() {
this.$toast.show(__('Link copied'));
},
// TODO: END
updateMetrics(key, metrics) {
this.setPanelGroupMetrics({
metrics,
......@@ -298,6 +269,11 @@ export default {
key,
});
},
removeGraph(metrics, graphIndex) {
// At present graphs will not be removed, they should removed using the vuex store
// See https://gitlab.com/gitlab-org/gitlab/issues/27835
metrics.splice(graphIndex, 1);
},
showInvalidDateError() {
createFlash(s__('Metrics|Link contains an invalid time window.'));
},
......@@ -387,7 +363,7 @@ export default {
</gl-form-group>
<gl-form-group
v-if="!showEmptyState"
v-if="hasValidDates"
:label="s__('Metrics|Show last')"
label-size="sm"
label-for="monitor-time-window-dropdown"
......@@ -480,109 +456,40 @@ export default {
:show-panels="showPanels"
:collapse-group="groupHasData(groupData)"
>
<template v-if="additionalPanelTypesEnabled">
<vue-draggable
:value="groupData.metrics"
group="metrics-dashboard"
:component-data="{ attrs: { class: 'row mx-0 w-100' } }"
:disabled="!isRearrangingPanels"
@input="updateMetrics(groupData.key, $event)"
<vue-draggable
:value="groupData.metrics"
group="metrics-dashboard"
:component-data="{ attrs: { class: 'row mx-0 w-100' } }"
:disabled="!isRearrangingPanels"
@input="updateMetrics(groupData.key, $event)"
>
<div
v-for="(graphData, graphIndex) in groupData.metrics"
:key="`panel-type-${graphIndex}`"
class="col-12 col-lg-6 px-2 mb-2 draggable"
:class="{ 'draggable-enabled': isRearrangingPanels }"
>
<div
v-for="(graphData, graphIndex) in groupData.metrics"
:key="`panel-type-${graphIndex}`"
class="col-12 col-lg-6 px-2 mb-2 draggable"
:class="{ 'draggable-enabled': isRearrangingPanels }"
>
<div class="position-relative draggable-panel js-draggable-panel">
<div
v-if="isRearrangingPanels"
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
@click="removeMetric(groupData.key, groupData.metrics, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
/></a>
</div>
<panel-type
:clipboard-text="
generateLink(groupData.group, graphData.title, graphData.y_label)
"
:graph-data="graphData"
:alerts-endpoint="alertsEndpoint"
:prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
<div class="position-relative draggable-panel js-draggable-panel">
<div
v-if="isRearrangingPanels"
class="draggable-remove js-draggable-remove p-2 w-100 position-absolute d-flex justify-content-end"
@click="removeGraph(groupData.metrics, graphIndex)"
>
<a class="mx-2 p-2 draggable-remove-link" :aria-label="__('Remove')"
><icon name="close"
/></a>
</div>
</div>
</vue-draggable>
</template>
<template v-else>
<monitor-time-series-chart
v-for="(graphData, graphIndex) in chartsWithData(groupData.metrics)"
:key="graphIndex"
class="col-12 col-lg-6 pb-3"
:graph-data="graphData"
:deployment-data="deploymentData"
:thresholds="getGraphAlertValues(graphData.queries)"
:project-path="projectPath"
group-id="monitor-time-series-chart"
>
<div
class="d-flex align-items-center"
:class="alertWidgetAvailable ? 'justify-content-between' : 'justify-content-end'"
>
<alert-widget
v-if="alertWidgetAvailable && graphData"
:modal-id="`alert-modal-${index}-${graphIndex}`"
<panel-type
:clipboard-text="generateLink(groupData.group, graphData.title, graphData.y_label)"
:graph-data="graphData"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.queries"
:alerts-to-manage="getGraphAlerts(graphData.queries)"
@setAlerts="setAlerts"
:prometheus-alerts-available="prometheusAlertsAvailable"
:index="`${index}-${graphIndex}`"
/>
<gl-dropdown
v-gl-tooltip
class="ml-2 mr-3"
toggle-class="btn btn-transparent border-0"
:right="true"
:no-caret="true"
:title="__('More actions')"
>
<template slot="button-content">
<icon name="ellipsis_v" class="text-secondary" />
</template>
<gl-dropdown-item
v-track-event="downloadCSVOptions(graphData.title)"
:href="downloadCsv(graphData)"
download="chart_metrics.csv"
>
{{ __('Download CSV') }}
</gl-dropdown-item>
<gl-dropdown-item
v-track-event="
generateLinkToChartOptions(
generateLink(groupData.group, graphData.title, graphData.y_label),
)
"
class="js-chart-link"
:data-clipboard-text="
generateLink(groupData.group, graphData.title, graphData.y_label)
"
@click="showToast"
>
{{ __('Generate link to chart') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${index}-${graphIndex}`"
>
{{ __('Alerts') }}
</gl-dropdown-item>
</gl-dropdown>
</div>
</monitor-time-series-chart>
</template>
</div>
</vue-draggable>
</graph-group>
</div>
<empty-state
......
......@@ -56,16 +56,10 @@ export default {
},
},
mounted() {
const range = getTimeWindow(this.selectedTimeWindow);
if (range) {
this.selectedTimeWindowText = this.timeWindows[range];
} else {
this.customTime = {
from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
};
this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
}
this.verifyTimeRange();
},
updated() {
this.verifyTimeRange();
},
methods: {
activeTimeWindow(key) {
......@@ -87,6 +81,18 @@ export default {
closeDropdown() {
this.$refs.dropdown.hide();
},
verifyTimeRange() {
const range = getTimeWindow(this.selectedTimeWindow);
if (range) {
this.selectedTimeWindowText = this.timeWindows[range];
} else {
this.customTime = {
from: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.start)),
to: truncateZerosInDateTime(ISODateToString(this.selectedTimeWindow.end)),
};
this.selectedTimeWindowText = sprintf(s__('%{from} to %{to}'), this.customTime);
}
},
},
};
</script>
......
......@@ -78,9 +78,6 @@ export default {
}, sidebarAnimationDuration);
},
setInitialState() {
this.setFeatureFlags({
prometheusEndpointEnabled: true,
});
this.setEndpoints({
dashboardEndpoint: removeParams(['start', 'end'], this.dashboardUrl),
});
......
......@@ -11,13 +11,6 @@ export default (props = {}) => {
const el = document.getElementById('prometheus-graphs');
if (el && el.dataset) {
if (gon.features) {
store.dispatch('monitoringDashboard/setFeatureFlags', {
prometheusEndpointEnabled: gon.features.environmentMetricsUsePrometheusEndpoint,
additionalPanelTypesEnabled: gon.features.environmentMetricsAdditionalPanelTypes,
});
}
const [currentDashboard] = getParameterValues('dashboard');
// eslint-disable-next-line no-new
......
......@@ -35,14 +35,6 @@ export const setEndpoints = ({ commit }, endpoints) => {
commit(types.SET_ENDPOINTS, endpoints);
};
export const setFeatureFlags = (
{ commit },
{ prometheusEndpointEnabled, additionalPanelTypesEnabled },
) => {
commit(types.SET_DASHBOARD_ENABLED, prometheusEndpointEnabled);
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
};
export const setShowErrorBanner = ({ commit }, enabled) => {
commit(types.SET_SHOW_ERROR_BANNER, enabled);
};
......@@ -79,29 +71,7 @@ export const fetchData = ({ dispatch }, params) => {
dispatch('fetchEnvironmentsData');
};
export const fetchMetricsData = ({ state, dispatch }, params) => {
if (state.useDashboardEndpoint) {
return dispatch('fetchDashboard', params);
}
dispatch('requestMetricsData');
return backOffRequest(() => axios.get(state.metricsEndpoint, { params }))
.then(resp => resp.data)
.then(response => {
if (!response || !response.data || !response.success) {
dispatch('receiveMetricsDataFailure', null);
createFlash(s__('Metrics|Unexpected metrics data response from prometheus endpoint'));
}
dispatch('receiveMetricsDataSuccess', response.data);
})
.catch(error => {
dispatch('receiveMetricsDataFailure', error);
if (state.setShowErrorBanner) {
createFlash(s__('Metrics|There was an error while retrieving metrics'));
}
});
};
export const fetchMetricsData = ({ dispatch }, params) => dispatch('fetchDashboard', params);
export const fetchDashboard = ({ state, dispatch }, params) => {
dispatch('requestMetricsDashboard');
......
......@@ -9,8 +9,6 @@ export const RECEIVE_ENVIRONMENTS_DATA_SUCCESS = 'RECEIVE_ENVIRONMENTS_DATA_SUCC
export const RECEIVE_ENVIRONMENTS_DATA_FAILURE = 'RECEIVE_ENVIRONMENTS_DATA_FAILURE';
export const SET_QUERY_RESULT = 'SET_QUERY_RESULT';
export const SET_TIME_WINDOW = 'SET_TIME_WINDOW';
export const SET_DASHBOARD_ENABLED = 'SET_DASHBOARD_ENABLED';
export const SET_ADDITIONAL_PANEL_TYPES_ENABLED = 'SET_ADDITIONAL_PANEL_TYPES_ENABLED';
export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
......
......@@ -25,12 +25,10 @@ export default {
// for backwards compatibility, and to limit Vue template changes:
// for each group alias panels to metrics
// for each panel alias metrics to queries
if (state.useDashboardEndpoint) {
metrics = panels.map(panel => ({
...panel,
queries: panel.metrics,
}));
}
metrics = panels.map(panel => ({
...panel,
queries: panel.metrics,
}));
return {
...group,
......@@ -42,8 +40,6 @@ export default {
if (!state.dashboard.panel_groups.length) {
state.emptyState = 'noData';
} else {
state.showEmptyState = false;
}
},
[types.RECEIVE_METRICS_DATA_FAILURE](state, error) {
......@@ -90,9 +86,6 @@ export default {
state.currentDashboard = endpoints.currentDashboard;
state.projectPath = endpoints.projectPath;
},
[types.SET_DASHBOARD_ENABLED](state, enabled) {
state.useDashboardEndpoint = enabled;
},
[types.SET_GETTING_STARTED_EMPTY_STATE](state) {
state.emptyState = 'gettingStarted';
},
......@@ -103,9 +96,6 @@ export default {
[types.SET_ALL_DASHBOARDS](state, dashboards) {
state.allDashboards = dashboards;
},
[types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) {
state.additionalPanelTypesEnabled = enabled;
},
[types.SET_SHOW_ERROR_BANNER](state, enabled) {
state.showErrorBanner = enabled;
},
......
......@@ -7,8 +7,6 @@ export default () => ({
environmentsEndpoint: null,
deploymentsEndpoint: null,
dashboardEndpoint: invalidUrl,
useDashboardEndpoint: false,
additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
showErrorBanner: true,
......
......@@ -13,8 +13,6 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint, default_enabled: true)
push_frontend_feature_flag(:environment_metrics_additional_panel_types)
push_frontend_feature_flag(:prometheus_computed_alerts)
end
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { GlModal, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlModal } from '@gitlab/ui';
import Dashboard from 'ee/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import axios from '~/lib/utils/axios_utils';
import { metricsGroupsAPIResponse, mockApiEndpoint } from 'spec/monitoring/mock_data';
import {
metricsGroupsAPIResponse,
mockApiEndpoint,
mockedQueryResultPayload,
environmentData,
} from 'spec/monitoring/mock_data';
import propsData from 'spec/monitoring/components/dashboard_spec';
import AlertWidget from 'ee/monitoring/components/alert_widget.vue';
import CustomMetricsFormFields from 'ee/custom_metrics/components/custom_metrics_form_fields.vue';
import * as types from '~/monitoring/stores/mutation_types';
describe('Dashboard', () => {
let Component;
......@@ -37,76 +42,20 @@ describe('Dashboard', () => {
mock.restore();
});
describe('metrics with alert', () => {
describe('with license', () => {
beforeEach(() => {
vm = shallowMount(Component, {
propsData: {
...propsData,
hasMetrics: true,
prometheusAlertsAvailable: true,
alertsEndpoint: '/endpoint',
},
store,
});
});
it('shows alert widget and dropdown item', done => {
setTimeout(() => {
expect(vm.find(AlertWidget).exists()).toBe(true);
expect(
vm
.findAll(GlDropdownItem)
.filter(i => i.text() === 'Alerts')
.exists(),
).toBe(true);
done();
});
});
it('shows More actions dropdown on chart', done => {
setTimeout(() => {
expect(
vm
.findAll(GlDropdown)
.filter(d => d.attributes('data-original-title') === 'More actions')
.exists(),
).toBe(true);
done();
});
});
});
describe('without license', () => {
beforeEach(() => {
vm = shallowMount(Component, {
propsData: {
...propsData,
hasMetrics: true,
prometheusAlertsAvailable: false,
alertsEndpoint: '/endpoint',
},
store,
});
});
it('does not show alert widget', done => {
setTimeout(() => {
expect(vm.find(AlertWidget).exists()).toBe(false);
expect(
vm
.findAll(GlDropdownItem)
.filter(i => i.text() === 'Alerts')
.exists(),
).toBe(false);
done();
});
});
});
});
function setupComponentStore(component) {
component.vm.$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsGroupsAPIResponse,
);
component.vm.$store.commit(
`monitoringDashboard/${types.SET_QUERY_RESULT}`,
mockedQueryResultPayload,
);
component.vm.$store.commit(
`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
environmentData,
);
}
describe('add custom metrics', () => {
describe('when not available', () => {
......@@ -133,7 +82,7 @@ describe('Dashboard', () => {
});
describe('when available', () => {
beforeEach(done => {
beforeEach(() => {
vm = shallowMount(Component, {
propsData: {
...propsData,
......@@ -146,11 +95,17 @@ describe('Dashboard', () => {
store,
});
setTimeout(done);
setupComponentStore(vm);
});
it('renders add button on the dashboard', () => {
expect(vm.element.querySelector('.js-add-metric-button').innerText).toContain('Add metric');
it('renders add button on the dashboard', done => {
localVue.nextTick(() => {
expect(vm.element.querySelector('.js-add-metric-button').innerText).toContain(
'Add metric',
);
done();
});
});
it('uses modal for custom metrics form', () => {
......
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { createStore } from '~/monitoring/stores';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import PanelType from 'ee/monitoring/components/panel_type.vue';
import { graphDataPrometheusQueryRange } from 'spec/monitoring/mock_data';
import AlertWidget from 'ee/monitoring/components/alert_widget.vue';
describe('Panel Type', () => {
let localVue;
let panelType;
let store;
const dashboardWidth = 100;
const exampleText = 'example_text';
beforeEach(() => {
window.gon = {
...window.gon,
ee: true,
};
});
describe('metrics with alert', () => {
localVue = createLocalVue();
describe('with license', () => {
beforeEach(done => {
store = createStore();
panelType = shallowMount(PanelType, {
localVue,
propsData: {
clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange,
alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: true,
},
store,
sync: false,
});
panelType.vm.$nextTick(done);
});
afterEach(() => {
panelType.destroy();
});
it('shows alert widget and dropdown item', done => {
localVue.nextTick(() => {
expect(panelType.find(AlertWidget).exists()).toBe(true);
expect(
panelType
.findAll(GlDropdownItem)
.filter(i => i.text() === 'Alerts')
.exists(),
).toBe(true);
done();
});
});
it('shows More actions dropdown on chart', done => {
localVue.nextTick(() => {
expect(
panelType
.findAll(GlDropdown)
.filter(d => d.attributes('data-original-title') === 'More actions')
.exists(),
).toBe(true);
done();
});
});
});
describe('without license', () => {
beforeEach(done => {
store = createStore();
panelType = shallowMount(PanelType, {
localVue,
propsData: {
clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange,
alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: false,
},
sync: false,
store,
});
panelType.vm.$nextTick(done);
});
it('does not show alert widget', done => {
setTimeout(() => {
expect(panelType.find(AlertWidget).exists()).toBe(false);
expect(
panelType
.findAll(GlDropdownItem)
.filter(i => i.text() === 'Alerts')
.exists(),
).toBe(false);
done();
});
});
});
});
});
......@@ -10810,9 +10810,6 @@ msgstr ""
msgid "Metrics|Unexpected deployment data response from prometheus endpoint"
msgstr ""
msgid "Metrics|Unexpected metrics data response from prometheus endpoint"
msgstr ""
msgid "Metrics|Unit label"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { setTestTimeout } from 'helpers/timeout';
import { createStore } from '~/monitoring/stores';
import { GlLink } from '@gitlab/ui';
import { GlAreaChart, GlLineChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
import * as types from '~/monitoring/stores/mutation_types';
import { TEST_HOST } from 'spec/test_constants';
import MonitoringMock, {
import {
deploymentData,
mockProjectPath,
} from '../../../javascripts/monitoring/mock_data';
metricsGroupsAPIResponse,
mockedQueryResultPayload,
mockProjectDir,
mockHost,
} from '../mock_data';
import * as iconUtils from '~/lib/utils/icon_utils';
const mockSvgPathContent = 'mockSvgPathContent';
const mockSha = 'mockSha';
const mockWidgets = 'mockWidgets';
const projectPath = `${TEST_HOST}${mockProjectPath}`;
const commitUrl = `${projectPath}/commit/${mockSha}`;
jest.mock('~/lib/utils/icon_utils', () => ({
getSvgIconPathContent: jest.fn().mockImplementation(
......@@ -33,32 +34,44 @@ describe('Time series component', () => {
let store;
beforeEach(() => {
setTestTimeout(1000);
store = createStore();
store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`, MonitoringMock.data);
store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DATA_SUCCESS}`,
metricsGroupsAPIResponse,
);
store.commit(`monitoringDashboard/${types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS}`, deploymentData);
[, mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].metrics;
// Mock data contains 2 panels, pick the first one
store.commit(`monitoringDashboard/${types.SET_QUERY_RESULT}`, mockedQueryResultPayload);
[mockGraphData] = store.state.monitoringDashboard.dashboard.panel_groups[0].metrics;
makeTimeSeriesChart = (graphData, type) =>
shallowMount(TimeSeries, {
attachToDocument: true,
propsData: {
graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData,
projectPath,
projectPath: `${mockHost}${mockProjectDir}`,
},
slots: {
default: mockWidgets,
},
sync: false,
store,
attachToDocument: true,
});
});
describe('general functions', () => {
let timeSeriesChart;
beforeEach(() => {
beforeEach(done => {
timeSeriesChart = makeTimeSeriesChart(mockGraphData, 'area-chart');
timeSeriesChart.vm.$nextTick(done);
});
it('renders chart title', () => {
......@@ -83,18 +96,24 @@ describe('Time series component', () => {
describe('methods', () => {
describe('formatTooltipText', () => {
const mockDate = deploymentData[0].created_at;
const mockCommitUrl = deploymentData[0].commitUrl;
const generateSeriesData = type => ({
seriesData: [
{
seriesName: timeSeriesChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
dataIndex: 0,
},
],
value: mockDate,
let mockDate;
let mockCommitUrl;
let generateSeriesData;
beforeEach(() => {
mockDate = deploymentData[0].created_at;
mockCommitUrl = deploymentData[0].commitUrl;
generateSeriesData = type => ({
seriesData: [
{
seriesName: timeSeriesChart.vm.chartData[0].name,
componentSubType: type,
value: [mockDate, 5.55555],
dataIndex: 0,
},
],
value: mockDate,
});
});
describe('when series is of line type', () => {
......@@ -104,11 +123,11 @@ describe('Time series component', () => {
});
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
it('formats tooltip content', () => {
const name = 'Core Usage';
const name = 'Pod average';
const value = '5.556';
const dataIndex = 0;
const seriesLabel = timeSeriesChart.find(GlChartSeriesLabel);
......@@ -129,13 +148,13 @@ describe('Time series component', () => {
});
});
describe('when series is of scatter type', () => {
describe('when series is of scatter type, for deployments', () => {
beforeEach(() => {
timeSeriesChart.vm.formatTooltipText(generateSeriesData('scatter'));
});
it('formats tooltip title', () => {
expect(timeSeriesChart.vm.tooltip.title).toBe('31 May 2017, 9:23PM');
expect(timeSeriesChart.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM');
});
it('formats tooltip sha', () => {
......@@ -274,9 +293,9 @@ describe('Time series component', () => {
describe('scatterSeries', () => {
it('utilizes deployment data', () => {
expect(timeSeriesChart.vm.scatterSeries.data).toEqual([
['2017-05-31T21:23:37.881Z', 0],
['2017-05-30T20:08:04.629Z', 0],
['2017-05-30T17:42:38.409Z', 0],
['2019-07-16T10:14:25.589Z', 0],
['2019-07-16T11:14:25.589Z', 0],
['2019-07-16T12:14:25.589Z', 0],
]);
expect(timeSeriesChart.vm.scatterSeries.symbolSize).toBe(14);
......@@ -285,7 +304,7 @@ describe('Time series component', () => {
describe('yAxisLabel', () => {
it('constructs a label for the chart y-axis', () => {
expect(timeSeriesChart.vm.yAxisLabel).toBe('CPU');
expect(timeSeriesChart.vm.yAxisLabel).toBe('Memory Used per Pod');
});
});
});
......@@ -318,6 +337,10 @@ describe('Time series component', () => {
timeSeriesAreaChart.vm.$nextTick(done);
});
afterEach(() => {
timeSeriesAreaChart.destroy();
});
it('is a Vue instance', () => {
expect(glChart.exists()).toBe(true);
expect(glChart.isVueInstance()).toBe(true);
......@@ -343,6 +366,9 @@ describe('Time series component', () => {
});
describe('when tooltip is showing deployment data', () => {
const mockSha = 'mockSha';
const commitUrl = `${mockProjectDir}/commit/${mockSha}`;
beforeEach(done => {
timeSeriesAreaChart.vm.tooltip.isDeployment = true;
timeSeriesAreaChart.vm.$nextTick(done);
......
export const mockHost = 'http://test.host';
export const mockProjectDir = '/frontend-fixtures/environments-project';
export const anomalyDeploymentData = [
......@@ -159,3 +160,171 @@ export const anomalyMockGraphData = {
},
],
};
export const deploymentData = [
{
id: 111,
iid: 3,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
commitUrl:
'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master',
},
created_at: '2019-07-16T10:14:25.589Z',
tag: false,
tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': true,
},
{
id: 110,
iid: 2,
sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
commitUrl:
'http://test.host/frontend-fixtures/environments-project/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
ref: {
name: 'master',
},
created_at: '2019-07-16T11:14:25.589Z',
tag: false,
tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false,
},
{
id: 109,
iid: 1,
sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
commitUrl:
'http://test.host/frontend-fixtures/environments-project/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
ref: {
name: 'update2-readme',
},
created_at: '2019-07-16T12:14:25.589Z',
tag: false,
tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
'last?': false,
},
];
export const metricsNewGroupsAPIResponse = [
{
group: 'System metrics (Kubernetes)',
priority: 5,
panels: [
{
title: 'Memory Usage (Pod average)',
type: 'area-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
{
id: 'system_metrics_kubernetes_container_memory_average',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
label: 'Pod average',
unit: 'MB',
metric_id: 17,
prometheus_endpoint_path:
'/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
appearance: {
line: {
width: 2,
},
},
},
],
},
],
},
];
export const mockedQueryResultPayload = {
metricId: '17_system_metrics_kubernetes_container_memory_average',
result: [
{
metric: {},
values: [
[1563272065.589, '10.396484375'],
[1563272125.589, '10.333984375'],
[1563272185.589, '10.333984375'],
[1563272245.589, '10.333984375'],
[1563272305.589, '10.333984375'],
[1563272365.589, '10.333984375'],
[1563272425.589, '10.38671875'],
[1563272485.589, '10.333984375'],
[1563272545.589, '10.333984375'],
[1563272605.589, '10.333984375'],
[1563272665.589, '10.333984375'],
[1563272725.589, '10.333984375'],
[1563272785.589, '10.396484375'],
[1563272845.589, '10.333984375'],
[1563272905.589, '10.333984375'],
[1563272965.589, '10.3984375'],
[1563273025.589, '10.337890625'],
[1563273085.589, '10.34765625'],
[1563273145.589, '10.337890625'],
[1563273205.589, '10.337890625'],
[1563273265.589, '10.337890625'],
[1563273325.589, '10.337890625'],
[1563273385.589, '10.337890625'],
[1563273445.589, '10.337890625'],
[1563273505.589, '10.337890625'],
[1563273565.589, '10.337890625'],
[1563273625.589, '10.337890625'],
[1563273685.589, '10.337890625'],
[1563273745.589, '10.337890625'],
[1563273805.589, '10.337890625'],
[1563273865.589, '10.390625'],
[1563273925.589, '10.390625'],
],
},
],
};
export const metricsGroupsAPIResponse = [
{
group: 'System metrics (Kubernetes)',
priority: 5,
panels: [
{
title: 'Memory Usage (Pod average)',
type: 'area-chart',
y_label: 'Memory Used per Pod',
weight: 2,
metrics: [
{
id: 'system_metrics_kubernetes_container_memory_average',
query_range:
'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024',
label: 'Pod average',
unit: 'MB',
metric_id: 17,
prometheus_endpoint_path:
'/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024',
appearance: {
line: {
width: 2,
},
},
},
],
},
{
title: 'Core Usage (Total)',
type: 'area-chart',
y_label: 'Total Cores',
weight: 3,
metrics: [
{
id: 'system_metrics_kubernetes_container_cores_total',
query_range:
'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)',
label: 'Total',
unit: 'cores',
metric_id: 13,
},
],
},
],
},
];
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import PanelType from '~/monitoring/components/panel_type.vue';
import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
......@@ -10,15 +11,17 @@ import { anomalyMockGraphData } from '../../frontend/monitoring/mock_data';
import { createStore } from '~/monitoring/stores';
global.IS_EE = true;
global.URL.createObjectURL = jest.fn(() => {});
global.URL.createObjectURL = jest.fn();
describe('Panel Type component', () => {
let axiosMock;
let store;
let panelType;
const dashboardWidth = 100;
const exampleText = 'example_text';
beforeEach(() => {
setTestTimeout(1000);
axiosMock = new AxiosMockAdapter(axios);
});
......@@ -40,6 +43,7 @@ describe('Panel Type component', () => {
graphData: graphDataNoResult,
},
sync: false,
attachToDocument: true,
});
});
......@@ -65,14 +69,13 @@ describe('Panel Type component', () => {
});
describe('when Graph data is available', () => {
const exampleText = 'example_text';
const propsData = {
clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange,
};
beforeEach(() => {
beforeEach(done => {
store = createStore();
panelType = shallowMount(PanelType, {
propsData,
......@@ -80,6 +83,11 @@ describe('Panel Type component', () => {
sync: false,
attachToDocument: true,
});
panelType.vm.$nextTick(done);
});
afterEach(() => {
panelType.destroy();
});
describe('Time Series Chart panel type', () => {
......@@ -110,4 +118,49 @@ describe('Panel Type component', () => {
});
});
});
describe('when downloading metrics data as CSV', () => {
beforeEach(done => {
graphDataPrometheusQueryRange.y_label = 'metric';
store = createStore();
panelType = shallowMount(PanelType, {
propsData: {
clipboardText: exampleText,
dashboardWidth,
graphData: graphDataPrometheusQueryRange,
},
store,
sync: false,
attachToDocument: true,
});
panelType.vm.$nextTick(done);
});
afterEach(() => {
panelType.destroy();
});
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
const header = `timestamp,${graphDataPrometheusQueryRange.y_label}`;
const data = graphDataPrometheusQueryRange.queries[0].result[0].values;
const firstRow = `${data[0][0]},${data[0][1]}`;
const secondRow = `${data[1][0]},${data[1][1]}`;
expect(panelType.vm.csvText).toBe(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
});
});
describe('downloadCsv', () => {
it('produces a link with a Blob', () => {
expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
expect.objectContaining({
size: panelType.vm.csvText.length,
type: 'text/plain',
}),
);
});
});
});
});
This diff is collapsed.
......@@ -317,7 +317,7 @@ describe('Monitoring store actions', () => {
const metric = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics[0];
const state = storeState();
const data = metricsGroupsAPIResponse.data[0].metrics[0].queries[0];
const data = metricsGroupsAPIResponse[0].panels[0].metrics[0];
const response = { data };
mock.onGet('http://test').reply(200, response);
......
......@@ -21,79 +21,46 @@ describe('Monitoring mutations', () => {
beforeEach(() => {
stateCopy.dashboard.panel_groups = [];
groups = metricsGroupsAPIResponse.data;
groups = metricsGroupsAPIResponse;
});
it('adds a key to the group', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.dashboard.panel_groups[0].key).toBe('kubernetes-0');
expect(stateCopy.dashboard.panel_groups[1].key).toBe('nginx-1');
expect(stateCopy.dashboard.panel_groups[0].key).toBe('system-metrics-kubernetes--0');
});
it('normalizes values', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
const expectedTimestamp = '2017-05-25T08:22:34.925Z';
const expectedValue = 8.0390625;
const [
timestamp,
value,
] = stateCopy.dashboard.panel_groups[0].metrics[0].queries[0].result[0].values[0];
const expectedLabel = 'Pod average';
const { label, query_range } = stateCopy.dashboard.panel_groups[0].metrics[0].metrics[0];
expect(timestamp).toEqual(expectedTimestamp);
expect(value).toEqual(expectedValue);
expect(label).toEqual(expectedLabel);
expect(query_range.length).toBeGreaterThan(0);
});
it('contains two groups that contains, one of which has two queries sorted by priority', () => {
it('contains one group, which it has two panels and one metrics property', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.dashboard.panel_groups).toBeDefined();
expect(stateCopy.dashboard.panel_groups.length).toEqual(2);
expect(stateCopy.dashboard.panel_groups[0].metrics.length).toEqual(2);
expect(stateCopy.dashboard.panel_groups.length).toEqual(1);
expect(stateCopy.dashboard.panel_groups[0].panels.length).toEqual(2);
expect(stateCopy.dashboard.panel_groups[0].panels[0].metrics.length).toEqual(1);
expect(stateCopy.dashboard.panel_groups[0].panels[1].metrics.length).toEqual(1);
});
it('assigns queries a metric id', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.dashboard.panel_groups[1].metrics[0].queries[0].metricId).toEqual('100');
expect(stateCopy.dashboard.panel_groups[0].metrics[0].queries[0].metricId).toEqual(
'17_system_metrics_kubernetes_container_memory_average',
);
});
it('removes the data if all the values from a query are not defined', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, groups);
expect(stateCopy.dashboard.panel_groups[1].metrics[0].queries[0].result.length).toEqual(0);
});
it('assigns metric id of null if metric has no id', () => {
stateCopy.dashboard.panel_groups = [];
const noId = groups.map(group => ({
...group,
...{
metrics: group.metrics.map(metric => {
const { id, ...metricWithoutId } = metric;
return metricWithoutId;
}),
},
}));
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, noId);
stateCopy.dashboard.panel_groups.forEach(group => {
group.metrics.forEach(metric => {
expect(metric.queries.every(query => query.metricId === null)).toBe(true);
});
});
});
describe('dashboard endpoint enabled', () => {
describe('dashboard endpoint', () => {
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
beforeEach(() => {
stateCopy.useDashboardEndpoint = true;
});
it('aliases group panels to metrics for backwards compatibility', () => {
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
......@@ -143,7 +110,6 @@ describe('Monitoring mutations', () => {
const result = [{ values: [[0, 1], [1, 1], [1, 3]] }];
beforeEach(() => {
stateCopy.useDashboardEndpoint = true;
const dashboardGroups = metricsDashboardResponse.dashboard.panel_groups;
mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, dashboardGroups);
});
......
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