Skip to content

Commit 4353a82

Browse files
authored
Persist updated values and apply saved dashboard parameters (#7570)
Add support for saving dashboard parameters after clicking the Apply button. Parameters are applied in the following order: URL, dashboard parameters, query parameters. Persist the queued values only when “Done Editing” is clicked, keeping Query and Dashboard editors aligned.
1 parent 761eb0b commit 4353a82

File tree

5 files changed

+106
-74
lines changed

5 files changed

+106
-74
lines changed

client/app/pages/dashboards/DashboardPage.jsx

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ function DashboardSettings({ dashboardConfiguration }) {
3131
<Checkbox
3232
checked={!!dashboard.dashboard_filters_enabled}
3333
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
34-
data-test="DashboardFiltersCheckbox">
34+
data-test="DashboardFiltersCheckbox"
35+
>
3536
Use Dashboard Level Filters
3637
</Checkbox>
3738
</div>
@@ -90,9 +91,9 @@ function DashboardComponent(props) {
9091

9192
const [pageContainer, setPageContainer] = useState(null);
9293
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
93-
const onParametersEdit = parameters => {
94+
const onParametersEdit = (parameters) => {
9495
const paramOrder = map(parameters, "name");
95-
updateDashboard({ options: { globalParamOrder: paramOrder } });
96+
updateDashboard({ options: { ...dashboard.options, globalParamOrder: paramOrder } });
9697
};
9798

9899
useEffect(() => {
@@ -175,7 +176,7 @@ function DashboardPage({ dashboardSlug, dashboardId, onError }) {
175176

176177
useEffect(() => {
177178
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
178-
.then(dashboardData => {
179+
.then((dashboardData) => {
179180
recordEvent("view", "dashboard", dashboardData.id);
180181
setDashboard(dashboardData);
181182

@@ -207,14 +208,14 @@ routes.register(
207208
"Dashboards.LegacyViewOrEdit",
208209
routeWithUserSession({
209210
path: "/dashboard/:dashboardSlug",
210-
render: pageProps => <DashboardPage {...pageProps} />,
211+
render: (pageProps) => <DashboardPage {...pageProps} />,
211212
})
212213
);
213214

214215
routes.register(
215216
"Dashboards.ViewOrEdit",
216217
routeWithUserSession({
217218
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
218-
render: pageProps => <DashboardPage {...pageProps} />,
219+
render: (pageProps) => <DashboardPage {...pageProps} />,
219220
})
220221
);

client/app/pages/dashboards/components/DashboardHeader.jsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { DashboardStatusEnum } from "../hooks/useDashboard";
2222
import "./DashboardHeader.less";
2323

2424
function getDashboardTags() {
25-
return getTags("api/dashboards/tags").then(tags => map(tags, t => t.name));
25+
return getTags("api/dashboards/tags").then((tags) => map(tags, (t) => t.name));
2626
}
2727

2828
function buttonType(value) {
@@ -38,7 +38,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
3838
<h3>
3939
<EditInPlace
4040
isEditable={editingLayout}
41-
onDone={name => updateDashboard({ name })}
41+
onDone={(name) => updateDashboard({ name })}
4242
value={dashboard.name}
4343
ignoreBlanks
4444
/>
@@ -53,7 +53,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
5353
isArchived={dashboard.is_archived}
5454
canEdit={canEditDashboard}
5555
getAvailableTags={getDashboardTags}
56-
onEdit={tags => updateDashboard({ tags })}
56+
onEdit={(tags) => updateDashboard({ tags })}
5757
/>
5858
</div>
5959
);
@@ -89,14 +89,15 @@ function RefreshButton({ dashboardConfiguration }) {
8989
placement="bottomRight"
9090
overlay={
9191
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
92-
{refreshRateOptions.map(option => (
92+
{refreshRateOptions.map((option) => (
9393
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
9494
{durationHumanize(option)}
9595
</Menu.Item>
9696
))}
9797
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
9898
</Menu>
99-
}>
99+
}
100+
>
100101
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
101102
<i className="fa fa-angle-down" aria-hidden="true" />
102103
<span className="sr-only">Split button!</span>
@@ -166,7 +167,8 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
166167
<PlainButton onClick={archive}>Archive</PlainButton>
167168
</Menu.Item>
168169
</Menu>
169-
}>
170+
}
171+
>
170172
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
171173
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
172174
</Button>
@@ -216,7 +218,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
216218
type={buttonType(fullscreen)}
217219
className="icon-button m-l-5"
218220
onClick={toggleFullscreen}
219-
aria-label="Toggle fullscreen display">
221+
aria-label="Toggle fullscreen display"
222+
>
220223
<i className="zmdi zmdi-fullscreen" aria-hidden="true" />
221224
</Button>
222225
</Tooltip>
@@ -229,7 +232,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
229232
type={buttonType(dashboard.publicAccessEnabled)}
230233
onClick={showShareDashboardDialog}
231234
data-test="OpenShareForm"
232-
aria-label="Share">
235+
aria-label="Share"
236+
>
233237
<i className="zmdi zmdi-share" aria-hidden="true" />
234238
</Button>
235239
</Tooltip>
@@ -252,7 +256,11 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
252256
doneBtnClickedWhileSaving,
253257
dashboardStatus,
254258
retrySaveDashboardLayout,
259+
saveDashboardParameters,
255260
} = dashboardConfiguration;
261+
const handleDoneEditing = () => {
262+
saveDashboardParameters().then(() => setEditingLayout(false));
263+
};
256264
let status;
257265
if (dashboardStatus === DashboardStatusEnum.SAVED) {
258266
status = <span className="save-status">Saved</span>;
@@ -277,7 +285,7 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
277285
Retry
278286
</Button>
279287
) : (
280-
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={() => setEditingLayout(false)}>
288+
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={handleDoneEditing}>
281289
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
282290
</Button>
283291
)}

client/app/pages/dashboards/hooks/useDashboard.js

Lines changed: 41 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ export { DashboardStatusEnum } from "./useEditModeHandler";
2222

2323
function getAffectedWidgets(widgets, updatedParameters = []) {
2424
return !isEmpty(updatedParameters)
25-
? widgets.filter(widget =>
25+
? widgets.filter((widget) =>
2626
Object.values(widget.getParameterMappings())
2727
.filter(({ type }) => type === "dashboard-level")
2828
.some(({ mapTo }) =>
2929
includes(
30-
updatedParameters.map(p => p.name),
30+
updatedParameters.map((p) => p.name),
3131
mapTo
3232
)
3333
)
@@ -50,7 +50,7 @@ function useDashboard(dashboardData) {
5050
[dashboard]
5151
);
5252
const hasOnlySafeQueries = useMemo(
53-
() => every(dashboard.widgets, w => (w.getQuery() ? w.getQuery().is_safe : true)),
53+
() => every(dashboard.widgets, (w) => (w.getQuery() ? w.getQuery().is_safe : true)),
5454
[dashboard]
5555
);
5656

@@ -67,19 +67,19 @@ function useDashboard(dashboardData) {
6767

6868
const updateDashboard = useCallback(
6969
(data, includeVersion = true) => {
70-
setDashboard(currentDashboard => extend({}, currentDashboard, data));
70+
setDashboard((currentDashboard) => extend({}, currentDashboard, data));
7171
data = { ...data, id: dashboard.id };
7272
if (includeVersion) {
7373
data = { ...data, version: dashboard.version };
7474
}
7575
return Dashboard.save(data)
76-
.then(updatedDashboard => {
77-
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
76+
.then((updatedDashboard) => {
77+
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
7878
if (has(data, "name")) {
7979
location.setPath(url.parse(updatedDashboard.url).pathname, true);
8080
}
8181
})
82-
.catch(error => {
82+
.catch((error) => {
8383
const status = get(error, "response.status");
8484
if (status === 403) {
8585
notification.error("Dashboard update failed", "Permission Denied.");
@@ -102,25 +102,25 @@ function useDashboard(dashboardData) {
102102

103103
const loadWidget = useCallback((widget, forceRefresh = false) => {
104104
widget.getParametersDefs(); // Force widget to read parameters values from URL
105-
setDashboard(currentDashboard => extend({}, currentDashboard));
105+
setDashboard((currentDashboard) => extend({}, currentDashboard));
106106
return widget
107107
.load(forceRefresh)
108-
.catch(error => {
108+
.catch((error) => {
109109
// QueryResultErrors are expected
110110
if (error instanceof QueryResultError) {
111111
return;
112112
}
113113
return Promise.reject(error);
114114
})
115-
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard)));
115+
.finally(() => setDashboard((currentDashboard) => extend({}, currentDashboard)));
116116
}, []);
117117

118-
const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]);
118+
const refreshWidget = useCallback((widget) => loadWidget(widget, true), [loadWidget]);
119119

120-
const removeWidget = useCallback(widgetId => {
121-
setDashboard(currentDashboard =>
120+
const removeWidget = useCallback((widgetId) => {
121+
setDashboard((currentDashboard) =>
122122
extend({}, currentDashboard, {
123-
widgets: currentDashboard.widgets.filter(widget => widget.id !== undefined && widget.id !== widgetId),
123+
widgets: currentDashboard.widgets.filter((widget) => widget.id !== undefined && widget.id !== widgetId),
124124
})
125125
);
126126
}, []);
@@ -132,11 +132,11 @@ function useDashboard(dashboardData) {
132132
(forceRefresh = false, updatedParameters = []) => {
133133
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
134134
const loadWidgetPromises = compact(
135-
affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error))
135+
affectedWidgets.map((widget) => loadWidget(widget, forceRefresh).catch((error) => error))
136136
);
137137

138138
return Promise.all(loadWidgetPromises).then(() => {
139-
const queryResults = compact(map(dashboardRef.current.widgets, widget => widget.getQueryResult()));
139+
const queryResults = compact(map(dashboardRef.current.widgets, (widget) => widget.getQueryResult()));
140140
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
141141
setFilters(updatedFilters);
142142
});
@@ -145,7 +145,7 @@ function useDashboard(dashboardData) {
145145
);
146146

147147
const refreshDashboard = useCallback(
148-
updatedParameters => {
148+
(updatedParameters) => {
149149
if (!refreshing) {
150150
setRefreshing(true);
151151
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
@@ -154,15 +154,30 @@ function useDashboard(dashboardData) {
154154
[refreshing, loadDashboard]
155155
);
156156

157+
const saveDashboardParameters = useCallback(() => {
158+
const currentDashboard = dashboardRef.current;
159+
160+
return updateDashboard({
161+
options: {
162+
...currentDashboard.options,
163+
parameters: map(globalParameters, (p) => p.toSaveableObject()),
164+
},
165+
}).catch((error) => {
166+
console.error("Failed to persist parameter values:", error);
167+
notification.error("Parameter values could not be saved. Your changes may not be persisted.");
168+
throw error;
169+
});
170+
}, [globalParameters, updateDashboard]);
171+
157172
const archiveDashboard = useCallback(() => {
158173
recordEvent("archive", "dashboard", dashboard.id);
159-
Dashboard.delete(dashboard).then(updatedDashboard =>
160-
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
174+
Dashboard.delete(dashboard).then((updatedDashboard) =>
175+
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
161176
);
162177
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
163178

164179
const showShareDashboardDialog = useCallback(() => {
165-
const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard));
180+
const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
166181

167182
ShareDashboardDialog.showModal({
168183
dashboard,
@@ -175,8 +190,8 @@ function useDashboard(dashboardData) {
175190
const showAddTextboxDialog = useCallback(() => {
176191
TextboxDialog.showModal({
177192
isNew: true,
178-
}).onClose(text =>
179-
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)))
193+
}).onClose((text) =>
194+
dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
180195
);
181196
}, [dashboard]);
182197

@@ -188,13 +203,13 @@ function useDashboard(dashboardData) {
188203
.addWidget(visualization, {
189204
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
190205
})
191-
.then(widget => {
206+
.then((widget) => {
192207
const widgetsToSave = [
193208
widget,
194209
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
195210
];
196-
return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
197-
setDashboard(currentDashboard => extend({}, currentDashboard))
211+
return Promise.all(widgetsToSave.map((w) => w.save())).then(() =>
212+
setDashboard((currentDashboard) => extend({}, currentDashboard))
198213
);
199214
})
200215
);
@@ -238,6 +253,7 @@ function useDashboard(dashboardData) {
238253
setRefreshRate,
239254
disableRefreshRate,
240255
...editModeHandler,
256+
saveDashboardParameters,
241257
gridDisabled,
242258
setGridDisabled,
243259
fullscreen,

0 commit comments

Comments
 (0)