Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refine dialog wrapper and use it for all existing dialogs #3407

Merged
merged 8 commits into from
Feb 8, 2019
Merged
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions client/app/components/DialogWrapper.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React from 'react';
import PropTypes from 'prop-types';
import ReactDOM from 'react-dom';

/**
Wrapper for dialogs based on Ant's <Modal> component.
Using wrapped dialogs
=====================
Wrapped component is an object with two fields:
{
showModal: (dialogProps) => object({
result: Promise,
close: (result) => void,
dismiss: (reason) => void,
}),
Component: React.Component, // wrapped dialog component
}
To open dialog, use `showModal` method; optionally you can pass additional properties that
will be expanded on wrapped component:
const dialog = SomeWrappedDialog.showModal()
const dialog = SomeWrappedDialog.showModal({ greeting: 'Hello' })
To get result of modal, use `result` property:
dialog.result
.then(...) // pressed OK button or used `close` method; resolved value is a result of dialog
.catch(...) // pressed Cancel button or used `dismiss` method; optional argument is a rejection reason.
Also, dialog has `close` and `dismiss` methods that allows to close dialog by caller. Passed arguments
will be used to resolve/reject `dialog.result` promise.
Creating a dialog
================
1. Add imports:
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
2. define a `dialog` property on your component:
propTypes = {
dialog: DialogPropType.isRequired,
};
`dialog` property is an object:
{
props: object, // properties for <Modal> component;
close: (result) => void, // method to confirm dialog; `result` will be returned to caller
dismiss: (reason) => void, // method to reject dialog; `reason` will be returned to caller
}
3. expand additional properties on <Modal> component:
render() {
const { dialog } = this.props;
return (
<Modal {...dialog.props}>
);
}
4. wrap your component and export it:
export default wrapDialog(YourComponent).
Your component is ready to use. Wrapper will manage <Modal>'s visibility and events.
If you want to override behavior of `onOk`/`onCancel` - don't forget to close dialog:
customOkHandler() {
this.saveData().then(() => {
this.props.dialog.close({ success: true }); // or dismiss();
});
}
render() {
const { dialog } = this.props;
return (
<Modal {...dialog.props} onOk={() => this.customOkHandler()}>
);
}
*/

export const DialogPropType = PropTypes.shape({
props: PropTypes.shape({
visible: PropTypes.bool,
onOk: PropTypes.func,
onCancel: PropTypes.func,
afterClose: PropTypes.func,
}).isRequired,
close: PropTypes.func.isRequired,
dismiss: PropTypes.func.isRequired,
});

function openDialog(DialogComponent, props) {
const dialog = {
props: {
visible: true,
onOk: () => {},
onCancel: () => {},
afterClose: () => {},
},
close: () => {},
dismiss: () => {},
};

const dialogResult = {
resolve: () => {},
reject: () => {},
};

const container = document.createElement('div');
document.body.appendChild(container);

function render() {
ReactDOM.render(<DialogComponent {...props} dialog={dialog} />, container);
}

function destroyDialog() {
// Allow calling chain to roll up, and then destroy component
setTimeout(() => {
ReactDOM.unmountComponentAtNode(container);
document.body.removeChild(container);
}, 10);
}

function closeDialog(result) {
dialogResult.resolve(result);
dialog.props.visible = false;
render();
}

function dismissDialog(reason) {
dialogResult.reject(reason);
dialog.props.visible = false;
render();
}

dialog.props.onOk = closeDialog;
dialog.props.onCancel = dismissDialog;
dialog.props.afterClose = destroyDialog;
dialog.close = closeDialog;
dialog.dismiss = dismissDialog;

const result = {
close: closeDialog,
dismiss: dismissDialog,
result: new Promise((resolve, reject) => {
dialogResult.resolve = resolve;
dialogResult.reject = reject;
}),
};

render(); // show it only when all structures initialized to avoid unnecessary re-rendering

return result;
}

export function wrap(DialogComponent) {
return {
Component: DialogComponent,
showModal: props => openDialog(DialogComponent, props),
};
}
26 changes: 6 additions & 20 deletions client/app/components/dashboards/AddWidgetDialog.jsx
Original file line number Diff line number Diff line change
@@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import Select from 'antd/lib/select';
import Modal from 'antd/lib/modal';
import ModalOpener from '@/hoc/ModalOpener';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import highlight from '@/lib/highlight';
import {
MappingType,
@@ -21,13 +21,7 @@ const { Option, OptGroup } = Select;
class AddWidgetDialog extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onClose: PropTypes.func,
onConfirm: PropTypes.func,
};

static defaultProps = {
onClose: () => {},
onConfirm: () => {},
dialog: DialogPropType.isRequired,
};

constructor(props) {
@@ -40,14 +34,12 @@ class AddWidgetDialog extends React.Component {
searchedQueries: [],
selectedVis: null,
parameterMappings: [],
showModal: false, // show only after recent queries populated to avoid height "jump"
};

// Don't show draft (unpublished) queries in recent queries.
Query.recent().$promise.then((items) => {
this.setState({
recentQueries: items.filter(item => !item.is_draft),
showModal: true,
});
});

@@ -59,10 +51,6 @@ class AddWidgetDialog extends React.Component {
};
}

close = () => {
this.setState({ showModal: false });
}

selectQuery(queryId) {
// Clear previously selected query (if any)
this.setState({
@@ -149,8 +137,7 @@ class AddWidgetDialog extends React.Component {
.save()
.then(() => {
dashboard.widgets.push(widget);
this.props.onConfirm();
this.close();
this.props.dialog.close();
})
.catch(() => {
toastr.error('Widget can not be added');
@@ -295,19 +282,18 @@ class AddWidgetDialog extends React.Component {
this.props.dashboard.getParametersDefs(),
({ name, type }) => ({ name, type }),
);
const { dialog } = this.props;

return (
<Modal
visible={this.state.showModal}
afterClose={this.props.onClose}
{...dialog.props}
title="Add Widget"
onOk={() => this.saveWidget()}
okButtonProps={{
loading: this.state.saveInProgress,
disabled: !this.state.selectedQuery,
}}
okText="Add to Dashboard"
onCancel={this.close}
width={700}
>
{this.renderQueryInput()}
@@ -331,4 +317,4 @@ class AddWidgetDialog extends React.Component {
}
}

export default ModalOpener(AddWidgetDialog);
export default wrapDialog(AddWidgetDialog);
25 changes: 6 additions & 19 deletions client/app/components/dashboards/EditParameterMappingsDialog.jsx
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { map } from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import Modal from 'antd/lib/modal';
import ModalOpener from '@/hoc/ModalOpener';
import { wrap as wrapDialog, DialogPropType } from '@/components/DialogWrapper';
import {
ParameterMappingListInput,
parameterMappingsToEditableMappings,
@@ -13,13 +13,7 @@ class EditParameterMappingsDialog extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
widget: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
onClose: PropTypes.func,
onConfirm: PropTypes.func,
};

static defaultProps = {
onClose: () => {},
onConfirm: () => {},
dialog: DialogPropType.isRequired,
};

constructor(props) {
@@ -31,14 +25,9 @@ class EditParameterMappingsDialog extends React.Component {
props.widget.query.getParametersDefs(),
map(this.props.dashboard.getParametersDefs(), p => p.name),
),
showModal: true,
};
}

close = () => {
this.setState({ showModal: false });
}

saveWidget() {
const toastr = this.props.toastr; // eslint-disable-line react/prop-types
const widget = this.props.widget;
@@ -49,8 +38,7 @@ class EditParameterMappingsDialog extends React.Component {
widget
.save()
.then(() => {
this.props.onConfirm();
this.close();
this.props.dialog.close();
})
.catch(() => {
toastr.error('Widget cannot be updated');
@@ -65,14 +53,13 @@ class EditParameterMappingsDialog extends React.Component {
}

render() {
const { dialog } = this.props;
return (
<Modal
visible={this.state.showModal}
afterClose={this.props.onClose}
{...dialog.props}
title="Parameters"
onOk={() => this.saveWidget()}
okButtonProps={{ loading: this.state.saveInProgress }}
onCancel={this.close}
width={700}
>
{(this.state.parameterMappings.length > 0) && (
@@ -87,4 +74,4 @@ class EditParameterMappingsDialog extends React.Component {
}
}

export default ModalOpener(EditParameterMappingsDialog);
export default wrapDialog(EditParameterMappingsDialog);
7 changes: 4 additions & 3 deletions client/app/components/dashboards/widget.js
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import { filter } from 'lodash';
import template from './widget.html';
import editTextBoxTemplate from './edit-text-box.html';
import widgetDialogTemplate from './widget-dialog.html';
import editParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog';
import EditParameterMappingsDialog from '@/components/dashboards/EditParameterMappingsDialog';
import './widget.less';
import './widget-dialog.less';

@@ -52,7 +52,7 @@ const EditTextBoxComponent = {
},
};

function DashboardWidgetCtrl($location, $uibModal, $window, $rootScope, Events, currentUser) {
function DashboardWidgetCtrl($scope, $location, $uibModal, $window, $rootScope, Events, currentUser) {
this.canViewQuery = currentUser.hasPermission('view_query');

this.editTextBox = () => {
@@ -79,11 +79,12 @@ function DashboardWidgetCtrl($location, $uibModal, $window, $rootScope, Events,
this.hasParameters = () => this.widget.query.getParametersDefs().length > 0;

this.editParameterMappings = () => {
editParameterMappingsDialog.open({
EditParameterMappingsDialog.showModal({
dashboard: this.dashboard,
widget: this.widget,
}).result.then(() => {
this.localParameters = null;
$scope.$applyAsync();
$rootScope.$broadcast('dashboard.update-parameters');
});
};
Loading