diff --git a/.eslintrc.js b/.eslintrc.js index ec16c304e6..51b58fbc6f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -73,6 +73,7 @@ module.exports = { 'no-mixed-operators': 0, 'no-multi-assign': 0, 'no-multi-spaces': 0, + 'no-nested-ternary': 0, 'no-prototype-builtins': 0, 'no-restricted-properties': 0, 'no-restricted-imports': [ @@ -156,6 +157,7 @@ module.exports = { 'no-mixed-operators': 0, 'no-multi-assign': 0, 'no-multi-spaces': 0, + 'no-nested-ternary': 0, 'no-prototype-builtins': 0, 'no-restricted-properties': 0, 'no-shadow': 0, // re-enable up for discussion diff --git a/packages/superset-ui-chart-controls/package.json b/packages/superset-ui-chart-controls/package.json index ebaf5c4c6c..620ace4f6e 100644 --- a/packages/superset-ui-chart-controls/package.json +++ b/packages/superset-ui-chart-controls/package.json @@ -33,8 +33,9 @@ "peerDependencies": { "@types/react": "*", "@types/react-bootstrap": "*", - "antd": "^4.9.1", + "antd": "^4.9.4", "react": "^16.13.1", - "react-bootstrap": "^0.33.1" + "react-bootstrap": "^0.33.1", + "react-icons": "^4.2.0" } } diff --git a/packages/superset-ui-chart-controls/src/components/ColumnOption.tsx b/packages/superset-ui-chart-controls/src/components/ColumnOption.tsx index 8403ba38d6..4f69030f2f 100644 --- a/packages/superset-ui-chart-controls/src/components/ColumnOption.tsx +++ b/packages/superset-ui-chart-controls/src/components/ColumnOption.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; import { Tooltip } from './Tooltip'; -import { ColumnTypeLabel } from './ColumnTypeLabel'; +import { ColumnTypeLabel, LaxColumnType } from './ColumnTypeLabel'; import InfoTooltipWithTrigger from './InfoTooltipWithTrigger'; import { ColumnMeta } from '../types'; @@ -30,7 +30,7 @@ export type ColumnOptionProps = { export function ColumnOption({ column, showType = false }: ColumnOptionProps) { const hasExpression = column.expression && column.expression !== column.column_name; - let columnType = column.type; + let columnType: LaxColumnType | undefined = column.type; if (column.is_dttm) { columnType = 'time'; } else if (hasExpression) { diff --git a/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel.tsx b/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel.tsx index 215fefa988..bbf8c59b8b 100644 --- a/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel.tsx +++ b/packages/superset-ui-chart-controls/src/components/ColumnTypeLabel.tsx @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -16,14 +17,31 @@ * specific language governing permissions and limitations * under the License. */ +import { ColumnType, GenericDataType } from '@superset-ui/core'; import React from 'react'; +export type LaxColumnType = ColumnType | GenericDataType | 'expression' | 'aggregate' | 'time' | ''; + export type ColumnTypeLabelProps = { - type: string; + type?: LaxColumnType; }; -export function ColumnTypeLabel({ type }: ColumnTypeLabelProps) { +export function ColumnTypeLabel({ type: type_ }: ColumnTypeLabelProps) { + const type: string = + type_ === undefined || type_ === null + ? '?' + : type_ === GenericDataType.BOOLEAN + ? 'bool' + : type_ === GenericDataType.NUMERIC + ? 'FLOAT' + : type_ === GenericDataType.TEMPORAL + ? 'time' + : type_ === GenericDataType.STRING + ? 'string' + : type_; + let stringIcon; + if (typeof type !== 'string') { stringIcon = '?'; } else if (type === '' || type === 'expression') { diff --git a/packages/superset-ui-chart-controls/src/components/ControlForm/ControlFormItem.tsx b/packages/superset-ui-chart-controls/src/components/ControlForm/ControlFormItem.tsx new file mode 100644 index 0000000000..0d8cacb4f9 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/components/ControlForm/ControlFormItem.tsx @@ -0,0 +1,114 @@ +/** + * 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 React, { useState, FunctionComponentElement, ChangeEvent } from 'react'; +import { JsonValue, useTheme } from '@superset-ui/core'; +import ControlHeader, { ControlHeaderProps } from '../ControlHeader'; +import InfoTooltipWithTrigger from '../InfoTooltipWithTrigger'; +import { ControlFormItemComponents, ControlFormItemSpec } from './controls'; + +export * from './controls'; + +export type ControlFormItemProps = ControlFormItemSpec & { + name: string; + onChange?: (fieldValue: JsonValue) => void; +}; + +export type ControlFormItemNode = FunctionComponentElement; + +/** + * Accept `false` or `0`, but not empty string. + */ +function isEmptyValue(value?: JsonValue) { + return value == null || value === ''; +} + +export function ControlFormItem({ + name, + label, + description, + width, + validators, + required, + onChange, + value: initialValue, + defaultValue, + controlType, + ...props +}: ControlFormItemProps) { + const { gridUnit } = useTheme(); + const [hovered, setHovered] = useState(false); + const [value, setValue] = useState(initialValue === undefined ? defaultValue : initialValue); + const [validationErrors, setValidationErrors] = useState< + ControlHeaderProps['validationErrors'] + >(); + + const handleChange = (e: ChangeEvent | JsonValue) => { + const fieldValue = + e && typeof e === 'object' && 'target' in e + ? e.target.type === 'checkbox' || e.target.type === 'radio' + ? e.target.checked + : e.target.value + : e; + const errors = + (validators + ?.map(validator => (!required && isEmptyValue(fieldValue) ? false : validator(fieldValue))) + .filter(x => !!x) as string[]) || []; + setValidationErrors(errors); + setValue(fieldValue); + if (errors.length === 0 && onChange) { + onChange(fieldValue as JsonValue); + } + }; + + const Control = ControlFormItemComponents[controlType]; + + return ( +
setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {controlType === 'Checkbox' ? ( + + {label} {hovered && description && } + + ) : ( + <> + {label && ( + + )} + {/* @ts-ignore */} + + + )} +
+ ); +} + +export default ControlFormItem; diff --git a/packages/superset-ui-chart-controls/src/components/ControlForm/controls.tsx b/packages/superset-ui-chart-controls/src/components/ControlForm/controls.tsx new file mode 100644 index 0000000000..a1b689cbaa --- /dev/null +++ b/packages/superset-ui-chart-controls/src/components/ControlForm/controls.tsx @@ -0,0 +1,92 @@ +/** + * 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 React, { ReactNode } from 'react'; +import { Slider, InputNumber, Input } from 'antd'; +import Checkbox, { CheckboxProps } from 'antd/lib/checkbox'; +import Select, { SelectOption } from '../Select'; +import RadioButtonControl, { + RadioButtonOption, +} from '../../shared-controls/components/RadioButtonControl'; + +export const ControlFormItemComponents = { + Slider, + InputNumber, + Input, + Select, + // Directly export Checkbox will result in "using name from external module" error + // ref: https://stackoverflow.com/questions/43900035/ts4023-exported-variable-x-has-or-is-using-name-y-from-external-module-but + Checkbox: Checkbox as React.ForwardRefExoticComponent< + CheckboxProps & React.RefAttributes + >, + RadioButtonControl, +}; + +export type ControlType = keyof typeof ControlFormItemComponents; + +export type ControlFormValueValidator = (value: V) => string | false; + +export type ControlFormItemSpec = { + controlType: T; + label: ReactNode; + description: ReactNode; + placeholder?: string; + required?: boolean; + validators?: ControlFormValueValidator[]; + width?: number | string; + /** + * Time to delay change propagation. + */ + debounceDelay?: number; +} & (T extends 'Select' + ? { + options: SelectOption[]; + value?: string; + defaultValue?: string; + creatable?: boolean; + minWidth?: number | string; + validators?: ControlFormValueValidator[]; + } + : T extends 'RadioButtonControl' + ? { + options: RadioButtonOption[]; + value?: string; + defaultValue?: string; + } + : T extends 'Checkbox' + ? { + value?: boolean; + defaultValue?: boolean; + } + : T extends 'InputNumber' | 'Slider' + ? { + min?: number; + max?: number; + step?: number; + value?: number; + defaultValue?: number; + validators?: ControlFormValueValidator[]; + } + : T extends 'Input' + ? { + controlType: 'Input'; + value?: string; + defaultValue?: string; + validators?: ControlFormValueValidator[]; + } + : {}); diff --git a/packages/superset-ui-chart-controls/src/components/ControlForm/index.tsx b/packages/superset-ui-chart-controls/src/components/ControlForm/index.tsx new file mode 100644 index 0000000000..189ac428da --- /dev/null +++ b/packages/superset-ui-chart-controls/src/components/ControlForm/index.tsx @@ -0,0 +1,120 @@ +/** + * 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 React, { FunctionComponentElement, useMemo } from 'react'; +import { FAST_DEBOUNCE, JsonObject, JsonValue, useTheme } from '@superset-ui/core'; +import { debounce } from 'lodash'; +import { ControlFormItemNode } from './ControlFormItem'; + +export * from './ControlFormItem'; + +export type ControlFormRowProps = { + children: ControlFormItemNode | ControlFormItemNode[]; +}; + +export function ControlFormRow({ children }: ControlFormRowProps) { + const { gridUnit } = useTheme(); + return ( +
+ {children} +
+ ); +} + +type ControlFormRowNode = FunctionComponentElement; + +export type ControlFormProps = { + /** + * Form field values dict. + */ + value?: JsonObject; + onChange: (value: JsonObject) => void; + children: ControlFormRowNode | ControlFormRowNode[]; +}; + +/** + * Light weight form for control panel. + */ +export default function ControlForm({ onChange, value, children }: ControlFormProps) { + const theme = useTheme(); + const debouncedOnChange = useMemo( + () => + ({ + 0: onChange, + [FAST_DEBOUNCE]: debounce(onChange, FAST_DEBOUNCE), + } as Record), + [onChange], + ); + + const updatedChildren = React.Children.map(children, row => { + if ('children' in row.props) { + const defaultWidth = Array.isArray(row.props.children) + ? `${100 / row.props.children.length}%` + : undefined; + return React.cloneElement(row, { + children: React.Children.map(row.props.children, item => { + const { + name, + width, + debounceDelay = FAST_DEBOUNCE, + onChange: onItemValueChange, + } = item.props; + return React.cloneElement(item, { + width: width || defaultWidth, + value: value?.[name], + onChange(fieldValue: JsonValue) { + // call `onChange` on each FormItem + if (onItemValueChange) { + onItemValueChange(fieldValue); + } + // propagate to the form + if (!(debounceDelay in debouncedOnChange)) { + debouncedOnChange[debounceDelay] = debounce(onChange, debounceDelay); + } + debouncedOnChange[debounceDelay]({ + ...value, + [name]: fieldValue, + }); + }, + }); + }), + }); + } + return row; + }); + return ( +
+ {updatedChildren} +
+ ); +} diff --git a/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx b/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx new file mode 100644 index 0000000000..b7370be474 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/components/ControlHeader.tsx @@ -0,0 +1,138 @@ +/** + * 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 React, { ReactNode } from 'react'; +import { t } from '@superset-ui/core'; +import { InfoTooltipWithTrigger } from './InfoTooltipWithTrigger'; +import { Tooltip } from './Tooltip'; + +type ValidationError = string; + +export type ControlHeaderProps = { + name?: string; + label?: ReactNode; + description?: ReactNode; + validationErrors?: ValidationError[]; + renderTrigger?: boolean; + rightNode?: ReactNode; + leftNode?: ReactNode; + hovered?: boolean; + required?: boolean; + warning?: string; + danger?: string; + onClick?: () => void; + tooltipOnClick?: () => void; +}; + +export default function ControlHeader({ + name, + description, + label, + tooltipOnClick, + onClick, + warning, + danger, + leftNode, + rightNode, + validationErrors = [], + renderTrigger = false, + hovered = false, + required = false, +}: ControlHeaderProps) { + const renderOptionalIcons = () => { + if (hovered) { + return ( + + {description && ( + + {' '} + + )} + {renderTrigger && ( + + {' '} + + )} + + ); + } + return null; + }; + + if (!label) { + return null; + } + const labelClass = validationErrors.length > 0 ? 'text-danger' : ''; + return ( +
+
+ +
+ {rightNode &&
{rightNode}
} +
+
+ ); +} diff --git a/packages/superset-ui-chart-controls/src/components/Select.tsx b/packages/superset-ui-chart-controls/src/components/Select.tsx new file mode 100644 index 0000000000..845d54e981 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/components/Select.tsx @@ -0,0 +1,89 @@ +/** + * 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 React, { useState, ReactNode, ReactElement } from 'react'; +import AntdSelect, { SelectProps as AntdSelectProps, OptionProps } from 'antd/lib/select'; + +export const { Option } = AntdSelect; + +export type SelectOption = [VT, ReactNode]; + +export type SelectProps = Omit, 'options'> & { + creatable?: boolean; + minWidth?: string | number; + options?: SelectOption[]; +}; + +/** + * AntD select with creatable options. + */ +export default function Select({ + creatable, + children, + onSearch, + dropdownMatchSelectWidth = false, + minWidth = '100%', + showSearch: showSearch_ = true, + options, + ...props +}: SelectProps) { + const [searchValue, setSearchValue] = useState(); + // force show search if creatable + const showSearch = showSearch_ || creatable; + const handleSearch = showSearch + ? (input: string) => { + if (creatable) { + setSearchValue(input); + } + if (onSearch) { + onSearch(input); + } + } + : undefined; + + const searchValueNotFound = React.Children.toArray(children).every( + node => node && (node as ReactElement).props.value !== searchValue, + ); + + return ( + + dropdownMatchSelectWidth={dropdownMatchSelectWidth} + showSearch={showSearch} + onSearch={handleSearch} + {...props} + css={{ + minWidth, + }} + > + {options?.map(([val, label]) => ( + + ))} + {children} + {searchValue && searchValueNotFound && ( + + )} + + ); +} + +Select.Option = Option; diff --git a/packages/superset-ui-chart-controls/src/components/Tooltip.tsx b/packages/superset-ui-chart-controls/src/components/Tooltip.tsx index 6ef65e9dc6..e8235d4aa7 100644 --- a/packages/superset-ui-chart-controls/src/components/Tooltip.tsx +++ b/packages/superset-ui-chart-controls/src/components/Tooltip.tsx @@ -16,3 +16,5 @@ export const Tooltip = ({ overlayStyle, color, ...props }: TooltipProps) => { /> ); }; + +export default Tooltip; diff --git a/packages/superset-ui-chart-controls/src/constants.ts b/packages/superset-ui-chart-controls/src/constants.ts index 5661492d27..8c371f4515 100644 --- a/packages/superset-ui-chart-controls/src/constants.ts +++ b/packages/superset-ui-chart-controls/src/constants.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { t } from '@superset-ui/core'; +import { t, QueryMode } from '@superset-ui/core'; +import { DTTM_ALIAS } from '@superset-ui/core/src/query/buildQueryObject'; import { ColumnMeta } from './types'; // eslint-disable-next-line import/prefer-default-export @@ -28,8 +29,17 @@ export const TIME_FILTER_LABELS = { granularity: t('Time Granularity'), }; +export const COLUMN_NAME_ALIASES: Record = { + [DTTM_ALIAS]: t('Time'), +}; + export const TIME_COLUMN_OPTION: ColumnMeta = { - verbose_name: t('Time'), - column_name: '__timestamp', + verbose_name: COLUMN_NAME_ALIASES[DTTM_ALIAS], + column_name: DTTM_ALIAS, description: t('A reference to the [Time] configuration, taking granularity into account'), }; + +export const QueryModeLabel = { + [QueryMode.aggregate]: t('Aggregate'), + [QueryMode.raw]: t('Raw records'), +}; diff --git a/packages/superset-ui-chart-controls/src/index.ts b/packages/superset-ui-chart-controls/src/index.ts index d260931680..dda617ab62 100644 --- a/packages/superset-ui-chart-controls/src/index.ts +++ b/packages/superset-ui-chart-controls/src/index.ts @@ -19,7 +19,7 @@ import * as sectionsModule from './sections'; export * from './utils'; -export { default as sharedControls } from './shared-controls'; +export * from './constants'; // can't do `export * as sections from './sections'`, babel-transformer will fail export const sections = sectionsModule; @@ -30,6 +30,7 @@ export * from './components/ColumnTypeLabel'; export * from './components/MetricOption'; // React control components +export { default as sharedControls } from './shared-controls'; export { default as sharedControlComponents } from './shared-controls/components'; export * from './shared-controls/components'; export * from './types'; diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx new file mode 100644 index 0000000000..1971c06460 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigControl.tsx @@ -0,0 +1,135 @@ +/** + * 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 React, { useMemo, useState } from 'react'; +import { ChartDataResponseResult, useTheme, t } from '@superset-ui/core'; +import ControlHeader from '../../../components/ControlHeader'; +import { ControlComponentProps } from '../types'; + +import ColumnConfigItem from './ColumnConfigItem'; +import { ColumnConfigInfo, ColumnConfig, ColumnConfigFormLayout } from './types'; +import { DEFAULT_CONFIG_FORM_LAYOUT } from './constants'; +import { COLUMN_NAME_ALIASES } from '../../../constants'; + +export type ColumnConfigControlProps = ControlComponentProps< + Record +> & { + queryResponse?: ChartDataResponseResult; + configFormLayout?: ColumnConfigFormLayout; +}; + +/** + * Max number of columns to show by default. + */ +const MAX_NUM_COLS = 10; + +/** + * Add per-column config to queried results. + */ +export default function ColumnConfigControl({ + queryResponse, + value, + onChange, + configFormLayout = DEFAULT_CONFIG_FORM_LAYOUT, + ...props +}: ColumnConfigControlProps) { + const { colnames, coltypes } = queryResponse || {}; + const theme = useTheme(); + const columnConfigs = useMemo(() => { + const configs: Record = {}; + colnames?.forEach((col, idx) => { + configs[col] = { + name: COLUMN_NAME_ALIASES[col] || col, + type: coltypes?.[idx], + config: value?.[col] || {}, + }; + }); + return configs; + }, [value, colnames, coltypes]); + const [showAllColumns, setShowAllColumns] = useState(false); + + const getColumnInfo = (col: string) => columnConfigs[col] || {}; + const setColumnConfig = (col: string, config: T) => { + if (onChange) { + // Only keep configs for known columns + const validConfigs: Record = + colnames && value + ? Object.fromEntries(Object.entries(value).filter(([key]) => colnames.includes(key))) + : { ...value }; + onChange({ + ...validConfigs, + [col]: config, + }); + } + }; + + if (!colnames) return null; + + const needShowMoreButton = colnames.length > MAX_NUM_COLS + 2; + const cols = needShowMoreButton && !showAllColumns ? colnames.slice(0, MAX_NUM_COLS) : colnames; + + return ( + <> + +
+ {cols.map(col => ( + setColumnConfig(col, config as T)} + configFormLayout={configFormLayout} + /> + ))} + {needShowMoreButton && ( +
setShowAllColumns(!showAllColumns)} + > + {showAllColumns ? ( + <> +   {t('Show less columns')} + + ) : ( + <> +   + {t('Show all columns')} + + )} +
+ )} +
+ + ); +} diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigItem.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigItem.tsx new file mode 100644 index 0000000000..b4851f5785 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigItem.tsx @@ -0,0 +1,81 @@ +/** + * 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 React from 'react'; +import { useTheme } from '@superset-ui/core'; +import { Popover } from 'antd'; +import ColumnTypeLabel from '../../../components/ColumnTypeLabel'; +import ColumnConfigPopover, { ColumnConfigPopoverProps } from './ColumnConfigPopover'; + +export type ColumnConfigItemProps = ColumnConfigPopoverProps; + +export default React.memo(function ColumnConfigItem({ + column, + onChange, + configFormLayout, +}: ColumnConfigItemProps) { + const { colors, gridUnit } = useTheme(); + const caretWidth = gridUnit * 6; + return ( + ( + + )} + trigger="click" + placement="right" + > +
.fa': { + color: colors.grayscale.light2, + }, + '&:hover > .fa': { + color: colors.grayscale.light1, + }, + }} + > + + {column.name} + +
+
+ ); +}); diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigPopover.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigPopover.tsx new file mode 100644 index 0000000000..cb493609ec --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/ColumnConfigPopover.tsx @@ -0,0 +1,62 @@ +/** + * 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 React from 'react'; +import { GenericDataType } from '@superset-ui/core'; +import ControlForm, { + ControlFormRow, + ControlFormItem, + ControlFormItemSpec, +} from '../../../components/ControlForm'; +import { SHARED_COLUMN_CONFIG_PROPS, SharedColumnConfigProp } from './constants'; +import { ColumnConfig, ColumnConfigFormLayout, ColumnConfigInfo } from './types'; + +export type ColumnConfigPopoverProps = { + column: ColumnConfigInfo; + configFormLayout: ColumnConfigFormLayout; + onChange: (value: ColumnConfig) => void; +}; + +export default function ColumnConfigPopover({ + column, + configFormLayout, + onChange, +}: ColumnConfigPopoverProps) { + return ( + + {configFormLayout[column.type === undefined ? GenericDataType.STRING : column.type].map( + (row, i) => ( + + {row.map(meta => { + const key = typeof meta === 'string' ? meta : meta.name; + const override = + typeof meta === 'string' ? {} : 'override' in meta ? meta.override : meta.config; + const props = { + ...(key in SHARED_COLUMN_CONFIG_PROPS + ? SHARED_COLUMN_CONFIG_PROPS[key as SharedColumnConfigProp] + : undefined), + ...override, + } as ControlFormItemSpec; + return ; + })} + + ), + )} + + ); +} diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx new file mode 100644 index 0000000000..7669977f5d --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/constants.tsx @@ -0,0 +1,153 @@ +/** + * 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 React from 'react'; +import { GenericDataType, t, validateNumber } from '@superset-ui/core'; +import { FaAlignLeft, FaAlignRight, FaAlignCenter } from 'react-icons/fa'; +import { + D3_FORMAT_DOCS, + D3_FORMAT_OPTIONS, + D3_TIME_FORMAT_DOCS, + D3_TIME_FORMAT_OPTIONS, +} from '../../../utils'; +import { ControlFormItemSpec } from '../../../components/ControlForm'; +import { ColumnConfigFormLayout } from './types'; + +export type SharedColumnConfigProp = + | 'alignPositiveNegative' + | 'colorPositiveNegative' + | 'columnWidth' + | 'fractionDigits' + | 'd3NumberFormat' + | 'd3TimeFormat' + | 'horizontalAlign' + | 'showCellBars'; + +/** + * All configurable column formatting properties. + */ +export const SHARED_COLUMN_CONFIG_PROPS = { + d3NumberFormat: { + controlType: 'Select', + label: t('D3 format'), + description: D3_FORMAT_DOCS, + options: D3_FORMAT_OPTIONS, + defaultValue: D3_FORMAT_OPTIONS[0][0], + creatable: true, + minWidth: '10em', + debounceDelay: 400, + } as ControlFormItemSpec<'Select'>, + + d3TimeFormat: { + controlType: 'Select', + label: t('D3 format'), + description: D3_TIME_FORMAT_DOCS, + options: D3_TIME_FORMAT_OPTIONS, + defaultValue: D3_TIME_FORMAT_OPTIONS[0][0], + creatable: true, + minWidth: '10em', + debounceDelay: 400, + } as ControlFormItemSpec<'Select'>, + + fractionDigits: { + controlType: 'Slider', + label: t('Fraction digits'), + description: t('Number of decimal digits to round numbers to'), + min: 0, + step: 1, + max: 100, + defaultValue: 100, + } as ControlFormItemSpec<'Slider'>, + + columnWidth: { + controlType: 'InputNumber', + label: t('Width'), + description: t( + 'Default column width in pixels, may still be restricted by the shortest/longest word in the column', + ), + width: 120, + placeholder: 'auto', + debounceDelay: 400, + validators: [validateNumber], + } as ControlFormItemSpec<'InputNumber'>, + + horizontalAlign: { + controlType: 'RadioButtonControl', + label: t('Text align'), + description: t('Horizontal alignment'), + width: 130, + debounceDelay: 50, + defaultValue: 'left', + options: [ + ['left', ], + ['center', ], + ['right', ], + ], + } as ControlFormItemSpec<'RadioButtonControl'> & { + value: 'left' | 'right' | 'center'; + defaultValue: 'left' | 'right' | 'center'; + }, + + showCellBars: { + controlType: 'Checkbox', + label: t('Show cell bars'), + description: t('Whether to display a bar chart background in table columns'), + defaultValue: true, + debounceDelay: 200, + } as ControlFormItemSpec<'Checkbox'>, + + alignPositiveNegative: { + controlType: 'Checkbox', + label: t('Align +/-'), + description: t('Whether to align positive and negative values in cell bar chart at 0'), + defaultValue: false, + debounceDelay: 200, + } as ControlFormItemSpec<'Checkbox'>, + + colorPositiveNegative: { + controlType: 'Checkbox', + label: t('Color +/-'), + description: t('Whether to colorize numeric values by if they are positive or negative'), + defaultValue: false, + debounceDelay: 200, + } as ControlFormItemSpec<'Checkbox'>, +}; + +export type SharedColumnConfig = { + [key in SharedColumnConfigProp]?: typeof SHARED_COLUMN_CONFIG_PROPS[key]['value']; +}; + +export const DEFAULT_CONFIG_FORM_LAYOUT: ColumnConfigFormLayout = { + [GenericDataType.STRING]: [ + ['columnWidth', { name: 'horizontalAlign', override: { defaultValue: 'left' } }], + ], + [GenericDataType.NUMERIC]: [ + ['columnWidth', { name: 'horizontalAlign', override: { defaultValue: 'right' } }], + ['d3NumberFormat'], + ['fractionDigits'], + ['alignPositiveNegative', 'colorPositiveNegative'], + ['showCellBars'], + ], + [GenericDataType.TEMPORAL]: [ + ['columnWidth', { name: 'horizontalAlign', override: { defaultValue: 'left' } }], + ['d3TimeFormat'], + ], + [GenericDataType.BOOLEAN]: [ + ['columnWidth', { name: 'horizontalAlign', override: { defaultValue: 'left' } }], + ], +}; diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/index.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/index.tsx new file mode 100644 index 0000000000..130fe8d091 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/index.tsx @@ -0,0 +1,24 @@ +/** + * 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 ColumnConfigControl from './ColumnConfigControl'; + +export * from './types'; +export { default as __hack__reexport__ } from './types'; + +export default ColumnConfigControl; diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/types.ts b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/types.ts new file mode 100644 index 0000000000..75ad35abcc --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/ColumnConfigControl/types.ts @@ -0,0 +1,48 @@ +/** + * 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 { GenericDataType, JsonObject, StrictJsonValue } from '@superset-ui/core'; +import { ControlFormItemSpec } from '../../../components/ControlForm'; +import { SHARED_COLUMN_CONFIG_PROPS, SharedColumnConfigProp } from './constants'; + +/** + * Column formatting configs. + */ +export type ColumnConfig = { + [key in SharedColumnConfigProp]?: typeof SHARED_COLUMN_CONFIG_PROPS[key]['value']; +} & + Record; + +/** + * All required info about a column to render the + * formatting. + */ +export interface ColumnConfigInfo { + name: string; + type?: GenericDataType; + config: JsonObject; +} + +export type ColumnConfigFormItem = + | SharedColumnConfigProp + | { name: SharedColumnConfigProp; override: Partial } + | { name: string; config: ControlFormItemSpec }; + +export type ColumnConfigFormLayout = Record; + +export default {}; diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx index 326985c63f..9999956de0 100644 --- a/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/RadioButtonControl.tsx @@ -16,14 +16,12 @@ * specific language governing permissions and limitations * under the License. */ -import React, { ReactText, ReactNode, MouseEvent, useCallback } from 'react'; -import { styled } from '@superset-ui/core'; -import { InfoTooltipWithTrigger } from '../../components/InfoTooltipWithTrigger'; +import React, { ReactNode } from 'react'; +import { JsonValue, useTheme } from '@superset-ui/core'; +import ControlHeader from '../../components/ControlHeader'; -export interface RadioButtonOption { - label: string; - value: ReactText; -} +// [value, label] +export type RadioButtonOption = [JsonValue, Exclude]; export interface RadioButtonControlProps { label?: ReactNode; @@ -31,61 +29,52 @@ export interface RadioButtonControlProps { options: RadioButtonOption[]; hovered?: boolean; value?: string; - onChange: (opt: string) => void; + onChange: (opt: RadioButtonOption[0]) => void; } -const Styles = styled.div` - .btn:focus { - outline: none; - } - .control-label + .btn-group { - margin-top: 1px; - } - .btn-group .btn.active { - background: ${({ theme }) => theme.colors.secondary.light5}; - box-shadow: none; - font-weight: ${({ theme }) => theme.typography.weights.bold}; - } -`; - export default function RadioButtonControl({ - label: controlLabel, - description, value: initialValue, - hovered, options, onChange, + ...props }: RadioButtonControlProps) { - const currentValue = initialValue || options[0].value; - const onClick = useCallback( - (e: MouseEvent) => { - onChange(e.currentTarget.value); - }, - [onChange], - ); + const currentValue = initialValue || options[0][0]; + const theme = useTheme(); return ( - - {controlLabel && ( -
- {controlLabel}{' '} - {hovered && description && ( - - )} -
- )} +
+
- {options.map(({ label, value }, i) => ( + {options.map(([val, label]) => ( ))}
- +
); } diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx b/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx index 93bfbcbe68..b6e635e25f 100644 --- a/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/index.tsx @@ -17,8 +17,10 @@ * under the License. */ import RadioButtonControl from './RadioButtonControl'; +import ColumnConfigControl from './ColumnConfigControl'; export * from './RadioButtonControl'; +export * from './ColumnConfigControl'; /** * Shared chart controls. Can be referred via string shortcuts in chart control @@ -26,4 +28,5 @@ export * from './RadioButtonControl'; */ export default { RadioButtonControl, + ColumnConfigControl, }; diff --git a/packages/superset-ui-chart-controls/src/shared-controls/components/types.ts b/packages/superset-ui-chart-controls/src/shared-controls/components/types.ts new file mode 100644 index 0000000000..fb13957e91 --- /dev/null +++ b/packages/superset-ui-chart-controls/src/shared-controls/components/types.ts @@ -0,0 +1,38 @@ +/** + * 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 { QueryFormData, JsonValue } from '@superset-ui/core'; +import { ReactNode } from 'react'; + +/** + * Props passed to control components. + * + * Ref: superset-frontend/src/explore/components/Control.tsx + */ +export interface ControlComponentProps { + name: string; + label?: ReactNode; + description?: ReactNode; + formData?: QueryFormData | null; + value?: ValueType | null; + validationErrors?: any[]; + hidden?: boolean; + renderTrigger?: boolean; + hovered?: boolean; + onChange?: (value: ValueType) => void; +} diff --git a/packages/superset-ui-chart-controls/src/shared-controls/index.tsx b/packages/superset-ui-chart-controls/src/shared-controls/index.tsx index 11948e0c0f..740556fa29 100644 --- a/packages/superset-ui-chart-controls/src/shared-controls/index.tsx +++ b/packages/superset-ui-chart-controls/src/shared-controls/index.tsx @@ -43,10 +43,18 @@ import { SequentialScheme, legacyValidateInteger, validateNonEmpty, - smartDateFormatter, } from '@superset-ui/core'; -import { mainMetric, formatSelectOptions } from '../utils'; +import { + mainMetric, + formatSelectOptions, + D3_FORMAT_OPTIONS, + D3_FORMAT_DOCS, + D3_TIME_FORMAT_OPTIONS, + D3_TIME_FORMAT_DOCS, + DEFAULT_TIME_FORMAT, + DEFAULT_NUMBER_FORMAT, +} from '../utils'; import { TIME_FILTER_LABELS, TIME_COLUMN_OPTION } from '../constants'; import { Metric, @@ -56,6 +64,7 @@ import { SelectControlConfig, } from '../types'; import { ColumnOption } from '../components/ColumnOption'; + import { dnd_adhoc_filters, dnd_adhoc_metric, @@ -71,40 +80,9 @@ const sequentialSchemeRegistry = getSequentialSchemeRegistry(); export const PRIMARY_COLOR = { r: 0, g: 122, b: 135, a: 1 }; -// input choices & options -export const D3_FORMAT_OPTIONS = [ - ['SMART_NUMBER', 'Adaptative formating'], - ['~g', 'Original value'], - [',d', ',d (12345.432 => 12,345)'], - ['.1s', '.1s (12345.432 => 10k)'], - ['.3s', '.3s (12345.432 => 12.3k)'], - [',.1%', ',.1% (12345.432 => 1,234,543.2%)'], - ['.3%', '.3% (12345.432 => 1234543.200%)'], - ['.4r', '.4r (12345.432 => 12350)'], - [',.3f', ',.3f (12345.432 => 12,345.432)'], - ['+,', '+, (12345.432 => +12,345.432)'], - ['$,.2f', '$,.2f (12345.432 => $12,345.43)'], - ['DURATION', 'Duration in ms (66000 => 1m 6s)'], - ['DURATION_SUB', 'Duration in ms (100.40008 => 100ms 400µs 80ns)'], -]; - const ROW_LIMIT_OPTIONS = [10, 50, 100, 250, 500, 1000, 5000, 10000, 50000]; const SERIES_LIMITS = [0, 5, 10, 25, 50, 100, 500]; -export const D3_FORMAT_DOCS = t('D3 format syntax: https://github.com/d3/d3-format'); - -export const D3_TIME_FORMAT_OPTIONS = [ - ['smart_date', t('Adaptative formating')], - ['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'], - ['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'], - ['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'], - ['%Y-%m-%d %H:%M:%S', '%Y-%m-%d %H:%M:%S | 2019-01-14 01:32:10'], - ['%d-%m-%Y %H:%M:%S', '%Y-%m-%d %H:%M:%S | 14-01-2019 01:32:10'], - ['%H:%M:%S', '%H:%M:%S | 01:32:10'], -]; - -export const D3_TIME_FORMAT_DOCS = t('D3 time format syntax: https://github.com/d3/d3-time-format'); - type Control = { savedMetrics?: Metric[] | null; default?: unknown; @@ -417,7 +395,7 @@ const y_axis_format: SharedControlConfig<'SelectControl'> = { freeForm: true, label: t('Y Axis Format'), renderTrigger: true, - default: 'SMART_NUMBER', + default: DEFAULT_NUMBER_FORMAT, choices: D3_FORMAT_OPTIONS, description: D3_FORMAT_DOCS, mapStateToProps: state => { @@ -439,7 +417,7 @@ const x_axis_time_format: SharedControlConfig<'SelectControl'> = { freeForm: true, label: t('Time format'), renderTrigger: true, - default: smartDateFormatter.id, + default: DEFAULT_TIME_FORMAT, choices: D3_TIME_FORMAT_OPTIONS, description: D3_TIME_FORMAT_DOCS, }; diff --git a/packages/superset-ui-chart-controls/src/types.ts b/packages/superset-ui-chart-controls/src/types.ts index 812852f71a..0708bfdd8a 100644 --- a/packages/superset-ui-chart-controls/src/types.ts +++ b/packages/superset-ui-chart-controls/src/types.ts @@ -18,7 +18,14 @@ * under the License. */ import React, { ReactNode, ReactText, ReactElement } from 'react'; -import { QueryFormData, DatasourceType, Metric, JsonValue, Column } from '@superset-ui/core'; +import { + QueryFormData, + DatasourceType, + Metric, + JsonValue, + Column, + ColumnType, +} from '@superset-ui/core'; import sharedControls from './shared-controls'; import sharedControlComponents from './shared-controls/components'; @@ -40,7 +47,7 @@ export type SharedControlComponents = typeof sharedControlComponents; * ---------------------------------------------*/ export type ColumnMeta = Omit & { id?: number; - type?: string; + type?: ColumnType; } & AnyDict; export interface DatasourceMeta { @@ -186,8 +193,15 @@ export interface BaseControlConfig< validators?: ControlValueValidator[]; warning?: ReactNode; error?: ReactNode; - // override control panel state props - mapStateToProps?: (state: ControlPanelState, control: this) => ExtraControlProps; + /** + * Add additional props to chart control. + */ + mapStateToProps?: ( + state: ControlPanelState, + controlState: this & ExtraControlProps, + // TODO: add strict `chartState` typing (see superset-frontend/src/explore/types) + chartState?: AnyDict, + ) => ExtraControlProps; visibility?: (props: ControlPanelsContainerProps) => boolean; } @@ -196,7 +210,7 @@ export interface ControlValueValidator< O extends SelectOption = SelectOption, V = unknown > { - (value: V, state: ControlState): boolean | string; + (value: V, state?: ControlState): boolean | string; } /** -------------------------------------------- diff --git a/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts b/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts index fa377ab547..026ac63540 100644 --- a/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts +++ b/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts @@ -16,15 +16,15 @@ * specific language governing permissions and limitations * under the License. */ +import { t, smartDateFormatter, NumberFormats } from '@superset-ui/core'; // D3 specific formatting config - export const D3_FORMAT_DOCS = 'D3 format syntax: https://github.com/d3/d3-format'; // input choices & options -export const D3_FORMAT_OPTIONS = [ - ['SMART_NUMBER', 'Adaptative formating'], - ['~g', 'Original value'], +export const D3_FORMAT_OPTIONS: [string, string][] = [ + [NumberFormats.SMART_NUMBER, t('Adaptative formating')], + ['~g', t('Original value')], [',d', ',d (12345.432 => 12,345)'], ['.1s', '.1s (12345.432 => 10k)'], ['.3s', '.3s (12345.432 => 12.3k)'], @@ -34,12 +34,14 @@ export const D3_FORMAT_OPTIONS = [ [',.3f', ',.3f (12345.432 => 12,345.432)'], ['+,', '+, (12345.432 => +12,345.432)'], ['$,.2f', '$,.2f (12345.432 => $12,345.43)'], - ['DURATION', 'Duration in ms (66000 => 1m 6s)'], - ['DURATION_SUB', 'Duration in ms (100.40008 => 100ms 400µs 80ns)'], + ['DURATION', t('Duration in ms (66000 => 1m 6s)')], + ['DURATION_SUB', t('Duration in ms (1.40008 => 1ms 400µs 80ns)')], ]; -export const D3_TIME_FORMAT_OPTIONS = [ - ['smart_date', 'Adaptative formating'], +export const D3_TIME_FORMAT_DOCS = t('D3 time format syntax: https://github.com/d3/d3-time-format'); + +export const D3_TIME_FORMAT_OPTIONS: [string, string][] = [ + [smartDateFormatter.id, t('Adaptative formating')], ['%d/%m/%Y', '%d/%m/%Y | 14/01/2019'], ['%m/%d/%Y', '%m/%d/%Y | 01/14/2019'], ['%Y-%m-%d', '%Y-%m-%d | 2019-01-14'], @@ -47,3 +49,6 @@ export const D3_TIME_FORMAT_OPTIONS = [ ['%d-%m-%Y %H:%M:%S', '%Y-%m-%d %H:%M:%S | 14-01-2019 01:32:10'], ['%H:%M:%S', '%H:%M:%S | 01:32:10'], ]; + +export const DEFAULT_NUMBER_FORMAT = D3_FORMAT_OPTIONS[0][0]; +export const DEFAULT_TIME_FORMAT = D3_TIME_FORMAT_OPTIONS[0][0]; diff --git a/packages/superset-ui-core/src/components/constants.ts b/packages/superset-ui-core/src/components/constants.ts new file mode 100644 index 0000000000..f252d725dd --- /dev/null +++ b/packages/superset-ui-core/src/components/constants.ts @@ -0,0 +1,28 @@ +/** + * 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. + */ + +/** + * Faster debounce delay for inputs without expensive operation. + */ +export const FAST_DEBOUNCE = 250; + +/** + * Slower debounce delay for inputs with expensive API calls. + */ +export const SLOW_DEBOUNCE = 500; diff --git a/packages/superset-ui-core/src/components/index.ts b/packages/superset-ui-core/src/components/index.ts index 2a00f24481..aa48853b59 100644 --- a/packages/superset-ui-core/src/components/index.ts +++ b/packages/superset-ui-core/src/components/index.ts @@ -1 +1,2 @@ +export * from './constants'; export { default as SafeMarkdown } from './SafeMarkdown'; diff --git a/packages/superset-ui-core/src/style/index.ts b/packages/superset-ui-core/src/style/index.ts index 5e5cfe9d66..fe92d05ef3 100644 --- a/packages/superset-ui-core/src/style/index.ts +++ b/packages/superset-ui-core/src/style/index.ts @@ -20,7 +20,7 @@ import emotionStyled, { CreateStyled } from '@emotion/styled'; import { useTheme as useThemeBasic } from 'emotion-theming'; export { ThemeProvider, withTheme } from 'emotion-theming'; -export { css } from '@emotion/core'; +export { css, InterpolationWithTheme } from '@emotion/core'; export function useTheme() { const theme = useThemeBasic(); @@ -36,6 +36,10 @@ export function useTheme() { const defaultTheme = { borderRadius: 4, colors: { + text: { + label: '#879399', + help: '#737373', + }, primary: { base: '#20A7C9', dark1: '#1A85A0', diff --git a/packages/superset-ui-demo/package.json b/packages/superset-ui-demo/package.json index 8214ee82a1..3b802cc55d 100644 --- a/packages/superset-ui-demo/package.json +++ b/packages/superset-ui-demo/package.json @@ -68,7 +68,7 @@ "@types/react-loadable": "^5.5.3", "@types/react-resizable": "^1.7.2", "@types/storybook__react": "5.2.1", - "antd": "^4.9.1", + "antd": "^4.9.4", "bootstrap": "^3.4.1", "core-js": "3.8.3", "gh-pages": "^3.0.0", @@ -77,6 +77,7 @@ "memoize-one": "^5.1.1", "react": "^16.13.1", "react-bootstrap": "^0.33.1", + "react-icons": "^4.2.0", "react-loadable": "^5.5.0", "react-resizable": "^1.10.1", "storybook-addon-jsx": "^7.2.3" diff --git a/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx b/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx index d926eeca1b..42ef0ebc15 100644 --- a/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx +++ b/plugins/plugin-chart-echarts/src/Graph/controlPanel.tsx @@ -114,14 +114,8 @@ const controlPanel: ControlPanelConfig = { label: t('Graph layout'), default: DEFAULT_FORM_DATA.layout, options: [ - { - label: 'force', - value: 'force', - }, - { - label: 'circular', - value: 'circular', - }, + ['force', t('Force')], + ['circular', t('Circular')], ], description: t('Layout type of graph'), }, diff --git a/plugins/plugin-chart-table/package.json b/plugins/plugin-chart-table/package.json index 886402fff5..cdf3d01121 100644 --- a/plugins/plugin-chart-table/package.json +++ b/plugins/plugin-chart-table/package.json @@ -34,7 +34,6 @@ "d3-array": "^2.4.0", "match-sorter": "^6.1.0", "memoize-one": "^5.1.1", - "react-icons": "^3.10.0", "react-table": "^7.2.1", "regenerator-runtime": "^0.13.5", "xss": "^1.0.6" @@ -42,6 +41,7 @@ "peerDependencies": { "@types/react": "*", "react": "^16.13.1", - "react-dom": "^16.13.1" + "react-dom": "^16.13.1", + "react-icons": "^4.2.0" } } diff --git a/plugins/plugin-chart-table/src/DataTable/DataTable.tsx b/plugins/plugin-chart-table/src/DataTable/DataTable.tsx index a9cde915f2..8a1e091a87 100644 --- a/plugins/plugin-chart-table/src/DataTable/DataTable.tsx +++ b/plugins/plugin-chart-table/src/DataTable/DataTable.tsx @@ -107,11 +107,11 @@ export default function DataTable({ sortBy: sortByRef.current, pageSize: initialPageSize > 0 ? initialPageSize : resultsSize || 10, }; - const defaultWrapperRef = useRef(null); const globalControlRef = useRef(null); const paginationRef = useRef(null); const wrapperRef = userWrapperRef || defaultWrapperRef; + const paginationData = JSON.stringify(serverPaginationData); const defaultGetTableSize = useCallback(() => { if (wrapperRef.current) { @@ -134,7 +134,7 @@ export default function DataTable({ hasGlobalControl, paginationRef, resultsSize, - JSON.stringify(serverPaginationData), + paginationData, ]); const defaultGlobalFilter: FilterType = useCallback( diff --git a/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx b/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx index 0e858fe2e0..1dbdeead69 100644 --- a/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx +++ b/plugins/plugin-chart-table/src/DataTable/hooks/useSticky.tsx @@ -49,7 +49,7 @@ type ColGroup = ReactElementWithChildren<'colgroup', Col>; export type Table = ReactElementWithChildren<'table', (Thead | Tbody | ColGroup)[]>; export type TableRenderer = () => Table; export type GetTableSize = () => Partial | undefined; -export type SetStickyState = (size?: StickyState) => void; +export type SetStickyState = (size?: Partial) => void; export enum ReducerActions { init = 'init', // this is from global reducer @@ -95,6 +95,7 @@ const mergeStyleProp = (node: ReactElement<{ style?: CSSProperties }>, style: CS ...style, }, }); +const fixedTableLayout: CSSProperties = { tableLayout: 'fixed' }; /** * An HOC for generating sticky header and fixed-height scrollable area @@ -117,14 +118,11 @@ function StickyWrap({ } let thead: Thead | undefined; let tbody: Tbody | undefined; - let colgroup: ColGroup | undefined; React.Children.forEach(table.props.children, node => { if (node.type === 'thead') { thead = node; } else if (node.type === 'tbody') { tbody = node; - } else if (node.type === 'colgroup') { - colgroup = node; } }); if (!thead || !tbody) { @@ -139,13 +137,13 @@ function StickyWrap({ const scrollHeaderRef = useRef(null); // fixed header const scrollBodyRef = useRef(null); // main body + const scrollBarSize = getScrollBarSize(); const { bodyHeight, columnWidths } = sticky; const needSizer = !columnWidths || sticky.width !== maxWidth || sticky.height !== maxHeight || sticky.setStickyState !== setStickyState; - const scrollBarSize = getScrollBarSize(); // update scrollable area and header column sizes when mounted useLayoutEffect(() => { @@ -199,21 +197,23 @@ function StickyWrap({ visibility: 'hidden', }} > - {React.cloneElement(table, {}, colgroup, theadWithRef, tbody)} + {React.cloneElement(table, {}, theadWithRef, tbody)}
); } // reuse previously column widths, will be updated by `useLayoutEffect` above const colWidths = columnWidths?.slice(0, columnCount); - if (colWidths && bodyHeight) { - const tableStyle: CSSProperties = { tableLayout: 'fixed' }; - const bodyCols = colWidths.map((w, i) => ( - // eslint-disable-next-line react/no-array-index-key - - )); - const bodyColgroup = {bodyCols}; + if (colWidths && bodyHeight) { + const bodyColgroup = ( + + {colWidths.map((w, i) => ( + // eslint-disable-next-line react/no-array-index-key + + ))} + + ); // header columns do not have vertical scroll bars, // so we add scroll bar size to the last column @@ -237,7 +237,7 @@ function StickyWrap({ overflow: 'hidden', }} > - {React.cloneElement(table, mergeStyleProp(table, tableStyle), headerColgroup, thead)} + {React.cloneElement(table, mergeStyleProp(table, fixedTableLayout), headerColgroup, thead)} {headerTable} ); @@ -257,7 +257,7 @@ function StickyWrap({ }} onScroll={sticky.hasHorizontalScroll ? onScroll : undefined} > - {React.cloneElement(table, mergeStyleProp(table, tableStyle), bodyColgroup, tbody)} + {React.cloneElement(table, mergeStyleProp(table, fixedTableLayout), bodyColgroup, tbody)} ); } @@ -332,12 +332,14 @@ function useInstance(instance: TableInstance) { export default function useSticky(hooks: Hooks) { hooks.useInstance.push(useInstance); - hooks.stateReducers.push((newState, action_) => { + hooks.stateReducers.push((newState, action_, prevState) => { const action = action_ as ReducerAction; if (action.type === ReducerActions.init) { return { ...newState, - sticky: newState.sticky || {}, + sticky: { + ...prevState?.sticky, + }, }; } if (action.type === ReducerActions.setStickyState) { @@ -348,7 +350,8 @@ export default function useSticky(hooks: Hooks) { return { ...newState, sticky: { - ...newState.sticky, + ...prevState?.sticky, + ...newState?.sticky, ...action.size, }, }; diff --git a/plugins/plugin-chart-table/src/TableChart.tsx b/plugins/plugin-chart-table/src/TableChart.tsx index 08bf8b524c..fc1e6d302f 100644 --- a/plugins/plugin-chart-table/src/TableChart.tsx +++ b/plugins/plugin-chart-table/src/TableChart.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import React, { useState, useMemo, useCallback } from 'react'; +import React, { useState, useMemo, useCallback, CSSProperties } from 'react'; import { ColumnInstance, DefaultSortTypes, ColumnWithLooseAccessor } from 'react-table'; import { extent as d3Extent, max as d3Max } from 'd3-array'; import { FaSort, FaSortUp as FaSortAsc, FaSortDown as FaSortDesc } from 'react-icons/fa'; @@ -150,8 +150,8 @@ export default function TableChart( isRawRecords, rowCount = 0, columns: columnsMeta, - alignPositiveNegative = false, - colorPositiveNegative = false, + alignPositiveNegative: defaultAlignPN = false, + colorPositiveNegative: defaultColorPN = false, includeSearch = false, pageSize = 0, serverPagination = false, @@ -173,10 +173,10 @@ export default function TableChart( return PAGE_SIZE_OPTIONS.filter(([n]) => serverPagination ? getServerPagination(n) : n <= 2 * data.length, ) as SizeOption[]; - }, [data.length, rowCount]); + }, [data.length, rowCount, serverPagination]); const getValueRange = useCallback( - function getValueRange(key: string) { + function getValueRange(key: string, alignPositiveNegative: boolean) { if (typeof data?.[0]?.[key] === 'number') { const nums = data.map(row => row[key]) as number[]; return (alignPositiveNegative @@ -185,7 +185,7 @@ export default function TableChart( } return null; }, - [alignPositiveNegative, data], + [data], ); const isActiveFilterValue = useCallback( @@ -213,14 +213,33 @@ export default function TableChart( const getColumnConfigs = useCallback( (column: DataColumnMeta, i: number): ColumnWithLooseAccessor => { - const { key, label, dataType, isMetric } = column; + const { key, label, dataType, isMetric, config = {} } = column; + const isNumber = dataType === GenericDataType.NUMERIC; + const isFilter = !isNumber && emitFilter; + const textAlign = config.horizontalAlign + ? config.horizontalAlign + : isNumber + ? 'right' + : 'left'; + const columnWidth = Number.isNaN(Number(config.columnWidth)) + ? config.columnWidth + : Number(config.columnWidth); + const alignPositiveNegative = + config.alignPositiveNegative === undefined ? defaultAlignPN : config.alignPositiveNegative; + const colorPositiveNegative = + config.colorPositiveNegative === undefined ? defaultColorPN : config.colorPositiveNegative; + const fractionDigits = isNumber ? config.fractionDigits : undefined; + + const valueRange = + (config.showCellBars === undefined ? showCellBars : config.showCellBars) && + (isMetric || isRawRecords) && + getValueRange(key, alignPositiveNegative); + let className = ''; - if (dataType === GenericDataType.NUMERIC) { - className += ' dt-metric'; - } else if (emitFilter) { + if (isFilter) { className += ' dt-is-filter'; } - const valueRange = showCellBars && (isMetric || isRawRecords) && getValueRange(key); + return { id: String(i), // to allow duplicate column keys // must use custom accessor to allow `.` in column names @@ -228,8 +247,13 @@ export default function TableChart( // so we ask TS not to check. accessor: ((datum: D) => datum[key]) as never, Cell: ({ value }: { column: ColumnInstance; value: DataRecordValue }) => { - const [isHtml, text, customClassName] = formatValue(column, value); - const style = { + let rounded = value; + if (fractionDigits !== undefined && typeof value === 'number') { + rounded = Number(value.toFixed(fractionDigits)); + } + const [isHtml, text] = formatValue(column, rounded); + const html = isHtml ? { __html: text } : undefined; + const style: CSSProperties = { background: valueRange ? cellBar({ value: value as number, @@ -238,15 +262,17 @@ export default function TableChart( colorPositiveNegative, }) : undefined, + textAlign, }; - const html = isHtml ? { __html: text } : undefined; const cellProps = { // show raw number in title in case of numeric values title: typeof value === 'number' ? String(value) : undefined, onClick: emitFilter && !valueRange ? () => toggleFilter(key, value) : undefined, - className: `${className} ${customClassName || ''} ${ - isActiveFilterValue(key, value) ? ' dt-is-active-filter' : '' - }`, + className: [ + className, + value == null ? 'dt-is-null' : '', + isActiveFilterValue(key, value) ? ' dt-is-active-filter' : '', + ].join(' '), style, }; if (html) { @@ -260,10 +286,22 @@ export default function TableChart( Header: ({ column: col, onClick, style }) => ( + {/* can't use `columnWidth &&` because it may also be zero */} + {config.columnWidth ? ( + // column width hint +
+ ) : null} {label} @@ -273,11 +311,12 @@ export default function TableChart( }; }, [ - alignPositiveNegative, - colorPositiveNegative, + defaultAlignPN, + defaultColorPN, emitFilter, getValueRange, isActiveFilterValue, + isRawRecords, showCellBars, sortDesc, toggleFilter, @@ -300,7 +339,6 @@ export default function TableChart( pageSize={pageSize} serverPaginationData={serverPaginationData} pageSizeOptions={pageSizeOptions} - width={width} height={height} serverPagination={serverPagination} onServerPaginationChange={handleServerPaginationChange} diff --git a/plugins/plugin-chart-table/src/controlPanel.tsx b/plugins/plugin-chart-table/src/controlPanel.tsx index 565a0b817b..17524462cc 100644 --- a/plugins/plugin-chart-table/src/controlPanel.tsx +++ b/plugins/plugin-chart-table/src/controlPanel.tsx @@ -20,11 +20,11 @@ import React from 'react'; import { t, - validateNonEmpty, addLocaleData, smartDateFormatter, QueryMode, QueryFormColumn, + ChartDataResponseResult, } from '@superset-ui/core'; import { D3_TIME_FORMAT_OPTIONS, @@ -35,6 +35,7 @@ import { ControlPanelsContainerProps, sharedControls, sections, + QueryModeLabel, } from '@superset-ui/chart-controls'; import i18n from './i18n'; @@ -42,11 +43,6 @@ import { PAGE_SIZE_OPTIONS } from './consts'; addLocaleData(i18n); -const QueryModeLabel = { - [QueryMode.aggregate]: t('Aggregate'), - [QueryMode.raw]: t('Raw Records'), -}; - function getQueryMode(controls: ControlStateMapping): QueryMode { const mode = controls?.query_mode?.value; if (mode === QueryMode.aggregate || mode === QueryMode.raw) { @@ -69,17 +65,11 @@ const isRawMode = isQueryMode(QueryMode.raw); const queryMode: ControlConfig<'RadioButtonControl'> = { type: 'RadioButtonControl', - label: t('Query Mode'), + label: t('Query mode'), default: null, options: [ - { - label: QueryModeLabel[QueryMode.aggregate], - value: QueryMode.aggregate, - }, - { - label: QueryModeLabel[QueryMode.raw], - value: QueryMode.raw, - }, + [QueryMode.aggregate, QueryModeLabel[QueryMode.aggregate]], + [QueryMode.raw, QueryModeLabel[QueryMode.raw]], ], mapStateToProps: ({ controls }) => ({ value: getQueryMode(controls) }), }; @@ -105,7 +95,7 @@ const all_columns: typeof sharedControls.groupby = { const percent_metrics: typeof sharedControls.metrics = { type: 'MetricsControl', - label: t('Percentage Metrics'), + label: t('Percentage metrics'), description: t( 'Metrics for which percentage of total are to be displayed. Calculated from only data within the row limit.', ), @@ -221,7 +211,7 @@ const config: ControlPanelConfig = { name: 'include_time', config: { type: 'CheckboxControl', - label: t('Include Time'), + label: t('Include time'), description: t( 'Whether to include the time granularity as defined in the time section', ), @@ -233,7 +223,7 @@ const config: ControlPanelConfig = { name: 'order_desc', config: { type: 'CheckboxControl', - label: t('Sort Descending'), + label: t('Sort descending'), default: true, description: t('Whether to sort descending or ascending'), visibility: isAggMode, @@ -253,13 +243,12 @@ const config: ControlPanelConfig = { config: { type: 'SelectControl', freeForm: true, - label: t('Table Timestamp Format'), + label: t('Timestamp format'), default: smartDateFormatter.id, renderTrigger: true, - validators: [validateNonEmpty], clearable: false, choices: D3_TIME_FORMAT_OPTIONS, - description: t('Timestamp Format'), + description: t('D3 time format for datetime columns'), }, }, ], @@ -270,7 +259,7 @@ const config: ControlPanelConfig = { type: 'SelectControl', freeForm: true, renderTrigger: true, - label: t('Page Length'), + label: t('Page length'), default: null, choices: PAGE_SIZE_OPTIONS, description: t('Rows per page, 0 means no pagination'), @@ -285,20 +274,20 @@ const config: ControlPanelConfig = { name: 'include_search', config: { type: 'CheckboxControl', - label: t('Search Box'), + label: t('Search box'), renderTrigger: true, default: false, description: t('Whether to include a client-side search box'), }, }, { - name: 'table_filter', + name: 'show_cell_bars', config: { type: 'CheckboxControl', - label: t('Emit Filter Events'), + label: t('Cell bars'), renderTrigger: true, - default: false, - description: t('Whether to apply filter to dashboards when table cells are clicked'), + default: true, + description: t('Whether to display a bar chart background in table columns'), }, }, ], @@ -310,7 +299,9 @@ const config: ControlPanelConfig = { label: t('Align +/-'), renderTrigger: true, default: false, - description: t('Whether to align the background chart for +/- values'), + description: t( + 'Whether to align background charts with both positive and negative values at 0', + ), }, }, { @@ -320,22 +311,39 @@ const config: ControlPanelConfig = { label: t('Color +/-'), renderTrigger: true, default: true, - description: t('Whether to color +/- values'), + description: t( + 'Whether to colorize numeric values by if they are positive or negative', + ), }, }, ], [ { - name: 'show_cell_bars', + name: 'table_filter', config: { type: 'CheckboxControl', - label: t('Show Cell Bars'), + label: t('Allow cross filter'), renderTrigger: true, - default: true, - description: t('Enable to display bar chart background elements in table columns'), + default: false, + description: t('Whether to apply filter to dashboards when table cells are clicked'), + }, + }, + ], + [ + { + name: 'column_config', + config: { + type: 'ColumnConfigControl', + label: t('Cuztomize columns'), + description: t('Further customize how to display each column'), + renderTrigger: true, + mapStateToProps(explore, control, chart) { + return { + queryResponse: chart?.queriesResponse?.[0] as ChartDataResponseResult | undefined, + }; + }, }, }, - null, ], ], }, diff --git a/plugins/plugin-chart-table/src/transformProps.ts b/plugins/plugin-chart-table/src/transformProps.ts index 360300fb89..0d3e3cdb0b 100644 --- a/plugins/plugin-chart-table/src/transformProps.ts +++ b/plugins/plugin-chart-table/src/transformProps.ts @@ -78,6 +78,7 @@ const processColumns = memoizeOne(function processColumns(props: TableChartProps time_grain_sqla: granularity, metrics: metrics_, percent_metrics: percentMetrics_, + column_config: columnConfig = {}, }, queriesData, } = props; @@ -100,23 +101,31 @@ const processColumns = memoizeOne(function processColumns(props: TableChartProps .map((key: string, i) => { const label = verboseMap?.[key] || key; const dataType = coltypes[i]; - // fallback to column level formats defined in datasource - const format = columnFormats?.[key]; + const config = columnConfig[key] || {}; // for the purpose of presentation, only numeric values are treated as metrics + // because users can also add things like `MAX(str_col)` as a metric. const isMetric = metricsSet.has(key) && isNumeric(key, records); const isPercentMetric = percentMetricsSet.has(key); const isTime = dataType === GenericDataType.TEMPORAL; + const savedFormat = columnFormats?.[key]; + const numberFormat = config.d3NumberFormat || savedFormat; + let formatter; - if (isTime) { - const timeFormat = format || tableTimestampFormat; + + if (isTime || config.d3TimeFormat) { + // string types may also apply d3-time format + // pick adhoc format first, fallback to column level formats defined in + // datasource + const customFormat = config.d3TimeFormat || savedFormat; + const timeFormat = customFormat || tableTimestampFormat; // When format is "Adaptive Formatting" (smart_date) if (timeFormat === smartDateFormatter.id) { if (isTimeColumn(key)) { // time column use formats based on granularity formatter = getTimeFormatterForGranularity(granularity); - } else if (format) { + } else if (customFormat) { // other columns respect the column-specific format - formatter = getTimeFormatter(format); + formatter = getTimeFormatter(customFormat); } else if (isNumeric(key, records)) { // if column is numeric values, it is considered a timestamp64 formatter = getTimeFormatter(DATABASE_DATETIME); @@ -127,11 +136,11 @@ const processColumns = memoizeOne(function processColumns(props: TableChartProps } else if (timeFormat) { formatter = getTimeFormatter(timeFormat); } - } else if (isMetric) { - formatter = getNumberFormatter(format); } else if (isPercentMetric) { // percent metrics have a default format - formatter = getNumberFormatter(format || PERCENT_3_POINT); + formatter = getNumberFormatter(numberFormat || PERCENT_3_POINT); + } else if (isMetric || numberFormat) { + formatter = getNumberFormatter(numberFormat); } return { key, @@ -140,6 +149,7 @@ const processColumns = memoizeOne(function processColumns(props: TableChartProps isMetric, isPercentMetric, formatter, + config, }; }); return [ diff --git a/plugins/plugin-chart-table/src/types.ts b/plugins/plugin-chart-table/src/types.ts index 8b47a3c15c..963d69786b 100644 --- a/plugins/plugin-chart-table/src/types.ts +++ b/plugins/plugin-chart-table/src/types.ts @@ -31,6 +31,7 @@ import { QueryFormData, SetDataMaskHook, } from '@superset-ui/core'; +import { ColumnConfig } from '@superset-ui/chart-controls'; export type CustomFormatter = (value: DataRecordValue) => string; @@ -43,6 +44,7 @@ export interface DataColumnMeta { formatter?: TimeFormatter | NumberFormatter | CustomFormatter; isMetric?: boolean; isPercentMetric?: boolean; + config?: ColumnConfig; } export interface TableChartData { @@ -67,6 +69,7 @@ export type TableChartFormData = QueryFormData & { table_timestamp_format?: string; table_filter?: boolean; time_grain_sqla?: TimeGranularity; + column_config?: Record; }; export interface TableChartProps extends ChartProps { diff --git a/plugins/plugin-chart-table/src/utils/formatValue.ts b/plugins/plugin-chart-table/src/utils/formatValue.ts index 09a665d38b..23386841a9 100644 --- a/plugins/plugin-chart-table/src/utils/formatValue.ts +++ b/plugins/plugin-chart-table/src/utils/formatValue.ts @@ -41,16 +41,16 @@ function isProbablyHTML(text: string) { export default function formatValue( { formatter }: DataColumnMeta, value: DataRecordValue, -): [boolean, string, string | null] { +): [boolean, string] { if (value === null) { - return [false, 'N/A', 'dt-is-null']; + return [false, 'N/A']; } if (formatter) { // in case percent metric can specify percent format in the future - return [false, formatter(value as number), null]; + return [false, formatter(value as number)]; } if (typeof value === 'string') { - return isProbablyHTML(value) ? [true, xss.process(value), null] : [false, value, null]; + return isProbablyHTML(value) ? [true, xss.process(value)] : [false, value]; } - return [false, value.toString(), null]; + return [false, value.toString()]; } diff --git a/plugins/plugin-chart-table/src/utils/isEqualColumns.ts b/plugins/plugin-chart-table/src/utils/isEqualColumns.ts index 44c0621be2..666cc0d04b 100644 --- a/plugins/plugin-chart-table/src/utils/isEqualColumns.ts +++ b/plugins/plugin-chart-table/src/utils/isEqualColumns.ts @@ -27,6 +27,8 @@ export default function isEqualColumns(propsA: TableChartProps[], propsB: TableC a.datasource.verboseMap === b.datasource.verboseMap && a.formData.tableTimestampFormat === b.formData.tableTimestampFormat && a.formData.timeGrainSqla === b.formData.timeGrainSqla && + JSON.stringify(a.formData.columnConfig || null) === + JSON.stringify(b.formData.columnConfig || null) && isEqualArray(a.formData.metrics, b.formData.metrics) && isEqualArray(a.queriesData?.[0]?.colnames, b.queriesData?.[0]?.colnames) && isEqualArray(a.queriesData?.[0]?.coltypes, b.queriesData?.[0]?.coltypes) diff --git a/yarn.lock b/yarn.lock index 5369f10e43..c577dda442 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,58 +93,33 @@ execa "^4.0.0" fast-glob "^3.2.2" -"@ant-design/colors@^3.1.0": - version "3.2.2" - resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-3.2.2.tgz#5ad43d619e911f3488ebac303d606e66a8423903" - integrity sha512-YKgNbG2dlzqMhA9NtI3/pbY16m3Yl/EeWBRa+lB1X1YaYxHrxNexiQYCLTWO/uDvAjLFMEDU+zR901waBtMtjQ== - dependencies: - tinycolor2 "^1.4.1" - -"@ant-design/colors@^5.0.0": - version "5.0.0" - resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-5.0.0.tgz#46b73b4cc6935b35fc8b84555e8e42c8cfc190e6" - integrity sha512-Pe1rYorgVC1v4f+InDXvIlQH715pO1g7BsOhy/ehX/U6ebPKqojmkYJKU3lF+84Zmvyar7ngZ28hesAa1nWjLg== +"@ant-design/colors@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@ant-design/colors/-/colors-6.0.0.tgz#9b9366257cffcc47db42b9d0203bb592c13c0298" + integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ== dependencies: - "@ctrl/tinycolor" "^3.1.6" - -"@ant-design/css-animation@^1.7.2": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@ant-design/css-animation/-/css-animation-1.7.3.tgz#60a1c970014e86b28f940510d69e503e428f1136" - integrity sha512-LrX0OGZtW+W6iLnTAqnTaoIsRelYeuLZWsrmBJFUXDALQphPsN8cE5DCsmoSlL0QYb94BQxINiuS70Ar/8BNgA== + "@ctrl/tinycolor" "^3.4.0" "@ant-design/icons-svg@^4.0.0": version "4.1.0" resolved "https://registry.yarnpkg.com/@ant-design/icons-svg/-/icons-svg-4.1.0.tgz#480b025f4b20ef7fe8f47d4a4846e4fee84ea06c" integrity sha512-Fi03PfuUqRs76aI3UWYpP864lkrfPo0hluwGqh7NJdLhvH4iRDc3jbJqZIvRDLHKbXrvAfPPV3+zjUccfFvWOQ== -"@ant-design/icons@^4.2.2": - version "4.2.2" - resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.2.2.tgz#6533c5a02aec49238ec4748074845ad7d85a4f5e" - integrity sha512-DrVV+wcupnHS7PehJ6KiTcJtAR5c25UMgjGECCc6pUT9rsvw0AuYG+a4HDjfxEQuDqKTHwW+oX/nIvCymyLE8Q== +"@ant-design/icons@^4.6.2": + version "4.6.2" + resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.6.2.tgz#290f2e8cde505ab081fda63e511e82d3c48be982" + integrity sha512-QsBG2BxBYU/rxr2eb8b2cZ4rPKAPBpzAR+0v6rrZLp/lnyvflLH3tw1vregK+M7aJauGWjIGNdFmUfpAOtw25A== dependencies: - "@ant-design/colors" "^3.1.0" - "@ant-design/icons-svg" "^4.0.0" - "@babel/runtime" "^7.10.4" - classnames "^2.2.6" - insert-css "^2.0.0" - rc-util "^5.0.1" - -"@ant-design/icons@^4.3.0": - version "4.3.0" - resolved "https://registry.yarnpkg.com/@ant-design/icons/-/icons-4.3.0.tgz#420e0cd527486c0fe57f81310d681950fc4cfacf" - integrity sha512-UoIbw4oz/L/msbkgqs2nls2KP7XNKScOxVR54wRrWwnXOzJaGNwwSdYjHQz+5ETf8C53YPpzMOnRX99LFCdeIQ== - dependencies: - "@ant-design/colors" "^5.0.0" + "@ant-design/colors" "^6.0.0" "@ant-design/icons-svg" "^4.0.0" "@babel/runtime" "^7.11.2" classnames "^2.2.6" - insert-css "^2.0.0" - rc-util "^5.0.1" + rc-util "^5.9.4" -"@ant-design/react-slick@~0.27.0": - version "0.27.11" - resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.27.11.tgz#ce788312ed8e64fcba2f7bb4556f47486b407c6e" - integrity sha512-KPJ1lleHW11bameFauI77Lb9N7O/4ulT1kplVdRQykWLv3oKVSGKVaekC3DM/Z0MYmKfCXCucpFnfgGMEHNM+w== +"@ant-design/react-slick@~0.28.1": + version "0.28.2" + resolved "https://registry.yarnpkg.com/@ant-design/react-slick/-/react-slick-0.28.2.tgz#d2826f8a837b86b8d9cb0c38533ee8a491621f1b" + integrity sha512-nkrvXsO29pLToFaBb3MlJY4McaUFR4UHtXTz6A5HBzYmxH4SwKerX54mWdGc/6tKpHvS3vUwjEOt2T5XqZEo8Q== dependencies: "@babel/runtime" "^7.10.4" classnames "^2.2.5" @@ -1866,10 +1841,10 @@ dependencies: find-up "^2.1.0" -"@ctrl/tinycolor@^3.1.6": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.2.0.tgz#77a8a33edb2fdc02318c828be78f6fb3d6c65eb2" - integrity sha512-cP1tbXA1qJp/er2CJaO+Pbe38p7RlhV9WytUxUe79xj++Q6s/jKVvzJ9U2dF9f1/lZAdG+j94A38CsNR+uW4gw== +"@ctrl/tinycolor@^3.4.0": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@ctrl/tinycolor/-/tinycolor-3.4.0.tgz#c3c5ae543c897caa9c2a68630bed355be5f9990f" + integrity sha512-JZButFdZ1+/xAfpguQHoabIXkcqRRKpMrWKBkpEZZyxfY9C1DpADFB8PEqGSTeFr135SaTRfKqGKx5xSCLI7ZQ== "@data-ui/event-flow@^0.0.84": version "0.0.84" @@ -6293,52 +6268,51 @@ ansicolors@~0.2.1: resolved "https://registry.yarnpkg.com/ansicolors/-/ansicolors-0.2.1.tgz#be089599097b74a5c9c4a84a0cdbcdb62bd87aef" integrity sha1-vgiVmQl7dKXJxKhKDNvNtivYeu8= -antd@^4.9.1: - version "4.9.1" - resolved "https://registry.yarnpkg.com/antd/-/antd-4.9.1.tgz#486c6e143e04fbd6e110a9ed9f9333bcba54b0f3" - integrity sha512-q+Uf8xWeUB+O+xELq3tvprj2cEot/JnCAjS24scIadHSFzCkUr1nVcHU7dTtZommx7zQgC2ajWBOCVMmJD/lrw== +antd@^4.9.4: + version "4.14.1" + resolved "https://registry.yarnpkg.com/antd/-/antd-4.14.1.tgz#f04b7323793045fd159aaf18037adc8ce4f5aaeb" + integrity sha512-984zBd4EtsBfCC4dUmDAZfaCphjcm7+ldKBWJHPyheUZL5S3X7ZSz+Ld75XGNFj4pLjcGMi2SwGOr/4hmByNsg== dependencies: - "@ant-design/colors" "^5.0.0" - "@ant-design/css-animation" "^1.7.2" - "@ant-design/icons" "^4.3.0" - "@ant-design/react-slick" "~0.27.0" - "@babel/runtime" "^7.11.2" + "@ant-design/colors" "^6.0.0" + "@ant-design/icons" "^4.6.2" + "@ant-design/react-slick" "~0.28.1" + "@babel/runtime" "^7.12.5" array-tree-filter "^2.1.0" classnames "^2.2.6" copy-to-clipboard "^3.2.0" lodash "^4.17.20" moment "^2.25.3" - omit.js "^2.0.2" rc-cascader "~1.4.0" rc-checkbox "~2.3.0" rc-collapse "~3.1.0" - rc-dialog "~8.4.0" - rc-drawer "~4.1.0" + rc-dialog "~8.5.1" + rc-drawer "~4.3.0" rc-dropdown "~3.2.0" - rc-field-form "~1.17.0" - rc-image "~4.2.0" - rc-input-number "~6.1.0" + rc-field-form "~1.20.0" + rc-image "~5.2.4" + rc-input-number "~7.0.1" rc-mentions "~1.5.0" rc-menu "~8.10.0" rc-motion "^2.4.0" rc-notification "~4.5.2" - rc-pagination "~3.1.2" - rc-picker "~2.4.1" + rc-pagination "~3.1.6" + rc-picker "~2.5.10" rc-progress "~3.1.0" rc-rate "~2.9.0" - rc-resize-observer "^0.2.3" - rc-select "~11.5.3" - rc-slider "~9.6.1" + rc-resize-observer "^1.0.0" + rc-select "~12.1.6" + rc-slider "~9.7.1" rc-steps "~4.1.0" rc-switch "~3.2.0" - rc-table "~7.11.0" + rc-table "~7.13.0" rc-tabs "~11.7.0" rc-textarea "~0.3.0" - rc-tooltip "~5.0.0" - rc-tree "~4.0.0" - rc-tree-select "~4.2.0" - rc-upload "~3.3.1" - rc-util "^5.1.0" + rc-tooltip "~5.1.0" + rc-tree "~4.1.0" + rc-tree-select "~4.3.0" + rc-trigger "^5.2.1" + rc-upload "~4.2.0-alpha.0" + rc-util "^5.9.4" scroll-into-view-if-needed "^2.2.25" warning "^4.0.3" @@ -9457,11 +9431,6 @@ dateformat@^3.0.0, dateformat@^3.0.3: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== -dayjs@^1.8.30: - version "1.9.6" - resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.9.6.tgz#6f0c77d76ac1ff63720dd1197e5cb87b67943d70" - integrity sha512-HngNLtPEBWRo8EFVmHFmSXAjtCX8rGNqeXQI0Gh7wCTSqwaKgPIDqu9m07wABVopNwzvOeCb+2711vQhDlcIXw== - debounce@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" @@ -13163,11 +13132,6 @@ inquirer@^7.0.0, inquirer@^7.1.0: strip-ansi "^6.0.0" through "^2.3.6" -insert-css@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/insert-css/-/insert-css-2.0.0.tgz#eb5d1097b7542f4c79ea3060d3aee07d053880f4" - integrity sha1-610Ql7dUL0x56jBg067gfQU4gPQ= - internal-ip@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/internal-ip/-/internal-ip-4.3.0.tgz#845452baad9d2ca3b69c635a137acb9a0dad0907" @@ -17062,7 +17026,7 @@ octokit-pagination-methods@^1.1.0: resolved "https://registry.yarnpkg.com/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== -omit.js@^2.0.0, omit.js@^2.0.2: +omit.js@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/omit.js/-/omit.js-2.0.2.tgz#dd9b8436fab947a5f3ff214cb2538631e313ec2f" integrity sha512-hJmu9D+bNB40YpL9jYebQl4lsTW6yEHRTroJzNLqQJYHm7c+NQnJGfZmIWh8S3q3KoaxV1aLhV6B3+0N0/kyJg== @@ -18414,24 +18378,24 @@ rc-collapse@~3.1.0: rc-util "^5.2.1" shallowequal "^1.1.0" -rc-dialog@~8.4.0: - version "8.4.3" - resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-8.4.3.tgz#de8650ce7d1fcb6c1f7e065b94a6894b9a5a54a4" - integrity sha512-LHsWXb+2Cy4vEOeJcPvk9M0WSr80Gi438ov5rXt3E6XB4j+53Z+vMFRr+TagnVuOVQRCLmmzT4qutfm2U1OK6w== +rc-dialog@~8.5.0, rc-dialog@~8.5.1: + version "8.5.2" + resolved "https://registry.yarnpkg.com/rc-dialog/-/rc-dialog-8.5.2.tgz#530e289c25a31c15c85a0e8a4ba3f33414bff418" + integrity sha512-3n4taFcjqhTE9uNuzjB+nPDeqgRBTEGBfe46mb1e7r88DgDo0lL4NnxY/PZ6PJKd2tsCt+RrgF/+YeTvJ/Thsw== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.6" rc-motion "^2.3.0" - rc-util "^5.0.1" + rc-util "^5.6.1" -rc-drawer@~4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-4.1.0.tgz#d7bf0bc030300b62d282bc04e053b9acad6b08b4" - integrity sha512-kjeQFngPjdzAFahNIV0EvEBoIKMOnvUsAxpkSPELoD/1DuR4nLafom5ryma+TIxGwkFJ92W6yjsMi1U9aiOTeQ== +rc-drawer@~4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-4.3.1.tgz#356333a7af01b777abd685c96c2ce62efb44f3f3" + integrity sha512-GMfFy4maqxS9faYXEhQ+0cA1xtkddEQzraf6SAdzWbn444DrrLogwYPk1NXSpdXjLCLxgxOj9MYtyYG42JsfXg== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.6" - rc-util "^5.0.1" + rc-util "^5.7.0" rc-dropdown@^3.1.3, rc-dropdown@~3.2.0: version "3.2.0" @@ -18442,30 +18406,29 @@ rc-dropdown@^3.1.3, rc-dropdown@~3.2.0: classnames "^2.2.6" rc-trigger "^5.0.4" -rc-field-form@~1.17.0: - version "1.17.2" - resolved "https://registry.yarnpkg.com/rc-field-form/-/rc-field-form-1.17.2.tgz#81b09d320f9b455673867bf3a1f5b2aac0fd0a15" - integrity sha512-+pufRy5x4G5yHxQ3k1nhgQqyqerPVJQ2jaLGojHjNpmZ2Si20o1KniMLsZxe6X8dfq4ePmH6M3IngfDnS+CrMA== +rc-field-form@~1.20.0: + version "1.20.0" + resolved "https://registry.yarnpkg.com/rc-field-form/-/rc-field-form-1.20.0.tgz#2201092095429f7f020825462835c4086d2baf16" + integrity sha512-jkzsIfXR7ywEYdeAtktt1aLff88wxIPDLpq7KShHNl4wlsWrCE+TzkXBfjvVzYOVZt5GGrD8YDqNO/q6eaR/eA== dependencies: "@babel/runtime" "^7.8.4" async-validator "^3.0.3" - rc-util "^5.0.0" + rc-util "^5.8.0" -rc-image@~4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-4.2.0.tgz#3b7a977f9ecfbac046296c2908d99cb1f8795c65" - integrity sha512-yGqq6wPrIn86hMfC1Hl7M3NNS6zqnl9dvFWJg/StuI86jZBU0rm9rePTfKs+4uiwU3HXxpfsXlaG2p8GWRDLiw== +rc-image@~5.2.4: + version "5.2.4" + resolved "https://registry.yarnpkg.com/rc-image/-/rc-image-5.2.4.tgz#ff1059f937bde6ca918c6f1beb316beba911f255" + integrity sha512-kWOjhZC1OoGKfvWqtDoO9r8WUNswBwnjcstI6rf7HMudz0usmbGvewcWqsOhyaBRJL9+I4eeG+xiAoxV1xi75Q== dependencies: - "@ant-design/icons" "^4.2.2" "@babel/runtime" "^7.11.2" classnames "^2.2.6" - rc-dialog "~8.4.0" + rc-dialog "~8.5.0" rc-util "^5.0.6" -rc-input-number@~6.1.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-6.1.1.tgz#818c426942d1b4dc4d6d2639d741ca67773a9118" - integrity sha512-9t2xf1G0YEism7FAXAvF1huBk7ZNABPBf6NL+3/aDL123WiT/vhhod4cldiDWTM1Yb2EDKR//ZIa546ScdsUaA== +rc-input-number@~7.0.1: + version "7.0.2" + resolved "https://registry.yarnpkg.com/rc-input-number/-/rc-input-number-7.0.2.tgz#913f4f1e734dc0fdf9ad31d077b9b75f5dd3389c" + integrity sha512-9AcD3/D18Oa41xZnBFvJ0fdp6AJkf/en8uKi8E69Ct+sh64qIYbWUXeh1PXhJgrCHIoNNT8OWaTebypT4/d3ZA== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.5" @@ -18541,23 +18504,32 @@ rc-notification@~4.5.2: rc-motion "^2.2.0" rc-util "^5.0.1" -rc-pagination@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.2.tgz#ab5eacd9c51f869e350d2245064babe91bc1f046" - integrity sha512-KbJvkTvRiD51vTIAi0oTARPUHNb0iV6njbDBe8yLkc3PWYDJaszASfuss6YJ98EIxEeGzuEk6xsUAEKWRJgz2g== +rc-overflow@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/rc-overflow/-/rc-overflow-1.0.2.tgz#f56bcd920029979989f576d55084b81f9632c19c" + integrity sha512-GXj4DAyNxm4f57LvXLwhJaZoJHzSge2l2lQq64MZP7NJAfLpQqOLD+v9JMV9ONTvDPZe8kdzR+UMmkAn7qlzFA== + dependencies: + "@babel/runtime" "^7.11.1" + classnames "^2.2.1" + rc-resize-observer "^1.0.0" + rc-util "^5.5.1" + +rc-pagination@~3.1.6: + version "3.1.6" + resolved "https://registry.yarnpkg.com/rc-pagination/-/rc-pagination-3.1.6.tgz#db3c06e50270b52fe272ac527c1fdc2c8d28af1f" + integrity sha512-Pb2zJEt8uxXzYCWx/2qwsYZ3vSS9Eqdw0cJBli6C58/iYhmvutSBqrBJh51Z5UzYc5ZcW5CMeP5LbbKE1J3rpw== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.1" -rc-picker@~2.4.1: - version "2.4.3" - resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.4.3.tgz#ad15ee1d85e4b3e213ec66215ecd39e6a09be995" - integrity sha512-tOIHslTQKpoGNmbpp6YOBwS39dQSvtAuhOm3bWCkkc4jCqUqeR/velCwqefZX1BX4+t1gUMc1dIia9XvOKrEkg== +rc-picker@~2.5.10: + version "2.5.10" + resolved "https://registry.yarnpkg.com/rc-picker/-/rc-picker-2.5.10.tgz#0db17c535a37abbe5d016bdcdfb13d6626f802d0" + integrity sha512-d2or2jql9SSY8CaRPybpbKkXBq3bZ6g88UKyWQZBLTCrc92Xm87RfRC/P3UEQo/CLmia3jVF7IXVi1HmNe2DZA== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.1" date-fns "^2.15.0" - dayjs "^1.8.30" moment "^2.24.0" rc-trigger "^5.0.4" rc-util "^5.4.0" @@ -18580,7 +18552,7 @@ rc-rate@~2.9.0: classnames "^2.2.5" rc-util "^5.0.1" -rc-resize-observer@^0.2.0, rc-resize-observer@^0.2.1, rc-resize-observer@^0.2.3: +rc-resize-observer@^0.2.1, rc-resize-observer@^0.2.3: version "0.2.6" resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-0.2.6.tgz#c1b642f6d1293e34c4e3715f47f69443a167b825" integrity sha512-YX6nYnd6fk7zbuvT6oSDMKiZjyngjHoy+fz+vL3Tez38d/G5iGdaDJa2yE7345G6sc4Mm1IGRUIwclvltddhmA== @@ -18590,36 +18562,33 @@ rc-resize-observer@^0.2.0, rc-resize-observer@^0.2.1, rc-resize-observer@^0.2.3: rc-util "^5.0.0" resize-observer-polyfill "^1.5.1" -rc-select@^11.1.1: - version "11.5.0" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-11.5.0.tgz#360d2762207c2fef2622e7fcc542fc94dfc9d10f" - integrity sha512-izVcxMMo64ZbuYDaB+zsybPjli5Ub6fKM4OeChDqn4MwrHnPjCEsO3bXjeSEXK2LCC2DXQAdr1oYvHGw9QAGVw== +rc-resize-observer@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/rc-resize-observer/-/rc-resize-observer-1.0.0.tgz#97fb89856f62fec32ab6e40933935cf58e2e102d" + integrity sha512-RgKGukg1mlzyGdvzF7o/LGFC8AeoMH9aGzXTUdp6m+OApvmRdUuOscq/Y2O45cJA+rXt1ApWlpFoOIioXL3AGg== dependencies: "@babel/runtime" "^7.10.1" - classnames "2.x" - rc-motion "^2.0.1" - rc-trigger "^5.0.4" - rc-util "^5.0.1" - rc-virtual-list "^3.2.0" - warning "^4.0.3" + classnames "^2.2.1" + rc-util "^5.0.0" + resize-observer-polyfill "^1.5.1" -rc-select@~11.5.3: - version "11.5.3" - resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-11.5.3.tgz#682913f3669596fb794e2b4a5c619974c5ab45d1" - integrity sha512-ASSO4J/ayfbQQ+KOEounIMGhySDHpQtrIuH1WEABOBy8HgKec8kOLmyLH+YIXSUDnTf/gtxmflgFtl7sQ9pkSw== +rc-select@^12.0.0, rc-select@~12.1.6: + version "12.1.7" + resolved "https://registry.yarnpkg.com/rc-select/-/rc-select-12.1.7.tgz#46c14833e57c04fe733a94418edf81def0df5760" + integrity sha512-sLZlfp+U7Typ+jPM5gTi8I4/oJalRw8kyhxZZ9Q4mEfO2p+otd1Chmzhh+wPraBY3IwE0RZM2/x1Leg/kQKk/w== dependencies: "@babel/runtime" "^7.10.1" classnames "2.x" rc-motion "^2.0.1" + rc-overflow "^1.0.0" rc-trigger "^5.0.4" rc-util "^5.0.1" rc-virtual-list "^3.2.0" - warning "^4.0.3" -rc-slider@~9.6.1: - version "9.6.2" - resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.6.2.tgz#7ea1e9494ed90f602e871c43bccfe3057a0c59f6" - integrity sha512-uctdE1768ZmSjCcRmx6ffm/uoW/zl/SOvanvoilWyZ1NRlwkZCa1R20AIJlU9VDJo/FswWnqXqt6iDp2CnDVig== +rc-slider@~9.7.1: + version "9.7.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.7.1.tgz#63535177a74a3ee44f090909e8c6f98426eb9dba" + integrity sha512-r9r0dpFA3PEvxBhIfVi1lVzxuSogWxeY+tGvi2AqMM1rPgaOXQ7WbtT+9kVFkJ9K8TntA/vYPgiCCKfN29KTkw== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.5" @@ -18645,14 +18614,14 @@ rc-switch@~3.2.0: classnames "^2.2.1" rc-util "^5.0.1" -rc-table@~7.11.0: - version "7.11.1" - resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.11.1.tgz#b31f548feeb0fc03a2b650cb1fedbed0a8926bb7" - integrity sha512-Xq7ibC/a2kj8ywLeKhGcv689JZaldjPxxe15h89qGho6/sR9YkIUD07KjLCGFaJ0LkhGBNY1XYv2VOUFGOQuYg== +rc-table@~7.13.0: + version "7.13.3" + resolved "https://registry.yarnpkg.com/rc-table/-/rc-table-7.13.3.tgz#25d5f5ec47ee2d8a293aff18c4c4b8876f78c22b" + integrity sha512-oP4fknjvKCZAaiDnvj+yzBaWcg+JYjkASbeWonU1BbrLcomkpKvMUgPODNEzg0QdXA9OGW0PO86h4goDSW06Kg== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.5" - rc-resize-observer "^0.2.0" + rc-resize-observer "^1.0.0" rc-util "^5.4.0" shallowequal "^1.1.0" @@ -18679,7 +18648,7 @@ rc-textarea@^0.3.0, rc-textarea@~0.3.0: omit.js "^2.0.0" rc-resize-observer "^0.2.3" -rc-tooltip@^5.0.1, rc-tooltip@~5.0.0: +rc-tooltip@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.0.1.tgz#b82c4259604d2cb62ca610ed7932dd37fc6ef61d" integrity sha512-3AnxhUS0j74xAV3khrKw8o6rg+Ima3nw09DJBezMPnX3ImQUAnayWsPSlN1mEnihjA43rcFkGM1emiKE+CXyMQ== @@ -18687,18 +18656,26 @@ rc-tooltip@^5.0.1, rc-tooltip@~5.0.0: "@babel/runtime" "^7.11.2" rc-trigger "^5.0.0" -rc-tree-select@~4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-4.2.0.tgz#ca19163b2ccfe0772fd7b8148266dddd197d0fe1" - integrity sha512-VrrvBiOov6WR44RTGMqSw1Dmodg6Y++EH6a6R0ew43qsV4Ob0FGYRgoX811kImtt2Z+oAPJ6zZXN4WKtsQd3Gw== +rc-tooltip@~5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.1.0.tgz#abb453c463c31a705aa01d268279f4ae6ae3b15f" + integrity sha512-pFqD1JZwNIpbdcefB7k5xREoHAWM/k3yQwYF0iminbmDXERgq4rvBfUwIvlCqqZSM7HDr9hYeYr6ZsVNaKtvCQ== + dependencies: + "@babel/runtime" "^7.11.2" + rc-trigger "^5.0.0" + +rc-tree-select@~4.3.0: + version "4.3.1" + resolved "https://registry.yarnpkg.com/rc-tree-select/-/rc-tree-select-4.3.1.tgz#4881bae5f6a5d696c5f61e52ad9489313f356eb4" + integrity sha512-OeV8u5kBEJ8MbatP04Rh8T3boOHGjdGBTEm1a0bubBbB2GNNhlMOr4ZxezkHYtXf02JdBS/WyydmI/RMjXgtJA== dependencies: "@babel/runtime" "^7.10.1" classnames "2.x" - rc-select "^11.1.1" + rc-select "^12.0.0" rc-tree "^4.0.0" rc-util "^5.0.5" -rc-tree@^4.0.0, rc-tree@~4.0.0: +rc-tree@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-4.0.0.tgz#2f972b4a5e23ea17df05ec9f7ec43de350bea3bf" integrity sha512-C2xlkA+/IypkHBPzbpAJGVWJh2HjeRbYCusA/m5k09WT6hQT0nC7LtLVmnb7QZecdBQPhoOgQh8gPwBR+xEMjQ== @@ -18709,6 +18686,17 @@ rc-tree@^4.0.0, rc-tree@~4.0.0: rc-util "^5.0.0" rc-virtual-list "^3.0.1" +rc-tree@~4.1.0: + version "4.1.5" + resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-4.1.5.tgz#734ab1bfe835e78791be41442ca0e571147ab6fa" + integrity sha512-q2vjcmnBDylGZ9/ZW4F9oZMKMJdbFWC7um+DAQhZG1nqyg1iwoowbBggUDUaUOEryJP+08bpliEAYnzJXbI5xQ== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-util "^5.0.0" + rc-virtual-list "^3.0.1" + rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.1.2.tgz#f0f89bba2318699e704492bddb20506ecd8f8916" @@ -18720,16 +18708,27 @@ rc-trigger@^5.0.0, rc-trigger@^5.0.4, rc-trigger@^5.1.2: rc-motion "^2.0.0" rc-util "^5.5.0" -rc-upload@~3.3.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-3.3.1.tgz#ad8658b2a796031930b35d2b07ab312b7cd4c9ed" - integrity sha512-KWkJbVM9BwU8qi/2jZwmZpAcdRzDkuyfn/yAOLu+nm47dyd6//MtxzQD3XZDFkC6jQ6D5FmlKn6DhmOfV3v43w== +rc-trigger@^5.2.1: + version "5.2.3" + resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-5.2.3.tgz#8c55046ab432d7b52d51c69afb57ebb5bbe37e17" + integrity sha512-6Fokao07HUbqKIDkDRFEM0AGZvsvK0Fbp8A/KFgl1ngaqfO1nY037cISCG1Jm5fxImVsXp9awdkP7Vu5cxjjog== + dependencies: + "@babel/runtime" "^7.11.2" + classnames "^2.2.6" + rc-align "^4.0.0" + rc-motion "^2.0.0" + rc-util "^5.5.0" + +rc-upload@~4.2.0-alpha.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/rc-upload/-/rc-upload-4.2.0.tgz#5e21cab29f10ecb69d71cfb9055912d0e1e08ee0" + integrity sha512-BXtvBs1PnwLjaUzBBU5z4yb9NMSaxc6mUIoPmS9LUAzaTz12L3TLrwu+8dnopYUiyLmYFS3LEO7aUfEWBqJfSA== dependencies: "@babel/runtime" "^7.10.1" classnames "^2.2.5" rc-util "^5.2.0" -rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.5, rc-util@^5.0.6, rc-util@^5.0.7, rc-util@^5.1.0, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.4.0, rc-util@^5.5.0: +rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.5, rc-util@^5.0.6, rc-util@^5.0.7, rc-util@^5.2.0, rc-util@^5.2.1, rc-util@^5.3.0, rc-util@^5.4.0, rc-util@^5.5.0: version "5.5.0" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.5.0.tgz#76321bcb5c12f01f42bff9b971f170ff19506e5a" integrity sha512-YJB+zZGvCll/bhxXRVLAekr7lOvTgqMlRIhgINoINfUek7wQvi5sft46NOi3yYUYhocpuW4k8+5okW46sBsZAQ== @@ -18737,6 +18736,15 @@ rc-util@^5.0.0, rc-util@^5.0.1, rc-util@^5.0.5, rc-util@^5.0.6, rc-util@^5.0.7, react-is "^16.12.0" shallowequal "^1.1.0" +rc-util@^5.5.1, rc-util@^5.6.1, rc-util@^5.7.0, rc-util@^5.8.0, rc-util@^5.9.4: + version "5.9.5" + resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.9.5.tgz#45703fc735f4110cd387e0fb4b891522010ab9dd" + integrity sha512-YQFlk5j8aEOpkJV5VibcCYk8prve8s9BALiN561FoL9OfQRk41nvfD8jENIvsDsbfq9AFO7Iq7YFEENJkn9Hog== + dependencies: + "@babel/runtime" "^7.12.5" + react-is "^16.12.0" + shallowequal "^1.1.0" + rc-virtual-list@^3.0.1, rc-virtual-list@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/rc-virtual-list/-/rc-virtual-list-3.2.2.tgz#95f8f0c4238e081f4a998354492632eed6d71924"