mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ReturnToPrevious : Add logic to show the new component in AppChrome
behind the new toggle (#81035)
This commit is contained in:
parent
b7517330ee
commit
8ee7b1e00c
@ -55,4 +55,5 @@ export {
|
|||||||
createDataSourcePluginEventProperties,
|
createDataSourcePluginEventProperties,
|
||||||
} from './analytics/plugins/eventProperties';
|
} from './analytics/plugins/eventProperties';
|
||||||
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';
|
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';
|
||||||
|
export { setReturnToPreviousHook, useReturnToPrevious } from './utils/returnToPrevious';
|
||||||
export { type EmbeddedDashboardProps, EmbeddedDashboard, setEmbeddedDashboard } from './components/EmbeddedDashboard';
|
export { type EmbeddedDashboardProps, EmbeddedDashboard, setEmbeddedDashboard } from './components/EmbeddedDashboard';
|
||||||
|
23
packages/grafana-runtime/src/utils/returnToPrevious.ts
Normal file
23
packages/grafana-runtime/src/utils/returnToPrevious.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
interface ReturnToPreviousData {
|
||||||
|
title: string;
|
||||||
|
href: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ReturnToPreviousHook = () => (rtp: ReturnToPreviousData) => void;
|
||||||
|
|
||||||
|
let rtpHook: ReturnToPreviousHook | undefined = undefined;
|
||||||
|
|
||||||
|
export const setReturnToPreviousHook = (hook: ReturnToPreviousHook) => {
|
||||||
|
rtpHook = hook;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useReturnToPrevious: ReturnToPreviousHook = () => {
|
||||||
|
if (!rtpHook) {
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
throw new Error('useReturnToPrevious hook not found in @grafana/runtime');
|
||||||
|
}
|
||||||
|
return () => console.error('ReturnToPrevious hook not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return rtpHook();
|
||||||
|
};
|
@ -35,6 +35,7 @@ import {
|
|||||||
setPluginExtensionGetter,
|
setPluginExtensionGetter,
|
||||||
setEmbeddedDashboard,
|
setEmbeddedDashboard,
|
||||||
setAppEvents,
|
setAppEvents,
|
||||||
|
setReturnToPreviousHook,
|
||||||
type GetPluginExtensions,
|
type GetPluginExtensions,
|
||||||
} from '@grafana/runtime';
|
} from '@grafana/runtime';
|
||||||
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
import { setPanelDataErrorView } from '@grafana/runtime/src/components/PanelDataErrorView';
|
||||||
@ -52,7 +53,7 @@ import appEvents from './core/app_events';
|
|||||||
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
import { AppChromeService } from './core/components/AppChrome/AppChromeService';
|
||||||
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
import { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
||||||
import { PluginPage } from './core/components/Page/PluginPage';
|
import { PluginPage } from './core/components/Page/PluginPage';
|
||||||
import { GrafanaContextType } from './core/context/GrafanaContext';
|
import { GrafanaContextType, useReturnToPreviousInternal } from './core/context/GrafanaContext';
|
||||||
import { initIconCache } from './core/icons/iconBundle';
|
import { initIconCache } from './core/icons/iconBundle';
|
||||||
import { initializeI18n } from './core/internationalization';
|
import { initializeI18n } from './core/internationalization';
|
||||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||||
@ -247,6 +248,8 @@ export class GrafanaApp {
|
|||||||
config,
|
config,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setReturnToPreviousHook(useReturnToPreviousInternal);
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('reactRoot')!);
|
const root = createRoot(document.getElementById('reactRoot')!);
|
||||||
root.render(
|
root.render(
|
||||||
React.createElement(AppWrapper, {
|
React.createElement(AppWrapper, {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { render, screen, waitFor } from '@testing-library/react';
|
import { render, screen, waitFor, act } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { KBarProvider } from 'kbar';
|
import { KBarProvider } from 'kbar';
|
||||||
import React, { ReactNode } from 'react';
|
import React, { ReactNode } from 'react';
|
||||||
@ -132,8 +132,10 @@ describe('AppChrome', () => {
|
|||||||
|
|
||||||
it('should not render a skip link if the page is chromeless', async () => {
|
it('should not render a skip link if the page is chromeless', async () => {
|
||||||
const { context } = setup(<Page navId="child1">Children</Page>);
|
const { context } = setup(<Page navId="child1">Children</Page>);
|
||||||
context.chrome.update({
|
act(() => {
|
||||||
chromeless: true,
|
context.chrome.update({
|
||||||
|
chromeless: true,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
waitFor(() => {
|
waitFor(() => {
|
||||||
expect(screen.queryByRole('link', { name: 'Skip to main content' })).not.toBeInTheDocument();
|
expect(screen.queryByRole('link', { name: 'Skip to main content' })).not.toBeInTheDocument();
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { PropsWithChildren } from 'react';
|
import React, { PropsWithChildren, useEffect } from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
|
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
|
||||||
import config from 'app/core/config';
|
import config from 'app/core/config';
|
||||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
@ -16,6 +17,7 @@ import { DOCKED_LOCAL_STORAGE_KEY, DOCKED_MENU_OPEN_LOCAL_STORAGE_KEY } from './
|
|||||||
import { MegaMenu as DockedMegaMenu } from './DockedMegaMenu/MegaMenu';
|
import { MegaMenu as DockedMegaMenu } from './DockedMegaMenu/MegaMenu';
|
||||||
import { MegaMenu } from './MegaMenu/MegaMenu';
|
import { MegaMenu } from './MegaMenu/MegaMenu';
|
||||||
import { NavToolbar } from './NavToolbar/NavToolbar';
|
import { NavToolbar } from './NavToolbar/NavToolbar';
|
||||||
|
import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious';
|
||||||
import { SectionNav } from './SectionNav/SectionNav';
|
import { SectionNav } from './SectionNav/SectionNav';
|
||||||
import { TopSearchBar } from './TopBar/TopSearchBar';
|
import { TopSearchBar } from './TopBar/TopSearchBar';
|
||||||
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
||||||
@ -53,6 +55,18 @@ export function AppChrome({ children }: Props) {
|
|||||||
chrome.setMegaMenuOpen(!state.megaMenuOpen);
|
chrome.setMegaMenuOpen(!state.megaMenuOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const path = locationService.getLocation().pathname;
|
||||||
|
const shouldShowReturnToPrevious =
|
||||||
|
config.featureToggles.returnToPrevious && state.returnToPrevious && path !== state.returnToPrevious.href;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.returnToPrevious && path === state.returnToPrevious.href) {
|
||||||
|
chrome.clearReturnToPrevious();
|
||||||
|
}
|
||||||
|
// We only want to pay attention when the location changes
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [chrome, path]);
|
||||||
|
|
||||||
// Chromeless routes are without topNav, mega menu, search & command palette
|
// Chromeless routes are without topNav, mega menu, search & command palette
|
||||||
// We check chromeless twice here instead of having a separate path so {children}
|
// We check chromeless twice here instead of having a separate path so {children}
|
||||||
// doesn't get re-mounted when chromeless goes from true to false.
|
// doesn't get re-mounted when chromeless goes from true to false.
|
||||||
@ -105,6 +119,9 @@ export function AppChrome({ children }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!state.chromeless && <CommandPalette />}
|
{!state.chromeless && <CommandPalette />}
|
||||||
|
{shouldShowReturnToPrevious && state.returnToPrevious && (
|
||||||
|
<ReturnToPrevious href={state.returnToPrevious.href} title={state.returnToPrevious.title} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,8 @@ import { KioskMode } from 'app/types';
|
|||||||
|
|
||||||
import { RouteDescriptor } from '../../navigation/types';
|
import { RouteDescriptor } from '../../navigation/types';
|
||||||
|
|
||||||
|
import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious';
|
||||||
|
|
||||||
export interface AppChromeState {
|
export interface AppChromeState {
|
||||||
chromeless?: boolean;
|
chromeless?: boolean;
|
||||||
sectionNav: NavModel;
|
sectionNav: NavModel;
|
||||||
@ -21,6 +23,10 @@ export interface AppChromeState {
|
|||||||
megaMenuDocked: boolean;
|
megaMenuDocked: boolean;
|
||||||
kioskMode: KioskMode | null;
|
kioskMode: KioskMode | null;
|
||||||
layout: PageLayoutType;
|
layout: PageLayoutType;
|
||||||
|
returnToPrevious?: {
|
||||||
|
href: ReturnToPreviousProps['href'];
|
||||||
|
title: ReturnToPreviousProps['title'];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked';
|
export const DOCKED_LOCAL_STORAGE_KEY = 'grafana.navigation.docked';
|
||||||
@ -40,6 +46,9 @@ export class AppChromeService {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private sessionStorageData = window.sessionStorage.getItem('returnToPrevious');
|
||||||
|
private returnToPreviousData = this.sessionStorageData ? JSON.parse(this.sessionStorageData) : undefined;
|
||||||
|
|
||||||
readonly state = new BehaviorSubject<AppChromeState>({
|
readonly state = new BehaviorSubject<AppChromeState>({
|
||||||
chromeless: true, // start out hidden to not flash it on pages without chrome
|
chromeless: true, // start out hidden to not flash it on pages without chrome
|
||||||
sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } },
|
sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } },
|
||||||
@ -48,6 +57,7 @@ export class AppChromeService {
|
|||||||
megaMenuDocked: this.megaMenuDocked,
|
megaMenuDocked: this.megaMenuDocked,
|
||||||
kioskMode: null,
|
kioskMode: null,
|
||||||
layout: PageLayoutType.Canvas,
|
layout: PageLayoutType.Canvas,
|
||||||
|
returnToPrevious: this.returnToPreviousData,
|
||||||
});
|
});
|
||||||
|
|
||||||
public setMatchedRoute(route: RouteDescriptor) {
|
public setMatchedRoute(route: RouteDescriptor) {
|
||||||
@ -83,6 +93,16 @@ export class AppChromeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setReturnToPrevious = (returnToPrevious: ReturnToPreviousProps) => {
|
||||||
|
this.update({ returnToPrevious });
|
||||||
|
window.sessionStorage.setItem('returnToPrevious', JSON.stringify(returnToPrevious));
|
||||||
|
};
|
||||||
|
|
||||||
|
public clearReturnToPrevious = () => {
|
||||||
|
this.update({ returnToPrevious: undefined });
|
||||||
|
window.sessionStorage.removeItem('returnToPrevious');
|
||||||
|
};
|
||||||
|
|
||||||
private ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) {
|
private ignoreStateUpdate(newState: AppChromeState, current: AppChromeState) {
|
||||||
if (isShallowEqual(newState, current)) {
|
if (isShallowEqual(newState, current)) {
|
||||||
return true;
|
return true;
|
||||||
|
@ -2,7 +2,9 @@ import { css } from '@emotion/css';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
import { useStyles2 } from '@grafana/ui';
|
import { useStyles2 } from '@grafana/ui';
|
||||||
|
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
import { DismissableButton } from './DismissableButton';
|
import { DismissableButton } from './DismissableButton';
|
||||||
@ -14,11 +16,13 @@ export interface ReturnToPreviousProps {
|
|||||||
|
|
||||||
export const ReturnToPrevious = ({ href, title }: ReturnToPreviousProps) => {
|
export const ReturnToPrevious = ({ href, title }: ReturnToPreviousProps) => {
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
const { chrome } = useGrafana();
|
||||||
const handleOnClick = () => {
|
const handleOnClick = () => {
|
||||||
console.log('Going to...', href);
|
locationService.push(href);
|
||||||
|
chrome.clearReturnToPrevious();
|
||||||
};
|
};
|
||||||
const handleOnDismiss = () => {
|
const handleOnDismiss = () => {
|
||||||
console.log('Closing button');
|
chrome.clearReturnToPrevious();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -26,3 +26,10 @@ export function useGrafana(): GrafanaContextType {
|
|||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Implementation of useReturnToPrevious that's made available through
|
||||||
|
// @grafana/runtime
|
||||||
|
export function useReturnToPreviousInternal() {
|
||||||
|
const { chrome } = useGrafana();
|
||||||
|
return chrome.setReturnToPrevious;
|
||||||
|
}
|
||||||
|
@ -42,6 +42,7 @@ import { DataSourceType, GRAFANA_RULES_SOURCE_NAME } from './utils/datasource';
|
|||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getPluginLinkExtensions: jest.fn(),
|
getPluginLinkExtensions: jest.fn(),
|
||||||
|
useReturnToPrevious: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('./api/buildInfo');
|
jest.mock('./api/buildInfo');
|
||||||
jest.mock('./api/prometheus');
|
jest.mock('./api/prometheus');
|
||||||
|
@ -52,6 +52,7 @@ const mockRoute = (id?: string): GrafanaRouteComponentProps<{ id?: string; sourc
|
|||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getPluginLinkExtensions: jest.fn(),
|
getPluginLinkExtensions: jest.fn(),
|
||||||
|
useReturnToPrevious: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('../../hooks/useAbilities');
|
jest.mock('../../hooks/useAbilities');
|
||||||
jest.mock('../../api/buildInfo');
|
jest.mock('../../api/buildInfo');
|
||||||
|
@ -24,6 +24,7 @@ import { RuleDetails } from './RuleDetails';
|
|||||||
jest.mock('@grafana/runtime', () => ({
|
jest.mock('@grafana/runtime', () => ({
|
||||||
...jest.requireActual('@grafana/runtime'),
|
...jest.requireActual('@grafana/runtime'),
|
||||||
getPluginLinkExtensions: jest.fn(),
|
getPluginLinkExtensions: jest.fn(),
|
||||||
|
useReturnToPrevious: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('../../hooks/useIsRuleEditable');
|
jest.mock('../../hooks/useIsRuleEditable');
|
||||||
|
@ -4,7 +4,7 @@ import React, { Fragment, useState } from 'react';
|
|||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data';
|
import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config, locationService, useReturnToPrevious } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ClipboardButton,
|
ClipboardButton,
|
||||||
@ -55,6 +55,8 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
|
|||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const notifyApp = useAppNotification();
|
const notifyApp = useAppNotification();
|
||||||
|
|
||||||
|
const setReturnToPrevious = useReturnToPrevious();
|
||||||
|
|
||||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
||||||
const [redirectToClone, setRedirectToClone] = useState<
|
const [redirectToClone, setRedirectToClone] = useState<
|
||||||
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
|
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
|
||||||
@ -135,16 +137,31 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
|
|||||||
const dashboardUID = rule.annotations[Annotation.dashboardUID];
|
const dashboardUID = rule.annotations[Annotation.dashboardUID];
|
||||||
if (dashboardUID) {
|
if (dashboardUID) {
|
||||||
buttons.push(
|
buttons.push(
|
||||||
<LinkButton
|
config.featureToggles.returnToPrevious ? (
|
||||||
size="sm"
|
<LinkButton
|
||||||
key="dashboard"
|
size="sm"
|
||||||
variant="primary"
|
key="dashboard"
|
||||||
icon="apps"
|
variant="primary"
|
||||||
target="_blank"
|
icon="apps"
|
||||||
href={`d/${encodeURIComponent(dashboardUID)}`}
|
href={`d/${encodeURIComponent(dashboardUID)}`}
|
||||||
>
|
onClick={() => {
|
||||||
Go to dashboard
|
setReturnToPrevious({ title: rule.name, href: locationService.getLocation().pathname });
|
||||||
</LinkButton>
|
}}
|
||||||
|
>
|
||||||
|
Go to dashboard
|
||||||
|
</LinkButton>
|
||||||
|
) : (
|
||||||
|
<LinkButton
|
||||||
|
size="sm"
|
||||||
|
key="dashboard"
|
||||||
|
variant="primary"
|
||||||
|
icon="apps"
|
||||||
|
target="_blank"
|
||||||
|
href={`d/${encodeURIComponent(dashboardUID)}`}
|
||||||
|
>
|
||||||
|
Go to dashboard
|
||||||
|
</LinkButton>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
const panelId = rule.annotations[Annotation.panelID];
|
const panelId = rule.annotations[Annotation.panelID];
|
||||||
if (panelId) {
|
if (panelId) {
|
||||||
|
Loading…
Reference in New Issue
Block a user