Skip to content

Commit cc60153

Browse files
committed
Add the ability to launch multiple modals with useModal hook
1 parent f0ceab6 commit cc60153

File tree

9 files changed

+248
-19
lines changed

9 files changed

+248
-19
lines changed

dynamic-demo-plugin/locales/en/plugin__console-demo-plugin.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,15 @@
4444
"Storage Classes": "Storage Classes",
4545
"StorageClasses present in this cluster:": "StorageClasses present in this cluster:",
4646
"Component is resolving": "Component is resolving",
47+
"Test overlay with props": "Test overlay with props",
48+
"Test modal launched with useOverlay": "Test modal launched with useOverlay",
49+
"Overlay modal": "Overlay modal",
4750
"Modal Launchers": "Modal Launchers",
4851
"Launch Modal": "Launch Modal",
4952
"Launch Modal Asynchronously": "Launch Modal Asynchronously",
53+
"Launch overlay": "Launch overlay",
54+
"Launch overlay with props": "Launch overlay with props",
55+
"Launch overlay modal": "Launch overlay modal",
5056
"Hello {{planet}}! I am Thor!": "Hello {{planet}}! I am Thor!",
5157
"Hello {{planet}}! I am Loki!": "Hello {{planet}}! I am Loki!",
5258
"Added title": "Added title",

dynamic-demo-plugin/src/components/Modals/ModalPage.tsx

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
K8sResourceCommon,
66
useK8sWatchResource,
77
useModal,
8+
useOverlay,
89
} from '@openshift-console/dynamic-plugin-sdk';
910
import './modal.scss';
1011
import { useTranslation } from 'react-i18next';
@@ -50,10 +51,39 @@ const LoadingComponent: React.FC = () => {
5051
);
5152
};
5253

54+
const OverlayComponent = ({ closeOverlay, heading = 'Default heading' }) => {
55+
const [right] = React.useState(`${800 * Math.random()}px`);
56+
const [top] = React.useState(`${800 * Math.random()}px`);
57+
58+
return (
59+
<div style={{
60+
backgroundColor: 'gray',
61+
padding: '1rem 4rem',
62+
position: 'absolute',
63+
right,
64+
textAlign: 'center',
65+
top,
66+
zIndex: 999,
67+
}}>
68+
<h2>{heading}</h2>
69+
<Button onClick={closeOverlay}>Close</Button>
70+
</div>
71+
);
72+
};
73+
74+
const OverlayModal = ({ body, closeOverlay, title }) => (
75+
<Modal isOpen onClose={closeOverlay}>
76+
<ModalHeader title={title} />
77+
<ModalBody>{body}</ModalBody>
78+
</Modal>
79+
);
80+
5381
export const TestModalPage: React.FC<{ closeComponent: any }> = () => {
54-
const launchModal = useModal();
5582
const { t } = useTranslation("plugin__console-demo-plugin");
5683

84+
const launchModal = useModal();
85+
const launchOverlay = useOverlay();
86+
5787
const TestComponent = ({ closeModal, ...rest }) => (
5888
<TestModal closeModal={closeModal} {...rest} />
5989
);
@@ -75,6 +105,27 @@ export const TestModalPage: React.FC<{ closeComponent: any }> = () => {
75105
const onClick = React.useCallback(() => launchModal(TestComponent, {}), [launchModal]);
76106
const onAsyncClick = React.useCallback(() => launchModal(AsyncTestComponent, {}), [launchModal]);
77107

108+
const onClickOverlayBasic = React.useCallback(
109+
() => {
110+
launchOverlay(OverlayComponent, {});
111+
},
112+
[launchOverlay],
113+
);
114+
115+
const onClickOverlayWithProps = React.useCallback(
116+
() => {
117+
launchOverlay(OverlayComponent, { heading: t('Test overlay with props') });
118+
},
119+
[launchOverlay],
120+
);
121+
122+
const onClickOverlayModal = React.useCallback(
123+
() => {
124+
launchOverlay(OverlayModal, { body: t('Test modal launched with useOverlay'), title: t('Overlay modal') });
125+
},
126+
[launchOverlay],
127+
);
128+
78129
return (
79130
<Flex
80131
alignItems={{ default: 'alignItemsCenter' }}
@@ -88,6 +139,15 @@ export const TestModalPage: React.FC<{ closeComponent: any }> = () => {
88139
<Button onClick={onAsyncClick}>
89140
{t('Launch Modal Asynchronously')}
90141
</Button>
142+
<Button onClick={onClickOverlayBasic}>
143+
{t('plugin__console-demo-plugin~Launch overlay')}
144+
</Button>
145+
<Button onClick={onClickOverlayWithProps}>
146+
{t('plugin__console-demo-plugin~Launch overlay with props')}
147+
</Button>
148+
<Button onClick={onClickOverlayModal}>
149+
{t('plugin__console-demo-plugin~Launch overlay modal')}
150+
</Button>
91151
</Flex>
92152
);
93153
};

frontend/packages/console-dynamic-plugin-sdk/docs/api.md

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
54. [DocumentTitle](#documenttitle)
5757
55. [usePrometheusPoll](#useprometheuspoll)
5858
56. [Timestamp](#timestamp)
59-
57. [useModal](#usemodal)
59+
57. [useOverlay](#useOverlay)
6060
58. [ActionServiceProvider](#actionserviceprovider)
6161
59. [NamespaceBar](#namespacebar)
6262
60. [ErrorBoundaryFallbackPage](#errorboundaryfallbackpage)
@@ -74,6 +74,7 @@
7474
72. [DEPRECATED] [ListPageFilter](#listpagefilter)
7575
73. [DEPRECATED] [useListPageFilter](#uselistpagefilter)
7676
74. [DEPRECATED] [YAMLEditor](#yamleditor)
77+
75. [DEPRECATED] [useModal](#usemodal)
7778

7879
---
7980

@@ -1921,24 +1922,46 @@ A component to render timestamp.<br/>The timestamps are synchronized between ind
19211922

19221923
---
19231924

1924-
## `useModal`
1925+
## `useOverlay`
19251926

19261927
### Summary
19271928

1928-
A hook to launch Modals.
1929+
The `useOverlay` hook inserts a component directly to the DOM, outside the web console's page structure. This allows the component to be freely styled and positioning via CSS. For example, to float the overlay in the top right corner of the UI: `style={{ position: 'absolute', right: '2rem', top: '2rem', zIndex: 999 }}`.<br/><br/>It is possible to add multiple overlays by calling `useOverlay` multiple times.<br/><br/>A `closeOverlay` function is passed to the overlay component. Calling it removes the component from the DOM without affecting any other overlays that may have been added via useOverlay.<br/><br/>Additional props can be passed to `useOverlay` and they will be passed through to the overlay component.
19291930

19301931

19311932

19321933
### Example
19331934

19341935

19351936
```tsx
1937+
const OverlayComponent = ({ closeOverlay, heading }) => {
1938+
return (
1939+
<div style={{ position: 'absolute', right: '2rem', top: '2rem', zIndex: 999 }}>
1940+
<h2>{heading}</h2>
1941+
<Button onClick={closeOverlay}>Close</Button>
1942+
</div>
1943+
);
1944+
};
1945+
1946+
const ModalComponent = ({ body, closeOverlay, title }) => (
1947+
<Modal isOpen onClose={closeOverlay}>
1948+
<ModalHeader title={title} />
1949+
<ModalBody>{body}</ModalBody>
1950+
</Modal>
1951+
);
1952+
19361953
const AppPage: React.FC = () => {
1937-
const launchModal = useModal();
1938-
const onClick = () => launchModal(ModalComponent);
1939-
return (
1940-
<Button onClick={onClick}>Launch a Modal</Button>
1941-
)
1954+
const launchOverlay = useOverlay();
1955+
const onClickOverlay = () => {
1956+
launchOverlay(OverlayComponent, { heading: 'Test overlay' });
1957+
};
1958+
const onClickModal = () => {
1959+
launchOverlay(ModalComponent, { body: 'Test modal', title: 'Overlay modal' });
1960+
};
1961+
return (
1962+
<Button onClick={onClickOverlay}>Launch an Overlay</Button>
1963+
<Button onClick={onClickModal}>Launch a Modal</Button>
1964+
)
19421965
}
19431966
```
19441967

@@ -2627,3 +2650,32 @@ A tuple containing the data filtered by all static filteres, the data filtered b
26272650
26282651
26292652
2653+
---
2654+
2655+
## `useModal`
2656+
2657+
### Summary [DEPRECATED]
2658+
2659+
@deprecated - Use useOverlay from \@console/dynamic-plugin-sdk instead.<br/>A hook to launch Modals.
2660+
2661+
2662+
2663+
### Example
2664+
2665+
2666+
```tsx
2667+
const AppPage: React.FC = () => {
2668+
const launchModal = useModal();
2669+
const onClick = () => launchModal(ModalComponent);
2670+
return (
2671+
<Button onClick={onClick}>Launch a Modal</Button>
2672+
)
2673+
}
2674+
```
2675+
2676+
2677+
2678+
2679+
2680+
2681+

frontend/packages/console-dynamic-plugin-sdk/src/api/dynamic-core-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -720,6 +720,7 @@ export const Timestamp: React.FC<TimestampProps> = require('@console/shared/src/
720720
.default;
721721

722722
export { useModal } from '../app/modal-support/useModal';
723+
export { useOverlay } from '../app/modal-support/useOverlay';
723724

724725
/**
725726
* Component that allows to receive contributions from other plugins for the `console.action/provider` extension type.

frontend/packages/console-dynamic-plugin-sdk/src/app/modal-support/ModalProvider.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as React from 'react';
22

33
type CloseModal = () => void;
4+
type CloseModalContextValue = () => void;
45

56
type UnknownProps = { [key: string]: unknown };
67
export type ModalComponent<P = UnknownProps> = React.FC<P & { closeModal: CloseModal }>;
@@ -9,7 +10,7 @@ export type LaunchModal = <P = UnknownProps>(component: ModalComponent<P>, extra
910

1011
type ModalContextValue = {
1112
launchModal: LaunchModal;
12-
closeModal: CloseModal;
13+
closeModal: CloseModalContextValue;
1314
};
1415

1516
export const ModalContext = React.createContext<ModalContextValue>({
@@ -30,7 +31,7 @@ export const ModalProvider: React.FC = ({ children }) => {
3031
},
3132
[setOpen, setComponent, setComponentProps],
3233
);
33-
const closeModal = React.useCallback<CloseModal>(() => setOpen(false), [setOpen]);
34+
const closeModal = React.useCallback<CloseModalContextValue>(() => setOpen(false), [setOpen]);
3435

3536
return (
3637
<ModalContext.Provider value={{ launchModal, closeModal }}>
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import * as React from 'react';
2+
import * as _ from 'lodash';
3+
4+
type CloseOverlay = () => void;
5+
type CloseOverlayContextValue = (id: string) => void;
6+
type UnknownProps = { [key: string]: unknown };
7+
8+
export type OverlayComponent<P = UnknownProps> = React.FC<P & { closeOverlay: CloseOverlay }>;
9+
10+
export type LaunchOverlay = <P = UnknownProps>(
11+
component: OverlayComponent<P>,
12+
extraProps: P,
13+
) => void;
14+
15+
type OverlayContextValue = {
16+
launchOverlay: LaunchOverlay;
17+
closeOverlay: CloseOverlayContextValue;
18+
};
19+
20+
export const OverlayContext = React.createContext<OverlayContextValue>({
21+
launchOverlay: () => {},
22+
closeOverlay: () => {},
23+
});
24+
25+
type ComponentMap = {
26+
[id: string]: {
27+
Component: OverlayComponent;
28+
props: { [key: string]: any };
29+
};
30+
};
31+
32+
export const OverlayProvider: React.FC = ({ children }) => {
33+
const [componentsMap, setComponentsMap] = React.useState<ComponentMap>({});
34+
35+
const launchOverlay = React.useCallback<LaunchOverlay>((component, componentProps) => {
36+
const id = _.uniqueId('plugin-overlay-');
37+
setComponentsMap((components) => ({
38+
...components,
39+
[id]: { Component: component, props: componentProps },
40+
}));
41+
}, []);
42+
43+
const closeOverlay = React.useCallback<CloseOverlayContextValue>((id) => {
44+
setComponentsMap((components) => _.omit(components, id));
45+
}, []);
46+
47+
return (
48+
<OverlayContext.Provider value={{ launchOverlay, closeOverlay }}>
49+
{_.map(componentsMap, (c, id) => (
50+
<c.Component {...c.props} key={id} closeOverlay={() => closeOverlay(id)} />
51+
))}
52+
{children}
53+
</OverlayContext.Provider>
54+
);
55+
};

frontend/packages/console-dynamic-plugin-sdk/src/app/modal-support/useModal.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { LaunchModal, ModalContext } from './ModalProvider';
44
type UseModalLauncher = () => LaunchModal;
55

66
/**
7+
* @deprecated - Use useOverlay from \@console/dynamic-plugin-sdk instead.
78
* A hook to launch Modals.
89
* @example
910
*```tsx
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as React from 'react';
2+
import { LaunchOverlay, OverlayContext } from './OverlayProvider';
3+
4+
type UseOverlayLauncher = () => LaunchOverlay;
5+
6+
/**
7+
* The `useOverlay` hook inserts a component directly to the DOM, outside the web console's page structure. This allows the component to be freely styled and positioning via CSS. For example, to float the overlay in the top right corner of the UI: `style={{ position: 'absolute', right: '2rem', top: '2rem', zIndex: 999 }}`.
8+
*
9+
* It is possible to add multiple overlays by calling `useOverlay` multiple times.
10+
*
11+
* A `closeOverlay` function is passed to the overlay component. Calling it removes the component from the DOM without affecting any other overlays that may have been added via useOverlay.
12+
*
13+
* Additional props can be passed to `useOverlay` and they will be passed through to the overlay component.
14+
* @example
15+
*```tsx
16+
* const OverlayComponent = ({ closeOverlay, heading }) => {
17+
* return (
18+
* <div style={{ position: 'absolute', right: '2rem', top: '2rem', zIndex: 999 }}>
19+
* <h2>{heading}</h2>
20+
* <Button onClick={closeOverlay}>Close</Button>
21+
* </div>
22+
* );
23+
* };
24+
*
25+
* const ModalComponent = ({ body, closeOverlay, title }) => (
26+
* <Modal isOpen onClose={closeOverlay}>
27+
* <ModalHeader title={title} />
28+
* <ModalBody>{body}</ModalBody>
29+
* </Modal>
30+
* );
31+
*
32+
* const AppPage: React.FC = () => {
33+
* const launchOverlay = useOverlay();
34+
* const onClickOverlay = () => {
35+
* launchOverlay(OverlayComponent, { heading: 'Test overlay' });
36+
* };
37+
* const onClickModal = () => {
38+
* launchOverlay(ModalComponent, { body: 'Test modal', title: 'Overlay modal' });
39+
* };
40+
* return (
41+
* <Button onClick={onClickOverlay}>Launch an Overlay</Button>
42+
* <Button onClick={onClickModal}>Launch a Modal</Button>
43+
* )
44+
* }
45+
* ```
46+
*/
47+
export const useOverlay: UseOverlayLauncher = () => {
48+
const { launchOverlay } = React.useContext(OverlayContext);
49+
return launchOverlay;
50+
};

frontend/public/components/app.jsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { initConsolePlugins } from '@console/dynamic-plugin-sdk/src/runtime/plug
4444
import { GuidedTour } from '@console/app/src/components/tour';
4545
import QuickStartDrawer from '@console/app/src/components/quick-starts/QuickStartDrawerAsync';
4646
import { ModalProvider } from '@console/dynamic-plugin-sdk/src/app/modal-support/ModalProvider';
47+
import { OverlayProvider } from '@console/dynamic-plugin-sdk/src/app/modal-support/OverlayProvider';
4748
import { settleAllPromises } from '@console/dynamic-plugin-sdk/src/utils/promise';
4849
import ToastProvider from '@console/shared/src/components/toast/ToastProvider';
4950
import { useToast } from '@console/shared/src/components/toast';
@@ -287,14 +288,16 @@ const App = (props) => {
287288
<CaptureTelemetry />
288289
<DetectNamespace>
289290
<ModalProvider>
290-
{contextProviderExtensions.reduce(
291-
(children, e) => (
292-
<EnhancedProvider key={e.uid} {...e.properties}>
293-
{children}
294-
</EnhancedProvider>
295-
),
296-
content,
297-
)}
291+
<OverlayProvider>
292+
{contextProviderExtensions.reduce(
293+
(children, e) => (
294+
<EnhancedProvider key={e.uid} {...e.properties}>
295+
{children}
296+
</EnhancedProvider>
297+
),
298+
content,
299+
)}
300+
</OverlayProvider>
298301
</ModalProvider>
299302
</DetectNamespace>
300303
<DetectLanguage />

0 commit comments

Comments
 (0)