diff --git a/.betterer.results b/.betterer.results
index 6798e873c59..d0d5515d2d8 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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.", "5"]
],
+ "public/test/mocks/getGrafanaContextMock.ts:5381": [
+ [0, 0, 0, "Unexpected any. Specify a different type.", "0"]
+ ],
"public/test/mocks/workers.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
diff --git a/public/app/app.ts b/public/app/app.ts
index 3841fcd79e4..e018e7ddf54 100644
--- a/public/app/app.ts
+++ b/public/app/app.ts
@@ -59,6 +59,7 @@ import { GAEchoBackend } from './core/services/echo/backends/analytics/GABackend
import { RudderstackBackend } from './core/services/echo/backends/analytics/RudderstackBackend';
import { GrafanaJavascriptAgentBackend } from './core/services/echo/backends/grafana-javascript-agent/GrafanaJavascriptAgentBackend';
import { SentryEchoBackend } from './core/services/echo/backends/sentry/SentryBackend';
+import { KeybindingSrv } from './core/services/keybindingSrv';
import { initDevFeatures } from './dev';
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
import { PanelDataErrorView } from './features/panel/components/PanelDataErrorView';
@@ -157,10 +158,19 @@ export class GrafanaApp {
// Preload selected app plugins
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 = {
backend: backendSrv,
location: locationService,
- chrome: new AppChromeService(),
+ chrome: chromeService,
+ keybindings: keybindingsService,
config,
};
diff --git a/public/app/core/components/AppChrome/AppChrome.tsx b/public/app/core/components/AppChrome/AppChrome.tsx
index 879e1a28b08..aefa8d54f21 100644
--- a/public/app/core/components/AppChrome/AppChrome.tsx
+++ b/public/app/core/components/AppChrome/AppChrome.tsx
@@ -5,6 +5,7 @@ import { GrafanaTheme2 } from '@grafana/data';
import { config } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { useGrafana } from 'app/core/context/GrafanaContext';
+import { KioskMode } from 'app/types';
import { MegaMenu } from '../MegaMenu/MegaMenu';
@@ -23,9 +24,11 @@ export function AppChrome({ children }: Props) {
return {children};
}
+ const searchBarHidden = state.searchBarHidden || state.kioskMode === KioskMode.TV;
+
const contentClass = cx({
[styles.content]: true,
- [styles.contentNoSearchBar]: state.searchBarHidden,
+ [styles.contentNoSearchBar]: searchBarHidden,
[styles.contentChromeless]: state.chromeless,
});
@@ -33,21 +36,20 @@ export function AppChrome({ children }: Props) {
{!state.chromeless && (
- {!state.searchBarHidden && }
+ {!searchBarHidden && }
)}
{children}
- {!state.chromeless && (
- chrome.setMegaMenu(false)} />
- )}
+ {!state.chromeless && chrome.setMegaMenu(false)} />}
);
}
diff --git a/public/app/core/components/AppChrome/AppChromeService.tsx b/public/app/core/components/AppChrome/AppChromeService.tsx
index f9092690817..bc984da9c17 100644
--- a/public/app/core/components/AppChrome/AppChromeService.tsx
+++ b/public/app/core/components/AppChrome/AppChromeService.tsx
@@ -1,9 +1,13 @@
+import { t } from '@lingui/macro';
import { useObservable } from 'react-use';
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 { isShallowEqual } from 'app/core/utils/isShallowEqual';
+import { KioskMode } from 'app/types';
import { RouteDescriptor } from '../../navigation/types';
@@ -14,6 +18,7 @@ export interface AppChromeState {
actions?: React.ReactNode;
searchBarHidden?: boolean;
megaMenuOpen?: boolean;
+ kioskMode: KioskMode | null;
}
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
sectionNav: defaultSection,
searchBarHidden: store.getBool(this.searchBarStorageKey, false),
+ kioskMode: null,
});
- registerRouteRender(route: RouteDescriptor) {
+ setMatchedRoute(route: RouteDescriptor) {
if (this.currentRoute !== route) {
this.currentRoute = route;
this.routeChangeHandled = false;
@@ -51,6 +57,9 @@ export class AppChromeService {
this.routeChangeHandled = true;
}
+ // KioskMode overrides chromeless state
+ newState.chromeless = newState.kioskMode === KioskMode.Full || this.currentRoute?.chromeless;
+
Object.assign(newState, update);
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 });
};
@@ -66,14 +80,59 @@ export class AppChromeService {
this.update({ megaMenuOpen });
};
- toggleSearchBar = () => {
+ onToggleSearchBar = () => {
const searchBarHidden = !this.state.getValue().searchBarHidden;
store.set(this.searchBarStorageKey, searchBarHidden);
this.update({ searchBarHidden });
};
- useState() {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- return useObservable(this.state, this.state.getValue());
+ onToggleKioskMode = () => {
+ const nextMode = this.getNextKioskMode();
+ 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;
}
}
diff --git a/public/app/core/components/AppChrome/NavToolbar.tsx b/public/app/core/components/AppChrome/NavToolbar.tsx
index 48d39dba974..727d6a26e6c 100644
--- a/public/app/core/components/AppChrome/NavToolbar.tsx
+++ b/public/app/core/components/AppChrome/NavToolbar.tsx
@@ -13,6 +13,7 @@ import { TOP_BAR_LEVEL_HEIGHT } from './types';
export interface Props {
onToggleSearchBar(): void;
onToggleMegaMenu(): void;
+ onToggleKioskMode(): void;
searchBarHidden?: boolean;
sectionNav: NavModelItem;
pageNav?: NavModelItem;
@@ -26,6 +27,7 @@ export function NavToolbar({
pageNav,
onToggleMegaMenu,
onToggleSearchBar,
+ onToggleKioskMode,
}: Props) {
const styles = useStyles2(getStyles);
const breadcrumbs = buildBreadcrumbs(sectionNav, pageNav);
@@ -39,7 +41,10 @@ export function NavToolbar({
{actions}
{actions && }
-
+ {searchBarHidden && (
+
+ )}
+
diff --git a/public/app/core/components/MegaMenu/MegaMenu.test.tsx b/public/app/core/components/MegaMenu/MegaMenu.test.tsx
index e7fab5a8e6a..6ca38ce58a2 100644
--- a/public/app/core/components/MegaMenu/MegaMenu.test.tsx
+++ b/public/app/core/components/MegaMenu/MegaMenu.test.tsx
@@ -36,7 +36,7 @@ const setup = () => {
const context = getGrafanaContextMock();
const store = configureStore({ navBarTree });
- context.chrome.toggleMegaMenu();
+ context.chrome.onToggleMegaMenu();
return render(
diff --git a/public/app/core/components/NavBar/NavBar.tsx b/public/app/core/components/NavBar/NavBar.tsx
index 0c3be17b2bc..a1af49b439d 100644
--- a/public/app/core/components/NavBar/NavBar.tsx
+++ b/public/app/core/components/NavBar/NavBar.tsx
@@ -10,7 +10,7 @@ import { GrafanaTheme2, NavModelItem, NavSection } from '@grafana/data';
import { config, locationSearchToObject, locationService, reportInteraction } from '@grafana/runtime';
import { Icon, useTheme2, CustomScrollbar } from '@grafana/ui';
import { getKioskMode } from 'app/core/navigation/kiosk';
-import { KioskMode, StoreState } from 'app/types';
+import { StoreState } from 'app/types';
import { OrgSwitcher } from '../OrgSwitcher';
@@ -177,9 +177,8 @@ export const NavBar = React.memo(() => {
function shouldHideNavBar(location: HistoryLocation) {
const queryParams = locationSearchToObject(location.search);
- const kiosk = getKioskMode(queryParams);
- if (kiosk !== KioskMode.Off) {
+ if (getKioskMode(queryParams)) {
return true;
}
diff --git a/public/app/core/context/GrafanaContext.ts b/public/app/core/context/GrafanaContext.ts
index b9596eddfc9..7acf311c368 100644
--- a/public/app/core/context/GrafanaContext.ts
+++ b/public/app/core/context/GrafanaContext.ts
@@ -5,12 +5,14 @@ import { LocationService } from '@grafana/runtime/src/services/LocationService';
import { BackendSrv } from '@grafana/runtime/src/services/backendSrv';
import { AppChromeService } from '../components/AppChrome/AppChromeService';
+import { KeybindingSrv } from '../services/keybindingSrv';
export interface GrafanaContextType {
backend: BackendSrv;
location: LocationService;
config: GrafanaConfig;
chrome: AppChromeService;
+ keybindings: KeybindingSrv;
}
export const GrafanaContext = React.createContext(undefined);
diff --git a/public/app/core/core.ts b/public/app/core/core.ts
index 0833d4a58f4..3895898176d 100644
--- a/public/app/core/core.ts
+++ b/public/app/core/core.ts
@@ -3,18 +3,7 @@ import { colors, JsonExplorer } from '@grafana/ui/';
import appEvents from './app_events';
import { profiler } from './profiler';
import { contextSrv } from './services/context_srv';
-import { KeybindingSrv } from './services/keybindingSrv';
import TimeSeries, { updateLegendValues } from './time_series2';
import { assignModelProperties } from './utils/model_utils';
-export {
- profiler,
- appEvents,
- colors,
- assignModelProperties,
- contextSrv,
- KeybindingSrv,
- JsonExplorer,
- TimeSeries,
- updateLegendValues,
-};
+export { profiler, appEvents, colors, assignModelProperties, contextSrv, JsonExplorer, TimeSeries, updateLegendValues };
diff --git a/public/app/core/navigation/GrafanaRoute.tsx b/public/app/core/navigation/GrafanaRoute.tsx
index 4ead00a6316..e6521382f43 100644
--- a/public/app/core/navigation/GrafanaRoute.tsx
+++ b/public/app/core/navigation/GrafanaRoute.tsx
@@ -5,21 +5,21 @@ import Drop from 'tether-drop';
import { locationSearchToObject, navigationLogger, reportPageview } from '@grafana/runtime';
import { useGrafana } from '../context/GrafanaContext';
-import { keybindingSrv } from '../services/keybindingSrv';
import { GrafanaRouteComponentProps, RouteDescriptor } from './types';
export interface Props extends Omit {}
export function GrafanaRoute(props: Props) {
- const { chrome } = useGrafana();
+ const { chrome, keybindings } = useGrafana();
- chrome.registerRouteRender(props.route);
+ chrome.setMatchedRoute(props.route);
useEffect(() => {
+ keybindings.clearAndInitGlobalBindings();
+
updateBodyClassNames(props.route);
cleanupDOM();
- reportPageview();
navigationLogger('GrafanaRoute', false, 'Mounted', props.match);
return () => {
@@ -30,12 +30,6 @@ export function GrafanaRoute(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
- useEffect(() => {
- // unbinds all and re-bind global keybindins
- keybindingSrv.reset();
- keybindingSrv.initGlobals();
- }, [chrome, props.route]);
-
useEffect(() => {
cleanupDOM();
reportPageview();
diff --git a/public/app/core/navigation/kiosk.ts b/public/app/core/navigation/kiosk.ts
index 092164e06c2..64452c4265a 100644
--- a/public/app/core/navigation/kiosk.ts
+++ b/public/app/core/navigation/kiosk.ts
@@ -1,33 +1,9 @@
-import { t } from '@lingui/macro';
-
-import { AppEvents, UrlQueryMap } from '@grafana/data';
-import { locationService } from '@grafana/runtime';
+import { UrlQueryMap } from '@grafana/data';
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) {
case 'tv':
return KioskMode.TV;
@@ -36,10 +12,6 @@ export function getKioskMode(queryParams: UrlQueryMap): KioskMode {
case true:
return KioskMode.Full;
default:
- return KioskMode.Off;
+ return null;
}
}
-
-export function exitKioskMode() {
- locationService.partial({ kiosk: null });
-}
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 19fdb1a786e..86fda1ed86f 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -3,7 +3,7 @@ import Mousetrap from 'mousetrap';
import 'mousetrap-global-bind';
import 'mousetrap/plugins/global-bind/mousetrap-global-bind';
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 { getExploreUrl } from 'app/core/utils/explore';
import { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
@@ -20,20 +20,20 @@ import {
ZoomOutEvent,
AbsoluteTimeEvent,
} from '../../types/events';
+import { AppChromeService } from '../components/AppChrome/AppChromeService';
import { HelpModal } from '../components/help/HelpModal';
import { contextSrv } from '../core';
-import { exitKioskMode, toggleKioskMode } from '../navigation/kiosk';
import { toggleTheme } from './toggleTheme';
import { withFocusedPanel } from './withFocusedPanelId';
export class KeybindingSrv {
- reset() {
- Mousetrap.reset();
- }
+ constructor(private locationService: LocationService, private chromeService: AppChromeService) {}
- initGlobals() {
- if (locationService.getLocation().pathname !== '/login') {
+ clearAndInitGlobalBindings() {
+ Mousetrap.reset();
+
+ if (this.locationService.getLocation().pathname !== '/login') {
this.bind(['?', 'h'], this.showHelpModal);
this.bind('g h', this.goToHome);
this.bind('g a', this.openAlerting);
@@ -42,7 +42,7 @@ export class KeybindingSrv {
this.bind('t a', this.makeAbsoluteTime);
this.bind('f', this.openSearch);
this.bind('esc', this.exit);
- this.bindGlobal('esc', this.globalEsc);
+ this.bindGlobalEsc();
}
this.bind('t t', () => toggleTheme(false));
@@ -53,6 +53,10 @@ export class KeybindingSrv {
}
}
+ bindGlobalEsc() {
+ this.bindGlobal('esc', this.globalEsc);
+ }
+
globalEsc() {
const anyDoc = document as any;
const activeElement = anyDoc.activeElement;
@@ -82,29 +86,29 @@ export class KeybindingSrv {
toggleNav() {
window.location.href =
config.appSubUrl +
- locationUtil.getUrlForPartial(locationService.getLocation(), {
+ locationUtil.getUrlForPartial(this.locationService.getLocation(), {
'__feature.topnav': (!config.featureToggles.topnav).toString(),
});
}
private openSearch() {
- locationService.partial({ search: 'open' });
+ this.locationService.partial({ search: 'open' });
}
private closeSearch() {
- locationService.partial({ search: null });
+ this.locationService.partial({ search: null });
}
private openAlerting() {
- locationService.push('/alerting');
+ this.locationService.push('/alerting');
}
private goToHome() {
- locationService.push('/');
+ this.locationService.push('/');
}
private goToProfile() {
- locationService.push('/profile');
+ this.locationService.push('/profile');
}
private makeAbsoluteTime() {
@@ -116,30 +120,31 @@ export class KeybindingSrv {
}
private exit() {
- const search = locationService.getSearchObject();
+ const search = this.locationService.getSearchObject();
if (search.editview) {
- locationService.partial({ editview: null, editIndex: null });
+ this.locationService.partial({ editview: null, editIndex: null });
return;
}
if (search.inspect) {
- locationService.partial({ inspect: null, inspectTab: null });
+ this.locationService.partial({ inspect: null, inspectTab: null });
return;
}
if (search.editPanel) {
- locationService.partial({ editPanel: null, tab: null });
+ this.locationService.partial({ editPanel: null, tab: null });
return;
}
if (search.viewPanel) {
- locationService.partial({ viewPanel: null, tab: null });
+ this.locationService.partial({ viewPanel: null, tab: null });
return;
}
- if (search.kiosk) {
- exitKioskMode();
+ const { kioskMode } = this.chromeService.state.getValue();
+ if (kioskMode) {
+ this.chromeService.exitKioskMode();
}
if (search.search) {
@@ -148,7 +153,7 @@ export class KeybindingSrv {
}
private showDashEditView() {
- locationService.partial({
+ this.locationService.partial({
editview: 'settings',
});
}
@@ -230,15 +235,15 @@ export class KeybindingSrv {
// edit panel
this.bindWithPanelId('e', (panelId) => {
if (dashboard.canEditPanelById(panelId)) {
- const isEditing = locationService.getSearchObject().editPanel !== undefined;
- locationService.partial({ editPanel: isEditing ? null : panelId });
+ const isEditing = this.locationService.getSearchObject().editPanel !== undefined;
+ this.locationService.partial({ editPanel: isEditing ? null : panelId });
}
});
// view panel
this.bindWithPanelId('v', (panelId) => {
- const isViewing = locationService.getSearchObject().viewPanel !== undefined;
- locationService.partial({ viewPanel: isViewing ? null : panelId });
+ const isViewing = this.locationService.getSearchObject().viewPanel !== undefined;
+ this.locationService.partial({ viewPanel: isViewing ? null : panelId });
});
//toggle legend
@@ -252,7 +257,7 @@ export class KeybindingSrv {
});
this.bindWithPanelId('i', (panelId) => {
- locationService.partial({ inspect: panelId });
+ this.locationService.partial({ inspect: panelId });
});
// jump to explore if permissions allow
@@ -268,7 +273,7 @@ export class KeybindingSrv {
if (url) {
const urlWithoutBase = locationUtil.stripBaseFromUrl(url);
if (urlWithoutBase) {
- locationService.push(urlWithoutBase);
+ this.locationService.push(urlWithoutBase);
}
}
});
@@ -322,7 +327,7 @@ export class KeybindingSrv {
});
this.bind('d n', () => {
- locationService.push('/dashboard/new');
+ this.locationService.push('/dashboard/new');
});
this.bind('d r', () => {
@@ -334,17 +339,15 @@ export class KeybindingSrv {
});
this.bind('d k', () => {
- toggleKioskMode();
+ this.chromeService.onToggleKioskMode();
});
//Autofit panels
this.bind('d a', () => {
// this has to be a full page reload
- const queryParams = locationService.getSearchObject();
+ const queryParams = this.locationService.getSearchObject();
const newUrlParam = queryParams.autofitpanels ? '' : '&autofitpanels';
window.location.href = window.location.href + newUrlParam;
});
}
}
-
-export const keybindingSrv = new KeybindingSrv();
diff --git a/public/app/features/commandPalette/CommandPalette.tsx b/public/app/features/commandPalette/CommandPalette.tsx
index d70b21be277..839a04733ec 100644
--- a/public/app/features/commandPalette/CommandPalette.tsx
+++ b/public/app/features/commandPalette/CommandPalette.tsx
@@ -18,10 +18,9 @@ import { useSelector } from 'react-redux';
import { GrafanaTheme2 } from '@grafana/data';
import { reportInteraction, locationService } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
+import { useGrafana } from 'app/core/context/GrafanaContext';
import { StoreState } from 'app/types';
-import { keybindingSrv } from '../../core/services/keybindingSrv';
-
import { ResultItem } from './ResultItem';
import getDashboardNavActions from './actions/dashboard.nav.actions';
import getGlobalActions from './actions/global.static.actions';
@@ -33,6 +32,7 @@ import getGlobalActions from './actions/global.static.actions';
export const CommandPalette = () => {
const styles = useStyles2(getSearchStyles);
+ const { keybindings } = useGrafana();
const [actions, setActions] = useState([]);
const [staticActions, setStaticActions] = useState([]);
const { query, showing } = useKBar((state) => ({
@@ -63,14 +63,14 @@ export const CommandPalette = () => {
setActions([...staticActions, ...dashAct]);
});
- keybindingSrv.bindGlobal('esc', () => {
+ keybindings.bindGlobal('esc', () => {
query.setVisualState(VisualState.animatingOut);
});
}
return () => {
- keybindingSrv.bindGlobal('esc', () => {
- keybindingSrv.globalEsc();
+ keybindings.bindGlobal('esc', () => {
+ keybindings.globalEsc();
});
};
// eslint-disable-next-line react-hooks/exhaustive-deps
diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx
index 897a9b25ef8..684484ff544 100644
--- a/public/app/features/dashboard/components/DashNav/DashNav.tsx
+++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx
@@ -18,7 +18,7 @@ import {
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbarSeparator';
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 { SaveDashboardDrawer } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDrawer';
import { ShareModal } from 'app/features/dashboard/components/ShareModal';
@@ -45,7 +45,7 @@ const selectors = e2eSelectors.pages.Dashboard.DashNav;
export interface OwnProps {
dashboard: DashboardModel;
isFullscreen: boolean;
- kioskMode: KioskMode;
+ kioskMode?: KioskMode | null;
hideTimePicker: boolean;
folderTitle?: string;
title: string;
@@ -73,6 +73,7 @@ type Props = OwnProps & ConnectedProps;
export const DashNav = React.memo((props) => {
const forceUpdate = useForceUpdate();
+ const { chrome } = useGrafana();
const onStarDashboard = () => {
const dashboardSrv = getDashboardSrv();
@@ -90,7 +91,7 @@ export const DashNav = React.memo((props) => {
};
const onToggleTVMode = () => {
- toggleKioskMode();
+ chrome.onToggleKioskMode();
};
const onOpenSettings = () => {
@@ -127,7 +128,7 @@ export const DashNav = React.memo((props) => {
const { canStar, canShare, isStarred } = dashboard.meta;
const buttons: ReactNode[] = [];
- if (kioskMode !== KioskMode.Off || isPlaylistRunning()) {
+ if (kioskMode || isPlaylistRunning()) {
return [];
}
@@ -235,7 +236,7 @@ export const DashNav = React.memo((props) => {
const { snapshot } = dashboard;
const snapshotUrl = snapshot && snapshot.originalUrl;
const buttons: ReactNode[] = [];
- const tvButton = (
+ const tvButton = config.featureToggles.topnav ? null : (
-
-
-
-
+
+
+
+
+
+
+
);
ctx.container = container;
@@ -144,11 +150,13 @@ function dashboardPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
Object.assign(props, newProps);
rerender(
-
-
-
-
-
+
+
+
+
+
+
+
);
};
@@ -179,6 +187,7 @@ describe('DashboardPage', () => {
routeName: 'normal-dashboard',
urlSlug: 'my-dash',
urlUid: '11',
+ keybindingSrv: expect.anything(),
});
});
});
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx
index d355df8d54e..4680ec0d367 100644
--- a/public/app/features/dashboard/containers/DashboardPage.tsx
+++ b/public/app/features/dashboard/containers/DashboardPage.tsx
@@ -9,6 +9,7 @@ import { Themeable2, withTheme2 } from '@grafana/ui';
import { notifyApp } from 'app/core/actions';
import { Page } from 'app/core/components/Page/Page';
import { config } from 'app/core/config';
+import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { createErrorNotification } from 'app/core/copy/appNotification';
import { getKioskMode } from 'app/core/navigation/kiosk';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
@@ -96,6 +97,8 @@ export interface State {
}
export class UnthemedDashboardPage extends PureComponent {
+ static contextType = GrafanaContext;
+
private forceRouteReloadCounter = 0;
state: State = this.getCleanState();
@@ -139,6 +142,7 @@ export class UnthemedDashboardPage extends PureComponent {
routeName: this.props.route.routeName,
fixUrl: !isPublic,
accessToken: match.params.accessToken,
+ keybindingSrv: this.context.keybindings,
});
// small delay to start live updates
@@ -336,7 +340,7 @@ export class UnthemedDashboardPage extends PureComponent {
}
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 && (
diff --git a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx
index 608fa06cadc..40b378351d1 100644
--- a/public/app/features/dashboard/containers/SoloPanelPage.test.tsx
+++ b/public/app/features/dashboard/containers/SoloPanelPage.test.tsx
@@ -1,6 +1,8 @@
import { render, screen } from '@testing-library/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 { getRouteComponentProps } from '../../../core/navigation/__mocks__/routeProps';
@@ -83,13 +85,22 @@ function soloPanelPageScenario(description: string, scenarioFn: (ctx: ScenarioCo
Object.assign(props, propOverrides);
ctx.dashboard = props.dashboard;
- let { rerender } = render();
+
+ const context = getGrafanaContextMock();
+ const renderPage = (props: Props) => (
+
+
+
+ );
+
+ let { rerender } = render(renderPage(props));
+
// prop updates will be submitted by rerendering the same component with different props
ctx.rerender = (newProps?: Partial) => {
- Object.assign(props, newProps);
- rerender();
+ rerender(renderPage(Object.assign(props, newProps)));
};
},
+
rerender: () => {
// will be replaced while mount() is called
},
diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx
index 2d7360f2cdb..257fd94b201 100644
--- a/public/app/features/dashboard/containers/SoloPanelPage.tsx
+++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import AutoSizer from 'react-virtualized-auto-sizer';
+import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
import { StoreState } from 'app/types';
@@ -34,6 +35,8 @@ export interface State {
}
export class SoloPanelPage extends Component {
+ static contextType = GrafanaContext;
+
state: State = {
panel: null,
notFound: false,
@@ -48,6 +51,7 @@ export class SoloPanelPage extends Component {
urlType: match.params.type,
routeName: route.routeName,
fixUrl: false,
+ keybindingSrv: this.context.keybindings,
});
}
diff --git a/public/app/features/dashboard/state/initDashboard.test.ts b/public/app/features/dashboard/state/initDashboard.test.ts
index 4425f8491a7..381562b4c2a 100644
--- a/public/app/features/dashboard/state/initDashboard.test.ts
+++ b/public/app/features/dashboard/state/initDashboard.test.ts
@@ -5,7 +5,7 @@ import { Subject } from 'rxjs';
import { FetchError, locationService, setEchoSrv } from '@grafana/runtime';
import appEvents from 'app/core/app_events';
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 { createConstantVariableAdapter } from 'app/features/variables/constant/adapter';
import { constantBuilder } from 'app/features/variables/shared/testing/builders';
@@ -193,6 +193,9 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
urlUid: DASH_UID,
fixUrl: false,
routeName: DashboardRoutes.Normal,
+ keybindingSrv: {
+ setupDashboardBindings: jest.fn(),
+ } as unknown as KeybindingSrv,
},
backendSrv: getBackendSrv(),
loaderSrv,
@@ -221,8 +224,6 @@ function describeInitScenario(description: string, scenarioFn: ScenarioFn) {
};
beforeEach(async () => {
- keybindingSrv.setupDashboardBindings = jest.fn();
-
setDashboardSrv({
setCurrent: jest.fn(),
} as any);
@@ -273,7 +274,7 @@ describeInitScenario('Initializing new dashboard', (ctx) => {
expect(getTimeSrv().init).toBeCalled();
expect(getDashboardSrv().setCurrent).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(getDashboardSrv().setCurrent).toBeCalled();
expect(getDashboardQueryRunner().run).toBeCalled();
- expect(keybindingSrv.setupDashboardBindings).toBeCalled();
+ expect(ctx.args.keybindingSrv.setupDashboardBindings).toBeCalled();
});
it('Should initialize redux variables if newVariables is enabled', () => {
diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts
index c5918ef2a22..dc6d007d023 100644
--- a/public/app/features/dashboard/state/initDashboard.ts
+++ b/public/app/features/dashboard/state/initDashboard.ts
@@ -4,7 +4,7 @@ import { notifyApp } from 'app/core/actions';
import appEvents from 'app/core/app_events';
import { createErrorNotification } from 'app/core/copy/appNotification';
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 { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
@@ -32,6 +32,7 @@ export interface InitDashboardArgs {
accessToken?: string;
routeName?: string;
fixUrl: boolean;
+ keybindingSrv: KeybindingSrv;
}
async function fetchDashboard(
@@ -213,7 +214,7 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult {
dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk);
}
- keybindingSrv.setupDashboardBindings(dashboard);
+ args.keybindingSrv.setupDashboardBindings(dashboard);
} catch (err) {
if (err instanceof Error) {
dispatch(notifyApp(createErrorNotification('Dashboard init failed', err)));
diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx
index e787021d6e7..1a1529b2159 100644
--- a/public/app/features/explore/Wrapper.tsx
+++ b/public/app/features/explore/Wrapper.tsx
@@ -57,6 +57,7 @@ class WrapperUnconnected extends PureComponent {
//This is needed for breadcrumbs and topnav.
//We should probably abstract this out at some point
this.context.chrome.update({ sectionNav: this.props.navModel.node });
+ this.context.keybindings.setupTimeRangeBindings(false);
lastSavedUrl.left = undefined;
lastSavedUrl.right = undefined;
diff --git a/public/app/features/explore/state/explorePane.ts b/public/app/features/explore/state/explorePane.ts
index 74e2456b9c0..51946988a6d 100644
--- a/public/app/features/explore/state/explorePane.ts
+++ b/public/app/features/explore/state/explorePane.ts
@@ -14,7 +14,6 @@ import {
DataSourceRef,
} from '@grafana/data';
import { getDataSourceSrv } from '@grafana/runtime';
-import { keybindingSrv } from 'app/core/services/keybindingSrv';
import {
DEFAULT_RANGE,
getQueryKeys,
@@ -178,8 +177,6 @@ export function initializeExplore(
}
dispatch(updateTime({ exploreId }));
- keybindingSrv.setupTimeRangeBindings(false);
-
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 already have something in the url. Adding basically the same state as additional history item prevents
diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts
index c924459dc17..2958f366dbe 100644
--- a/public/app/types/dashboard.ts
+++ b/public/app/types/dashboard.ts
@@ -90,7 +90,6 @@ export interface DashboardInitError {
}
export enum KioskMode {
- Off = 'off',
TV = 'tv',
Full = 'full',
}
diff --git a/public/test/mocks/getGrafanaContextMock.ts b/public/test/mocks/getGrafanaContextMock.ts
index f671eee493d..2d2ccedf460 100644
--- a/public/test/mocks/getGrafanaContextMock.ts
+++ b/public/test/mocks/getGrafanaContextMock.ts
@@ -2,6 +2,7 @@ import { GrafanaConfig } from '@grafana/data';
import { BackendSrv, LocationService } from '@grafana/runtime';
import { AppChromeService } from 'app/core/components/AppChrome/AppChromeService';
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 */
export function getGrafanaContextMock(overrides: Partial = {}): GrafanaContextType {
@@ -13,6 +14,12 @@ export function getGrafanaContextMock(overrides: Partial = {
location: {} as LocationService,
// eslint-disable-next-line
config: {} as GrafanaConfig,
+ // eslint-disable-next-line
+ keybindings: {
+ clearAndInitGlobalBindings: jest.fn(),
+ setupDashboardBindings: jest.fn(),
+ setupTimeRangeBindings: jest.fn(),
+ } as any as KeybindingSrv,
...overrides,
};
}