| /** |
| * 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, keyBy } from 'lodash'; |
| import shortid from 'shortid'; |
| import { CategoricalColorNamespace } from '@superset-ui/core'; |
| import querystring from 'query-string'; |
| |
| import { chart } from 'src/chart/chartReducer'; |
| import { initSliceEntities } from 'src/dashboard/reducers/sliceEntities'; |
| import { getInitialState as getInitialNativeFilterState } from 'src/dashboard/reducers/nativeFilters'; |
| import { getParam } from 'src/modules/utils'; |
| import { applyDefaultFormData } from 'src/explore/store'; |
| import { buildActiveFilters } from 'src/dashboard/util/activeDashboardFilters'; |
| import getPermissions from 'src/dashboard/util/getPermissions'; |
| import { |
| DASHBOARD_FILTER_SCOPE_GLOBAL, |
| dashboardFilter, |
| } from 'src/dashboard/reducers/dashboardFilters'; |
| import { |
| DASHBOARD_HEADER_ID, |
| GRID_DEFAULT_CHART_WIDTH, |
| GRID_COLUMN_COUNT, |
| } 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'; |
| |
| const reservedQueryParams = new Set(['standalone', 'edit']); |
| |
| /** |
| * Returns the url params that are used to customize queries |
| * in datasets built using sql lab. |
| * We may want to extract this to some kind of util in the future. |
| */ |
| const extractUrlParams = queryParams => |
| Object.entries(queryParams).reduce((acc, [key, value]) => { |
| if (reservedQueryParams.has(key)) return acc; |
| // if multiple url params share the same key (?foo=bar&foo=baz), they will appear as an array. |
| // Only one value can be used for a given query param, so we just take the first one. |
| if (Array.isArray(value)) { |
| return { |
| ...acc, |
| [key]: value[0], |
| }; |
| } |
| return { ...acc, [key]: value }; |
| }, {}); |
| |
| export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD'; |
| |
| export const hydrateDashboard = (dashboardData, chartData, datasourcesData) => ( |
| dispatch, |
| getState, |
| ) => { |
| const { user, common } = getState(); |
| const { metadata } = dashboardData; |
| const queryParams = querystring.parse(window.location.search); |
| const urlParams = extractUrlParams(queryParams); |
| const editMode = queryParams.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 = JSON.parse( |
| getParam('preselect_filters') || 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, |
| ...urlParams, |
| }, |
| }; |
| 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' || |
| slice.form_data.viz_type === 'filter_select' |
| ) { |
| 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 || [], |
| }); |
| |
| const { roles } = getState().user; |
| |
| return dispatch({ |
| type: HYDRATE_DASHBOARD, |
| data: { |
| datasources: keyBy(datasourcesData, 'uid'), |
| sliceEntities: { ...initSliceEntities, slices, isLoading: false }, |
| charts: chartQueries, |
| // read-only data |
| dashboardInfo: { |
| ...dashboardData, |
| userId: String(user.userId), // legacy, please use state.user instead |
| dash_edit_perm: getPermissions('can_write', 'Dashboard', roles), |
| dash_save_perm: getPermissions('can_save_dash', 'Superset', roles), |
| dash_share_perm: getPermissions( |
| 'can_share_dashboard', |
| 'Superset', |
| roles, |
| ), |
| superset_can_explore: getPermissions('can_explore', 'Superset', roles), |
| superset_can_share: getPermissions( |
| 'can_share_chart', |
| 'Superset', |
| roles, |
| ), |
| superset_can_csv: getPermissions('can_csv', 'Superset', roles), |
| slice_can_edit: getPermissions('can_slice', 'Superset', roles), |
| common: { |
| // legacy, please use state.common instead |
| flash_messages: common.flash_messages, |
| conf: common.conf, |
| }, |
| }, |
| dashboardFilters, |
| nativeFilters, |
| dashboardState: { |
| 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: getPermissions('can_write', 'Dashboard', roles) && editMode, |
| isPublished: dashboardData.published, |
| hasUnsavedChanges: false, |
| maxUndoHistoryExceeded: false, |
| lastModifiedTime: dashboardData.changed_on, |
| }, |
| dashboardLayout, |
| messageToasts: [], |
| impressionId: shortid.generate(), |
| }, |
| }); |
| }; |