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.
This commit is contained in:
Vladislav Denisov
2025-12-12 03:59:05 +01:00
committed by GitHub
parent 761eb0b68b
commit 4353a82c7a
5 changed files with 106 additions and 74 deletions

View File

@@ -31,7 +31,8 @@ function DashboardSettings({ dashboardConfiguration }) {
<Checkbox <Checkbox
checked={!!dashboard.dashboard_filters_enabled} checked={!!dashboard.dashboard_filters_enabled}
onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })} onChange={({ target }) => updateDashboard({ dashboard_filters_enabled: target.checked })}
data-test="DashboardFiltersCheckbox"> data-test="DashboardFiltersCheckbox"
>
Use Dashboard Level Filters Use Dashboard Level Filters
</Checkbox> </Checkbox>
</div> </div>
@@ -90,9 +91,9 @@ function DashboardComponent(props) {
const [pageContainer, setPageContainer] = useState(null); const [pageContainer, setPageContainer] = useState(null);
const [bottomPanelStyles, setBottomPanelStyles] = useState({}); const [bottomPanelStyles, setBottomPanelStyles] = useState({});
const onParametersEdit = parameters => { const onParametersEdit = (parameters) => {
const paramOrder = map(parameters, "name"); const paramOrder = map(parameters, "name");
updateDashboard({ options: { globalParamOrder: paramOrder } }); updateDashboard({ options: { ...dashboard.options, globalParamOrder: paramOrder } });
}; };
useEffect(() => { useEffect(() => {
@@ -175,7 +176,7 @@ function DashboardPage({ dashboardSlug, dashboardId, onError }) {
useEffect(() => { useEffect(() => {
Dashboard.get({ id: dashboardId, slug: dashboardSlug }) Dashboard.get({ id: dashboardId, slug: dashboardSlug })
.then(dashboardData => { .then((dashboardData) => {
recordEvent("view", "dashboard", dashboardData.id); recordEvent("view", "dashboard", dashboardData.id);
setDashboard(dashboardData); setDashboard(dashboardData);
@@ -207,7 +208,7 @@ routes.register(
"Dashboards.LegacyViewOrEdit", "Dashboards.LegacyViewOrEdit",
routeWithUserSession({ routeWithUserSession({
path: "/dashboard/:dashboardSlug", path: "/dashboard/:dashboardSlug",
render: pageProps => <DashboardPage {...pageProps} />, render: (pageProps) => <DashboardPage {...pageProps} />,
}) })
); );
@@ -215,6 +216,6 @@ routes.register(
"Dashboards.ViewOrEdit", "Dashboards.ViewOrEdit",
routeWithUserSession({ routeWithUserSession({
path: "/dashboards/:dashboardId([^-]+)(-.*)?", path: "/dashboards/:dashboardId([^-]+)(-.*)?",
render: pageProps => <DashboardPage {...pageProps} />, render: (pageProps) => <DashboardPage {...pageProps} />,
}) })
); );

View File

@@ -22,7 +22,7 @@ import { DashboardStatusEnum } from "../hooks/useDashboard";
import "./DashboardHeader.less"; import "./DashboardHeader.less";
function getDashboardTags() { function getDashboardTags() {
return getTags("api/dashboards/tags").then(tags => map(tags, t => t.name)); return getTags("api/dashboards/tags").then((tags) => map(tags, (t) => t.name));
} }
function buttonType(value) { function buttonType(value) {
@@ -38,7 +38,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
<h3> <h3>
<EditInPlace <EditInPlace
isEditable={editingLayout} isEditable={editingLayout}
onDone={name => updateDashboard({ name })} onDone={(name) => updateDashboard({ name })}
value={dashboard.name} value={dashboard.name}
ignoreBlanks ignoreBlanks
/> />
@@ -53,7 +53,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
isArchived={dashboard.is_archived} isArchived={dashboard.is_archived}
canEdit={canEditDashboard} canEdit={canEditDashboard}
getAvailableTags={getDashboardTags} getAvailableTags={getDashboardTags}
onEdit={tags => updateDashboard({ tags })} onEdit={(tags) => updateDashboard({ tags })}
/> />
</div> </div>
); );
@@ -89,14 +89,15 @@ function RefreshButton({ dashboardConfiguration }) {
placement="bottomRight" placement="bottomRight"
overlay={ overlay={
<Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}> <Menu onClick={onRefreshRateSelected} selectedKeys={[`${refreshRate}`]}>
{refreshRateOptions.map(option => ( {refreshRateOptions.map((option) => (
<Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}> <Menu.Item key={`${option}`} disabled={!includes(allowedIntervals, option)}>
{durationHumanize(option)} {durationHumanize(option)}
</Menu.Item> </Menu.Item>
))} ))}
{refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>} {refreshRate && <Menu.Item key={null}>Disable auto refresh</Menu.Item>}
</Menu> </Menu>
}> }
>
<Button className="icon-button hidden-xs" type={buttonType(refreshRate)}> <Button className="icon-button hidden-xs" type={buttonType(refreshRate)}>
<i className="fa fa-angle-down" aria-hidden="true" /> <i className="fa fa-angle-down" aria-hidden="true" />
<span className="sr-only">Split button!</span> <span className="sr-only">Split button!</span>
@@ -166,7 +167,8 @@ function DashboardMoreOptionsButton({ dashboardConfiguration }) {
<PlainButton onClick={archive}>Archive</PlainButton> <PlainButton onClick={archive}>Archive</PlainButton>
</Menu.Item> </Menu.Item>
</Menu> </Menu>
}> }
>
<Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions"> <Button className="icon-button m-l-5" data-test="DashboardMoreButton" aria-label="More actions">
<EllipsisOutlinedIcon rotate={90} aria-hidden="true" /> <EllipsisOutlinedIcon rotate={90} aria-hidden="true" />
</Button> </Button>
@@ -216,7 +218,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
type={buttonType(fullscreen)} type={buttonType(fullscreen)}
className="icon-button m-l-5" className="icon-button m-l-5"
onClick={toggleFullscreen} onClick={toggleFullscreen}
aria-label="Toggle fullscreen display"> aria-label="Toggle fullscreen display"
>
<i className="zmdi zmdi-fullscreen" aria-hidden="true" /> <i className="zmdi zmdi-fullscreen" aria-hidden="true" />
</Button> </Button>
</Tooltip> </Tooltip>
@@ -229,7 +232,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
type={buttonType(dashboard.publicAccessEnabled)} type={buttonType(dashboard.publicAccessEnabled)}
onClick={showShareDashboardDialog} onClick={showShareDashboardDialog}
data-test="OpenShareForm" data-test="OpenShareForm"
aria-label="Share"> aria-label="Share"
>
<i className="zmdi zmdi-share" aria-hidden="true" /> <i className="zmdi zmdi-share" aria-hidden="true" />
</Button> </Button>
</Tooltip> </Tooltip>
@@ -252,7 +256,11 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
doneBtnClickedWhileSaving, doneBtnClickedWhileSaving,
dashboardStatus, dashboardStatus,
retrySaveDashboardLayout, retrySaveDashboardLayout,
saveDashboardParameters,
} = dashboardConfiguration; } = dashboardConfiguration;
const handleDoneEditing = () => {
saveDashboardParameters().then(() => setEditingLayout(false));
};
let status; let status;
if (dashboardStatus === DashboardStatusEnum.SAVED) { if (dashboardStatus === DashboardStatusEnum.SAVED) {
status = <span className="save-status">Saved</span>; status = <span className="save-status">Saved</span>;
@@ -277,7 +285,7 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
Retry Retry
</Button> </Button>
) : ( ) : (
<Button loading={doneBtnClickedWhileSaving} type="primary" onClick={() => setEditingLayout(false)}> <Button loading={doneBtnClickedWhileSaving} type="primary" onClick={handleDoneEditing}>
{!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing {!doneBtnClickedWhileSaving && <i className="fa fa-check m-r-5" aria-hidden="true" />} Done Editing
</Button> </Button>
)} )}

View File

@@ -22,12 +22,12 @@ export { DashboardStatusEnum } from "./useEditModeHandler";
function getAffectedWidgets(widgets, updatedParameters = []) { function getAffectedWidgets(widgets, updatedParameters = []) {
return !isEmpty(updatedParameters) return !isEmpty(updatedParameters)
? widgets.filter(widget => ? widgets.filter((widget) =>
Object.values(widget.getParameterMappings()) Object.values(widget.getParameterMappings())
.filter(({ type }) => type === "dashboard-level") .filter(({ type }) => type === "dashboard-level")
.some(({ mapTo }) => .some(({ mapTo }) =>
includes( includes(
updatedParameters.map(p => p.name), updatedParameters.map((p) => p.name),
mapTo mapTo
) )
) )
@@ -50,7 +50,7 @@ function useDashboard(dashboardData) {
[dashboard] [dashboard]
); );
const hasOnlySafeQueries = useMemo( const hasOnlySafeQueries = useMemo(
() => every(dashboard.widgets, w => (w.getQuery() ? w.getQuery().is_safe : true)), () => every(dashboard.widgets, (w) => (w.getQuery() ? w.getQuery().is_safe : true)),
[dashboard] [dashboard]
); );
@@ -67,19 +67,19 @@ function useDashboard(dashboardData) {
const updateDashboard = useCallback( const updateDashboard = useCallback(
(data, includeVersion = true) => { (data, includeVersion = true) => {
setDashboard(currentDashboard => extend({}, currentDashboard, data)); setDashboard((currentDashboard) => extend({}, currentDashboard, data));
data = { ...data, id: dashboard.id }; data = { ...data, id: dashboard.id };
if (includeVersion) { if (includeVersion) {
data = { ...data, version: dashboard.version }; data = { ...data, version: dashboard.version };
} }
return Dashboard.save(data) return Dashboard.save(data)
.then(updatedDashboard => { .then((updatedDashboard) => {
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, keys(data)))); setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
if (has(data, "name")) { if (has(data, "name")) {
location.setPath(url.parse(updatedDashboard.url).pathname, true); location.setPath(url.parse(updatedDashboard.url).pathname, true);
} }
}) })
.catch(error => { .catch((error) => {
const status = get(error, "response.status"); const status = get(error, "response.status");
if (status === 403) { if (status === 403) {
notification.error("Dashboard update failed", "Permission Denied."); notification.error("Dashboard update failed", "Permission Denied.");
@@ -102,25 +102,25 @@ function useDashboard(dashboardData) {
const loadWidget = useCallback((widget, forceRefresh = false) => { const loadWidget = useCallback((widget, forceRefresh = false) => {
widget.getParametersDefs(); // Force widget to read parameters values from URL widget.getParametersDefs(); // Force widget to read parameters values from URL
setDashboard(currentDashboard => extend({}, currentDashboard)); setDashboard((currentDashboard) => extend({}, currentDashboard));
return widget return widget
.load(forceRefresh) .load(forceRefresh)
.catch(error => { .catch((error) => {
// QueryResultErrors are expected // QueryResultErrors are expected
if (error instanceof QueryResultError) { if (error instanceof QueryResultError) {
return; return;
} }
return Promise.reject(error); return Promise.reject(error);
}) })
.finally(() => setDashboard(currentDashboard => extend({}, currentDashboard))); .finally(() => setDashboard((currentDashboard) => extend({}, currentDashboard)));
}, []); }, []);
const refreshWidget = useCallback(widget => loadWidget(widget, true), [loadWidget]); const refreshWidget = useCallback((widget) => loadWidget(widget, true), [loadWidget]);
const removeWidget = useCallback(widgetId => { const removeWidget = useCallback((widgetId) => {
setDashboard(currentDashboard => setDashboard((currentDashboard) =>
extend({}, currentDashboard, { extend({}, currentDashboard, {
widgets: currentDashboard.widgets.filter(widget => widget.id !== undefined && widget.id !== widgetId), widgets: currentDashboard.widgets.filter((widget) => widget.id !== undefined && widget.id !== widgetId),
}) })
); );
}, []); }, []);
@@ -132,11 +132,11 @@ function useDashboard(dashboardData) {
(forceRefresh = false, updatedParameters = []) => { (forceRefresh = false, updatedParameters = []) => {
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters); const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
const loadWidgetPromises = compact( const loadWidgetPromises = compact(
affectedWidgets.map(widget => loadWidget(widget, forceRefresh).catch(error => error)) affectedWidgets.map((widget) => loadWidget(widget, forceRefresh).catch((error) => error))
); );
return Promise.all(loadWidgetPromises).then(() => { return Promise.all(loadWidgetPromises).then(() => {
const queryResults = compact(map(dashboardRef.current.widgets, widget => widget.getQueryResult())); const queryResults = compact(map(dashboardRef.current.widgets, (widget) => widget.getQueryResult()));
const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search); const updatedFilters = collectDashboardFilters(dashboardRef.current, queryResults, location.search);
setFilters(updatedFilters); setFilters(updatedFilters);
}); });
@@ -145,7 +145,7 @@ function useDashboard(dashboardData) {
); );
const refreshDashboard = useCallback( const refreshDashboard = useCallback(
updatedParameters => { (updatedParameters) => {
if (!refreshing) { if (!refreshing) {
setRefreshing(true); setRefreshing(true);
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false)); loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
@@ -154,15 +154,30 @@ function useDashboard(dashboardData) {
[refreshing, loadDashboard] [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(() => { const archiveDashboard = useCallback(() => {
recordEvent("archive", "dashboard", dashboard.id); recordEvent("archive", "dashboard", dashboard.id);
Dashboard.delete(dashboard).then(updatedDashboard => Dashboard.delete(dashboard).then((updatedDashboard) =>
setDashboard(currentDashboard => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"]))) setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, ["is_archived"])))
); );
}, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps }, [dashboard]); // eslint-disable-line react-hooks/exhaustive-deps
const showShareDashboardDialog = useCallback(() => { const showShareDashboardDialog = useCallback(() => {
const handleDialogClose = () => setDashboard(currentDashboard => extend({}, currentDashboard)); const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
ShareDashboardDialog.showModal({ ShareDashboardDialog.showModal({
dashboard, dashboard,
@@ -175,8 +190,8 @@ function useDashboard(dashboardData) {
const showAddTextboxDialog = useCallback(() => { const showAddTextboxDialog = useCallback(() => {
TextboxDialog.showModal({ TextboxDialog.showModal({
isNew: true, isNew: true,
}).onClose(text => }).onClose((text) =>
dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard))) dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
); );
}, [dashboard]); }, [dashboard]);
@@ -188,13 +203,13 @@ function useDashboard(dashboardData) {
.addWidget(visualization, { .addWidget(visualization, {
parameterMappings: editableMappingsToParameterMappings(parameterMappings), parameterMappings: editableMappingsToParameterMappings(parameterMappings),
}) })
.then(widget => { .then((widget) => {
const widgetsToSave = [ const widgetsToSave = [
widget, widget,
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets), ...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
]; ];
return Promise.all(widgetsToSave.map(w => w.save())).then(() => return Promise.all(widgetsToSave.map((w) => w.save())).then(() =>
setDashboard(currentDashboard => extend({}, currentDashboard)) setDashboard((currentDashboard) => extend({}, currentDashboard))
); );
}) })
); );
@@ -238,6 +253,7 @@ function useDashboard(dashboardData) {
setRefreshRate, setRefreshRate,
disableRefreshRate, disableRefreshRate,
...editModeHandler, ...editModeHandler,
saveDashboardParameters,
gridDisabled, gridDisabled,
setGridDisabled, setGridDisabled,
fullscreen, fullscreen,

View File

@@ -10,9 +10,9 @@ export const urlForDashboard = ({ id, slug }) => `dashboards/${id}-${slug}`;
export function collectDashboardFilters(dashboard, queryResults, urlParams) { export function collectDashboardFilters(dashboard, queryResults, urlParams) {
const filters = {}; const filters = {};
_.each(queryResults, queryResult => { _.each(queryResults, (queryResult) => {
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : []; const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
_.each(queryFilters, queryFilter => { _.each(queryFilters, (queryFilter) => {
const hasQueryStringValue = _.has(urlParams, queryFilter.name); const hasQueryStringValue = _.has(urlParams, queryFilter.name);
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) { if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
@@ -44,7 +44,7 @@ function prepareWidgetsForDashboard(widgets) {
const defaultWidgetSizeY = const defaultWidgetSizeY =
Math.max( Math.max(
_.chain(widgets) _.chain(widgets)
.map(w => w.options.position.sizeY) .map((w) => w.options.position.sizeY)
.max() .max()
.value(), .value(),
20 20
@@ -55,11 +55,11 @@ function prepareWidgetsForDashboard(widgets) {
// 2. update position of widgets in each row - place it right below // 2. update position of widgets in each row - place it right below
// biggest widget from previous row // biggest widget from previous row
_.chain(widgets) _.chain(widgets)
.sortBy(widget => widget.options.position.row) .sortBy((widget) => widget.options.position.row)
.groupBy(widget => widget.options.position.row) .groupBy((widget) => widget.options.position.row)
.reduce((row, widgetsAtRow) => { .reduce((row, widgetsAtRow) => {
let height = 1; let height = 1;
_.each(widgetsAtRow, widget => { _.each(widgetsAtRow, (widget) => {
height = Math.max( height = Math.max(
height, height,
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
@@ -74,8 +74,8 @@ function prepareWidgetsForDashboard(widgets) {
.value(); .value();
// Sort widgets by updated column and row value // Sort widgets by updated column and row value
widgets = _.sortBy(widgets, widget => widget.options.position.col); widgets = _.sortBy(widgets, (widget) => widget.options.position.col);
widgets = _.sortBy(widgets, widget => widget.options.position.row); widgets = _.sortBy(widgets, (widget) => widget.options.position.row);
return widgets; return widgets;
} }
@@ -85,7 +85,7 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
// Find first free row for each column // Find first free row for each column
const bottomLine = _.chain(existingWidgets) const bottomLine = _.chain(existingWidgets)
.map(w => { .map((w) => {
const options = _.extend({}, w.options); const options = _.extend({}, w.options);
const position = _.extend({ row: 0, sizeY: 0 }, options.position); const position = _.extend({ row: 0, sizeY: 0 }, options.position);
return { return {
@@ -97,21 +97,24 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
height: position.sizeY, height: position.sizeY,
}; };
}) })
.reduce((result, item) => { .reduce(
const from = Math.max(item.left, 0); (result, item) => {
const to = Math.min(item.right, result.length + 1); const from = Math.max(item.left, 0);
for (let i = from; i < to; i += 1) { const to = Math.min(item.right, result.length + 1);
result[i] = Math.max(result[i], item.bottom); for (let i = from; i < to; i += 1) {
} result[i] = Math.max(result[i], item.bottom);
return result; }
}, _.map(new Array(dashboardGridOptions.columns), _.constant(0))) return result;
},
_.map(new Array(dashboardGridOptions.columns), _.constant(0))
)
.value(); .value();
// Go through columns, pick them by count necessary to hold new block, // Go through columns, pick them by count necessary to hold new block,
// and calculate bottom-most free row per group. // and calculate bottom-most free row per group.
// Choose group with the top-most free row (comparing to other groups) // Choose group with the top-most free row (comparing to other groups)
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1)) return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
.map(col => ({ .map((col) => ({
col, col,
row: _.chain(bottomLine) row: _.chain(bottomLine)
.slice(col, col + width) .slice(col, col + width)
@@ -126,14 +129,14 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
export function Dashboard(dashboard) { export function Dashboard(dashboard) {
_.extend(this, dashboard); _.extend(this, dashboard);
Object.defineProperty(this, "url", { Object.defineProperty(this, "url", {
get: function() { get: function () {
return urlForDashboard(this); return urlForDashboard(this);
}, },
}); });
} }
function prepareDashboardWidgets(widgets) { function prepareDashboardWidgets(widgets) {
return prepareWidgetsForDashboard(_.map(widgets, widget => new Widget(widget))); return prepareWidgetsForDashboard(_.map(widgets, (widget) => new Widget(widget)));
} }
function transformSingle(dashboard) { function transformSingle(dashboard) {
@@ -154,7 +157,7 @@ function transformResponse(data) {
return data; return data;
} }
const saveOrCreateUrl = data => (data.id ? `api/dashboards/${data.id}` : "api/dashboards"); const saveOrCreateUrl = (data) => (data.id ? `api/dashboards/${data.id}` : "api/dashboards");
const DashboardService = { const DashboardService = {
get: ({ id, slug }) => { get: ({ id, slug }) => {
const params = {}; const params = {};
@@ -164,12 +167,12 @@ const DashboardService = {
return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse); return axios.get(`api/dashboards/${id || slug}`, { params }).then(transformResponse);
}, },
getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse), getByToken: ({ token }) => axios.get(`api/dashboards/public/${token}`).then(transformResponse),
save: data => axios.post(saveOrCreateUrl(data), data).then(transformResponse), save: (data) => axios.post(saveOrCreateUrl(data), data).then(transformResponse),
delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse), delete: ({ id }) => axios.delete(`api/dashboards/${id}`).then(transformResponse),
query: params => axios.get("api/dashboards", { params }).then(transformResponse), query: (params) => axios.get("api/dashboards", { params }).then(transformResponse),
recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse), recent: (params) => axios.get("api/dashboards/recent", { params }).then(transformResponse),
myDashboards: params => axios.get("api/dashboards/my", { params }).then(transformResponse), myDashboards: (params) => axios.get("api/dashboards/my", { params }).then(transformResponse),
favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse), favorites: (params) => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`), favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`), unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
fork: ({ id }) => axios.post(`api/dashboards/${id}/fork`, { id }).then(transformResponse), fork: ({ id }) => axios.post(`api/dashboards/${id}/fork`, { id }).then(transformResponse),
@@ -187,13 +190,13 @@ Dashboard.prototype.canEdit = function canEdit() {
Dashboard.prototype.getParametersDefs = function getParametersDefs() { Dashboard.prototype.getParametersDefs = function getParametersDefs() {
const globalParams = {}; const globalParams = {};
const queryParams = location.search; const queryParams = location.search;
_.each(this.widgets, widget => { _.each(this.widgets, (widget) => {
if (widget.getQuery()) { if (widget.getQuery()) {
const mappings = widget.getParameterMappings(); const mappings = widget.getParameterMappings();
widget widget
.getQuery() .getQuery()
.getParametersDefs(false) .getParametersDefs(false)
.forEach(param => { .forEach((param) => {
const mapping = mappings[param.name]; const mapping = mappings[param.name];
if (mapping.type === Widget.MappingType.DashboardLevel) { if (mapping.type === Widget.MappingType.DashboardLevel) {
// create global param // create global param
@@ -210,15 +213,19 @@ Dashboard.prototype.getParametersDefs = function getParametersDefs() {
}); });
} }
}); });
const mergedValues = {
..._.mapValues(globalParams, (p) => p.value),
...Object.fromEntries((this.options.parameters || []).map((param) => [param.name, param.value])),
};
const resultingGlobalParams = _.values( const resultingGlobalParams = _.values(
_.each(globalParams, param => { _.each(globalParams, (param) => {
param.setValue(param.value); // apply global param value to all locals param.setValue(mergedValues[param.name]); // apply merged value
param.fromUrlParams(queryParams); // try to initialize from url (may do nothing) param.fromUrlParams(queryParams); // allow param-specific parsing logic
}) })
); );
// order dashboard params using paramOrder // order dashboard params using paramOrder
return _.sortBy(resultingGlobalParams, param => return _.sortBy(resultingGlobalParams, (param) =>
_.includes(this.options.globalParamOrder, param.name) _.includes(this.options.globalParamOrder, param.name)
? _.indexOf(this.options.globalParamOrder, param.name) ? _.indexOf(this.options.globalParamOrder, param.name)
: _.size(this.options.globalParamOrder) : _.size(this.options.globalParamOrder)

View File

@@ -58,7 +58,7 @@ class Parameter {
updateLocals() { updateLocals() {
if (isArray(this.locals)) { if (isArray(this.locals)) {
each(this.locals, local => { each(this.locals, (local) => {
local.setValue(this.value); local.setValue(this.value);
}); });
} }
@@ -117,7 +117,7 @@ class Parameter {
/** Get a saveable version of the Parameter by omitting unnecessary props */ /** Get a saveable version of the Parameter by omitting unnecessary props */
toSaveableObject() { toSaveableObject() {
return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId"]); return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId", "locals"]);
} }
} }