diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js
index 5d535b3be9b3f..6c98f701fc93d 100644
--- a/superset-frontend/src/dashboard/actions/hydrate.js
+++ b/superset-frontend/src/dashboard/actions/hydrate.js
@@ -59,23 +59,23 @@ import { updateColorSchema } from './dashboardInfo';
export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';
export const hydrateDashboard =
- (
- dashboardData,
- chartData,
+ ({
+ dashboard,
+ charts,
filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
- dataMaskApplied,
- ) =>
+ dataMask,
+ activeTabs,
+ }) =>
(dispatch, getState) => {
const { user, common, dashboardState } = getState();
-
- const { metadata } = dashboardData;
+ const { metadata, position_data: positionData } = dashboard;
const regularUrlParams = extractUrlParams('regular');
const reservedUrlParams = extractUrlParams('reserved');
const editMode = reservedUrlParams.edit === 'true';
let preselectFilters = {};
- chartData.forEach(chart => {
+ charts.forEach(chart => {
// eslint-disable-next-line no-param-reassign
chart.slice_id = chart.form_data.slice_id;
});
@@ -98,12 +98,10 @@ export const hydrateDashboard =
updateColorSchema(metadata, metadata?.label_colors);
}
- // dashboard layout
- const { position_data } = dashboardData;
// new dash: position_json could be {} or null
const layout =
- position_data && Object.keys(position_data).length > 0
- ? position_data
+ positionData && Object.keys(positionData).length > 0
+ ? positionData
: getEmptyLayout();
// create a lookup to sync layout names with slice names
@@ -128,7 +126,7 @@ export const hydrateDashboard =
const sliceIds = new Set();
const slicesFromExploreCount = new Map();
- chartData.forEach(slice => {
+ charts.forEach(slice => {
const key = slice.slice_id;
const form_data = {
...slice.form_data,
@@ -269,7 +267,7 @@ export const hydrateDashboard =
id: DASHBOARD_HEADER_ID,
type: DASHBOARD_HEADER_TYPE,
meta: {
- text: dashboardData.dashboard_title,
+ text: dashboard.dashboard_title,
},
};
@@ -291,7 +289,7 @@ export const hydrateDashboard =
let filterConfig = metadata?.native_filter_configuration || [];
if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
filterConfig = getNativeFilterConfig(
- chartData,
+ charts,
filterScopes,
preselectFilters,
);
@@ -302,7 +300,7 @@ export const hydrateDashboard =
filterConfig,
});
metadata.show_native_filters =
- dashboardData?.metadata?.show_native_filters ??
+ dashboard?.metadata?.show_native_filters ??
(isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
[
FILTER_BOX_MIGRATION_STATES.CONVERTED,
@@ -343,7 +341,7 @@ export const hydrateDashboard =
}
const { roles } = user;
- const canEdit = canUserEditDashboard(dashboardData, user);
+ const canEdit = canUserEditDashboard(dashboard, user);
return dispatch({
type: HYDRATE_DASHBOARD,
@@ -352,7 +350,7 @@ export const hydrateDashboard =
charts: chartQueries,
// read-only data
dashboardInfo: {
- ...dashboardData,
+ ...dashboard,
metadata,
userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead
dash_edit_perm: canEdit,
@@ -380,7 +378,7 @@ export const hydrateDashboard =
conf: common?.conf,
},
},
- dataMask: dataMaskApplied,
+ dataMask,
dashboardFilters,
nativeFilters,
dashboardState: {
@@ -394,17 +392,17 @@ export const hydrateDashboard =
// dashboard viewers can set refresh frequency for the current visit,
// only persistent refreshFrequency will be saved to backend
shouldPersistRefreshFrequency: false,
- css: dashboardData.css || '',
+ css: dashboard.css || '',
colorNamespace: metadata?.color_namespace || null,
colorScheme: metadata?.color_scheme || null,
editMode: canEdit && editMode,
- isPublished: dashboardData.published,
+ isPublished: dashboard.published,
hasUnsavedChanges: false,
maxUndoHistoryExceeded: false,
- lastModifiedTime: dashboardData.changed_on,
+ lastModifiedTime: dashboard.changed_on,
isRefreshing: false,
isFiltersRefreshing: false,
- activeTabs: dashboardState?.activeTabs || [],
+ activeTabs: activeTabs || dashboardState?.activeTabs || [],
filterboxMigrationState,
datasetsStatus: ResourceStatus.LOADING,
},
diff --git a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
index f2af7af1dcbca..2d13cedcd63e2 100644
--- a/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
+++ b/superset-frontend/src/dashboard/components/URLShortLinkButton/index.tsx
@@ -20,10 +20,11 @@ import React, { useState } from 'react';
import { t } from '@superset-ui/core';
import Popover, { PopoverProps } from 'src/components/Popover';
import CopyToClipboard from 'src/components/CopyToClipboard';
-import { getDashboardPermalink, getUrlParam } from 'src/utils/urlUtils';
+import { getDashboardPermalink } from 'src/utils/urlUtils';
import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { URL_PARAMS } from 'src/constants';
-import { getFilterValue } from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
+import { useSelector } from 'react-redux';
+import { RootState } from 'src/dashboard/types';
+import { getClientErrorObject } from 'src/utils/getClientErrorObject';
export type URLShortLinkButtonProps = {
dashboardId: number;
@@ -42,19 +43,27 @@ export default function URLShortLinkButton({
}: URLShortLinkButtonProps) {
const [shortUrl, setShortUrl] = useState('');
const { addDangerToast } = useToasts();
+ const { dataMask, activeTabs } = useSelector((state: RootState) => ({
+ dataMask: state.dataMask,
+ activeTabs: state.dashboardState.activeTabs,
+ }));
const getCopyUrl = async () => {
- const nativeFiltersKey = getUrlParam(URL_PARAMS.nativeFiltersKey);
try {
- const filterState = await getFilterValue(dashboardId, nativeFiltersKey);
const url = await getDashboardPermalink({
dashboardId,
- filterState,
- hash: anchorLinkId,
+ dataMask,
+ activeTabs,
+ anchor: anchorLinkId,
});
setShortUrl(url);
} catch (error) {
- addDangerToast(error);
+ if (error) {
+ addDangerToast(
+ (await getClientErrorObject(error)).error ||
+ t('Something went wrong.'),
+ );
+ }
}
};
@@ -66,7 +75,14 @@ export default function URLShortLinkButton({
trigger="click"
placement={placement}
content={
-
+ // eslint-disable-next-line jsx-a11y/no-static-element-interactions
+
{
+ e.stopPropagation();
+ }}
+ >
{
+ if (tabIndex === 0 && props.activeTabs.includes(tabId)) {
+ tabIndex = index;
+ }
+ });
+ }
const { children: tabIds } = props.component;
const activeKey = tabIds[tabIndex];
@@ -408,6 +417,7 @@ Tabs.defaultProps = defaultProps;
function mapStateToProps(state) {
return {
nativeFilters: state.nativeFilters,
+ activeTabs: state.dashboardState.activeTabs,
directPathToChild: state.dashboardState.directPathToChild,
filterboxMigrationState: state.dashboardState.filterboxMigrationState,
};
diff --git a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
index f9016e5263c02..d0d8844cdd2b7 100644
--- a/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
+++ b/superset-frontend/src/dashboard/components/menu/ShareMenuItems/index.tsx
@@ -20,9 +20,9 @@ import React from 'react';
import copyTextToClipboard from 'src/utils/copy';
import { t, logging } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
-import { getDashboardPermalink, getUrlParam } from 'src/utils/urlUtils';
-import { URL_PARAMS } from 'src/constants';
-import { getFilterValue } from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
+import { getDashboardPermalink } from 'src/utils/urlUtils';
+import { RootState } from 'src/dashboard/types';
+import { useSelector } from 'react-redux';
interface ShareMenuItemProps {
url?: string;
@@ -48,17 +48,17 @@ const ShareMenuItems = (props: ShareMenuItemProps) => {
dashboardComponentId,
...rest
} = props;
+ const { dataMask, activeTabs } = useSelector((state: RootState) => ({
+ dataMask: state.dataMask,
+ activeTabs: state.dashboardState.activeTabs,
+ }));
async function generateUrl() {
- const nativeFiltersKey = getUrlParam(URL_PARAMS.nativeFiltersKey);
- let filterState = {};
- if (nativeFiltersKey && dashboardId) {
- filterState = await getFilterValue(dashboardId, nativeFiltersKey);
- }
return getDashboardPermalink({
dashboardId,
- filterState,
- hash: dashboardComponentId,
+ dataMask,
+ activeTabs,
+ anchor: dashboardComponentId,
});
}
diff --git a/superset-frontend/src/dashboard/containers/DashboardPage.tsx b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
index 39ba7dbceb824..ef3b4893e31a6 100644
--- a/superset-frontend/src/dashboard/containers/DashboardPage.tsx
+++ b/superset-frontend/src/dashboard/containers/DashboardPage.tsx
@@ -54,13 +54,13 @@ import {
import { URL_PARAMS } from 'src/constants';
import { getUrlParam } from 'src/utils/urlUtils';
import { canUserEditDashboard } from 'src/dashboard/util/permissionUtils';
-import { getFilterSets } from '../actions/nativeFilters';
-import { setDatasetsStatus } from '../actions/dashboardState';
+import { getFilterSets } from 'src/dashboard/actions/nativeFilters';
+import { setDatasetsStatus } from 'src/dashboard/actions/dashboardState';
import {
getFilterValue,
getPermalinkValue,
-} from '../components/nativeFilters/FilterBar/keyValue';
-import { filterCardPopoverStyle } from '../styles';
+} from 'src/dashboard/components/nativeFilters/FilterBar/keyValue';
+import { filterCardPopoverStyle } from 'src/dashboard/styles';
export const MigrationContext = React.createContext(
FILTER_BOX_MIGRATION_STATES.NOOP,
@@ -183,19 +183,20 @@ export const DashboardPage: FC = ({ idOrSlug }: PageProps) => {
async function getDataMaskApplied() {
const permalinkKey = getUrlParam(URL_PARAMS.permalinkKey);
const nativeFilterKeyValue = getUrlParam(URL_PARAMS.nativeFiltersKey);
- let dataMaskFromUrl = nativeFilterKeyValue || {};
-
const isOldRison = getUrlParam(URL_PARAMS.nativeFilters);
+
+ let dataMask = nativeFilterKeyValue || {};
+ let activeTabs: string[] | undefined = [];
if (permalinkKey) {
const permalinkValue = await getPermalinkValue(permalinkKey);
if (permalinkValue) {
- dataMaskFromUrl = permalinkValue.state.filterState;
+ ({ dataMask, activeTabs } = permalinkValue.state);
}
} else if (nativeFilterKeyValue) {
- dataMaskFromUrl = await getFilterValue(id, nativeFilterKeyValue);
+ dataMask = await getFilterValue(id, nativeFilterKeyValue);
}
if (isOldRison) {
- dataMaskFromUrl = isOldRison;
+ dataMask = isOldRison;
}
if (readyToRender) {
@@ -207,12 +208,13 @@ export const DashboardPage: FC = ({ idOrSlug }: PageProps) => {
}
}
dispatch(
- hydrateDashboard(
+ hydrateDashboard({
dashboard,
charts,
+ activeTabs,
filterboxMigrationState,
- dataMaskFromUrl,
- ),
+ dataMask,
+ }),
);
}
return null;
diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts
index e4b8227689ce4..aabc2e5c2e773 100644
--- a/superset-frontend/src/dashboard/types.ts
+++ b/superset-frontend/src/dashboard/types.ts
@@ -27,6 +27,7 @@ import {
import { Dataset } from '@superset-ui/chart-controls';
import { chart } from 'src/components/Chart/chartReducer';
import componentTypes from 'src/dashboard/util/componentTypes';
+import { UrlParamEntries } from 'src/utils/urlUtils';
import { User } from 'src/types/bootstrapTypes';
import { ChartState } from '../explore/types';
@@ -145,13 +146,17 @@ export type ActiveFilters = {
[key: string]: ActiveFilter;
};
-export type DashboardPermalinkValue = {
+export interface DashboardPermalinkState {
+ dataMask: DataMaskStateWithId;
+ activeTabs: string[];
+ anchor: string;
+ urlParams?: UrlParamEntries;
+}
+
+export interface DashboardPermalinkValue {
dashboardId: string;
- state: {
- filterState: DataMaskStateWithId;
- hash: string;
- };
-};
+ state: DashboardPermalinkState;
+}
export type EmbeddedDashboard = {
uuid: string;
diff --git a/superset-frontend/src/hooks/apiResources/dashboards.ts b/superset-frontend/src/hooks/apiResources/dashboards.ts
index 9f512d5b15b2f..b21cc668c06a1 100644
--- a/superset-frontend/src/hooks/apiResources/dashboards.ts
+++ b/superset-frontend/src/hooks/apiResources/dashboards.ts
@@ -26,6 +26,7 @@ export const useDashboard = (idOrSlug: string | number) =>
useApiV1Resource(`/api/v1/dashboard/${idOrSlug}`),
dashboard => ({
...dashboard,
+ // TODO: load these at the API level
metadata:
(dashboard.json_metadata && JSON.parse(dashboard.json_metadata)) || {},
position_data:
diff --git a/superset-frontend/src/utils/getClientErrorObject.ts b/superset-frontend/src/utils/getClientErrorObject.ts
index 3451528f025ca..b6b6c58995dab 100644
--- a/superset-frontend/src/utils/getClientErrorObject.ts
+++ b/superset-frontend/src/utils/getClientErrorObject.ts
@@ -50,10 +50,15 @@ export function parseErrorJson(responseObject: JsonObject): ClientErrorObject {
}
// Marshmallow field validation returns the error mssage in the format
// of { message: { field1: [msg1, msg2], field2: [msg], } }
- if (error.message && typeof error.message === 'object' && !error.error) {
- error.error =
- Object.values(error.message as Record)[0]?.[0] ||
- t('Invalid input');
+ if (!error.error && error.message) {
+ if (typeof error.message === 'object') {
+ error.error =
+ Object.values(error.message as Record)[0]?.[0] ||
+ t('Invalid input');
+ }
+ if (typeof error.message === 'string') {
+ error.error = error.message;
+ }
}
if (error.stack) {
error = {
diff --git a/superset-frontend/src/utils/urlUtils.ts b/superset-frontend/src/utils/urlUtils.ts
index bd570291f2cba..0ca104ef024f6 100644
--- a/superset-frontend/src/utils/urlUtils.ts
+++ b/superset-frontend/src/utils/urlUtils.ts
@@ -19,7 +19,6 @@
import { JsonObject, QueryFormData, SupersetClient } from '@superset-ui/core';
import rison from 'rison';
import { isEmpty } from 'lodash';
-import { getClientErrorObject } from './getClientErrorObject';
import {
RESERVED_CHART_URL_PARAMS,
RESERVED_DASHBOARD_URL_PARAMS,
@@ -96,7 +95,7 @@ function getUrlParams(excludedParams: string[]): URLSearchParams {
return urlParams;
}
-type UrlParamEntries = [string, string][];
+export type UrlParamEntries = [string, string][];
function getUrlParamEntries(urlParams: URLSearchParams): UrlParamEntries {
const urlEntries: [string, string][] = [];
@@ -134,14 +133,7 @@ function getPermalink(endpoint: string, jsonPayload: JsonObject) {
return SupersetClient.post({
endpoint,
jsonPayload,
- })
- .then(result => result.json.url as string)
- .catch(response =>
- // @ts-ignore
- getClientErrorObject(response).then(({ error, statusText }) =>
- Promise.reject(error || statusText),
- ),
- );
+ }).then(result => result.json.url as string);
}
export function getChartPermalink(
@@ -156,17 +148,30 @@ export function getChartPermalink(
export function getDashboardPermalink({
dashboardId,
- filterState,
- hash, // the anchor part of the link which corresponds to the tab/chart id
+ dataMask,
+ activeTabs,
+ anchor, // the anchor part of the link which corresponds to the tab/chart id
}: {
dashboardId: string | number;
- filterState: JsonObject;
- hash?: string;
+ /**
+ * Current applied data masks (for native filters).
+ */
+ dataMask: JsonObject;
+ /**
+ * Current active tabs in the dashboard.
+ */
+ activeTabs: string[];
+ /**
+ * The "anchor" component for the permalink. It will be scrolled into view
+ * and highlighted upon page load.
+ */
+ anchor?: string;
}) {
// only encode filter box state if non-empty
return getPermalink(`/api/v1/dashboard/${dashboardId}/permalink`, {
- filterState,
urlParams: getDashboardUrlParams(),
- hash,
+ dataMask,
+ activeTabs,
+ anchor,
});
}
diff --git a/superset/dashboards/permalink/schemas.py b/superset/dashboards/permalink/schemas.py
index a0fc1cbc5598f..ce222d7ed62c8 100644
--- a/superset/dashboards/permalink/schemas.py
+++ b/superset/dashboards/permalink/schemas.py
@@ -18,10 +18,16 @@
class DashboardPermalinkPostSchema(Schema):
- filterState = fields.Dict(
+ dataMask = fields.Dict(
required=False,
allow_none=True,
- description="Native filter state",
+ description="Data mask used for native filter state",
+ )
+ activeTabs = fields.List(
+ fields.String(),
+ required=False,
+ allow_none=True,
+ description="Current active dashboard tabs",
)
urlParams = fields.List(
fields.Tuple(
@@ -37,6 +43,8 @@ class DashboardPermalinkPostSchema(Schema):
allow_none=True,
description="URL Parameters",
)
- hash = fields.String(
- required=False, allow_none=True, description="Optional anchor link"
+ anchor = fields.String(
+ required=False,
+ allow_none=True,
+ description="Optional anchor link added to url hash",
)
diff --git a/superset/dashboards/permalink/types.py b/superset/dashboards/permalink/types.py
index e93076ba23785..91c5a9620cf71 100644
--- a/superset/dashboards/permalink/types.py
+++ b/superset/dashboards/permalink/types.py
@@ -18,8 +18,9 @@
class DashboardPermalinkState(TypedDict):
- filterState: Optional[Dict[str, Any]]
- hash: Optional[str]
+ dataMask: Optional[Dict[str, Any]]
+ activeTabs: Optional[List[str]]
+ anchor: Optional[str]
urlParams: Optional[List[Tuple[str, str]]]
diff --git a/superset/key_value/models.py b/superset/key_value/models.py
index f846d9039d4e0..f92457d190178 100644
--- a/superset/key_value/models.py
+++ b/superset/key_value/models.py
@@ -21,6 +21,8 @@
from superset import security_manager
from superset.models.helpers import AuditMixinNullable, ImportExportMixin
+VALUE_MAX_SIZE = 2**24 - 1
+
class KeyValueEntry(Model, AuditMixinNullable, ImportExportMixin):
"""Key value store entity"""
@@ -28,7 +30,7 @@ class KeyValueEntry(Model, AuditMixinNullable, ImportExportMixin):
__tablename__ = "key_value"
id = Column(Integer, primary_key=True)
resource = Column(String(32), nullable=False)
- value = Column(LargeBinary(length=2**24 - 1), nullable=False)
+ value = Column(LargeBinary(length=VALUE_MAX_SIZE), nullable=False)
created_on = Column(DateTime, nullable=True)
created_by_fk = Column(Integer, ForeignKey("ab_user.id"), nullable=True)
changed_on = Column(DateTime, nullable=True)
diff --git a/superset/migrations/shared/utils.py b/superset/migrations/shared/utils.py
index 4b0c4e1440dd5..614590409bd20 100644
--- a/superset/migrations/shared/utils.py
+++ b/superset/migrations/shared/utils.py
@@ -17,16 +17,16 @@
import logging
import os
import time
-from typing import Any
+from typing import Any, Callable, Iterator, Optional, Union
from uuid import uuid4
from alembic import op
-from sqlalchemy import engine_from_config
+from sqlalchemy import engine_from_config, inspect
from sqlalchemy.dialects.mysql.base import MySQLDialect
from sqlalchemy.dialects.postgresql.base import PGDialect
from sqlalchemy.engine import reflection
from sqlalchemy.exc import NoSuchTableError
-from sqlalchemy.orm import Session
+from sqlalchemy.orm import Query, Session
logger = logging.getLogger(__name__)
@@ -80,16 +80,38 @@ def assign_uuids(
print(f"Done. Assigned {count} uuids in {time.time() - start_time:.3f}s.\n")
return
- # Othwewise Use Python uuid function
+ for obj in paginated_update(
+ session.query(model),
+ lambda current, total: print(
+ f" uuid assigned to {current} out of {total}", end="\r"
+ ),
+ batch_size=batch_size,
+ ):
+ obj.uuid = uuid4
+ print(f"Done. Assigned {count} uuids in {time.time() - start_time:.3f}s.\n")
+
+
+def paginated_update(
+ query: Query,
+ print_page_progress: Optional[Union[Callable[[int, int], None], bool]] = None,
+ batch_size: int = DEFAULT_BATCH_SIZE,
+) -> Iterator[Any]:
+ """
+ Update models in small batches so we don't have to load everything in memory.
+ """
start = 0
+ count = query.count()
+ session: Session = inspect(query).session
+ if print_page_progress is None or print_page_progress is True:
+ print_page_progress = lambda current, total: print(
+ f" {current}/{total}", end="\r"
+ )
while start < count:
end = min(start + batch_size, count)
- for obj in session.query(model)[start:end]:
- obj.uuid = uuid4()
+ for obj in query[start:end]:
+ yield obj
session.merge(obj)
session.commit()
- if start + batch_size < count:
- print(f" uuid assigned to {end} out of {count}\r", end="")
+ if print_page_progress:
+ print_page_progress(end, count)
start += batch_size
-
- print(f"Done. Assigned {count} uuids in {time.time() - start_time:.3f}s.\n")
diff --git a/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py b/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py
new file mode 100644
index 0000000000000..ecd424d12a154
--- /dev/null
+++ b/superset/migrations/versions/2022-06-27_14-59_7fb8bca906d2_permalink_rename_filterstate.py
@@ -0,0 +1,91 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""permalink_rename_filterState
+
+Revision ID: 7fb8bca906d2
+Revises: f3afaf1f11f0
+Create Date: 2022-06-27 14:59:20.740380
+
+"""
+
+# revision identifiers, used by Alembic.
+revision = "7fb8bca906d2"
+down_revision = "f3afaf1f11f0"
+
+import pickle
+
+from alembic import op
+from sqlalchemy import Column, Integer, LargeBinary, String
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import Session
+
+from superset import db
+from superset.migrations.shared.utils import paginated_update
+
+Base = declarative_base()
+VALUE_MAX_SIZE = 2**24 - 1
+DASHBOARD_PERMALINK_RESOURCE_TYPE = "dashboard_permalink"
+
+
+class KeyValueEntry(Base):
+ __tablename__ = "key_value"
+ id = Column(Integer, primary_key=True)
+ resource = Column(String(32), nullable=False)
+ value = Column(LargeBinary(length=VALUE_MAX_SIZE), nullable=False)
+
+
+def upgrade():
+ bind = op.get_bind()
+ session: Session = db.Session(bind=bind)
+ for entry in paginated_update(
+ session.query(KeyValueEntry).filter(
+ KeyValueEntry.resource == DASHBOARD_PERMALINK_RESOURCE_TYPE
+ )
+ ):
+ value = pickle.loads(entry.value) or {}
+ state = value.get("state")
+ if state:
+ if "filterState" in state:
+ state["dataMask"] = state["filterState"]
+ del state["filterState"]
+ if "hash" in state:
+ state["anchor"] = state["hash"]
+ del state["hash"]
+ entry.value = pickle.dumps(value)
+ session.commit()
+
+
+def downgrade():
+ bind = op.get_bind()
+ session: Session = db.Session(bind=bind)
+ for entry in paginated_update(
+ session.query(KeyValueEntry).filter(
+ KeyValueEntry.resource == DASHBOARD_PERMALINK_RESOURCE_TYPE
+ ),
+ ):
+ value = pickle.loads(entry.value) or {}
+ state = value.get("state")
+ if state:
+ if "dataMask" in state:
+ state["filterState"] = state["dataMask"]
+ del state["dataMask"]
+ if "anchor" in state:
+ state["hash"] = state["anchor"]
+ del state["anchor"]
+ entry.value = pickle.dumps(value)
+ session.merge(entry)
+ session.commit()
diff --git a/superset/views/core.py b/superset/views/core.py
index 10c6aaa048403..1625a691aa97f 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -2001,13 +2001,13 @@ def dashboard_permalink( # pylint: disable=no-self-use
return redirect("/dashboard/list/")
if not value:
return json_error_response(_("permalink state not found"), status=404)
- dashboard_id = value["dashboardId"]
+ dashboard_id, state = value["dashboardId"], value.get("state", {})
url = f"/superset/dashboard/{dashboard_id}?permalink_key={key}"
- url_params = value["state"].get("urlParams")
+ url_params = state.get("urlParams")
if url_params:
params = parse.urlencode(url_params)
url = f"{url}&{params}"
- hash_ = value["state"].get("hash")
+ hash_ = state.get("anchor", state.get("hash"))
if hash_:
url = f"{url}#{hash_}"
return redirect(url)
diff --git a/tests/integration_tests/dashboards/permalink/api_tests.py b/tests/integration_tests/dashboards/permalink/api_tests.py
index 33186131d559f..12d758d5eb3f9 100644
--- a/tests/integration_tests/dashboards/permalink/api_tests.py
+++ b/tests/integration_tests/dashboards/permalink/api_tests.py
@@ -38,8 +38,8 @@
from tests.integration_tests.test_app import app
STATE = {
- "filterState": {"FILTER_1": "foo"},
- "hash": "my-anchor",
+ "dataMask": {"FILTER_1": "foo"},
+ "activeTabs": ["my-anchor"],
}