From f6e127e55c767b95e5217848e2ffa5ed961a353e Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 19 May 2020 17:31:03 +0300 Subject: [PATCH] Chore: modal refactor --- .../ConfigProvider/ConfigProvider.tsx | 34 +++ src/components/Modal/ModalController.tsx | 70 ++++++ src/components/index.ts | 1 + src/panel-triggers/components/AckModal.tsx | 227 ++++++++++-------- .../components/AlertList/AlertCard.tsx | 55 ++--- .../components/Problems/ProblemDetails.tsx | 45 ++-- .../components/Problems/Problems.tsx | 2 +- 7 files changed, 280 insertions(+), 154 deletions(-) create mode 100644 src/components/ConfigProvider/ConfigProvider.tsx create mode 100644 src/components/Modal/ModalController.tsx diff --git a/src/components/ConfigProvider/ConfigProvider.tsx b/src/components/ConfigProvider/ConfigProvider.tsx new file mode 100644 index 0000000..58c4705 --- /dev/null +++ b/src/components/ConfigProvider/ConfigProvider.tsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { config, GrafanaBootConfig } from '@grafana/runtime'; +import { ThemeContext, getTheme } from '@grafana/ui'; +import { GrafanaThemeType } from '@grafana/data'; + +export const ConfigContext = React.createContext(config); +export const ConfigConsumer = ConfigContext.Consumer; + +export const provideConfig = (component: React.ComponentType) => { + const ConfigProvider = (props: any) => ( + {React.createElement(component, { ...props })} + ); + + return ConfigProvider; +}; + +export const getCurrentThemeName = () => + config.bootData.user.lightTheme ? GrafanaThemeType.Light : GrafanaThemeType.Dark; + +export const getCurrentTheme = () => getTheme(getCurrentThemeName()); + +export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { + return ( + + {config => { + return {children}; + }} + + ); +}; + +export const provideTheme = (component: React.ComponentType) => { + return provideConfig((props: any) => {React.createElement(component, { ...props })}); +}; diff --git a/src/components/Modal/ModalController.tsx b/src/components/Modal/ModalController.tsx new file mode 100644 index 0000000..0d944f1 --- /dev/null +++ b/src/components/Modal/ModalController.tsx @@ -0,0 +1,70 @@ +import React, { FC } from 'react'; +import ReactDOM from 'react-dom'; +import { provideTheme } from '../ConfigProvider/ConfigProvider'; + +interface ModalWrapperProps { + showModal: (component: React.ComponentType, props: T) => void; + hideModal: () => void; +} + +type ModalWrapper = FC; + +interface Props { + children: ModalWrapper; +} + +interface State { + component: React.ComponentType | null; + props: any; +} + +export class ModalController extends React.Component { + modalRoot = document.body; + modalNode = document.createElement('div'); + + constructor(props: Props) { + super(props); + this.state = { + component: null, + props: {}, + }; + } + + showModal = (component: React.ComponentType, props: any) => { + this.setState({ + component, + props + }); + }; + + hideModal = () => { + this.modalRoot.removeChild(this.modalNode); + this.setState({ + component: null, + props: {}, + }); + }; + + renderModal() { + const { component, props } = this.state; + if (!component) { + return null; + } + + this.modalRoot.appendChild(this.modalNode); + const modal = React.createElement(provideTheme(component), props); + return ReactDOM.createPortal(modal, this.modalNode); + } + + render() { + const { children } = this.props; + const ChildrenComponent = children; + + return ( + <> + + {this.renderModal()} + + ); + } +} diff --git a/src/components/index.ts b/src/components/index.ts index b2fa48e..ec905c1 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,3 +1,4 @@ export { GFHeartIcon } from './GFHeartIcon/GFHeartIcon'; export { FAIcon } from './FAIcon/FAIcon'; export { Tooltip } from './Tooltip/Tooltip'; +export { ModalController } from './Modal/ModalController'; diff --git a/src/panel-triggers/components/AckModal.tsx b/src/panel-triggers/components/AckModal.tsx index 1d54fec..b542316 100644 --- a/src/panel-triggers/components/AckModal.tsx +++ b/src/panel-triggers/components/AckModal.tsx @@ -1,25 +1,24 @@ import React, { PureComponent } from 'react'; -import ReactDOM from 'react-dom'; -import classNames from 'classnames'; +import { cx, css } from 'emotion'; import { ZBX_ACK_ACTION_ADD_MESSAGE, ZBX_ACK_ACTION_ACK, ZBX_ACK_ACTION_CHANGE_SEVERITY, ZBX_ACK_ACTION_CLOSE } from '../../datasource-zabbix/constants'; -import { Button, Input, VerticalGroup, Spinner } from '@grafana/ui'; +import { Button, VerticalGroup, Spinner, Modal, Input, Forms, stylesFactory, withTheme, Themeable } from '@grafana/ui'; import { FAIcon } from '../../components'; import * as grafanaUi from '@grafana/ui'; -const Checkbox: any = grafanaUi.Forms?.Checkbox || (grafanaUi as any).Checkbox; -const RadioButtonGroup: any = grafanaUi.Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup; +import { GrafanaTheme } from '@grafana/data'; +const Checkbox: any = Forms?.Checkbox || (grafanaUi as any).Checkbox; +const RadioButtonGroup: any = Forms?.RadioButtonGroup || (grafanaUi as any).RadioButtonGroup; const KEYBOARD_ENTER_KEY = 13; const KEYBOARD_ESCAPE_KEY = 27; -interface Props { +interface Props extends Themeable { canAck?: boolean; canClose?: boolean; - isOpen?: boolean; severity?: number; withBackdrop?: boolean; onSubmit: (data?: AckProblemData) => Promise | any; - onClose?: () => void; + onDismiss?: () => void; } interface State { @@ -50,9 +49,7 @@ const severityOptions = [ {value: 5, label: 'Disaster'} ]; -export class AckModal extends PureComponent { - modalContainer: HTMLElement; - +export class AckModalUnthemed extends PureComponent { static defaultProps: Partial = { withBackdrop: true, }; @@ -70,8 +67,6 @@ export class AckModal extends PureComponent { selectedSeverity: props.severity || 0, loading: false, }; - - this.modalContainer = document.body; } handleChange = (event: React.ChangeEvent) => { @@ -108,7 +103,7 @@ export class AckModal extends PureComponent { dismiss = () => { this.setState({ value: '', error: false, errorMessage: '', ackError: '', loading: false }); - this.props.onClose(); + this.props.onDismiss(); } submit = () => { @@ -141,7 +136,7 @@ export class AckModal extends PureComponent { } ackData.action = action; - this.props.onSubmit(ackData).then((response) => { + this.props.onSubmit(ackData).then(() => { this.dismiss(); }).catch(err => { this.setState({ @@ -151,87 +146,131 @@ export class AckModal extends PureComponent { }); } - render() { - const { canClose, canAck } = this.props; - if (!this.props.isOpen || !this.modalContainer) { - return null; - } + renderActions() { + const { canClose } = this.props; - const inputClass = classNames({ 'zbx-ack-error': this.state.error }); - - const modalNode = ( -
-
-
-

- {this.state.loading ? : } - Acknowledge Problem -

- - - - -
-
-
- -
- -
- - - - {this.state.changeSeverity && - - } - {canClose && - - } - -
- - {this.state.ackError && -
- {this.state.ackError} -
- } - -
- - -
-
-
-
- ); - - const modalNodeWithBackdrop = [ - modalNode, -
+ const actions = [ + , + , + this.state.changeSeverity && + , + canClose && + , ]; - const modal = this.props.withBackdrop ? modalNodeWithBackdrop : modalNode; - return ReactDOM.createPortal(modal, this.modalContainer); + // doesn't handle empty elements properly, so don't return it + return actions.filter(e => e); + } + + render() { + const { theme } = this.props; + + const styles = getStyles(theme); + const modalClass = cx(styles.modal); + const modalTitleClass = cx(styles.modalHeaderTitle); + const inputGroupClass = cx('gf-form', styles.inputGroup); + const inputClass = cx(this.state.error && styles.input); + const inputHintClass = cx('gf-form-hint-text', styles.inputHint); + const inputErrorClass = cx('gf-form-hint-text', styles.inputError); + + return ( + + {this.state.loading ? : } + Acknowledge Problem + + } + > +
+ +
+ +
+ + {this.renderActions()} + +
+ + {this.state.ackError && +
+ {this.state.ackError} +
+ } + +
+ + +
+
+ ); } } + +const getStyles = stylesFactory((theme: GrafanaTheme) => { + return { + modal: css` + width: 500px; + `, + modalHeaderTitle: css` + font-size: ${theme.typography.heading.h3}; + padding-top: ${theme.spacing.sm}; + margin: 0 ${theme.spacing.md}; + display: flex; + `, + inputGroup: css` + margin-bottom: 16px; + `, + input: css` + border-color: ${theme.colors.red}; + border-radius: 2px; + outline-offset: 2px; + box-shadow: 0 0 0 2px ${theme.colors.pageBg}, 0 0 0px 4px ${theme.colors.red}; + `, + inputHint: css` + display: inherit; + float: right; + color: ${theme.colors.textWeak}; + `, + inputError: css` + float: left; + color: ${theme.colors.red}; + `, + ackError: css` + color: ${theme.colors.red}; + `, + }; +}); + +export const AckModal = withTheme(AckModalUnthemed); diff --git a/src/panel-triggers/components/AlertList/AlertCard.tsx b/src/panel-triggers/components/AlertList/AlertCard.tsx index a1d76ff..ad738f3 100644 --- a/src/panel-triggers/components/AlertList/AlertCard.tsx +++ b/src/panel-triggers/components/AlertList/AlertCard.tsx @@ -6,10 +6,10 @@ import { isNewProblem, formatLastChange } from '../../utils'; import { ProblemsPanelOptions, TriggerSeverity } from '../../types'; import { AckProblemData, AckModal } from '../AckModal'; import EventTag from '../EventTag'; -import Tooltip from '../../../components/Tooltip/Tooltip'; import AlertAcknowledges from './AlertAcknowledges'; import AlertIcon from './AlertIcon'; import { ProblemDTO, ZBXTag } from '../../../datasource-zabbix/types'; +import { ModalController, Tooltip } from '../../../components'; interface AlertCardProps { problem: ProblemDTO; @@ -18,15 +18,7 @@ interface AlertCardProps { onProblemAck?: (problem: ProblemDTO, data: AckProblemData) => Promise | any; } -interface AlertCardState { - showAckDialog: boolean; -} - -export default class AlertCard extends PureComponent { - constructor(props) { - super(props); - this.state = { showAckDialog: false }; - } +export default class AlertCard extends PureComponent { handleTagClick = (tag: ZBXTag, ctrlKey?: boolean, shiftKey?: boolean) => { if (this.props.onTagClick) { @@ -36,23 +28,7 @@ export default class AlertCard extends PureComponent { const problem = this.props.problem; - return this.props.onProblemAck(problem, data).then(result => { - this.closeAckDialog(); - }).catch(err => { - console.log(err); - this.closeAckDialog(); - }); - } - - showAckDialog = () => { - const problem = this.props.problem; - if (problem.showAckButton) { - this.setState({ showAckDialog: true }); - } - } - - closeAckDialog = () => { - this.setState({ showAckDialog: false }); + return this.props.onProblemAck(problem, data); } render() { @@ -61,10 +37,11 @@ export default class AlertCard extends PureComponent s.priority === Number(problem.severity)); + severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); if (problem.severity) { - severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === Number(problem.severity)); + severityDesc = _.find(panelOptions.triggerSeverity, s => s.priority === problemSeverity); } const lastchange = formatLastChange(problem.timestamp, panelOptions.customLastChangeFormat && panelOptions.lastChangeFormat); @@ -161,14 +138,24 @@ export default class AlertCard extends PureComponent )} {problem.eventid && ( - + + {({ showModal, hideModal }) => ( + { + showModal(AckModal, { + canClose: problem.manual_close === '1', + severity: problemSeverity, + onSubmit: this.ackProblem, + onDismiss: hideModal, + }); + }} + /> + )} + )} - ); } diff --git a/src/panel-triggers/components/Problems/ProblemDetails.tsx b/src/panel-triggers/components/Problems/ProblemDetails.tsx index 19b28f7..c7b384c 100644 --- a/src/panel-triggers/components/Problems/ProblemDetails.tsx +++ b/src/panel-triggers/components/Problems/ProblemDetails.tsx @@ -9,7 +9,7 @@ import EventTag from '../EventTag'; import ProblemStatusBar from './ProblemStatusBar'; import AcknowledgesList from './AcknowledgesList'; import ProblemTimeline from './ProblemTimeline'; -import { FAIcon, Tooltip } from '../../../components'; +import { FAIcon, Tooltip, ModalController } from '../../../components'; import { renderUrl } from '../../utils'; import { getLocationSrv } from '@grafana/runtime'; @@ -28,17 +28,15 @@ interface ProblemDetailsState { events: ZBXEvent[]; alerts: ZBXAlert[]; show: boolean; - showAckDialog: boolean; } -export default class ProblemDetails extends PureComponent { +export class ProblemDetails extends PureComponent { constructor(props) { super(props); this.state = { events: [], alerts: [], show: false, - showAckDialog: false, }; } @@ -76,17 +74,7 @@ export default class ProblemDetails extends PureComponent { const problem = this.props.original as ProblemDTO; - return this.props.onProblemAck(problem, data).then(result => { - this.closeAckDialog(); - }); - } - - showAckDialog = () => { - this.setState({ showAckDialog: true }); - } - - closeAckDialog = () => { - this.setState({ showAckDialog: false }); + return this.props.onProblemAck(problem, data); } openInExplore = () => { @@ -149,10 +137,23 @@ export default class ProblemDetails extends PureComponent {problem.showAckButton &&
- + + {({ showModal, hideModal }) => ( + { + showModal(AckModal, { + canClose: problem.manual_close === '1', + severity: problemSeverity, + onSubmit: this.ackProblem, + onDismiss: hideModal, + }); + }} + /> + )} +
} @@ -208,12 +209,6 @@ export default class ProblemDetails extends PureComponent} {problem.hosts && } - ); } diff --git a/src/panel-triggers/components/Problems/Problems.tsx b/src/panel-triggers/components/Problems/Problems.tsx index 71910af..14dd373 100644 --- a/src/panel-triggers/components/Problems/Problems.tsx +++ b/src/panel-triggers/components/Problems/Problems.tsx @@ -5,7 +5,7 @@ import _ from 'lodash'; import moment from 'moment'; import { isNewProblem } from '../../utils'; import EventTag from '../EventTag'; -import ProblemDetails from './ProblemDetails'; +import { ProblemDetails } from './ProblemDetails'; import { AckProblemData } from '../AckModal'; import { GFHeartIcon, FAIcon } from '../../../components'; import { ProblemsPanelOptions, GFTimeRange, RTCell, TriggerSeverity, RTResized } from '../../types';