ReturnToPrevious : Add logic to show the new component in AppChrome behind the new toggle (#81035)

This commit is contained in:
Laura Fernández 2024-01-30 13:34:59 +01:00 committed by GitHub
parent b7517330ee
commit 8ee7b1e00c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 115 additions and 18 deletions

View File

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

View 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();
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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