mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
ModalsContext: Unify modals context and manager (#84916)
* ModalsContext: Unify modals context and manager * Clear on location change * fixes * Update * use generics to avoid anys --------- Co-authored-by: joshhunt <josh@trtr.co>
This commit is contained in:
parent
ce57ce4125
commit
e90b87589f
@ -22,6 +22,10 @@ interface ModalsProviderProps {
|
||||
props?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated.
|
||||
* Not the real implementation used by core.
|
||||
*/
|
||||
export class ModalsProvider extends Component<ModalsProviderProps, ModalsContextState> {
|
||||
constructor(props: ModalsProviderProps) {
|
||||
super(props);
|
||||
|
@ -5,7 +5,7 @@ import { Router, Redirect, Switch, RouteComponentProps } from 'react-router-dom'
|
||||
import { CompatRouter, CompatRoute } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { config, locationService, navigationLogger, reportInteraction } from '@grafana/runtime';
|
||||
import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, ModalsProvider, PortalContainer } from '@grafana/ui';
|
||||
import { ErrorBoundaryAlert, GlobalStyles, ModalRoot, PortalContainer } from '@grafana/ui';
|
||||
import { getAppRoutes } from 'app/routes/routes';
|
||||
import { store } from 'app/store/store';
|
||||
|
||||
@ -15,6 +15,7 @@ import { GrafanaApp } from './app';
|
||||
import { AppChrome } from './core/components/AppChrome/AppChrome';
|
||||
import { AppNotificationList } from './core/components/AppNotifications/AppNotificationList';
|
||||
import { GrafanaContext } from './core/context/GrafanaContext';
|
||||
import { ModalsContextProvider } from './core/context/ModalsContextProvider';
|
||||
import { GrafanaRoute } from './core/navigation/GrafanaRoute';
|
||||
import { RouteDescriptor } from './core/navigation/types';
|
||||
import { contextSrv } from './core/services/context_srv';
|
||||
@ -102,11 +103,11 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
actions={[]}
|
||||
options={{ enableHistory: true, callbacks: { onSelectAction: commandPaletteActionSelected } }}
|
||||
>
|
||||
<ModalsProvider>
|
||||
<GlobalStyles />
|
||||
<div className="grafana-app">
|
||||
<Router history={locationService.getHistory()}>
|
||||
<CompatRouter>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<CompatRouter>
|
||||
<ModalsContextProvider>
|
||||
<GlobalStyles />
|
||||
<div className="grafana-app">
|
||||
<AppChrome>
|
||||
{pageBanners.map((Banner, index) => (
|
||||
<Banner key={index.toString()} />
|
||||
@ -118,13 +119,13 @@ export class AppWrapper extends React.Component<AppWrapperProps, AppWrapperState
|
||||
<Hook key={index.toString()} />
|
||||
))}
|
||||
</AppChrome>
|
||||
</CompatRouter>
|
||||
</Router>
|
||||
</div>
|
||||
<LiveConnectionWarning />
|
||||
<ModalRoot />
|
||||
<PortalContainer />
|
||||
</ModalsProvider>
|
||||
</div>
|
||||
<LiveConnectionWarning />
|
||||
<ModalRoot />
|
||||
<PortalContainer />
|
||||
</ModalsContextProvider>
|
||||
</CompatRouter>
|
||||
</Router>
|
||||
</KBarProvider>
|
||||
</ThemeProvider>
|
||||
</GrafanaContext.Provider>
|
||||
|
@ -58,7 +58,6 @@ import { initIconCache } from './core/icons/iconBundle';
|
||||
import { initializeI18n } from './core/internationalization';
|
||||
import { setMonacoEnv } from './core/monacoEnv';
|
||||
import { interceptLinkClicks } from './core/navigation/patch/interceptLinkClicks';
|
||||
import { ModalManager } from './core/services/ModalManager';
|
||||
import { NewFrontendAssetsChecker } from './core/services/NewFrontendAssetsChecker';
|
||||
import { backendSrv } from './core/services/backend_srv';
|
||||
import { contextSrv } from './core/services/context_srv';
|
||||
@ -207,10 +206,6 @@ export class GrafanaApp {
|
||||
setDataSourceSrv(dataSourceSrv);
|
||||
initWindowRuntime();
|
||||
|
||||
// init modal manager
|
||||
const modalManager = new ModalManager();
|
||||
modalManager.init();
|
||||
|
||||
let preloadResults: PluginPreloadResult[] = [];
|
||||
|
||||
if (contextSrv.user.orgRole !== '') {
|
||||
|
@ -1,18 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { ModalRoot, ModalsProvider } from '@grafana/ui';
|
||||
|
||||
import { connectWithProvider } from '../../utils/connectWithReduxStore';
|
||||
|
||||
/**
|
||||
* Component that enables rendering React modals from Angular
|
||||
*/
|
||||
export const AngularModalProxy = connectWithProvider((props: Record<string, unknown>) => {
|
||||
return (
|
||||
<>
|
||||
<ModalsProvider {...props}>
|
||||
<ModalRoot />
|
||||
</ModalsProvider>
|
||||
</>
|
||||
);
|
||||
});
|
124
public/app/core/context/ModalsContextProvider.tsx
Normal file
124
public/app/core/context/ModalsContextProvider.tsx
Normal file
@ -0,0 +1,124 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { ConfirmModal, ConfirmModalProps, ModalsContext } from '@grafana/ui';
|
||||
import { ModalsContextState } from '@grafana/ui/src/components/Modal/ModalsContext';
|
||||
import { ShowConfirmModalEvent, ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import appEvents from '../app_events';
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
interface StateType<TProps = any> {
|
||||
component: React.ComponentType<TProps> | null;
|
||||
props: TProps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the ModalsContext state logic (not used that much, only needed in edge cases)
|
||||
* Also implements the handling of the events ShowModalReactEvent and ShowConfirmModalEvent.
|
||||
*/
|
||||
export function ModalsContextProvider(props: Props) {
|
||||
const [state, setState] = useState<StateType>({
|
||||
component: null,
|
||||
props: {},
|
||||
});
|
||||
|
||||
const contextValue: ModalsContextState = useMemo(() => {
|
||||
function showModal<TProps = {}>(component: React.ComponentType<TProps>, props: TProps) {
|
||||
setState({ component, props });
|
||||
}
|
||||
|
||||
function hideModal() {
|
||||
setState({ component: null, props: {} });
|
||||
}
|
||||
|
||||
return {
|
||||
component: state.component,
|
||||
props: {
|
||||
...state.props,
|
||||
isOpen: true,
|
||||
onDismiss: hideModal,
|
||||
},
|
||||
showModal,
|
||||
hideModal,
|
||||
};
|
||||
}, [state]);
|
||||
|
||||
useEffect(() => {
|
||||
appEvents.subscribe(ShowModalReactEvent, ({ payload }) => {
|
||||
setState({
|
||||
component: payload.component,
|
||||
props: payload.props,
|
||||
});
|
||||
});
|
||||
|
||||
appEvents.subscribe(ShowConfirmModalEvent, (e) => {
|
||||
showConfirmModal(e, setState);
|
||||
});
|
||||
|
||||
// Dismiss the modal when the route changes (if there's a link in the modal)
|
||||
let prevPath = '';
|
||||
locationService.getHistory().listen((location) => {
|
||||
if (location.pathname !== prevPath) {
|
||||
setState({ component: null, props: {} });
|
||||
}
|
||||
prevPath = location.pathname;
|
||||
});
|
||||
}, []);
|
||||
|
||||
return <ModalsContext.Provider value={contextValue}>{props.children}</ModalsContext.Provider>;
|
||||
}
|
||||
|
||||
function showConfirmModal({ payload }: ShowConfirmModalEvent, setState: (state: StateType) => void) {
|
||||
const {
|
||||
confirmText,
|
||||
onConfirm = () => undefined,
|
||||
onDismiss,
|
||||
text2,
|
||||
altActionText,
|
||||
onAltAction,
|
||||
noText,
|
||||
text,
|
||||
text2htmlBind,
|
||||
yesText = 'Yes',
|
||||
icon,
|
||||
title = 'Confirm',
|
||||
yesButtonVariant,
|
||||
} = payload;
|
||||
|
||||
const hideModal = () => setState({ component: null, props: {} });
|
||||
|
||||
const props: ConfirmModalProps = {
|
||||
confirmText: yesText,
|
||||
confirmButtonVariant: yesButtonVariant,
|
||||
confirmationText: confirmText,
|
||||
icon,
|
||||
title,
|
||||
body: text,
|
||||
description: text2 && text2htmlBind ? textUtil.sanitize(text2) : text2,
|
||||
isOpen: true,
|
||||
dismissText: noText,
|
||||
onConfirm: () => {
|
||||
onConfirm();
|
||||
hideModal();
|
||||
},
|
||||
onDismiss: () => {
|
||||
onDismiss?.();
|
||||
hideModal();
|
||||
},
|
||||
onAlternative: onAltAction
|
||||
? () => {
|
||||
onAltAction();
|
||||
hideModal();
|
||||
}
|
||||
: undefined,
|
||||
alternativeText: altActionText,
|
||||
};
|
||||
|
||||
setState({ component: ConfirmModal, props });
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
import { config, CopyPanelEvent } from '@grafana/runtime';
|
||||
import { ConfirmModal, ConfirmModalProps } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { copyPanel } from 'app/features/dashboard/utils/panel';
|
||||
|
||||
import {
|
||||
ShowConfirmModalEvent,
|
||||
ShowConfirmModalPayload,
|
||||
ShowModalReactEvent,
|
||||
ShowModalReactPayload,
|
||||
} from '../../types/events';
|
||||
import { AngularModalProxy } from '../components/modals/AngularModalProxy';
|
||||
import { provideTheme } from '../utils/ConfigProvider';
|
||||
|
||||
export class ModalManager {
|
||||
reactModalRoot = document.body;
|
||||
reactModalNode = document.createElement('div');
|
||||
root = createRoot(this.reactModalNode);
|
||||
|
||||
init() {
|
||||
appEvents.subscribe(ShowConfirmModalEvent, (e) => this.showConfirmModal(e.payload));
|
||||
appEvents.subscribe(ShowModalReactEvent, (e) => this.showModalReact(e.payload));
|
||||
appEvents.subscribe(CopyPanelEvent, (e) => copyPanel(e.payload));
|
||||
}
|
||||
|
||||
showModalReact(options: ShowModalReactPayload) {
|
||||
const { component, props } = options;
|
||||
const modalProps = {
|
||||
component,
|
||||
props: {
|
||||
...props,
|
||||
isOpen: true,
|
||||
onDismiss: this.onReactModalDismiss,
|
||||
},
|
||||
};
|
||||
|
||||
const elem = React.createElement(provideTheme(AngularModalProxy, config.theme2), modalProps);
|
||||
this.reactModalRoot.appendChild(this.reactModalNode);
|
||||
this.root.render(elem);
|
||||
}
|
||||
|
||||
onReactModalDismiss = () => {
|
||||
this.root.render(null);
|
||||
this.reactModalRoot.removeChild(this.reactModalNode);
|
||||
};
|
||||
|
||||
showConfirmModal(payload: ShowConfirmModalPayload) {
|
||||
const {
|
||||
confirmText,
|
||||
onConfirm = () => undefined,
|
||||
onDismiss,
|
||||
text2,
|
||||
altActionText,
|
||||
onAltAction,
|
||||
noText,
|
||||
text,
|
||||
text2htmlBind,
|
||||
yesText = 'Yes',
|
||||
icon,
|
||||
title = 'Confirm',
|
||||
yesButtonVariant,
|
||||
} = payload;
|
||||
const props: ConfirmModalProps = {
|
||||
confirmText: yesText,
|
||||
confirmButtonVariant: yesButtonVariant,
|
||||
confirmationText: confirmText,
|
||||
icon,
|
||||
title,
|
||||
body: text,
|
||||
description: text2 && text2htmlBind ? textUtil.sanitize(text2) : text2,
|
||||
isOpen: true,
|
||||
dismissText: noText,
|
||||
onConfirm: () => {
|
||||
onConfirm();
|
||||
this.onReactModalDismiss();
|
||||
},
|
||||
onDismiss: () => {
|
||||
onDismiss?.();
|
||||
this.onReactModalDismiss();
|
||||
},
|
||||
onAlternative: onAltAction
|
||||
? () => {
|
||||
onAltAction();
|
||||
this.onReactModalDismiss();
|
||||
}
|
||||
: undefined,
|
||||
alternativeText: altActionText,
|
||||
};
|
||||
const modalProps = {
|
||||
component: ConfirmModal,
|
||||
props,
|
||||
};
|
||||
|
||||
const elem = React.createElement(provideTheme(AngularModalProxy, config.theme2), modalProps);
|
||||
this.reactModalRoot.appendChild(this.reactModalNode);
|
||||
this.root.render(elem);
|
||||
}
|
||||
}
|
@ -11,12 +11,12 @@ const AlertRulesDrawerContent = React.lazy(
|
||||
|
||||
interface Props {
|
||||
dashboardUid: string;
|
||||
onClose: () => void;
|
||||
onDismiss: () => void;
|
||||
}
|
||||
|
||||
export function AlertRulesDrawer({ dashboardUid, onClose }: Props) {
|
||||
export function AlertRulesDrawer({ dashboardUid, onDismiss }: Props) {
|
||||
return (
|
||||
<Drawer title="Alert rules" subtitle={<DrawerSubtitle dashboardUid={dashboardUid} />} onClose={onClose} size="lg">
|
||||
<Drawer title="Alert rules" subtitle={<DrawerSubtitle dashboardUid={dashboardUid} />} onClose={onDismiss} size="lg">
|
||||
<React.Suspense fallback={<LoadingPlaceholder text="Loading alert rules" />}>
|
||||
<AlertRulesDrawerContent dashboardUid={dashboardUid} />
|
||||
</React.Suspense>
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
|
||||
import { ToolbarButton } from '@grafana/ui';
|
||||
import { ModalsContext, ToolbarButton } from '@grafana/ui';
|
||||
|
||||
import { t } from '../../../../core/internationalization';
|
||||
import { useDashNavModalController } from '../../../dashboard/components/DashNav/DashNav';
|
||||
import { alertRuleApi } from '../api/alertRuleApi';
|
||||
import { GRAFANA_RULES_SOURCE_NAME } from '../utils/datasource';
|
||||
|
||||
@ -14,7 +13,7 @@ interface AlertRulesToolbarButtonProps {
|
||||
}
|
||||
|
||||
export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesToolbarButtonProps) {
|
||||
const { showModal, hideModal } = useDashNavModalController();
|
||||
const { showModal, hideModal } = useContext(ModalsContext);
|
||||
|
||||
const { data: namespaces = [] } = alertRuleApi.endpoints.prometheusRuleNamespaces.useQuery({
|
||||
ruleSourceName: GRAFANA_RULES_SOURCE_NAME,
|
||||
@ -25,11 +24,18 @@ export default function AlertRulesToolbarButton({ dashboardUid }: AlertRulesTool
|
||||
return null;
|
||||
}
|
||||
|
||||
const onShowDrawer = () => {
|
||||
showModal(AlertRulesDrawer, {
|
||||
dashboardUid: dashboardUid,
|
||||
onDismiss: hideModal,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ToolbarButton
|
||||
tooltip={t('dashboard.toolbar.alert-rules', 'Alert rules')}
|
||||
icon="bell"
|
||||
onClick={() => showModal(<AlertRulesDrawer dashboardUid={dashboardUid} onClose={hideModal} />)}
|
||||
onClick={onShowDrawer}
|
||||
key="button-alerting"
|
||||
/>
|
||||
);
|
||||
|
@ -9,7 +9,6 @@ import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { Trans, t } from 'app/core/internationalization';
|
||||
import { DashNavModalContextProvider, DashNavModalRoot } from 'app/features/dashboard/components/DashNav/DashNav';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
|
||||
@ -510,12 +509,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
lastGroup = action.group;
|
||||
}
|
||||
|
||||
return (
|
||||
<DashNavModalContextProvider>
|
||||
<ToolbarButtonRow alignment="right">{actionElements}</ToolbarButtonRow>
|
||||
<DashNavModalRoot />
|
||||
</DashNavModalContextProvider>
|
||||
);
|
||||
return <ToolbarButtonRow alignment="right">{actionElements}</ToolbarButtonRow>;
|
||||
}
|
||||
|
||||
function addDynamicActions(
|
||||
|
@ -2,7 +2,6 @@ import { css } from '@emotion/css';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { connect, ConnectedProps } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { createStateContext } from 'react-use';
|
||||
|
||||
import { textUtil } from '@grafana/data';
|
||||
import { selectors as e2eSelectors } from '@grafana/e2e-selectors/src';
|
||||
@ -49,27 +48,6 @@ const mapDispatchToProps = {
|
||||
updateTimeZoneForSession,
|
||||
};
|
||||
|
||||
export const [useDashNavModelContext, DashNavModalContextProvider] = createStateContext<{ component: React.ReactNode }>(
|
||||
{
|
||||
component: null,
|
||||
}
|
||||
);
|
||||
|
||||
export function useDashNavModalController() {
|
||||
const [_, setContextState] = useDashNavModelContext();
|
||||
|
||||
return {
|
||||
showModal: (component: React.ReactNode) => setContextState({ component }),
|
||||
hideModal: () => setContextState({ component: null }),
|
||||
};
|
||||
}
|
||||
|
||||
export function DashNavModalRoot() {
|
||||
const [contextState] = useDashNavModelContext();
|
||||
|
||||
return <>{contextState.component}</>;
|
||||
}
|
||||
|
||||
const connector = connect(null, mapDispatchToProps);
|
||||
|
||||
const selectors = e2eSelectors.pages.Dashboard.DashNav;
|
||||
@ -363,12 +341,11 @@ export const DashNav = React.memo<Props>((props) => {
|
||||
return (
|
||||
<AppChromeUpdate
|
||||
actions={
|
||||
<DashNavModalContextProvider>
|
||||
<>
|
||||
{renderLeftActions()}
|
||||
<NavToolbarSeparator leftActionsSeparator />
|
||||
<ToolbarButtonRow alignment="right">{renderRightActions()}</ToolbarButtonRow>
|
||||
<DashNavModalRoot />
|
||||
</DashNavModalContextProvider>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -5,7 +5,6 @@ import { mockToolkitActionCreator } from 'test/core/redux/mocks';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { NavModel } from '@grafana/data';
|
||||
import { ModalManager } from 'app/core/services/ModalManager';
|
||||
|
||||
import { backendSrv } from '../../core/services/backend_srv';
|
||||
import { Organization } from '../../types';
|
||||
@ -81,7 +80,6 @@ describe('Render', () => {
|
||||
});
|
||||
|
||||
it('should show a modal when submitting', async () => {
|
||||
new ModalManager().init();
|
||||
setup({
|
||||
organization: {
|
||||
name: 'Cool org',
|
||||
|
@ -5,7 +5,9 @@ import { Router } from 'react-router-dom';
|
||||
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { ModalRoot } from '@grafana/ui';
|
||||
import { GrafanaContext, GrafanaContextType } from 'app/core/context/GrafanaContext';
|
||||
import { ModalsContextProvider } from 'app/core/context/ModalsContextProvider';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
@ -30,7 +32,10 @@ export function TestProvider(props: Props) {
|
||||
return (
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<GrafanaContext.Provider value={context}>{children}</GrafanaContext.Provider>
|
||||
<ModalsContextProvider>
|
||||
<GrafanaContext.Provider value={context}>{children}</GrafanaContext.Provider>
|
||||
<ModalRoot />
|
||||
</ModalsContextProvider>
|
||||
</Router>
|
||||
</Provider>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user