Skip to content

Commit a90897b

Browse files
committed
CONSOLE-3565: Expose useAnnotationsModal hook in dynamic plugin SDK
- Factor out AnnotationsModal component from annotationsModal launcher - Factor out ModalWrapper from createModalLauncher and export as internal API from SDK - Move ModalWrapperProps, TagsModalProps, and AnnotationsModalProps to SDK internal types - Implement useAnnotationsModal hook in dynamic plugin SDK and use in core details page component - Improve SDK ModalComponent prop types - Fix useModal hook example in SDK docs - Address some minor tech debt issues in modal factory
1 parent 0742e71 commit a90897b

File tree

10 files changed

+152
-94
lines changed

10 files changed

+152
-94
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -2051,7 +2051,7 @@ A hook to launch Modals.
20512051

20522052
```tsx
20532053
const AppPage: React.FC = () => {
2054-
const [launchModal] = useModal();
2054+
const launchModal = useModal();
20552055
const onClick = () => launchModal(ModalComponent);
20562056
return (
20572057
<Button onClick={onClick}>Launch a Modal</Button>

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
QuickStartsLoaderProps,
2222
UseURLPoll,
2323
UseLastNamespace,
24+
AnnotationsModalProps,
25+
ModalWrapperProps,
2426
} from './internal-types';
2527

2628
export const ActivityItem: React.FC<ActivityItemProps> = require('@console/shared/src/components/dashboard/activity-card/ActivityItem')
@@ -69,4 +71,7 @@ export const useURLPoll: UseURLPoll = require('@console/internal/components/util
6971
.useURLPoll;
7072
export const useLastNamespace: UseLastNamespace = require('@console/app/src/components/detect-namespace/useLastNamespace')
7173
.useLastNamespace;
72-
74+
export const ModalWrapper: React.FC<ModalWrapperProps> = require('@console/internal/components/factory/modal')
75+
.ModalWrapper;
76+
export const AnnotationsModal: React.FC<AnnotationsModalProps> = require('@console/internal/components/modals/tags')
77+
.AnnotationsModal;

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

+18
Original file line numberDiff line numberDiff line change
@@ -294,3 +294,21 @@ export type UseURLPoll = <R>(
294294
delay?: number,
295295
...dependencies: any[]
296296
) => [R, any, boolean];
297+
298+
export type ModalWrapperProps = {
299+
blocking?: boolean;
300+
className?: string;
301+
onClose?: (event?: React.SyntheticEvent) => void;
302+
};
303+
304+
export type TagsModalProps = {
305+
cancel?: () => void;
306+
close?: () => void;
307+
kind: K8sModel;
308+
path: string;
309+
resource: K8sResourceCommon;
310+
tags?: { [key: string]: string };
311+
titleKey: string;
312+
};
313+
314+
export type AnnotationsModalProps = Omit<TagsModalProps, 'path' | 'tags' | 'titleKey'>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import * as React from 'react';
2+
import { AnnotationsModalProps } from '../../../api/internal-types';
3+
import { K8sResourceCommon, getGroupVersionKindForResource, useK8sModel } from '../../../lib-core';
4+
import { AnnotationsModal, ModalWrapper } from '../../../lib-internal';
5+
import { ModalComponent } from '../../modal-support/ModalProvider';
6+
import { useModal } from '../../modal-support/useModal';
7+
8+
type UseAnnotationsModal = (resource: K8sResourceCommon) => () => void;
9+
10+
const AnnotationsModalComponent: ModalComponent<AnnotationsModalProps> = ({
11+
closeModal,
12+
kind,
13+
resource,
14+
}) => {
15+
return (
16+
<ModalWrapper blocking onClose={closeModal}>
17+
<AnnotationsModal cancel={closeModal} close={closeModal} kind={kind} resource={resource} />
18+
</ModalWrapper>
19+
);
20+
};
21+
22+
/**
23+
* A hook for launching a modal for editing a resource's annotations.
24+
*
25+
* @hook useAnnotationsModal
26+
* @argument resource - The resource to edit annotations for.
27+
* @returns a function which will launch a modal for editing a resource's annotations.
28+
* @example
29+
* const PodAnnotationsButton = ({ pod }) => {
30+
* const launchAnnotationsModal = useAnnotationsModal<PodKind>(pod);
31+
* return <button onClick={launchAnnotationsModal}>Edit Pod Annotations</button>
32+
* }
33+
*/
34+
export const useAnnotationsModal: UseAnnotationsModal = (resource) => {
35+
const launcher = useModal();
36+
const groupVersionKind = getGroupVersionKindForResource(resource);
37+
const [kind] = useK8sModel(groupVersionKind);
38+
return React.useCallback(
39+
() =>
40+
resource &&
41+
kind &&
42+
launcher<AnnotationsModalProps>(AnnotationsModalComponent, { kind, resource }),
43+
[launcher, kind, resource],
44+
);
45+
};

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

+10-13
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,40 @@
11
import * as React from 'react';
2-
import * as _ from 'lodash';
32

43
type CloseModal = () => void;
54

6-
export type ModalComponent = React.FC<{
7-
closeModal: CloseModal;
8-
[key: string]: unknown;
9-
}>;
5+
type UnknownProps = { [key: string]: unknown };
6+
export type ModalComponent<P = UnknownProps> = React.FC<P & { closeModal: CloseModal }>;
107

11-
export type LaunchModal = (component: ModalComponent, extraProps: Record<string, unknown>) => void;
8+
export type LaunchModal = <P = UnknownProps>(component: ModalComponent<P>, extraProps: P) => void;
129

1310
type ModalContextValue = {
1411
launchModal: LaunchModal;
1512
closeModal: CloseModal;
1613
};
1714

1815
export const ModalContext = React.createContext<ModalContextValue>({
19-
launchModal: _.noop,
20-
closeModal: _.noop,
16+
launchModal: () => {},
17+
closeModal: () => {},
2118
});
2219

2320
export const ModalProvider: React.FC = ({ children }) => {
2421
const [isOpen, setOpen] = React.useState(false);
2522
const [Component, setComponent] = React.useState<ModalComponent>();
26-
const [componentProps, setComponentProps] = React.useState<unknown>({});
23+
const [componentProps, setComponentProps] = React.useState({});
2724

28-
const launchModal = React.useCallback(
29-
(component: ModalComponent, compProps: unknown) => {
25+
const launchModal = React.useCallback<LaunchModal>(
26+
(component, compProps) => {
3027
setComponent(() => component);
3128
setComponentProps(compProps);
3229
setOpen(true);
3330
},
3431
[setOpen, setComponent, setComponentProps],
3532
);
36-
const closeModal = React.useCallback(() => setOpen(false), [setOpen]);
33+
const closeModal = React.useCallback<CloseModal>(() => setOpen(false), [setOpen]);
3734

3835
return (
3936
<ModalContext.Provider value={{ launchModal, closeModal }}>
40-
{isOpen && !!Component && <Component {...(componentProps as {})} closeModal={closeModal} />}
37+
{isOpen && !!Component && <Component {...componentProps} closeModal={closeModal} />}
4138
{children}
4239
</ModalContext.Provider>
4340
);

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type UseModalLauncher = () => LaunchModal;
88
* @example
99
*```tsx
1010
* const AppPage: React.FC = () => {
11-
* const [launchModal] = useModal();
11+
* const launchModal = useModal();
1212
* const onClick = () => launchModal(ModalComponent);
1313
* return (
1414
* <Button onClick={onClick}>Launch a Modal</Button>

frontend/public/components/factory/modal.tsx

+58-59
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,84 @@
1+
import * as classNames from 'classnames';
2+
import * as Modal from 'react-modal';
13
import * as React from 'react';
24
import * as ReactDOM from 'react-dom';
35
import { Provider } from 'react-redux';
4-
import * as Modal from 'react-modal';
56
import { Router } from 'react-router-dom';
67
import { CompatRouter } from 'react-router-dom-v5-compat';
7-
import * as classNames from 'classnames';
8-
import * as _ from 'lodash-es';
9-
import { ActionGroup, Button, Text, TextContent, TextVariants } from '@patternfly/react-core';
108
import { useTranslation } from 'react-i18next';
11-
import i18next from 'i18next';
9+
import { ActionGroup, Button, Text, TextContent, TextVariants } from '@patternfly/react-core';
10+
import { ModalWrapperProps } from '@console/dynamic-plugin-sdk/src/api/internal-types';
1211
import CloseButton from '@console/shared/src/components/close-button';
13-
1412
import store from '../../redux';
1513
import { ButtonBar } from '../utils/button-bar';
1614
import { history } from '../utils/router';
1715

18-
export const createModal: CreateModal = (getModalContainer) => {
19-
const modalContainer = document.getElementById('modal-container');
16+
export const createModal: CreateModal = (getModalElement) => {
17+
const containerElement = document.getElementById('modal-container');
2018
const result = new Promise<void>((resolve) => {
2119
const closeModal = (e?: React.SyntheticEvent) => {
2220
if (e && e.stopPropagation) {
2321
e.stopPropagation();
2422
}
25-
ReactDOM.unmountComponentAtNode(modalContainer);
23+
ReactDOM.unmountComponentAtNode(containerElement);
2624
resolve();
2725
};
2826
Modal.setAppElement(document.getElementById('app-content'));
29-
modalContainer && ReactDOM.render(getModalContainer(closeModal), modalContainer);
27+
containerElement && ReactDOM.render(getModalElement(closeModal), containerElement);
3028
});
3129
return { result };
3230
};
3331

34-
export const createModalLauncher: CreateModalLauncher = (Component, modalWrapper = true) => (
35-
props,
36-
) => {
32+
export const ModalWrapper: React.FC<ModalWrapperProps> = ({
33+
blocking,
34+
className,
35+
children,
36+
onClose,
37+
}) => {
38+
const { t } = useTranslation();
39+
const parentSelector = React.useCallback(() => document.querySelector('#modal-container'), []);
40+
return (
41+
<Modal
42+
className={classNames('modal-dialog', className)}
43+
contentLabel={t('public~Modal')}
44+
isOpen
45+
onRequestClose={onClose}
46+
overlayClassName="co-overlay"
47+
parentSelector={parentSelector}
48+
shouldCloseOnOverlayClick={!blocking}
49+
>
50+
{children}
51+
</Modal>
52+
);
53+
};
54+
55+
export const createModalLauncher: CreateModalLauncher = (Component, modalWrapper = true) => ({
56+
blocking,
57+
modalClassName,
58+
close,
59+
cancel,
60+
...props
61+
}) => {
3762
const getModalContainer: GetModalContainer = (onClose) => {
38-
const _handleClose = (e: React.SyntheticEvent) => {
39-
onClose && onClose(e);
40-
props.close && props.close();
63+
const handleClose = (e: React.SyntheticEvent) => {
64+
onClose?.(e);
65+
close?.();
4166
};
42-
const _handleCancel = (e: React.SyntheticEvent) => {
43-
props.cancel && props.cancel();
44-
_handleClose(e);
67+
const handleCancel = (e: React.SyntheticEvent) => {
68+
cancel?.();
69+
handleClose(e);
4570
};
4671

4772
return (
4873
<Provider store={store}>
4974
<Router {...{ history, basename: window.SERVER_FLAGS.basePath }}>
5075
<CompatRouter>
5176
{modalWrapper ? (
52-
<Modal
53-
isOpen={true}
54-
contentLabel={i18next.t('public~Modal')}
55-
onRequestClose={_handleClose}
56-
className={classNames('modal-dialog', props.modalClassName)}
57-
overlayClassName="co-overlay"
58-
shouldCloseOnOverlayClick={!props.blocking}
59-
parentSelector={() => document.getElementById('modal-container')}
60-
>
61-
<Component
62-
{...(_.omit(props, 'blocking', 'modalClassName') as any)}
63-
cancel={_handleCancel}
64-
close={_handleClose}
65-
/>
66-
</Modal>
77+
<ModalWrapper blocking={blocking} className={modalClassName} onClose={handleClose}>
78+
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
79+
</ModalWrapper>
6780
) : (
68-
<Component
69-
{...(_.omit(props, 'blocking', 'modalClassName') as any)}
70-
cancel={_handleCancel}
71-
close={_handleClose}
72-
/>
81+
<Component {...(props as any)} cancel={handleCancel} close={handleClose} />
7382
)}
7483
</CompatRouter>
7584
</Router>
@@ -79,7 +88,7 @@ export const createModalLauncher: CreateModalLauncher = (Component, modalWrapper
7988
return createModal(getModalContainer);
8089
};
8190

82-
export const ModalTitle: React.SFC<ModalTitleProps> = ({
91+
export const ModalTitle: React.FC<ModalTitleProps> = ({
8392
children,
8493
className = 'modal-header',
8594
close,
@@ -102,13 +111,13 @@ export const ModalTitle: React.SFC<ModalTitleProps> = ({
102111
</div>
103112
);
104113

105-
export const ModalBody: React.SFC<ModalBodyProps> = ({ children }) => (
114+
export const ModalBody: React.FC<ModalBodyProps> = ({ children }) => (
106115
<div className="modal-body">
107116
<div className="modal-body-content">{children}</div>
108117
</div>
109118
);
110119

111-
export const ModalFooter: React.SFC<ModalFooterProps> = ({
120+
export const ModalFooter: React.FC<ModalFooterProps> = ({
112121
message,
113122
errorMessage,
114123
inProgress,
@@ -126,7 +135,7 @@ export const ModalFooter: React.SFC<ModalFooterProps> = ({
126135
);
127136
};
128137

129-
export const ModalSubmitFooter: React.SFC<ModalSubmitFooterProps> = ({
138+
export const ModalSubmitFooter: React.FC<ModalSubmitFooterProps> = ({
130139
message,
131140
errorMessage,
132141
inProgress,
@@ -137,7 +146,7 @@ export const ModalSubmitFooter: React.SFC<ModalSubmitFooterProps> = ({
137146
submitDisabled,
138147
submitDanger,
139148
buttonAlignment = 'right',
140-
resetText = i18next.t('public~Reset'),
149+
resetText,
141150
reset,
142151
}) => {
143152
const { t } = useTranslation();
@@ -163,31 +172,21 @@ export const ModalSubmitFooter: React.SFC<ModalSubmitFooterProps> = ({
163172
</Button>
164173
);
165174

166-
const submitButton = submitDanger ? (
175+
const submitButton = (
167176
<Button
168-
type="submit"
169-
variant="danger"
170-
isDisabled={submitDisabled}
171177
data-test="confirm-action"
172178
id="confirm-action"
173-
>
174-
{submitText}
175-
</Button>
176-
) : (
177-
<Button
178-
type="submit"
179-
variant="primary"
180179
isDisabled={submitDisabled}
181-
data-test="confirm-action"
182-
id="confirm-action"
180+
type="submit"
181+
variant={submitDanger ? 'danger' : 'primary'}
183182
>
184-
{submitText}
183+
{submitText || t('public~Submit')}
185184
</Button>
186185
);
187186

188187
const resetButton = (
189188
<Button variant="link" isInline onClick={onResetClick} id="reset-action">
190-
{resetText}
189+
{resetText || t('public~Reset')}
191190
</Button>
192191
);
193192

0 commit comments

Comments
 (0)