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