| /** |
| * 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. |
| */ |
| /* eslint-disable camelcase */ |
| import { isString } from 'lodash'; |
| import { |
| Behavior, |
| CategoricalColorNamespace, |
| getChartMetadataRegistry, |
| } from '@superset-ui/core'; |
| |
| import { chart } from 'src/chart/chartReducer'; |
| import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; |
| import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters'; |
| import { applyDefaultFormData } from 'src/explore/store'; |
| import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; |
| import findPermission, { |
| canUserEditDashboard, |
| } from 'src/dashboard/util/findPermission'; |
| import { |
| DASHBOARD_FILTER_SCOPE_GLOBAL, |
| dashboardFilter, |
| } from 'src/dashboard/reducers/dashboardFilters'; |
| import { |
| DASHBOARD_HEADER_ID, |
| GRID_DEFAULT_CHART_WIDTH, |
| GRID_COLUMN_COUNT, |
| DASHBOARD_ROOT_ID, |
| } from 'src/dashboard/util/constants'; |
| import { |
| DASHBOARD_HEADER_TYPE, |
| CHART_TYPE, |
| ROW_TYPE, |
| } from 'src/dashboard/util/componentTypes'; |
| import findFirstParentContainerId from 'src/dashboard/util/findFirstParentContainer'; |
| import getEmptyLayout from 'src/dashboard/util/getEmptyLayout'; |
| import getFilterConfigsFromFormdata from 'src/dashboard/util/getFilterConfigsFromFormdata'; |
| import getLocationHash from 'src/dashboard/util/getLocationHash'; |
| import newComponentFactory from 'src/dashboard/util/newComponentFactory'; |
| import { TIME_RANGE } from 'src/visualizations/FilterBox/FilterBox'; |
| import { URL_PARAMS } from 'src/constants'; |
| import { getUrlParam } from 'src/utils/urlUtils'; |
| import { FeatureFlag, isFeatureEnabled } from '../../featureFlags'; |
| import extractUrlParams from '../util/extractUrlParams'; |
| |
| export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; |
| |
| export const hydrateDashboard = (dashboardData, chartData) => ( |
| dispatch, |
| getState, |
| ) => { |
| const { user, common } = 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 scheme = metadata.color_scheme; |
| const namespace = metadata.color_namespace; |
| const colorMap = isString(metadata.label_colors) |
| ? JSON.parse(metadata.label_colors) |
| : metadata.label_colors; |
| Object.keys(colorMap).forEach(label => { |
| CategoricalColorNamespace.getScale(scheme, namespace).setColor( |
| label, |
| colorMap[label], |
| ); |
| }); |
| } |
| |
| // 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), |
| }; |
| |
| 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; |
| } |
| |
| // 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), |
| }; |
| } |
| |
| // 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); |
| } |
| |
| 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: String(user.userId), // 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: [], |
| }, |
| dashboardLayout, |
| }, |
| }); |
| }; |