mirror of
https://github.com/grafana/grafana.git
synced 2025-02-11 16:15:42 -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,
|
||||
} from './analytics/plugins/eventProperties';
|
||||
export { usePluginInteractionReporter } from './analytics/plugins/usePluginInteractionReporter';
|
||||
export { setReturnToPreviousHook, useReturnToPrevious } from './utils/returnToPrevious';
|
||||
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,
|
||||
setEmbeddedDashboard,
|
||||
setAppEvents,
|
||||
setReturnToPreviousHook,
|
||||
type GetPluginExtensions,
|
||||
} from '@grafana/runtime';
|
||||
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 { getAllOptionEditors, getAllStandardFieldConfigs } from './core/components/OptionsUI/registry';
|
||||
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 { initializeI18n } from './core/internationalization';
|
||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||
@ -247,6 +248,8 @@ export class GrafanaApp {
|
||||
config,
|
||||
};
|
||||
|
||||
setReturnToPreviousHook(useReturnToPreviousInternal);
|
||||
|
||||
const root = createRoot(document.getElementById('reactRoot')!);
|
||||
root.render(
|
||||
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 { KBarProvider } from 'kbar';
|
||||
import React, { ReactNode } from 'react';
|
||||
@ -132,8 +132,10 @@ describe('AppChrome', () => {
|
||||
|
||||
it('should not render a skip link if the page is chromeless', async () => {
|
||||
const { context } = setup(<Page navId="child1">Children</Page>);
|
||||
context.chrome.update({
|
||||
chromeless: true,
|
||||
act(() => {
|
||||
context.chrome.update({
|
||||
chromeless: true,
|
||||
});
|
||||
});
|
||||
waitFor(() => {
|
||||
expect(screen.queryByRole('link', { name: 'Skip to main content' })).not.toBeInTheDocument();
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import classNames from 'classnames';
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import React, { PropsWithChildren, useEffect } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { useStyles2, LinkButton, useTheme2 } from '@grafana/ui';
|
||||
import config from 'app/core/config';
|
||||
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 } from './MegaMenu/MegaMenu';
|
||||
import { NavToolbar } from './NavToolbar/NavToolbar';
|
||||
import { ReturnToPrevious } from './ReturnToPrevious/ReturnToPrevious';
|
||||
import { SectionNav } from './SectionNav/SectionNav';
|
||||
import { TopSearchBar } from './TopBar/TopSearchBar';
|
||||
import { TOP_BAR_LEVEL_HEIGHT } from './types';
|
||||
@ -53,6 +55,18 @@ export function AppChrome({ children }: Props) {
|
||||
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
|
||||
// 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.
|
||||
@ -105,6 +119,9 @@ export function AppChrome({ children }: Props) {
|
||||
</>
|
||||
)}
|
||||
{!state.chromeless && <CommandPalette />}
|
||||
{shouldShowReturnToPrevious && state.returnToPrevious && (
|
||||
<ReturnToPrevious href={state.returnToPrevious.href} title={state.returnToPrevious.title} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,8 @@ import { KioskMode } from 'app/types';
|
||||
|
||||
import { RouteDescriptor } from '../../navigation/types';
|
||||
|
||||
import { ReturnToPreviousProps } from './ReturnToPrevious/ReturnToPrevious';
|
||||
|
||||
export interface AppChromeState {
|
||||
chromeless?: boolean;
|
||||
sectionNav: NavModel;
|
||||
@ -21,6 +23,10 @@ export interface AppChromeState {
|
||||
megaMenuDocked: boolean;
|
||||
kioskMode: KioskMode | null;
|
||||
layout: PageLayoutType;
|
||||
returnToPrevious?: {
|
||||
href: ReturnToPreviousProps['href'];
|
||||
title: ReturnToPreviousProps['title'];
|
||||
};
|
||||
}
|
||||
|
||||
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>({
|
||||
chromeless: true, // start out hidden to not flash it on pages without chrome
|
||||
sectionNav: { node: { text: t('nav.home.title', 'Home') }, main: { text: '' } },
|
||||
@ -48,6 +57,7 @@ export class AppChromeService {
|
||||
megaMenuDocked: this.megaMenuDocked,
|
||||
kioskMode: null,
|
||||
layout: PageLayoutType.Canvas,
|
||||
returnToPrevious: this.returnToPreviousData,
|
||||
});
|
||||
|
||||
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) {
|
||||
if (isShallowEqual(newState, current)) {
|
||||
return true;
|
||||
|
@ -2,7 +2,9 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { useGrafana } from 'app/core/context/GrafanaContext';
|
||||
import { t } from 'app/core/internationalization';
|
||||
|
||||
import { DismissableButton } from './DismissableButton';
|
||||
@ -14,11 +16,13 @@ export interface ReturnToPreviousProps {
|
||||
|
||||
export const ReturnToPrevious = ({ href, title }: ReturnToPreviousProps) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { chrome } = useGrafana();
|
||||
const handleOnClick = () => {
|
||||
console.log('Going to...', href);
|
||||
locationService.push(href);
|
||||
chrome.clearReturnToPrevious();
|
||||
};
|
||||
const handleOnDismiss = () => {
|
||||
console.log('Closing button');
|
||||
chrome.clearReturnToPrevious();
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -26,3 +26,10 @@ export function useGrafana(): GrafanaContextType {
|
||||
}
|
||||
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.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
useReturnToPrevious: jest.fn(),
|
||||
}));
|
||||
jest.mock('./api/buildInfo');
|
||||
jest.mock('./api/prometheus');
|
||||
|
@ -52,6 +52,7 @@ const mockRoute = (id?: string): GrafanaRouteComponentProps<{ id?: string; sourc
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
useReturnToPrevious: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../hooks/useAbilities');
|
||||
jest.mock('../../api/buildInfo');
|
||||
|
@ -24,6 +24,7 @@ import { RuleDetails } from './RuleDetails';
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getPluginLinkExtensions: jest.fn(),
|
||||
useReturnToPrevious: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks/useIsRuleEditable');
|
||||
|
@ -4,7 +4,7 @@ import React, { Fragment, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, textUtil, urlUtil } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { config, locationService, useReturnToPrevious } from '@grafana/runtime';
|
||||
import {
|
||||
Button,
|
||||
ClipboardButton,
|
||||
@ -55,6 +55,8 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
|
||||
const location = useLocation();
|
||||
const notifyApp = useAppNotification();
|
||||
|
||||
const setReturnToPrevious = useReturnToPrevious();
|
||||
|
||||
const [ruleToDelete, setRuleToDelete] = useState<CombinedRule>();
|
||||
const [redirectToClone, setRedirectToClone] = useState<
|
||||
{ identifier: RuleIdentifier; isProvisioned: boolean } | undefined
|
||||
@ -135,16 +137,31 @@ export const RuleDetailsActionButtons = ({ rule, rulesSource, isViewMode }: Prop
|
||||
const dashboardUID = rule.annotations[Annotation.dashboardUID];
|
||||
if (dashboardUID) {
|
||||
buttons.push(
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="dashboard"
|
||||
variant="primary"
|
||||
icon="apps"
|
||||
target="_blank"
|
||||
href={`d/${encodeURIComponent(dashboardUID)}`}
|
||||
>
|
||||
Go to dashboard
|
||||
</LinkButton>
|
||||
config.featureToggles.returnToPrevious ? (
|
||||
<LinkButton
|
||||
size="sm"
|
||||
key="dashboard"
|
||||
variant="primary"
|
||||
icon="apps"
|
||||
href={`d/${encodeURIComponent(dashboardUID)}`}
|
||||
onClick={() => {
|
||||
setReturnToPrevious({ title: rule.name, href: locationService.getLocation().pathname });
|
||||
}}
|
||||
>
|
||||
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];
|
||||
if (panelId) {
|
||||
|
Loading…
Reference in New Issue
Block a user