airbnb/caravel

View on GitHub
superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx

Summary

Maintainability
B
5 hrs
Test Coverage
/* eslint-disable camelcase */
/**
 * 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.
 */

/**
 * This file exports all controls available for use in chart plugins internal to Superset.
 * It is not recommended to use the controls here for any third-party plugins.
 *
 * While the React components located in `controls/components` represent different
 * types of controls (CheckboxControl, SelectControl, TextControl, ...), the controls here
 * represent instances of control types, that can be reused across visualization types.
 *
 * When controls are reused across viz types, their values are carried over as a user
 * changes the chart types.
 *
 * While the keys defined in the control itself get passed to the controlType as props,
 * here's a list of the keys that are common to all controls, and as a result define the
 * control interface.
 */
import { isEmpty } from 'lodash';
import {
  t,
  getCategoricalSchemeRegistry,
  getSequentialSchemeRegistry,
  SequentialScheme,
  legacyValidateInteger,
  ComparisonType,
  ensureIsArray,
  isDefined,
  NO_TIME_RANGE,
  validateMaxValue,
} from '@superset-ui/core';

import {
  formatSelectOptions,
  displayTimeRelatedControls,
  D3_FORMAT_OPTIONS,
  D3_FORMAT_DOCS,
  D3_TIME_FORMAT_OPTIONS,
  D3_TIME_FORMAT_DOCS,
  DEFAULT_TIME_FORMAT,
  DEFAULT_NUMBER_FORMAT,
} from '../utils';
import { DEFAULT_MAX_ROW, TIME_FILTER_LABELS } from '../constants';
import {
  SharedControlConfig,
  Dataset,
  ControlState,
  ControlPanelState,
} from '../types';

import {
  dndAdhocFilterControl,
  dndAdhocMetricControl,
  dndAdhocMetricsControl,
  dndGranularitySqlaControl,
  dndSortByControl,
  dndSecondaryMetricControl,
  dndSizeControl,
  dndXControl,
  dndYControl,
  dndColumnsControl,
  dndEntityControl,
  dndGroupByControl,
  dndSeriesControl,
  dndAdhocMetricControl2,
  dndXAxisControl,
} from './dndControls';

const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const sequentialSchemeRegistry = getSequentialSchemeRegistry();

export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 };

const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000];
const SERIES_LIMITS = [5, 10, 25, 50, 100, 500];

const appContainer = document.getElementById('app');
const { user } = JSON.parse(
  appContainer?.getAttribute('data-bootstrap') || '{}',
);

type SelectDefaultOption = {
  label: string;
  value: string;
};

const datasourceControl: SharedControlConfig<'DatasourceControl'> = {
  type: 'DatasourceControl',
  label: t('Datasource'),
  default: null,
  description: null,
  mapStateToProps: ({ datasource, form_data }) => ({
    datasource,
    form_data,
    user,
  }),
};

const viz_type: SharedControlConfig<'VizTypeControl'> = {
  type: 'VizTypeControl',
  label: t('Visualization Type'),
  default: 'table',
  description: t('The type of visualization to display'),
};

const color_picker: SharedControlConfig<'ColorPickerControl'> = {
  type: 'ColorPickerControl',
  label: t('Fixed Color'),
  description: t('Use this to define a static color for all circles'),
  default: PRIMARY_COLOR,
  renderTrigger: true,
};

const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
  type: 'ColorSchemeControl',
  label: t('Linear Color Scheme'),
  choices: () =>
    (sequentialSchemeRegistry.values() as SequentialScheme[]).map(value => [
      value.id,
      value.label,
    ]),
  default: sequentialSchemeRegistry.getDefaultKey(),
  clearable: false,
  description: '',
  renderTrigger: true,
  schemes: () => sequentialSchemeRegistry.getMap(),
  isLinear: true,
  mapStateToProps: state => ({
    dashboardId: state?.form_data?.dashboardId,
  }),
};

const granularity: SharedControlConfig<'SelectControl'> = {
  type: 'SelectControl',
  freeForm: true,
  label: TIME_FILTER_LABELS.granularity,
  default: 'one day',
  choices: [
    [null, t('all')],
    ['PT5S', t('5 seconds')],
    ['PT30S', t('30 seconds')],
    ['PT1M', t('1 minute')],
    ['PT5M', t('5 minutes')],
    ['PT30M', t('30 minutes')],
    ['PT1H', t('1 hour')],
    ['PT6H', t('6 hour')],
    ['P1D', t('1 day')],
    ['P7D', t('7 days')],
    ['P1W', t('week')],
    ['week_starting_sunday', t('week starting Sunday')],
    ['week_ending_saturday', t('week ending Saturday')],
    ['P1M', t('month')],
    ['P3M', t('quarter')],
    ['P1Y', t('year')],
  ],
  description: t(
    'The time granularity for the visualization. Note that you ' +
      'can type and use simple natural language as in `10 seconds`, ' +
      '`1 day` or `56 weeks`',
  ),
};

const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
  type: 'SelectControl',
  label: TIME_FILTER_LABELS.time_grain_sqla,
  placeholder: t('None'),
  initialValue: (control: ControlState, state: ControlPanelState) => {
    if (!isDefined(state)) {
      // If a chart is in a Dashboard, the ControlPanelState is empty.
      return control.value;
    }
    // If a chart is a new one that isn't saved, metadata is null. In this
    // case we want to default P1D. If the chart has been saved, we want
    // to use whichever value was chosen, either nothing or valid a time grain.
    return state?.metadata || 'time_grain_sqla' in (state?.form_data ?? {})
      ? state?.form_data?.time_grain_sqla
      : 'P1D';
  },
  description: t(
    'Select a time grain for the visualization. The ' +
      'grain is the time interval represented by a ' +
      'single point on the chart.',
  ),
  mapStateToProps: ({ datasource }) => ({
    choices: (datasource as Dataset)?.time_grain_sqla || [],
  }),
  visibility: displayTimeRelatedControls,
};

const time_range: SharedControlConfig<'DateFilterControl'> = {
  type: 'DateFilterControl',
  freeForm: true,
  label: TIME_FILTER_LABELS.time_range,
  default: NO_TIME_RANGE, // this value is an empty filter constant so shouldn't translate it.
  description: t(
    'This control filters the whole chart based on the selected time range. All relative times, e.g. "Last month", ' +
      '"Last 7 days", "now", etc. are evaluated on the server using the server\'s ' +
      'local time (sans timezone). All tooltips and placeholder times are expressed ' +
      'in UTC (sans timezone). The timestamps are then evaluated by the database ' +
      "using the engine's local timezone. Note one can explicitly set the timezone " +
      'per the ISO 8601 format if specifying either the start and/or end time.',
  ),
};

const row_limit: SharedControlConfig<'SelectControl'> = {
  type: 'SelectControl',
  freeForm: true,
  label: t('Row limit'),
  clearable: false,
  mapStateToProps: state => ({ maxValue: state?.common?.conf?.SQL_MAX_ROW }),
  validators: [
    legacyValidateInteger,
    (v, state) => validateMaxValue(v, state?.maxValue || DEFAULT_MAX_ROW),
  ],
  default: 10000,
  choices: formatSelectOptions(ROW_LIMIT_OPTIONS),
  description: t(
    'Limits the number of the rows that are computed in the query that is the source of the data used for this chart.',
  ),
};

const order_desc: SharedControlConfig<'CheckboxControl'> = {
  type: 'CheckboxControl',
  label: t('Sort Descending'),
  default: true,
  description: t(
    'If enabled, this control sorts the results/values descending, otherwise it sorts the results ascending.',
  ),
  visibility: ({ controls }) =>
    Boolean(
      controls?.timeseries_limit_metric.value &&
        !isEmpty(controls?.timeseries_limit_metric.value),
    ),
};

const limit: SharedControlConfig<'SelectControl'> = {
  type: 'SelectControl',
  freeForm: true,
  label: t('Series limit'),
  placeholder: t('None'),
  validators: [legacyValidateInteger],
  choices: formatSelectOptions(SERIES_LIMITS),
  clearable: true,
  description: t(
    'Limits the number of series that get displayed. A joined subquery (or an extra phase ' +
      'where subqueries are not supported) is applied to limit the number of series that get ' +
      'fetched and rendered. This feature is useful when grouping by high cardinality ' +
      'column(s) though does increase the query complexity and cost.',
  ),
};

const series_limit: SharedControlConfig<'SelectControl'> = {
  type: 'SelectControl',
  freeForm: true,
  label: t('Series limit'),
  placeholder: t('None'),
  validators: [legacyValidateInteger],
  choices: formatSelectOptions(SERIES_LIMITS),
  description: t(
    'Limits the number of series that get displayed. A joined subquery (or an extra phase ' +
      'where subqueries are not supported) is applied to limit the number of series that get ' +
      'fetched and rendered. This feature is useful when grouping by high cardinality ' +
      'column(s) though does increase the query complexity and cost.',
  ),
};

const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> =
  {
    type: 'SelectControl',
    freeForm: true,
    label: t('Y Axis Format'),
    renderTrigger: true,
    default: DEFAULT_NUMBER_FORMAT,
    choices: D3_FORMAT_OPTIONS,
    description: D3_FORMAT_DOCS,
    tokenSeparators: ['\n', '\t', ';'],
    filterOption: ({ data: option }, search) =>
      option.label.includes(search) || option.value.includes(search),
    mapStateToProps: state => {
      const isPercentage =
        state.controls?.comparison_type?.value === ComparisonType.Percentage;
      return {
        choices: isPercentage
          ? D3_FORMAT_OPTIONS.filter(option => option[0].includes('%'))
          : D3_FORMAT_OPTIONS,
      };
    },
  };

const currency_format: SharedControlConfig<'CurrencyControl'> = {
  type: 'CurrencyControl',
  label: t('Currency format'),
  renderTrigger: true,
};

const x_axis_time_format: SharedControlConfig<
  'SelectControl',
  SelectDefaultOption
> = {
  type: 'SelectControl',
  freeForm: true,
  label: t('Time format'),
  renderTrigger: true,
  default: DEFAULT_TIME_FORMAT,
  choices: D3_TIME_FORMAT_OPTIONS,
  description: D3_TIME_FORMAT_DOCS,
  filterOption: ({ data: option }, search) =>
    option.label.includes(search) || option.value.includes(search),
};

const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
  type: 'ColorSchemeControl',
  label: t('Color Scheme'),
  default: categoricalSchemeRegistry.getDefaultKey(),
  renderTrigger: true,
  choices: () => categoricalSchemeRegistry.keys().map(s => [s, s]),
  description: t('The color scheme for rendering chart'),
  schemes: () => categoricalSchemeRegistry.getMap(),
  mapStateToProps: state => ({
    dashboardId: state?.form_data?.dashboardId,
  }),
};

const truncate_metric: SharedControlConfig<'CheckboxControl'> = {
  type: 'CheckboxControl',
  label: t('Truncate Metric'),
  default: true,
  description: t('Whether to truncate metrics'),
};

const show_empty_columns: SharedControlConfig<'CheckboxControl'> = {
  type: 'CheckboxControl',
  label: t('Show empty columns'),
  default: true,
  description: t('Show empty columns'),
};

const temporal_columns_lookup: SharedControlConfig<'HiddenControl'> = {
  type: 'HiddenControl',
  initialValue: (control: ControlState, state: ControlPanelState | null) =>
    Object.fromEntries(
      ensureIsArray<Record<string, any>>(state?.datasource?.columns)
        .filter(option => option.is_dttm)
        .map(option => [option.column_name ?? option.name, option.is_dttm]),
    ),
};

const sort_by_metric: SharedControlConfig<'CheckboxControl'> = {
  type: 'CheckboxControl',
  label: t('Sort by metric'),
  description: t(
    'Whether to sort results by the selected metric in descending order.',
  ),
};

export default {
  metrics: dndAdhocMetricsControl,
  metric: dndAdhocMetricControl,
  datasource: datasourceControl,
  viz_type,
  color_picker,
  metric_2: dndAdhocMetricControl2,
  linear_color_scheme,
  secondary_metric: dndSecondaryMetricControl,
  groupby: dndGroupByControl,
  columns: dndColumnsControl,
  granularity,
  granularity_sqla: dndGranularitySqlaControl,
  time_grain_sqla,
  time_range,
  row_limit,
  limit,
  timeseries_limit_metric: dndSortByControl,
  orderby: dndSortByControl,
  order_desc,
  series: dndSeriesControl,
  entity: dndEntityControl,
  x: dndXControl,
  y: dndYControl,
  size: dndSizeControl,
  y_axis_format,
  x_axis_time_format,
  adhoc_filters: dndAdhocFilterControl,
  color_scheme,
  series_columns: dndColumnsControl,
  series_limit,
  series_limit_metric: dndSortByControl,
  legacy_order_by: dndSortByControl,
  truncate_metric,
  x_axis: dndXAxisControl,
  show_empty_columns,
  temporal_columns_lookup,
  currency_format,
  sort_by_metric,
};