From 89e3fa72457c6b067fae345ab9afc9ff8e3ca984 Mon Sep 17 00:00:00 2001
From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
Date: Thu, 16 Oct 2025 14:27:47 +0200
Subject: [PATCH] Add open Assistant keyboard shortcut (#112228)
---
package.json | 2 +-
packages/grafana-flamegraph/package.json | 2 +-
.../src/FlameGraphContainer.test.tsx | 5 +-
.../src/FlameGraphHeader.test.tsx | 5 +-
.../core/components/help/HelpModal.test.tsx | 151 ++++++++
public/app/core/components/help/HelpModal.tsx | 327 +++++++++---------
public/app/core/services/keybindingSrv.ts | 28 ++
.../logs/components/panel/LogLine.test.tsx | 5 +-
.../components/panel/LogLineContext.test.tsx | 5 +-
.../components/panel/LogLineDetails.test.tsx | 5 +-
.../components/panel/LogLineMenu.test.tsx | 5 +-
.../logs/components/panel/LogList.test.tsx | 4 +-
.../logs/components/panel/LogListContext.tsx | 2 +-
.../components/panel/LogListControls.test.tsx | 4 +-
.../panel/__mocks__/LogListContext.tsx | 4 +-
.../grafana-pyroscope-datasource/package.json | 2 +-
.../app/plugins/panel/logs/LogsPanel.test.tsx | 2 +-
public/locales/en-US/grafana.json | 2 +
yarn.lock | 14 +-
19 files changed, 397 insertions(+), 177 deletions(-)
create mode 100644 public/app/core/components/help/HelpModal.test.tsx
diff --git a/package.json b/package.json
index 39fc401989a..0b08c45534b 100644
--- a/package.json
+++ b/package.json
@@ -279,7 +279,7 @@
"@formatjs/intl-durationformat": "^0.7.0",
"@glideapps/glide-data-grid": "^6.0.0",
"@grafana/alerting": "workspace:*",
- "@grafana/assistant": "0.0.18",
+ "@grafana/assistant": "0.1.0",
"@grafana/aws-sdk": "0.7.1",
"@grafana/azure-sdk": "0.0.7",
"@grafana/data": "workspace:*",
diff --git a/packages/grafana-flamegraph/package.json b/packages/grafana-flamegraph/package.json
index b7279e5244b..3185d65d5c1 100644
--- a/packages/grafana-flamegraph/package.json
+++ b/packages/grafana-flamegraph/package.json
@@ -83,7 +83,7 @@
"typescript": "5.9.2"
},
"peerDependencies": {
- "@grafana/assistant": "^0.0.17 || ^0.0.18",
+ "@grafana/assistant": "^0.1.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
diff --git a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx
index e8786e42466..10e2eeb8d03 100644
--- a/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx
+++ b/packages/grafana-flamegraph/src/FlameGraphContainer.test.tsx
@@ -10,7 +10,10 @@ import FlameGraphContainer, { labelSearch } from './FlameGraphContainer';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from './constants';
jest.mock('@grafana/assistant', () => ({
- useAssistant: jest.fn(() => [false, null]), // [isAvailable, openAssistant]
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: false,
+ openAssistant: undefined,
+ }),
createAssistantContextItem: jest.fn(),
OpenAssistantButton: () =>
OpenAssistantButton
,
}));
diff --git a/packages/grafana-flamegraph/src/FlameGraphHeader.test.tsx b/packages/grafana-flamegraph/src/FlameGraphHeader.test.tsx
index 59be0ed931e..ce00964a8d5 100644
--- a/packages/grafana-flamegraph/src/FlameGraphHeader.test.tsx
+++ b/packages/grafana-flamegraph/src/FlameGraphHeader.test.tsx
@@ -8,7 +8,10 @@ import FlameGraphHeader from './FlameGraphHeader';
import { ColorScheme, SelectedView } from './types';
jest.mock('@grafana/assistant', () => ({
- useAssistant: jest.fn(() => [false, null]), // [isAvailable, openAssistant]
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: false,
+ openAssistant: undefined,
+ }),
createAssistantContextItem: jest.fn(),
OpenAssistantButton: () => OpenAssistantButton
,
}));
diff --git a/public/app/core/components/help/HelpModal.test.tsx b/public/app/core/components/help/HelpModal.test.tsx
new file mode 100644
index 00000000000..d343c5560b0
--- /dev/null
+++ b/public/app/core/components/help/HelpModal.test.tsx
@@ -0,0 +1,151 @@
+import { renderHook } from '@testing-library/react';
+
+import { useAssistant } from '@grafana/assistant';
+
+import { useShortcuts } from './HelpModal';
+
+// Mock the assistant hook
+jest.mock('@grafana/assistant', () => ({
+ useAssistant: jest.fn(),
+}));
+
+// Mock getModKey
+jest.mock('app/core/utils/browser', () => ({
+ getModKey: jest.fn(() => 'ctrl'),
+}));
+
+const mockUseAssistant = useAssistant as jest.MockedFunction;
+
+describe('useShortcuts', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return shortcuts without assistant shortcut when assistant is not available', () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ toggleAssistant: jest.fn(),
+ } as unknown as ReturnType);
+
+ const { result } = renderHook(() => useShortcuts());
+
+ expect(result.current).toHaveLength(4); // Global, Time range, Dashboard, Focused panel
+
+ // Check that global shortcuts don't include assistant shortcut
+ const globalCategory = result.current.find((category) => category.category.includes('Global'));
+ expect(globalCategory).toBeDefined();
+
+ const assistantShortcut = globalCategory!.shortcuts.find((shortcut) => shortcut.keys.includes('ctrl + .'));
+ expect(assistantShortcut).toBeUndefined();
+ });
+
+ it('should return shortcuts with assistant shortcut when assistant is available', () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ const { result } = renderHook(() => useShortcuts());
+
+ expect(result.current).toHaveLength(4); // Global, Time range, Dashboard, Focused panel
+
+ // Check that global shortcuts include assistant shortcut
+ const globalCategory = result.current.find((category) => category.category.includes('Global'));
+ expect(globalCategory).toBeDefined();
+
+ const assistantShortcut = globalCategory!.shortcuts.find((shortcut) => shortcut.keys.includes('ctrl + .'));
+ expect(assistantShortcut).toBeDefined();
+ expect(assistantShortcut!.description).toContain('Assistant');
+ });
+
+ it('should include all expected shortcut categories', () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ const { result } = renderHook(() => useShortcuts());
+
+ const categories = result.current.map((category) => category.category);
+
+ expect(categories).toEqual(
+ expect.arrayContaining([
+ expect.stringContaining('Global'),
+ expect.stringContaining('Time range'),
+ expect.stringContaining('Dashboard'),
+ expect.stringContaining('Focused panel'),
+ ])
+ );
+ });
+
+ it('should use the correct modKey in shortcuts', () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ const { result } = renderHook(() => useShortcuts());
+
+ // Find a shortcut that uses modKey (like save dashboard)
+ const dashboardCategory = result.current.find((category) => category.category.includes('Dashboard'));
+ const saveShortcut = dashboardCategory!.shortcuts.find((shortcut) =>
+ shortcut.description.includes('Save dashboard')
+ );
+
+ expect(saveShortcut).toBeDefined();
+ expect(saveShortcut!.keys[0]).toBe('ctrl + s');
+ });
+
+ it('should memoize results when dependencies do not change', () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ const { result, rerender } = renderHook(() => useShortcuts());
+ const firstResult = result.current;
+
+ // Rerender without changing dependencies
+ rerender();
+
+ // Should return the same reference (memoized)
+ expect(result.current).toBe(firstResult);
+ });
+
+ it('should update when assistant availability changes', () => {
+ mockUseAssistant.mockReturnValue({
+ isAvailable: false,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+
+ const { result, rerender } = renderHook(() => useShortcuts());
+ const firstResult = result.current;
+
+ // Change assistant availability
+ mockUseAssistant.mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ closeAssistant: jest.fn(),
+ toggleAssistant: jest.fn(),
+ });
+ rerender();
+
+ // Should return a different reference (not memoized)
+ expect(result.current).not.toBe(firstResult);
+
+ // And should now include assistant shortcut
+ const globalCategory = result.current.find((category) => category.category.includes('Global'));
+ const assistantShortcut = globalCategory!.shortcuts.find((shortcut) => shortcut.keys.includes('ctrl + .'));
+ expect(assistantShortcut).toBeDefined();
+ });
+});
diff --git a/public/app/core/components/help/HelpModal.tsx b/public/app/core/components/help/HelpModal.tsx
index 375f3494daa..4017443b252 100644
--- a/public/app/core/components/help/HelpModal.tsx
+++ b/public/app/core/components/help/HelpModal.tsx
@@ -1,173 +1,19 @@
import { css } from '@emotion/css';
import { useMemo } from 'react';
+import { useAssistant } from '@grafana/assistant';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans, t } from '@grafana/i18n';
import { Grid, Modal, useStyles2, Text } from '@grafana/ui';
import { getModKey } from 'app/core/utils/browser';
-const getShortcuts = (modKey: string) => {
- return [
- {
- category: t('help-modal.shortcuts-category.global', 'Global'),
- shortcuts: [
- {
- keys: ['g', 'h'],
- description: t('help-modal.shortcuts-description.go-to-home-dashboard', 'Go to Home Dashboard'),
- },
- {
- keys: ['g', 'd'],
- description: t('help-modal.shortcuts-description.go-to-dashboards', 'Go to Dashboards'),
- },
- { keys: ['g', 'e'], description: t('help-modal.shortcuts-description.go-to-explore', 'Go to Explore') },
- { keys: ['g', 'p'], description: t('help-modal.shortcuts-description.go-to-profile', 'Go to Profile') },
- { keys: [`${modKey} + k`], description: t('help-modal.shortcuts-description.open-search', 'Open search') },
- {
- keys: ['esc'],
- description: t('help-modal.shortcuts-description.exit-edit/setting-views', 'Exit edit/setting views'),
- },
- {
- keys: ['?'],
- description: t('help-modal.shortcuts-description.show-all-shortcuts', 'Show all keyboard shortcuts'),
- },
- { keys: ['c', 't'], description: t('help-modal.shortcuts-description.change-theme', 'Change theme') },
- ],
- },
- {
- category: t('help-modal.shortcuts-category.time-range', 'Time range'),
- shortcuts: [
- {
- keys: ['t', 'z'],
- description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'),
- },
- {
- keys: ['t', '←'],
- description: t('help-modal.shortcuts-description.move-time-range-back', 'Move time range back'),
- },
- {
- keys: ['t', '→'],
- description: t('help-modal.shortcuts-description.move-time-range-forward', 'Move time range forward'),
- },
- {
- keys: ['t', 'a'],
- description: t(
- 'help-modal.shortcuts-description.make-time-range-permanent',
- 'Make time range absolute/permanent'
- ),
- },
- {
- keys: ['t', 'c'],
- description: t('help-modal.shortcuts-description.copy-time-range', 'Copy time range'),
- },
- {
- keys: ['t', 'v'],
- description: t('help-modal.shortcuts-description.paste-time-range', 'Paste time range'),
- },
- ],
- },
- {
- category: t('help-modal.shortcuts-category.dashboard', 'Dashboard'),
- shortcuts: [
- {
- keys: [`${modKey} + s`],
- description: t('help-modal.shortcuts-description.save-dashboard', 'Save dashboard'),
- },
- {
- keys: ['d', 'r'],
- description: t('help-modal.shortcuts-description.refresh-all-panels', 'Refresh all panels'),
- },
- {
- keys: ['d', 's'],
- description: t('help-modal.shortcuts-description.dashboard-settings', 'Dashboard settings'),
- },
- {
- keys: ['d', 'v'],
- description: t('help-modal.shortcuts-description.toggle-active-mode', 'Toggle in-active / view mode'),
- },
- {
- keys: ['d', 'k'],
- description: t('help-modal.shortcuts-description.toggle-kiosk', 'Toggle kiosk mode (hides top nav)'),
- },
- {
- keys: ['d', '⇧ + e'],
- description: t('help-modal.shortcuts-description.expand-all-rows', 'Expand all rows'),
- },
- {
- keys: ['d', '⇧ + c'],
- description: t('help-modal.shortcuts-description.collapse-all-rows', 'Collapse all rows'),
- },
- {
- keys: ['d', 'a'],
- description: t(
- 'help-modal.shortcuts-description.toggle-auto-fit',
- 'Toggle auto fit panels (experimental feature)'
- ),
- },
- {
- keys: [`${modKey} + o`],
- description: t('help-modal.shortcuts-description.toggle-graph-crosshair', 'Toggle shared graph crosshair'),
- },
- {
- keys: ['d', 'l'],
- description: t('help-modal.shortcuts-description.toggle-all-panel-legends', 'Toggle all panel legends'),
- },
- {
- keys: ['d', 'x'],
- description: t('help-modal.shortcuts-description.toggle-exemplars', 'Toggle exemplars in all panel'),
- },
- ],
- },
- {
- category: t('help-modal.shortcuts-category.focused-panel', 'Focused panel'),
- shortcuts: [
- {
- keys: ['e'],
- description: t('help-modal.shortcuts-description.toggle-panel-edit', 'Toggle panel edit view'),
- },
- {
- keys: ['v'],
- description: t('help-modal.shortcuts-description.toggle-panel-fullscreen', 'Toggle panel fullscreen view'),
- },
- {
- keys: ['p', 'u'],
- description: t('help-modal.shortcuts-description.open-share-link-drawer', 'Share panel link'),
- },
- {
- keys: ['p', 'e'],
- description: t('help-modal.shortcuts-description.open-share-embed-drawer', 'Share panel embed'),
- },
- {
- keys: ['p', 's'],
- description: t('help-modal.shortcuts-description.open-shared-modal', 'Share panel snapshot'),
- },
- {
- keys: ['p', 'x'],
- description: t('help-modal.shortcuts-description.explore-panel', 'Explore panel'),
- },
- {
- keys: ['i'],
- description: t('help-modal.shortcuts-description.inspect-panel', 'Inspect panel'),
- },
- { keys: ['p', 'd'], description: t('help-modal.shortcuts-description.duplicate-panel', 'Duplicate Panel') },
- { keys: ['p', 'r'], description: t('help-modal.shortcuts-description.remove-panel', 'Remove Panel') },
- {
- keys: ['p', 'l'],
- description: t('help-modal.shortcuts-description.toggle-panel-legend', 'Toggle panel legend'),
- },
- ],
- },
- ];
-};
-
export interface HelpModalProps {
onDismiss: () => void;
}
export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => {
const styles = useStyles2(getStyles);
-
- const modKey = useMemo(() => getModKey(), []);
- const shortcuts = useMemo(() => getShortcuts(modKey), [modKey]);
+ const shortcuts = useShortcuts();
return (
@@ -213,6 +59,175 @@ export const HelpModal = ({ onDismiss }: HelpModalProps): JSX.Element => {
);
};
+export const useShortcuts = () => {
+ const { isAvailable: assistantAvailable } = useAssistant();
+ const modKey = useMemo(() => getModKey(), []);
+
+ return useMemo(() => {
+ const globalShortcuts = [
+ {
+ keys: ['g', 'h'],
+ description: t('help-modal.shortcuts-description.go-to-home-dashboard', 'Go to Home Dashboard'),
+ },
+ {
+ keys: ['g', 'd'],
+ description: t('help-modal.shortcuts-description.go-to-dashboards', 'Go to Dashboards'),
+ },
+ { keys: ['g', 'e'], description: t('help-modal.shortcuts-description.go-to-explore', 'Go to Explore') },
+ { keys: ['g', 'p'], description: t('help-modal.shortcuts-description.go-to-profile', 'Go to Profile') },
+ { keys: ['g', 'a'], description: t('help-modal.shortcuts-description.open-alerting', 'Go to Alerting') },
+ { keys: [`${modKey} + k`], description: t('help-modal.shortcuts-description.open-search', 'Open search') },
+ {
+ keys: ['esc'],
+ description: t('help-modal.shortcuts-description.exit-edit/setting-views', 'Exit edit/setting views'),
+ },
+ {
+ keys: ['?'],
+ description: t('help-modal.shortcuts-description.show-all-shortcuts', 'Show all keyboard shortcuts'),
+ },
+ { keys: ['c', 't'], description: t('help-modal.shortcuts-description.change-theme', 'Change theme') },
+ ];
+
+ // Add assistant shortcut only if assistant is available
+ if (assistantAvailable) {
+ globalShortcuts.push({
+ keys: [`${modKey} + .`],
+ description: t('help-modal.shortcuts-description.open-assistant', 'Open Assistant'),
+ });
+ }
+
+ return [
+ {
+ category: t('help-modal.shortcuts-category.global', 'Global'),
+ shortcuts: globalShortcuts,
+ },
+ {
+ category: t('help-modal.shortcuts-category.time-range', 'Time range'),
+ shortcuts: [
+ {
+ keys: ['t', 'z'],
+ description: t('help-modal.shortcuts-description.zoom-out-time-range', 'Zoom out time range'),
+ },
+ {
+ keys: ['t', '←'],
+ description: t('help-modal.shortcuts-description.move-time-range-back', 'Move time range back'),
+ },
+ {
+ keys: ['t', '→'],
+ description: t('help-modal.shortcuts-description.move-time-range-forward', 'Move time range forward'),
+ },
+ {
+ keys: ['t', 'a'],
+ description: t(
+ 'help-modal.shortcuts-description.make-time-range-permanent',
+ 'Make time range absolute/permanent'
+ ),
+ },
+ {
+ keys: ['t', 'c'],
+ description: t('help-modal.shortcuts-description.copy-time-range', 'Copy time range'),
+ },
+ {
+ keys: ['t', 'v'],
+ description: t('help-modal.shortcuts-description.paste-time-range', 'Paste time range'),
+ },
+ ],
+ },
+ {
+ category: t('help-modal.shortcuts-category.dashboard', 'Dashboard'),
+ shortcuts: [
+ {
+ keys: [`${modKey} + s`],
+ description: t('help-modal.shortcuts-description.save-dashboard', 'Save dashboard'),
+ },
+ {
+ keys: ['d', 'r'],
+ description: t('help-modal.shortcuts-description.refresh-all-panels', 'Refresh all panels'),
+ },
+ {
+ keys: ['d', 's'],
+ description: t('help-modal.shortcuts-description.dashboard-settings', 'Dashboard settings'),
+ },
+ {
+ keys: ['d', 'v'],
+ description: t('help-modal.shortcuts-description.toggle-active-mode', 'Toggle in-active / view mode'),
+ },
+ {
+ keys: ['d', 'k'],
+ description: t('help-modal.shortcuts-description.toggle-kiosk', 'Toggle kiosk mode (hides top nav)'),
+ },
+ {
+ keys: ['d', '⇧ + e'],
+ description: t('help-modal.shortcuts-description.expand-all-rows', 'Expand all rows'),
+ },
+ {
+ keys: ['d', '⇧ + c'],
+ description: t('help-modal.shortcuts-description.collapse-all-rows', 'Collapse all rows'),
+ },
+ {
+ keys: ['d', 'a'],
+ description: t(
+ 'help-modal.shortcuts-description.toggle-auto-fit',
+ 'Toggle auto fit panels (experimental feature)'
+ ),
+ },
+ {
+ keys: [`${modKey} + o`],
+ description: t('help-modal.shortcuts-description.toggle-graph-crosshair', 'Toggle shared graph crosshair'),
+ },
+ {
+ keys: ['d', 'l'],
+ description: t('help-modal.shortcuts-description.toggle-all-panel-legends', 'Toggle all panel legends'),
+ },
+ {
+ keys: ['d', 'x'],
+ description: t('help-modal.shortcuts-description.toggle-exemplars', 'Toggle exemplars in all panel'),
+ },
+ ],
+ },
+ {
+ category: t('help-modal.shortcuts-category.focused-panel', 'Focused panel'),
+ shortcuts: [
+ {
+ keys: ['e'],
+ description: t('help-modal.shortcuts-description.toggle-panel-edit', 'Toggle panel edit view'),
+ },
+ {
+ keys: ['v'],
+ description: t('help-modal.shortcuts-description.toggle-panel-fullscreen', 'Toggle panel fullscreen view'),
+ },
+ {
+ keys: ['p', 'u'],
+ description: t('help-modal.shortcuts-description.open-share-link-drawer', 'Share panel link'),
+ },
+ {
+ keys: ['p', 'e'],
+ description: t('help-modal.shortcuts-description.open-share-embed-drawer', 'Share panel embed'),
+ },
+ {
+ keys: ['p', 's'],
+ description: t('help-modal.shortcuts-description.open-shared-modal', 'Share panel snapshot'),
+ },
+ {
+ keys: ['p', 'x'],
+ description: t('help-modal.shortcuts-description.explore-panel', 'Explore panel'),
+ },
+ {
+ keys: ['i'],
+ description: t('help-modal.shortcuts-description.inspect-panel', 'Inspect panel'),
+ },
+ { keys: ['p', 'd'], description: t('help-modal.shortcuts-description.duplicate-panel', 'Duplicate Panel') },
+ { keys: ['p', 'r'], description: t('help-modal.shortcuts-description.remove-panel', 'Remove Panel') },
+ {
+ keys: ['p', 'l'],
+ description: t('help-modal.shortcuts-description.toggle-panel-legend', 'Toggle panel legend'),
+ },
+ ],
+ },
+ ];
+ }, [modKey, assistantAvailable]);
+};
+
interface KeyProps {
children: string;
}
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 37f70eb6b9a..9b4202b74dc 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -1,3 +1,4 @@
+import { toggleAssistant, isAssistantAvailable } from '@grafana/assistant';
import { LegacyGraphHoverClearEvent, SetPanelAttentionEvent, locationUtil } from '@grafana/data';
import { LocationService } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
@@ -38,6 +39,7 @@ export class KeybindingSrv {
}
/** string for VizPanel key and number for panelId */
private panelId: string | number | null = null;
+ private assistantSubscription: { unsubscribe: () => void } | null = null;
clearAndInitGlobalBindings(route: RouteDescriptor) {
mousetrap.reset();
@@ -51,6 +53,8 @@ export class KeybindingSrv {
this.bind('g e', this.goToExplore);
this.bind('g a', this.openAlerting);
this.bind('g p', this.goToProfile);
+ // Conditionally bind open Assistant shortcut ('o a') if Assistant is available
+ this.bindAssistantShortcutIfAvailable();
this.bind('esc', this.exit);
this.bindGlobalEsc();
}
@@ -120,6 +124,30 @@ export class KeybindingSrv {
appEvents.publish(new ShowModalReactEvent({ component: HelpModal }));
}
+ private bindAssistantShortcutIfAvailable() {
+ // Clean up any existing subscription
+ if (this.assistantSubscription) {
+ this.assistantSubscription.unsubscribe();
+ }
+ // Subscribe to assistant availability and bind/unbind shortcut accordingly
+ this.assistantSubscription = isAssistantAvailable().subscribe((available) => {
+ if (available) {
+ this.bind('mod+.', this.toggleAssistant);
+ } else {
+ // Unbind the shortcut if assistant becomes unavailable
+ mousetrap.unbind('mod+.');
+ }
+ });
+ }
+
+ private toggleAssistant() {
+ toggleAssistant({
+ origin: 'grafana/keyboard-shortcut',
+ prompt: '',
+ context: [],
+ });
+ }
+
private exit() {
const search = this.locationService.getSearchObject();
diff --git a/public/app/features/logs/components/panel/LogLine.test.tsx b/public/app/features/logs/components/panel/LogLine.test.tsx
index 6e4b1599398..5222fae50d9 100644
--- a/public/app/features/logs/components/panel/LogLine.test.tsx
+++ b/public/app/features/logs/components/panel/LogLine.test.tsx
@@ -18,7 +18,10 @@ import { LogLineVirtualization } from './virtualization';
jest.mock('@grafana/assistant', () => ({
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn(() => [true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ }),
}));
jest.mock('./LogListContext');
diff --git a/public/app/features/logs/components/panel/LogLineContext.test.tsx b/public/app/features/logs/components/panel/LogLineContext.test.tsx
index 8b88a9cb2c9..fc6e6ed2a13 100644
--- a/public/app/features/logs/components/panel/LogLineContext.test.tsx
+++ b/public/app/features/logs/components/panel/LogLineContext.test.tsx
@@ -14,7 +14,10 @@ import { DEFAULT_TIME_WINDOW, LogLineContext, PAGE_SIZE } from './LogLineContext
jest.mock('@grafana/assistant', () => ({
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn(() => [true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ }),
}));
const dfBefore = createDataFrame({
diff --git a/public/app/features/logs/components/panel/LogLineDetails.test.tsx b/public/app/features/logs/components/panel/LogLineDetails.test.tsx
index ccb35f93064..7deccf416ff 100644
--- a/public/app/features/logs/components/panel/LogLineDetails.test.tsx
+++ b/public/app/features/logs/components/panel/LogLineDetails.test.tsx
@@ -30,7 +30,10 @@ import { defaultValue } from './__mocks__/LogListContext';
jest.mock('@grafana/assistant', () => {
return {
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ }),
};
});
diff --git a/public/app/features/logs/components/panel/LogLineMenu.test.tsx b/public/app/features/logs/components/panel/LogLineMenu.test.tsx
index a8c03ea385e..78e11921e8a 100644
--- a/public/app/features/logs/components/panel/LogLineMenu.test.tsx
+++ b/public/app/features/logs/components/panel/LogLineMenu.test.tsx
@@ -15,7 +15,10 @@ jest.mock('./LogListContext');
jest.mock('@grafana/assistant', () => ({
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn(() => [true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ openAssistant: jest.fn(),
+ }),
}));
jest.mock('@grafana/runtime', () => ({
diff --git a/public/app/features/logs/components/panel/LogList.test.tsx b/public/app/features/logs/components/panel/LogList.test.tsx
index 7f292ab7216..db4d3904577 100644
--- a/public/app/features/logs/components/panel/LogList.test.tsx
+++ b/public/app/features/logs/components/panel/LogList.test.tsx
@@ -21,7 +21,9 @@ import { LogList, Props } from './LogList';
jest.mock('@grafana/assistant', () => ({
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn(() => [true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ }),
}));
jest.mock('@grafana/runtime', () => {
diff --git a/public/app/features/logs/components/panel/LogListContext.tsx b/public/app/features/logs/components/panel/LogListContext.tsx
index 9ec63e2765d..bb30028523e 100644
--- a/public/app/features/logs/components/panel/LogListContext.tsx
+++ b/public/app/features/logs/components/panel/LogListContext.tsx
@@ -265,7 +265,7 @@ export const LogListContextProvider = ({
const [detailsMode, setDetailsMode] = useState(
detailsModeProp ?? getDefaultDetailsMode(containerElement)
);
- const [isAssistantAvailable, openAssistant] = useAssistant();
+ const { isAvailable: isAssistantAvailable, openAssistant } = useAssistant();
const [prettifyJSON, setPrettifyJSONState] = useState(prettifyJSONProp);
const [wrapLogMessage, setWrapLogMessageState] = useState(wrapLogMessageProp);
diff --git a/public/app/features/logs/components/panel/LogListControls.test.tsx b/public/app/features/logs/components/panel/LogListControls.test.tsx
index bfd71304f2f..29955c8d46d 100644
--- a/public/app/features/logs/components/panel/LogListControls.test.tsx
+++ b/public/app/features/logs/components/panel/LogListControls.test.tsx
@@ -49,7 +49,9 @@ jest.mock('../../utils', () => ({
jest.mock('@grafana/assistant', () => {
return {
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ }),
};
});
diff --git a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
index 03e5e394870..af289c71f2c 100644
--- a/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
+++ b/public/app/features/logs/components/panel/__mocks__/LogListContext.tsx
@@ -9,7 +9,9 @@ import { LogListModel } from '../processing';
jest.mock('@grafana/assistant', () => {
return {
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({
+ isAvailable: true,
+ }),
};
});
diff --git a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json
index fa5a5a2d12a..80b07923827 100644
--- a/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json
+++ b/public/app/plugins/datasource/grafana-pyroscope-datasource/package.json
@@ -39,7 +39,7 @@
"webpack": "5.101.0"
},
"peerDependencies": {
- "@grafana/assistant": "^0.0.17 || ^0.0.18",
+ "@grafana/assistant": "^0.1.0",
"@grafana/runtime": "*"
},
"scripts": {
diff --git a/public/app/plugins/panel/logs/LogsPanel.test.tsx b/public/app/plugins/panel/logs/LogsPanel.test.tsx
index bd879def92a..b3d6aee7d6d 100644
--- a/public/app/plugins/panel/logs/LogsPanel.test.tsx
+++ b/public/app/plugins/panel/logs/LogsPanel.test.tsx
@@ -61,7 +61,7 @@ jest.mock('@grafana/data', () => ({
jest.mock('@grafana/assistant', () => {
return {
...jest.requireActual('@grafana/assistant'),
- useAssistant: jest.fn().mockReturnValue([true, jest.fn()]),
+ useAssistant: jest.fn().mockReturnValue({ isAvailable: true }),
};
});
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 9c91547b203..dbcb5040a1e 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -9097,6 +9097,8 @@
"make-time-range-permanent": "Make time range absolute/permanent",
"move-time-range-back": "Move time range back",
"move-time-range-forward": "Move time range forward",
+ "open-alerting": "Go to Alerting",
+ "open-assistant": "Open Assistant",
"open-search": "Open search",
"open-share-embed-drawer": "Share panel embed",
"open-share-link-drawer": "Share panel link",
diff --git a/yarn.lock b/yarn.lock
index ab4d9f3cc1b..5f02d48c5c1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2536,7 +2536,7 @@ __metadata:
typescript: "npm:5.9.2"
webpack: "npm:5.101.0"
peerDependencies:
- "@grafana/assistant": ^0.0.17 || ^0.0.18
+ "@grafana/assistant": ^0.1.0
"@grafana/runtime": "*"
languageName: unknown
linkType: soft
@@ -2993,9 +2993,9 @@ __metadata:
languageName: unknown
linkType: soft
-"@grafana/assistant@npm:0.0.18":
- version: 0.0.18
- resolution: "@grafana/assistant@npm:0.0.18"
+"@grafana/assistant@npm:0.1.0":
+ version: 0.1.0
+ resolution: "@grafana/assistant@npm:0.1.0"
peerDependencies:
"@grafana/data": ">=12.1.0"
"@grafana/runtime": ">=12.1.0"
@@ -3003,7 +3003,7 @@ __metadata:
"@grafana/ui": ">=12.1.0"
react: ">=18.0.0"
rxjs: ">=7.0.0"
- checksum: 10/6e95f6e7026121f220a20064aa1fc715cf969d93e02b506268dd589d4400b399a6e7d1f2a1a69edb973b604be932c7b091deee1960f90eccf8ea53b67314438e
+ checksum: 10/90cfee9860bc128190ec1357e554685892b3ddd080d914457da311f1d45294dfcc15a427be36c381463eefe39268b5abbc39e6b541ad402e78264cc6d038c23f
languageName: node
linkType: hard
@@ -3223,7 +3223,7 @@ __metadata:
tslib: "npm:2.8.1"
typescript: "npm:5.9.2"
peerDependencies:
- "@grafana/assistant": ^0.0.17 || ^0.0.18
+ "@grafana/assistant": ^0.1.0
react: ^18.0.0
react-dom: ^18.0.0
languageName: unknown
@@ -18277,7 +18277,7 @@ __metadata:
"@formatjs/intl-durationformat": "npm:^0.7.0"
"@glideapps/glide-data-grid": "npm:^6.0.0"
"@grafana/alerting": "workspace:*"
- "@grafana/assistant": "npm:0.0.18"
+ "@grafana/assistant": "npm:0.1.0"
"@grafana/aws-sdk": "npm:0.7.1"
"@grafana/azure-sdk": "npm:0.0.7"
"@grafana/data": "workspace:*"