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. 29
      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.", "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"],

@ -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,
};

@ -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 <main className="main-view">{children}</main>;
}
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) {
<main className="main-view">
{!state.chromeless && (
<div className={cx(styles.topNav)}>
{!state.searchBarHidden && <TopSearchBar />}
{!searchBarHidden && <TopSearchBar />}
<NavToolbar
searchBarHidden={state.searchBarHidden}
searchBarHidden={searchBarHidden}
sectionNav={state.sectionNav}
pageNav={state.pageNav}
actions={state.actions}
onToggleSearchBar={chrome.toggleSearchBar}
onToggleMegaMenu={chrome.toggleMegaMenu}
onToggleSearchBar={chrome.onToggleSearchBar}
onToggleMegaMenu={chrome.onToggleMegaMenu}
onToggleKioskMode={chrome.onToggleKioskMode}
/>
</div>
)}
<div className={contentClass}>{children}</div>
{!state.chromeless && (
<MegaMenu searchBarHidden={state.searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />
)}
{!state.chromeless && <MegaMenu searchBarHidden={searchBarHidden} onClose={() => chrome.setMegaMenu(false)} />}
</main>
);
}

@ -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;
}
}

@ -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({
<div className={styles.actions}>
{actions}
{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" />
</ToolbarButton>
</div>

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

@ -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;
}

@ -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<GrafanaContextType | undefined>(undefined);

@ -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 };

@ -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<GrafanaRouteComponentProps, 'queryParams'> {}
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();

@ -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 });
}

@ -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() {
constructor(private locationService: LocationService, private chromeService: AppChromeService) {}
clearAndInitGlobalBindings() {
Mousetrap.reset();
}
initGlobals() {
if (locationService.getLocation().pathname !== '/login') {
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();

@ -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<Action[]>([]);
const [staticActions, setStaticActions] = useState<Action[]>([]);
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

@ -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<typeof connector>;
export const DashNav = React.memo<Props>((props) => {
const forceUpdate = useForceUpdate();
const { chrome } = useGrafana();
const onStarDashboard = () => {
const dashboardSrv = getDashboardSrv();
@ -90,7 +91,7 @@ export const DashNav = React.memo<Props>((props) => {
};
const onToggleTVMode = () => {
toggleKioskMode();
chrome.onToggleKioskMode();
};
const onOpenSettings = () => {
@ -127,7 +128,7 @@ export const DashNav = React.memo<Props>((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>((props) => {
const { snapshot } = dashboard;
const snapshotUrl = snapshot && snapshot.originalUrl;
const buttons: ReactNode[] = [];
const tvButton = (
const tvButton = config.featureToggles.topnav ? null : (
<ToolbarButton
tooltip={t({ id: 'dashboard.toolbar.tv-button', message: 'Cycle view mode' })}
icon="monitor"

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

@ -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<Props, State> {
static contextType = GrafanaContext;
private forceRouteReloadCounter = 0;
state: State = this.getCleanState();
@ -139,6 +142,7 @@ export class UnthemedDashboardPage extends PureComponent<Props, State> {
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<Props, State> {
}
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 && (
<header data-testid={selectors.pages.Dashboard.DashNav.navV2}>

@ -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(<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
ctx.rerender = (newProps?: Partial<Props>) => {
Object.assign(props, newProps);
rerender(<SoloPanelPage {...props} />);
rerender(renderPage(Object.assign(props, newProps)));
};
},
rerender: () => {
// will be replaced while mount() is called
},

@ -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<Props, State> {
static contextType = GrafanaContext;
state: State = {
panel: null,
notFound: false,
@ -48,6 +51,7 @@ export class SoloPanelPage extends Component<Props, State> {
urlType: match.params.type,
routeName: route.routeName,
fixUrl: false,
keybindingSrv: this.context.keybindings,
});
}

@ -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', () => {

@ -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<void> {
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)));

@ -57,6 +57,7 @@ class WrapperUnconnected extends PureComponent<Props> {
//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;

@ -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

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

@ -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> = {}): GrafanaContextType {
@ -13,6 +14,12 @@ export function getGrafanaContextMock(overrides: Partial<GrafanaContextType> = {
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,
};
}

Loading…
Cancel
Save