mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
7352c181c2
commit
b8e72d6173
@ -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() {
|
||||
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();
|
||||
|
@ -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(
|
||||
<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(
|
||||
<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…
Reference in New Issue
Block a user