superset hydrate 源码

  • 2022-10-20
  • 浏览 (245)

superset hydrate 代码

文件路径:/superset-frontend/src/dashboard/actions/hydrate.js

/**
 * 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 { Behavior, getChartMetadataRegistry } from '@superset-ui/core';

import { chart } from 'src/components/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 } from 'src/utils/findPermission';
import { canUserEditDashboard } from 'src/dashboard/util/permissionUtils';
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 { FILTER_BOX_MIGRATION_STATES } from 'src/explore/constants';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { FeatureFlag, isFeatureEnabled } from '../../featureFlags';
import extractUrlParams from '../util/extractUrlParams';
import getNativeFilterConfig from '../util/filterboxMigrationHelper';
import { updateColorSchema } from './dashboardInfo';
import { getChartIdsInFilterScope } from '../util/getChartIdsInFilterScope';
import updateComponentParentsList from '../util/updateComponentParentsList';

export const HYDRATE_DASHBOARD = 'HYDRATE_DASHBOARD';

export const hydrateDashboard =
  ({
    history,
    dashboard,
    charts,
    filterboxMigrationState = FILTER_BOX_MIGRATION_STATES.NOOP,
    dataMask,
    activeTabs,
  }) =>
  (dispatch, getState) => {
    const { user, common, dashboardState } = getState();
    const { metadata, position_data: positionData } = dashboard;
    const regularUrlParams = extractUrlParams('regular');
    const reservedUrlParams = extractUrlParams('reserved');
    const editMode = reservedUrlParams.edit === 'true';

    let preselectFilters = {};

    charts.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) {
      //
    }

    if (metadata?.shared_label_colors) {
      updateColorSchema(metadata, metadata?.shared_label_colors);
    }

    // Priming the color palette with user's label-color mapping provided in
    // the dashboard's JSON metadata
    if (metadata?.label_colors) {
      updateColorSchema(metadata, metadata?.label_colors);
    }

    // new dash: position_json could be {} or null
    const layout =
      positionData && Object.keys(positionData).length > 0
        ? positionData
        : 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();
    const slicesFromExploreCount = new Map();

    charts.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: 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(),
        );

        const count = (slicesFromExploreCount.get(slice.slice_id) ?? 0) + 1;
        chartHolder.id = `${CHART_TYPE}-explore-${slice.slice_id}-${count}`;
        slicesFromExploreCount.set(slice.slice_id, count);

        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);
        if (
          [
            FILTER_BOX_MIGRATION_STATES.NOOP,
            FILTER_BOX_MIGRATION_STATES.SNOOZED,
          ].includes(filterboxMigrationState)
        ) {
          dashboardFilters[key] = {
            ...dashboardFilter,
            chartId: key,
            componentId,
            datasourceId: slice.form_data.datasource,
            filterName: slice.slice_name,
            directPathToFilter,
            columns,
            labels,
            scopes: scopesByChartId,
            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;
      }
    });

    // make sure that parents tree is built
    if (
      Object.values(layout).some(
        element => element.id !== DASHBOARD_ROOT_ID && !element.parents,
      )
    ) {
      updateComponentParentsList({
        currentComponent: layout[DASHBOARD_ROOT_ID],
        layout,
      });
    }

    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: dashboard.dashboard_title,
      },
    };

    const dashboardLayout = {
      past: [],
      present: layout,
      future: [],
    };

    // Searches for a focused_chart parameter in the URL to automatically focus a chart
    const focusedChartId = getUrlParam(URL_PARAMS.dashboardFocusedChart);
    let focusedChartLayoutId;
    if (focusedChartId) {
      // Converts focused_chart to dashboard layout id
      const found = Object.values(dashboardLayout.present).find(
        element => element.meta?.chartId === focusedChartId,
      );
      focusedChartLayoutId = found?.id;
      // Removes the focused_chart parameter from the URL
      const params = new URLSearchParams(window.location.search);
      params.delete(URL_PARAMS.dashboardFocusedChart.name);
      history.replace({
        search: params.toString(),
      });
    }

    // find direct link component and path from root
    const directLinkComponentId = focusedChartLayoutId || getLocationHash();
    let directPathToChild = dashboardState.directPathToChild || [];
    if (layout[directLinkComponentId]) {
      directPathToChild = (layout[directLinkComponentId].parents || []).slice();
      directPathToChild.push(directLinkComponentId);
    }

    // should convert filter_box to filter component?
    let filterConfig = metadata?.native_filter_configuration || [];
    if (filterboxMigrationState === FILTER_BOX_MIGRATION_STATES.REVIEWING) {
      filterConfig = getNativeFilterConfig(
        charts,
        filterScopes,
        preselectFilters,
      );
      metadata.native_filter_configuration = filterConfig;
      metadata.show_native_filters = true;
    }
    const nativeFilters = getInitialNativeFilterState({
      filterConfig,
    });
    metadata.show_native_filters =
      dashboard?.metadata?.show_native_filters ??
      (isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) &&
        [
          FILTER_BOX_MIGRATION_STATES.CONVERTED,
          FILTER_BOX_MIGRATION_STATES.REVIEWING,
          FILTER_BOX_MIGRATION_STATES.NOOP,
        ].includes(filterboxMigrationState));

    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]?.form_data?.viz_type,
            ) ?? {}
          )?.behaviors ?? [];

        if (!metadata.chart_configuration) {
          metadata.chart_configuration = {};
        }
        if (behaviors.includes(Behavior.INTERACTIVE_CHART)) {
          if (!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
                },
              },
            };
          }
          metadata.chart_configuration[chartId].crossFilters.chartsInScope =
            getChartIdsInFilterScope(
              metadata.chart_configuration[chartId].crossFilters.scope,
              chartQueries,
              dashboardLayout.present,
            );
        }
        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(dashboard, user);

    return dispatch({
      type: HYDRATE_DASHBOARD,
      data: {
        sliceEntities: { ...initSliceEntities, slices, isLoading: false },
        charts: chartQueries,
        // read-only data
        dashboardInfo: {
          ...dashboard,
          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,
          },
        },
        dataMask,
        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: dashboard.css || '',
          colorNamespace: metadata?.color_namespace || null,
          colorScheme: metadata?.color_scheme || null,
          editMode: canEdit && editMode,
          isPublished: dashboard.published,
          hasUnsavedChanges: false,
          maxUndoHistoryExceeded: false,
          lastModifiedTime: dashboard.changed_on,
          isRefreshing: false,
          isFiltersRefreshing: false,
          activeTabs: activeTabs || dashboardState?.activeTabs || [],
          filterboxMigrationState,
          datasetsStatus: ResourceStatus.LOADING,
        },
        dashboardLayout,
      },
    });
  };

相关信息

superset 源码目录

相关文章

superset dashboardFilters 源码

superset dashboardInfo 源码

superset dashboardLayout 源码

superset dashboardState 源码

superset datasources 源码

superset nativeFilters 源码

superset sliceEntities 源码

0  赞