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>
import { mapActions, mapState, mapGetters } from 'vuex';
import VueDraggable from 'vuedraggable';
import Mousetrap from 'mousetrap';
import { GlIcon, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
import DashboardHeader from './dashboard_header.vue';
import DashboardPanel from './dashboard_panel.vue';
......@@ -24,7 +25,7 @@ import {
expandedPanelPayloadFromUrl,
convertVariablesForURL,
} from '../utils';
import { metricStates } from '../constants';
import { metricStates, keyboardShortcutKeys } from '../constants';
import { defaultTimeRange } from '~/vue_shared/constants';
export default {
......@@ -149,6 +150,7 @@ export default {
selectedTimeRange: timeRangeFromUrl() || defaultTimeRange,
isRearrangingPanels: false,
originalDocumentTitle: document.title,
hoveredPanel: '',
};
},
computed: {
......@@ -214,9 +216,13 @@ export default {
},
created() {
window.addEventListener('keyup', this.onKeyup);
Mousetrap.bind(Object.values(keyboardShortcutKeys), this.runShortcut);
},
destroyed() {
window.removeEventListener('keyup', this.onKeyup);
Mousetrap.unbind(Object.values(keyboardShortcutKeys));
},
mounted() {
if (!this.hasMetrics) {
......@@ -326,6 +332,56 @@ export default {
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: {
goBackLabel: s__('Metrics|Go back (Esc)'),
......@@ -407,6 +463,8 @@ export default {
'draggable-enabled': isRearrangingPanels,
'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
......@@ -420,6 +478,7 @@ export default {
</div>
<dashboard-panel
:ref="`dashboard-panel-${groupData.key}-${graphIndex}`"
:settings-path="settingsPath"
:clipboard-text="generatePanelUrl(groupData.group, graphData)"
:graph-data="graphData"
......
......@@ -2,6 +2,7 @@
import { mapState } from 'vuex';
import { pickBy } from 'lodash';
import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import {
GlResizeObserverDirective,
GlIcon,
......@@ -29,7 +30,6 @@ import MonitorStackedColumnChart from './charts/stacked_column.vue';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import AlertWidget from './alert_widget.vue';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import { isSafeURL } from '~/lib/utils/url_utility';
const events = {
timeRangeZoom: 'timerangezoom',
......@@ -244,6 +244,9 @@ export default {
this.hasMetricsInDb
);
},
alertModalId() {
return `alert-modal-${this.graphData.id}`;
},
},
mounted() {
this.refreshTitleTooltip();
......@@ -282,6 +285,11 @@ export default {
onExpand() {
this.$emit(events.expand);
},
onExpandFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.onExpand();
}
},
setAlerts(alertPath, alertAttributes) {
if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes);
......@@ -292,6 +300,34 @@ export default {
safeUrl(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,
};
......@@ -313,7 +349,7 @@ export default {
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
:modal-id="`alert-modal-${graphData.id}`"
:modal-id="alertModalId"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
......@@ -328,7 +364,7 @@ export default {
ref="contextualMenu"
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
v-gl-tooltip
toggle-class="shadow-none border-0"
......@@ -383,7 +419,7 @@ export default {
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="`alert-modal-${graphData.id}`"
v-gl-modal="alertModalId"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
......
......@@ -251,3 +251,17 @@ export const VARIABLE_TYPES = {
* before passing the data to the backend.
*/
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', () => {
});
});
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', () => {
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