Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,15 @@ assists people when migrating to a new version.
`test_empty_membership` and `test_empty_membership_in_or_group`, derived from
the existing `identity` and `or` fixtures.

- `AUTO_REFRESH_POLICY` (default `False`) enables per-dashboard auto-refresh
policies in Dashboard properties, JSON, API writes and imports. Policy dashboards
use configured standard intervals only; invalid defaults must be corrected when
changing refresh settings. Unrelated saves preserve outdated settings. A PUT that
omits `auto_refresh_policy` keeps the stored policy; send
`null` (or turn the policy off in Properties) to remove it. Disabling the flag
restores ordinary refresh behavior and retains stored policies for re-enabling.
No migration is required.

- Dashboard and chart `viewers` can no longer be added through the API or the MCP
`manage_dashboard_roles` tool while the `ENABLE_VIEWERS` feature flag is off; such
requests are rejected with a 422. Enforcement of existing viewers is unchanged (a
Expand Down
76 changes: 76 additions & 0 deletions docs/docs/using-superset/creating-your-first-dashboard.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -598,3 +598,79 @@ When saving or adding a chart to a dashboard from Explore, you can select which
- [Dashboard Customization](https://docs.preset.io/docs/dashboard-customization) - Advanced dashboard styling and layout options
- [Blog: BI Dashboard Best Practices](https://preset.io/blog/bi-dashboard-best-practices/)
:::


## Dashboard auto-refresh policy

Operators can enable `AUTO_REFRESH_POLICY` in `FEATURE_FLAGS`; it defaults to
`False`. With the flag enabled, dashboard editors can use **Dashboard properties
→ Refresh settings → Use a refresh policy** to allow or prohibit auto-refresh and
select optional shortest and longest intervals from `DASHBOARD_AUTO_REFRESH_INTERVALS`.
The shortest interval must not exceed the longest. “Don't refresh” remains
available. Policy dashboards use standard intervals only; Custom remains available
on dashboards without a policy.

When auto-refresh is prohibited, its session menu, status indicator and automatic
timers are absent, including in embedded dashboards with hidden headers. Manual
refresh remains available. Inactive bounds are retained and validated when
re-enabled. Turning auto-refresh off keeps the default refresh frequency; a
positive default shows an error and blocks saving until “Don't refresh” is picked.
Invalid saved defaults must be corrected before saving; submitted values are never
silently reset or clamped. Changes apply to viewers on their next load.

Advanced JSON and authorized API/import writes use the same policy. For example,
inside dashboard JSON metadata:

```json
{
"refresh_frequency": 60,
"auto_refresh_policy": {
"auto_refresh_allowed": true,
"minimum_refresh_interval": {"value": 30, "unit": "seconds"},
"maximum_refresh_interval": {"value": 1, "unit": "hours"}
}
}
```

A dashboard without the key has no policy. A PUT that omits `auto_refresh_policy`
keeps the stored policy; send `null` (or turn the policy off in Properties) to
remove it. Dashboard copy also keeps the original policy unless the request sets
the key. An empty policy object enables standard-only intervals with auto-refresh allowed. Switches
must be booleans; `auto_refresh_allowed` defaults to `true`. Each bound is optional or
`null`; otherwise it contains exactly `value` (a positive integer) and `unit`
(`seconds`, `minutes`, or `hours`). When a policy is enabled or changed, active
bounds must normalize to configured standard intervals. Unchanged stored bounds
survive interval-list changes, but positive frequencies must still be standard,
within the bounds, and leave at least one permitted positive option. There is no
separate numeric upper limit beyond the configured standard intervals for active
new bounds. `refresh_frequency` is zero or a positive integer number of seconds.
Prohibited auto-refresh requires zero.

`POST /api/v1/dashboard/`, `PUT /api/v1/dashboard/<id>` and dashboard copy validate
the effective metadata before saving. V1 imports, legacy CLI imports, dashboard
MCP writes and version restores also validate policies. REST PUT, Properties
saves, MCP updates, and version restores validate refresh rules only when the policy or default
frequency changes. Unchanged invalid settings appear as warnings in Properties
and do not block unrelated saves. Warnings without a dedicated control direct
editors to Advanced settings and show each validation message on its own line,
with the policy key for field-specific problems. Copy creates a new dashboard and validates all
refresh rules, so an outdated default must be fixed before copying. Existing dashboard
permissions apply. Successful policy changes create an existing audit-log row
with actor, dashboard, timestamp, previous/new policy and write source.

Dashboard REST validation returns HTTP 422 with the existing `message` field map
and an additional `errors` list containing `field`, `code`, and translated
`message`. Fields are `json_metadata.refresh_frequency`,
`json_metadata.auto_refresh_policy`, or a nested policy field such as
`json_metadata.auto_refresh_policy.minimum_refresh_interval`. Codes are
`invalid_policy`, `invalid_boundary`, `minimum_greater_than_maximum`,
`no_standard_interval`,
`invalid_refresh_frequency`, `auto_refresh_prohibited`, `custom_not_allowed`,
`below_minimum`, and `above_maximum`. Imports retain their existing bundle-specific
error envelope; invalid assets imports retain the existing error-status behavior.
Rejected writes do not persist metadata or successful policy-history changes.

With `AUTO_REFRESH_POLICY` disabled, the editor, timers and selectors retain their
ordinary behavior, policy validation/history are inactive, and the stored policy
key is accepted and retained. Turning the flag back on reapplies the stored policy
and validates it on the next save. No database migration is required.
7 changes: 7 additions & 0 deletions docs/static/feature-flags.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@
"lifecycle": "development",
"description": "Enables experimental tabs UI for Alerts and Reports"
},
{
"name": "AUTO_REFRESH_POLICY",
"default": false,
"lifecycle": "development",
"description": "Enables per-dashboard auto-refresh policies and standard interval bounds.",
"docs": "/docs/using-superset/creating-your-first-dashboard"
},
{
"name": "CHART_PLUGINS_EXPERIMENTAL",
"default": false,
Expand Down
11 changes: 7 additions & 4 deletions docs/static/resources/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -4063,7 +4063,7 @@
"type": "boolean"
},
"json_metadata": {
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.",
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters. When AUTO_REFRESH_POLICY is enabled, the optional `auto_refresh_policy` key holds the dashboard's auto-refresh policy: `auto_refresh_allowed` (boolean, default true), `minimum_refresh_interval` and `maximum_refresh_interval` (each null or `{\"value\": <whole number >= 1>, \"unit\": \"seconds\" | \"minutes\" | \"hours\"}`; the length must be one of the standard refresh intervals, no other upper limit). With a policy, `refresh_frequency` must be 0 or a whole number of seconds that is a standard interval within the bounds, and 0 when auto-refresh isn't allowed. On create, a missing or null `auto_refresh_policy` means no policy. An update or copy that omits `auto_refresh_policy` keeps the stored policy; send null to remove it.",
"type": "string"
},
"replace_metadata": {
Expand Down Expand Up @@ -4404,7 +4404,7 @@
"type": "boolean"
},
"json_metadata": {
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.",
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters. When AUTO_REFRESH_POLICY is enabled, the optional `auto_refresh_policy` key holds the dashboard's auto-refresh policy: `auto_refresh_allowed` (boolean, default true), `minimum_refresh_interval` and `maximum_refresh_interval` (each null or `{\"value\": <whole number >= 1>, \"unit\": \"seconds\" | \"minutes\" | \"hours\"}`; the length must be one of the standard refresh intervals, no other upper limit). With a policy, `refresh_frequency` must be 0 or a whole number of seconds that is a standard interval within the bounds, and 0 when auto-refresh isn't allowed. On create, a missing or null `auto_refresh_policy` means no policy. An update or copy that omits `auto_refresh_policy` keeps the stored policy; send null to remove it.",
"type": "string"
},
"position_json": {
Expand Down Expand Up @@ -4774,7 +4774,7 @@
"type": "boolean"
},
"json_metadata": {
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.",
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters. When AUTO_REFRESH_POLICY is enabled, the optional `auto_refresh_policy` key holds the dashboard's auto-refresh policy: `auto_refresh_allowed` (boolean, default true), `minimum_refresh_interval` and `maximum_refresh_interval` (each null or `{\"value\": <whole number >= 1>, \"unit\": \"seconds\" | \"minutes\" | \"hours\"}`; the length must be one of the standard refresh intervals, no other upper limit). With a policy, `refresh_frequency` must be 0 or a whole number of seconds that is a standard interval within the bounds, and 0 when auto-refresh isn't allowed. On create, a missing or null `auto_refresh_policy` means no policy. An update or copy that omits `auto_refresh_policy` keeps the stored policy; send null to remove it.",
"type": "string"
},
"position_json": {
Expand Down Expand Up @@ -4914,7 +4914,7 @@
"type": "boolean"
},
"json_metadata": {
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters.",
"description": "This JSON object is generated dynamically when clicking the save or overwrite button in the dashboard view. It is exposed here for reference and for power users who may want to alter specific parameters. When AUTO_REFRESH_POLICY is enabled, the optional `auto_refresh_policy` key holds the dashboard's auto-refresh policy: `auto_refresh_allowed` (boolean, default true), `minimum_refresh_interval` and `maximum_refresh_interval` (each null or `{\"value\": <whole number >= 1>, \"unit\": \"seconds\" | \"minutes\" | \"hours\"}`; the length must be one of the standard refresh intervals, no other upper limit). With a policy, `refresh_frequency` must be 0 or a whole number of seconds that is a standard interval within the bounds, and 0 when auto-refresh isn't allowed. On create, a missing or null `auto_refresh_policy` means no policy. An update or copy that omits `auto_refresh_policy` keeps the stored policy; send null to remove it.",
"nullable": true,
"type": "string"
},
Expand Down Expand Up @@ -19332,6 +19332,9 @@
"404": {
"$ref": "#/components/responses/404"
},
"422": {
"$ref": "#/components/responses/422"
},
"500": {
"$ref": "#/components/responses/500"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export enum FeatureFlag {
AlertReportsFilter = 'ALERT_REPORTS_FILTER',
AlertReportsRetry = 'ALERT_REPORTS_RETRY',
AllowFullCsvExport = 'ALLOW_FULL_CSV_EXPORT',
AutoRefreshPolicy = 'AUTO_REFRESH_POLICY',
ChartPluginsExperimental = 'CHART_PLUGINS_EXPERIMENTAL',
ConfirmDashboardDiff = 'CONFIRM_DASHBOARD_DIFF',
CssTemplates = 'CSS_TEMPLATES',
Expand Down
143 changes: 142 additions & 1 deletion superset-frontend/src/dashboard/actions/hydrate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { createMemoryHistory } from 'history';
import { JsonObject } from '@superset-ui/core';
import { GetState, FilterBarOrientation } from '../types';
import { HYDRATE_DASHBOARD, hydrateDashboard } from './hydrate';
import {
CHART_TYPE,
Expand All @@ -27,7 +30,6 @@ import {
TAB_TYPE,
} from '../util/componentTypes';
import { DASHBOARD_GRID_ID, DASHBOARD_ROOT_ID } from '../util/constants';
import { FilterBarOrientation } from '../types';

/**
* Regression guard for the follow-up to PR #39417 / PR #41832: the default
Expand Down Expand Up @@ -529,3 +531,142 @@ test('falls back to vertical when metadata has no filter_bar_orientation', () =>
FilterBarOrientation.Vertical,
);
});

const hydratedRefreshFrequency = (
metadata: JsonObject,
intervals: unknown[] = [
[0, "Don't refresh"],
[60, '1 minute'],
[300, '5 minutes'],
],
enabled = true,
) => {
window.featureFlags = { AUTO_REFRESH_POLICY: enabled };
const dispatch = jest.fn(action => action);
const getState = (() => ({
user: { userId: 1, roles: {} },
common: {
conf: {
DASHBOARD_AUTO_REFRESH_INTERVALS: intervals,
},
},
dashboardState: {},
})) as unknown as GetState;

hydrateDashboard({
history: createMemoryHistory(),
dashboard: {
id: 1,
dashboard_title: 'Dashboard',
metadata,
position_data: null,
published: false,
owners: [],
} as unknown as Parameters<typeof hydrateDashboard>[0]['dashboard'],
charts: [],
dataMask: {},
activeTabs: null,
chartStates: null,
})(dispatch, getState);

return dispatch.mock.calls[0][0].data.dashboardState.refreshFrequency;
};

test('a prohibited policy starts without auto-refresh', () => {
expect(
hydratedRefreshFrequency({
refresh_frequency: 60,
auto_refresh_policy: { auto_refresh_allowed: false },
}),
).toBe(0);
});

test('a saved frequency the policy does not allow falls back to 0', () => {
expect(
hydratedRefreshFrequency({
refresh_frequency: 60,
auto_refresh_policy: {
auto_refresh_allowed: true,
minimum_refresh_interval: { value: 5, unit: 'minutes' },
},
}),
).toBe(0);
});

test('a saved frequency the policy allows is kept', () => {
expect(
hydratedRefreshFrequency({
refresh_frequency: 300,
auto_refresh_policy: {
auto_refresh_allowed: true,
minimum_refresh_interval: { value: 5, unit: 'minutes' },
},
}),
).toBe(300);
});

test('without a policy the saved frequency is kept, Custom values included', () => {
expect(hydratedRefreshFrequency({ refresh_frequency: 45 })).toBe(45);
});

test('a no-bounds policy drops a saved frequency the admin list no longer has', () => {
expect(
hydratedRefreshFrequency(
{
refresh_frequency: 300,
auto_refresh_policy: { auto_refresh_allowed: true },
},
[
[0, "Don't refresh"],
[60, '1 minute'],
[3600, '1 hour'],
],
),
).toBe(0);
});

test('flag off hydrates the saved frequency despite a prohibited policy', () => {
expect(
hydratedRefreshFrequency(
{
refresh_frequency: 45,
auto_refresh_policy: { auto_refresh_allowed: false },
},
[],
false,
),
).toBe(45);
});

test.each([
{ enabled: false, expandAllSlices: true },
{ enabled: true, expandAllSlices: true },
{ enabled: false, expandAllSlices: false },
{ enabled: true, expandAllSlices: false },
{ enabled: false, expandAllSlices: undefined },
{ enabled: true, expandAllSlices: undefined },
])(
'hydrates chart description expansion with policy flag $enabled and saved value $expandAllSlices',
({ enabled, expandAllSlices }) => {
const originalFeatureFlags = window.featureFlags;
window.featureFlags = { AUTO_REFRESH_POLICY: enabled };
try {
const action = hydrate(flatTabsPositionData, {
metadata: {
...(expandAllSlices === undefined
? {}
: { expand_all_slices: expandAllSlices }),
expanded_slices: { 7: true },
auto_refresh_policy: { auto_refresh_allowed: false },
},
});

expect(action.data.dashboardState.expandAllSlices).toBe(
expandAllSlices ?? false,
);
expect(action.data.dashboardState.expandedSlices).toEqual({ 7: true });
} finally {
window.featureFlags = originalFeatureFlags;
}
},
);
21 changes: 19 additions & 2 deletions superset-frontend/src/dashboard/actions/hydrate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@
* under the License.
*/
/* eslint-disable camelcase */
import { DataMaskStateWithId, JsonObject } from '@superset-ui/core';
import {
FeatureFlag,
isFeatureEnabled,
DataMaskStateWithId,
JsonObject,
} from '@superset-ui/core';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
// eslint-disable-next-line import/no-extraneous-dependencies
Expand Down Expand Up @@ -64,6 +69,10 @@ import extractUrlParams from '../util/extractUrlParams';
import updateComponentParentsList from '../util/updateComponentParentsList';
import { AUTO_REFRESH_STATE_DEFAULTS } from '../types/autoRefresh';
import { migrateChartCustomizationArray } from '../util/migrateChartCustomization';
import {
getAllowedRefreshOptions,
getEffectiveRefreshFrequency,
} from '../components/RefreshFrequency';
import {
DashboardLayout,
FilterBarOrientation,
Expand Down Expand Up @@ -408,7 +417,15 @@ export const hydrateDashboard =
focusedFilterField: null,
expandedSlices: metadata?.expanded_slices || {},
expandAllSlices: metadata?.expand_all_slices || false,
refreshFrequency: metadata?.refresh_frequency || 0,
refreshFrequency: getEffectiveRefreshFrequency(
metadata?.refresh_frequency || 0,
getAllowedRefreshOptions(
isFeatureEnabled(FeatureFlag.AutoRefreshPolicy)
? metadata?.auto_refresh_policy
: undefined,
common?.conf?.DASHBOARD_AUTO_REFRESH_INTERVALS,
),
),
// dashboard viewers can set refresh frequency for the current visit,
// only persistent refreshFrequency will be saved to backend
shouldPersistRefreshFrequency: false,
Expand Down
Loading
Loading