feat(fe): upgrade `superset-frontend` to Typescript v5 (#31979)

Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
Đỗ Trọng Hải 2025-01-29 18:40:33 +07:00 committed by GitHub
parent a21f184058
commit 19e8a7049b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
141 changed files with 1095 additions and 572 deletions

View File

@ -290,7 +290,7 @@
"thread-loader": "^4.0.4",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.1",
"typescript": "^4.8.4",
"typescript": "5.1.6",
"vm-browserify": "^1.1.2",
"webpack": "^5.97.1",
"webpack-bundle-analyzer": "^4.10.1",
@ -13499,6 +13499,38 @@
"node": ">=12.0.0"
}
},
"node_modules/@wdio/config/node_modules/@types/node": {
"version": "18.19.74",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz",
"integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@wdio/config/node_modules/@wdio/types": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz",
"integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^18.0.0",
"got": "^11.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "^4.6.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@wdio/config/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
@ -13543,6 +13575,29 @@
"node": ">=10"
}
},
"node_modules/@wdio/config/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/@wdio/config/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
},
"node_modules/@wdio/logger": {
"version": "7.26.0",
"resolved": "https://registry.npmjs.org/@wdio/logger/-/logger-7.26.0.tgz",
@ -13586,7 +13641,32 @@
"node": ">=12.0.0"
}
},
"node_modules/@wdio/types": {
"node_modules/@wdio/utils": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.30.2.tgz",
"integrity": "sha512-np7I+smszFUennbQKdzbMN/zUL3s3EZq9pCCUcTRjjs9TE4tnn0wfmGdoz2o7REYu6kn9NfFFJyVIM2VtBbKEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@wdio/logger": "7.26.0",
"@wdio/types": "7.30.2",
"p-iteration": "^1.1.8"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@wdio/utils/node_modules/@types/node": {
"version": "18.19.74",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz",
"integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@wdio/utils/node_modules/@wdio/types": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz",
"integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==",
@ -13608,38 +13688,29 @@
}
}
},
"node_modules/@wdio/types/node_modules/@types/node": {
"version": "18.19.74",
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.74.tgz",
"integrity": "sha512-HMwEkkifei3L605gFdV+/UwtpxP6JSzM+xFk2Ia6DNFSwSVBRh9qp5Tgf4lNFOMfPVuU0WnkcWpXZpgn5ufO4A==",
"node_modules/@wdio/utils/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~5.26.4"
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/@wdio/types/node_modules/undici-types": {
"node_modules/@wdio/utils/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true,
"license": "MIT"
},
"node_modules/@wdio/utils": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/utils/-/utils-7.30.2.tgz",
"integrity": "sha512-np7I+smszFUennbQKdzbMN/zUL3s3EZq9pCCUcTRjjs9TE4tnn0wfmGdoz2o7REYu6kn9NfFFJyVIM2VtBbKEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@wdio/logger": "7.26.0",
"@wdio/types": "7.30.2",
"p-iteration": "^1.1.8"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz",
@ -49029,16 +49100,16 @@
}
},
"node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"version": "5.1.6",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz",
"integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/typescript-json-schema": {
@ -50328,6 +50399,44 @@
"undici-types": "~5.26.4"
}
},
"node_modules/webdriver/node_modules/@wdio/types": {
"version": "7.30.2",
"resolved": "https://registry.npmjs.org/@wdio/types/-/types-7.30.2.tgz",
"integrity": "sha512-uZ8o7FX8RyBsaXiOWa59UKTCHTtADNvOArYTcHNEIzt+rh4JdB/uwqfc8y4TCNA2kYm7PWaQpUFwpStLeg0H1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "^18.0.0",
"got": "^11.8.1"
},
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"typescript": "^4.6.2"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/webdriver/node_modules/typescript": {
"version": "4.9.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"dev": true,
"license": "Apache-2.0",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
}
},
"node_modules/webdriver/node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",

View File

@ -357,7 +357,7 @@
"thread-loader": "^4.0.4",
"ts-jest": "^29.2.5",
"ts-loader": "^9.5.1",
"typescript": "^4.8.4",
"typescript": "5.1.6",
"vm-browserify": "^1.1.2",
"webpack": "^5.97.1",
"webpack-bundle-analyzer": "^4.10.1",

View File

@ -363,6 +363,13 @@ export type CustomControlItem = {
config: BaseControlConfig<any, any, any>;
};
export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
typeof obj === 'object' &&
obj !== null &&
typeof ('name' in obj && obj.name) === 'string' &&
typeof ('config' in obj && obj.config) === 'object' &&
(obj as CustomControlItem).config !== null;
// use ReactElement instead of ReactNode because `string`, `number`, etc. may
// interfere with other ControlSetItem types
export type ExpandedControlItem = CustomControlItem | ReactElement | null;

View File

@ -20,8 +20,10 @@ import { AdhocColumn } from '@superset-ui/core';
import {
ColumnMeta,
ControlPanelSectionConfig,
CustomControlItem,
isColumnMeta,
isControlPanelSectionConfig,
isCustomControlItem,
isSavedExpression,
} from '../src';
@ -43,6 +45,13 @@ const CONTROL_PANEL_SECTION_CONFIG: ControlPanelSectionConfig = {
description: 'My Description',
controlSetRows: [],
};
const CUSTOM_CONTROL_ITEM: CustomControlItem = {
name: 'Custom Control Item',
config: {
type: 'config',
foo: 'bar',
},
};
test('isColumnMeta returns false for AdhocColumn', () => {
expect(isColumnMeta(ADHOC_COLUMN)).toEqual(false);
@ -73,3 +82,11 @@ test('isControlPanelSectionConfig returns true for section', () => {
test('isControlPanelSectionConfig returns true for null value', () => {
expect(isControlPanelSectionConfig(null)).toEqual(false);
});
test('isCustomControlItem returns true for proper CustomControlItem', () => {
expect(isCustomControlItem(CUSTOM_CONTROL_ITEM)).toEqual(true);
});
test('isCustomControlItem returns false for generic object', () => {
expect(isCustomControlItem({})).toEqual(false);
});

View File

@ -105,7 +105,7 @@ export function parseStringResponse(str: string): string {
}
export function getErrorFromStatusCode(status: number): string | null {
return ERROR_CODE_LOOKUP[status] || null;
return ERROR_CODE_LOOKUP[status as keyof typeof ERROR_CODE_LOOKUP] || null;
}
export function retrieveErrorMessage(

View File

@ -62,6 +62,10 @@ export interface FreeFormAdhocFilter extends BaseAdhocFilter {
sqlExpression: string;
}
export interface LatestPartitionAdhocFilter extends BaseAdhocFilter {
datasource?: { schema?: string; datasource_name?: string };
}
export type AdhocFilter = SimpleAdhocFilter | FreeFormAdhocFilter;
//---------------------------------------------------

View File

@ -349,15 +349,17 @@ export type Query = {
};
export type QueryResults = {
results: {
displayLimitReached: boolean;
columns: QueryColumn[];
data: Record<string, unknown>[];
expanded_columns: QueryColumn[];
selected_columns: QueryColumn[];
query: { limit: number };
query_id?: number;
};
results: InnerQueryResults;
};
export type InnerQueryResults = {
displayLimitReached: boolean;
columns: QueryColumn[];
data: Record<string, unknown>[];
expanded_columns: QueryColumn[];
selected_columns: QueryColumn[];
query: { limit: number };
query_id?: number;
};
export type QueryResponse = Query & QueryResults;

View File

@ -79,6 +79,9 @@ export interface ChartDataResponseResult {
| 'timed_out';
from_dttm: number | null;
to_dttm: number | null;
// TODO(hainenber): define proper type for below attributes
rejected_filters?: any[];
applied_filters?: any[];
}
export interface TimeseriesChartDataResponseResult

View File

@ -93,7 +93,7 @@ export function initFeatureFlags(featureFlags?: FeatureFlagMap) {
export function isFeatureEnabled(feature: FeatureFlag): boolean {
try {
return !!window.featureFlags[feature];
return !!window.featureFlags[feature as keyof FeatureFlagMap];
} catch (error) {
logger.error(`Failed to query feature flag ${feature}`);
}

View File

@ -19,7 +19,7 @@
import { t } from '../translation';
export default function validateInteger(v: unknown) {
export default function validateInteger(v: any) {
if (
(typeof v === 'string' &&
v.trim().length > 0 &&

View File

@ -202,4 +202,82 @@ describe('customTimeRangeDecode', () => {
matchedFlag: false,
});
});
it('9) empty string returns default', () => {
const SEVEN_DAYS_AGO = new Date();
SEVEN_DAYS_AGO.setHours(0, 0, 0, 0);
const MIDNIGHT = new Date();
MIDNIGHT.setHours(0, 0, 0, 0);
expect(customTimeRangeDecode('')).toEqual({
customRange: {
sinceDatetime: SEVEN_DAYS_AGO.setDate(
SEVEN_DAYS_AGO.getDate() - 7,
).toString(),
sinceMode: 'relative',
sinceGrain: 'day',
sinceGrainValue: -7,
untilDatetime: MIDNIGHT.toString(),
untilMode: 'specific',
untilGrain: 'day',
untilGrainValue: 7,
anchorMode: 'now',
anchorValue: 'now',
},
matchedFlag: false,
});
});
it('10) both undefined returns default', () => {
const SEVEN_DAYS_AGO = new Date();
SEVEN_DAYS_AGO.setHours(0, 0, 0, 0);
const MIDNIGHT = new Date();
MIDNIGHT.setHours(0, 0, 0, 0);
expect(customTimeRangeDecode('undefined : undefined')).toEqual({
customRange: {
sinceDatetime: SEVEN_DAYS_AGO.setDate(
SEVEN_DAYS_AGO.getDate() - 7,
).toString(),
sinceMode: 'relative',
sinceGrain: 'day',
sinceGrainValue: -7,
untilDatetime: MIDNIGHT.toString(),
untilMode: 'specific',
untilGrain: 'day',
untilGrainValue: 7,
anchorMode: 'now',
anchorValue: 'now',
},
matchedFlag: false,
});
});
it('11) 1 side undefined returns default', () => {
const SEVEN_DAYS_AGO = new Date();
SEVEN_DAYS_AGO.setHours(0, 0, 0, 0);
const MIDNIGHT = new Date();
MIDNIGHT.setHours(0, 0, 0, 0);
expect(customTimeRangeDecode('undefined : now')).toEqual({
customRange: {
sinceDatetime: SEVEN_DAYS_AGO.setDate(
SEVEN_DAYS_AGO.getDate() - 7,
).toString(),
sinceMode: 'relative',
sinceGrain: 'day',
sinceGrainValue: -7,
untilDatetime: MIDNIGHT.toString(),
untilMode: 'specific',
untilGrain: 'day',
untilGrainValue: 7,
anchorMode: 'now',
anchorValue: 'now',
},
matchedFlag: false,
});
});
});

View File

@ -54,7 +54,7 @@ function getCategories(fd: QueryFormData, data: JsonObject[]) {
const fixedColor = [c.r, c.g, c.b, 255 * c.a];
const appliedScheme = fd.color_scheme;
const colorFn = getScale(appliedScheme);
const categories = {};
const categories: Record<any, { color: any; enabled: boolean }> = {};
data.forEach(d => {
if (d.cat_color != null && !categories.hasOwnProperty(d.cat_color)) {
let color;

View File

@ -97,6 +97,7 @@ const DeckMulti = (props: DeckMultiProps) => {
endpoint: url,
})
.then(({ json }) => {
// @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators.
const layer = layerGenerators[subsliceCopy.form_data.viz_type](
subsliceCopy.form_data,
json,

View File

@ -59,7 +59,7 @@ export type LegendProps = {
format: string | null;
forceCategorical?: boolean;
position?: null | 'tl' | 'tr' | 'bl' | 'br';
categories: Record<string, { enabled: boolean; color: number[] }>;
categories: Record<string, { enabled: boolean; color: number[] | undefined }>;
toggleCategory?: (key: string) => void;
showSingleCategory?: (key: string) => void;
};
@ -101,7 +101,7 @@ const Legend = ({
}
const categories = Object.entries(categoriesObject).map(([k, v]) => {
const style = { color: `rgba(${v.color.join(', ')})` };
const style = { color: `rgba(${v.color?.join(', ')})` };
const icon = v.enabled ? '\u25FC' : '\u25FB';
return (

View File

@ -59,7 +59,7 @@ const alterProps = (props: JsonObject, propOverrides: JsonObject) => {
const newProps: JsonObject = {};
Object.keys(props).forEach(k => {
if (k in propertyMap) {
newProps[propertyMap[k]] = props[k];
newProps[propertyMap[k as keyof typeof propertyMap]] = props[k];
} else {
newProps[k] = props[k];
}

View File

@ -122,7 +122,11 @@ export function getAggFunc(
sortedArr = arr.sort(d3ascending);
}
return d3quantile(sortedArr, percentiles[type], acc);
return d3quantile(
sortedArr,
percentiles[type as keyof typeof percentiles],
acc,
);
};
} else if (type in d3functions) {
d3func = d3functions[type];

View File

@ -172,8 +172,11 @@ export function getBuckets(
) {
const breakPoints = getBreakPoints(fd, features, accessor);
const colorScaler = getBreakPointColorScaler(fd, features, accessor);
const buckets = {};
breakPoints.slice(1).forEach((value, i) => {
const buckets: Record<
string,
{ color: [number, number, number, number] | undefined; enabled: boolean }
> = {};
breakPoints.slice(1).forEach((_, i) => {
const range = `${breakPoints[i]} - ${breakPoints[i + 1]}`;
const mid =
0.5 * (parseFloat(breakPoints[i]) + parseFloat(breakPoints[i + 1]));

View File

@ -36,7 +36,7 @@ export function getExploreLongUrl(
formData: JsonObject,
endpointType: string,
allowOverflow = true,
extraSearch = {},
extraSearch: Record<string, any> = {},
): string | undefined {
if (!formData.datasource) {
return undefined;

View File

@ -34,19 +34,26 @@ const GLOBAL_CONTEXT = {
d3array,
};
type GlobalContext = {
console: Console;
_: _.UnderscoreStatic;
colors: typeof colors;
d3array: typeof d3array;
};
// Copied/modified from https://github.com/hacksparrow/safe-eval/blob/master/index.js
export default function sandboxedEval(
code: string,
context?: Context,
opts?: RunningScriptOptions | string,
) {
const sandbox = {};
const sandbox: Context = {};
const resultKey = `SAFE_EVAL_${Math.floor(Math.random() * 1000000)}`;
sandbox[resultKey] = {};
const codeToEval = `${resultKey}=${code}`;
const sandboxContext = { ...GLOBAL_CONTEXT, ...context };
const sandboxContext: GlobalContext = { ...GLOBAL_CONTEXT, ...context };
Object.keys(sandboxContext).forEach(key => {
sandbox[key] = sandboxContext[key];
sandbox[key] = sandboxContext[key as keyof GlobalContext];
});
try {
vm.runInNewContext(codeToEval, sandbox, opts);

View File

@ -71,7 +71,7 @@ export const selectedChartMutator = (
return [];
}
const data: Record<string, any> = [];
const data: any[] = [];
if (value && typeof value === 'string') {
const parsedValue = JSON.parse(value);
let itemFound = false;

View File

@ -208,7 +208,7 @@ export const stripGeomColumnFromLabelMap = (
labelMap: { [key: string]: string[] },
geomColumn: string,
) => {
const newLabelMap = {};
const newLabelMap: Record<string, string[]> = {};
Object.entries(labelMap).forEach(([key, value]) => {
if (key === geomColumn) {
return;

View File

@ -75,6 +75,7 @@ describe('layerUtil', () => {
// @ts-ignore
expect(style!.length).toEqual(3);
// @ts-ignore upgrade `ol` package for better type of StyleLike type.
const colorAtLayer = style![1].getImage().getFill().getColor();
expect(colorToExpect).toEqual(colorAtLayer);
});

View File

@ -165,7 +165,8 @@ export default function transformProps(chartProps: ChartProps) {
percentDifferenceNum = (bigNumber - prevNumber) / Math.abs(prevNumber);
}
const compType = compTitles[formData.timeComparison];
const compType =
compTitles[formData.timeComparison as keyof typeof compTitles];
bigNumber = numberFormatter(bigNumber);
prevNumber = numberFormatter(prevNumber);
valueDifference = numberFormatter(valueDifference);

View File

@ -35,7 +35,7 @@ const getFontSizeMapping = (
proportionValues: number[],
actualSizes: number[],
) =>
proportionValues.reduce((acc, value, index) => {
proportionValues.reduce<Record<number, number>>((acc, value, index) => {
acc[value] = actualSizes[index] ?? actualSizes[actualSizes.length - 1];
return acc;
}, {});

View File

@ -112,12 +112,15 @@ export default {
Array.isArray(colnames) && Array.isArray(coltypes)
? colnames
.filter(
(colname: string, index: number) =>
(_: string, index: number) =>
coltypes[index] === GenericDataType.Numeric,
)
.map(colname => ({
.map((colname: string | number) => ({
value: colname,
label: verboseMap[colname] ?? colname,
label:
(Array.isArray(verboseMap)
? verboseMap[colname as number]
: verboseMap[colname as string]) ?? colname,
}))
: [];
return {

View File

@ -38,18 +38,37 @@ import { getPadding } from '../Timeseries/transformers';
import { convertInteger } from '../utils/convertInteger';
import { NULL_STRING } from '../constants';
const isIterable = (obj: any): obj is Iterable<any> =>
obj != null && typeof obj[Symbol.iterator] === 'function';
function normalizeSymbolSize(
nodes: ScatterSeriesOption[],
maxBubbleValue: number,
) {
const [bubbleMinValue, bubbleMaxValue] = extent(nodes, x => x.data?.[0]?.[2]);
const nodeSpread = bubbleMaxValue - bubbleMinValue;
nodes.forEach(node => {
// eslint-disable-next-line no-param-reassign
node.symbolSize =
(((node.data?.[0]?.[2] - bubbleMinValue) / nodeSpread) *
(maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE;
});
const [bubbleMinValue, bubbleMaxValue] = extent<ScatterSeriesOption, number>(
nodes,
x => {
const tmpValue = x.data?.[0];
const result = isIterable(tmpValue) ? tmpValue[2] : null;
if (typeof result === 'number') {
return result;
}
return null;
},
);
if (bubbleMinValue !== undefined && bubbleMaxValue !== undefined) {
const nodeSpread = bubbleMaxValue - bubbleMinValue;
nodes.forEach(node => {
const tmpValue = node.data?.[0];
const calculated = isIterable(tmpValue) ? tmpValue[2] : null;
if (typeof calculated === 'number') {
// eslint-disable-next-line no-param-reassign
node.symbolSize =
(((calculated - bubbleMinValue) / nodeSpread) *
(maxBubbleValue * 2) || 0) + MINIMUM_BUBBLE_SIZE;
}
});
}
}
export function formatTooltip(

View File

@ -518,7 +518,9 @@ export default function transformProps(
minorTick: { show: minorTicks },
minInterval:
xAxisType === AxisType.Time && timeGrainSqla
? TIMEGRAIN_TO_TIMESTAMP[timeGrainSqla]
? TIMEGRAIN_TO_TIMESTAMP[
timeGrainSqla as keyof typeof TIMEGRAIN_TO_TIMESTAMP
]
: 0,
...getMinAndMaxFromBounds(
xAxisType,

View File

@ -29,7 +29,7 @@ export const retainFormDataSuffix = (
* > removeFormDataSuffix(fd, '_b')
* { metrics: ['zee'], limit: 100, ... }
* */
const newFormData = {};
const newFormData: Record<string, any> = {};
Object.entries(formData)
.sort(([a], [b]) => {
@ -63,7 +63,7 @@ export const removeFormDataSuffix = (
* > removeUnusedFormData(fd, '_b')
* { metrics: ['foo', 'bar'], limit: 100, ... }
* */
const newFormData = {};
const newFormData: Record<string, any> = {};
Object.entries(formData).forEach(([key, value]) => {
if (!key.endsWith(controlSuffix)) {
newFormData[key] = value;

View File

@ -469,7 +469,7 @@ describe('Does transformProps transform series correctly', () => {
(totals, currentStack) => {
const total = Object.keys(currentStack).reduce((stackSum, key) => {
if (key === '__timestamp') return stackSum;
return stackSum + currentStack[key];
return stackSum + currentStack[key as keyof typeof currentStack];
}, 0);
totals.push(total);
return totals;

View File

@ -415,7 +415,12 @@ const config: ControlPanelConfig = {
const chartStatus = chart?.chartStatus;
const metricColumn = values.map(value => {
if (typeof value === 'string') {
return { value, label: verboseMap[value] ?? value };
return {
value,
label: Array.isArray(verboseMap)
? value
: verboseMap[value],
};
}
return { value: value.label, label: value.label };
});

View File

@ -60,11 +60,12 @@ export function generatePageItems(
items[i] = i + left;
}
// replace non-ending items with placeholders
if (items[0] > 0) {
if (typeof items[0] === 'number' && items[0] > 0) {
items[0] = 0;
items[1] = 'prev-more';
}
if (items[items.length - 1] < total - 1) {
const lastItem = items[items.length - 1];
if (typeof lastItem === 'number' && lastItem < total - 1) {
items[items.length - 1] = total - 1;
items[items.length - 2] = 'next-more';
}

View File

@ -649,9 +649,11 @@ const config: ControlPanelConfig = {
(colname: string, index: number) =>
coltypes[index] === GenericDataType.Numeric,
)
.map(colname => ({
.map((colname: string) => ({
value: colname,
label: verboseMap[colname] ?? colname,
label: Array.isArray(verboseMap)
? colname
: verboseMap[colname],
}))
: [];
const columnOptions = explore?.controls?.time_compare?.value

View File

@ -16,10 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
export const caches = {};
export const caches: Record<string, Record<string, Response>> = {};
export default class Cache {
cache: object;
cache: Record<string, Response>;
constructor(key: string) {
caches[key] = caches[key] || {};

View File

@ -40,9 +40,10 @@ const exposedProperties = ['window', 'navigator', 'document'];
const { defaultView } = document;
if (defaultView != null) {
Object.keys(defaultView).forEach(property => {
if (typeof global[property] === 'undefined') {
if (typeof global[property as keyof typeof global] === 'undefined') {
exposedProperties.push(property);
global[property] = defaultView[property];
// @ts-ignore due to string-type index signature doesn't apply for `typeof globalThis`.
global[property] = defaultView[property as keyof typeof defaultView];
}
});
}

View File

@ -17,7 +17,7 @@
* under the License.
*/
import { fireEvent, render } from 'spec/helpers/testing-library';
import KeyboardShortcutButton, { KEY_MAP } from '.';
import KeyboardShortcutButton, { KEY_MAP, KeyboardShortcut } from '.';
test('renders shortcut description', () => {
const { getByText, getByRole } = render(
@ -26,7 +26,7 @@ test('renders shortcut description', () => {
fireEvent.click(getByRole('button'));
expect(getByText('Keyboard shortcuts')).toBeInTheDocument();
Object.keys(KEY_MAP)
.filter(key => Boolean(KEY_MAP[key]))
.filter(key => Boolean(KEY_MAP[key as KeyboardShortcut]))
.forEach(key => {
expect(getByText(key)).toBeInTheDocument();
});

View File

@ -42,7 +42,7 @@ export enum KeyboardShortcut {
CtrlRight = 'ctrl+]',
}
export const KEY_MAP = {
export const KEY_MAP: Record<KeyboardShortcut, string | undefined> = {
[KeyboardShortcut.CtrlR]: t('Run query'),
[KeyboardShortcut.CtrlEnter]: t('Run query'),
[KeyboardShortcut.AltEnter]: t('Run query'),
@ -62,15 +62,14 @@ export const KEY_MAP = {
[KeyboardShortcut.CtrlH]: userOS !== 'MacOS' ? t('Replace') : undefined,
};
const KeyMapByCommand = Object.entries(KEY_MAP).reduce(
(acc, [shortcut, command]) => {
if (command) {
acc[command] = [...(acc[command] || []), shortcut];
}
return acc;
},
{} as Record<string, string[]>,
);
const KeyMapByCommand = Object.entries(KEY_MAP).reduce<
Record<string, string[]>
>((acc, [shortcut, command]) => {
if (command) {
acc[command] = [...(acc[command] || []), shortcut];
}
return acc;
}, {});
const ShortcutDescription = styled.span`
font-size: ${({ theme }) => theme.typography.sizes.m}px;

View File

@ -103,7 +103,9 @@ const QueryTable = ({
columns.map(column => ({
accessor: column,
Header:
QUERY_HISTORY_TABLE_HEADERS_LOCALIZED[column] || setHeaders(column),
QUERY_HISTORY_TABLE_HEADERS_LOCALIZED[
column as keyof typeof QUERY_HISTORY_TABLE_HEADERS_LOCALIZED
] || setHeaders(column),
disableSortBy: true,
})),
[columns],
@ -221,6 +223,17 @@ const QueryTable = ({
label: t('Unknown Status'),
},
},
started: {
config: {
icon: (
<Icons.LoadingOutlined
iconColor={theme.colors.primary.base}
iconSize="m"
/>
),
label: t('Started'),
},
},
};
return queries

View File

@ -68,10 +68,10 @@ const getValidator = () => {
const rules: any = getValidationRules();
return (formData: Record<string, any>, errors: FormValidation) => {
rules.forEach((rule: any) => {
const test = validators[rule.name];
const test = validators[rule.name as keyof typeof validators];
const args = rule.arguments.map((name: string) => formData[name]);
const container = rule.container || rule.arguments.slice(-1)[0];
if (!test(...args)) {
if (!test(args[0], args[1])) {
errors[container]?.addError(rule.message);
}
});

View File

@ -29,7 +29,7 @@ export const STATE_TYPE_MAP: Record<string, Type> = {
success: 'success',
};
export const STATE_TYPE_MAP_LOCALIZED = {
export const STATE_TYPE_MAP_LOCALIZED: Record<string, string> = {
offline: t('offline'),
failed: t('failed'),
pending: t('pending'),

View File

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
// TODO: requires redux-localstorage > 1.0 for typescript support
import persistState from 'redux-localstorage';
import { pickBy } from 'lodash';
import { isFeatureEnabled, FeatureFlag } from '@superset-ui/core';
@ -129,6 +128,8 @@ const sqlLabPersistStateConfig = {
},
};
// TODO: requires redux-localstorage > 1.0 for typescript support
/** @type {any} */
export const persistSqlLabStateEnhancer = persistState(
sqlLabPersistStateConfig.paths,
sqlLabPersistStateConfig.config,

View File

@ -147,7 +147,7 @@ export default function getInitialState({
}),
};
const destroyedQueryEditors = {};
const destroyedQueryEditors: Record<string, number> = {};
/**
* If the `SQLLAB_BACKEND_PERSISTENCE` feature flag is off, or if the user

View File

@ -30,7 +30,7 @@ import {
import { queries, defaultQueryEditor } from '../fixtures';
describe('reduxStateToLocalStorageHelper', () => {
const queriesObj = {};
const queriesObj: Record<string, any> = {};
beforeEach(() => {
queries.forEach(q => {
queriesObj[q.id] = q;

View File

@ -16,8 +16,18 @@
* specific language governing permissions and limitations
* under the License.
*/
import type { QueryResponse } from '@superset-ui/core';
import type { QueryEditor, SqlLabRootState, Table } from 'src/SqlLab/types';
import type {
InnerQueryResults,
Query,
QueryResponse,
QueryResults,
} from '@superset-ui/core';
import type {
CursorPosition,
QueryEditor,
SqlLabRootState,
Table,
} from 'src/SqlLab/types';
import type { ThunkDispatch } from 'redux-thunk';
import { pick } from 'lodash';
import { tableApiUtil } from 'src/hooks/apiResources/tables';
@ -71,6 +81,20 @@ export function emptyTablePersistData(tables: Table[]) {
.filter(({ queryEditorId }) => Boolean(queryEditorId));
}
type InnerEmptyQueryResults = {
[key in string]: Query &
QueryResults & {
inLocalStorage?: boolean;
};
};
type EmptyQueryResults = Record<
string,
InnerEmptyQueryResults & {
results: InnerQueryResults | {};
}
>;
export function emptyQueryResults(
queries: SqlLabRootState['sqlLab']['queries'],
) {
@ -86,7 +110,7 @@ export function emptyQueryResults(
[key]: query,
};
return updatedQueries;
}, {});
}, {} as EmptyQueryResults);
}
export function clearQueryEditors(queryEditors: QueryEditor[]) {
@ -94,10 +118,15 @@ export function clearQueryEditors(queryEditors: QueryEditor[]) {
// only return selected keys
Object.keys(editor)
.filter(key => PERSISTENT_QUERY_EDITOR_KEYS.has(key))
.reduce(
.reduce<
Record<
string,
string | number | boolean | CursorPosition | null | undefined
>
>(
(accumulator, key) => ({
...accumulator,
[key]: editor[key],
[key]: editor[key as keyof QueryEditor],
}),
{},
),

View File

@ -115,7 +115,7 @@ export const formatValueHandler = (
})
.join(', ');
}
if (controlsMap[key]?.type === 'BoundsControl') {
if (controlsMap[key]?.type === 'BoundsControl' && Array.isArray(value)) {
return `Min: ${value[0]}, Max: ${value[1]}`;
}
if (controlsMap[key]?.type === 'CollectionControl' && Array.isArray(value)) {

View File

@ -28,6 +28,7 @@ import {
t,
useTheme,
ContextMenuFilters,
AdhocFilter,
} from '@superset-ui/core';
import { useDispatch, useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
@ -224,7 +225,7 @@ export default function DrillByModal({
const getFormDataChangesFromConfigs = useCallback(
(configs: DrillByConfigs) =>
configs.reduce(
configs.reduce<Record<string, any>>(
(acc, config) => {
if (config?.groupbyFieldName && config.column) {
acc.formData[config.groupbyFieldName] = getNewGroupby(
@ -246,7 +247,7 @@ export default function DrillByModal({
return acc;
},
{
formData: {},
formData: {} as Record<string, string | string[] | Set<string>>,
overriddenGroupbyFields: new Set<string>(),
overriddenAdhocFilterFields: new Set<string>(),
},
@ -256,7 +257,7 @@ export default function DrillByModal({
const getFiltersFromConfigsByFieldName = useCallback(
() =>
drillByConfigs.reduce((acc, config) => {
drillByConfigs.reduce<Record<string, AdhocFilter[]>>((acc, config) => {
const adhocFilterFieldName =
config.adhocFilterFieldName || DEFAULT_ADHOC_FILTER_FIELD_NAME;
acc[adhocFilterFieldName] = [
@ -295,7 +296,7 @@ export default function DrillByModal({
...formData,
...overrideFormData,
};
overriddenAdhocFilterFields.forEach(adhocFilterField => ({
overriddenAdhocFilterFields.forEach((adhocFilterField: string) => ({
...newFormData,
[adhocFilterField]: [
...formData[adhocFilterField],

View File

@ -90,7 +90,9 @@ export default function DrillDetailPane({
const [resultsPages, setResultsPages] = useState<Map<number, ResultsPage>>(
new Map(),
);
const [timeFormatting, setTimeFormatting] = useState({});
const [timeFormatting, setTimeFormatting] = useState<
Record<string, TimeFormatting>
>({});
const SAMPLES_ROW_LIMIT = useSelector(
(state: { common: { conf: JsonObject } }) =>
@ -140,7 +142,10 @@ export default function DrillDetailPane({
: TimeFormatting.Formatted
}
onChange={value =>
setTimeFormatting(state => ({ ...state, [column]: value }))
setTimeFormatting(state => ({
...state,
[column]: parseInt(value, 10) as TimeFormatting,
}))
}
/>
) : (

View File

@ -34,10 +34,10 @@ export const CheckboxGallery = () =>
<div style={{ marginBottom: '16px' }} key={status}>
<Checkbox
onChange={() => {}}
checked={STATUSES[status]}
checked={STATUSES[status as keyof typeof STATUSES]}
style={{ marginRight: '8px' }}
/>
{`I'm a${STATUSES[status] ? '' : 'n'} ${status} checkbox`}
{`I'm a${STATUSES[status as keyof typeof STATUSES] ? '' : 'n'} ${status} checkbox`}
</div>
));

View File

@ -24,6 +24,7 @@ import CollectionTable from './CollectionTable';
const props = {
collection: mockDatasource['7__table'].columns,
tableColumns: ['column_name', 'type', 'groupby'],
sortColumns: [],
};
test('renders a table', () => {

View File

@ -36,9 +36,9 @@ import { recurseReactClone } from './utils';
interface CRUDCollectionProps {
allowAddItem?: boolean;
allowDeletes?: boolean;
collection: Array<object>;
columnLabels?: object;
columnLabelTooltips?: object;
collection: Record<PropertyKey, any>[];
columnLabels?: Record<PropertyKey, any>;
columnLabelTooltips?: Record<PropertyKey, any>;
emptyMessage?: ReactNode;
expandFieldset?: ReactNode;
extraButtons?: ReactNode;
@ -58,8 +58,8 @@ interface CRUDCollectionProps {
record: any,
) => ReactNode)[];
onChange?: (arg0: any) => void;
tableColumns: Array<any>;
sortColumns: Array<string>;
tableColumns: any[];
sortColumns: string[];
stickyHeader?: boolean;
}
@ -72,14 +72,14 @@ enum SortOrder {
}
interface CRUDCollectionState {
collection: object;
collectionArray: Array<object>;
expandedColumns: object;
collection: Record<PropertyKey, any>;
collectionArray: Record<PropertyKey, any>[];
expandedColumns: Record<PropertyKey, any>;
sortColumn: string;
sort: SortOrder;
}
function createCollectionArray(collection: object) {
function createCollectionArray(collection: Record<PropertyKey, any>) {
return Object.keys(collection).map(k => collection[k]);
}
@ -89,7 +89,7 @@ function createKeyedCollection(arr: Array<object>) {
id: o.id || nanoid(),
}));
const collection = {};
const collection: Record<PropertyKey, any> = {};
collectionArray.forEach((o: any) => {
collection[o.id] = o;
});
@ -301,7 +301,8 @@ export default class CRUDCollection extends PureComponent<
// newly ordered collection
const sorted = [...this.state.collectionArray].sort(
(a: object, b: object) => compareSort(a[col], b[col]),
(a: Record<PropertyKey, any>, b: Record<PropertyKey, any>) =>
compareSort(a[col], b[col]),
);
const newCollection =
sort === SortOrder.Asc ? sorted : sorted.reverse();

View File

@ -74,7 +74,7 @@ const interactiveTypes = {
},
};
export const InteractiveDatePicker = (args: DatePickerProps) => (
export const InteractiveDatePicker: any = (args: DatePickerProps) => (
<DatePicker {...args} />
);
@ -87,7 +87,7 @@ InteractiveDatePicker.args = {
InteractiveDatePicker.argTypes = interactiveTypes;
export const InteractiveRangePicker = (args: RangePickerProps) => (
export const InteractiveRangePicker: any = (args: RangePickerProps) => (
<RangePicker {...args} />
);

View File

@ -19,4 +19,8 @@
import { DatePicker as AntdDatePicker } from 'antd-v5';
export const DatePicker = AntdDatePicker;
export const { RangePicker } = AntdDatePicker;
// Disable ESLint rule to allow tsc to infer proper type for RangePicker.
// eslint-disable-next-line prefer-destructuring
export const RangePicker: typeof AntdDatePicker.RangePicker =
AntdDatePicker.RangePicker;

View File

@ -144,7 +144,10 @@ const ImageContainer = ({
size: EmptyStateSize;
}) => {
if (!image) return null;
const mappedImage = typeof image === 'string' ? imageMap[image] : image;
const mappedImage =
typeof image === 'string'
? imageMap[image as keyof typeof imageMap]
: image;
return (
<div role="img" aria-label="empty">
<Empty

View File

@ -52,7 +52,10 @@ const collapseStyle = (theme: SupersetTheme) => css`
const extractInvalidValues = (messages: object, payload: object): string[] => {
const invalidValues: string[] = [];
const recursiveExtract = (messages: object, payload: object) => {
const recursiveExtract = (
messages: Record<string, any>,
payload: Record<string, any>,
) => {
Object.keys(messages).forEach(key => {
const value = payload[key];
const message = messages[key];
@ -66,7 +69,10 @@ const extractInvalidValues = (messages: object, payload: object): string[] => {
}
});
};
recursiveExtract(messages, payload);
recursiveExtract(
messages as Record<string, any>,
payload as Record<string, any>,
);
return invalidValues;
};

View File

@ -95,7 +95,7 @@ const FilterableTable = ({
}: FilterableTableProps) => {
const formatTableData = (data: Record<string, unknown>[]): Datum[] =>
data.map(row => {
const newRow = {};
const newRow: Record<string, any> = {};
Object.entries(row).forEach(([key, val]) => {
if (['string', 'number'].indexOf(typeof val) >= 0) {
newRow[key] = val;
@ -116,7 +116,7 @@ const FilterableTable = ({
const getWidthsForColumns = () => {
const PADDING = 50; // accounts for cell padding and width of sorting icon
const widthsByColumnKey = {};
const widthsByColumnKey: Record<string, number> = {};
const cellContent = ([] as string[]).concat(
...orderedColumnKeys.map(key => {
const cellContentList = list.map((data: Datum) =>

View File

@ -41,7 +41,7 @@ export default function FlashProvider({ children, messages }: Props) {
messages.forEach(message => {
const [type, text] = message;
const flash = flashObj[type];
const toast = toasts[flash];
const toast = toasts[flash as keyof typeof toasts];
if (toast) {
toast(text);
}

View File

@ -127,6 +127,8 @@ const AntdEnhancedIcons = Object.keys(AntdIcons)
.map(k => ({
[k]: (props: IconType) => {
const whatRole = props?.onClick ? 'button' : 'img';
// @ts-ignore TODO(hainenber): fix the type compatiblity between
// StyledIcon component prop and AntdIcon values
return <StyledIcon component={AntdIcons[k]} role={whatRole} {...props} />;
},
}))

View File

@ -28,7 +28,7 @@ export default {
component: Icon,
};
const palette = { Default: null };
const palette: Record<string, string | null> = { Default: null };
Object.entries(supersetTheme.colors).forEach(([familyName, family]) => {
Object.entries(family).forEach(([colorName, colorValue]) => {
palette[`${familyName} / ${colorName}`] = colorValue;

View File

@ -123,10 +123,11 @@ export default function Label(props: LabelProps) {
borderColor: borderColorHover,
opacity: 1,
},
...(monospace
? { 'font-family': theme.typography.families.monospace }
: {}),
};
if (monospace) {
css['font-family'] = theme.typography.families.monospace;
}
return (
<Tag
onClick={onClick}

View File

@ -65,19 +65,21 @@ export type Filters = Filter[];
export type ViewModeType = 'card' | 'table';
export type InnerFilterValue =
| string
| boolean
| number
| null
| undefined
| string[]
| number[]
| { label: string; value: string | number };
export interface FilterValue {
id: string;
urlDisplay?: string;
operator?: string;
value:
| string
| boolean
| number
| null
| undefined
| string[]
| number[]
| { label: string; value: string | number };
value: InnerFilterValue;
}
export interface FetchDataConfig {

View File

@ -39,6 +39,7 @@ import {
FetchDataConfig,
Filter,
FilterValue,
InnerFilterValue,
InternalFilter,
SortColumn,
ViewModeType,
@ -138,7 +139,7 @@ export function convertFiltersRison(
list: Filter[],
): FilterValue[] {
const filters: FilterValue[] = [];
const refs = {};
const refs: Record<string, FilterValue> = {};
Object.keys(filterObj).forEach(id => {
const filter: FilterValue = {
@ -300,7 +301,7 @@ export function useListViewState({
useEffect(() => {
// From internalFilters, produce a simplified obj
const filterObj = {};
const filterObj: Record<string, InnerFilterValue> = {};
internalFilters.forEach(filter => {
if (

View File

@ -70,8 +70,10 @@ const loadOptions = async (search: string, page: number, pageSize: number) => {
const optionFilterProps = ['label', 'value', 'gender'];
const data = OPTIONS.filter(option =>
optionFilterProps.some(prop => {
const optionProp = option?.[prop]
? String(option[prop]).trim().toLowerCase()
const optionProp = option?.[prop as keyof typeof option]
? String(option[prop as keyof typeof option])
.trim()
.toLowerCase()
: '';
return optionProp.includes(searchValue);
}),

View File

@ -50,8 +50,8 @@ export function getValue(
}
export function isEqual(a: V | LabeledValue, b: V | LabeledValue, key: string) {
const actualA = isObject(a) && key in a ? a[key] : a;
const actualB = isObject(b) && key in b ? b[key] : b;
const actualA = isObject(a) && key in a ? a[key as keyof LabeledValue] : a;
const actualB = isObject(b) && key in b ? b[key as keyof LabeledValue] : b;
// When comparing the values we use the equality
// operator to automatically convert different types
// eslint-disable-next-line eqeqeq
@ -84,10 +84,15 @@ export function hasOption(
* */
export const propertyComparator =
(property: string) => (a: AntdLabeledValue, b: AntdLabeledValue) => {
if (typeof a[property] === 'string' && typeof b[property] === 'string') {
return a[property].localeCompare(b[property]);
const propertyA = a[property as keyof LabeledValue];
const propertyB = b[property as keyof LabeledValue];
if (typeof propertyA === 'string' && typeof propertyB === 'string') {
return propertyA.localeCompare(propertyB);
}
return (a[property] as number) - (b[property] as number);
if (typeof propertyA === 'number' && typeof propertyB === 'number') {
return propertyA - propertyB;
}
return String(propertyA).localeCompare(String(propertyB)); // fallback to string comparison
};
export const sortSelectedFirstHelper = (

View File

@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { Steps as AntdSteps } from 'antd-v5';
import { Steps, StepsProps } from '.';
export default {
title: 'Steps',
component: Steps,
component: Steps as typeof AntdSteps,
};
export const InteractiveSteps = (args: StepsProps) => <Steps {...args} />;

View File

@ -46,7 +46,7 @@ export default {
argTypes: { onClick: { action: 'clicked' } },
} as Meta<typeof Table>;
export interface BasicData {
interface BasicData {
name: string;
category: string;
price: number;
@ -54,7 +54,7 @@ export interface BasicData {
key: number;
}
export interface RendererData {
interface RendererData {
key: number;
buttonCell: string;
textCell: string;
@ -62,7 +62,7 @@ export interface RendererData {
dollarCell: number;
}
export interface ExampleData {
interface ExampleData {
title: string;
name: string;
age: number;
@ -71,8 +71,8 @@ export interface ExampleData {
key: number;
}
function generateValues(amount: number, row = 0): object {
const cells = {};
function generateValues(amount: number, row = 0): Record<string, number> {
const cells: Record<string, number> = {};
for (let i = 0; i < amount; i += 1) {
cells[`col-${i}`] = i * row * 0.75;
}
@ -94,7 +94,12 @@ function generateColumns(amount: number): ColumnsType<ExampleData>[] {
locale={LocaleCode.en_US}
/>
),
sorter: (a: BasicData, b: BasicData) => numericalSort(`col-${i}`, a, b),
sorter: (a: BasicData, b: BasicData) =>
numericalSort(
`col-${i}`,
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
});
}
return newCols as ColumnsType<ExampleData>[];
@ -168,19 +173,34 @@ const basicColumns: ColumnsType<BasicData> = [
dataIndex: 'name',
key: 'name',
width: 100,
sorter: (a: BasicData, b: BasicData) => alphabeticalSort('name', a, b),
sorter: (a: BasicData, b: BasicData) =>
alphabeticalSort(
'name',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
},
{
title: 'Category',
dataIndex: 'category',
key: 'category',
sorter: (a: BasicData, b: BasicData) => alphabeticalSort('category', a, b),
sorter: (a: BasicData, b: BasicData) =>
alphabeticalSort(
'category',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
},
{
title: 'Price',
dataIndex: 'price',
key: 'price',
sorter: (a: BasicData, b: BasicData) => numericalSort('price', a, b),
sorter: (a: BasicData, b: BasicData) =>
numericalSort(
'price',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
width: 100,
},
{
@ -201,7 +221,12 @@ const bigColumns: ColumnsType<ExampleData> = [
title: 'Age',
dataIndex: 'age',
key: 'age',
sorter: (a: ExampleData, b: ExampleData) => numericalSort('age', a, b),
sorter: (a: ExampleData, b: ExampleData) =>
numericalSort(
'age',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
width: 75,
},
{
@ -381,7 +406,12 @@ const paginationColumns: ColumnsType<BasicData> = [
locale={LocaleCode.en_US}
/>
),
sorter: (a: BasicData, b: BasicData) => numericalSort('price', a, b),
sorter: (a: BasicData, b: BasicData) =>
numericalSort(
'price',
a as Record<PropertyKey, any>,
b as Record<PropertyKey, any>,
),
},
{
title: 'Description',

View File

@ -95,7 +95,7 @@ function ActionMenu(props: ActionMenuProps) {
const { menuOptions, setVisible } = props;
const handleClick: MenuProps['onClick'] = ({ key }) => {
setVisible?.(false);
const menuItem = menuOptions[key];
const menuItem = menuOptions[parseInt(key, 10)];
if (menuItem) {
menuItem?.onClick?.(menuItem);
}

View File

@ -25,6 +25,7 @@ import AntTable, {
import { PaginationProps } from 'antd/lib/pagination';
import { t, useTheme, logging, styled } from '@superset-ui/core';
import Loading from 'src/components/Loading';
import { RowSelectionType } from 'antd/lib/table/interface';
import InteractiveTableUtils from './utils/InteractiveTableUtils';
import VirtualTable from './VirtualTable';
@ -226,11 +227,12 @@ const defaultLocale = {
cancelSort: t('Click to cancel sorting'),
};
const selectionMap = {};
const selectionMap = {
[SelectionType.Multi]: 'checkbox',
[SelectionType.Single]: 'radio',
[SelectionType.Disabled]: null,
};
const noop = () => {};
selectionMap[SelectionType.Multi] = 'checkbox';
selectionMap[SelectionType.Single] = 'radio';
selectionMap[SelectionType.Disabled] = null;
export function Table<RecordType extends object>(
props: TableProps<RecordType>,
@ -277,7 +279,7 @@ export function Table<RecordType extends object>(
const selectionTypeValue = selectionMap[selectionType];
const rowSelection = {
type: selectionTypeValue,
type: selectionMap[selectionType] as RowSelectionType,
selectedRowKeys,
onChange: onSelectChange,
};
@ -398,7 +400,7 @@ export function Table<RecordType extends object>(
{!virtualize && (
<StyledTable
{...sharedProps}
rowSelection={selectionTypeValue ? rowSelection : undefined}
rowSelection={selectionTypeValue !== null ? rowSelection : undefined}
sticky={sticky}
/>
)}

View File

@ -45,14 +45,20 @@ const rows = [
* 1 or greater means the first item comes before the second item
*/
test('alphabeticalSort sorts correctly', () => {
// @ts-ignore
expect(alphabeticalSort('name', rows[0], rows[1])).toBe(-1);
// @ts-ignore
expect(alphabeticalSort('name', rows[1], rows[0])).toBe(1);
// @ts-ignore
expect(alphabeticalSort('category', rows[1], rows[0])).toBe(0);
});
test('numericalSort sorts correctly', () => {
// @ts-ignore
expect(numericalSort('cost', rows[1], rows[2])).toBe(0);
// @ts-ignore
expect(numericalSort('cost', rows[1], rows[0])).toBeLessThan(0);
// @ts-ignore
expect(numericalSort('cost', rows[4], rows[1])).toBeGreaterThan(0);
});
@ -73,6 +79,7 @@ test('alphabeticalSort bad inputs no errors', () => {
expect(
alphabeticalSort(
'name',
// @ts-ignore
{ name: { title: 'the name attribute should not be an object' } },
{ name: 'Doug' },
),
@ -93,6 +100,7 @@ test('numericalSort bad inputs no errors', () => {
expect(
numericalSort(
'name',
// @ts-ignore
{ name: { title: 'the name attribute should not be an object' } },
{ name: 'Doug' },
),

View File

@ -23,8 +23,11 @@
* @param b Second row object to compare
* @returns number
*/
export const alphabeticalSort = (key: string, a: object, b: object): number =>
a?.[key]?.localeCompare?.(b?.[key]);
export const alphabeticalSort = (
key: string,
a: Record<PropertyKey, string>,
b: Record<PropertyKey, string>,
): number => a?.[key]?.localeCompare?.(b?.[key]);
/**
* @param key The name of the row's attribute used to compare values for numerical sorting
@ -32,5 +35,8 @@ export const alphabeticalSort = (key: string, a: object, b: object): number =>
* @param b Second row object to compare
* @returns number
*/
export const numericalSort = (key: string, a: object, b: object): number =>
a?.[key] - b?.[key];
export const numericalSort = (
key: string,
a: Record<PropertyKey, number>,
b: Record<PropertyKey, number>,
): number => a?.[key] - b?.[key];

View File

@ -70,8 +70,8 @@ export default function TimezoneSelector({
const offsets = getOffsetKey(name);
return (
(isDST(currentDate.tz(name), name)
? offsetsToName[offsets]?.[1]
: offsetsToName[offsets]?.[0]) || name
? offsetsToName[offsets as keyof typeof offsetsToName]?.[1]
: offsetsToName[offsets as keyof typeof offsetsToName]?.[0]) || name
);
};

View File

@ -1,76 +0,0 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-env browser */
import PropTypes from 'prop-types';
import { PureComponent } from 'react';
import { getCategoricalSchemeRegistry, t } from '@superset-ui/core';
import ColorSchemeControl from 'src/explore/components/controls/ColorSchemeControl';
const propTypes = {
onChange: PropTypes.func,
labelMargin: PropTypes.number,
colorScheme: PropTypes.string,
hasCustomLabelsColor: PropTypes.bool,
};
const defaultProps = {
hasCustomLabelsColor: false,
colorScheme: undefined,
onChange: () => {},
};
class ColorSchemeControlWrapper extends PureComponent {
constructor(props) {
super(props);
this.state = { hovered: false };
this.categoricalSchemeRegistry = getCategoricalSchemeRegistry();
this.choices = this.categoricalSchemeRegistry.keys().map(s => [s, s]);
this.schemes = this.categoricalSchemeRegistry.getMap();
}
setHover(hovered) {
this.setState({ hovered });
}
render() {
const { colorScheme, labelMargin = 0, hasCustomLabelsColor } = this.props;
return (
<ColorSchemeControl
description={t(
"Any color palette selected here will override the colors applied to this dashboard's individual charts",
)}
labelMargin={labelMargin}
name="color_scheme"
onChange={this.props.onChange}
value={colorScheme}
choices={this.choices}
clearable
schemes={this.schemes}
hovered={this.state.hovered}
hasCustomLabelsColor={hasCustomLabelsColor}
/>
);
}
}
ColorSchemeControlWrapper.propTypes = propTypes;
ColorSchemeControlWrapper.defaultProps = defaultProps;
export default ColorSchemeControlWrapper;

View File

@ -0,0 +1,64 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/* eslint-env browser */
import { getCategoricalSchemeRegistry, t } from '@superset-ui/core';
import { useEffect, useState } from 'react';
import ColorSchemeControl from 'src/explore/components/controls/ColorSchemeControl';
interface ColorSchemeControlWrapperProps {
colorScheme?: string;
hasCustomLabelsColor: boolean;
hovered?: boolean;
onChange: () => void;
}
const ColorSchemeControlWrapper = ({
colorScheme,
hasCustomLabelsColor = false,
hovered = false,
onChange = () => {},
}: ColorSchemeControlWrapperProps) => {
const [choices, setChoices] = useState<string[][]>([]);
const [schemes, setSchemes] = useState({});
useEffect(() => {
// Registry initialization
const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
setChoices(categoricalSchemeRegistry.keys().map(s => [s, s]));
setSchemes(categoricalSchemeRegistry.getMap());
}, []); // Empty dependency array ensures this runs only once
return (
<ColorSchemeControl
description={t(
"Any color palette selected here will override the colors applied to this dashboard's individual charts",
)}
name="color_scheme"
onChange={onChange}
value={colorScheme ?? ''}
choices={choices}
clearable
hovered={hovered}
schemes={schemes}
hasCustomLabelsColor={hasCustomLabelsColor}
/>
);
};
export default ColorSchemeControlWrapper;

View File

@ -122,7 +122,7 @@ const DashboardContainer: FC<DashboardContainerProps> = ({ topLevelTabs }) => {
const [dashboardLabelsColorInitiated, setDashboardLabelsColorInitiated] =
useState(false);
const prevRenderedChartIds = useRef<number[]>([]);
const prevTabIndexRef = useRef();
const prevTabIndexRef = useRef<number>();
const tabIndex = useMemo(() => {
const nextTabIndex = findTabIndexByComponentId({
currentComponent: getRootLevelTabsComponent(dashboardLayout),

View File

@ -175,10 +175,10 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
setDashboardIndicators(indicatorsInitialState);
} else if (prevChartStatus !== 'success') {
if (
chart?.queriesResponse?.[0]?.rejected_filters !==
prevChart?.queriesResponse?.[0]?.rejected_filters ||
chart?.queriesResponse?.[0]?.applied_filters !==
prevChart?.queriesResponse?.[0]?.applied_filters ||
chart?.queriesResponse?.rejected_filters !==
prevChart?.queriesResponse?.rejected_filters ||
chart?.queriesResponse?.applied_filters !==
prevChart?.queriesResponse?.applied_filters ||
dashboardFilters !== prevDashboardFilters ||
datasources !== prevDatasources
) {
@ -215,10 +215,10 @@ export const FiltersBadge = ({ chartId }: FiltersBadgeProps) => {
setNativeIndicators(indicatorsInitialState);
} else if (prevChartStatus !== 'success') {
if (
chart?.queriesResponse?.[0]?.rejected_filters !==
prevChart?.queriesResponse?.[0]?.rejected_filters ||
chart?.queriesResponse?.[0]?.applied_filters !==
prevChart?.queriesResponse?.[0]?.applied_filters ||
chart?.queriesResponse?.rejected_filters !==
prevChart?.queriesResponse?.rejected_filters ||
chart?.queriesResponse?.applied_filters !==
prevChart?.queriesResponse?.applied_filters ||
nativeFilters !== prevNativeFilters ||
chartLayoutItems !== prevChartLayoutItems ||
dataMask !== prevDataMask ||

View File

@ -463,7 +463,6 @@ const PropertiesModal = ({
hasCustomLabelsColor={hasCustomLabelsColor}
onChange={onColorSchemeChange}
colorScheme={colorScheme}
labelMargin={4}
/>
</Col>
</Row>
@ -532,7 +531,6 @@ const PropertiesModal = ({
hasCustomLabelsColor={hasCustomLabelsColor}
onChange={onColorSchemeChange}
colorScheme={colorScheme}
labelMargin={4}
/>
</Col>
</Row>

View File

@ -30,6 +30,7 @@ import {
import { RootState } from 'src/dashboard/types';
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme';
import { Divider, Filter } from '@superset-ui/core';
type Props = { dashboardPageId: string };
@ -66,7 +67,9 @@ const selectDashboardContextForExplore = createSelector(
(state: RootState) => state.dataMask,
],
(metadata, dashboardId, colorScheme, filters, dataMask) => {
const nativeFilters = Object.keys(filters).reduce((acc, key) => {
const nativeFilters = Object.keys(filters).reduce<
Record<string, Pick<Filter | Divider, 'chartsInScope'>>
>((acc, key) => {
acc[key] = pick(filters[key], ['chartsInScope']);
return acc;
}, {});

View File

@ -26,6 +26,7 @@ import {
isCrossFilterScopeGlobal,
GlobalChartCrossFilterConfig,
GLOBAL_SCOPE_POINTER,
ChartCrossFiltersConfig,
} from 'src/dashboard/types';
import { getChartIdsInFilterScope } from 'src/dashboard/util/getChartIdsInFilterScope';
import { useChartIds } from 'src/dashboard/util/charts/useChartIds';
@ -39,7 +40,9 @@ const getUpdatedGloballyScopedChartsInScope = (
configs: ChartConfiguration,
globalChartsInScope: number[],
) =>
Object.entries(configs).reduce((acc, [id, config]) => {
Object.entries(configs).reduce<
Record<string, { id: number; crossFilters: ChartCrossFiltersConfig }>
>((acc, [id, config]) => {
if (isCrossFilterScopeGlobal(config.crossFilters.scope)) {
acc[id] = {
id: Number(config.id),

View File

@ -191,7 +191,7 @@ const FilterBar: FC<FiltersBarProps> = ({
useEffect(() => {
if (previousFilters && dashboardId === previousDashboardId) {
const updates = {};
const updates: Record<string, DataMaskWithId> = {};
Object.values(filters).forEach(currentFilter => {
const previousFilter = previousFilters?.[currentFilter.id];
if (!previousFilter) {
@ -208,7 +208,9 @@ const FilterBar: FC<FiltersBarProps> = ({
const dataMaskChanged = !isEqual(currentDataMask, previousDataMask);
if (typeChanged || targetsChanged || dataMaskChanged) {
updates[currentFilter.id] = getInitialDataMask(currentFilter.id);
updates[currentFilter.id] = getInitialDataMask(
currentFilter.id,
) as DataMaskWithId;
}
});

View File

@ -80,7 +80,9 @@ export const hasTemporalColumns = (
export const doesColumnMatchFilterType = (filterType: string, column: Column) =>
!column.type_generic ||
!(filterType in FILTER_SUPPORTED_TYPES) ||
FILTER_SUPPORTED_TYPES[filterType]?.includes(column.type_generic);
FILTER_SUPPORTED_TYPES[
filterType as keyof typeof FILTER_SUPPORTED_TYPES
]?.includes(column.type_generic);
export const mostUsedDataset = (
datasets: DatasourcesState,

View File

@ -91,7 +91,7 @@ const selectIndicatorValue = (
(columnKey === TIME_FILTER_MAP.time_grain_sqla
? datasource.time_grain_sqla
: datasource.granularity) || []
).reduce(
).reduce<Record<string, string>>(
(map, [key, value]) => ({
...map,
[key]: value,
@ -189,8 +189,16 @@ export const getCrossFilterIndicator = (
return filterObject;
};
const cachedIndicatorsForChart = {};
const cachedDashboardFilterDataForChart = {};
const cachedIndicatorsForChart: Record<number, Indicator[]> = {};
const cachedDashboardFilterDataForChart: Record<
string,
{
appliedColumns: Set<string>;
rejectedColumns: Set<string>;
matchingFilters: Filter[];
matchingDatasources: Datasource[];
}
> = {};
// inspects redux state to find what the filter indicators should be shown for a given chart
export const selectIndicatorsForChart = (
chartId: number,
@ -214,10 +222,10 @@ export const selectIndicatorsForChart = (
const cachedFilterData = cachedDashboardFilterDataForChart[chartId];
if (
cachedIndicatorsForChart[chartId] &&
areObjectsEqual(cachedFilterData?.appliedColumns, appliedColumns) &&
areObjectsEqual(cachedFilterData?.rejectedColumns, rejectedColumns) &&
areObjectsEqual(cachedFilterData?.matchingFilters, matchingFilters) &&
areObjectsEqual(cachedFilterData?.matchingDatasources, matchingDatasources)
areObjectsEqual(cachedFilterData.appliedColumns, appliedColumns) &&
areObjectsEqual(cachedFilterData.rejectedColumns, rejectedColumns) &&
areObjectsEqual(cachedFilterData.matchingFilters, matchingFilters) &&
areObjectsEqual(cachedFilterData.matchingDatasources, matchingDatasources)
) {
return cachedIndicatorsForChart[chartId];
}
@ -286,7 +294,7 @@ export const selectChartCrossFilters = (
rejectedColumns: Set<string>,
filterEmitter = false,
): Indicator[] | CrossFilterIndicator[] => {
let crossFilterIndicators: any = [];
let crossFilterIndicators: Indicator[] | CrossFilterIndicator[] = [];
crossFilterIndicators = Object.values(chartConfiguration)
.filter(chartConfig => {
const inScope =
@ -322,8 +330,18 @@ export const selectChartCrossFilters = (
return crossFilterIndicators;
};
const cachedNativeIndicatorsForChart = {};
const cachedNativeFilterDataForChart: any = {};
const cachedNativeIndicatorsForChart: Record<number, any> = {};
const cachedNativeFilterDataForChart: Record<
number,
{
nativeFilters: Filters;
chartLayoutItems: LayoutItem[];
chartConfiguration: ChartConfiguration;
dataMask: DataMaskStateWithId;
appliedColumns: Set<string>;
rejectedColumns: Set<string>;
}
> = {};
export const selectNativeIndicatorsForChart = (
nativeFilters: Filters,
dataMask: DataMaskStateWithId,
@ -374,7 +392,7 @@ export const selectNativeIndicatorsForChart = (
};
});
let crossFilterIndicators: any = [];
let crossFilterIndicators: (Indicator | CrossFilterIndicator)[] = [];
crossFilterIndicators = selectChartCrossFilters(
dataMask,
chartId,
@ -383,7 +401,9 @@ export const selectNativeIndicatorsForChart = (
appliedColumns,
rejectedColumns,
);
const indicators = crossFilterIndicators.concat(nativeFilterIndicators);
const indicators = crossFilterIndicators.concat(
nativeFilterIndicators as Indicator[],
);
cachedNativeIndicatorsForChart[chartId] = indicators;
cachedNativeFilterDataForChart[chartId] = {
nativeFilters,

View File

@ -45,12 +45,12 @@ export function useFilterConfigMap() {
const filterConfig = useFilterConfiguration();
return useMemo(
() =>
filterConfig.reduce(
filterConfig.reduce<Record<string, Filter | Divider>>(
(acc: Record<string, Filter | Divider>, filter: Filter) => {
acc[filter.id] = filter;
return acc;
},
{} as Record<string, Filter | Divider>,
{},
),
[filterConfig],
);

View File

@ -27,9 +27,13 @@ import {
getChartMetadataRegistry,
QueryFormData,
t,
ExtraFormDataOverride,
TimeGranularity,
ExtraFormDataAppend,
} from '@superset-ui/core';
import { LayoutItem } from 'src/dashboard/types';
import extractUrlParams from 'src/dashboard/util/extractUrlParams';
import { isIterable, OnlyKeyWithType } from 'src/utils/types';
import { TAB_TYPE } from '../../util/componentTypes';
import getBootstrapData from '../../../utils/getBootstrapData';
@ -103,22 +107,26 @@ export function mergeExtraFormData(
): ExtraFormData {
const mergedExtra: ExtraFormData = {};
EXTRA_FORM_DATA_APPEND_KEYS.forEach((key: string) => {
const originalExtraData = originalExtra[key as keyof ExtraFormDataAppend];
const newExtraData = newExtra[key as keyof ExtraFormDataAppend];
const mergedValues = [
...(originalExtra[key] || []),
...(newExtra[key] || []),
...(isIterable(originalExtraData) ? originalExtraData : []),
...(isIterable(newExtraData) ? newExtraData : []),
];
if (mergedValues.length) {
mergedExtra[key] = mergedValues;
mergedExtra[key as OnlyKeyWithType<ExtraFormData, any[]>] = mergedValues;
}
});
EXTRA_FORM_DATA_OVERRIDE_KEYS.forEach((key: string) => {
const originalValue = originalExtra[key];
const originalValue = originalExtra[key as keyof ExtraFormDataOverride];
if (originalValue !== undefined) {
mergedExtra[key] = originalValue;
mergedExtra[key as OnlyKeyWithType<ExtraFormData, typeof originalValue>] =
originalValue as TimeGranularity;
}
const newValue = newExtra[key];
const newValue = newExtra[key as keyof ExtraFormDataOverride];
if (newValue !== undefined) {
mergedExtra[key] = newValue;
mergedExtra[key as OnlyKeyWithType<ExtraFormData, typeof newValue>] =
newValue as TimeGranularity;
}
});
return mergedExtra;

View File

@ -27,6 +27,7 @@ import {
UPDATE_CASCADE_PARENT_IDS,
} from 'src/dashboard/actions/nativeFilters';
import {
Divider,
Filter,
FilterConfiguration,
NativeFiltersState,
@ -41,7 +42,7 @@ export function getInitialState({
state?: NativeFiltersState;
}): NativeFiltersState {
const state: Partial<NativeFiltersState> = {};
const filters = {};
const filters: Record<string, Filter | Divider> = {};
if (filterConfig) {
filterConfig.forEach(filter => {
const { id } = filter;

View File

@ -25,6 +25,7 @@ import {
JsonObject,
NativeFilterScope,
NativeFiltersState,
NativeFilterTarget,
} from '@superset-ui/core';
import { Dataset } from '@superset-ui/chart-controls';
import { chart } from 'src/components/Chart/chartReducer';
@ -183,26 +184,30 @@ export type Charts = { [key: number]: Chart };
type ComponentTypesKeys = keyof typeof componentTypes;
export type ComponentType = (typeof componentTypes)[ComponentTypesKeys];
export type LayoutItemMeta = {
chartId: number;
defaultText?: string;
height: number;
placeholder?: string;
sliceName?: string;
sliceNameOverride?: string;
text?: string;
uuid: string;
width: number;
};
/** State of dashboardLayout item in redux */
export type LayoutItem = {
children: string[];
parents?: string[];
type: ComponentType;
id: string;
meta: {
chartId: number;
defaultText?: string;
height: number;
placeholder?: string;
sliceName?: string;
sliceNameOverride?: string;
text?: string;
uuid: string;
width: number;
};
meta: LayoutItemMeta;
};
type ActiveFilter = {
filterType?: string;
targets: number[] | [Partial<NativeFilterTarget>];
scope: number[];
values: ExtraFormData;
};

View File

@ -20,6 +20,7 @@ import {
DataMaskStateWithId,
PartialFilters,
JsonObject,
DataMaskWithId,
} from '@superset-ui/core';
import { ActiveFilters, ChartConfiguration } from '../types';
@ -28,9 +29,12 @@ export const getRelevantDataMask = (
prop: string,
): JsonObject | DataMaskStateWithId =>
Object.values(dataMask)
.filter(item => item[prop])
.filter(item => item[prop as keyof DataMaskWithId])
.reduce(
(prev, next) => ({ ...prev, [next.id]: prop ? next[prop] : next }),
(prev, next) => ({
...prev,
[next.id]: prop ? next[prop as keyof DataMaskWithId] : next,
}),
{},
);
@ -45,13 +49,14 @@ export const getAllActiveFilters = ({
nativeFilters: PartialFilters;
allSliceIds: number[];
}): ActiveFilters => {
const activeFilters = {};
const activeFilters: ActiveFilters = {};
// Combine native filters with cross filters, because they have similar logic
Object.values(dataMask).forEach(({ id: filterId, extraFormData }) => {
Object.values(dataMask).forEach(({ id: filterId, extraFormData = {} }) => {
const scope =
nativeFilters?.[filterId]?.chartsInScope ??
chartConfiguration?.[filterId]?.crossFilters?.chartsInScope ??
chartConfiguration?.[parseInt(filterId, 10)]?.crossFilters
?.chartsInScope ??
allSliceIds ??
[];
const filterType = nativeFilters?.[filterId]?.filterType;

View File

@ -17,8 +17,10 @@
* under the License.
*/
import {
DataMask,
DataMaskStateWithId,
DataRecordFilters,
DataRecordValue,
JsonObject,
PartialFilters,
} from '@superset-ui/core';
@ -29,10 +31,36 @@ import { isEqual } from 'lodash';
import getEffectiveExtraFilters from './getEffectiveExtraFilters';
import { getAllActiveFilters } from '../activeAllDashboardFilters';
interface CachedFormData {
extra_form_data?: JsonObject;
extra_filters: {
col: string;
op: string;
val: DataRecordValue[];
}[];
own_color_scheme?: string;
color_scheme?: string;
color_namespace?: string;
chart_id: number;
label_colors?: Record<string, string>;
shared_label_colors?: string[];
map_label_colors?: Record<string, string>;
}
export type CachedFormDataWithExtraControls = CachedFormData & {
[key: string]: any;
};
// We cache formData objects so that our connected container components don't always trigger
// render cascades. we cannot leverage the reselect library because our cache size is >1
const cachedFiltersByChart = {};
const cachedFormdataByChart = {};
const cachedFiltersByChart: Record<number, DataRecordFilters> = {};
const cachedFormdataByChart: Record<
number,
CachedFormData & {
dataMask: DataMask;
extraControls: Record<string, string | boolean | null>;
}
> = {};
export interface GetFormDataWithExtraFiltersArguments {
chartConfiguration: ChartConfiguration;
@ -113,7 +141,7 @@ export default function getFormDataWithExtraFilters({
};
}
const formData = {
const formData: CachedFormDataWithExtraControls = {
...chart.form_data,
chart_id: chart.id,
label_colors: labelsColor,

View File

@ -21,9 +21,11 @@ import {
Behavior,
getChartMetadataRegistry,
isDefined,
NativeFilterScope,
} from '@superset-ui/core';
import { getChartIdsInFilterScope } from './getChartIdsInFilterScope';
import {
ChartConfiguration,
ChartsState,
DashboardInfo,
DashboardLayout,
@ -66,7 +68,7 @@ export const getCrossFiltersConfiguration = (
// If user just added cross filter to dashboard it's not saving its scope on server,
// so we tweak it until user will update scope and will save it in server
const chartConfiguration = {};
const chartConfiguration: ChartConfiguration = {};
chartLayoutItems.forEach(layoutItem => {
const chartId = layoutItem.meta?.chartId;
@ -92,6 +94,7 @@ export const getCrossFiltersConfiguration = (
id: chartId,
crossFilters: {
scope: GLOBAL_SCOPE_POINTER,
chartsInScope: [],
},
};
}
@ -101,7 +104,8 @@ export const getCrossFiltersConfiguration = (
id => id !== Number(chartId),
)
: getChartIdsInFilterScope(
chartConfiguration[chartId].crossFilters.scope,
chartConfiguration[chartId].crossFilters
.scope as NativeFilterScope,
Object.values(charts).map(chart => chart.id),
chartLayoutItems,
);

View File

@ -48,15 +48,17 @@ function findParentId(structure: IStructure): string | null {
return parentId;
}
const cache = {};
const cache: Record<string, string | null> = {};
export default function findParentIdWithCache(
structure: IStructure,
): string | null {
let parentId = null;
if (structure) {
const { childId, layout = {} } = structure;
if (cache[childId]) {
const lastParent = layout?.[cache[childId]] || {};
const cachedValue = cache[childId];
if (cachedValue) {
const lastParent = layout?.[cachedValue] || {};
if (lastParent?.children && lastParent?.children?.includes?.(childId)) {
return lastParent.id;
}

View File

@ -17,16 +17,17 @@
* under the License.
*/
import findTabIndexByComponentId from 'src/dashboard/util/findTabIndexByComponentId';
import { LayoutItem, LayoutItemMeta } from '../types';
describe('findTabIndexByComponentId', () => {
const topLevelTabsComponent = {
const topLevelTabsComponent: LayoutItem = {
children: ['TAB-0g-5l347I2', 'TAB-qrwN_9VB5'],
id: 'TABS-MNQQSW-kyd',
meta: {},
meta: {} as LayoutItemMeta,
parents: ['ROOT_ID'],
type: 'TABS',
};
const rowLevelTabsComponent = {
const rowLevelTabsComponent: LayoutItem = {
children: [
'TAB-TwyUUGp2Bg',
'TAB-Zl1BQAUvN',
@ -34,7 +35,7 @@ describe('findTabIndexByComponentId', () => {
'TAB---e53RNei',
],
id: 'TABS-Oduxop1L7I',
meta: {},
meta: {} as LayoutItemMeta,
parents: ['ROOT_ID', 'TABS-MNQQSW-kyd', 'TAB-qrwN_9VB5'],
type: 'TABS',
};

View File

@ -16,10 +16,15 @@
* specific language governing permissions and limitations
* under the License.
*/
export default function findTabIndexByComponentId({
import { LayoutItem } from '../types';
const findTabIndexByComponentId = ({
currentComponent,
directPathToChild = [],
}) {
}: {
currentComponent: LayoutItem;
directPathToChild: string[];
}): number => {
if (
!currentComponent ||
directPathToChild.length === 0 ||
@ -38,4 +43,6 @@ export default function findTabIndexByComponentId({
);
}
return -1;
}
};
export default findTabIndexByComponentId;

View File

@ -17,6 +17,7 @@
* under the License.
*/
import getFormDataWithExtraFilters, {
CachedFormDataWithExtraControls,
GetFormDataWithExtraFiltersArguments,
} from 'src/dashboard/util/charts/getFormDataWithExtraFilters';
import { sliceId as chartId } from 'spec/fixtures/mockChartQueries';
@ -87,7 +88,8 @@ describe('getFormDataWithExtraFilters', () => {
});
it('should compose extra control', () => {
const result = getFormDataWithExtraFilters(mockArgs);
const result: CachedFormDataWithExtraControls =
getFormDataWithExtraFilters(mockArgs);
expect(result.stack).toEqual('Stacked');
});
});

View File

@ -43,7 +43,7 @@ describe('isValidChild', () => {
// every unique parent > child relationship is tested, but because this
// test representation WILL result in duplicates, we hash each test
// to keep track of which we've run
const didTest = {};
const didTest: Record<string, boolean> = {};
const validExamples = [
[ROOT, GRID, CHART], // chart is valid because it is wrapped in a row
[ROOT, GRID, MARKDOWN], // markdown is valid because it is wrapped in a row

View File

@ -55,7 +55,7 @@ const depthFour = rootDepth + 4;
const depthFive = rootDepth + 5;
// when moving components around the depth of child is irrelevant, note these are parent depths
const parentMaxDepthLookup = {
const parentMaxDepthLookup: Record<string, Record<string, number>> = {
[DASHBOARD_ROOT_TYPE]: {
[TABS_TYPE]: rootDepth,
[DASHBOARD_GRID_TYPE]: rootDepth,

View File

@ -41,25 +41,15 @@ import { areObjectsEqual } from '../reduxUtils';
export function getInitialDataMask(
id?: string | number,
moreProps?: DataMask,
): DataMask;
export function getInitialDataMask(
id: string | number,
moreProps: DataMask = {},
): DataMaskWithId {
let otherProps = {};
if (id) {
otherProps = {
id,
};
}
): DataMask | DataMaskWithId {
return {
...otherProps,
...(id !== undefined ? { id } : {}),
extraFormData: {},
filterState: {},
ownState: {},
...moreProps,
} as DataMaskWithId;
} as DataMask | DataMaskWithId;
}
function fillNativeFilters(
@ -132,7 +122,7 @@ function updateDataMaskForFilterChanges(
const dataMaskReducer = produce(
(draft: DataMaskStateWithId, action: AnyDataMaskAction) => {
const cleanState = {};
const cleanState: DataMaskStateWithId = {};
switch (action.type) {
case CLEAR_DATA_MASK_STATE:
return cleanState;
@ -151,7 +141,7 @@ const dataMaskReducer = produce(
action.data.dashboardInfo?.metadata?.chart_configuration,
).forEach(id => {
cleanState[id] = {
...getInitialDataMask(id), // take initial data
...(getInitialDataMask(id) as DataMaskWithId), // take initial data
};
});
fillNativeFilters(

View File

@ -102,7 +102,10 @@ export default function Control(props: ControlProps) {
if (!type || isVisible === false) return null;
const ControlComponent = typeof type === 'string' ? controlMap[type] : type;
const ControlComponent =
typeof type === 'string'
? controlMap[type as keyof typeof controlMap]
: type;
if (!ControlComponent) {
// eslint-disable-next-line no-console
console.warn(`Unknown controlType: ${type}`);

View File

@ -48,6 +48,7 @@ import {
CustomControlItem,
Dataset,
ExpandedControlItem,
isCustomControlItem,
isTemporalColumn,
sections,
} from '@superset-ui/chart-controls';
@ -678,8 +679,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
return controlItem;
}
if (
controlItem.name &&
controlItem.config &&
isCustomControlItem(controlItem) &&
controlItem.name !== 'datasource'
) {
return renderControl(controlItem);

View File

@ -320,7 +320,7 @@ export const useTableColumns = (
return {
// react-table requires a non-empty id, therefore we introduce a fallback value in case the key is empty
id: key || index,
accessor: row => row[key],
accessor: (row: Record<string, any>) => row[key],
Header:
colType === GenericDataType.Temporal &&
typeof firstValue !== 'string' ? (

View File

@ -277,7 +277,8 @@ export default function DataSourcePanel({
};
const datasourceIsSaveable =
datasource.type && saveableDatasets[datasource.type];
datasource.type &&
saveableDatasets[datasource.type as keyof typeof saveableDatasets];
const mainBody = useMemo(
() => (

View File

@ -19,7 +19,7 @@
import { forwardRef, RefObject, MouseEvent } from 'react';
import { css, styled } from '@superset-ui/core';
import Button from 'src/components/Button';
import Button, { ButtonStyle } from 'src/components/Button';
interface ControlPanelAlertProps {
title: string;
@ -88,6 +88,8 @@ const Title = styled.p`
const typeChart = {
warning: 'warning',
danger: 'danger',
error: 'primary',
info: 'primary',
};
export const ExploreAlert = forwardRef(
@ -119,7 +121,7 @@ export const ExploreAlert = forwardRef(
</Button>
)}
<Button
buttonStyle={type in typeChart ? typeChart[type] : 'primary'}
buttonStyle={typeChart[type] as ButtonStyle}
buttonSize="small"
onClick={primaryButtonAction}
>

View File

@ -31,7 +31,6 @@ const defaultProps = () => ({
hasCustomLabelsColor: false,
sharedLabelsColors: [],
label: 'Color scheme',
labelMargin: 0,
name: 'color',
value: 'supersetDefault',
clearable: true,

View File

@ -57,7 +57,7 @@ export interface ColorSchemeControlProps {
colorNamespace?: string;
chartId?: number;
dashboardId?: number;
label: string;
label?: string;
name: string;
onChange?: (value: string) => void;
value: string;
@ -65,7 +65,9 @@ export interface ColorSchemeControlProps {
defaultScheme?: string;
choices: string[][] | (() => string[][]);
schemes: ColorSchemes | (() => ColorSchemes);
isLinear: boolean;
isLinear?: boolean;
description?: string;
hovered?: boolean;
}
const StyledAlert = styled(Icons.AlertSolid)`

Some files were not shown because too many files have changed in this diff Show More