diff --git a/superset-frontend/src/dashboard/actions/hydrate.js b/superset-frontend/src/dashboard/actions/hydrate.js index 4e0787b43665d..4453693d90bc9 100644 --- a/superset-frontend/src/dashboard/actions/hydrate.js +++ b/superset-frontend/src/dashboard/actions/hydrate.js @@ -60,329 +60,328 @@ import extractUrlParams from '../util/extractUrlParams'; export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; -export const hydrateDashboard = - (dashboardData, chartData) => (dispatch, getState) => { - const { user, common, dashboardState } = getState(); - let { metadata } = dashboardData; - const regularUrlParams = extractUrlParams('regular'); - const reservedUrlParams = extractUrlParams('reserved'); - const editMode = reservedUrlParams.edit === 'true'; - - let preselectFilters = {}; - - chartData.forEach(chart => { - // eslint-disable-next-line no-param-reassign - chart.slice_id = chart.form_data.slice_id; +export const hydrateDashboard = (dashboardData, chartData) => ( + dispatch, + getState, +) => { + const { user, common, dashboardState } = getState(); + let { metadata } = dashboardData; + const regularUrlParams = extractUrlParams('regular'); + const reservedUrlParams = extractUrlParams('reserved'); + const editMode = reservedUrlParams.edit === 'true'; + + let preselectFilters = {}; + + chartData.forEach(chart => { + // eslint-disable-next-line no-param-reassign + chart.slice_id = chart.form_data.slice_id; + }); + try { + // allow request parameter overwrite dashboard metadata + preselectFilters = + getUrlParam(URL_PARAMS.preselectFilters) || + JSON.parse(metadata.default_filters); + } catch (e) { + // + } + + // Priming the color palette with user's label-color mapping provided in + // the dashboard's JSON metadata + if (metadata?.label_colors) { + const namespace = metadata.color_namespace; + const colorMap = isString(metadata.label_colors) + ? JSON.parse(metadata.label_colors) + : metadata.label_colors; + const categoricalNamespace = CategoricalColorNamespace.getNamespace( + namespace, + ); + + Object.keys(colorMap).forEach(label => { + categoricalNamespace.setColor(label, colorMap[label]); }); - try { - // allow request parameter overwrite dashboard metadata - preselectFilters = - getUrlParam(URL_PARAMS.preselectFilters) || - JSON.parse(metadata.default_filters); - } catch (e) { - // + } + + // 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 + : getEmptyLayout(); + + // create a lookup to sync layout names with slice names + const chartIdToLayoutId = {}; + Object.values(layout).forEach(layoutComponent => { + if (layoutComponent.type === CHART_TYPE) { + chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id; } + }); + + // find root level chart container node for newly-added slices + const parentId = findFirstParentContainerId(layout); + const parent = layout[parentId]; + let newSlicesContainer; + let newSlicesContainerWidth = 0; + + const filterScopes = metadata?.filter_scopes || {}; + + const chartQueries = {}; + const dashboardFilters = {}; + const slices = {}; + const sliceIds = new Set(); + chartData.forEach(slice => { + const key = slice.slice_id; + const form_data = { + ...slice.form_data, + url_params: { + ...slice.form_data.url_params, + ...regularUrlParams, + }, + }; + chartQueries[key] = { + ...chart, + id: key, + form_data, + formData: applyDefaultFormData(form_data), + }; - // Priming the color palette with user's label-color mapping provided in - // the dashboard's JSON metadata - if (metadata?.label_colors) { - const namespace = metadata.color_namespace; - const colorMap = isString(metadata.label_colors) - ? JSON.parse(metadata.label_colors) - : metadata.label_colors; - const categoricalNamespace = - CategoricalColorNamespace.getNamespace(namespace); - - Object.keys(colorMap).forEach(label => { - categoricalNamespace.setColor(label, colorMap[label]); - }); - } + slices[key] = { + slice_id: key, + slice_url: slice.slice_url, + slice_name: slice.slice_name, + form_data: slice.form_data, + viz_type: slice.form_data.viz_type, + datasource: slice.form_data.datasource, + description: slice.description, + description_markeddown: slice.description_markeddown, + owners: slice.owners, + modified: slice.modified, + changed_on: new Date(slice.changed_on).getTime(), + }; - // 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 - : getEmptyLayout(); - - // create a lookup to sync layout names with slice names - const chartIdToLayoutId = {}; - Object.values(layout).forEach(layoutComponent => { - if (layoutComponent.type === CHART_TYPE) { - chartIdToLayoutId[layoutComponent.meta.chartId] = layoutComponent.id; + sliceIds.add(key); + + // if there are newly added slices from explore view, fill slices into 1 or more rows + if (!chartIdToLayoutId[key] && layout[parentId]) { + if ( + newSlicesContainerWidth === 0 || + newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT + ) { + newSlicesContainer = newComponentFactory( + ROW_TYPE, + (parent.parents || []).slice(), + ); + layout[newSlicesContainer.id] = newSlicesContainer; + parent.children.push(newSlicesContainer.id); + newSlicesContainerWidth = 0; } - }); - // find root level chart container node for newly-added slices - const parentId = findFirstParentContainerId(layout); - const parent = layout[parentId]; - let newSlicesContainer; - let newSlicesContainerWidth = 0; - - const filterScopes = metadata?.filter_scopes || {}; - - const chartQueries = {}; - const dashboardFilters = {}; - const slices = {}; - const sliceIds = new Set(); - chartData.forEach(slice => { - const key = slice.slice_id; - const form_data = { - ...slice.form_data, - url_params: { - ...slice.form_data.url_params, - ...regularUrlParams, + const chartHolder = newComponentFactory( + CHART_TYPE, + { + chartId: slice.slice_id, }, - }; - chartQueries[key] = { - ...chart, - id: key, - form_data, - formData: applyDefaultFormData(form_data), - }; + (newSlicesContainer.parents || []).slice(), + ); - slices[key] = { - slice_id: key, - slice_url: slice.slice_url, - slice_name: slice.slice_name, - form_data: slice.form_data, - viz_type: slice.form_data.viz_type, - datasource: slice.form_data.datasource, - description: slice.description, - description_markeddown: slice.description_markeddown, - owners: slice.owners, - modified: slice.modified, - changed_on: new Date(slice.changed_on).getTime(), - }; - - sliceIds.add(key); - - // if there are newly added slices from explore view, fill slices into 1 or more rows - if (!chartIdToLayoutId[key] && layout[parentId]) { - if ( - newSlicesContainerWidth === 0 || - newSlicesContainerWidth + GRID_DEFAULT_CHART_WIDTH > GRID_COLUMN_COUNT - ) { - newSlicesContainer = newComponentFactory( - ROW_TYPE, - (parent.parents || []).slice(), - ); - layout[newSlicesContainer.id] = newSlicesContainer; - parent.children.push(newSlicesContainer.id); - newSlicesContainerWidth = 0; - } - - const chartHolder = newComponentFactory( - CHART_TYPE, - { - chartId: slice.slice_id, - }, - (newSlicesContainer.parents || []).slice(), - ); + layout[chartHolder.id] = chartHolder; + newSlicesContainer.children.push(chartHolder.id); + chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id; + newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH; + } - layout[chartHolder.id] = chartHolder; - newSlicesContainer.children.push(chartHolder.id); - chartIdToLayoutId[chartHolder.meta.chartId] = chartHolder.id; - newSlicesContainerWidth += GRID_DEFAULT_CHART_WIDTH; + // build DashboardFilters for interactive filter features + if (slice.form_data.viz_type === 'filter_box') { + const configs = getFilterConfigsFromFormdata(slice.form_data); + let { columns } = configs; + const { labels } = configs; + if (preselectFilters[key]) { + Object.keys(columns).forEach(col => { + if (preselectFilters[key][col]) { + columns = { + ...columns, + [col]: preselectFilters[key][col], + }; + } + }); } - // build DashboardFilters for interactive filter features - if (slice.form_data.viz_type === 'filter_box') { - const configs = getFilterConfigsFromFormdata(slice.form_data); - let { columns } = configs; - const { labels } = configs; - if (preselectFilters[key]) { - Object.keys(columns).forEach(col => { - if (preselectFilters[key][col]) { - columns = { - ...columns, - [col]: preselectFilters[key][col], - }; - } - }); - } - - const scopesByChartId = Object.keys(columns).reduce((map, column) => { - const scopeSettings = { - ...filterScopes[key], - }; - const { scope, immune } = { - ...DASHBOARD_FILTER_SCOPE_GLOBAL, - ...scopeSettings[column], - }; - - return { - ...map, - [column]: { - scope, - immune, - }, - }; - }, {}); - - const componentId = chartIdToLayoutId[key]; - const directPathToFilter = (layout[componentId].parents || []).slice(); - directPathToFilter.push(componentId); - dashboardFilters[key] = { - ...dashboardFilter, - chartId: key, - componentId, - datasourceId: slice.form_data.datasource, - filterName: slice.slice_name, - directPathToFilter, - columns, - labels, - scopes: scopesByChartId, - isInstantFilter: !!slice.form_data.instant_filtering, - isDateFilter: Object.keys(columns).includes(TIME_RANGE), + const scopesByChartId = Object.keys(columns).reduce((map, column) => { + const scopeSettings = { + ...filterScopes[key], + }; + const { scope, immune } = { + ...DASHBOARD_FILTER_SCOPE_GLOBAL, + ...scopeSettings[column], }; - } - - // sync layout names with current slice names in case a slice was edited - // in explore since the layout was updated. name updates go through layout for undo/redo - // functionality and python updates slice names based on layout upon dashboard save - const layoutId = chartIdToLayoutId[key]; - if (layoutId && layout[layoutId]) { - layout[layoutId].meta.sliceName = slice.slice_name; - } - }); - buildActiveFilters({ - dashboardFilters, - components: layout, - }); - - // store the header as a layout component so we can undo/redo changes - layout[DASHBOARD_HEADER_ID] = { - id: DASHBOARD_HEADER_ID, - type: DASHBOARD_HEADER_TYPE, - meta: { - text: dashboardData.dashboard_title, - }, - }; - - const dashboardLayout = { - past: [], - present: layout, - future: [], - }; - // find direct link component and path from root - const directLinkComponentId = getLocationHash(); - let directPathToChild = []; - if (layout[directLinkComponentId]) { - directPathToChild = (layout[directLinkComponentId].parents || []).slice(); - directPathToChild.push(directLinkComponentId); + return { + ...map, + [column]: { + scope, + immune, + }, + }; + }, {}); + + const componentId = chartIdToLayoutId[key]; + const directPathToFilter = (layout[componentId].parents || []).slice(); + directPathToFilter.push(componentId); + dashboardFilters[key] = { + ...dashboardFilter, + chartId: key, + componentId, + datasourceId: slice.form_data.datasource, + filterName: slice.slice_name, + directPathToFilter, + columns, + labels, + scopes: scopesByChartId, + isInstantFilter: !!slice.form_data.instant_filtering, + isDateFilter: Object.keys(columns).includes(TIME_RANGE), + }; } - const nativeFilters = getInitialNativeFilterState({ - filterConfig: metadata?.native_filter_configuration || [], - filterSetsConfig: metadata?.filter_sets_configuration || [], - }); - - if (!metadata) { - metadata = {}; + // sync layout names with current slice names in case a slice was edited + // in explore since the layout was updated. name updates go through layout for undo/redo + // functionality and python updates slice names based on layout upon dashboard save + const layoutId = chartIdToLayoutId[key]; + if (layoutId && layout[layoutId]) { + layout[layoutId].meta.sliceName = slice.slice_name; } + }); + buildActiveFilters({ + dashboardFilters, + components: layout, + }); + + // store the header as a layout component so we can undo/redo changes + layout[DASHBOARD_HEADER_ID] = { + id: DASHBOARD_HEADER_ID, + type: DASHBOARD_HEADER_TYPE, + meta: { + text: dashboardData.dashboard_title, + }, + }; - metadata.show_native_filters = - dashboardData?.metadata?.show_native_filters ?? - isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS); - - if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { - // If user just added cross filter to dashboard it's not saving it scope on server, - // so we tweak it until user will update scope and will save it in server - Object.values(dashboardLayout.present).forEach(layoutItem => { - const chartId = layoutItem.meta?.chartId; - const behaviors = - ( - getChartMetadataRegistry().get( - chartQueries[chartId]?.formData?.viz_type, - ) ?? {} - )?.behaviors ?? []; - - if (!metadata.chart_configuration) { - metadata.chart_configuration = {}; - } - if ( - behaviors.includes(Behavior.INTERACTIVE_CHART) && - !metadata.chart_configuration[chartId] - ) { - metadata.chart_configuration[chartId] = { - id: chartId, - crossFilters: { - scope: { - rootPath: [DASHBOARD_ROOT_ID], - excluded: [chartId], // By default it doesn't affects itself - }, - }, - }; - } - }); - } + const dashboardLayout = { + past: [], + present: layout, + future: [], + }; - const { roles } = user; - const canEdit = canUserEditDashboard(dashboardData, user); - - return dispatch({ - type: HYDRATE_DASHBOARD, - data: { - sliceEntities: { ...initSliceEntities, slices, isLoading: false }, - charts: chartQueries, - // read-only data - dashboardInfo: { - ...dashboardData, - metadata, - userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead - dash_edit_perm: canEdit, - dash_save_perm: findPermission('can_save_dash', 'Superset', roles), - dash_share_perm: findPermission( - 'can_share_dashboard', - 'Superset', - roles, - ), - superset_can_explore: findPermission( - 'can_explore', - 'Superset', - roles, - ), - superset_can_share: findPermission( - 'can_share_chart', - 'Superset', - roles, - ), - superset_can_csv: findPermission('can_csv', 'Superset', roles), - slice_can_edit: findPermission('can_slice', 'Superset', roles), - common: { - // legacy, please use state.common instead - flash_messages: common.flash_messages, - conf: common.conf, + // find direct link component and path from root + const directLinkComponentId = getLocationHash(); + let directPathToChild = []; + if (layout[directLinkComponentId]) { + directPathToChild = (layout[directLinkComponentId].parents || []).slice(); + directPathToChild.push(directLinkComponentId); + } + + const nativeFilters = getInitialNativeFilterState({ + filterConfig: metadata?.native_filter_configuration || [], + filterSetsConfig: metadata?.filter_sets_configuration || [], + }); + + if (!metadata) { + metadata = {}; + } + + metadata.show_native_filters = + dashboardData?.metadata?.show_native_filters ?? + isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS); + + if (isFeatureEnabled(FeatureFlag.DASHBOARD_CROSS_FILTERS)) { + // If user just added cross filter to dashboard it's not saving it scope on server, + // so we tweak it until user will update scope and will save it in server + Object.values(dashboardLayout.present).forEach(layoutItem => { + const chartId = layoutItem.meta?.chartId; + const behaviors = + ( + getChartMetadataRegistry().get( + chartQueries[chartId]?.formData?.viz_type, + ) ?? {} + )?.behaviors ?? []; + + if (!metadata.chart_configuration) { + metadata.chart_configuration = {}; + } + if ( + behaviors.includes(Behavior.INTERACTIVE_CHART) && + !metadata.chart_configuration[chartId] + ) { + metadata.chart_configuration[chartId] = { + id: chartId, + crossFilters: { + scope: { + rootPath: [DASHBOARD_ROOT_ID], + excluded: [chartId], // By default it doesn't affects itself + }, }, + }; + } + }); + } + + const { roles } = user; + const canEdit = canUserEditDashboard(dashboardData, user); + + return dispatch({ + type: HYDRATE_DASHBOARD, + data: { + sliceEntities: { ...initSliceEntities, slices, isLoading: false }, + charts: chartQueries, + // read-only data + dashboardInfo: { + ...dashboardData, + metadata, + userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead + dash_edit_perm: canEdit, + dash_save_perm: findPermission('can_save_dash', 'Superset', roles), + dash_share_perm: findPermission( + 'can_share_dashboard', + 'Superset', + roles, + ), + superset_can_explore: findPermission('can_explore', 'Superset', roles), + superset_can_share: findPermission( + 'can_share_chart', + 'Superset', + roles, + ), + superset_can_csv: findPermission('can_csv', 'Superset', roles), + slice_can_edit: findPermission('can_slice', 'Superset', roles), + common: { + // legacy, please use state.common instead + flash_messages: common.flash_messages, + conf: common.conf, }, - dashboardFilters, - nativeFilters, - dashboardState: { - preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters), - sliceIds: Array.from(sliceIds), - directPathToChild, - directPathLastUpdated: Date.now(), - focusedFilterField: null, - expandedSlices: metadata?.expanded_slices || {}, - refreshFrequency: metadata?.refresh_frequency || 0, - // dashboard viewers can set refresh frequency for the current visit, - // only persistent refreshFrequency will be saved to backend - shouldPersistRefreshFrequency: false, - css: dashboardData.css || '', - colorNamespace: metadata?.color_namespace || null, - colorScheme: metadata?.color_scheme || null, - editMode: canEdit && editMode, - isPublished: dashboardData.published, - hasUnsavedChanges: false, - maxUndoHistoryExceeded: false, - lastModifiedTime: dashboardData.changed_on, - isRefreshing: false, - activeTabs: dashboardState?.activeTabs || [], - }, - dashboardLayout, }, - }); - }; + dashboardFilters, + nativeFilters, + dashboardState: { + preselectNativeFilters: getUrlParam(URL_PARAMS.nativeFilters), + sliceIds: Array.from(sliceIds), + directPathToChild, + directPathLastUpdated: Date.now(), + focusedFilterField: null, + expandedSlices: metadata?.expanded_slices || {}, + refreshFrequency: metadata?.refresh_frequency || 0, + // dashboard viewers can set refresh frequency for the current visit, + // only persistent refreshFrequency will be saved to backend + shouldPersistRefreshFrequency: false, + css: dashboardData.css || '', + colorNamespace: metadata?.color_namespace || null, + colorScheme: metadata?.color_scheme || null, + editMode: canEdit && editMode, + isPublished: dashboardData.published, + hasUnsavedChanges: false, + maxUndoHistoryExceeded: false, + lastModifiedTime: dashboardData.changed_on, + isRefreshing: false, + activeTabs: dashboardState?.activeTabs || [], + }, + dashboardLayout, + }, + }); +};