diff --git a/client/app/pages/dashboards/DashboardPage.jsx b/client/app/pages/dashboards/DashboardPage.jsx
index 63c796ee4..190e043cb 100644
--- a/client/app/pages/dashboards/DashboardPage.jsx
+++ b/client/app/pages/dashboards/DashboardPage.jsx
@@ -31,7 +31,8 @@ function DashboardSettings({ dashboardConfiguration }) {
updateDashboard({ dashboard_filters_enabled: target.checked })}
- data-test="DashboardFiltersCheckbox">
+ data-test="DashboardFiltersCheckbox"
+ >
Use Dashboard Level Filters
@@ -90,9 +91,9 @@ function DashboardComponent(props) {
const [pageContainer, setPageContainer] = useState(null);
const [bottomPanelStyles, setBottomPanelStyles] = useState({});
- const onParametersEdit = parameters => {
+ const onParametersEdit = (parameters) => {
const paramOrder = map(parameters, "name");
- updateDashboard({ options: { globalParamOrder: paramOrder } });
+ updateDashboard({ options: { ...dashboard.options, globalParamOrder: paramOrder } });
};
useEffect(() => {
@@ -175,7 +176,7 @@ function DashboardPage({ dashboardSlug, dashboardId, onError }) {
useEffect(() => {
Dashboard.get({ id: dashboardId, slug: dashboardSlug })
- .then(dashboardData => {
+ .then((dashboardData) => {
recordEvent("view", "dashboard", dashboardData.id);
setDashboard(dashboardData);
@@ -207,7 +208,7 @@ routes.register(
"Dashboards.LegacyViewOrEdit",
routeWithUserSession({
path: "/dashboard/:dashboardSlug",
- render: pageProps => ,
+ render: (pageProps) => ,
})
);
@@ -215,6 +216,6 @@ routes.register(
"Dashboards.ViewOrEdit",
routeWithUserSession({
path: "/dashboards/:dashboardId([^-]+)(-.*)?",
- render: pageProps => ,
+ render: (pageProps) => ,
})
);
diff --git a/client/app/pages/dashboards/components/DashboardHeader.jsx b/client/app/pages/dashboards/components/DashboardHeader.jsx
index b8b27a392..86d7e12ae 100644
--- a/client/app/pages/dashboards/components/DashboardHeader.jsx
+++ b/client/app/pages/dashboards/components/DashboardHeader.jsx
@@ -22,7 +22,7 @@ import { DashboardStatusEnum } from "../hooks/useDashboard";
import "./DashboardHeader.less";
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) {
@@ -38,7 +38,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
updateDashboard({ name })}
+ onDone={(name) => updateDashboard({ name })}
value={dashboard.name}
ignoreBlanks
/>
@@ -53,7 +53,7 @@ function DashboardPageTitle({ dashboardConfiguration }) {
isArchived={dashboard.is_archived}
canEdit={canEditDashboard}
getAvailableTags={getDashboardTags}
- onEdit={tags => updateDashboard({ tags })}
+ onEdit={(tags) => updateDashboard({ tags })}
/>
);
@@ -89,14 +89,15 @@ function RefreshButton({ dashboardConfiguration }) {
placement="bottomRight"
overlay={
- }>
+ }
+ >
@@ -229,7 +232,8 @@ function DashboardControl({ dashboardConfiguration, headerExtra }) {
type={buttonType(dashboard.publicAccessEnabled)}
onClick={showShareDashboardDialog}
data-test="OpenShareForm"
- aria-label="Share">
+ aria-label="Share"
+ >
@@ -252,7 +256,11 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
doneBtnClickedWhileSaving,
dashboardStatus,
retrySaveDashboardLayout,
+ saveDashboardParameters,
} = dashboardConfiguration;
+ const handleDoneEditing = () => {
+ saveDashboardParameters().then(() => setEditingLayout(false));
+ };
let status;
if (dashboardStatus === DashboardStatusEnum.SAVED) {
status = Saved;
@@ -277,7 +285,7 @@ function DashboardEditControl({ dashboardConfiguration, headerExtra }) {
Retry
) : (
- setEditingLayout(false)}>
+
{!doneBtnClickedWhileSaving && } Done Editing
)}
diff --git a/client/app/pages/dashboards/hooks/useDashboard.js b/client/app/pages/dashboards/hooks/useDashboard.js
index 43eeb336d..d74f8f346 100644
--- a/client/app/pages/dashboards/hooks/useDashboard.js
+++ b/client/app/pages/dashboards/hooks/useDashboard.js
@@ -22,12 +22,12 @@ export { DashboardStatusEnum } from "./useEditModeHandler";
function getAffectedWidgets(widgets, updatedParameters = []) {
return !isEmpty(updatedParameters)
- ? widgets.filter(widget =>
+ ? widgets.filter((widget) =>
Object.values(widget.getParameterMappings())
.filter(({ type }) => type === "dashboard-level")
.some(({ mapTo }) =>
includes(
- updatedParameters.map(p => p.name),
+ updatedParameters.map((p) => p.name),
mapTo
)
)
@@ -50,7 +50,7 @@ function useDashboard(dashboardData) {
[dashboard]
);
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]
);
@@ -67,19 +67,19 @@ function useDashboard(dashboardData) {
const updateDashboard = useCallback(
(data, includeVersion = true) => {
- setDashboard(currentDashboard => extend({}, currentDashboard, data));
+ 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))));
+ .then((updatedDashboard) => {
+ setDashboard((currentDashboard) => extend({}, currentDashboard, pick(updatedDashboard, keys(data))));
if (has(data, "name")) {
location.setPath(url.parse(updatedDashboard.url).pathname, true);
}
})
- .catch(error => {
+ .catch((error) => {
const status = get(error, "response.status");
if (status === 403) {
notification.error("Dashboard update failed", "Permission Denied.");
@@ -102,25 +102,25 @@ function useDashboard(dashboardData) {
const loadWidget = useCallback((widget, forceRefresh = false) => {
widget.getParametersDefs(); // Force widget to read parameters values from URL
- setDashboard(currentDashboard => extend({}, currentDashboard));
+ setDashboard((currentDashboard) => extend({}, currentDashboard));
return widget
.load(forceRefresh)
- .catch(error => {
+ .catch((error) => {
// QueryResultErrors are expected
if (error instanceof QueryResultError) {
return;
}
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 => {
- setDashboard(currentDashboard =>
+ const removeWidget = useCallback((widgetId) => {
+ setDashboard((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 = []) => {
const affectedWidgets = getAffectedWidgets(dashboardRef.current.widgets, updatedParameters);
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(() => {
- 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);
setFilters(updatedFilters);
});
@@ -145,7 +145,7 @@ function useDashboard(dashboardData) {
);
const refreshDashboard = useCallback(
- updatedParameters => {
+ (updatedParameters) => {
if (!refreshing) {
setRefreshing(true);
loadDashboard(true, updatedParameters).finally(() => setRefreshing(false));
@@ -154,15 +154,30 @@ function useDashboard(dashboardData) {
[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.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));
+ const handleDialogClose = () => setDashboard((currentDashboard) => extend({}, currentDashboard));
ShareDashboardDialog.showModal({
dashboard,
@@ -175,8 +190,8 @@ function useDashboard(dashboardData) {
const showAddTextboxDialog = useCallback(() => {
TextboxDialog.showModal({
isNew: true,
- }).onClose(text =>
- dashboard.addWidget(text).then(() => setDashboard(currentDashboard => extend({}, currentDashboard)))
+ }).onClose((text) =>
+ dashboard.addWidget(text).then(() => setDashboard((currentDashboard) => extend({}, currentDashboard)))
);
}, [dashboard]);
@@ -188,13 +203,13 @@ function useDashboard(dashboardData) {
.addWidget(visualization, {
parameterMappings: editableMappingsToParameterMappings(parameterMappings),
})
- .then(widget => {
+ .then((widget) => {
const widgetsToSave = [
widget,
...synchronizeWidgetTitles(widget.options.parameterMappings, dashboard.widgets),
];
- return Promise.all(widgetsToSave.map(w => w.save())).then(() =>
- setDashboard(currentDashboard => extend({}, currentDashboard))
+ return Promise.all(widgetsToSave.map((w) => w.save())).then(() =>
+ setDashboard((currentDashboard) => extend({}, currentDashboard))
);
})
);
@@ -238,6 +253,7 @@ function useDashboard(dashboardData) {
setRefreshRate,
disableRefreshRate,
...editModeHandler,
+ saveDashboardParameters,
gridDisabled,
setGridDisabled,
fullscreen,
diff --git a/client/app/services/dashboard.js b/client/app/services/dashboard.js
index a4d3550ba..067f6a8dc 100644
--- a/client/app/services/dashboard.js
+++ b/client/app/services/dashboard.js
@@ -10,9 +10,9 @@ export const urlForDashboard = ({ id, slug }) => `dashboards/${id}-${slug}`;
export function collectDashboardFilters(dashboard, queryResults, urlParams) {
const filters = {};
- _.each(queryResults, queryResult => {
+ _.each(queryResults, (queryResult) => {
const queryFilters = queryResult && queryResult.getFilters ? queryResult.getFilters() : [];
- _.each(queryFilters, queryFilter => {
+ _.each(queryFilters, (queryFilter) => {
const hasQueryStringValue = _.has(urlParams, queryFilter.name);
if (!(hasQueryStringValue || dashboard.dashboard_filters_enabled)) {
@@ -44,7 +44,7 @@ function prepareWidgetsForDashboard(widgets) {
const defaultWidgetSizeY =
Math.max(
_.chain(widgets)
- .map(w => w.options.position.sizeY)
+ .map((w) => w.options.position.sizeY)
.max()
.value(),
20
@@ -55,11 +55,11 @@ function prepareWidgetsForDashboard(widgets) {
// 2. update position of widgets in each row - place it right below
// biggest widget from previous row
_.chain(widgets)
- .sortBy(widget => widget.options.position.row)
- .groupBy(widget => widget.options.position.row)
+ .sortBy((widget) => widget.options.position.row)
+ .groupBy((widget) => widget.options.position.row)
.reduce((row, widgetsAtRow) => {
let height = 1;
- _.each(widgetsAtRow, widget => {
+ _.each(widgetsAtRow, (widget) => {
height = Math.max(
height,
widget.options.position.autoHeight ? defaultWidgetSizeY : widget.options.position.sizeY
@@ -74,8 +74,8 @@ function prepareWidgetsForDashboard(widgets) {
.value();
// Sort widgets by updated column and row value
- widgets = _.sortBy(widgets, widget => widget.options.position.col);
- widgets = _.sortBy(widgets, widget => widget.options.position.row);
+ widgets = _.sortBy(widgets, (widget) => widget.options.position.col);
+ widgets = _.sortBy(widgets, (widget) => widget.options.position.row);
return widgets;
}
@@ -85,7 +85,7 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
// Find first free row for each column
const bottomLine = _.chain(existingWidgets)
- .map(w => {
+ .map((w) => {
const options = _.extend({}, w.options);
const position = _.extend({ row: 0, sizeY: 0 }, options.position);
return {
@@ -97,21 +97,24 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
height: position.sizeY,
};
})
- .reduce((result, item) => {
- const from = Math.max(item.left, 0);
- const to = Math.min(item.right, result.length + 1);
- 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)))
+ .reduce(
+ (result, item) => {
+ const from = Math.max(item.left, 0);
+ const to = Math.min(item.right, result.length + 1);
+ 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))
+ )
.value();
// Go through columns, pick them by count necessary to hold new block,
// and calculate bottom-most free row per group.
// Choose group with the top-most free row (comparing to other groups)
return _.chain(_.range(0, dashboardGridOptions.columns - width + 1))
- .map(col => ({
+ .map((col) => ({
col,
row: _.chain(bottomLine)
.slice(col, col + width)
@@ -126,14 +129,14 @@ function calculateNewWidgetPosition(existingWidgets, newWidget) {
export function Dashboard(dashboard) {
_.extend(this, dashboard);
Object.defineProperty(this, "url", {
- get: function() {
+ get: function () {
return urlForDashboard(this);
},
});
}
function prepareDashboardWidgets(widgets) {
- return prepareWidgetsForDashboard(_.map(widgets, widget => new Widget(widget)));
+ return prepareWidgetsForDashboard(_.map(widgets, (widget) => new Widget(widget)));
}
function transformSingle(dashboard) {
@@ -154,7 +157,7 @@ function transformResponse(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 = {
get: ({ id, slug }) => {
const params = {};
@@ -164,12 +167,12 @@ const DashboardService = {
return axios.get(`api/dashboards/${id || slug}`, { params }).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),
- query: params => axios.get("api/dashboards", { params }).then(transformResponse),
- recent: params => axios.get("api/dashboards/recent", { params }).then(transformResponse),
- myDashboards: params => axios.get("api/dashboards/my", { params }).then(transformResponse),
- favorites: params => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
+ query: (params) => axios.get("api/dashboards", { params }).then(transformResponse),
+ recent: (params) => axios.get("api/dashboards/recent", { params }).then(transformResponse),
+ myDashboards: (params) => axios.get("api/dashboards/my", { params }).then(transformResponse),
+ favorites: (params) => axios.get("api/dashboards/favorites", { params }).then(transformResponse),
favorite: ({ id }) => axios.post(`api/dashboards/${id}/favorite`),
unfavorite: ({ id }) => axios.delete(`api/dashboards/${id}/favorite`),
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() {
const globalParams = {};
const queryParams = location.search;
- _.each(this.widgets, widget => {
+ _.each(this.widgets, (widget) => {
if (widget.getQuery()) {
const mappings = widget.getParameterMappings();
widget
.getQuery()
.getParametersDefs(false)
- .forEach(param => {
+ .forEach((param) => {
const mapping = mappings[param.name];
if (mapping.type === Widget.MappingType.DashboardLevel) {
// 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(
- _.each(globalParams, param => {
- param.setValue(param.value); // apply global param value to all locals
- param.fromUrlParams(queryParams); // try to initialize from url (may do nothing)
+ _.each(globalParams, (param) => {
+ param.setValue(mergedValues[param.name]); // apply merged value
+ param.fromUrlParams(queryParams); // allow param-specific parsing logic
})
);
// order dashboard params using paramOrder
- return _.sortBy(resultingGlobalParams, param =>
+ return _.sortBy(resultingGlobalParams, (param) =>
_.includes(this.options.globalParamOrder, param.name)
? _.indexOf(this.options.globalParamOrder, param.name)
: _.size(this.options.globalParamOrder)
diff --git a/client/app/services/parameters/Parameter.js b/client/app/services/parameters/Parameter.js
index 03bc698a2..12c022f6c 100644
--- a/client/app/services/parameters/Parameter.js
+++ b/client/app/services/parameters/Parameter.js
@@ -58,7 +58,7 @@ class Parameter {
updateLocals() {
if (isArray(this.locals)) {
- each(this.locals, local => {
+ each(this.locals, (local) => {
local.setValue(this.value);
});
}
@@ -117,7 +117,7 @@ class Parameter {
/** Get a saveable version of the Parameter by omitting unnecessary props */
toSaveableObject() {
- return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId"]);
+ return omit(this, ["$$value", "urlPrefix", "pendingValue", "parentQueryId", "locals"]);
}
}