Files
redash/client/app/pages/dashboards/hooks/useDashboard.js
Vladislav Denisov 4353a82c7a 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.
2025-12-12 11:59:05 +09:00

271 lines
9.4 KiB
JavaScript

import { useState, useEffect, useMemo, useCallback, useRef } from "react";
import { isEmpty, includes, compact, map, has, pick, keys, extend, every, get } from "lodash";
import notification from "@/services/notification";
import location from "@/services/location";
import url from "@/services/url";
import { Dashboard, collectDashboardFilters } from "@/services/dashboard";
import { currentUser } from "@/services/auth";
import recordEvent from "@/services/recordEvent";
import { QueryResultError } from "@/services/query";
import AddWidgetDialog from "@/components/dashboards/AddWidgetDialog";
import TextboxDialog from "@/components/dashboards/TextboxDialog";
import PermissionsEditorDialog from "@/components/PermissionsEditorDialog";
import { editableMappingsToParameterMappings, synchronizeWidgetTitles } from "@/components/ParameterMappingInput";
import ShareDashboardDialog from "../components/ShareDashboardDialog";
import useFullscreenHandler from "../../../lib/hooks/useFullscreenHandler";
import useRefreshRateHandler from "./useRefreshRateHandler";
import useEditModeHandler from "./useEditModeHandler";
import useDuplicateDashboard from "./useDuplicateDashboard";
import { policy } from "@/services/policy";
export { DashboardStatusEnum } from "./useEditModeHandler";
function getAffectedWidgets(widgets, updatedParameters = []) {
return !isEmpty(updatedParameters)
? widgets.filter((widget) =>
Object.values(widget.getParameterMappings())
.filter(({ type }) => type === "dashboard-level")
.some(({ mapTo }) =>
includes(
updatedParameters.map((p) => p.name),
mapTo
)
)
)
: widgets;
}
function useDashboard(dashboardData) {
const [dashboard, setDashboard] = useState(dashboardData);
const [filters, setFilters] = useState([]);
const [refreshing, setRefreshing] = useState(false);
const [gridDisabled, setGridDisabled] = useState(false);
const globalParameters = useMemo(() => dashboard.getParametersDefs(), [dashboard]);
const canEditDashboard = !dashboard.is_archived && policy.canEdit(dashboard);
const isDashboardOwnerOrAdmin = useMemo(
() =>
!dashboard.is_archived &&
has(dashboard, "user.id") &&
(currentUser.id === dashboard.user.id || currentUser.isAdmin),
[dashboard]
);
const hasOnlySafeQueries = useMemo(
() => every(dashboard.widgets, (w) => (w.getQuery() ? w.getQuery().is_safe : true)),
[dashboard]
);
const [isDuplicating, duplicateDashboard] = useDuplicateDashboard(dashboard);
const managePermissions = useCallback(() => {
const aclUrl = `api/dashboards/${dashboard.id}/acl`;
PermissionsEditorDialog.showModal({
aclUrl,
context: "dashboard",
author: dashboard.user,
});
}, [dashboard]);
const updateDashboard = useCallback(
(data, includeVersion = true) => {
setDashboard((currentDashboard) => extend({}, currentDashboard, data));
data = { ...data, id: dashboard.id };
if (includeVersion) {
data = { ...data, version: dashboard.version };
}
return Dashboard.save(data)
.then((updatedDashboard) => {
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
if (has(data, "name")) {
location.setPath(url.parse(updatedDashboard.url).pathname, true);
}
})
.catch((error) => {
const status = get(error, "response.status");
if (status === 403) {
notification.error("Dashboard update failed", "Permission Denied.");
} else if (status === 409) {
notification.error(
"It seems like the dashboard has been modified by another user. ",
"Please copy/backup your changes and reload this page.",
{ duration: null }
);
}
});
},
[dashboard]
);
const togglePublished = useCallback(() => {
recordEvent("toggle_published", "dashboard", dashboard.id);
updateDashboard({ is_draft: !dashboard.is_draft }, false);
}, [dashboard, updateDashboard]);
const loadWidget = useCallback((widget, forceRefresh = false) => {
widget.getParametersDefs(); // Force widget to read parameters values from URL
setDashboard((currentDashboard) => extend({}, currentDashboard));
return widget
.load(forceRefresh)
.catch((error) => {
// QueryResultErrors are expected
if (error instanceof QueryResultError) {
return;
}
return Promise.reject(error);
})
.finally(() => setDashboard((currentDashboard) => extend({}, currentDashboard)));
}, []);
const refreshWidget = useCallback((widget) => loadWidget(widget, true), [loadWidget]);
const removeWidget = useCallback((widgetId) => {
setDashboard((currentDashboard) =>
extend({}, currentDashboard, {
widgets: currentDashboard.widgets.filter((widget) => widget.id !== undefined && widget.id !== widgetId),
})
);
}, []);
const dashboardRef = useRef();
dashboardRef.current = dashboard;
const loadDashboard = useCallback(
(forceRefresh = false, updatedParameters = []) => {
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
const loadWidgetPromises = compact(
affectedWidgets.map((widget) => loadWidget(widget, forceRefresh).catch((error) => error))
);
return Promise.all(loadWidgetPromises).then(() => {
const queryResults = compact(map(dashboardRef.current.widgets, (widget) => widget.getQueryResult()));
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
setFilters(updatedFilters);
});
},
[loadWidget]
);
const refreshDashboard = useCallback(
(updatedParameters) => {
if (!refreshing) {
setRefreshing(true);
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
}
},
[refreshing, loadDashboard]
);
const saveDashboardParameters = useCallback(() => {
const currentDashboard = dashboardRef.current;
return updateDashboard({
options: {
...currentDashboard.options,
parameters: map(globalParameters, (p) => p.toSaveableObject()),
},
}).catch((error) => {
console.error("Failed to persist parameter values:", error);
notification.error("Parameter values could not be saved. Your changes may not be persisted.");
throw error;
});
}, [globalParameters, updateDashboard]);
const archiveDashboard = useCallback(() => {
recordEvent("archive", "dashboard", dashboard.id);
Dashboard.delete(dashboard).then((updatedDashboard) =>
setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
);
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
const showShareDashboardDialog = useCallback(() => {
const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
ShareDashboardDialog.showModal({
dashboard,
hasOnlySafeQueries,
})
.onClose(handleDialogClose)
.onDismiss(handleDialogClose);
}, [dashboard, hasOnlySafeQueries]);
const showAddTextboxDialog = useCallback(() => {
TextboxDialog.showModal({
isNew: true,
}).onClose((text) =>
dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
);
}, [dashboard]);
const showAddWidgetDialog = useCallback(() => {
AddWidgetDialog.showModal({
dashboard,
}).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);
const [fullscreen, toggleFullscreen] = useFullscreenHandler();
const editModeHandler = useEditModeHandler(!gridDisabled && canEditDashboard, dashboard.widgets);
useEffect(() => {
setDashboard(dashboardData);
loadDashboard();
}, [dashboardData]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
document.title = dashboard.name;
}, [dashboard.name]);
// reload dashboard when filter option changes
useEffect(() => {
loadDashboard();
}, [dashboard.dashboard_filters_enabled]); // eslint-disable-line react-hooks/exhaustive-deps
return {
dashboard,
globalParameters,
refreshing,
filters,
setFilters,
loadDashboard,
refreshDashboard,
updateDashboard,
togglePublished,
archiveDashboard,
loadWidget,
refreshWidget,
removeWidget,
canEditDashboard,
isDashboardOwnerOrAdmin,
refreshRate,
setRefreshRate,
disableRefreshRate,
...editModeHandler,
saveDashboardParameters,
gridDisabled,
setGridDisabled,
fullscreen,
toggleFullscreen,
showShareDashboardDialog,
showAddTextboxDialog,
showAddWidgetDialog,
managePermissions,
isDuplicating,
duplicateDashboard,
};
}
export default useDashboard;