refactor(monorepo): move superset-ui to superset(stage 2) (#17552)

This commit is contained in:
Yongjie Zhao
2021-11-30 08:29:57 +08:00
committed by GitHub
parent bfba4f1689
commit 3c41ff68a4
1315 changed files with 27755 additions and 15167 deletions

View File

@@ -0,0 +1,132 @@
/* eslint-disable no-underscore-dangle */
/**
* 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 {
Annotation,
AnnotationData,
AnnotationLayer,
AnnotationOpacity,
AnnotationType,
evalExpression,
FormulaAnnotationLayer,
isRecordAnnotationResult,
isTableAnnotationLayer,
isTimeseriesAnnotationResult,
TimeseriesDataRecord,
} from '@superset-ui/core';
export function evalFormula(
formula: FormulaAnnotationLayer,
data: TimeseriesDataRecord[],
): [Date, number][] {
const { value: expression } = formula;
return data.map(row => [
new Date(Number(row.__timestamp)),
evalExpression(expression, row.__timestamp as number),
]);
}
export function parseAnnotationOpacity(opacity?: AnnotationOpacity): number {
switch (opacity) {
case AnnotationOpacity.Low:
return 0.2;
case AnnotationOpacity.Medium:
return 0.5;
case AnnotationOpacity.High:
return 0.8;
default:
return 1;
}
}
const NATIVE_COLUMN_NAMES = {
descriptionColumns: ['long_descr'],
intervalEndColumn: 'end_dttm',
timeColumn: 'start_dttm',
titleColumn: 'short_descr',
};
export function extractRecordAnnotations(
annotationLayer: AnnotationLayer,
annotationData: AnnotationData,
): Annotation[] {
const { name } = annotationLayer;
const result = annotationData[name];
if (isRecordAnnotationResult(result)) {
const { records } = result;
const {
descriptionColumns = [],
intervalEndColumn = '',
timeColumn = '',
titleColumn = '',
} = isTableAnnotationLayer(annotationLayer)
? annotationLayer
: NATIVE_COLUMN_NAMES;
return records.map(record => ({
descriptions: descriptionColumns.map(
column => (record[column] || '') as string,
) as string[],
intervalEnd: (record[intervalEndColumn] || '') as string,
time: (record[timeColumn] || '') as string,
title: (record[titleColumn] || '') as string,
}));
}
throw new Error('Please rerun the query.');
}
export function formatAnnotationLabel(
name?: string,
title?: string,
descriptions: string[] = [],
): string {
const labels: string[] = [];
const titleLabels: string[] = [];
const filteredDescriptions = descriptions.filter(
description => !!description,
);
if (name) titleLabels.push(name);
if (title) titleLabels.push(title);
if (titleLabels.length > 0) labels.push(titleLabels.join(' - '));
if (filteredDescriptions.length > 0)
labels.push(filteredDescriptions.join('\n'));
return labels.join('\n\n');
}
export function extractAnnotationLabels(
layers: AnnotationLayer[],
data: AnnotationData,
): string[] {
const formulaAnnotationLabels = layers
.filter(anno => anno.annotationType === AnnotationType.Formula && anno.show)
.map(anno => anno.name);
const timeseriesAnnotationLabels = layers
.filter(
anno => anno.annotationType === AnnotationType.Timeseries && anno.show,
)
.flatMap(anno => {
const result = data[anno.name];
return isTimeseriesAnnotationResult(result)
? result.map(annoSeries => annoSeries.key)
: [];
});
return formulaAnnotationLabels.concat(timeseriesAnnotationLabels);
}

View File

@@ -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 { validateNumber } from '@superset-ui/core';
// eslint-disable-next-line import/prefer-default-export
export function parseYAxisBound(
bound?: string | number | null,
): number | undefined {
if (bound === undefined || bound === null || Number.isNaN(Number(bound))) {
return undefined;
}
return Number(bound);
}
export function parseNumbersList(value: string, delim = ';') {
if (!value || !value.trim()) return [];
return value.split(delim).map(num => {
if (validateNumber(num)) throw new Error('All values must be numeric');
return Number(num);
});
}

View File

@@ -0,0 +1,140 @@
/**
* 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 { TimeseriesDataRecord, NumberFormatter } from '@superset-ui/core';
import { CallbackDataParams, OptionName } from 'echarts/types/src/util/types';
import { TooltipMarker } from 'echarts/types/src/util/format';
import {
ForecastSeriesContext,
ForecastSeriesEnum,
ProphetValue,
} from '../types';
import { sanitizeHtml } from './series';
const seriesTypeRegex = new RegExp(
`(.+)(${ForecastSeriesEnum.ForecastLower}|${ForecastSeriesEnum.ForecastTrend}|${ForecastSeriesEnum.ForecastUpper})$`,
);
export const extractForecastSeriesContext = (
seriesName: OptionName,
): ForecastSeriesContext => {
const name = seriesName as string;
const regexMatch = seriesTypeRegex.exec(name);
if (!regexMatch) return { name, type: ForecastSeriesEnum.Observation };
return {
name: regexMatch[1],
type: regexMatch[2] as ForecastSeriesEnum,
};
};
export const extractForecastSeriesContexts = (
seriesNames: string[],
): { [key: string]: ForecastSeriesEnum[] } =>
seriesNames.reduce((agg, name) => {
const context = extractForecastSeriesContext(name);
const currentContexts = agg[context.name] || [];
currentContexts.push(context.type);
return { ...agg, [context.name]: currentContexts };
}, {} as { [key: string]: ForecastSeriesEnum[] });
export const extractProphetValuesFromTooltipParams = (
params: (CallbackDataParams & { seriesId: string })[],
): Record<string, ProphetValue> => {
const values: Record<string, ProphetValue> = {};
params.forEach(param => {
const { marker, seriesId, value } = param;
const context = extractForecastSeriesContext(seriesId);
const numericValue = (value as [Date, number])[1];
if (numericValue) {
if (!(context.name in values))
values[context.name] = {
marker: marker || '',
};
const prophetValues = values[context.name];
if (context.type === ForecastSeriesEnum.Observation)
prophetValues.observation = numericValue;
if (context.type === ForecastSeriesEnum.ForecastTrend)
prophetValues.forecastTrend = numericValue;
if (context.type === ForecastSeriesEnum.ForecastLower)
prophetValues.forecastLower = numericValue;
if (context.type === ForecastSeriesEnum.ForecastUpper)
prophetValues.forecastUpper = numericValue;
}
});
return values;
};
export const formatProphetTooltipSeries = ({
seriesName,
observation,
forecastTrend,
forecastLower,
forecastUpper,
marker,
formatter,
}: ProphetValue & {
seriesName: string;
marker: TooltipMarker;
formatter: NumberFormatter;
}): string => {
let row = `${marker}${sanitizeHtml(seriesName)}: `;
let isObservation = false;
if (observation) {
isObservation = true;
row += `${formatter(observation)}`;
}
if (forecastTrend) {
if (isObservation) row += ', ';
row += `ŷ = ${formatter(forecastTrend)}`;
}
if (forecastLower && forecastUpper)
// the lower bound needs to be added to the upper bound
row = `${row.trim()} (${formatter(forecastLower)}, ${formatter(
forecastLower + forecastUpper,
)})`;
return `${row.trim()}`;
};
export function rebaseTimeseriesDatum(
data: TimeseriesDataRecord[],
verboseMap: Record<string, string> = {},
) {
const keys = data.length > 0 ? Object.keys(data[0]) : [];
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return data.map(row => {
const newRow: TimeseriesDataRecord = { __timestamp: '' };
keys.forEach(key => {
const forecastContext = extractForecastSeriesContext(key);
const lowerKey = `${forecastContext.name}${ForecastSeriesEnum.ForecastLower}`;
let value = row[key] as number;
if (
forecastContext.type === ForecastSeriesEnum.ForecastUpper &&
keys.includes(lowerKey) &&
value !== null &&
row[lowerKey] !== null
) {
value -= row[lowerKey] as number;
}
const newKey =
key !== '__timestamp' && verboseMap[key] ? verboseMap[key] : key;
newRow[newKey] = value;
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
return newRow;
});
}

View File

@@ -0,0 +1,228 @@
/* eslint-disable no-underscore-dangle */
/**
* 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 {
ChartDataResponseResult,
DataRecord,
DataRecordValue,
ensureIsArray,
GenericDataType,
NumberFormatter,
TimeFormatter,
TimeseriesDataRecord,
} from '@superset-ui/core';
import { format, LegendComponentOption, SeriesOption } from 'echarts';
import { NULL_STRING, TIMESERIES_CONSTANTS } from '../constants';
import { LegendOrientation, LegendType } from '../types';
import { defaultLegendPadding } from '../defaults';
function isDefined<T>(value: T | undefined | null): boolean {
return value !== undefined && value !== null;
}
export function extractTimeseriesSeries(
data: TimeseriesDataRecord[],
opts: { fillNeighborValue?: number } = {},
): SeriesOption[] {
const { fillNeighborValue } = opts;
if (data.length === 0) return [];
const rows: TimeseriesDataRecord[] = data.map(datum => ({
...datum,
__timestamp:
datum.__timestamp || datum.__timestamp === 0
? new Date(datum.__timestamp)
: null,
}));
return Object.keys(rows[0])
.filter(key => key !== '__timestamp')
.map(key => ({
id: key,
name: key,
data: rows.map((row, idx) => {
const isNextToDefinedValue =
isDefined(rows[idx - 1]?.[key]) || isDefined(rows[idx + 1]?.[key]);
return [
row.__timestamp,
!isDefined(row[key]) &&
isNextToDefinedValue &&
fillNeighborValue !== undefined
? fillNeighborValue
: row[key],
];
}),
}));
}
export function formatSeriesName(
name: DataRecordValue | undefined,
{
numberFormatter,
timeFormatter,
coltype,
}: {
numberFormatter?: NumberFormatter;
timeFormatter?: TimeFormatter;
coltype?: GenericDataType;
} = {},
): string {
if (name === undefined || name === null) {
return NULL_STRING;
}
if (typeof name === 'number') {
return numberFormatter ? numberFormatter(name) : name.toString();
}
if (typeof name === 'boolean') {
return name.toString();
}
if (name instanceof Date || coltype === GenericDataType.TEMPORAL) {
const d = name instanceof Date ? name : new Date(name);
return timeFormatter ? timeFormatter(d) : d.toISOString();
}
return name;
}
export const getColtypesMapping = ({
coltypes = [],
colnames = [],
}: ChartDataResponseResult): Record<string, GenericDataType> =>
colnames.reduce(
(accumulator, item, index) => ({ ...accumulator, [item]: coltypes[index] }),
{},
);
export function extractGroupbyLabel({
datum = {},
groupby,
numberFormatter,
timeFormatter,
coltypeMapping = {},
}: {
datum?: DataRecord;
groupby?: string[] | null;
numberFormatter?: NumberFormatter;
timeFormatter?: TimeFormatter;
coltypeMapping: Record<string, GenericDataType>;
}): string {
return ensureIsArray(groupby)
.map(val =>
formatSeriesName(datum[val], {
numberFormatter,
timeFormatter,
...(coltypeMapping[val] && { coltype: coltypeMapping[val] }),
}),
)
.join(', ');
}
export function getLegendProps(
type: LegendType,
orientation: LegendOrientation,
show: boolean,
zoomable = false,
): LegendComponentOption | LegendComponentOption[] {
const legend: LegendComponentOption | LegendComponentOption[] = {
orient: [LegendOrientation.Top, LegendOrientation.Bottom].includes(
orientation,
)
? 'horizontal'
: 'vertical',
show,
type,
};
switch (orientation) {
case LegendOrientation.Left:
legend.left = 0;
break;
case LegendOrientation.Right:
legend.right = 0;
legend.top = zoomable ? TIMESERIES_CONSTANTS.legendRightTopOffset : 0;
break;
case LegendOrientation.Bottom:
legend.bottom = 0;
break;
case LegendOrientation.Top:
default:
legend.top = 0;
legend.right = zoomable ? TIMESERIES_CONSTANTS.legendTopRightOffset : 0;
break;
}
return legend;
}
export function getChartPadding(
show: boolean,
orientation: LegendOrientation,
margin?: string | number | null,
padding?: { top?: number; bottom?: number; left?: number; right?: number },
): {
bottom: number;
left: number;
right: number;
top: number;
} {
let legendMargin;
if (!show) {
legendMargin = 0;
} else if (
margin === null ||
margin === undefined ||
typeof margin === 'string'
) {
legendMargin = defaultLegendPadding[orientation];
} else {
legendMargin = margin;
}
const { bottom = 0, left = 0, right = 0, top = 0 } = padding || {};
return {
left: left + (orientation === LegendOrientation.Left ? legendMargin : 0),
right: right + (orientation === LegendOrientation.Right ? legendMargin : 0),
top: top + (orientation === LegendOrientation.Top ? legendMargin : 0),
bottom:
bottom + (orientation === LegendOrientation.Bottom ? legendMargin : 0),
};
}
export function dedupSeries(series: SeriesOption[]): SeriesOption[] {
const counter = new Map<string, number>();
return series.map(row => {
let { id } = row;
if (id === undefined) return row;
id = String(id);
const count = counter.get(id) || 0;
const suffix = count > 0 ? ` (${count})` : '';
counter.set(id, count + 1);
return {
...row,
id: `${id}${suffix}`,
};
});
}
export function sanitizeHtml(text: string): string {
return format.encodeHTML(text);
}
// TODO: Better use other method to maintain this state
export const currentSeries = {
name: '',
legend: '',
};