airbnb/caravel

View on GitHub
superset-frontend/src/explore/controlUtils/standardizedFormData.ts

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * 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.
 */
import { omit } from 'lodash';
import {
  ensureIsArray,
  getChartControlPanelRegistry,
  QueryFormColumn,
  QueryFormData,
  QueryFormMetric,
} from '@superset-ui/core';
import {
  ControlStateMapping,
  getStandardizedControls,
  isStandardizedFormData,
  StandardizedControls,
  StandardizedFormDataInterface,
} from '@superset-ui/chart-controls';
import { getControlsState } from 'src/explore/store';
import { getFormDataFromControls } from './getFormDataFromControls';

export const sharedMetricsKey = [
  'metric', // via sharedControls, scalar
  'metrics', // via sharedControls, array
  'metric_2', // via sharedControls, scalar
  'size', // via sharedControls, scalar
  'x', // via sharedControls, scalar
  'y', // via sharedControls, scalar
  'secondary_metric', // via sharedControls, scalar
];
export const sharedColumnsKey = [
  'groupby', // via sharedControls, array
  'columns', // via sharedControls, array
  'groupbyColumns', // via pivot table v2, array
  'groupbyRows', // via pivot table v2, array
  'entity', // via sharedControls, scalar
  'series', // via sharedControls, scalar
  'series_columns', // via sharedControls, array
];

export const publicControls = [
  // time section
  'granularity_sqla', // via sharedControls
  'time_grain_sqla', // via sharedControls
  'time_range', // via sharedControls
  // filters
  'adhoc_filters', // via sharedControls
  // subquery limit(series limit)
  'limit', // via sharedControls
  // order by clause
  'timeseries_limit_metric', // via sharedControls
  'series_limit_metric', // via sharedControls
  // desc or asc in order by clause
  'order_desc', // via sharedControls
  // outer query limit
  'row_limit', // via sharedControls
  // x asxs column
  'x_axis', // via sharedControls
  // advanced analytics - rolling window
  'rolling_type', // via sections.advancedAnalytics
  'rolling_periods', // via sections.advancedAnalytics
  'min_periods', // via sections.advancedAnalytics
  // advanced analytics - time comparison
  'time_compare', // via sections.advancedAnalytics
  'comparison_type', // via sections.advancedAnalytics
  // advanced analytics - resample
  'resample_rule', // via sections.advancedAnalytics
  'resample_method', // via sections.advancedAnalytics
];

export class StandardizedFormData {
  private sfd: StandardizedFormDataInterface;

  constructor(sourceFormData: QueryFormData) {
    /*
     * Support form_data for smooth switching between different viz
     * */
    const formData = Object.freeze(sourceFormData);

    // generates an ordered map, the key is viz_type and the value is form_data. the last item is current viz.
    const mfd = formData?.standardizedFormData?.memorizedFormData;
    const vizType = formData.viz_type;
    let memorizedFormData = new Map<string, QueryFormData>();
    let controls: StandardizedControls;
    if (
      Array.isArray(mfd) &&
      mfd.length > 0 &&
      formData.datasource === mfd.slice(-1)[0][1]?.datasource
    ) {
      memorizedFormData = new Map(
        formData.standardizedFormData.memorizedFormData,
      );
      if (memorizedFormData.has(vizType)) {
        memorizedFormData.delete(vizType);
      }
      memorizedFormData.set(vizType, formData);
      controls = StandardizedFormData.getStandardizedControls(formData);
    } else {
      // reset the `memorizedFormData` if a request between different datasource.
      const restFormData = omit(
        formData,
        'standardizedFormData',
      ) as QueryFormData;
      memorizedFormData.set(vizType, restFormData);
      controls = StandardizedFormData.getStandardizedControls(restFormData);
    }

    this.sfd = {
      controls,
      memorizedFormData,
    };
  }

  static getStandardizedControls(
    formData: QueryFormData,
  ): StandardizedControls {
    // 1. initial StandardizedControls
    const controls: StandardizedControls = {
      metrics: [],
      columns: [],
    };

    // 2. collect current sharedControls
    Object.entries(formData).forEach(([key, value]) => {
      if (sharedMetricsKey.includes(key)) {
        controls.metrics.push(...ensureIsArray<QueryFormMetric>(value));
      }
      if (sharedColumnsKey.includes(key)) {
        controls.columns.push(...ensureIsArray<QueryFormColumn>(value));
      }
    });

    // 3. append inherit sharedControls
    if (isStandardizedFormData(formData)) {
      const { metrics, columns } = formData.standardizedFormData.controls;
      controls.metrics.push(...metrics);
      controls.columns.push(...columns);
    }

    return controls;
  }

  private getLatestFormData(vizType: string): QueryFormData {
    if (this.has(vizType)) {
      return this.get(vizType);
    }
    return this.memorizedFormData.slice(-1)[0][1];
  }

  private get standardizedControls() {
    return this.sfd.controls;
  }

  private get memorizedFormData() {
    return Array.from(this.sfd.memorizedFormData.entries());
  }

  serialize() {
    return {
      controls: this.standardizedControls,
      memorizedFormData: this.memorizedFormData,
    };
  }

  has(vizType: string): boolean {
    return this.sfd.memorizedFormData.has(vizType);
  }

  get(vizType: string): QueryFormData {
    return this.sfd.memorizedFormData.get(vizType) as QueryFormData;
  }

  transform(
    targetVizType: string,
    exploreState: Record<string, any>,
  ): {
    formData: QueryFormData;
    controlsState: ControlStateMapping;
  } {
    /*
     * Transform form_data between different viz. Return new form_data and controlsState.
     * 1. get memorized form_data by viz type or get previous form_data
     * 2. collect public control values
     * 3. generate initial targetControlsState
     * 4. attach `standardizedFormData` to the initial form_data
     * 5. call formDataOverrides to transform initial form_data if the plugin was defined
     * 6. use final form_data to generate controlsState
     * 7. to refresh validator message
     * */
    const latestFormData = this.getLatestFormData(targetVizType);
    const publicFormData = {};
    publicControls.forEach(key => {
      if (key in exploreState.form_data) {
        publicFormData[key] = exploreState.form_data[key];
      }
    });
    const targetControlsState = getControlsState(exploreState, {
      ...latestFormData,
      ...publicFormData,
      viz_type: targetVizType,
    });
    const targetFormData = {
      ...getFormDataFromControls(targetControlsState),
      standardizedFormData: this.serialize(),
    };

    let rv = {
      formData: targetFormData,
      controlsState: targetControlsState,
    };

    const controlPanel = getChartControlPanelRegistry().get(targetVizType);
    if (controlPanel?.formDataOverrides) {
      getStandardizedControls().setStandardizedControls(targetFormData);
      const transformed = {
        ...controlPanel.formDataOverrides(targetFormData),
        standardizedFormData: {
          controls: { ...getStandardizedControls().controls },
          memorizedFormData: this.memorizedFormData,
        },
      };
      getStandardizedControls().clear();
      rv = {
        formData: transformed,
        controlsState: getControlsState(exploreState, transformed),
      };
    }

    // refresh validator message
    rv.controlsState = getControlsState(
      { ...exploreState, controls: rv.controlsState },
      rv.formData,
    );
    return rv;
  }
}