Commit f5a4733c authored by Tristan Read's avatar Tristan Read Committed by Kushal Pandya

Add runbook to metric dropdown

parent 7fa48bd7
...@@ -311,6 +311,7 @@ export default { ...@@ -311,6 +311,7 @@ export default {
:disabled="formDisabled" :disabled="formDisabled"
data-testid="alertRunbookField" data-testid="alertRunbookField"
type="text" type="text"
:placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
/> />
</gl-form-group> </gl-form-group>
</div> </div>
......
<script> <script>
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { pickBy } from 'lodash'; import { mapValues, pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { convertToFixedRange } from '~/lib/utils/datetime_range'; import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility'; import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { import {
GlResizeObserverDirective, GlResizeObserverDirective,
GlIcon, GlIcon,
GlLink,
GlLoadingIcon, GlLoadingIcon,
GlNewDropdown as GlDropdown, GlNewDropdown as GlDropdown,
GlNewDropdownItem as GlDropdownItem, GlNewDropdownItem as GlDropdownItem,
GlNewDropdownDivider as GlDropdownDivider, GlNewDropdownDivider as GlDropdownDivider,
GlModal, GlModal,
GlModalDirective, GlModalDirective,
GlSprintf,
GlTooltip, GlTooltip,
GlTooltipDirective, GlTooltipDirective,
} from '@gitlab/ui'; } from '@gitlab/ui';
...@@ -44,12 +46,14 @@ export default { ...@@ -44,12 +46,14 @@ export default {
MonitorEmptyChart, MonitorEmptyChart,
AlertWidget, AlertWidget,
GlIcon, GlIcon,
GlLink,
GlLoadingIcon, GlLoadingIcon,
GlTooltip, GlTooltip,
GlDropdown, GlDropdown,
GlDropdownItem, GlDropdownItem,
GlDropdownDivider, GlDropdownDivider,
GlModal, GlModal,
GlSprintf,
}, },
directives: { directives: {
GlResizeObserver: GlResizeObserverDirective, GlResizeObserver: GlResizeObserverDirective,
...@@ -341,6 +345,19 @@ export default { ...@@ -341,6 +345,19 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click(); this.$refs.copyChartLink.$el.firstChild.click();
} }
}, },
getAlertRunbooks(queries) {
const hasRunbook = alert => Boolean(alert.runbookUrl);
const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
const alertToRunbookTransform = alert => {
const alertQuery = queries.find(query => query.metricId === alert.metricId);
return {
key: alert.metricId,
href: alert.runbookUrl,
label: alertQuery.label,
};
};
return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
},
}, },
panelTypes, panelTypes,
}; };
...@@ -436,6 +453,25 @@ export default { ...@@ -436,6 +453,25 @@ export default {
> >
{{ __('Alerts') }} {{ __('Alerts') }}
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item
v-for="runbook in getAlertRunbooks(graphData.metrics)"
:key="runbook.key"
:href="safeUrl(runbook.href)"
data-testid="runbookLink"
target="_blank"
rel="noopener noreferrer"
>
<span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
<span>
<gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
<template #label>
{{ runbook.label }}
</template>
</gl-sprintf>
</span>
<gl-icon name="external-link" />
</span>
</gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length"> <template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider /> <gl-dropdown-divider />
......
---
title: Add runbook to metric chart dropdown
merge_request: 39288
author:
type: added
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
.alert-modal-message { .alert-modal-message {
margin-left: -1rem; margin-left: -1rem;
margin-right: -3rem; margin-right: -1rem;
margin-top: -1rem; margin-top: -1rem;
} }
......
...@@ -15433,6 +15433,9 @@ msgstr "" ...@@ -15433,6 +15433,9 @@ msgstr ""
msgid "Metrics|View logs" msgid "Metrics|View logs"
msgstr "" msgstr ""
msgid "Metrics|View runbook - %{label}"
msgstr ""
msgid "Metrics|Y-axis label" msgid "Metrics|Y-axis label"
msgstr "" msgstr ""
...@@ -19429,6 +19432,9 @@ msgstr "" ...@@ -19429,6 +19432,9 @@ msgstr ""
msgid "PrometheusAlerts|Threshold" msgid "PrometheusAlerts|Threshold"
msgstr "" msgstr ""
msgid "PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks"
msgstr ""
msgid "PrometheusService|%{exporters} with %{metrics} were found" msgid "PrometheusService|%{exporters} with %{metrics} were found"
msgstr "" msgstr ""
......
...@@ -9,6 +9,7 @@ import AlertWidget from '~/monitoring/components/alert_widget.vue'; ...@@ -9,6 +9,7 @@ import AlertWidget from '~/monitoring/components/alert_widget.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue'; import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { import {
mockAlert,
mockLogsHref, mockLogsHref,
mockLogsPath, mockLogsPath,
mockNamespace, mockNamespace,
...@@ -55,9 +56,10 @@ describe('Dashboard Panel', () => { ...@@ -55,9 +56,10 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' }); const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem); const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text); const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text);
const findAlertsWidget = () => wrapper.find(AlertWidget);
const createWrapper = (props, options) => { const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
wrapper = shallowMount(DashboardPanel, { wrapper = mountFn(DashboardPanel, {
propsData: { propsData: {
graphData, graphData,
settingsPath: dashboardProps.settingsPath, settingsPath: dashboardProps.settingsPath,
...@@ -78,6 +80,9 @@ describe('Dashboard Panel', () => { ...@@ -78,6 +80,9 @@ describe('Dashboard Panel', () => {
}); });
}; };
const setMetricsSavedToDb = val =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
beforeEach(() => { beforeEach(() => {
setTestTimeout(1000); setTestTimeout(1000);
...@@ -601,10 +606,6 @@ describe('Dashboard Panel', () => { ...@@ -601,10 +606,6 @@ describe('Dashboard Panel', () => {
}); });
describe('panel alerts', () => { describe('panel alerts', () => {
const setMetricsSavedToDb = val =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
const findAlertsWidget = () => wrapper.find(AlertWidget);
beforeEach(() => { beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []); mockGetterReturnValue('metricsSavedToDb', []);
...@@ -730,4 +731,60 @@ describe('Dashboard Panel', () => { ...@@ -730,4 +731,60 @@ describe('Dashboard Panel', () => {
expect(findManageLinksItem().exists()).toBe(false); expect(findManageLinksItem().exists()).toBe(false);
}); });
}); });
describe('Runbook url', () => {
const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
const { metricId } = graphData.metrics[0];
const { alert_path: alertPath } = mockAlert;
const mockRunbookAlert = {
...mockAlert,
metricId,
};
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
});
it('does not show a runbook link when alerts are not present', () => {
createWrapper();
expect(findRunbookLinks().length).toBe(0);
});
describe('when alerts are present', () => {
beforeEach(() => {
setMetricsSavedToDb([metricId]);
createWrapper({
alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: true,
});
});
it('does not show a runbook link when a runbook is not set', async () => {
findAlertsWidget().vm.$emit('setAlerts', alertPath, {
...mockRunbookAlert,
runbookUrl: '',
});
await wrapper.vm.$nextTick();
expect(findRunbookLinks().length).toBe(0);
});
it('shows a runbook link when a runbook is set', async () => {
findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
await wrapper.vm.$nextTick();
expect(findRunbookLinks().length).toBe(1);
expect(
findRunbookLinks()
.at(0)
.attributes('href'),
).toBe(invalidUrl);
});
});
});
}); });
import invalidUrl from '~/lib/utils/invalid_url';
// This import path needs to be relative for now because this mock data is used in // This import path needs to be relative for now because this mock data is used in
// Karma specs too, where the helpers/test_constants alias can not be resolved // Karma specs too, where the helpers/test_constants alias can not be resolved
import { TEST_HOST } from '../helpers/test_constants'; import { TEST_HOST } from '../helpers/test_constants';
...@@ -630,3 +631,14 @@ export const dashboardActionsMenuProps = { ...@@ -630,3 +631,14 @@ export const dashboardActionsMenuProps = {
validateQueryPath: 'https://path/to/validateQuery', validateQueryPath: 'https://path/to/validateQuery',
isOotbDashboard: true, isOotbDashboard: true,
}; };
export const mockAlert = {
alert_path: 'alert_path',
id: 8,
metricId: 'mock_metric_id',
operator: '>',
query: 'testQuery',
runbookUrl: invalidUrl,
threshold: 5,
title: 'alert title',
};
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