TopNav: KioskMode rewrite move to AppChrome responsibility and make it a global feature (#55149)

* Initial progress

* Moving keybindingSrv to context

* Simplfy KioskMode

* Removed unused logic

* Make kiosk=tv behave as before but when topnav is enabled

* Minor fix

* Fixing tests

* Fixing bug with notice when entering kiosk mode

* Fixed test
pull/55316/head
Torkel Ödegaard 3 years ago committed by GitHub
parent 7352c181c2
commit b8e72d6173
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .betterer.results
  2. 12
      public/app/app.ts
  3. 18
      public/app/core/components/AppChrome/AppChrome.tsx
  4. 73
      public/app/core/components/AppChrome/AppChromeService.tsx
  5. 7
      public/app/core/components/AppChrome/NavToolbar.tsx
  6. 2
      public/app/core/components/MegaMenu/MegaMenu.test.tsx
  7. 5
      public/app/core/components/NavBar/NavBar.tsx
  8. 2
      public/app/core/context/GrafanaContext.ts
  9. 13
      public/app/core/core.ts
  10. 14
      public/app/core/navigation/GrafanaRoute.tsx
  11. 36
      public/app/core/navigation/kiosk.ts
  12. 67
      public/app/core/services/keybindingSrv.ts
  13. 10
      public/app/features/commandPalette/CommandPalette.tsx
  14. 11
      public/app/features/dashboard/components/DashNav/DashNav.tsx
  15. 9
      public/app/features/dashboard/containers/DashboardPage.test.tsx
  16. 6
      public/app/features/dashboard/containers/DashboardPage.tsx
  17. 17
      public/app/features/dashboard/containers/SoloPanelPage.test.tsx
  18. 4
      public/app/features/dashboard/containers/SoloPanelPage.tsx
  19. 11
      public/app/features/dashboard/state/initDashboard.test.ts
  20. 5
      public/app/features/dashboard/state/initDashboard.ts
  21. 1
      public/app/features/explore/Wrapper.tsx
  22. 3
      public/app/features/explore/state/explorePane.ts
  23. 1
      public/app/types/dashboard.ts
  24. 7
      public/test/mocks/getGrafanaContextMock.ts

@ -9352,6 +9352,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Do not use any type assertions.", "4"],
[0, 0, 0, "Do not use any type assertions.", "5"] [0, 0, 0, "Do not use any type assertions.", "5"]
], ],
"public/test/mocks/getGrafanaContextMock.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/test/mocks/workers.ts:5381": [ "public/test/mocks/workers.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "1"],

@ -59,6 +59,7 @@ import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend'; import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend'; import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend';
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend'; import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
import { KeybindingSrv } from './core/services/keybindingSrv';
import { initDevFeatures } from './dev'; import { initDevFeatures } from './dev';
import { getTimeSrv } from './features/dashboard/services/TimeSrv'; import { getTimeSrv } from './features/dashboard/services/TimeSrv';
import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView'; import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView';
@ -157,10 +158,19 @@ export class GrafanaApp {
// Preload selected app plugins // Preload selected app plugins
await preloadPlugins(config.pluginsToPreload); await preloadPlugins(config.pluginsToPreload);
// initialize chrome service
const queryParams = locationService.getSearchObject();
const chromeService = new AppChromeService();
const keybindingsService = new KeybindingSrv(locationService, chromeService);
// Read initial kiosk mode from url at app startup
chromeService.setKioskModeFromUrl(queryParams.kiosk);
this.context = { this.context = {
backend: backendSrv, backend: backendSrv,
location: locationService, location: locationService,
chrome: new AppChromeService(), chrome: chromeService,
keybindings: keybindingsService,
config, config,
}; };

@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { KioskMode } from 'app/types';
import { MegaMenu } from '../MegaMenu/MegaMenu'; import { MegaMenu } from '../MegaMenu/MegaMenu';
@ -23,9 +24,11 @@ export function AppChrome({ children }: Props) {
return <main className="main-view">{children}</main>; return <main className="main-view">{children}</main>;
} }
const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
const contentClass = cx({ const contentClass = cx({
[styles.content]: true, [styles.content]: true,
[styles.contentNoSearchBar]: state.searchBarHidden, [styles.contentNoSearchBar]: searchBarHidden,
[styles.contentChromeless]: state.chromeless, [styles.contentChromeless]: state.chromeless,
}); });
@ -33,21 +36,20 @@ export function AppChrome({ children }: Props) {
<main className="main-view"> <main className="main-view">
{!state.chromeless && ( {!state.chromeless && (
<div className={cx(styles.topNav)}> <div className={cx(styles.topNav)}>
{!state.searchBarHidden && <TopSearchBar />} {!searchBarHidden && <TopSearchBar />}
<NavToolbar <NavToolbar
searchBarHidden={state.searchBarHidden} searchBarHidden={searchBarHidden}
sectionNav={state.sectionNav} sectionNav={state.sectionNav}
pageNav={state.pageNav} pageNav={state.pageNav}
actions={state.actions} actions={state.actions}
onToggleSearchBar={chrome.toggleSearchBar} onToggleSearchBar={chrome.onToggleSearchBar}
onToggleMegaMenu={chrome.toggleMegaMenu} onToggleMegaMenu={chrome.onToggleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/> />
</div> </div>
)} )}
<div className={contentClass}>{children}</div> <div className={contentClass}>{children}</div>
{!state.chromeless && ( {!state.chromeless && <MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />}
<MegaMenu searchBarHidden={state.searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
)}
</main> </main>
); );
} }

@ -1,9 +1,13 @@
import { t } from '@lingui/macro';
import { useObservable } from 'react-use'; import { useObservable } from 'react-use';
import { BehaviorSubject } from 'rxjs'; import { BehaviorSubject } from 'rxjs';
import { NavModelItem } from '@grafana/data'; import { AppEvents, NavModelItem, UrlQueryValue } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
import store from 'app/core/store'; import store from 'app/core/store';
import { isShallowEqual } from 'app/core/utils/isShallowEqual'; import { isShallowEqual } from 'app/core/utils/isShallowEqual';
import { KioskMode } from 'app/types';
import { RouteDescriptor } from '../../navigation/types'; import { RouteDescriptor } from '../../navigation/types';
@ -14,6 +18,7 @@ export interface AppChromeState {
actions?: React.ReactNode; actions?: React.ReactNode;
searchBarHidden?: boolean; searchBarHidden?: boolean;
megaMenuOpen?: boolean; megaMenuOpen?: boolean;
kioskMode: KioskMode | null;
} }
const defaultSection: NavModelItem = { text: 'Grafana' }; const defaultSection: NavModelItem = { text: 'Grafana' };
@ -27,9 +32,10 @@ export class AppChromeService {
chromeless: true, // start out hidden to not flash it on pages without chrome chromeless: true, // start out hidden to not flash it on pages without chrome
sectionNav: defaultSection, sectionNav: defaultSection,
searchBarHidden: store.getBool(this.searchBarStorageKey, false), searchBarHidden: store.getBool(this.searchBarStorageKey, false),
kioskMode: null,
}); });
registerRouteRender(route: RouteDescriptor) { setMatchedRoute(route: RouteDescriptor) {
if (this.currentRoute !== route) { if (this.currentRoute !== route) {
this.currentRoute = route; this.currentRoute = route;
this.routeChangeHandled = false; this.routeChangeHandled = false;
@ -51,6 +57,9 @@ export class AppChromeService {
this.routeChangeHandled = true; this.routeChangeHandled = true;
} }
// KioskMode overrides chromeless state
newState.chromeless = newState.kioskMode === KioskMode.Full || this.currentRoute?.chromeless;
Object.assign(newState, update); Object.assign(newState, update);
if (!isShallowEqual(current, newState)) { if (!isShallowEqual(current, newState)) {
@ -58,7 +67,12 @@ export class AppChromeService {
} }
} }
toggleMegaMenu = () => { useState() {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useObservable(this.state, this.state.getValue());
}
onToggleMegaMenu = () => {
this.update({ megaMenuOpen: !this.state.getValue().megaMenuOpen }); this.update({ megaMenuOpen: !this.state.getValue().megaMenuOpen });
}; };
@ -66,14 +80,59 @@ export class AppChromeService {
this.update({ megaMenuOpen }); this.update({ megaMenuOpen });
}; };
toggleSearchBar = () => { onToggleSearchBar = () => {
const searchBarHidden = !this.state.getValue().searchBarHidden; const searchBarHidden = !this.state.getValue().searchBarHidden;
store.set(this.searchBarStorageKey, searchBarHidden); store.set(this.searchBarStorageKey, searchBarHidden);
this.update({ searchBarHidden }); this.update({ searchBarHidden });
}; };
useState() { onToggleKioskMode = () => {
// eslint-disable-next-line react-hooks/rules-of-hooks const nextMode = this.getNextKioskMode();
return useObservable(this.state, this.state.getValue()); this.update({ kioskMode: nextMode });
locationService.partial({ kiosk: this.getKioskUrlValue(nextMode) });
};
exitKioskMode() {
this.update({ kioskMode: undefined });
locationService.partial({ kiosk: null });
}
setKioskModeFromUrl(kiosk: UrlQueryValue) {
switch (kiosk) {
case 'tv':
this.update({ kioskMode: KioskMode.TV });
break;
case '1':
case true:
this.update({ kioskMode: KioskMode.Full });
}
}
getKioskUrlValue(mode: KioskMode | null) {
switch (mode) {
case KioskMode.TV:
return 'tv';
case KioskMode.Full:
return true;
default:
return null;
}
}
private getNextKioskMode() {
const { kioskMode, searchBarHidden } = this.state.getValue();
if (searchBarHidden || kioskMode === KioskMode.TV) {
appEvents.emit(AppEvents.alertSuccess, [
t({ id: 'navigation.kiosk.tv-alert', message: 'Press ESC to exit kiosk mode' }),
]);
return KioskMode.Full;
}
if (!kioskMode) {
return KioskMode.TV;
}
return null;
} }
} }

@ -13,6 +13,7 @@ import { TOP_BAR_LEVEL_HEIGHT } from './types';
export interface Props { export interface Props {
onToggleSearchBar(): void; onToggleSearchBar(): void;
onToggleMegaMenu(): void; onToggleMegaMenu(): void;
onToggleKioskMode(): void;
searchBarHidden?: boolean; searchBarHidden?: boolean;
sectionNav: NavModelItem; sectionNav: NavModelItem;
pageNav?: NavModelItem; pageNav?: NavModelItem;
@ -26,6 +27,7 @@ export function NavToolbar({
pageNav, pageNav,
onToggleMegaMenu, onToggleMegaMenu,
onToggleSearchBar, onToggleSearchBar,
onToggleKioskMode,
}: Props) { }: Props) {
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav); const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav);
@ -39,7 +41,10 @@ export function NavToolbar({
<div className={styles.actions}> <div className={styles.actions}>
{actions} {actions}
{actions && <NavToolbarSeparator />} {actions && <NavToolbarSeparator />}
<ToolbarButton onClick={onToggleSearchBar} narrow tooltip="Toggle top search bar"> {searchBarHidden && (
<ToolbarButton onClick={onToggleKioskMode} narrow title="Enable kiosk mode" icon="monitor" />
)}
<ToolbarButton onClick={onToggleSearchBar} narrow title="Toggle top search bar">
<Icon name={searchBarHidden ? 'angle-down' : 'angle-up'} size="xl" /> <Icon name={searchBarHidden ? 'angle-down' : 'angle-up'} size="xl" />
</ToolbarButton> </ToolbarButton>
</div> </div>

@ -36,7 +36,7 @@ const setup = () => {
const context = getGrafanaContextMock(); const context = getGrafanaContextMock();
const store = configureStore({ navBarTree }); const store = configureStore({ navBarTree });
context.chrome.toggleMegaMenu(); context.chrome.onToggleMegaMenu();
return render( return render(
<Provider store={store}> <Provider store={store}>

@ -10,7 +10,7 @@ import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime'; import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime';
import { Icon, useTheme2, CustomScrollbar } from '@grafana/ui'; import { Icon, useTheme2, CustomScrollbar } from '@grafana/ui';
import { getKioskMode } from 'app/core/navigation/kiosk'; import { getKioskMode } from 'app/core/navigation/kiosk';
import { KioskMode, StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { OrgSwitcher } from '../OrgSwitcher'; import { OrgSwitcher } from '../OrgSwitcher';
@ -177,9 +177,8 @@ export const NavBar = React.memo(() => {
function shouldHideNavBar(location: HistoryLocation) { function shouldHideNavBar(location: HistoryLocation) {
const queryParams = locationSearchToObject(location.search); const queryParams = locationSearchToObject(location.search);
const kiosk = getKioskMode(queryParams);
if (kiosk !== KioskMode.Off) { if (getKioskMode(queryParams)) {
return true; return true;
} }

@ -5,12 +5,14 @@ import { LocationService } from '@grafana/runtime/src/services/LocationService';
import { BackendSrv } from '@grafana/runtime/src/services/backendSrv'; import { BackendSrv } from '@grafana/runtime/src/services/backendSrv';
import { AppChromeService } from '../components/AppChrome/AppChromeService'; import { AppChromeService } from '../components/AppChrome/AppChromeService';
import { KeybindingSrv } from '../services/keybindingSrv';
export interface GrafanaContextType { export interface GrafanaContextType {
backend: BackendSrv; backend: BackendSrv;
location: LocationService; location: LocationService;
config: GrafanaConfig; config: GrafanaConfig;
chrome: AppChromeService; chrome: AppChromeService;
keybindings: KeybindingSrv;
} }
export const GrafanaContext = React.createContext<GrafanaContextType | undefined>(undefined); export const GrafanaContext = React.createContext<GrafanaContextType | undefined>(undefined);

@ -3,18 +3,7 @@ import { colors, JsonExplorer } from '@grafana/ui/';
import appEvents from './app_events'; import appEvents from './app_events';
import { profiler } from './profiler'; import { profiler } from './profiler';
import { contextSrv } from './services/context_srv'; import { contextSrv } from './services/context_srv';
import { KeybindingSrv } from './services/keybindingSrv';
import TimeSeries, { updateLegendValues } from './time_series2'; import TimeSeries, { updateLegendValues } from './time_series2';
import { assignModelProperties } from './utils/model_utils'; import { assignModelProperties } from './utils/model_utils';
export { export { profiler, appEvents, colors, assignModelProperties, contextSrv, JsonExplorer, TimeSeries, updateLegendValues };
profiler,
appEvents,
colors,
assignModelProperties,
contextSrv,
KeybindingSrv,
JsonExplorer,
TimeSeries,
updateLegendValues,
};

@ -5,21 +5,21 @@ import Drop from 'tether-drop';
import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime'; import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime';
import { useGrafana } from '../context/GrafanaContext'; import { useGrafana } from '../context/GrafanaContext';
import { keybindingSrv } from '../services/keybindingSrv';
import { GrafanaRouteComponentProps, RouteDescriptor } from './types'; import { GrafanaRouteComponentProps, RouteDescriptor } from './types';
export interface Props extends Omit<GrafanaRouteComponentProps, 'queryParams'> {} export interface Props extends Omit<GrafanaRouteComponentProps, 'queryParams'> {}
export function GrafanaRoute(props: Props) { export function GrafanaRoute(props: Props) {
const { chrome } = useGrafana(); const { chrome, keybindings } = useGrafana();
chrome.registerRouteRender(props.route); chrome.setMatchedRoute(props.route);
useEffect(() => { useEffect(() => {
keybindings.clearAndInitGlobalBindings();
updateBodyClassNames(props.route); updateBodyClassNames(props.route);
cleanupDOM(); cleanupDOM();
reportPageview();
navigationLogger('GrafanaRoute', false, 'Mounted', props.match); navigationLogger('GrafanaRoute', false, 'Mounted', props.match);
return () => { return () => {
@ -30,12 +30,6 @@ export function GrafanaRoute(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
useEffect(() => {
// unbinds all and re-bind global keybindins
keybindingSrv.reset();
keybindingSrv.initGlobals();
}, [chrome, props.route]);
useEffect(() => { useEffect(() => {
cleanupDOM(); cleanupDOM();
reportPageview(); reportPageview();

@ -1,33 +1,9 @@
import { t } from '@lingui/macro'; import { UrlQueryMap } from '@grafana/data';
import { AppEvents, UrlQueryMap } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { KioskMode } from '../../types'; import { KioskMode } from '../../types';
import appEvents from '../app_events';
export function toggleKioskMode() {
let kiosk = locationService.getSearchObject().kiosk;
switch (kiosk) {
case 'tv':
kiosk = true;
appEvents.emit(AppEvents.alertSuccess, [
t({ id: 'navigation.kiosk.tv-alert', message: 'Press ESC to exit Kiosk mode' }),
]);
break;
case '1':
case true:
kiosk = null;
break;
default:
kiosk = 'tv';
}
locationService.partial({ kiosk });
}
export function getKioskMode(queryParams: UrlQueryMap): KioskMode { // TODO Remove after topnav feature toggle is permanent and old NavBar is removed
export function getKioskMode(queryParams: UrlQueryMap): KioskMode | null {
switch (queryParams.kiosk) { switch (queryParams.kiosk) {
case 'tv': case 'tv':
return KioskMode.TV; return KioskMode.TV;
@ -36,10 +12,6 @@ export function getKioskMode(queryParams: UrlQueryMap): KioskMode {
case true: case true:
return KioskMode.Full; return KioskMode.Full;
default: default:
return KioskMode.Off; return null;
} }
} }
export function exitKioskMode() {
locationService.partial({ kiosk: null });
}

@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap';
import 'mousetrap-global-bind'; import 'mousetrap-global-bind';
import 'mousetrap/plugins/global-bind/mousetrap-global-bind'; import 'mousetrap/plugins/global-bind/mousetrap-global-bind';
import { LegacyGraphHoverClearEvent, locationUtil } from '@grafana/data'; import { LegacyGraphHoverClearEvent, locationUtil } from '@grafana/data';
import { config, locationService } from '@grafana/runtime'; import { config, LocationService } from '@grafana/runtime';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { getExploreUrl } from 'app/core/utils/explore'; import { getExploreUrl } from 'app/core/utils/explore';
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer'; import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
@ -20,20 +20,20 @@ import {
ZoomOutEvent, ZoomOutEvent,
AbsoluteTimeEvent, AbsoluteTimeEvent,
} from '../../types/events'; } from '../../types/events';
import { AppChromeService } from '../components/AppChrome/AppChromeService';
import { HelpModal } from '../components/help/HelpModal'; import { HelpModal } from '../components/help/HelpModal';
import { contextSrv } from '../core'; import { contextSrv } from '../core';
import { exitKioskMode, toggleKioskMode } from '../navigation/kiosk';
import { toggleTheme } from './toggleTheme'; import { toggleTheme } from './toggleTheme';
import { withFocusedPanel } from './withFocusedPanelId'; import { withFocusedPanel } from './withFocusedPanelId';
export class KeybindingSrv { export class KeybindingSrv {
reset() { constructor(private locationService: LocationService, private chromeService: AppChromeService) {}
clearAndInitGlobalBindings() {
Mousetrap.reset(); Mousetrap.reset();
}
initGlobals() { if (this.locationService.getLocation().pathname !== '/login') {
if (locationService.getLocation().pathname !== '/login') {
this.bind(['?', 'h'], this.showHelpModal); this.bind(['?', 'h'], this.showHelpModal);
this.bind('g h', this.goToHome); this.bind('g h', this.goToHome);
this.bind('g a', this.openAlerting); this.bind('g a', this.openAlerting);
@ -42,7 +42,7 @@ export class KeybindingSrv {
this.bind('t a', this.makeAbsoluteTime); this.bind('t a', this.makeAbsoluteTime);
this.bind('f', this.openSearch); this.bind('f', this.openSearch);
this.bind('esc', this.exit); this.bind('esc', this.exit);
this.bindGlobal('esc', this.globalEsc); this.bindGlobalEsc();
} }
this.bind('t t', () => toggleTheme(false)); this.bind('t t', () => toggleTheme(false));
@ -53,6 +53,10 @@ export class KeybindingSrv {
} }
} }
bindGlobalEsc() {
this.bindGlobal('esc', this.globalEsc);
}
globalEsc() { globalEsc() {
const anyDoc = document as any; const anyDoc = document as any;
const activeElement = anyDoc.activeElement; const activeElement = anyDoc.activeElement;
@ -82,29 +86,29 @@ export class KeybindingSrv {
toggleNav() { toggleNav() {
window.location.href = window.location.href =
config.appSubUrl + config.appSubUrl +
locationUtil.getUrlForPartial(locationService.getLocation(), { locationUtil.getUrlForPartial(this.locationService.getLocation(), {
'__feature.topnav': (!config.featureToggles.topnav).toString(), '__feature.topnav': (!config.featureToggles.topnav).toString(),
}); });
} }
private openSearch() { private openSearch() {
locationService.partial({ search: 'open' }); this.locationService.partial({ search: 'open' });
} }
private closeSearch() { private closeSearch() {
locationService.partial({ search: null }); this.locationService.partial({ search: null });
} }
private openAlerting() { private openAlerting() {
locationService.push('/alerting'); this.locationService.push('/alerting');
} }
private goToHome() { private goToHome() {
locationService.push('/'); this.locationService.push('/');
} }
private goToProfile() { private goToProfile() {
locationService.push('/profile'); this.locationService.push('/profile');
} }
private makeAbsoluteTime() { private makeAbsoluteTime() {
@ -116,30 +120,31 @@ export class KeybindingSrv {
} }
private exit() { private exit() {
const search = locationService.getSearchObject(); const search = this.locationService.getSearchObject();
if (search.editview) { if (search.editview) {
locationService.partial({ editview: null, editIndex: null }); this.locationService.partial({ editview: null, editIndex: null });
return; return;
} }
if (search.inspect) { if (search.inspect) {
locationService.partial({ inspect: null, inspectTab: null }); this.locationService.partial({ inspect: null, inspectTab: null });
return; return;
} }
if (search.editPanel) { if (search.editPanel) {
locationService.partial({ editPanel: null, tab: null }); this.locationService.partial({ editPanel: null, tab: null });
return; return;
} }
if (search.viewPanel) { if (search.viewPanel) {
locationService.partial({ viewPanel: null, tab: null }); this.locationService.partial({ viewPanel: null, tab: null });
return; return;
} }
if (search.kiosk) { const { kioskMode } = this.chromeService.state.getValue();
exitKioskMode(); if (kioskMode) {
this.chromeService.exitKioskMode();
} }
if (search.search) { if (search.search) {
@ -148,7 +153,7 @@ export class KeybindingSrv {
} }
private showDashEditView() { private showDashEditView() {
locationService.partial({ this.locationService.partial({
editview: 'settings', editview: 'settings',
}); });
} }
@ -230,15 +235,15 @@ export class KeybindingSrv {
// edit panel // edit panel
this.bindWithPanelId('e', (panelId) => { this.bindWithPanelId('e', (panelId) => {
if (dashboard.canEditPanelById(panelId)) { if (dashboard.canEditPanelById(panelId)) {
const isEditing = locationService.getSearchObject().editPanel !== undefined; const isEditing = this.locationService.getSearchObject().editPanel !== undefined;
locationService.partial({ editPanel: isEditing ? null : panelId }); this.locationService.partial({ editPanel: isEditing ? null : panelId });
} }
}); });
// view panel // view panel
this.bindWithPanelId('v', (panelId) => { this.bindWithPanelId('v', (panelId) => {
const isViewing = locationService.getSearchObject().viewPanel !== undefined; const isViewing = this.locationService.getSearchObject().viewPanel !== undefined;
locationService.partial({ viewPanel: isViewing ? null : panelId }); this.locationService.partial({ viewPanel: isViewing ? null : panelId });
}); });
//toggle legend //toggle legend
@ -252,7 +257,7 @@ export class KeybindingSrv {
}); });
this.bindWithPanelId('i', (panelId) => { this.bindWithPanelId('i', (panelId) => {
locationService.partial({ inspect: panelId }); this.locationService.partial({ inspect: panelId });
}); });
// jump to explore if permissions allow // jump to explore if permissions allow
@ -268,7 +273,7 @@ export class KeybindingSrv {
if (url) { if (url) {
const urlWithoutBase = locationUtil.stripBaseFromUrl(url); const urlWithoutBase = locationUtil.stripBaseFromUrl(url);
if (urlWithoutBase) { if (urlWithoutBase) {
locationService.push(urlWithoutBase); this.locationService.push(urlWithoutBase);
} }
} }
}); });
@ -322,7 +327,7 @@ export class KeybindingSrv {
}); });
this.bind('d n', () => { this.bind('d n', () => {
locationService.push('/dashboard/new'); this.locationService.push('/dashboard/new');
}); });
this.bind('d r', () => { this.bind('d r', () => {
@ -334,17 +339,15 @@ export class KeybindingSrv {
}); });
this.bind('d k', () => { this.bind('d k', () => {
toggleKioskMode(); this.chromeService.onToggleKioskMode();
}); });
//Autofit panels //Autofit panels
this.bind('d a', () => { this.bind('d a', () => {
// this has to be a full page reload // this has to be a full page reload
const queryParams = locationService.getSearchObject(); const queryParams = this.locationService.getSearchObject();
const newUrlParam = queryParams.autofitpanels ? '' : '&autofitpanels'; const newUrlParam = queryParams.autofitpanels ? '' : '&autofitpanels';
window.location.href = window.location.href + newUrlParam; window.location.href = window.location.href + newUrlParam;
}); });
} }
} }
export const keybindingSrv = new KeybindingSrv();

@ -18,10 +18,9 @@ import { useSelector } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction, locationService } from '@grafana/runtime'; import { reportInteraction, locationService } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { keybindingSrv } from '../../core/services/keybindingSrv';
import { ResultItem } from './ResultItem'; import { ResultItem } from './ResultItem';
import getDashboardNavActions from './actions/dashboard.nav.actions'; import getDashboardNavActions from './actions/dashboard.nav.actions';
import getGlobalActions from './actions/global.static.actions'; import getGlobalActions from './actions/global.static.actions';
@ -33,6 +32,7 @@ import getGlobalActions from './actions/global.static.actions';
export const CommandPalette = () => { export const CommandPalette = () => {
const styles = useStyles2(getSearchStyles); const styles = useStyles2(getSearchStyles);
const { keybindings } = useGrafana();
const [actions, setActions] = useState<Action[]>([]); const [actions, setActions] = useState<Action[]>([]);
const [staticActions, setStaticActions] = useState<Action[]>([]); const [staticActions, setStaticActions] = useState<Action[]>([]);
const { query, showing } = useKBar((state) => ({ const { query, showing } = useKBar((state) => ({
@ -63,14 +63,14 @@ export const CommandPalette = () => {
setActions([...staticActions, ...dashAct]); setActions([...staticActions, ...dashAct]);
}); });
keybindingSrv.bindGlobal('esc', () => { keybindings.bindGlobal('esc', () => {
query.setVisualState(VisualState.animatingOut); query.setVisualState(VisualState.animatingOut);
}); });
} }
return () => { return () => {
keybindingSrv.bindGlobal('esc', () => { keybindings.bindGlobal('esc', () => {
keybindingSrv.globalEsc(); keybindings.globalEsc();
}); });
}; };
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

@ -18,7 +18,7 @@ import {
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbarSeparator'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbarSeparator';
import config from 'app/core/config'; import config from 'app/core/config';
import { toggleKioskMode } from 'app/core/navigation/kiosk'; import { useGrafana } from 'app/core/context/GrafanaContext';
import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal'; import { DashboardCommentsModal } from 'app/features/dashboard/components/DashboardComments/DashboardCommentsModal';
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer'; import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { ShareModal } from 'app/features/dashboard/components/ShareModal';
@ -45,7 +45,7 @@ const selectors = e2eSelectors.pages.Dashboard.DashNav;
export interface OwnProps { export interface OwnProps {
dashboard: DashboardModel; dashboard: DashboardModel;
isFullscreen: boolean; isFullscreen: boolean;
kioskMode: KioskMode; kioskMode?: KioskMode | null;
hideTimePicker: boolean; hideTimePicker: boolean;
folderTitle?: string; folderTitle?: string;
title: string; title: string;
@ -73,6 +73,7 @@ type Props = OwnProps & ConnectedProps<typeof connector>;
export const DashNav = React.memo<Props>((props) => { export const DashNav = React.memo<Props>((props) => {
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const { chrome } = useGrafana();
const onStarDashboard = () => { const onStarDashboard = () => {
const dashboardSrv = getDashboardSrv(); const dashboardSrv = getDashboardSrv();
@ -90,7 +91,7 @@ export const DashNav = React.memo<Props>((props) => {
}; };
const onToggleTVMode = () => { const onToggleTVMode = () => {
toggleKioskMode(); chrome.onToggleKioskMode();
}; };
const onOpenSettings = () => { const onOpenSettings = () => {
@ -127,7 +128,7 @@ export const DashNav = React.memo<Props>((props) => {
const { canStar, canShare, isStarred } = dashboard.meta; const { canStar, canShare, isStarred } = dashboard.meta;
const buttons: ReactNode[] = []; const buttons: ReactNode[] = [];
if (kioskMode !== KioskMode.Off || isPlaylistRunning()) { if (kioskMode || isPlaylistRunning()) {
return []; return [];
} }
@ -235,7 +236,7 @@ export const DashNav = React.memo<Props>((props) => {
const { snapshot } = dashboard; const { snapshot } = dashboard;
const snapshotUrl = snapshot && snapshot.originalUrl; const snapshotUrl = snapshot && snapshot.originalUrl;
const buttons: ReactNode[] = []; const buttons: ReactNode[] = [];
const tvButton = ( const tvButton = config.featureToggles.topnav ? null : (
<ToolbarButton <ToolbarButton
tooltip={t({ id: 'dashboard.toolbar.tv-button', message: 'Cycle view mode' })} tooltip={t({ id: 'dashboard.toolbar.tv-button', message: 'Cycle view mode' })}
icon="monitor" icon="monitor"

@ -5,11 +5,13 @@ import { Router } from 'react-router-dom';
import { useEffectOnce } from 'react-use'; import { useEffectOnce } from 'react-use';
import { AutoSizerProps } from 'react-virtualized-auto-sizer'; import { AutoSizerProps } from 'react-virtualized-auto-sizer';
import { mockToolkitActionCreator } from 'test/core/redux/mocks'; import { mockToolkitActionCreator } from 'test/core/redux/mocks';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { createTheme } from '@grafana/data'; import { createTheme } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, locationService, setDataSourceSrv } from '@grafana/runtime'; import { config, locationService, setDataSourceSrv } from '@grafana/runtime';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps'; import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types'; import { DashboardInitPhase, DashboardMeta, DashboardRoutes } from 'app/types';
@ -130,12 +132,16 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
ctx.props = props; ctx.props = props;
ctx.dashboard = props.dashboard; ctx.dashboard = props.dashboard;
const context = getGrafanaContextMock();
const { container, rerender, unmount } = render( const { container, rerender, unmount } = render(
<GrafanaContext.Provider value={context}>
<Provider store={store}> <Provider store={store}>
<Router history={locationService.getHistory()}> <Router history={locationService.getHistory()}>
<UnthemedDashboardPage {...props} /> <UnthemedDashboardPage {...props} />
</Router> </Router>
</Provider> </Provider>
</GrafanaContext.Provider>
); );
ctx.container = container; ctx.container = container;
@ -144,11 +150,13 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
Object.assign(props, newProps); Object.assign(props, newProps);
rerender( rerender(
<GrafanaContext.Provider value={context}>
<Provider store={store}> <Provider store={store}>
<Router history={locationService.getHistory()}> <Router history={locationService.getHistory()}>
<UnthemedDashboardPage {...props} /> <UnthemedDashboardPage {...props} />
</Router> </Router>
</Provider> </Provider>
</GrafanaContext.Provider>
); );
}; };
@ -179,6 +187,7 @@ describe('DashboardPage', () => {
routeName: 'normal-dashboard', routeName: 'normal-dashboard',
urlSlug: 'my-dash', urlSlug: 'my-dash',
urlUid: '11', urlUid: '11',
keybindingSrv: expect.anything(),
}); });
}); });
}); });

@ -9,6 +9,7 @@ import { Themeable2, withTheme2 } from '@grafana/ui';
import { notifyApp } from 'app/core/actions'; import { notifyApp } from 'app/core/actions';
import { Page } from 'app/core/components/Page/Page'; import { Page } from 'app/core/components/Page/Page';
import { config } from 'app/core/config'; import { config } from 'app/core/config';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { getKioskMode } from 'app/core/navigation/kiosk'; import { getKioskMode } from 'app/core/navigation/kiosk';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@ -96,6 +97,8 @@ export interface State {
} }
export class UnthemedDashboardPage extends PureComponent<Props, State> { export class UnthemedDashboardPage extends PureComponent<Props, State> {
static contextType = GrafanaContext;
private forceRouteReloadCounter = 0; private forceRouteReloadCounter = 0;
state: State = this.getCleanState(); state: State = this.getCleanState();
@ -139,6 +142,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
routeName: this.props.route.routeName, routeName: this.props.route.routeName,
fixUrl: !isPublic, fixUrl: !isPublic,
accessToken: match.params.accessToken, accessToken: match.params.accessToken,
keybindingSrv: this.context.keybindings,
}); });
// small delay to start live updates // small delay to start live updates
@ -336,7 +340,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
} }
const inspectPanel = this.getInspectPanel(); const inspectPanel = this.getInspectPanel();
const showSubMenu = !editPanel && kioskMode === KioskMode.Off && !this.props.queryParams.editview; const showSubMenu = !editPanel && !kioskMode && !this.props.queryParams.editview;
const toolbar = kioskMode !== KioskMode.Full && !queryParams.editview && ( const toolbar = kioskMode !== KioskMode.Full && !queryParams.editview && (
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}> <header data-testid={selectors.pages.Dashboard.DashNav.navV2}>

@ -1,6 +1,8 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { DashboardMeta, DashboardRoutes } from 'app/types'; import { DashboardMeta, DashboardRoutes } from 'app/types';
import { getRouteComponentProps } from '../../../core/navigation/__mocks__/routeProps'; import { getRouteComponentProps } from '../../../core/navigation/__mocks__/routeProps';
@ -83,13 +85,22 @@ function soloPanelPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
ctx.dashboard = props.dashboard; ctx.dashboard = props.dashboard;
let { rerender } = render(<SoloPanelPage {...props} />);
const context = getGrafanaContextMock();
const renderPage = (props: Props) => (
<GrafanaContext.Provider value={context}>
<SoloPanelPage {...props} />
</GrafanaContext.Provider>
);
let { rerender } = render(renderPage(props));
// prop updates will be submitted by rerendering the same component with different props // prop updates will be submitted by rerendering the same component with different props
ctx.rerender = (newProps?: Partial<Props>) => { ctx.rerender = (newProps?: Partial<Props>) => {
Object.assign(props, newProps); rerender(renderPage(Object.assign(props, newProps)));
rerender(<SoloPanelPage {...props} />);
}; };
}, },
rerender: () => { rerender: () => {
// will be replaced while mount() is called // will be replaced while mount() is called
}, },

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect, ConnectedProps } from 'react-redux'; import { connect, ConnectedProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
@ -34,6 +35,8 @@ export interface State {
} }
export class SoloPanelPage extends Component<Props, State> { export class SoloPanelPage extends Component<Props, State> {
static contextType = GrafanaContext;
state: State = { state: State = {
panel: null, panel: null,
notFound: false, notFound: false,
@ -48,6 +51,7 @@ export class SoloPanelPage extends Component<Props, State> {
urlType: match.params.type, urlType: match.params.type,
routeName: route.routeName, routeName: route.routeName,
fixUrl: false, fixUrl: false,
keybindingSrv: this.context.keybindings,
}); });
} }

@ -5,7 +5,7 @@ import { Subject } from 'rxjs';
import { FetchError, locationService, setEchoSrv } from '@grafana/runtime'; import { FetchError, locationService, setEchoSrv } from '@grafana/runtime';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { getBackendSrv } from 'app/core/services/backend_srv'; import { getBackendSrv } from 'app/core/services/backend_srv';
import { keybindingSrv } from 'app/core/services/keybindingSrv'; import { KeybindingSrv } from 'app/core/services/keybindingSrv';
import { variableAdapters } from 'app/features/variables/adapters'; import { variableAdapters } from 'app/features/variables/adapters';
import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter'; import { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
import { constantBuilder } from 'app/features/variables/shared/testing/builders'; import { constantBuilder } from 'app/features/variables/shared/testing/builders';
@ -193,6 +193,9 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
urlUid: DASH_UID, urlUid: DASH_UID,
fixUrl: false, fixUrl: false,
routeName: DashboardRoutes.Normal, routeName: DashboardRoutes.Normal,
keybindingSrv: {
setupDashboardBindings: jest.fn(),
} as unknown as KeybindingSrv,
}, },
backendSrv: getBackendSrv(), backendSrv: getBackendSrv(),
loaderSrv, loaderSrv,
@ -221,8 +224,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
}; };
beforeEach(async () => { beforeEach(async () => {
keybindingSrv.setupDashboardBindings = jest.fn();
setDashboardSrv({ setDashboardSrv({
setCurrent: jest.fn(), setCurrent: jest.fn(),
} as any); } as any);
@ -273,7 +274,7 @@ describeInitScenario('Initializing new dashboard', (ctx) => {
expect(getTimeSrv().init).toBeCalled(); expect(getTimeSrv().init).toBeCalled();
expect(getDashboardSrv().setCurrent).toBeCalled(); expect(getDashboardSrv().setCurrent).toBeCalled();
expect(getDashboardQueryRunner().run).toBeCalled(); expect(getDashboardQueryRunner().run).toBeCalled();
expect(keybindingSrv.setupDashboardBindings).toBeCalled(); expect(ctx.args.keybindingSrv.setupDashboardBindings).toBeCalled();
}); });
}); });
@ -408,7 +409,7 @@ describeInitScenario('Initializing existing dashboard', (ctx) => {
expect(getTimeSrv().init).toBeCalled(); expect(getTimeSrv().init).toBeCalled();
expect(getDashboardSrv().setCurrent).toBeCalled(); expect(getDashboardSrv().setCurrent).toBeCalled();
expect(getDashboardQueryRunner().run).toBeCalled(); expect(getDashboardQueryRunner().run).toBeCalled();
expect(keybindingSrv.setupDashboardBindings).toBeCalled(); expect(ctx.args.keybindingSrv.setupDashboardBindings).toBeCalled();
}); });
it('Should initialize redux variables if newVariables is enabled', () => { it('Should initialize redux variables if newVariables is enabled', () => {

@ -4,7 +4,7 @@ import { notifyApp } from 'app/core/actions';
import appEvents from 'app/core/app_events'; import appEvents from 'app/core/app_events';
import { createErrorNotification } from 'app/core/copy/appNotification'; import { createErrorNotification } from 'app/core/copy/appNotification';
import { backendSrv } from 'app/core/services/backend_srv'; import { backendSrv } from 'app/core/services/backend_srv';
import { keybindingSrv } from 'app/core/services/keybindingSrv'; import { KeybindingSrv } from 'app/core/services/keybindingSrv';
import store from 'app/core/store'; import store from 'app/core/store';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@ -32,6 +32,7 @@ export interface InitDashboardArgs {
accessToken?: string; accessToken?: string;
routeName?: string; routeName?: string;
fixUrl: boolean; fixUrl: boolean;
keybindingSrv: KeybindingSrv;
} }
async function fetchDashboard( async function fetchDashboard(
@ -213,7 +214,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk); dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk);
} }
keybindingSrv.setupDashboardBindings(dashboard); args.keybindingSrv.setupDashboardBindings(dashboard);
} catch (err) { } catch (err) {
if (err instanceof Error) { if (err instanceof Error) {
dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); dispatch(notifyApp(createErrorNotification('Dashboard init failed', err)));

@ -57,6 +57,7 @@ class WrapperUnconnected extends PureComponent<Props> {
//This is needed for breadcrumbs and topnav. //This is needed for breadcrumbs and topnav.
//We should probably abstract this out at some point //We should probably abstract this out at some point
this.context.chrome.update({ sectionNav: this.props.navModel.node }); this.context.chrome.update({ sectionNav: this.props.navModel.node });
this.context.keybindings.setupTimeRangeBindings(false);
lastSavedUrl.left = undefined; lastSavedUrl.left = undefined;
lastSavedUrl.right = undefined; lastSavedUrl.right = undefined;

@ -14,7 +14,6 @@ import {
DataSourceRef, DataSourceRef,
} from '@grafana/data'; } from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime'; import { getDataSourceSrv } from '@grafana/runtime';
import { keybindingSrv } from 'app/core/services/keybindingSrv';
import { import {
DEFAULT_RANGE, DEFAULT_RANGE,
getQueryKeys, getQueryKeys,
@ -178,8 +177,6 @@ export function initializeExplore(
} }
dispatch(updateTime({ exploreId })); dispatch(updateTime({ exploreId }));
keybindingSrv.setupTimeRangeBindings(false);
if (instance) { if (instance) {
// We do not want to add the url to browser history on init because when the pane is initialised it's because // We do not want to add the url to browser history on init because when the pane is initialised it's because
// we already have something in the url. Adding basically the same state as additional history item prevents // we already have something in the url. Adding basically the same state as additional history item prevents

@ -90,7 +90,6 @@ export interface DashboardInitError {
} }
export enum KioskMode { export enum KioskMode {
Off = 'off',
TV = 'tv', TV = 'tv',
Full = 'full', Full = 'full',
} }

@ -2,6 +2,7 @@ import { GrafanaConfig } from '@grafana/data';
import { BackendSrv, LocationService } from '@grafana/runtime'; import { BackendSrv, LocationService } from '@grafana/runtime';
import { AppChromeService } from 'app/core/components/AppChrome/AppChromeService'; import { AppChromeService } from 'app/core/components/AppChrome/AppChromeService';
import { GrafanaContextType } from 'app/core/context/GrafanaContext'; import { GrafanaContextType } from 'app/core/context/GrafanaContext';
import { KeybindingSrv } from 'app/core/services/keybindingSrv';
/** Not sure what this should evolve into, just a starting point */ /** Not sure what this should evolve into, just a starting point */
export function getGrafanaContextMock(overrides: Partial<GrafanaContextType> = {}): GrafanaContextType { export function getGrafanaContextMock(overrides: Partial<GrafanaContextType> = {}): GrafanaContextType {
@ -13,6 +14,12 @@ export function getGrafanaContextMock(overrides: Partial<GrafanaContextType> = {
location: {} as LocationService, location: {} as LocationService,
// eslint-disable-next-line // eslint-disable-next-line
config: {} as GrafanaConfig, config: {} as GrafanaConfig,
// eslint-disable-next-line
keybindings: {
clearAndInitGlobalBindings: jest.fn(),
setupDashboardBindings: jest.fn(),
setupTimeRangeBindings: jest.fn(),
} as any as KeybindingSrv,
...overrides, ...overrides,
}; };
} }

Loading…
Cancel
Save