Refactor dialog wrapper component (#4594)

* Dialog wrapper: stop using promises to handle results - replace with callbacks

* Dialog wrapper: handle async operation on close
This commit is contained in:
Levko Kravets 2020-03-10 22:22:42 +02:00 committed by GitHub
parent e552effd96
commit db71ff399c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 677 additions and 749 deletions

View File

@ -28,7 +28,7 @@ function onSearch(q) {
function DesktopNavbar() {
const showCreateDashboardDialog = useCallback(() => {
CreateDashboardDialog.showModal().result.catch(() => {}); // ignore dismiss
CreateDashboardDialog.showModal();
}, []);
return (

View File

@ -14,9 +14,10 @@ import ReactDOM from "react-dom";
{
showModal: (dialogProps) => object({
result: Promise,
close: (result) => void,
dismiss: (reason) => void,
onClose: (handler) => this,
onDismiss: (handler) => this,
}),
Component: React.Component, // wrapped dialog component
}
@ -28,15 +29,20 @@ import ReactDOM from "react-dom";
const dialog = SomeWrappedDialog.showModal({ greeting: 'Hello' })
To get result of modal, use `result` property:
To get result of modal, use `onClose`/`onDismiss` setters:
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.
dialog
.onClose(result => { ... }) // pressed OK button or used `close` method
.onDismiss(result => { ... }) // pressed Cancel button or used `dismiss` method
If `onClose`/`onDismiss` returns a promise - dialog wrapper will stop handling further close/dismiss
requests and will show loader on a corresponding button until that promise is fulfilled (either resolved or
rejected). If that promise will be rejected - dialog close/dismiss will be abandoned. Use promise returned
from `close`/`dismiss` methods to handle errors (if needed).
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. `update` methods allows to pass new properties
to dialog.
will be passed to a corresponding handler. Both methods will return the promise returned from `onClose` and
`onDismiss` callbacks. `update` method allows to pass new properties to dialog.
Creating a dialog
@ -88,21 +94,6 @@ import ReactDOM from "react-dom";
<Modal {...dialog.props} onOk={() => this.customOkHandler()}>
);
}
Settings
========
You can setup this wrapper to use custom `Promise` library (for example, Bluebird):
import DialogWrapper from 'path/to/DialogWrapper';
import Promise from 'bluebird';
DialogWrapper.Promise = Promise;
It could be useful to avoid `unhandledrejection` exception that would fire with native Promises,
or when some custom Promise library is used in application.
*/
export const DialogPropType = PropTypes.shape({
@ -116,17 +107,12 @@ export const DialogPropType = PropTypes.shape({
dismiss: PropTypes.func.isRequired,
});
// default export of module
const DialogWrapper = {
Promise,
DialogPropType,
wrap() {},
};
function openDialog(DialogComponent, props) {
const dialog = {
props: {
visible: true,
okButtonProps: {},
cancelButtonProps: {},
onOk: () => {},
onCancel: () => {},
afterClose: () => {},
@ -135,9 +121,11 @@ function openDialog(DialogComponent, props) {
dismiss: () => {},
};
const dialogResult = {
resolve: () => {},
reject: () => {},
let pendingCloseTask = null;
const handlers = {
onClose: () => {},
onDismiss: () => {},
};
const container = document.createElement("div");
@ -155,16 +143,43 @@ function openDialog(DialogComponent, props) {
}, 10);
}
function closeDialog(result) {
dialogResult.resolve(result);
dialog.props.visible = false;
function processDialogClose(result, setAdditionalDialogProps) {
dialog.props.okButtonProps = { disabled: true };
dialog.props.cancelButtonProps = { disabled: true };
setAdditionalDialogProps();
render();
return Promise.resolve(result)
.then(() => {
dialog.props.visible = false;
})
.finally(() => {
dialog.props.okButtonProps = {};
dialog.props.cancelButtonProps = {};
render();
});
}
function dismissDialog(reason) {
dialogResult.reject(reason);
dialog.props.visible = false;
render();
function closeDialog(result) {
if (!pendingCloseTask) {
pendingCloseTask = processDialogClose(handlers.onClose(result), () => {
dialog.props.okButtonProps.loading = true;
}).finally(() => {
pendingCloseTask = null;
});
}
return pendingCloseTask;
}
function dismissDialog(result) {
if (!pendingCloseTask) {
pendingCloseTask = processDialogClose(handlers.onDismiss(result), () => {
dialog.props.cancelButtonProps.loading = true;
}).finally(() => {
pendingCloseTask = null;
});
}
return pendingCloseTask;
}
dialog.props.onOk = closeDialog;
@ -180,20 +195,22 @@ function openDialog(DialogComponent, props) {
props = { ...props, ...newProps };
render();
},
result: new DialogWrapper.Promise((resolve, reject) => {
dialogResult.resolve = resolve;
dialogResult.reject = reject;
}),
onClose: handler => {
if (isFunction(handler)) {
handlers.onClose = handler;
}
return result;
},
onDismiss: handler => {
if (isFunction(handler)) {
handlers.onDismiss = handler;
}
return result;
},
};
render(); // show it only when all structures initialized to avoid unnecessary re-rendering
// Some known libraries support
// Bluebird: http://bluebirdjs.com/docs/api/suppressunhandledrejections.html
if (isFunction(result.result.suppressUnhandledRejections)) {
result.result.suppressUnhandledRejections();
}
return result;
}
@ -204,6 +221,7 @@ export function wrap(DialogComponent) {
};
}
DialogWrapper.wrap = wrap;
export default DialogWrapper;
export default {
DialogPropType,
wrap,
};

View File

@ -106,16 +106,14 @@ export default class Parameters extends React.Component {
showParameterSettings = (parameter, index) => {
const { onParametersEdit } = this.props;
EditParameterSettingsDialog.showModal({ parameter })
.result.then(updated => {
this.setState(({ parameters }) => {
const updatedParameter = extend(parameter, updated);
parameters[index] = createParameter(updatedParameter, updatedParameter.parentQueryId);
onParametersEdit();
return { parameters };
});
})
.catch(() => {}); // ignore dismiss
EditParameterSettingsDialog.showModal({ parameter }).onClose(updated => {
this.setState(({ parameters }) => {
const updatedParameter = extend(parameter, updated);
parameters[index] = createParameter(updatedParameter, updatedParameter.parentQueryId);
onParametersEdit();
return { parameters };
});
});
};
renderParameter(param, index) {

View File

@ -1,5 +1,5 @@
import { filter, debounce, find, isEmpty, size } from "lodash";
import React from "react";
import { filter, find, isEmpty, size } from "lodash";
import React, { useState, useCallback, useEffect } from "react";
import PropTypes from "prop-types";
import classNames from "classnames";
import Modal from "antd/lib/modal";
@ -8,191 +8,185 @@ import List from "antd/lib/list";
import Button from "antd/lib/button";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import BigMessage from "@/components/BigMessage";
import LoadingState from "@/components/items-list/components/LoadingState";
import notification from "@/services/notification";
import useSearchResults from "@/lib/hooks/useSearchResults";
class SelectItemsDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
dialogTitle: PropTypes.string,
inputPlaceholder: PropTypes.string,
selectedItemsTitle: PropTypes.string,
searchItems: PropTypes.func.isRequired, // (searchTerm: string): Promise<Items[]> if `searchTerm === ''` load all
itemKey: PropTypes.func, // (item) => string|number - return key of item (by default `id`)
// left list
// (item, { isSelected }) => {
// content: node, // item contents
// className: string = '', // additional class for item wrapper
// isDisabled: bool = false, // is item clickable or disabled
// }
renderItem: PropTypes.func,
// right list; args/results save as for `renderItem`. if not specified - `renderItem` will be used
renderStagedItem: PropTypes.func,
save: PropTypes.func, // (selectedItems[]) => Promise<any>
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
extraFooterContent: PropTypes.node,
showCount: PropTypes.bool,
};
function ItemsList({ items, renderItem, onItemClick }) {
const renderListItem = useCallback(
item => {
const { content, className, isDisabled } = renderItem(item);
return (
<List.Item
className={classNames("p-l-10", "p-r-10", { clickable: !isDisabled, disabled: isDisabled }, className)}
onClick={isDisabled ? null : () => onItemClick(item)}>
{content}
</List.Item>
);
},
[renderItem, onItemClick]
);
static defaultProps = {
dialogTitle: "Add Items",
inputPlaceholder: "Search...",
selectedItemsTitle: "Selected items",
itemKey: item => item.id,
renderItem: () => "",
renderStagedItem: null, // hidden by default
save: items => items,
width: "80%",
extraFooterContent: null,
showCount: false,
};
state = {
searchTerm: "",
loading: false,
items: [],
selected: [],
saveInProgress: false,
};
// eslint-disable-next-line react/sort-comp
loadItems = (searchTerm = "") => {
this.setState({ searchTerm, loading: true }, () => {
this.props
.searchItems(searchTerm)
.then(items => {
// If another search appeared while loading data - just reject this set
if (this.state.searchTerm === searchTerm) {
this.setState({ items, loading: false });
}
})
.catch(() => {
if (this.state.searchTerm === searchTerm) {
this.setState({ items: [], loading: false });
}
});
});
};
search = debounce(this.loadItems, 200);
componentDidMount() {
this.loadItems();
}
isSelected(item) {
const key = this.props.itemKey(item);
return !!find(this.state.selected, i => this.props.itemKey(i) === key);
}
toggleItem(item) {
if (this.isSelected(item)) {
const key = this.props.itemKey(item);
this.setState(({ selected }) => ({
selected: filter(selected, i => this.props.itemKey(i) !== key),
}));
} else {
this.setState(({ selected }) => ({
selected: [...selected, item],
}));
}
}
save() {
this.setState({ saveInProgress: true }, () => {
const selectedItems = this.state.selected;
Promise.resolve(this.props.save(selectedItems))
.then(() => {
this.props.dialog.close(selectedItems);
})
.catch(() => {
this.setState({ saveInProgress: false });
notification.error("Failed to save some of selected items.");
});
});
}
renderItem(item, isStagedList) {
const { renderItem, renderStagedItem } = this.props;
const isSelected = this.isSelected(item);
const render = isStagedList ? renderStagedItem : renderItem;
const { content, className, isDisabled } = render(item, { isSelected });
return (
<List.Item
className={classNames("p-l-10", "p-r-10", { clickable: !isDisabled, disabled: isDisabled }, className)}
onClick={isDisabled ? null : () => this.toggleItem(item)}>
{content}
</List.Item>
);
}
render() {
const { dialog, dialogTitle, inputPlaceholder } = this.props;
const { selectedItemsTitle, renderStagedItem, width, showCount } = this.props;
const { loading, saveInProgress, items, selected } = this.state;
const hasResults = items.length > 0;
return (
<Modal
{...dialog.props}
className="select-items-dialog"
width={width}
title={dialogTitle}
footer={
<div className="d-flex align-items-center">
<span className="flex-fill m-r-5" style={{ textAlign: "left", color: "rgba(0, 0, 0, 0.5)" }}>
{this.props.extraFooterContent}
</span>
<Button onClick={dialog.dismiss}>Cancel</Button>
<Button
onClick={() => this.save()}
loading={saveInProgress}
disabled={selected.length === 0}
type="primary">
Save
{showCount && !isEmpty(selected) ? ` (${size(selected)})` : null}
</Button>
</div>
}>
<div className="d-flex align-items-center m-b-10">
<div className="flex-fill">
<Input.Search
defaultValue={this.state.searchTerm}
onChange={event => this.search(event.target.value)}
placeholder={inputPlaceholder}
autoFocus
/>
</div>
{renderStagedItem && (
<div className="w-50 m-l-20">
<h5 className="m-0">{selectedItemsTitle}</h5>
</div>
)}
</div>
<div className="d-flex align-items-stretch" style={{ minHeight: "30vh", maxHeight: "50vh" }}>
<div className="flex-fill scrollbox">
{loading && <LoadingState className="" />}
{!loading && !hasResults && (
<BigMessage icon="fa-search" message="No items match your search." className="" />
)}
{!loading && hasResults && (
<List size="small" dataSource={items} renderItem={item => this.renderItem(item, false)} />
)}
</div>
{renderStagedItem && (
<div className="w-50 m-l-20 scrollbox">
{selected.length > 0 && (
<List size="small" dataSource={selected} renderItem={item => this.renderItem(item, true)} />
)}
</div>
)}
</div>
</Modal>
);
}
return <List size="small" dataSource={items} renderItem={renderListItem} />;
}
ItemsList.propTypes = {
items: PropTypes.array,
renderItem: PropTypes.func,
onItemClick: PropTypes.func,
};
ItemsList.defaultProps = {
items: [],
renderItem: () => {},
onItemClick: () => {},
};
function SelectItemsDialog({
dialog,
dialogTitle,
inputPlaceholder,
itemKey,
renderItem,
renderStagedItem,
searchItems,
selectedItemsTitle,
width,
showCount,
extraFooterContent,
}) {
const [selectedItems, setSelectedItems] = useState([]);
const [search, items, isLoading] = useSearchResults(searchItems, { initialResults: [] });
const hasResults = items.length > 0;
useEffect(() => {
search();
}, [search]);
const isItemSelected = useCallback(
item => {
const key = itemKey(item);
return !!find(selectedItems, i => itemKey(i) === key);
},
[selectedItems, itemKey]
);
const toggleItem = useCallback(
item => {
if (isItemSelected(item)) {
const key = itemKey(item);
setSelectedItems(filter(selectedItems, i => itemKey(i) !== key));
} else {
setSelectedItems([...selectedItems, item]);
}
},
[selectedItems, itemKey, isItemSelected]
);
const save = useCallback(() => {
dialog.close(selectedItems).catch(error => {
if (error) {
notification.error("Failed to save some of selected items.");
}
});
}, [dialog, selectedItems]);
return (
<Modal
{...dialog.props}
className="select-items-dialog"
width={width}
title={dialogTitle}
footer={
<div className="d-flex align-items-center">
<span className="flex-fill m-r-5" style={{ textAlign: "left", color: "rgba(0, 0, 0, 0.5)" }}>
{extraFooterContent}
</span>
<Button {...dialog.props.cancelButtonProps} onClick={dialog.dismiss}>
Cancel
</Button>
<Button
{...dialog.props.okButtonProps}
onClick={save}
disabled={selectedItems.length === 0 || dialog.props.okButtonProps.disabled}
type="primary">
Save
{showCount && !isEmpty(selectedItems) ? ` (${size(selectedItems)})` : null}
</Button>
</div>
}>
<div className="d-flex align-items-center m-b-10">
<div className="flex-fill">
<Input.Search onChange={event => search(event.target.value)} placeholder={inputPlaceholder} autoFocus />
</div>
{renderStagedItem && (
<div className="w-50 m-l-20">
<h5 className="m-0">{selectedItemsTitle}</h5>
</div>
)}
</div>
<div className="d-flex align-items-stretch" style={{ minHeight: "30vh", maxHeight: "50vh" }}>
<div className="flex-fill scrollbox">
{isLoading && <LoadingState className="" />}
{!isLoading && !hasResults && (
<BigMessage icon="fa-search" message="No items match your search." className="" />
)}
{!isLoading && hasResults && (
<ItemsList
items={items}
renderItem={item => renderItem(item, { isSelected: isItemSelected(item) })}
onItemClick={toggleItem}
/>
)}
</div>
{renderStagedItem && (
<div className="w-50 m-l-20 scrollbox">
{selectedItems.length > 0 && (
<ItemsList
items={selectedItems}
renderItem={item => renderStagedItem(item, { isSelected: true })}
onItemClick={toggleItem}
/>
)}
</div>
)}
</div>
</Modal>
);
}
SelectItemsDialog.propTypes = {
dialog: DialogPropType.isRequired,
dialogTitle: PropTypes.string,
inputPlaceholder: PropTypes.string,
selectedItemsTitle: PropTypes.string,
searchItems: PropTypes.func.isRequired, // (searchTerm: string): Promise<Items[]> if `searchTerm === ''` load all
itemKey: PropTypes.func, // (item) => string|number - return key of item (by default `id`)
// left list
// (item, { isSelected }) => {
// content: node, // item contents
// className: string = '', // additional class for item wrapper
// isDisabled: bool = false, // is item clickable or disabled
// }
renderItem: PropTypes.func,
// right list; args/results save as for `renderItem`. if not specified - `renderItem` will be used
renderStagedItem: PropTypes.func,
width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
extraFooterContent: PropTypes.node,
showCount: PropTypes.bool,
};
SelectItemsDialog.defaultProps = {
dialogTitle: "Add Items",
inputPlaceholder: "Search...",
selectedItemsTitle: "Selected items",
itemKey: item => item.id,
renderItem: () => "",
renderStagedItem: null, // hidden by default
width: "80%",
extraFooterContent: null,
showCount: false,
};
export default wrapDialog(SelectItemsDialog);

View File

@ -1,164 +1,156 @@
import { each, values, map, includes, first } from "lodash";
import React from "react";
import { map, includes, groupBy, first, find } from "lodash";
import React, { useState, useMemo, useCallback } from "react";
import PropTypes from "prop-types";
import Select from "antd/lib/select";
import Modal from "antd/lib/modal";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import { MappingType, ParameterMappingListInput } from "@/components/ParameterMappingInput";
import QuerySelector from "@/components/QuerySelector";
import notification from "@/services/notification";
import { Query } from "@/services/query";
const { Option, OptGroup } = Select;
function VisualizationSelect({ query, visualization, onChange }) {
const visualizationGroups = useMemo(() => {
return query ? groupBy(query.visualizations, "type") : {};
}, [query]);
class AddWidgetDialog extends React.Component {
static propTypes = {
dashboard: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
dialog: DialogPropType.isRequired,
onConfirm: PropTypes.func.isRequired,
};
const handleChange = useCallback(
visualizationId => {
const selectedVisualization = query ? find(query.visualizations, { id: visualizationId }) : null;
onChange(selectedVisualization || null);
},
[query, onChange]
);
state = {
saveInProgress: false,
selectedQuery: null,
selectedVis: null,
parameterMappings: [],
};
selectQuery(selectedQuery) {
// Clear previously selected query (if any)
this.setState({
selectedQuery: null,
selectedVis: null,
parameterMappings: [],
});
if (selectedQuery) {
Query.get({ id: selectedQuery.id }).then(query => {
if (query) {
const existingParamNames = map(this.props.dashboard.getParametersDefs(), param => param.name);
this.setState({
selectedQuery: query,
parameterMappings: map(query.getParametersDefs(), param => ({
name: param.name,
type: includes(existingParamNames, param.name)
? MappingType.DashboardMapToExisting
: MappingType.DashboardAddNew,
mapTo: param.name,
value: param.normalizedValue,
title: "",
param,
})),
});
if (query.visualizations.length) {
this.setState({ selectedVis: query.visualizations[0] });
}
}
});
}
if (!query) {
return null;
}
selectVisualization(query, visualizationId) {
each(query.visualizations, visualization => {
if (visualization.id === visualizationId) {
this.setState({ selectedVis: visualization });
return false;
}
});
}
saveWidget() {
const { selectedVis, parameterMappings } = this.state;
this.setState({ saveInProgress: true });
this.props
.onConfirm(selectedVis, parameterMappings)
.then(() => {
this.props.dialog.close();
})
.catch(() => {
notification.error("Widget could not be added");
})
.finally(() => {
this.setState({ saveInProgress: false });
});
}
updateParamMappings(parameterMappings) {
this.setState({ parameterMappings });
}
renderVisualizationInput() {
let visualizationGroups = {};
if (this.state.selectedQuery) {
each(this.state.selectedQuery.visualizations, vis => {
visualizationGroups[vis.type] = visualizationGroups[vis.type] || [];
visualizationGroups[vis.type].push(vis);
});
}
visualizationGroups = values(visualizationGroups);
return (
<div>
<div className="form-group">
<label htmlFor="choose-visualization">Choose Visualization</label>
<Select
id="choose-visualization"
className="w-100"
defaultValue={first(this.state.selectedQuery.visualizations).id}
onChange={visualizationId => this.selectVisualization(this.state.selectedQuery, visualizationId)}>
{visualizationGroups.map(visualizations => (
<OptGroup label={visualizations[0].type} key={visualizations[0].type}>
{visualizations.map(visualization => (
<Option value={visualization.id} key={visualization.id}>
{visualization.name}
</Option>
))}
</OptGroup>
))}
</Select>
</div>
return (
<div>
<div className="form-group">
<label htmlFor="choose-visualization">Choose Visualization</label>
<Select
id="choose-visualization"
className="w-100"
value={visualization ? visualization.id : undefined}
onChange={handleChange}>
{map(visualizationGroups, (visualizations, groupKey) => (
<Select.OptGroup key={groupKey} label={groupKey}>
{map(visualizations, visualization => (
<Select.Option key={`${visualization.id}`} value={visualization.id}>
{visualization.name}
</Select.Option>
))}
</Select.OptGroup>
))}
</Select>
</div>
);
}
render() {
const existingParams = this.props.dashboard.getParametersDefs();
const { dialog } = this.props;
return (
<Modal
{...dialog.props}
title="Add Widget"
onOk={() => this.saveWidget()}
okButtonProps={{
loading: this.state.saveInProgress,
disabled: !this.state.selectedQuery,
}}
okText="Add to Dashboard"
width={700}>
<div data-test="AddWidgetDialog">
<QuerySelector onChange={query => this.selectQuery(query)} />
{this.state.selectedQuery && this.renderVisualizationInput()}
{this.state.parameterMappings.length > 0 && [
<label key="parameters-title" htmlFor="parameter-mappings">
Parameters
</label>,
<ParameterMappingListInput
key="parameters-list"
id="parameter-mappings"
mappings={this.state.parameterMappings}
existingParams={existingParams}
onChange={mappings => this.updateParamMappings(mappings)}
/>,
]}
</div>
</Modal>
);
}
</div>
);
}
VisualizationSelect.propTypes = {
query: PropTypes.object,
visualization: PropTypes.object,
onChange: PropTypes.func,
};
VisualizationSelect.defaultProps = {
query: null,
visualization: null,
onChange: () => {},
};
function AddWidgetDialog({ dialog, dashboard }) {
const [selectedQuery, setSelectedQuery] = useState(null);
const [selectedVisualization, setSelectedVisualization] = useState(null);
const [parameterMappings, setParameterMappings] = useState([]);
const selectQuery = useCallback(
queryId => {
// Clear previously selected query (if any)
setSelectedQuery(null);
setSelectedVisualization(null);
setParameterMappings([]);
if (queryId) {
Query.get({ id: queryId }).then(query => {
if (query) {
const existingParamNames = map(dashboard.getParametersDefs(), param => param.name);
setSelectedQuery(query);
setParameterMappings(
map(query.getParametersDefs(), param => ({
name: param.name,
type: includes(existingParamNames, param.name)
? MappingType.DashboardMapToExisting
: MappingType.DashboardAddNew,
mapTo: param.name,
value: param.normalizedValue,
title: "",
param,
}))
);
if (query.visualizations.length > 0) {
setSelectedVisualization(first(query.visualizations));
}
}
});
}
},
[dashboard]
);
const saveWidget = useCallback(() => {
dialog.close({ visualization: selectedVisualization, parameterMappings }).catch(() => {
notification.error("Widget could not be added");
});
}, [dialog, selectedVisualization, parameterMappings]);
const existingParams = dashboard.getParametersDefs();
return (
<Modal
{...dialog.props}
title="Add Widget"
onOk={saveWidget}
okButtonProps={{
...dialog.props.okButtonProps,
disabled: !selectedQuery || dialog.props.okButtonProps.disabled,
}}
okText="Add to Dashboard"
width={700}>
<div data-test="AddWidgetDialog">
<QuerySelector onChange={query => selectQuery(query ? query.id : null)} />
{selectedQuery && (
<VisualizationSelect
query={selectedQuery}
visualization={selectedVisualization}
onChange={setSelectedVisualization}
/>
)}
{parameterMappings.length > 0 && [
<label key="parameters-title" htmlFor="parameter-mappings">
Parameters
</label>,
<ParameterMappingListInput
key="parameters-list"
id="parameter-mappings"
mappings={parameterMappings}
existingParams={existingParams}
onChange={setParameterMappings}
/>,
]}
</div>
</Modal>
);
}
AddWidgetDialog.propTypes = {
dialog: DialogPropType.isRequired,
dashboard: PropTypes.object.isRequired,
};
export default wrapDialog(AddWidgetDialog);

View File

@ -1,7 +1,8 @@
import { toString } from "lodash";
import { markdown } from "markdown";
import { debounce } from "lodash";
import React from "react";
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import { useDebouncedCallback } from "use-debounce";
import Modal from "antd/lib/modal";
import Input from "antd/lib/input";
import Tooltip from "antd/lib/tooltip";
@ -12,98 +13,78 @@ import notification from "@/services/notification";
import "./TextboxDialog.less";
class TextboxDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
onConfirm: PropTypes.func.isRequired,
text: PropTypes.string,
};
function TextboxDialog({ dialog, isNew, ...props }) {
const [text, setText] = useState(toString(props.text));
const [preview, setPreview] = useState(null);
static defaultProps = {
text: "",
};
useEffect(() => {
setText(props.text);
setPreview(markdown.toHTML(props.text));
}, [props.text]);
updatePreview = debounce(() => {
const text = this.state.text;
this.setState({
preview: markdown.toHTML(text),
const [updatePreview] = useDebouncedCallback(() => {
setPreview(markdown.toHTML(text));
}, 200);
const handleInputChange = useCallback(
event => {
setText(event.target.value);
updatePreview();
},
[updatePreview]
);
const saveWidget = useCallback(() => {
dialog.close(text).catch(() => {
notification.error(isNew ? "Widget could not be added" : "Widget could not be saved");
});
}, 100);
}, [dialog, isNew, text]);
constructor(props) {
super(props);
const { text } = props;
this.state = {
saveInProgress: false,
text,
preview: markdown.toHTML(text),
};
}
onTextChanged = event => {
this.setState({ text: event.target.value });
this.updatePreview();
};
saveWidget() {
this.setState({ saveInProgress: true });
this.props
.onConfirm(this.state.text)
.then(() => {
this.props.dialog.close();
})
.catch(() => {
notification.error("Widget could not be added");
})
.finally(() => {
this.setState({ saveInProgress: false });
});
}
render() {
const { dialog } = this.props;
const isNew = !this.props.text;
return (
<Modal
{...dialog.props}
title={isNew ? "Add Textbox" : "Edit Textbox"}
onOk={() => this.saveWidget()}
okButtonProps={{
loading: this.state.saveInProgress,
disabled: !this.state.text,
}}
okText={isNew ? "Add to Dashboard" : "Save"}
width={500}
wrapProps={{ "data-test": "TextboxDialog" }}>
<div className="textbox-dialog">
<Input.TextArea
className="resize-vertical"
rows="5"
value={this.state.text}
onChange={this.onTextChanged}
autoFocus
placeholder="This is where you write some text"
/>
<small>
Supports basic{" "}
<a target="_blank" rel="noopener noreferrer" href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">
<Tooltip title="Markdown guide opens in new window">Markdown</Tooltip>
</a>
.
</small>
{this.state.text && (
<React.Fragment>
<Divider dashed />
<strong className="preview-title">Preview:</strong>
<HtmlContent className="preview markdown">{this.state.preview}</HtmlContent>
</React.Fragment>
)}
</div>
</Modal>
);
}
return (
<Modal
{...dialog.props}
title={isNew ? "Add Textbox" : "Edit Textbox"}
onOk={saveWidget}
okText={isNew ? "Add to Dashboard" : "Save"}
width={500}
wrapProps={{ "data-test": "TextboxDialog" }}>
<div className="textbox-dialog">
<Input.TextArea
className="resize-vertical"
rows="5"
value={text}
onChange={handleInputChange}
autoFocus
placeholder="This is where you write some text"
/>
<small>
Supports basic{" "}
<a target="_blank" rel="noopener noreferrer" href="https://www.markdownguide.org/cheat-sheet/#basic-syntax">
<Tooltip title="Markdown guide opens in new window">Markdown</Tooltip>
</a>
.
</small>
{text && (
<React.Fragment>
<Divider dashed />
<strong className="preview-title">Preview:</strong>
<HtmlContent className="preview markdown">{preview}</HtmlContent>
</React.Fragment>
)}
</div>
</Modal>
);
}
TextboxDialog.propTypes = {
dialog: DialogPropType.isRequired,
isNew: PropTypes.bool,
text: PropTypes.string,
};
TextboxDialog.defaultProps = {
isNew: false,
text: "",
};
export default wrapDialog(TextboxDialog);

View File

@ -13,12 +13,11 @@ function TextboxWidget(props) {
const editTextBox = () => {
TextboxDialog.showModal({
text: widget.text,
onConfirm: newText => {
widget.text = newText;
setText(newText);
return widget.save();
},
}).result.catch(() => {}); // ignore dismiss
}).onClose(newText => {
widget.text = newText;
setText(newText);
return widget.save();
});
};
const TextboxMenuOptions = [

View File

@ -215,7 +215,7 @@ class VisualizationWidget extends React.Component {
}
expandWidget = () => {
ExpandedWidgetDialog.showModal({ widget: this.props.widget }).result.catch(() => {}); // ignore dismiss
ExpandedWidgetDialog.showModal({ widget: this.props.widget });
};
editParameterMappings = () => {
@ -223,16 +223,14 @@ class VisualizationWidget extends React.Component {
EditParameterMappingsDialog.showModal({
dashboard,
widget,
})
.result.then(valuesChanged => {
// refresh widget if any parameter value has been updated
if (valuesChanged) {
onRefresh();
}
onParameterMappingsChange();
this.setState({ localParameters: widget.getLocalParameters() });
})
.catch(() => {}); // ignore dismiss
}).onClose(valuesChanged => {
// refresh widget if any parameter value has been updated
if (valuesChanged) {
onRefresh();
}
onParameterMappingsChange();
this.setState({ localParameters: widget.getLocalParameters() });
});
};
renderVisualization() {

View File

@ -65,7 +65,7 @@ function EmptyState({
};
const showCreateDashboardDialog = useCallback(() => {
CreateDashboardDialog.showModal().result.catch(() => {}); // ignore dismiss
CreateDashboardDialog.showModal();
}, []);
// Show if `onboardingMode=false` or any requested step not completed

View File

@ -1,94 +1,74 @@
import React from "react";
import { isNil, get } from "lodash";
import React, { useCallback } from "react";
import PropTypes from "prop-types";
import { get, isNil } from "lodash";
import Button from "antd/lib/button";
import Modal from "antd/lib/modal";
import DynamicForm from "@/components/dynamic-form/DynamicForm";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
class QuerySnippetDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
querySnippet: PropTypes.object, // eslint-disable-line react/forbid-prop-types
readOnly: PropTypes.bool,
onSubmit: PropTypes.func.isRequired,
};
function QuerySnippetDialog({ querySnippet, dialog, readOnly }) {
const handleSubmit = useCallback(
(values, successCallback, errorCallback) => {
const querySnippetId = get(querySnippet, "id");
static defaultProps = {
querySnippet: null,
readOnly: false,
};
if (isNil(values.description)) {
values.description = "";
}
constructor(props) {
super(props);
this.state = { saving: false };
}
dialog
.close(querySnippetId ? { id: querySnippetId, ...values } : values)
.then(() => successCallback("Saved."))
.catch(() => errorCallback("Failed saving snippet."));
},
[dialog, querySnippet]
);
handleSubmit = (values, successCallback, errorCallback) => {
const { querySnippet, dialog, onSubmit } = this.props;
const querySnippetId = get(querySnippet, "id");
const isEditing = !!get(querySnippet, "id");
if (isNil(values.description)) {
values.description = "";
}
const formFields = [
{ name: "trigger", title: "Trigger", type: "text", required: true, autoFocus: !isEditing },
{ name: "description", title: "Description", type: "text" },
{ name: "snippet", title: "Snippet", type: "ace", required: true },
].map(field => ({ ...field, readOnly, initialValue: get(querySnippet, field.name, "") }));
this.setState({ saving: true });
onSubmit(querySnippetId ? { id: querySnippetId, ...values } : values)
.then(() => {
dialog.close();
successCallback("Saved.");
})
.catch(() => {
this.setState({ saving: false });
errorCallback("Failed saving snippet.");
});
};
render() {
const { saving } = this.state;
const { querySnippet, dialog, readOnly } = this.props;
const isEditing = !!get(querySnippet, "id");
const formFields = [
{ name: "trigger", title: "Trigger", type: "text", required: true, autoFocus: !isEditing },
{ name: "description", title: "Description", type: "text" },
{ name: "snippet", title: "Snippet", type: "ace", required: true },
].map(field => ({ ...field, readOnly, initialValue: get(querySnippet, field.name, "") }));
return (
<Modal
{...dialog.props}
title={isEditing ? querySnippet.trigger : "Create Query Snippet"}
footer={[
<Button key="cancel" onClick={dialog.dismiss}>
{readOnly ? "Close" : "Cancel"}
</Button>,
!readOnly && (
<Button
key="submit"
htmlType="submit"
loading={saving}
disabled={readOnly}
type="primary"
form="querySnippetForm"
data-test="SaveQuerySnippetButton">
{isEditing ? "Save" : "Create"}
</Button>
),
]}
wrapProps={{
"data-test": "QuerySnippetDialog",
}}>
<DynamicForm
id="querySnippetForm"
fields={formFields}
onSubmit={this.handleSubmit}
hideSubmitButton
feedbackIcons
/>
</Modal>
);
}
return (
<Modal
{...dialog.props}
title={isEditing ? querySnippet.trigger : "Create Query Snippet"}
footer={[
<Button key="cancel" {...dialog.props.cancelButtonProps} onClick={dialog.dismiss}>
{readOnly ? "Close" : "Cancel"}
</Button>,
!readOnly && (
<Button
key="submit"
{...dialog.props.okButtonProps}
disabled={readOnly || dialog.props.okButtonProps.disabled}
htmlType="submit"
type="primary"
form="querySnippetForm"
data-test="SaveQuerySnippetButton">
{isEditing ? "Save" : "Create"}
</Button>
),
]}
wrapProps={{
"data-test": "QuerySnippetDialog",
}}>
<DynamicForm id="querySnippetForm" fields={formFields} onSubmit={handleSubmit} hideSubmitButton feedbackIcons />
</Modal>
);
}
QuerySnippetDialog.propTypes = {
dialog: DialogPropType.isRequired,
querySnippet: PropTypes.object,
readOnly: PropTypes.bool,
};
QuerySnippetDialog.defaultProps = {
querySnippet: null,
readOnly: false,
};
export default wrapDialog(QuerySnippetDialog);

View File

@ -26,9 +26,7 @@ export class TagsControl extends React.Component {
};
editTags = (tags, getAvailableTags) => {
EditTagsDialog.showModal({ tags, getAvailableTags })
.result.then(this.props.onEdit)
.catch(() => {}); // ignore dismiss
EditTagsDialog.showModal({ tags, getAvailableTags }).onClose(this.props.onEdit);
};
renderEditButton() {

View File

@ -1,63 +1,46 @@
import React from "react";
import PropTypes from "prop-types";
import React, { useState, useEffect, useMemo, useRef, useCallback } from "react";
import Modal from "antd/lib/modal";
import Alert from "antd/lib/alert";
import DynamicForm from "@/components/dynamic-form/DynamicForm";
import { wrap as wrapDialog, DialogPropType } from "@/components/DialogWrapper";
import recordEvent from "@/services/recordEvent";
class CreateUserDialog extends React.Component {
static propTypes = {
dialog: DialogPropType.isRequired,
onCreate: PropTypes.func.isRequired,
};
function CreateUserDialog({ dialog }) {
const [error, setError] = useState(null);
const formRef = useRef();
constructor(props) {
super(props);
this.state = { savingUser: false, errorMessage: null };
this.form = React.createRef();
}
componentDidMount() {
useEffect(() => {
recordEvent("view", "page", "users/new");
}
}, []);
createUser = () => {
this.form.current.validateFieldsAndScroll((err, values) => {
if (!err) {
this.setState({ savingUser: true });
this.props
.onCreate(values)
.then(() => {
this.props.dialog.close();
})
.catch(error => {
// TODO: Is raw error message correct here?
this.setState({ savingUser: false, errorMessage: error.message });
});
}
});
};
const createUser = useCallback(() => {
if (formRef.current) {
formRef.current.validateFieldsAndScroll((err, values) => {
if (!err) {
dialog.close(values).catch(setError);
}
});
}
}, [dialog]);
render() {
const { savingUser, errorMessage } = this.state;
const formFields = [
{ name: "name", title: "Name", type: "text", autoFocus: true },
{ name: "email", title: "Email", type: "email" },
].map(field => ({ required: true, props: { onPressEnter: this.createUser }, ...field }));
const formFields = useMemo(() => {
const common = { required: true, props: { onPressEnter: createUser } };
return [
{ ...common, name: "name", title: "Name", type: "text", autoFocus: true },
{ ...common, name: "email", title: "Email", type: "email" },
];
}, [createUser]);
return (
<Modal
{...this.props.dialog.props}
title="Create a New User"
okText="Create"
okButtonProps={{ loading: savingUser }}
onOk={() => this.createUser()}>
<DynamicForm fields={formFields} ref={this.form} hideSubmitButton />
{errorMessage && <Alert message={errorMessage} type="error" showIcon />}
</Modal>
);
}
return (
<Modal {...dialog.props} title="Create a New User" okText="Create" onOk={createUser}>
<DynamicForm fields={formFields} ref={formRef} hideSubmitButton />
{error && <Alert message={error.message} type="error" showIcon />}
</Modal>
);
}
CreateUserDialog.propTypes = {
dialog: DialogPropType.isRequired,
};
export default wrapDialog(CreateUserDialog);

View File

@ -42,7 +42,7 @@ export default class UserEdit extends React.Component {
}
changePassword = () => {
ChangePasswordDialog.showModal({ user: this.props.user }).result.catch(() => {}); // ignore dismiss
ChangePasswordDialog.showModal({ user: this.props.user });
};
sendPasswordReset = () => {

View File

@ -11,7 +11,7 @@ export default function useSearchResults(fetch, { initialResults = null, debounc
setIsLoading(true);
currentSearchTerm.current = searchTerm;
fetch(searchTerm)
.catch(() => null)
.catch(() => initialResults)
.then(data => {
if (searchTerm === currentSearchTerm.current && !isDestroyed.current) {
setResult(data);

View File

@ -1,6 +1,6 @@
import { without, find, includes, map, toLower } from "lodash";
import React from "react";
import PropTypes from "prop-types";
import { without, find, isEmpty, includes, map } from "lodash";
import SelectItemsDialog from "@/components/SelectItemsDialog";
import { Destination as DestinationType, UserProfile as UserType } from "@/components/proptypes";
@ -98,9 +98,8 @@ export default class AlertDestinations extends React.Component {
dialogTitle: "Add Existing Alert Destinations",
inputPlaceholder: "Search destinations...",
searchItems: searchTerm => {
searchTerm = searchTerm.toLowerCase();
const filtered = dests.filter(d => isEmpty(searchTerm) || includes(d.name.toLowerCase(), searchTerm));
return Promise.resolve(filtered);
searchTerm = toLower(searchTerm);
return Promise.resolve(dests.filter(d => includes(toLower(d.name), searchTerm)));
},
renderItem: (item, { isSelected }) => {
const alreadyInGroup = !!find(subs, s => s.destination.id === item.id);
@ -117,17 +116,17 @@ export default class AlertDestinations extends React.Component {
className: isSelected || alreadyInGroup ? "selected" : "",
};
},
save: items => {
const promises = map(items, item => this.subscribe(item));
return Promise.all(promises)
.then(() => {
notification.success("Subscribed.");
})
.catch(() => {
notification.error("Failed saving subscription.");
});
},
}).result.catch(() => {}); // ignore dismiss
}).onClose(items => {
const promises = map(items, item => this.subscribe(item));
return Promise.all(promises)
.then(() => {
notification.success("Subscribed.");
})
.catch(() => {
notification.error("Failed saving subscription.");
return Promise.reject(null); // keep dialog visible but suppress its default error message
});
});
};
onUserEmailToggle = sub => {

View File

@ -56,7 +56,7 @@ function useDashboard(dashboardData) {
aclUrl,
context: "dashboard",
author: dashboard.user,
}).result.catch(() => {}); // ignore dismiss
});
}, [dashboard]);
const updateDashboard = useCallback(
@ -142,40 +142,42 @@ function useDashboard(dashboardData) {
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
const showShareDashboardDialog = useCallback(() => {
const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard));
ShareDashboardDialog.showModal({
dashboard,
hasOnlySafeQueries,
})
.result.catch(() => {}) // ignore dismiss
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
.onClose(handleDialogClose)
.onDismiss(handleDialogClose);
}, [dashboard, hasOnlySafeQueries]);
const showAddTextboxDialog = useCallback(() => {
TextboxDialog.showModal({
dashboard,
onConfirm: text =>
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard))),
}).result.catch(() => {}); // ignore dismiss
isNew: true,
}).onClose(text =>
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)))
);
}, [dashboard]);
const showAddWidgetDialog = useCallback(() => {
AddWidgetDialog.showModal({
dashboard,
onConfirm: (visualization, parameterMappings) =>
dashboard
.addWidget(visualization, {
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
})
.then(widget => {
const widgetsToSave = [
widget,
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
];
return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
setDashboard(currentDashboard => extend({}, currentDashboard))
);
}),
}).result.catch(() => {}); // ignore dismiss
}).onClose(({ visualization, parameterMappings }) =>
dashboard
.addWidget(visualization, {
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
})
.then(widget => {
const widgetsToSave = [
widget,
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
];
return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
setDashboard(currentDashboard => extend({}, currentDashboard))
);
})
);
}, [dashboard]);
const [refreshRate, setRefreshRate, disableRefreshRate] = useRefreshRateHandler(refreshDashboard);

View File

@ -84,16 +84,16 @@ class DataSourcesList extends React.Component {
onCreate: this.createDataSource,
});
this.newDataSourceDialog.result
.then((result = {}) => {
this.newDataSourceDialog
.onClose((result = {}) => {
this.newDataSourceDialog = null;
if (result.success) {
navigateTo(`data_sources/${result.data.id}`);
}
})
.catch(() => {
navigateTo("data_sources", true);
.onDismiss(() => {
this.newDataSourceDialog = null;
navigateTo("data_sources", true);
});
};

View File

@ -76,12 +76,12 @@ class DestinationsList extends React.Component {
imageFolder: IMG_ROOT,
onCreate: this.createDestination,
})
.result.then((result = {}) => {
.onClose((result = {}) => {
if (result.success) {
navigateTo(`destinations/${result.data.id}`);
}
})
.catch(() => {
.onDismiss(() => {
navigateTo("destinations", true);
});
};

View File

@ -1,4 +1,4 @@
import { filter, map, includes } from "lodash";
import { filter, map, includes, toLower } from "lodash";
import React from "react";
import Button from "antd/lib/button";
import Dropdown from "antd/lib/dropdown";
@ -140,8 +140,8 @@ class GroupDataSources extends React.Component {
inputPlaceholder: "Search data sources...",
selectedItemsTitle: "New Data Sources",
searchItems: searchTerm => {
searchTerm = searchTerm.toLowerCase();
return allDataSources.then(items => filter(items, ds => ds.name.toLowerCase().includes(searchTerm)));
searchTerm = toLower(searchTerm);
return allDataSources.then(items => filter(items, ds => includes(toLower(ds.name), searchTerm)));
},
renderItem: (item, { isSelected }) => {
const alreadyInGroup = includes(alreadyAddedDataSources, item.id);
@ -162,15 +162,10 @@ class GroupDataSources extends React.Component {
</DataSourcePreviewCard>
),
}),
save: items => {
const promises = map(items, ds => Group.addDataSource({ id: this.groupId }, { data_source_id: ds.id }));
return Promise.all(promises);
},
})
.result.catch(() => {}) // ignore dismiss
.finally(() => {
this.props.controller.update();
});
}).onClose(items => {
const promises = map(items, ds => Group.addDataSource({ id: this.groupId }, { data_source_id: ds.id }));
return Promise.all(promises).then(() => this.props.controller.update());
});
};
render() {

View File

@ -125,15 +125,10 @@ class GroupMembers extends React.Component {
</UserPreviewCard>
),
}),
save: items => {
const promises = map(items, u => Group.addMember({ id: this.groupId }, { user_id: u.id }));
return Promise.all(promises);
},
})
.result.catch(() => {}) // ignore dismiss
.finally(() => {
this.props.controller.update();
});
}).onClose(items => {
const promises = map(items, u => Group.addMember({ id: this.groupId }, { user_id: u.id }));
return Promise.all(promises).then(() => this.props.controller.update());
});
};
render() {

View File

@ -73,11 +73,9 @@ class GroupsList extends React.Component {
];
createGroup = () => {
CreateGroupDialog.showModal()
.result.then(group => {
Group.create(group).then(newGroup => navigateTo(`groups/${newGroup.id}`));
})
.catch(() => {}); // ignore dismiss
CreateGroupDialog.showModal().onClose(group =>
Group.create(group).then(newGroup => navigateTo(`groups/${newGroup.id}`))
);
};
onGroupDeleted = () => {

View File

@ -11,7 +11,6 @@ import Resizable from "@/components/Resizable";
import Parameters from "@/components/Parameters";
import EditInPlace from "@/components/EditInPlace";
import QueryEditor from "@/components/queries/QueryEditor";
import { Query } from "@/services/query";
import recordEvent from "@/services/recordEvent";
import { ExecutionStatus } from "@/services/query-result";

View File

@ -15,12 +15,10 @@ export default function useAddNewParameterDialog(query, onParameterAdded) {
value: null,
},
existingParams: map(query.getParameters().get(), p => p.name),
})
.result.then(param => {
const newQuery = query.clone();
param = newQuery.getParameters().add(param);
onParameterAddedRef.current(newQuery, param);
})
.catch(() => {}); // ignore dismiss
}).onClose(param => {
const newQuery = query.clone();
param = newQuery.getParameters().add(param);
onParameterAddedRef.current(newQuery, param);
});
}, [query]);
}

View File

@ -6,7 +6,7 @@ export default function useAddToDashboardDialog(query) {
return useCallback(
visualizationId => {
const visualization = find(query.visualizations, { id: visualizationId });
AddToDashboardDialog.showModal({ visualization }).result.catch(() => {}); // ignore dismiss
AddToDashboardDialog.showModal({ visualization });
},
[query.visualizations]
);

View File

@ -7,10 +7,8 @@ export default function useApiKeyDialog(query, onChange) {
onChangeRef.current = isFunction(onChange) ? onChange : () => {};
return useCallback(() => {
ApiKeyDialog.showModal({ query })
.result.then(updatedQuery => {
onChangeRef.current(updatedQuery);
})
.catch(() => {}); // ignore dismiss
ApiKeyDialog.showModal({ query }).onClose(updatedQuery => {
onChangeRef.current(updatedQuery);
});
}, [query]);
}

View File

@ -25,11 +25,9 @@ export default function useEditScheduleDialog(query, onChange) {
ScheduleDialog.showModal({
schedule: query.schedule,
refreshOptions,
})
.result.then(schedule => {
recordEvent("edit_schedule", "query", query.id);
updateQuery({ schedule });
})
.catch(() => {}); // ignore dismiss
}).onClose(schedule => {
recordEvent("edit_schedule", "query", query.id);
updateQuery({ schedule });
});
}, [query.id, query.schedule, queryFlags.canEdit, queryFlags.canSchedule, updateQuery]);
}

View File

@ -13,15 +13,13 @@ export default function useEditVisualizationDialog(query, queryResult, onChange)
query,
visualization,
queryResult,
})
.result.then(updatedVisualization => {
const filteredVisualizations = filter(query.visualizations, v => v.id !== updatedVisualization.id);
onChangeRef.current(
extend(query.clone(), { visualizations: [...filteredVisualizations, updatedVisualization] }),
updatedVisualization
);
})
.catch(() => {}); // ignore dismiss
}).onClose(updatedVisualization => {
const filteredVisualizations = filter(query.visualizations, v => v.id !== updatedVisualization.id);
onChangeRef.current(
extend(query.clone(), { visualizations: [...filteredVisualizations, updatedVisualization] }),
updatedVisualization
);
});
},
[query, queryResult]
);

View File

@ -6,7 +6,7 @@ export default function useEmbedDialog(query) {
return useCallback(
(unusedQuery, visualizationId) => {
const visualization = find(query.visualizations, { id: visualizationId });
EmbedQueryDialog.showModal({ query, visualization }).result.catch(() => {}); // ignore dismiss
EmbedQueryDialog.showModal({ query, visualization });
},
[query]
);

View File

@ -7,6 +7,6 @@ export default function usePermissionsEditorDialog(query) {
aclUrl: `api/queries/${query.id}/acl`,
context: "query",
author: query.user,
}).result.catch(() => {}); // ignore dismiss
});
}, [query.id, query.user]);
}

View File

@ -121,16 +121,18 @@ class QuerySnippetsList extends React.Component {
showSnippetDialog = (querySnippet = null) => {
const canSave = !querySnippet || canEditQuerySnippet(querySnippet);
navigateTo("query_snippets/" + get(querySnippet, "id", "new"), true);
const goToSnippetsList = () => navigateTo("query_snippets", true);
QuerySnippetDialog.showModal({
querySnippet,
onSubmit: this.saveQuerySnippet,
readOnly: !canSave,
})
.result.then(() => this.props.controller.update())
.catch(() => {}) // ignore dismiss
.finally(() => {
navigateTo("query_snippets", true);
});
.onClose(querySnippet =>
this.saveQuerySnippet(querySnippet).then(() => {
this.props.controller.update();
goToSnippetsList();
})
)
.onDismiss(goToSnippetsList);
};
render() {

View File

@ -164,14 +164,19 @@ class UsersList extends React.Component {
showCreateUserDialog = () => {
if (policy.isCreateUserEnabled()) {
CreateUserDialog.showModal({ onCreate: this.createUser })
.result.then(() => this.props.controller.update())
.catch(() => {}) // ignore dismiss
.finally(() => {
if (this.props.controller.params.isNewUserPage) {
navigateTo("users");
}
});
const goToUsersList = () => {
if (this.props.controller.params.isNewUserPage) {
navigateTo("users");
}
};
CreateUserDialog.showModal()
.onClose(values =>
this.createUser(values).then(() => {
this.props.controller.update();
goToUsersList();
})
)
.onDismiss(goToUsersList);
}
};