mirror of https://github.com/grafana/grafana
[v11.0.x] ModalsContext: Unify modals context and manager (#85342)
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>
(cherry picked from commit e90b87589f
)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
pull/85358/head
parent
d1b7585ad5
commit
9a284d4560
@ -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> |
||||
</> |
||||
); |
||||
}); |
@ -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); |
||||
} |
||||
} |
Loading…
Reference in new issue