Skip to content

Commit 0630489

Browse files
authored
Dashboard: Keyboard and mouse panel shortcuts improvement (grafana#87317)
* Add custom attention state to dashboard * Add attention state to DashboardGrid * Remove old functionality * Add attention state to keybindingSrv * Create PanelAttentionService * Add PanelAttentionService with scenes support * Remove unused code * Use viz panel key instead of VizPanel * Add type assertion * Add e2e test * Update comments * Use panel id for non-scenes use case * Support undefined service use case * Memoize singleton call * Debounce mouseover * Set panelAttention with appEvents * Use AppEvents for Scenes * Remove panelAttentionSrv * Wait in e2e to handle debounce * Move subscription to KeybindingSrv * Remove imports and reset keyboardShortcuts from main * Fix on* event handlers
1 parent 66d6b3d commit 0630489

File tree

6 files changed

+78
-18
lines changed

6 files changed

+78
-18
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { e2e } from '../utils';
2+
3+
describe('Dashboard Panel Attention', () => {
4+
beforeEach(() => {
5+
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
6+
// Open all panels dashboard
7+
e2e.flows.openDashboard({ uid: 'n1jR8vnnz' });
8+
});
9+
10+
it('Should give panel attention on focus', () => {
11+
e2e.components.Panels.Panel.title('State timeline').focus();
12+
cy.get('body').type('v');
13+
cy.url().should('include', 'viewPanel=41');
14+
});
15+
16+
it('Should give panel attention on hover', () => {
17+
e2e.components.Panels.Panel.title('State timeline').trigger('mousemove');
18+
cy.wait(100); // Wait because of debounce
19+
cy.get('body').type('v');
20+
cy.url().should('include', 'viewPanel=41');
21+
});
22+
23+
it('Should change panel attention between focus and mousemove', () => {
24+
e2e.components.Panels.Panel.title('Size, color mapped to different fields + share view').focus();
25+
e2e.components.Panels.Panel.title('State timeline').trigger('mousemove');
26+
cy.wait(100); // Wait because of debounce
27+
cy.get('body').type('v');
28+
cy.url().should('include', 'viewPanel=41');
29+
});
30+
});

packages/grafana-data/src/events/common.ts

+4
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,7 @@ export class DataSourceTestSucceeded extends BusEventBase {
6464
export class DataSourceTestFailed extends BusEventBase {
6565
static type = 'datasource-test-failed';
6666
}
67+
68+
export class SetPanelAttentionEvent extends BusEventWithPayload<{ panelId: string | number }> {
69+
static type = 'set-panel-attention';
70+
}

packages/grafana-ui/src/components/PanelChrome/PanelChrome.tsx

+13-1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ interface BaseProps {
5656
* callback when opening the panel menu
5757
*/
5858
onOpenMenu?: () => void;
59+
/**
60+
* Used for setting panel attention
61+
*/
62+
onFocus?: () => void;
63+
/**
64+
* Debounce the event handler, if possible
65+
*/
66+
onMouseMove?: () => void;
5967
}
6068

6169
interface FixedDimensions extends BaseProps {
@@ -121,6 +129,8 @@ export function PanelChrome({
121129
collapsible = false,
122130
collapsed,
123131
onToggleCollapse,
132+
onFocus,
133+
onMouseMove,
124134
}: PanelChromeProps) {
125135
const theme = useTheme2();
126136
const styles = useStyles2(getStyles);
@@ -240,7 +250,9 @@ export function PanelChrome({
240250
className={cx(styles.container, { [styles.transparentContainer]: isPanelTransparent })}
241251
style={containerStyles}
242252
data-testid={testid}
243-
tabIndex={0} //eslint-disable-line jsx-a11y/no-noninteractive-tabindex
253+
tabIndex={0} // eslint-disable-line jsx-a11y/no-noninteractive-tabindex
254+
onFocus={onFocus}
255+
onMouseMove={onMouseMove}
244256
ref={ref}
245257
>
246258
<div className={styles.loadingBarContainer}>

public/app/core/services/keybindingSrv.ts

+19-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Mousetrap from 'mousetrap';
22

33
import 'mousetrap-global-bind';
44
import 'mousetrap/plugins/global-bind/mousetrap-global-bind';
5-
import { LegacyGraphHoverClearEvent, locationUtil } from '@grafana/data';
5+
import { LegacyGraphHoverClearEvent, SetPanelAttentionEvent, locationUtil } from '@grafana/data';
66
import { LocationService } from '@grafana/runtime';
77
import appEvents from 'app/core/app_events';
88
import { getExploreUrl } from 'app/core/utils/explore';
@@ -27,13 +27,19 @@ import { contextSrv } from '../core';
2727
import { RouteDescriptor } from '../navigation/types';
2828

2929
import { toggleTheme } from './theme';
30-
import { withFocusedPanel } from './withFocusedPanelId';
3130

3231
export class KeybindingSrv {
3332
constructor(
3433
private locationService: LocationService,
3534
private chromeService: AppChromeService
36-
) {}
35+
) {
36+
// No cleanup needed, since KeybindingSrv is a singleton
37+
appEvents.subscribe(SetPanelAttentionEvent, (event) => {
38+
this.panelId = event.payload.panelId;
39+
});
40+
}
41+
/** string for VizPanel key and number for panelId */
42+
private panelId: string | number | null = null;
3743

3844
clearAndInitGlobalBindings(route: RouteDescriptor) {
3945
Mousetrap.reset();
@@ -182,7 +188,16 @@ export class KeybindingSrv {
182188
}
183189

184190
bindWithPanelId(keyArg: string, fn: (panelId: number) => void) {
185-
this.bind(keyArg, withFocusedPanel(fn));
191+
this.bind(keyArg, this.withFocusedPanel(fn));
192+
}
193+
194+
withFocusedPanel(fn: (panelId: number) => void) {
195+
return () => {
196+
if (typeof this.panelId === 'number') {
197+
fn(this.panelId);
198+
return;
199+
}
200+
};
186201
}
187202

188203
setupTimeRangeBindings(updateUrl = true) {

public/app/core/services/withFocusedPanelId.ts

-12
This file was deleted.

public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { debounce } from 'lodash';
12
import React, { PureComponent } from 'react';
23
import { Subscription } from 'rxjs';
34

@@ -16,6 +17,7 @@ import {
1617
PanelData,
1718
PanelPlugin,
1819
PanelPluginMeta,
20+
SetPanelAttentionEvent,
1921
TimeRange,
2022
toDataFrameDTO,
2123
toUtc,
@@ -30,6 +32,7 @@ import {
3032
SeriesVisibilityChangeMode,
3133
AdHocFilterItem,
3234
} from '@grafana/ui';
35+
import appEvents from 'app/core/app_events';
3336
import config from 'app/core/config';
3437
import { profiler } from 'app/core/profiler';
3538
import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel';
@@ -90,6 +93,7 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
9093

9194
// Can this eventBus be on PanelModel? when we have more complex event filtering, that may be a better option
9295
const eventBus = props.dashboard.events.newScopedBus(`panel:${props.panel.id}`, this.eventFilter);
96+
this.debouncedSetPanelAttention = debounce(this.setPanelAttention.bind(this), 100);
9397

9498
this.state = {
9599
isFirstLoad: true,
@@ -544,11 +548,16 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
544548
);
545549
}
546550

551+
setPanelAttention() {
552+
appEvents.publish(new SetPanelAttentionEvent({ panelId: this.props.panel.id }));
553+
}
554+
555+
debouncedSetPanelAttention() {}
556+
547557
render() {
548558
const { dashboard, panel, width, height, plugin } = this.props;
549559
const { errorMessage, data } = this.state;
550560
const { transparent } = panel;
551-
552561
const panelChromeProps = getPanelChromeProps({ ...this.props, data });
553562

554563
// Shift the hover menu down if it's on the top row so it doesn't get clipped by topnav
@@ -579,6 +588,8 @@ export class PanelStateWrapper extends PureComponent<Props, State> {
579588
displayMode={transparent ? 'transparent' : 'default'}
580589
onCancelQuery={panelChromeProps.onCancelQuery}
581590
onOpenMenu={panelChromeProps.onOpenMenu}
591+
onFocus={() => this.setPanelAttention()}
592+
onMouseMove={() => this.debouncedSetPanelAttention()}
582593
>
583594
{(innerWidth, innerHeight) => (
584595
<>

0 commit comments

Comments
 (0)