Commit 49b1e3ec authored by Phil Hughes's avatar Phil Hughes

Merge branch 'jivanvl-add-keyboard-shortcuts-metrics-dashboard' into 'master'

Add keyboard shortcuts to metrics dashboard

See merge request gitlab-org/gitlab!32804
parents a4ee624d a9e1da31
<script> <script>
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable'; import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui'; import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue'; import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue'; import DashboardPanel from './dashboard_panel.vue';
...@@ -24,7 +25,7 @@ import { ...@@ -24,7 +25,7 @@ import {
expandedPanelPayloadFromUrl, expandedPanelPayloadFromUrl,
convertVariablesForURL, convertVariablesForURL,
} from '../utils'; } from '../utils';
import { metricStates } from '../constants'; import { metricStates, keyboardShortcutKeys } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants'; import { defaultTimeRange } from '~/vue_shared/constants';
export default { export default {
...@@ -149,6 +150,7 @@ export default { ...@@ -149,6 +150,7 @@ export default {
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange, selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
isRearrangingPanels: false, isRearrangingPanels: false,
originalDocumentTitle: document.title, originalDocumentTitle: document.title,
hoveredPanel: '',
}; };
}, },
computed: { computed: {
...@@ -214,9 +216,13 @@ export default { ...@@ -214,9 +216,13 @@ export default {
}, },
created() { created() {
window.addEventListener('keyup', this.onKeyup); window.addEventListener('keyup', this.onKeyup);
Mousetrap.bind(Object.values(keyboardShortcutKeys), this.runShortcut);
}, },
destroyed() { destroyed() {
window.removeEventListener('keyup', this.onKeyup); window.removeEventListener('keyup', this.onKeyup);
Mousetrap.unbind(Object.values(keyboardShortcutKeys));
}, },
mounted() { mounted() {
if (!this.hasMetrics) { if (!this.hasMetrics) {
...@@ -326,6 +332,56 @@ export default { ...@@ -326,6 +332,56 @@ export default {
return isNumberOfPanelsEven || !isLastPanel; return isNumberOfPanelsEven || !isLastPanel;
}, },
/**
* TODO: Investigate this to utilize the eventBus from Vue
* The intentation behind this cleanup is to allow for better tests
* as well as use the correct eventBus facilities that are compatible
* with Vue 3
* https://gitlab.com/gitlab-org/gitlab/-/issues/225583
*/
//
runShortcut(e) {
const panel = this.$refs[this.hoveredPanel];
if (!panel) return;
const [panelInstance] = panel;
let actionToRun = '';
switch (e.key) {
case keyboardShortcutKeys.EXPAND:
actionToRun = 'onExpandFromKeyboardShortcut';
break;
case keyboardShortcutKeys.VISIT_LOGS:
actionToRun = 'visitLogsPageFromKeyboardShortcut';
break;
case keyboardShortcutKeys.SHOW_ALERT:
actionToRun = 'showAlertModalFromKeyboardShortcut';
break;
case keyboardShortcutKeys.DOWNLOAD_CSV:
actionToRun = 'downloadCsvFromKeyboardShortcut';
break;
case keyboardShortcutKeys.CHART_COPY:
actionToRun = 'copyChartLinkFromKeyboardShotcut';
break;
default:
actionToRun = 'onExpandFromKeyboardShortcut';
break;
}
panelInstance[actionToRun]();
},
setHoveredPanel(groupKey, graphIndex) {
this.hoveredPanel = `dashboard-panel-${groupKey}-${graphIndex}`;
},
clearHoveredPanel() {
this.hoveredPanel = '';
},
}, },
i18n: { i18n: {
goBackLabel: s__('Metrics|Go back (Esc)'), goBackLabel: s__('Metrics|Go back (Esc)'),
...@@ -407,6 +463,8 @@ export default { ...@@ -407,6 +463,8 @@ export default {
'draggable-enabled': isRearrangingPanels, 'draggable-enabled': isRearrangingPanels,
'col-lg-6': isPanelHalfWidth(graphIndex, groupData.panels.length), 'col-lg-6': isPanelHalfWidth(graphIndex, groupData.panels.length),
}" }"
@mouseover="setHoveredPanel(groupData.key, graphIndex)"
@mouseout="clearHoveredPanel"
> >
<div class="position-relative draggable-panel js-draggable-panel"> <div class="position-relative draggable-panel js-draggable-panel">
<div <div
...@@ -420,6 +478,7 @@ export default { ...@@ -420,6 +478,7 @@ export default {
</div> </div>
<dashboard-panel <dashboard-panel
:ref="`dashboard-panel-${groupData.key}-${graphIndex}`"
:settings-path="settingsPath" :settings-path="settingsPath"
:clipboard-text="generatePanelUrl(groupData.group, graphData)" :clipboard-text="generatePanelUrl(groupData.group, graphData)"
:graph-data="graphData" :graph-data="graphData"
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { mapState } from 'vuex'; import { mapState } from 'vuex';
import { pickBy } from 'lodash'; import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url'; import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { import {
GlResizeObserverDirective, GlResizeObserverDirective,
GlIcon, GlIcon,
...@@ -29,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue'; ...@@ -29,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event'; import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue'; import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils'; import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { isSafeURL } from '~/lib/utils/url_utility';
const events = { const events = {
timeRangeZoom: 'timerangezoom', timeRangeZoom: 'timerangezoom',
...@@ -244,6 +244,9 @@ export default { ...@@ -244,6 +244,9 @@ export default {
this.hasMetricsInDb this.hasMetricsInDb
); );
}, },
alertModalId() {
return `alert-modal-${this.graphData.id}`;
},
}, },
mounted() { mounted() {
this.refreshTitleTooltip(); this.refreshTitleTooltip();
...@@ -282,6 +285,11 @@ export default { ...@@ -282,6 +285,11 @@ export default {
onExpand() { onExpand() {
this.$emit(events.expand); this.$emit(events.expand);
}, },
onExpandFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.onExpand();
}
},
setAlerts(alertPath, alertAttributes) { setAlerts(alertPath, alertAttributes) {
if (alertAttributes) { if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes); this.$set(this.allAlerts, alertPath, alertAttributes);
...@@ -292,6 +300,34 @@ export default { ...@@ -292,6 +300,34 @@ export default {
safeUrl(url) { safeUrl(url) {
return isSafeURL(url) ? url : '#'; return isSafeURL(url) ? url : '#';
}, },
showAlertModal() {
this.$root.$emit('bv::show::modal', this.alertModalId);
},
showAlertModalFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.showAlertModal();
}
},
visitLogsPage() {
if (this.logsPathWithTimeRange) {
visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL()));
}
},
visitLogsPageFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.visitLogsPage();
}
},
downloadCsvFromKeyboardShortcut() {
if (this.csvText && this.isContextualMenuShown) {
this.$refs.downloadCsvLink.$el.firstChild.click();
}
},
copyChartLinkFromKeyboardShotcut() {
if (this.clipboardText && this.isContextualMenuShown) {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
}, },
panelTypes, panelTypes,
}; };
...@@ -313,7 +349,7 @@ export default { ...@@ -313,7 +349,7 @@ export default {
<alert-widget <alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable" v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1" class="mx-1"
:modal-id="`alert-modal-${graphData.id}`" :modal-id="alertModalId"
:alerts-endpoint="alertsEndpoint" :alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics" :relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)" :alerts-to-manage="getGraphAlerts(graphData.metrics)"
...@@ -328,7 +364,7 @@ export default { ...@@ -328,7 +364,7 @@ export default {
ref="contextualMenu" ref="contextualMenu"
data-qa-selector="prometheus_graph_widgets" data-qa-selector="prometheus_graph_widgets"
> >
<div class="d-flex align-items-center"> <div data-testid="dropdown-wrapper" class="d-flex align-items-center">
<gl-dropdown <gl-dropdown
v-gl-tooltip v-gl-tooltip
toggle-class="shadow-none border-0" toggle-class="shadow-none border-0"
...@@ -383,7 +419,7 @@ export default { ...@@ -383,7 +419,7 @@ export default {
</gl-dropdown-item> </gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
v-if="alertWidgetAvailable" v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${graphData.id}`" v-gl-modal="alertModalId"
data-qa-selector="alert_widget_menu_item" data-qa-selector="alert_widget_menu_item"
> >
{{ __('Alerts') }} {{ __('Alerts') }}
......
...@@ -251,3 +251,17 @@ export const VARIABLE_TYPES = { ...@@ -251,3 +251,17 @@ export const VARIABLE_TYPES = {
* before passing the data to the backend. * before passing the data to the backend.
*/ */
export const VARIABLE_PREFIX = 'var-'; export const VARIABLE_PREFIX = 'var-';
/**
* All of the actions inside each panel dropdown can be accessed
* via keyboard shortcuts than can be activated via mouse hovers
* and or focus via tabs.
*/
export const keyboardShortcutKeys = {
EXPAND: 'e',
VISIT_LOGS: 'l',
SHOW_ALERT: 'a',
DOWNLOAD_CSV: 'd',
CHART_COPY: 'c',
};
---
title: Add keyboard shortcuts to metrics dashboard
merge_request: 32804
author:
type: added
...@@ -1168,6 +1168,34 @@ describe('Dashboard', () => { ...@@ -1168,6 +1168,34 @@ describe('Dashboard', () => {
}); });
}); });
describe('keyboard shortcuts', () => {
const currentDashboard = dashboardGitResponse[1].path;
const panelRef = 'dashboard-panel-response-metrics-aws-elb-4-1'; // skip expanded panel
// While the recommendation in the documentation is to test
// with a data-testid attribute, I want to make sure that
// the dashboard panels have a ref attribute set.
const getDashboardPanel = () => wrapper.find({ ref: panelRef });
beforeEach(() => {
setupStoreWithData(store);
store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
currentDashboard,
});
createShallowWrapper({ hasMetrics: true });
wrapper.setData({ hoveredPanel: panelRef });
return wrapper.vm.$nextTick();
});
it('contains a ref attribute inside a DashboardPanel component', () => {
const dashboardPanel = getDashboardPanel();
expect(dashboardPanel.exists()).toBe(true);
});
});
describe('add custom metrics', () => { describe('add custom metrics', () => {
const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' }); const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' });
......
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