diff --git a/.github/workflows/prefer-typescript.yml b/.github/workflows/prefer-typescript.yml deleted file mode 100644 index 472eba6dd98..00000000000 --- a/.github/workflows/prefer-typescript.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Prefer TypeScript - -on: - push: - branches: - - "master" - - "[0-9].[0-9]*" - paths: - - "superset-frontend/src/**" - pull_request: - types: [synchronize, opened, reopened, ready_for_review] - paths: - - "superset-frontend/src/**" - -# cancel previous workflow jobs for PRs -concurrency: - group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} - cancel-in-progress: true - -jobs: - prefer_typescript: - if: github.ref == 'ref/heads/master' && github.event_name == 'pull_request' - name: Prefer TypeScript - runs-on: ubuntu-24.04 - permissions: - contents: read - pull-requests: write - steps: - - name: "Checkout ${{ github.ref }} ( ${{ github.sha }} )" - uses: actions/checkout@v6 - with: - persist-credentials: false - submodules: recursive - - name: Get changed files - id: changed - uses: ./.github/actions/file-changes-action - with: - githubToken: ${{ github.token }} - - - name: Determine if a .js or .jsx file was added - id: check - run: | - js_files_added() { - jq -r ' - map( - select( - endswith(".js") or endswith(".jsx") - ) - ) | join("\n") - ' ${HOME}/files_added.json - } - echo "js_files_added=$(js_files_added)" >> $GITHUB_OUTPUT - - - if: steps.check.outputs.js_files_added - name: Add Comment to PR - uses: ./.github/actions/comment-on-pr - continue-on-error: true - env: - GITHUB_TOKEN: ${{ github.token }} - with: - msg: | - ### WARNING: Prefer TypeScript - - Looks like your PR contains new `.js` or `.jsx` files: - - ``` - ${{steps.check.outputs.js_files_added}} - ``` - - As decided in [SIP-36](https://github.com/apache/superset/issues/9101), all new frontend code should be written in TypeScript. Please convert above files to TypeScript then re-request review. diff --git a/docs/developer_portal/contributing/development-setup.md b/docs/developer_portal/contributing/development-setup.md index fdddbf1af55..a543eb24442 100644 --- a/docs/developer_portal/contributing/development-setup.md +++ b/docs/developer_portal/contributing/development-setup.md @@ -788,7 +788,7 @@ pytest ./link_to_test.py ### Frontend Testing -We use [Jest](https://jestjs.io/) and [Enzyme](https://airbnb.io/enzyme/) to test TypeScript/JavaScript. Tests can be run with: +We use [Jest](https://jestjs.io/) and [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) to test TypeScript. Tests can be run with: ```bash cd superset-frontend diff --git a/docs/developer_portal/contributing/howtos.md b/docs/developer_portal/contributing/howtos.md index cb52bd5b4c9..e4469dfb07a 100644 --- a/docs/developer_portal/contributing/howtos.md +++ b/docs/developer_portal/contributing/howtos.md @@ -100,7 +100,7 @@ npm link superset-plugin-chart-hello-world ``` 7. **Import and register in Superset**: -Edit `superset-frontend/src/visualizations/presets/MainPreset.js` to include your plugin. +Edit `superset-frontend/src/visualizations/presets/MainPreset.ts` to include your plugin. ## Testing diff --git a/superset-frontend/.eslintrc.js b/superset-frontend/.eslintrc.js index 1630d899178..7a38fb54fd0 100644 --- a/superset-frontend/.eslintrc.js +++ b/superset-frontend/.eslintrc.js @@ -273,6 +273,53 @@ module.exports = { ], }, overrides: [ + // Ban JavaScript files in src/ - all new code must be TypeScript + { + files: ['src/**/*.js', 'src/**/*.jsx'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: + 'JavaScript files are not allowed in src/. Please use TypeScript (.ts/.tsx) instead.', + }, + ], + }, + }, + // Ban JavaScript files in plugins/ - all plugin source code must be TypeScript + { + files: ['plugins/**/src/**/*.js', 'plugins/**/src/**/*.jsx'], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: + 'JavaScript files are not allowed in plugins/. Please use TypeScript (.ts/.tsx) instead.', + }, + ], + }, + }, + // Ban JavaScript files in packages/ - with exceptions for config files and generators + { + files: ['packages/**/src/**/*.js', 'packages/**/src/**/*.jsx'], + excludedFiles: [ + 'packages/generator-superset/**/*', // Yeoman generator templates run via Node + 'packages/superset-ui-demo/.storybook/**/*', // Storybook config files + 'packages/**/__mocks__/**/*', // Test mocks + ], + rules: { + 'no-restricted-syntax': [ + 'error', + { + selector: 'Program', + message: + 'JavaScript files are not allowed in packages/. Please use TypeScript (.ts/.tsx) instead.', + }, + ], + }, + }, { files: ['*.ts', '*.tsx'], parser: '@typescript-eslint/parser', diff --git a/superset-frontend/.swcrc b/superset-frontend/.swcrc deleted file mode 100644 index fc5a4336695..00000000000 --- a/superset-frontend/.swcrc +++ /dev/null @@ -1,64 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/swcrc", - "jsc": { - "parser": { - "syntax": "typescript", - "tsx": true, - "decorators": false, - "dynamicImport": true - }, - "transform": { - "react": { - "runtime": "automatic", - "importSource": "@emotion/react", - "throwIfNamespace": true - }, - "optimizer": { - "globals": { - "vars": { - "process.env.NODE_ENV": "production" - } - } - } - }, - "target": "es2015", - "loose": true, - "externalHelpers": false, - "preserveAllComments": false, - "experimental": { - "plugins": [ - [ - "@swc/plugin-emotion", - { - "sourceMap": true, - "autoLabel": "dev-only", - "labelFormat": "[local]" - } - ], - [ - "@swc/plugin-transform-imports", - { - "lodash": { - "transform": "lodash/{{member}}", - "preventFullImport": true, - "skipDefaultConversion": false - }, - "lodash-es": { - "transform": "lodash-es/{{member}}", - "preventFullImport": true, - "skipDefaultConversion": false - } - } - ] - ] - } - }, - "module": { - "type": "es6", - "strict": false, - "strictMode": false, - "lazy": false, - "noInterop": false - }, - "minify": false -} diff --git a/superset-frontend/babel.config.js b/superset-frontend/babel.config.js index 892178c4112..9689190c92f 100644 --- a/superset-frontend/babel.config.js +++ b/superset-frontend/babel.config.js @@ -52,8 +52,6 @@ module.exports = { ['@babel/plugin-transform-private-methods', { loose: true }], ['@babel/plugin-transform-nullish-coalescing-operator', { loose: true }], ['@babel/plugin-transform-runtime', { corejs: 3 }], - // only used in packages/superset-ui-core/src/chart/components/reactify.tsx - ['babel-plugin-typescript-to-proptypes', { loose: true }], [ '@emotion/babel-plugin', { diff --git a/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/index.js b/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/index.ts similarity index 50% rename from superset-frontend/eslint-rules/eslint-plugin-i18n-strings/index.js rename to superset-frontend/eslint-rules/eslint-plugin-i18n-strings/index.ts index 7cb97fea283..107d7a887ca 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/index.js +++ b/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/index.ts @@ -22,27 +22,40 @@ * @author Apache */ +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +const plugin: { rules: Record } = { rules: { 'no-template-vars': { - create(context) { - function handler(node) { - if (node.arguments.length) { - const firstArgs = node.arguments[0]; + meta: { + type: 'problem', + docs: { + description: 'Disallow variables in translation template strings', + }, + schema: [], + }, + create(context: Rule.RuleContext): Rule.RuleListener { + function handler(node: Node): void { + const callNode = node as Node & { + arguments: Array; + }; + // Check all arguments (e.g., tn has singular and plural templates) + for (const arg of callNode.arguments ?? []) { if ( - firstArgs.type === 'TemplateLiteral' && - firstArgs.expressions.length + arg.type === 'TemplateLiteral' && + (arg as Node & { expressions?: Node[] }).expressions?.length ) { context.report({ node, message: "Don't use variables in translation string templates. Flask-babel is a static translation service, so it can't handle strings that include variables", }); + break; // Only report once per call } } } @@ -53,19 +66,29 @@ module.exports = { }, }, 'sentence-case-buttons': { - create(context) { - function isTitleCase(str) { + meta: { + type: 'suggestion', + docs: { + description: 'Enforce sentence case for button text in translations', + }, + schema: [], + }, + create(context: Rule.RuleContext): Rule.RuleListener { + function isTitleCase(str: string): boolean { // Match "Delete Dataset", "Create Chart", etc. (2+ title-cased words) return /^[A-Z][a-z]+(\s+[A-Z][a-z]*)+$/.test(str); } - function isButtonContext(node) { - const { parent } = node; + function isButtonContext(node: Node & { parent?: Node }): boolean { + const { parent } = node as Node & { + parent?: Node & Record; + }; if (!parent) return false; // Check for button-specific props if (parent.type === 'Property') { - const key = parent.key.name; + const key = (parent as unknown as { key: { name: string } }).key + .name; return [ 'primaryButtonName', 'secondaryButtonName', @@ -75,10 +98,16 @@ module.exports = { } // Check for Button components - if (parent.type === 'JSXExpressionContainer') { - const jsx = parent.parent; - if (jsx?.type === 'JSXElement') { - const elementName = jsx.openingElement.name.name; + // Cast to string because ESTree Node type doesn't include JSX types + if ((parent.type as string) === 'JSXExpressionContainer') { + const jsx = (parent as Node & { parent?: Node }).parent as + | (Node & { + type: string; + openingElement?: { name: { name: string } }; + }) + | undefined; + if ((jsx?.type as string) === 'JSXElement') { + const elementName = jsx?.openingElement?.name.name; return elementName === 'Button'; } } @@ -86,21 +115,24 @@ module.exports = { return false; } - function handler(node) { - if (node.arguments.length) { - const firstArg = node.arguments[0]; - if ( - firstArg.type === 'Literal' && - typeof firstArg.value === 'string' - ) { - const text = firstArg.value; + function handler(node: Node): void { + const callNode = node as Node & { + arguments: Array; + }; + // Check all string literal arguments (e.g., tn has singular and plural) + for (const arg of callNode.arguments ?? []) { + if (arg.type === 'Literal' && typeof arg.value === 'string') { + const text = arg.value; - if (isButtonContext(node) && isTitleCase(text)) { + if ( + isButtonContext(node as Node & { parent?: Node }) && + isTitleCase(text) + ) { const sentenceCase = text .toLowerCase() - .replace(/^\w/, c => c.toUpperCase()); + .replace(/^\w/, (c: string) => c.toUpperCase()); context.report({ - node: firstArg, + node: arg, message: `Button text should use sentence case: "${text}" should be "${sentenceCase}"`, }); } @@ -116,3 +148,5 @@ module.exports = { }, }, }; + +module.exports = plugin; diff --git a/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/no-template-vars.test.js b/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/no-template-vars.test.ts similarity index 88% rename from superset-frontend/eslint-rules/eslint-plugin-i18n-strings/no-template-vars.test.js rename to superset-frontend/eslint-rules/eslint-plugin-i18n-strings/no-template-vars.test.ts index 295a2f9fb8c..50607273da2 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/no-template-vars.test.js +++ b/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/no-template-vars.test.ts @@ -22,17 +22,19 @@ * @author Apache */ /* eslint-disable no-template-curly-in-string */ +import type { Rule } from 'eslint'; + const { RuleTester } = require('eslint'); -const plugin = require('.'); +const plugin: { rules: Record } = require('.'); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); -const rule = plugin.rules['no-template-vars']; +const rule: Rule.RuleModule = plugin.rules['no-template-vars']; -const errors = [ +const errors: Array<{ type: string }> = [ { type: 'CallExpression', }, diff --git a/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/package.json b/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/package.json index efbf7be7a95..48f5cacfde6 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/package.json +++ b/superset-frontend/eslint-rules/eslint-plugin-i18n-strings/package.json @@ -2,7 +2,7 @@ "name": "eslint-plugin-i18n-strings", "version": "1.0.0", "description": "Warns about translation variables", - "main": "index.js", + "main": "index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/superset-frontend/eslint-rules/eslint-plugin-icons/index.js b/superset-frontend/eslint-rules/eslint-plugin-icons/index.ts similarity index 55% rename from superset-frontend/eslint-rules/eslint-plugin-icons/index.js rename to superset-frontend/eslint-rules/eslint-plugin-icons/index.ts index a3dae1a3c10..280887348da 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-icons/index.js +++ b/superset-frontend/eslint-rules/eslint-plugin-icons/index.ts @@ -22,12 +22,29 @@ * @author Apache */ +import type { Rule } from 'eslint'; +import type { Node } from 'estree'; + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { +interface JSXAttribute { + name?: { name: string }; + value?: { type: string; value?: string; expression?: { value: string } }; +} + +interface JSXOpeningElement { + name: { name: string }; + attributes: JSXAttribute[]; +} + +interface JSXElementNode { + type: string; + openingElement: JSXOpeningElement; +} + +const plugin: { rules: Record } = { rules: { 'no-fa-icons-usage': { meta: { @@ -39,20 +56,27 @@ module.exports = { }, schema: [], }, - create(context) { + create(context: Rule.RuleContext): Rule.RuleListener { return { // Check for JSX elements with class names containing "fa" - JSXElement(node) { + JSXElement(node: Node): void { + const jsxNode = node as unknown as JSXElementNode; if ( - node.openingElement && - node.openingElement.name.name === 'i' && - node.openingElement.attributes && - node.openingElement.attributes.some( - attr => - attr.name && - attr.name.name === 'className' && - /fa fa-/.test(attr.value.value), - ) + jsxNode.openingElement && + jsxNode.openingElement.name.name === 'i' && + jsxNode.openingElement.attributes && + jsxNode.openingElement.attributes.some((attr: JSXAttribute) => { + if (attr.name?.name !== 'className') return false; + // Handle className="fa fa-home" + if (attr.value?.type === 'Literal') { + return /fa fa-/.test(attr.value.value ?? ''); + } + // Handle className={'fa fa-home'} + if (attr.value?.type === 'JSXExpressionContainer') { + return /fa fa-/.test(attr.value.expression?.value ?? ''); + } + return false; + }) ) { context.report({ node, @@ -66,3 +90,5 @@ module.exports = { }, }, }; + +module.exports = plugin; diff --git a/superset-frontend/eslint-rules/eslint-plugin-icons/no-fontawesome.test.js b/superset-frontend/eslint-rules/eslint-plugin-icons/no-fontawesome.test.ts similarity index 83% rename from superset-frontend/eslint-rules/eslint-plugin-icons/no-fontawesome.test.js rename to superset-frontend/eslint-rules/eslint-plugin-icons/no-fontawesome.test.ts index 52a81eabe9e..9e8c080a16f 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-icons/no-fontawesome.test.js +++ b/superset-frontend/eslint-rules/eslint-plugin-icons/no-fontawesome.test.ts @@ -22,16 +22,20 @@ * @author Apache */ +import type { Rule } from 'eslint'; + const { RuleTester } = require('eslint'); -const plugin = require('.'); +const plugin: { rules: Record } = require('.'); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ -const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); -const rule = plugin.rules['no-fa-icons-usage']; +const ruleTester = new RuleTester({ + parserOptions: { ecmaVersion: 6, ecmaFeatures: { jsx: true } }, +}); +const rule: Rule.RuleModule = plugin.rules['no-fa-icons-usage']; -const errors = [ +const errors: Array<{ message: string }> = [ { message: 'FontAwesome icons should not be used. Use the src/components/Icons component instead.', diff --git a/superset-frontend/eslint-rules/eslint-plugin-icons/package.json b/superset-frontend/eslint-rules/eslint-plugin-icons/package.json index f2118e936cd..fe545a0d38d 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-icons/package.json +++ b/superset-frontend/eslint-rules/eslint-plugin-icons/package.json @@ -2,7 +2,7 @@ "name": "eslint-plugin-icons", "version": "1.0.0", "description": "Warns about direct usage of Ant Design icons", - "main": "index.js", + "main": "index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/colors.js b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/colors.ts similarity index 97% rename from superset-frontend/eslint-rules/eslint-plugin-theme-colors/colors.js rename to superset-frontend/eslint-rules/eslint-plugin-theme-colors/colors.ts index 7c4d3a1a5d2..46974c7331f 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/colors.js +++ b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/colors.ts @@ -18,7 +18,7 @@ */ // https://www.w3.org/wiki/CSS/Properties/color/keywords -module.exports = [ +const COLOR_KEYWORDS: string[] = [ 'black', 'silver', 'gray', @@ -170,3 +170,5 @@ module.exports = [ 'whitesmoke', 'yellowgreen', ]; + +export default COLOR_KEYWORDS; diff --git a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/index.js b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/index.js deleted file mode 100644 index ed1bd4d392e..00000000000 --- a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/index.js +++ /dev/null @@ -1,119 +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. - */ - -/** - * @fileoverview Rule to warn about literal colors - * @author Apache - */ - -const COLOR_KEYWORDS = require('./colors'); - -function hasHexColor(quasi) { - if (typeof quasi === 'string') { - const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi; - return !!quasi.match(regex); - } - return false; -} - -function hasRgbColor(quasi) { - if (typeof quasi === 'string') { - const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i; - return !!quasi.match(regex); - } - return false; -} - -function hasLiteralColor(quasi, strict = false) { - if (typeof quasi === 'string') { - // matches literal colors at the start or end of a CSS prop - return COLOR_KEYWORDS.some(color => { - const regexColon = new RegExp(`: ${color}`); - const regexSemicolon = new RegExp(` ${color};`); - return ( - !!quasi.match(regexColon) || - !!quasi.match(regexSemicolon) || - (strict && quasi === color) - ); - }); - } - return false; -} - -const WARNING_MESSAGE = - 'Theme color variables are preferred over rgb(a)/hex/literal colors'; - -//------------------------------------------------------------------------------ -// Rule Definition -//------------------------------------------------------------------------------ - -/** @type {import('eslint').Rule.RuleModule} */ -module.exports = { - rules: { - 'no-literal-colors': { - create(context) { - const warned = []; - return { - TemplateElement(node) { - const rawValue = node?.value?.raw; - const isChildParentTagged = - node?.parent?.parent?.type === 'TaggedTemplateExpression'; - const isChildParentArrow = - node?.parent?.parent?.type === 'ArrowFunctionExpression'; - const isParentTemplateLiteral = - node?.parent?.type === 'TemplateLiteral'; - const loc = node?.parent?.parent?.loc; - const locId = loc && JSON.stringify(loc); - const hasWarned = warned.includes(locId); - if ( - !hasWarned && - (isChildParentTagged || - (isChildParentArrow && isParentTemplateLiteral)) && - rawValue && - (hasLiteralColor(rawValue) || - hasHexColor(rawValue) || - hasRgbColor(rawValue)) - ) { - context.report(node, loc, WARNING_MESSAGE); - warned.push(locId); - } - }, - Literal(node) { - const value = node?.value; - const isParentProperty = node?.parent?.type === 'Property'; - const locId = JSON.stringify(node.loc); - const hasWarned = warned.includes(locId); - - if ( - !hasWarned && - isParentProperty && - value && - (hasLiteralColor(value, true) || - hasHexColor(value) || - hasRgbColor(value)) - ) { - context.report(node, node.loc, WARNING_MESSAGE); - warned.push(locId); - } - }, - }; - }, - }, - }, -}; diff --git a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/index.ts b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/index.ts new file mode 100644 index 00000000000..6c6b7188f55 --- /dev/null +++ b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/index.ts @@ -0,0 +1,162 @@ +/** + * 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. + */ + +/** + * @fileoverview Rule to warn about literal colors + * @author Apache + */ + +import type { Rule } from 'eslint'; +import type { Node, SourceLocation } from 'estree'; + +import COLOR_KEYWORDS from './colors'; + +function hasHexColor(quasi: string): boolean { + const regex = /#([a-f0-9]{3}|[a-f0-9]{4}(?:[a-f0-9]{2}){0,2})\b/gi; + return !!quasi.match(regex); +} + +function hasRgbColor(quasi: string): boolean { + const regex = /rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)/i; + return !!quasi.match(regex); +} + +function hasLiteralColor(quasi: string, strict: boolean = false): boolean { + // matches literal colors at the start or end of a CSS prop + return COLOR_KEYWORDS.some((color: string) => { + const regexColon = new RegExp(`: ${color}`); + const regexSemicolon = new RegExp(` ${color};`); + return ( + !!quasi.match(regexColon) || + !!quasi.match(regexSemicolon) || + (strict && quasi === color) + ); + }); +} + +const WARNING_MESSAGE: string = + 'Theme color variables are preferred over rgb(a)/hex/literal colors'; + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +interface TemplateElementNode { + type: string; + value?: { raw: string }; + loc?: SourceLocation | null; + parent?: { + type: string; + parent?: { type: string; loc?: SourceLocation | null }; + }; +} + +interface LiteralNode { + type: string; + value?: unknown; + loc?: SourceLocation | null; + parent?: { type: string }; +} + +const plugin: { rules: Record } = { + rules: { + 'no-literal-colors': { + meta: { + type: 'suggestion', + docs: { + description: + 'Disallow literal color values; use theme colors instead', + }, + schema: [], + }, + create(context: Rule.RuleContext): Rule.RuleListener { + const warned: string[] = []; + return { + TemplateElement(node: Node): void { + const templateNode = node as TemplateElementNode; + const rawValue = templateNode?.value?.raw; + const isChildParentTagged = + templateNode?.parent?.parent?.type === 'TaggedTemplateExpression'; + const isChildParentArrow = + templateNode?.parent?.parent?.type === 'ArrowFunctionExpression'; + const isParentTemplateLiteral = + templateNode?.parent?.type === 'TemplateLiteral'; + const loc = templateNode?.parent?.parent?.loc; + const locId = loc && JSON.stringify(loc); + const hasWarned = locId ? warned.includes(locId) : false; + if ( + !hasWarned && + (isChildParentTagged || + (isChildParentArrow && isParentTemplateLiteral)) && + rawValue && + (hasLiteralColor(rawValue) || + hasHexColor(rawValue) || + hasRgbColor(rawValue)) + ) { + context.report({ + node, + ...(loc && { loc: loc as SourceLocation }), + message: WARNING_MESSAGE, + }); + if (locId) { + warned.push(locId); + } + } + }, + Literal(node: Node): void { + const literalNode = node as LiteralNode; + const value = literalNode?.value; + // Only process string literals (not numbers, booleans, null, or RegExp) + if (typeof value !== 'string') { + return; + } + const parent = literalNode?.parent as Node & { + type: string; + value?: Node; + }; + // Only check property values, not keys (e.g., { color: 'red' } not { red: 1 }) + const isPropertyValue = + parent?.type === 'Property' && parent.value === node; + const locId = node.loc ? JSON.stringify(node.loc) : null; + const hasWarned = locId ? warned.includes(locId) : false; + + if ( + !hasWarned && + isPropertyValue && + (hasLiteralColor(value, true) || + hasHexColor(value) || + hasRgbColor(value)) + ) { + context.report({ + node, + ...(node.loc && { loc: node.loc as SourceLocation }), + message: WARNING_MESSAGE, + }); + if (locId) { + warned.push(locId); + } + } + }, + }; + }, + }, + }, +}; + +module.exports = plugin; diff --git a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/package.json b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/package.json index 25938c97bd8..8ff5ecbe030 100644 --- a/superset-frontend/eslint-rules/eslint-plugin-theme-colors/package.json +++ b/superset-frontend/eslint-rules/eslint-plugin-theme-colors/package.json @@ -2,7 +2,7 @@ "name": "eslint-plugin-theme-colors", "version": "1.0.0", "description": "Warns about rgb(a)/hex/literal colors", - "main": "index.js", + "main": "index.ts", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/superset-frontend/jest.config.js b/superset-frontend/jest.config.js index e6dc6b753d5..44ae74b3de4 100644 --- a/superset-frontend/jest.config.js +++ b/superset-frontend/jest.config.js @@ -36,7 +36,13 @@ module.exports = { '^@apache-superset/core/(.*)$': '/packages/superset-core/src/$1', }, testEnvironment: '/spec/helpers/jsDomWithFetchAPI.ts', - modulePathIgnorePatterns: ['/packages/generator-superset'], + modulePathIgnorePatterns: [ + '/packages/generator-superset', + '/packages/.*/esm', + '/packages/.*/lib', + '/plugins/.*/esm', + '/plugins/.*/lib', + ], setupFilesAfterEnv: ['/spec/helpers/setup.ts'], snapshotSerializers: ['@emotion/jest/serializer'], testEnvironmentOptions: { @@ -59,7 +65,7 @@ module.exports = { ], coverageReporters: ['lcov', 'json-summary', 'html', 'text'], transformIgnorePatterns: [ - 'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|sinon|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)', + 'node_modules/(?!d3-(array|interpolate|color|time|scale|time-format)|internmap|@mapbox/tiny-sdf|remark-gfm|(?!@ngrx|(?!deck.gl)|d3-scale)|markdown-table|micromark-*.|decode-named-character-reference|character-entities|mdast-util-*.|unist-util-*.|ccount|escape-string-regexp|nanoid|uuid|@rjsf/*.|echarts|zrender|fetch-mock|pretty-ms|parse-ms|ol|@babel/runtime|@emotion|cheerio|cheerio/lib|parse5|dom-serializer|entities|htmlparser2|rehype-sanitize|hast-util-sanitize|unified|unist-.*|hast-.*|rehype-.*|remark-.*|mdast-.*|micromark-.*|parse-entities|property-information|space-separated-tokens|comma-separated-tokens|bail|devlop|zwitch|longest-streak|geostyler|geostyler-.*|react-error-boundary|react-json-tree|react-base16-styling|lodash-es)', ], preset: 'ts-jest', transform: { diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index f84a8ca9730..1a97f933ab2 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -62,6 +62,7 @@ "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", + "@types/d3-selection": "^3.0.11", "@types/d3-time-format": "^4.0.3", "@types/react-google-recaptcha": "^2.1.9", "@visx/axis": "^3.8.0", @@ -109,7 +110,6 @@ "mustache": "^4.2.0", "nanoid": "^5.1.6", "ol": "^7.5.2", - "prop-types": "^15.8.1", "query-string": "9.3.1", "re-resizable": "^6.11.2", "react": "^17.0.2", @@ -215,7 +215,6 @@ "@types/redux-localstorage": "^1.0.8", "@types/redux-mock-store": "^1.0.6", "@types/rison": "0.1.0", - "@types/sinon": "^17.0.3", "@types/tinycolor2": "^1.4.3", "@types/unzipper": "^0.10.11", "@typescript-eslint/eslint-plugin": "^7.18.0", @@ -225,7 +224,6 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-jsx-remove-data-test-id": "^3.0.0", "babel-plugin-lodash": "^3.3.4", - "babel-plugin-typescript-to-proptypes": "^2.0.0", "baseline-browser-mapping": "^2.9.19", "cheerio": "1.2.0", "concurrently": "^9.2.1", @@ -277,7 +275,6 @@ "react-refresh": "^0.18.0", "react-resizable": "^3.1.3", "redux-mock-store": "^1.5.4", - "sinon": "^18.0.0", "source-map": "^0.7.6", "source-map-support": "^0.5.21", "speed-measure-webpack-plugin": "^1.5.0", @@ -287,7 +284,6 @@ "terser-webpack-plugin": "^5.3.16", "thread-loader": "^4.0.4", "ts-jest": "^29.4.6", - "ts-loader": "^9.5.4", "tscw-config": "^1.1.2", "tsx": "^4.21.0", "typescript": "5.4.5", @@ -4122,18 +4118,18 @@ } }, "node_modules/@deck.gl/core": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.5.tgz", - "integrity": "sha512-/PGNX4Wd7rEahYi6ivC4WExJ3U6Hqgl42R83guNzTL6gM2+02PUQRoQG9QdFagj5d6kWYVN0LVJME2a5WQmzOg==", + "version": "9.2.6", + "resolved": "https://registry.npmjs.org/@deck.gl/core/-/core-9.2.6.tgz", + "integrity": "sha512-bBFfwfythPPpXS/OKUMvziQ8td84mRGMnYZfqdUvfUVltzjFtQCBQUJTzgo3LubvOzSnzo8GTWskxHaZzkqdKQ==", "license": "MIT", "dependencies": { "@loaders.gl/core": "^4.2.0", "@loaders.gl/images": "^4.2.0", - "@luma.gl/constants": "^9.2.4", - "@luma.gl/core": "^9.2.4", - "@luma.gl/engine": "^9.2.4", - "@luma.gl/shadertools": "^9.2.4", - "@luma.gl/webgl": "^9.2.4", + "@luma.gl/constants": "^9.2.6", + "@luma.gl/core": "^9.2.6", + "@luma.gl/engine": "^9.2.6", + "@luma.gl/shadertools": "^9.2.6", + "@luma.gl/webgl": "^9.2.6", "@math.gl/core": "^4.1.0", "@math.gl/sun": "^4.1.0", "@math.gl/types": "^4.1.0", @@ -14923,35 +14919,6 @@ "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", - "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "lodash.get": "^4.4.2", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, "node_modules/@standard-schema/spec": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", @@ -19413,6 +19380,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, "node_modules/@types/d3-shape": { "version": "1.3.12", "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-1.3.12.tgz", @@ -20408,23 +20381,6 @@ "@types/send": "*" } }, - "node_modules/@types/sinon": { - "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", - "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/sinonjs__fake-timers": "*" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/sizzle": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", @@ -24064,26 +24020,6 @@ "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-typescript-to-proptypes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/babel-plugin-typescript-to-proptypes/-/babel-plugin-typescript-to-proptypes-2.1.0.tgz", - "integrity": "sha512-jTV65uJPnSmW/SddPxv+OFBf05sv6JUq64iQCnuzU68/rK/O8dE8dVPIIy7URc4dG2MfmptKotmZGS0myO6loQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.15.4", - "@babel/plugin-syntax-typescript": "^7.14.5", - "@babel/types": "^7.15.6" - }, - "engines": { - "node": ">=12.17.0", - "npm": ">=6.13.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "typescript": "^4.0.0 || ^5.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -41588,13 +41524,6 @@ "dev": true, "license": "MIT" }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -44770,40 +44699,6 @@ "node": ">=v0.2.0" } }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, - "node_modules/nise/node_modules/@sinonjs/fake-timers": { - "version": "13.0.5", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", - "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1" - } - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", - "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - } - }, "node_modules/no-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", @@ -48249,9 +48144,9 @@ "license": "ISC" }, "node_modules/preact": { - "version": "10.25.4", - "resolved": "https://registry.npmjs.org/preact/-/preact-10.25.4.tgz", - "integrity": "sha512-jLdZDb+Q+odkHJ+MpW/9U5cODzqnB+fy2EiHSZES7ldV5LK7yjlVzTp7R8Xy6W6y75kfK8iWYtFVH7lvjwrCMA==", + "version": "10.28.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz", + "integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==", "license": "MIT", "peer": true, "funding": { @@ -53797,35 +53692,6 @@ "readable-stream": "3" } }, - "node_modules/sinon": { - "version": "18.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-18.0.1.tgz", - "integrity": "sha512-a2N2TDY1uGviajJ6r4D1CyRAkzE9NNVlYOV1wX5xQDuAk0ONgzgRl0EjCQuRCPxOwp13ghsMwt9Gdldujs39qw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "11.2.2", - "@sinonjs/samsam": "^8.0.0", - "diff": "^5.2.0", - "nise": "^6.0.0", - "supports-color": "^7" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/@sinonjs/fake-timers": { - "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, "node_modules/sirv": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", @@ -61271,7 +61137,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/cliui": { "version": "8.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61288,7 +61154,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { "version": "6.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61300,13 +61166,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { "version": "9.2.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { "version": "5.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61323,7 +61189,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { "version": "7.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61338,7 +61204,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/fs-minipass": { "version": "4.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61350,13 +61216,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/@isaacs/string-locale-compare": { "version": "1.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/agent": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61372,7 +61238,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/arborist": { "version": "9.1.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61420,7 +61286,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/config": { "version": "10.3.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61439,7 +61305,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/fs": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61451,7 +61317,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/git": { "version": "6.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61470,7 +61336,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/installed-package-contents": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61486,7 +61352,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/map-workspaces": { "version": "4.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61501,7 +61367,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/metavuln-calculator": { "version": "9.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61517,7 +61383,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/name-from-folder": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -61526,7 +61392,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/node-gyp": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -61535,7 +61401,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/package-json": { "version": "6.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61553,7 +61419,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/promise-spawn": { "version": "8.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61565,7 +61431,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/query": { "version": "4.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61577,7 +61443,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/redact": { "version": "3.2.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -61586,7 +61452,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@npmcli/run-script": { "version": "9.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61603,16 +61469,17 @@ }, "packages/superset-core/node_modules/npm/node_modules/@pkgjs/parseargs": { "version": "0.11.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "engines": { "node": ">=14" } }, "packages/superset-core/node_modules/npm/node_modules/@sigstore/bundle": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -61624,7 +61491,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@sigstore/core": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "engines": { @@ -61633,7 +61500,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@sigstore/protobuf-specs": { "version": "0.4.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "engines": { @@ -61642,7 +61509,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@sigstore/sign": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -61659,7 +61526,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@sigstore/tuf": { "version": "3.1.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -61672,7 +61539,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@sigstore/verify": { "version": "2.1.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -61686,7 +61553,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@tufjs/canonical-json": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61695,7 +61562,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/@tufjs/models": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61708,7 +61575,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/abbrev": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -61717,7 +61584,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/agent-base": { "version": "7.1.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61726,7 +61593,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ansi-regex": { "version": "5.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61735,7 +61602,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ansi-styles": { "version": "6.2.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61747,25 +61614,25 @@ }, "packages/superset-core/node_modules/npm/node_modules/aproba": { "version": "2.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, "packages/superset-core/node_modules/npm/node_modules/archy": { "version": "1.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/balanced-match": { "version": "1.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/bin-links": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61781,7 +61648,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/binary-extensions": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61793,7 +61660,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/brace-expansion": { "version": "2.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61802,7 +61669,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cacache": { "version": "19.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61825,7 +61692,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cacache/node_modules/chownr": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "engines": { @@ -61834,7 +61701,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cacache/node_modules/minizlib": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61846,7 +61713,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cacache/node_modules/mkdirp": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -61861,7 +61728,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cacache/node_modules/tar": { "version": "7.4.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -61878,7 +61745,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cacache/node_modules/yallist": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "engines": { @@ -61887,7 +61754,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/chalk": { "version": "5.4.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -61899,7 +61766,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/chownr": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -61908,7 +61775,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ci-info": { "version": "4.3.0", - "extraneous": true, + "dev": true, "funding": [ { "type": "github", @@ -61923,7 +61790,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cidr-regex": { "version": "4.1.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -61935,7 +61802,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cli-columns": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61948,7 +61815,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cmd-shim": { "version": "7.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -61957,7 +61824,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/color-convert": { "version": "2.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61969,19 +61836,19 @@ }, "packages/superset-core/node_modules/npm/node_modules/color-name": { "version": "1.1.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/common-ancestor-path": { "version": "1.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, "packages/superset-core/node_modules/npm/node_modules/cross-spawn": { "version": "7.0.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -61995,7 +61862,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cross-spawn/node_modules/which": { "version": "2.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62010,7 +61877,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/cssesc": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -62022,7 +61889,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/debug": { "version": "4.4.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62039,7 +61906,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/diff": { "version": "7.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-3-Clause", "engines": { @@ -62048,28 +61915,29 @@ }, "packages/superset-core/node_modules/npm/node_modules/eastasianwidth": { "version": "0.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/emoji-regex": { "version": "8.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/encoding": { "version": "0.1.13", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "iconv-lite": "^0.6.2" } }, "packages/superset-core/node_modules/npm/node_modules/env-paths": { "version": "2.2.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62078,19 +61946,19 @@ }, "packages/superset-core/node_modules/npm/node_modules/err-code": { "version": "2.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/exponential-backoff": { "version": "3.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0" }, "packages/superset-core/node_modules/npm/node_modules/fastest-levenshtein": { "version": "1.0.16", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62099,7 +61967,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/foreground-child": { "version": "3.3.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62115,7 +61983,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/fs-minipass": { "version": "3.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62127,7 +61995,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/glob": { "version": "10.4.5", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62147,13 +62015,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/graceful-fs": { "version": "4.2.11", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, "packages/superset-core/node_modules/npm/node_modules/hosted-git-info": { "version": "8.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62165,13 +62033,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/http-cache-semantics": { "version": "4.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-2-Clause" }, "packages/superset-core/node_modules/npm/node_modules/http-proxy-agent": { "version": "7.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62184,7 +62052,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/https-proxy-agent": { "version": "7.0.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62197,9 +62065,10 @@ }, "packages/superset-core/node_modules/npm/node_modules/iconv-lite": { "version": "0.6.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", + "optional": true, "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, @@ -62209,7 +62078,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ignore-walk": { "version": "7.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62221,7 +62090,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/imurmurhash": { "version": "0.1.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62230,7 +62099,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ini": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -62239,7 +62108,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/init-package-json": { "version": "8.2.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62257,7 +62126,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ip-address": { "version": "9.0.5", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62270,7 +62139,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/ip-regex": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62282,7 +62151,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/is-cidr": { "version": "5.1.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -62294,7 +62163,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/is-fullwidth-code-point": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62303,13 +62172,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/isexe": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, "packages/superset-core/node_modules/npm/node_modules/jackspeak": { "version": "3.4.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -62324,13 +62193,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/jsbn": { "version": "1.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/json-parse-even-better-errors": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62339,7 +62208,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/json-stringify-nice": { "version": "1.1.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -62348,28 +62217,28 @@ }, "packages/superset-core/node_modules/npm/node_modules/jsonparse": { "version": "1.3.1", + "dev": true, "engines": [ "node >= 0.2.0" ], - "extraneous": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/just-diff": { "version": "6.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/just-diff-apply": { "version": "5.5.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/libnpmaccess": { "version": "10.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62382,7 +62251,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmdiff": { "version": "8.0.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62401,7 +62270,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmexec": { "version": "10.1.5", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62423,7 +62292,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmfund": { "version": "7.0.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62435,7 +62304,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmorg": { "version": "8.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62448,7 +62317,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmpack": { "version": "9.0.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62463,7 +62332,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmpublish": { "version": "11.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62482,7 +62351,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmsearch": { "version": "9.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62494,7 +62363,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmteam": { "version": "8.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62507,7 +62376,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/libnpmversion": { "version": "8.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62523,13 +62392,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/lru-cache": { "version": "10.4.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, "packages/superset-core/node_modules/npm/node_modules/make-fetch-happen": { "version": "14.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62551,7 +62420,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { "version": "1.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -62560,7 +62429,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minimatch": { "version": "9.0.5", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62575,7 +62444,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass": { "version": "7.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -62584,7 +62453,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-collect": { "version": "2.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62596,7 +62465,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-fetch": { "version": "4.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62613,7 +62482,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-fetch/node_modules/minizlib": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62625,7 +62494,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-flush": { "version": "1.0.5", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62637,7 +62506,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { "version": "3.3.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62649,7 +62518,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-pipeline": { "version": "1.2.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62661,7 +62530,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { "version": "3.3.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62673,7 +62542,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-sized": { "version": "1.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62685,7 +62554,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { "version": "3.3.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62697,7 +62566,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minizlib": { "version": "2.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62710,7 +62579,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/minizlib/node_modules/minipass": { "version": "3.3.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62722,7 +62591,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/mkdirp": { "version": "1.0.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -62734,13 +62603,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/ms": { "version": "2.1.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/mute-stream": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -62749,7 +62618,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/node-gyp": { "version": "11.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62773,7 +62642,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/node-gyp/node_modules/chownr": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "engines": { @@ -62782,7 +62651,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62794,7 +62663,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "bin": { @@ -62809,7 +62678,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/node-gyp/node_modules/tar": { "version": "7.4.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62826,7 +62695,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/node-gyp/node_modules/yallist": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "engines": { @@ -62835,7 +62704,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/nopt": { "version": "8.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62850,7 +62719,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/normalize-package-data": { "version": "7.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -62864,7 +62733,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-audit-report": { "version": "6.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -62873,7 +62742,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-bundled": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62885,7 +62754,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-install-checks": { "version": "7.1.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "dependencies": { @@ -62897,7 +62766,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-normalize-package-bin": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -62906,7 +62775,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-package-arg": { "version": "12.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62921,7 +62790,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-packlist": { "version": "10.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62933,7 +62802,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-pick-manifest": { "version": "10.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62948,7 +62817,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-profile": { "version": "11.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62961,7 +62830,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-registry-fetch": { "version": "18.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -62980,7 +62849,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-registry-fetch/node_modules/minizlib": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -62992,7 +62861,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/npm-user-validate": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-2-Clause", "engines": { @@ -63001,7 +62870,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/p-map": { "version": "7.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63013,13 +62882,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/package-json-from-dist": { "version": "1.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0" }, "packages/superset-core/node_modules/npm/node_modules/pacote": { "version": "21.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63050,7 +62919,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/parse-conflict-json": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63064,7 +62933,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/path-key": { "version": "3.1.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63073,7 +62942,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/path-scurry": { "version": "1.11.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -63089,7 +62958,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/postcss-selector-parser": { "version": "7.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63102,7 +62971,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/proc-log": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63111,7 +62980,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/proggy": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63120,7 +62989,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/promise-all-reject-late": { "version": "1.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -63129,7 +62998,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/promise-call-limit": { "version": "3.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "funding": { @@ -63138,7 +63007,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/promise-retry": { "version": "2.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63151,7 +63020,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/promzard": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63163,7 +63032,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/qrcode-terminal": { "version": "0.12.0", - "extraneous": true, + "dev": true, "inBundle": true, "bin": { "qrcode-terminal": "bin/qrcode-terminal.js" @@ -63171,7 +63040,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/read": { "version": "4.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63183,7 +63052,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/read-cmd-shim": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63192,7 +63061,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/read-package-json-fast": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63205,7 +63074,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/retry": { "version": "0.12.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63214,13 +63083,14 @@ }, "packages/superset-core/node_modules/npm/node_modules/safer-buffer": { "version": "2.1.2", - "extraneous": true, + "dev": true, "inBundle": true, - "license": "MIT" + "license": "MIT", + "optional": true }, "packages/superset-core/node_modules/npm/node_modules/semver": { "version": "7.7.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "bin": { @@ -63232,7 +63102,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/shebang-command": { "version": "2.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63244,7 +63114,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/shebang-regex": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63253,7 +63123,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/signal-exit": { "version": "4.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63265,7 +63135,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/sigstore": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -63282,7 +63152,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/smart-buffer": { "version": "4.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63292,7 +63162,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/socks": { "version": "2.8.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63306,7 +63176,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/socks-proxy-agent": { "version": "8.0.5", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63320,7 +63190,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/spdx-correct": { "version": "3.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -63330,7 +63200,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63340,13 +63210,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/spdx-exceptions": { "version": "2.5.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "CC-BY-3.0" }, "packages/superset-core/node_modules/npm/node_modules/spdx-expression-parse": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63356,19 +63226,19 @@ }, "packages/superset-core/node_modules/npm/node_modules/spdx-license-ids": { "version": "3.0.21", - "extraneous": true, + "dev": true, "inBundle": true, "license": "CC0-1.0" }, "packages/superset-core/node_modules/npm/node_modules/sprintf-js": { "version": "1.1.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "BSD-3-Clause" }, "packages/superset-core/node_modules/npm/node_modules/ssri": { "version": "12.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63380,7 +63250,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/string-width": { "version": "4.2.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63395,7 +63265,7 @@ "packages/superset-core/node_modules/npm/node_modules/string-width-cjs": { "name": "string-width", "version": "4.2.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63409,7 +63279,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/strip-ansi": { "version": "6.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63422,7 +63292,7 @@ "packages/superset-core/node_modules/npm/node_modules/strip-ansi-cjs": { "name": "strip-ansi", "version": "6.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63434,7 +63304,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/supports-color": { "version": "10.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63446,7 +63316,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tar": { "version": "6.2.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63463,7 +63333,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tar/node_modules/fs-minipass": { "version": "2.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63475,7 +63345,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { "version": "3.3.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63487,7 +63357,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tar/node_modules/minipass": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63496,19 +63366,19 @@ }, "packages/superset-core/node_modules/npm/node_modules/text-table": { "version": "0.2.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/tiny-relative-date": { "version": "1.3.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/tinyglobby": { "version": "0.2.14", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63524,7 +63394,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { "version": "6.4.6", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "peerDependencies": { @@ -63538,7 +63408,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { "version": "4.0.3", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63550,7 +63420,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/treeverse": { "version": "3.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63559,7 +63429,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/tuf-js": { "version": "3.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63573,7 +63443,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/unique-filename": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63585,7 +63455,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/unique-slug": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63597,13 +63467,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/util-deprecate": { "version": "1.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/validate-npm-package-license": { "version": "3.0.4", - "extraneous": true, + "dev": true, "inBundle": true, "license": "Apache-2.0", "dependencies": { @@ -63613,7 +63483,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { "version": "3.0.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63623,7 +63493,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/validate-npm-package-name": { "version": "6.0.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63632,7 +63502,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/walk-up-path": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63641,7 +63511,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/which": { "version": "5.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63656,7 +63526,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/which/node_modules/isexe": { "version": "3.1.1", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "engines": { @@ -63665,7 +63535,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/wrap-ansi": { "version": "8.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63683,7 +63553,7 @@ "packages/superset-core/node_modules/npm/node_modules/wrap-ansi-cjs": { "name": "wrap-ansi", "version": "7.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63700,7 +63570,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { "version": "4.3.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63715,7 +63585,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { "version": "6.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "engines": { @@ -63727,13 +63597,13 @@ }, "packages/superset-core/node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { "version": "9.2.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT" }, "packages/superset-core/node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { "version": "5.1.2", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63750,7 +63620,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { "version": "7.1.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "MIT", "dependencies": { @@ -63765,7 +63635,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/write-file-atomic": { "version": "6.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC", "dependencies": { @@ -63778,7 +63648,7 @@ }, "packages/superset-core/node_modules/npm/node_modules/yallist": { "version": "4.0.0", - "extraneous": true, + "dev": true, "inBundle": true, "license": "ISC" }, @@ -65996,75 +65866,6 @@ "react-map-gl": "^6.1.19" } }, - "plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/aggregation-layers": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@deck.gl/aggregation-layers/-/aggregation-layers-9.2.5.tgz", - "integrity": "sha512-WHb3W2IhpdppHA7YAco/s6VL3hH1S+TCIMB9S+KtGGfC557eBGycuJvqJEEPeuRz9vVNchEevwuY+0SXL+4dOw==", - "license": "MIT", - "dependencies": { - "@luma.gl/constants": "^9.2.4", - "@luma.gl/shadertools": "^9.2.4", - "@math.gl/core": "^4.1.0", - "@math.gl/web-mercator": "^4.1.0", - "d3-hexbin": "^0.2.1" - }, - "peerDependencies": { - "@deck.gl/core": "~9.2.0", - "@deck.gl/layers": "~9.2.0", - "@luma.gl/core": "~9.2.4", - "@luma.gl/engine": "~9.2.4" - } - }, - "plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/extensions": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@deck.gl/extensions/-/extensions-9.2.5.tgz", - "integrity": "sha512-GJRPmG+GD1tdblpplQlb4jlNywRb8aQYPEowPLKxglXSGRzgpOrqJYI1PcJhCowdL7/S8bCY1ay8nkXE3gRsgw==", - "license": "MIT", - "dependencies": { - "@luma.gl/constants": "^9.2.4", - "@luma.gl/shadertools": "^9.2.4", - "@math.gl/core": "^4.1.0" - }, - "peerDependencies": { - "@deck.gl/core": "~9.2.0", - "@luma.gl/core": "~9.2.4", - "@luma.gl/engine": "~9.2.4" - } - }, - "plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/geo-layers": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@deck.gl/geo-layers/-/geo-layers-9.2.5.tgz", - "integrity": "sha512-QVjLwHEAtNqRdjBuYJwztCAwSTmgWujPT0geGWaFhr7ZvyigAqi3l2ETys5YqjjJ87bKnUBwd6iOyw1xkXbsCw==", - "license": "MIT", - "dependencies": { - "@loaders.gl/3d-tiles": "^4.2.0", - "@loaders.gl/gis": "^4.2.0", - "@loaders.gl/loader-utils": "^4.2.0", - "@loaders.gl/mvt": "^4.2.0", - "@loaders.gl/schema": "^4.2.0", - "@loaders.gl/terrain": "^4.2.0", - "@loaders.gl/tiles": "^4.2.0", - "@loaders.gl/wms": "^4.2.0", - "@luma.gl/gltf": "^9.2.4", - "@luma.gl/shadertools": "^9.2.4", - "@math.gl/core": "^4.1.0", - "@math.gl/culling": "^4.1.0", - "@math.gl/web-mercator": "^4.1.0", - "@types/geojson": "^7946.0.8", - "a5-js": "^0.5.0", - "h3-js": "^4.1.0", - "long": "^3.2.0" - }, - "peerDependencies": { - "@deck.gl/core": "~9.2.0", - "@deck.gl/extensions": "~9.2.0", - "@deck.gl/layers": "~9.2.0", - "@deck.gl/mesh-layers": "~9.2.0", - "@loaders.gl/core": "^4.2.0", - "@luma.gl/core": "~9.2.4", - "@luma.gl/engine": "~9.2.4" - } - }, "plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/mesh-layers": { "version": "9.2.5", "resolved": "https://registry.npmjs.org/@deck.gl/mesh-layers/-/mesh-layers-9.2.5.tgz", @@ -66084,32 +65885,6 @@ "@luma.gl/shadertools": "~9.2.4" } }, - "plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/react": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@deck.gl/react/-/react-9.2.5.tgz", - "integrity": "sha512-pkb7kR1CppvRI5KTnMVTYNZx6f/L4OmuWpyACjRBlVjUlmSd9PddKF3LX1o8435yHDKACAfMYb8N2Fc3HRNMcA==", - "license": "MIT", - "peerDependencies": { - "@deck.gl/core": "~9.2.0", - "@deck.gl/widgets": "~9.2.0", - "react": ">=16.3.0", - "react-dom": ">=16.3.0" - } - }, - "plugins/legacy-preset-chart-deckgl/node_modules/@deck.gl/widgets": { - "version": "9.2.5", - "resolved": "https://registry.npmjs.org/@deck.gl/widgets/-/widgets-9.2.5.tgz", - "integrity": "sha512-nzukGcyaVag6XRT8aP3A8Ul2bSdb/TyFmenbDNRhrYop0OdmDl9Bx0U9wZlVlY6vxpB3kssfBXAv+MTdRr2JfA==", - "license": "MIT", - "peer": true, - "dependencies": { - "preact": "^10.17.0" - }, - "peerDependencies": { - "@deck.gl/core": "~9.2.0", - "@luma.gl/core": "~9.2.4" - } - }, "plugins/legacy-preset-chart-deckgl/node_modules/@luma.gl/gltf": { "version": "9.2.4", "resolved": "https://registry.npmjs.org/@luma.gl/gltf/-/gltf-9.2.4.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index de33e5fe277..8cb6a67ec97 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -138,6 +138,7 @@ "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", + "@types/d3-selection": "^3.0.11", "@types/d3-time-format": "^4.0.3", "@types/react-google-recaptcha": "^2.1.9", "@visx/axis": "^3.8.0", @@ -191,7 +192,6 @@ "mustache": "^4.2.0", "nanoid": "^5.1.6", "ol": "^7.5.2", - "prop-types": "^15.8.1", "query-string": "9.3.1", "re-resizable": "^6.11.2", "react": "^17.0.2", @@ -297,7 +297,6 @@ "@types/redux-localstorage": "^1.0.8", "@types/redux-mock-store": "^1.0.6", "@types/rison": "0.1.0", - "@types/sinon": "^17.0.3", "@types/tinycolor2": "^1.4.3", "@types/unzipper": "^0.10.11", "@typescript-eslint/eslint-plugin": "^7.18.0", @@ -307,7 +306,6 @@ "babel-plugin-dynamic-import-node": "^2.3.3", "babel-plugin-jsx-remove-data-test-id": "^3.0.0", "babel-plugin-lodash": "^3.3.4", - "babel-plugin-typescript-to-proptypes": "^2.0.0", "baseline-browser-mapping": "^2.9.19", "cheerio": "1.2.0", "concurrently": "^9.2.1", @@ -359,7 +357,6 @@ "react-refresh": "^0.18.0", "react-resizable": "^3.1.3", "redux-mock-store": "^1.5.4", - "sinon": "^18.0.0", "source-map": "^0.7.6", "source-map-support": "^0.5.21", "speed-measure-webpack-plugin": "^1.5.0", @@ -369,7 +366,6 @@ "terser-webpack-plugin": "^5.3.16", "thread-loader": "^4.0.4", "ts-jest": "^29.4.6", - "ts-loader": "^9.5.4", "tscw-config": "^1.1.2", "tsx": "^4.21.0", "typescript": "5.4.5", diff --git a/superset-frontend/packages/generator-superset/test/app.test.js b/superset-frontend/packages/generator-superset/test/app.test.ts similarity index 92% rename from superset-frontend/packages/generator-superset/test/app.test.js rename to superset-frontend/packages/generator-superset/test/app.test.ts index 4a174d40133..ee0210b8738 100644 --- a/superset-frontend/packages/generator-superset/test/app.test.js +++ b/superset-frontend/packages/generator-superset/test/app.test.ts @@ -17,6 +17,7 @@ * under the License. */ +// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments import helpers, { result } from 'yeoman-test'; import appModule from '../generators/app'; diff --git a/superset-frontend/packages/generator-superset/test/plugin-chart.test.js b/superset-frontend/packages/generator-superset/test/plugin-chart.test.ts similarity index 89% rename from superset-frontend/packages/generator-superset/test/plugin-chart.test.js rename to superset-frontend/packages/generator-superset/test/plugin-chart.test.ts index a1d4c410497..4687d27db5f 100644 --- a/superset-frontend/packages/generator-superset/test/plugin-chart.test.js +++ b/superset-frontend/packages/generator-superset/test/plugin-chart.test.ts @@ -18,15 +18,17 @@ */ import { dirname, join } from 'path'; -import helpers, { result } from 'yeoman-test'; +// @ts-ignore -- yeoman-test type resolution differs between local and Docker environments +import helpers from 'yeoman-test'; +// @ts-ignore -- fs-extra/esm has no type declarations import { copySync } from 'fs-extra/esm'; import { fileURLToPath } from 'url'; import pluginChartModule from '../generators/plugin-chart'; test('generator-superset:plugin-chart:creates files', async () => { - await helpers + const result = await helpers .run(pluginChartModule) - .onTargetDirectory(dir => { + .onTargetDirectory((dir: string) => { // `dir` is the path to the new temporary directory const generatorDirname = dirname(fileURLToPath(import.meta.url)); copySync( diff --git a/superset-frontend/packages/superset-core/src/ui/theme/utils/utils.test.ts b/superset-frontend/packages/superset-core/src/ui/theme/utils/utils.test.ts index 20603d81a1d..c1a45d73388 100644 --- a/superset-frontend/packages/superset-core/src/ui/theme/utils/utils.test.ts +++ b/superset-frontend/packages/superset-core/src/ui/theme/utils/utils.test.ts @@ -202,7 +202,7 @@ test('serializeThemeConfig defaults to "default" for unknown algorithms', () => const unknownAlgorithm = () => ({}); const config: AntdThemeConfig = { token: { colorPrimary: '#ff0000' }, - // @ts-ignore + // @ts-expect-error algorithm: unknownAlgorithm, }; @@ -237,7 +237,7 @@ test('serializeThemeConfig defaults each unknown algorithm in array to "default" const unknownAlgorithm = () => ({}); const config: AntdThemeConfig = { token: { colorPrimary: '#ff0000' }, - // @ts-ignore + // @ts-expect-error algorithm: [antdThemeImport.darkAlgorithm, unknownAlgorithm], }; @@ -257,10 +257,10 @@ test('serializeThemeConfig handles mixed known and unknown algorithms in array', token: { colorPrimary: '#ff0000' }, algorithm: [ antdThemeImport.darkAlgorithm, - // @ts-ignore + // @ts-expect-error unknownAlgorithm1, antdThemeImport.compactAlgorithm, - // @ts-ignore + // @ts-expect-error unknownAlgorithm2, ], }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/extractExtraMetrics.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/extractExtraMetrics.test.ts index 116bfa7fd9a..dd2a10cbe55 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/extractExtraMetrics.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/operators/utils/extractExtraMetrics.test.ts @@ -129,7 +129,7 @@ test('returns empty array if timeseries_limit_metric is an empty array', () => { expect( extractExtraMetrics({ ...baseFormData, - // @ts-ignore + // @ts-expect-error timeseries_limit_metric: [], }), ).toEqual([]); diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx index f1c64ad4a71..a32a49028da 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/defineSavedMetrics.test.tsx @@ -51,7 +51,7 @@ describe('defineSavedMetrics', () => { uuid: '1', }, ]); - // @ts-ignore + // @ts-expect-error expect(defineSavedMetrics({ ...dataset, metrics: undefined })).toEqual([]); }); diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts index 68a06e2d15a..e1be6ae36e3 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getColorFormatters.test.ts @@ -306,7 +306,7 @@ test('getColorFunction BETWEEN with target value right undefined', () => { test('getColorFunction unsupported operator', () => { const colorFunction = getColorFunction( { - // @ts-ignore + // @ts-expect-error operator: 'unsupported operator', targetValue: 50, colorScheme: '#FF0000', diff --git a/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx b/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx index df88ff4b540..3d20b8b0f07 100644 --- a/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx +++ b/superset-frontend/packages/superset-ui-core/src/chart/components/FallbackComponent.tsx @@ -20,7 +20,6 @@ import { t } from '@apache-superset/core'; import { SupersetTheme } from '@apache-superset/core/ui'; import { FallbackPropsWithDimension } from './SuperChart'; -import { getErrorMessage } from 'react-error-boundary'; export type Props = Partial; @@ -39,7 +38,13 @@ export default function FallbackComponent({ error, height, width }: Props) {
{t('Oops! An error occurred!')}
- {error ? getErrorMessage(error) : 'Unknown Error'} + + {error instanceof Error + ? error.message + : error + ? String(error) + : t('Unknown Error')} + ); diff --git a/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx index dd643a8e1a6..5f8562cd3eb 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/AsyncEsmComponent/index.tsx @@ -119,7 +119,7 @@ export function AsyncEsmComponent< const Component = component || placeholder; return Component ? ( // placeholder does not get the ref - // @ts-ignore: Suppress TypeScript error for ref assignment + // @ts-expect-error: Suppress TypeScript error for ref assignment ) : null; }); diff --git a/superset-frontend/packages/superset-ui-core/src/components/CronPicker/CronPicker.stories.tsx b/superset-frontend/packages/superset-ui-core/src/components/CronPicker/CronPicker.stories.tsx index 1bc2748f939..1bbfa307487 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/CronPicker/CronPicker.stories.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/CronPicker/CronPicker.stories.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { useState, useRef, useCallback } from 'react'; +import { useState, useCallback, useEffect } from 'react'; import { Divider } from '../Divider'; import { Input } from '../Input'; import { CronPicker } from '.'; @@ -28,22 +28,19 @@ export default { }; export const InteractiveCronPicker = (props: CronProps) => { - // @ts-ignore - const inputRef = useRef(null); const [value, setValue] = useState(props.value); - const customSetValue = useCallback( - (newValue: string) => { - setValue(newValue); - inputRef.current?.setValue(newValue); - }, - [inputRef], - ); + useEffect(() => { + setValue(props.value); + }, [props.value]); + const customSetValue = useCallback((newValue: string) => { + setValue(newValue); + }, []); const [error, onError] = useState(); return (
{ setValue(event.target.value); }} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Dropdown/Dropdown.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Dropdown/Dropdown.test.tsx index 9baaef9adbf..02ef9440581 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Dropdown/Dropdown.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Dropdown/Dropdown.test.tsx @@ -26,7 +26,7 @@ const props = { describe('NoAnimationDropdown', () => { it('requires children', () => { expect(() => { - // @ts-ignore need to test the error case + // @ts-expect-error need to test the error case render(); }).toThrow(); }); diff --git a/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.test.jsx b/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.test.tsx similarity index 95% rename from superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.test.jsx rename to superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.test.tsx index 2b66127e237..2feca1d0fd7 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.test.jsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Form/LabeledErrorBoundInput.test.tsx @@ -17,13 +17,14 @@ * under the License. */ import { render, fireEvent, screen } from '@superset-ui/core/spec'; +import type { LabeledErrorBoundInputProps } from './types'; import { LabeledErrorBoundInput } from './LabeledErrorBoundInput'; -const defaultProps = { - id: 1, +const defaultProps: LabeledErrorBoundInputProps = { + id: '1', label: 'Username', name: 'Username', - validationMethods: () => {}, + validationMethods: { onBlur: () => {} }, errorMessage: '', helpText: 'This is a line of example help text', hasTooltip: false, diff --git a/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.test.jsx b/superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.test.tsx similarity index 100% rename from superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.test.jsx rename to superset-frontend/packages/superset-ui-core/src/components/IconTooltip/IconTooltip.test.tsx diff --git a/superset-frontend/packages/superset-ui-core/src/components/Label/Label.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/Label/Label.test.tsx index 1dff7c6e697..9d5dc184d31 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Label/Label.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Label/Label.test.tsx @@ -36,7 +36,7 @@ test('works with an onClick handler', () => { // test stories from the storybook! test('renders all the storybook gallery variants', () => { - // @ts-ignore: Suppress TypeScript error for LabelGallery usage + // @ts-expect-error: Suppress TypeScript error for LabelGallery usage const { container } = render(); const nonInteractiveLabelCount = 4; const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount; diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx index bfa2e801a30..172632da03d 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/AsyncSelect.tsx @@ -479,10 +479,10 @@ const AsyncSelect = forwardRef( fullSelectOptions.filter(opt => set.has(opt.value)), ); if (isSingleMode) { - // @ts-ignore + // @ts-expect-error onChange?.(selectValue, options[0]); } else { - // @ts-ignore + // @ts-expect-error onChange?.(array, options); } } @@ -619,7 +619,7 @@ const AsyncSelect = forwardRef( onBlur={handleOnBlur} onDeselect={handleOnDeselect} onOpenChange={handleOnDropdownVisibleChange} - // @ts-ignore + // @ts-expect-error onPaste={onPaste} onPopupScroll={handlePagination} onSearch={showSearch ? handleOnSearch : undefined} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx index 222ba643bcd..569f865333c 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/Select/Select.tsx @@ -748,7 +748,7 @@ const Select = forwardRef( onBlur={handleOnBlur} onDeselect={handleOnDeselect} onOpenChange={handleOnDropdownVisibleChange} - // @ts-ignore + // @ts-expect-error onPaste={onPaste} onPopupScroll={undefined} onSearch={shouldShowSearch ? handleOnSearch : undefined} diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/sorters.test.ts b/superset-frontend/packages/superset-ui-core/src/components/Table/sorters.test.ts index d4f506ac3ed..c396a43ef67 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/sorters.test.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/sorters.test.ts @@ -45,20 +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 + // @ts-expect-error + expect(alphabeticalSort('name', rows[0], rows[1])).toBeLessThan(0); + // @ts-expect-error + expect(alphabeticalSort('name', rows[1], rows[0])).toBeGreaterThan(0); + // @ts-expect-error expect(alphabeticalSort('category', rows[1], rows[0])).toBe(0); }); test('numericalSort sorts correctly', () => { - // @ts-ignore + // @ts-expect-error expect(numericalSort('cost', rows[1], rows[2])).toBe(0); - // @ts-ignore + // @ts-expect-error expect(numericalSort('cost', rows[1], rows[0])).toBeLessThan(0); - // @ts-ignore + // @ts-expect-error expect(numericalSort('cost', rows[4], rows[1])).toBeGreaterThan(0); }); @@ -68,10 +68,10 @@ test('numericalSort sorts correctly', () => { * In the case the sorter cannot perform the comparison it should return undefined and the next sort step will proceed without error */ test('alphabeticalSort bad inputs no errors', () => { - // @ts-ignore + // @ts-expect-error expect(alphabeticalSort('name', null, null)).toBe(undefined); // incorrect non-object values - // @ts-ignore + // @ts-expect-error expect(alphabeticalSort('name', 3, [])).toBe(undefined); // incorrect object values without specified key expect(alphabeticalSort('name', {}, {})).toBe(undefined); @@ -79,7 +79,7 @@ test('alphabeticalSort bad inputs no errors', () => { expect( alphabeticalSort( 'name', - // @ts-ignore + // @ts-expect-error { name: { title: 'the name attribute should not be an object' } }, { name: 'Doug' }, ), @@ -87,22 +87,22 @@ test('alphabeticalSort bad inputs no errors', () => { }); test('numericalSort bad inputs no errors', () => { - // @ts-ignore - expect(numericalSort('name', undefined, undefined)).toBe(NaN); - // @ts-ignore - expect(numericalSort('name', null, null)).toBe(NaN); + // @ts-expect-error + expect(numericalSort('name', undefined, undefined)).toBeNaN(); + // @ts-expect-error + expect(numericalSort('name', null, null)).toBeNaN(); // incorrect non-object values - // @ts-ignore - expect(numericalSort('name', 3, [])).toBe(NaN); + // @ts-expect-error + expect(numericalSort('name', 3, [])).toBeNaN(); // incorrect object values without specified key - expect(numericalSort('name', {}, {})).toBe(NaN); + expect(numericalSort('name', {}, {})).toBeNaN(); // Object as value for name when it should be a string expect( numericalSort( 'name', - // @ts-ignore + // @ts-expect-error { name: { title: 'the name attribute should not be an object' } }, { name: 'Doug' }, ), - ).toBe(NaN); + ).toBeNaN(); }); diff --git a/superset-frontend/packages/superset-ui-core/src/components/Table/utils/utils.test.ts b/superset-frontend/packages/superset-ui-core/src/components/Table/utils/utils.test.ts index fc60729dc98..1b0bc3505ef 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/Table/utils/utils.test.ts +++ b/superset-frontend/packages/superset-ui-core/src/components/Table/utils/utils.test.ts @@ -46,12 +46,12 @@ test('withinRange unsupported negative numbers', async () => { test('withinRange invalid inputs', async () => { // Invalid inputs should return falsy and not throw an error // We need ts-ignore here to be able to pass invalid values and pass linting - // @ts-ignore + // @ts-expect-error expect(withinRange(null, 60, undefined)).toBeFalsy(); - // @ts-ignore + // @ts-expect-error expect(withinRange([], 'hello', {})).toBeFalsy(); - // @ts-ignore + // @ts-expect-error expect(withinRange([], undefined, {})).toBeFalsy(); - // @ts-ignore + // @ts-expect-error expect(withinRange([], 'hello', {})).toBeFalsy(); }); diff --git a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx index 5012e1426f2..88278227a19 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TableCollection/TableCollection.test.tsx @@ -60,7 +60,7 @@ beforeEach(() => { parent: { child: 'Nested Value 3' }, }, ]; - // @ts-ignore + // @ts-expect-error const tableHookResult = renderHook(() => useTable({ columns, data })); tableHook = tableHookResult.result.current; defaultProps = { diff --git a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx index d7c86a492e9..dfe0a7e0057 100644 --- a/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx +++ b/superset-frontend/packages/superset-ui-core/src/components/TelemetryPixel/index.tsx @@ -47,7 +47,6 @@ export const TelemetryPixel = ({ const pixelPath = `https://apachesuperset.gateway.scarf.sh/pixel/${PIXEL_ID}/${version}/${sha}/${build}`; return process.env.SCARF_ANALYTICS === 'false' ? null : ( { test('falls back to setTimeout when queueMicrotask is not available', async () => { const originalQueueMicrotask = global.queueMicrotask; - // @ts-ignore - temporarily remove queueMicrotask for testing + // @ts-expect-error - temporarily remove queueMicrotask for testing delete global.queueMicrotask; const setTimeoutSpy = jest.spyOn(global, 'setTimeout'); diff --git a/superset-frontend/packages/superset-ui-core/src/math-expression/index.ts b/superset-frontend/packages/superset-ui-core/src/math-expression/index.ts index bf5ac817cd7..68e0a623f57 100644 --- a/superset-frontend/packages/superset-ui-core/src/math-expression/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/math-expression/index.ts @@ -109,7 +109,7 @@ export function evalExpression(expression: string, value: number): number { parsedExpression = subExpressions[1] ?? subExpressions[0]; // we can ignore the type requirement on `TOKENS`, as value is always `number` // and doesn't need to consider `number | undefined`. - // @ts-ignore + // @ts-expect-error return Number(mexp.eval(parsedExpression, TOKENS, { x: value })); } diff --git a/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts b/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts index 5a247d751ed..66f82af0499 100644 --- a/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts +++ b/superset-frontend/packages/superset-ui-core/src/models/ExtensibleFunction.ts @@ -22,7 +22,7 @@ */ export default class ExtensibleFunction extends Function { - // @ts-ignore + // @ts-expect-error constructor(fn: Function) { // eslint-disable-next-line @typescript-eslint/no-unsafe-return, no-constructor-return return Object.setPrototypeOf(fn, new.target.prototype); diff --git a/superset-frontend/packages/superset-ui-core/src/query/normalizeOrderBy.ts b/superset-frontend/packages/superset-ui-core/src/query/normalizeOrderBy.ts index 840cf4c1a18..67945d6b013 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/normalizeOrderBy.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/normalizeOrderBy.ts @@ -63,7 +63,6 @@ export default function normalizeOrderBy( ) { return { ...cloneQueryObject, - // @ts-ignore orderby: [[queryObject.legacy_order_by, isAsc]], }; } diff --git a/superset-frontend/packages/superset-ui-core/src/query/processExtraFormData.ts b/superset-frontend/packages/superset-ui-core/src/query/processExtraFormData.ts index 29eabe75e36..cea1a0d412a 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/processExtraFormData.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/processExtraFormData.ts @@ -38,7 +38,7 @@ export function overrideExtraFormData( ); EXTRA_FORM_DATA_OVERRIDE_EXTRA_KEYS.forEach(key => { if (key in overrideFormData) { - // @ts-ignore + // @ts-expect-error overriddenExtras[key] = overrideFormData[key]; } }); diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx index 201a5fccdfd..ba07a9eb829 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/SuperChartCore.test.tsx @@ -86,7 +86,7 @@ describe('SuperChartCore', () => { }); it('does not render if chartType is not set', async () => { - // @ts-ignore chartType is required + // @ts-expect-error chartType is required const { container } = render(); await waitFor(() => { diff --git a/superset-frontend/packages/superset-ui-core/test/chart/components/reactify.test.tsx b/superset-frontend/packages/superset-ui-core/test/chart/components/reactify.test.tsx index c2b7c43450a..44c62e90e9f 100644 --- a/superset-frontend/packages/superset-ui-core/test/chart/components/reactify.test.tsx +++ b/superset-frontend/packages/superset-ui-core/test/chart/components/reactify.test.tsx @@ -129,7 +129,7 @@ describe('reactify(renderFn)', () => { it('does not try to render if not mounted', () => { const anotherRenderFn = jest.fn(); const AnotherChart = reactify(anotherRenderFn); // enables valid new AnotherChart() call - // @ts-ignore + // @ts-expect-error new AnotherChart({ id: 'test' }).execute(); expect(anotherRenderFn).not.toHaveBeenCalled(); }); diff --git a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts index a5e0fe6cc66..b26ab3170c7 100644 --- a/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/connection/SupersetClientClass.test.ts @@ -529,15 +529,13 @@ describe('SupersetClientClass', () => { beforeEach(() => { originalLocation = window.location; - // @ts-ignore + // @ts-expect-error delete window.location; - // @ts-ignore window.location = { pathname: mockRequestPath, - // @ts-ignore search: mockRequestSearch, href: mockHref, - }; + } as unknown as Location; authSpy = jest .spyOn(SupersetClientClass.prototype, 'ensureAuth') .mockImplementation(); @@ -568,13 +566,11 @@ describe('SupersetClientClass', () => { it('should not redirect again if already on login page', async () => { const client = new SupersetClientClass({}); - // @ts-ignore window.location = { href: '/login?next=something', pathname: '/login', - // @ts-ignore search: '?next=something', - }; + } as unknown as Location; let error; try { diff --git a/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts index 979c4c1d612..251ecdded09 100644 --- a/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/currency-format/CurrencyFormatter.test.ts @@ -67,18 +67,18 @@ test('CurrencyFormatter:hasValidCurrency', () => { expect(currencyFormatter.hasValidCurrency()).toBe(true); const currencyFormatterWithoutPosition = new CurrencyFormatter({ - // @ts-ignore + // @ts-expect-error currency: { symbol: 'USD' }, }); expect(currencyFormatterWithoutPosition.hasValidCurrency()).toBe(true); const currencyFormatterWithoutSymbol = new CurrencyFormatter({ - // @ts-ignore + // @ts-expect-error currency: { symbolPosition: 'prefix' }, }); expect(currencyFormatterWithoutSymbol.hasValidCurrency()).toBe(false); - // @ts-ignore + // @ts-expect-error const currencyFormatterWithoutCurrency = new CurrencyFormatter({}); expect(currencyFormatterWithoutCurrency.hasValidCurrency()).toBe(false); }); @@ -129,12 +129,12 @@ test('CurrencyFormatter:format', () => { expect(currencyFormatterWithSuffix(VALUE)).toEqual('56.1M $'); const currencyFormatterWithoutPosition = new CurrencyFormatter({ - // @ts-ignore + // @ts-expect-error currency: { symbol: 'USD' }, }); expect(currencyFormatterWithoutPosition(VALUE)).toEqual('56.1M $'); - // @ts-ignore + // @ts-expect-error const currencyFormatterWithoutCurrency = new CurrencyFormatter({}); expect(currencyFormatterWithoutCurrency(VALUE)).toEqual('56.1M'); diff --git a/superset-frontend/packages/superset-ui-core/test/dimension/getBBoxDummyFill.ts b/superset-frontend/packages/superset-ui-core/test/dimension/getBBoxDummyFill.ts index 8f457933568..1f10944e1b6 100644 --- a/superset-frontend/packages/superset-ui-core/test/dimension/getBBoxDummyFill.ts +++ b/superset-frontend/packages/superset-ui-core/test/dimension/getBBoxDummyFill.ts @@ -28,10 +28,10 @@ const textToWidth = { export const SAMPLE_TEXT = Object.keys(textToWidth); export function addDummyFill() { - // @ts-ignore - fix jsdom + // @ts-expect-error - fix jsdom originalFn = SVGElement.prototype.getBBox; - // @ts-ignore - fix jsdom + // @ts-expect-error - fix jsdom SVGElement.prototype.getBBox = function getBBox() { let width = textToWidth[this.textContent as keyof typeof textToWidth] || 200; @@ -78,6 +78,6 @@ export function addDummyFill() { } export function removeDummyFill() { - // @ts-ignore - fix jsdom + // @ts-expect-error - fix jsdom SVGElement.prototype.getBBox = originalFn; } diff --git a/superset-frontend/packages/superset-ui-core/test/dimension/mergeMargin.test.ts b/superset-frontend/packages/superset-ui-core/test/dimension/mergeMargin.test.ts index 6567605efe7..86e9194efe0 100644 --- a/superset-frontend/packages/superset-ui-core/test/dimension/mergeMargin.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/dimension/mergeMargin.test.ts @@ -187,7 +187,7 @@ describe('mergeMargin(margin1, margin2, mode?)', () => { mergeMargin( { top: 10, - // @ts-ignore to let us pass `null` for testing + // @ts-expect-error to let us pass `null` for testing left: null, bottom: 20, right: NaN, diff --git a/superset-frontend/packages/superset-ui-core/test/models/ExtensibleFunction.test.ts b/superset-frontend/packages/superset-ui-core/test/models/ExtensibleFunction.test.ts index 11db53fd9af..2a52d0bfb5b 100644 --- a/superset-frontend/packages/superset-ui-core/test/models/ExtensibleFunction.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/models/ExtensibleFunction.test.ts @@ -54,9 +54,9 @@ describe('ExtensibleFunction', () => { x: unknown; constructor(x: unknown) { - // @ts-ignore + // @ts-expect-error super(function customName() { - // @ts-ignore + // @ts-expect-error return customName.x; }); // named function this.x = x; diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatter.test.ts index 7cb11444448..e8bee2dcffd 100644 --- a/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatter.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatter.test.ts @@ -24,7 +24,7 @@ describe('NumberFormatter', () => { it('requires config.id', () => { expect( () => - // @ts-ignore + // @ts-expect-error new NumberFormatter({ formatFunc: () => '', }), @@ -33,7 +33,7 @@ describe('NumberFormatter', () => { it('requires config.formatFunc', () => { expect( () => - // @ts-ignore + // @ts-expect-error new NumberFormatter({ id: 'my_format', }), diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatterRegistry.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatterRegistry.test.ts index 2ff49f155c0..d2b1521f903 100644 --- a/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatterRegistry.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/number-format/NumberFormatterRegistry.test.ts @@ -49,7 +49,7 @@ describe('NumberFormatterRegistry', () => { }); it('falls back to default format if format is null', () => { registry.setDefaultKey('.1f'); - // @ts-ignore + // @ts-expect-error const formatter = registry.get(null); expect(formatter.format(100)).toEqual('100.0'); }); diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/factories/createD3NumberFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/factories/createD3NumberFormatter.test.ts index 951ab039c5a..e438167a30d 100644 --- a/superset-frontend/packages/superset-ui-core/test/number-format/factories/createD3NumberFormatter.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/number-format/factories/createD3NumberFormatter.test.ts @@ -21,7 +21,7 @@ import { createD3NumberFormatter } from '@superset-ui/core'; describe('createD3NumberFormatter(config)', () => { it('requires config.formatString', () => { - // @ts-ignore -- intentionally pass invalid input + // @ts-expect-error -- intentionally pass invalid input expect(() => createD3NumberFormatter({})).toThrow(); }); describe('config.formatString', () => { diff --git a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts index 5e39c6f8cfe..027537b025c 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/api/v1/makeApi.test.ts @@ -151,7 +151,7 @@ describe('makeApi()', () => { makeApi({ method: 'POST', endpoint: '/test-formdata', - // @ts-ignore + // @ts-expect-error requestType: 'text', }); }).toThrow('Invalid request payload type'); diff --git a/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts b/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts index 1abbf5c3065..8f24e508dcc 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/getClientErrorObject.test.ts @@ -120,7 +120,7 @@ test('Handles Response that contains raw html be parsed as text', async () => { test('Handles TypeError Response', async () => { const error = new TypeError('Failed to fetch'); - // @ts-ignore + // @ts-expect-error const errorObj = await getClientErrorObject(error); expect(errorObj).toMatchObject({ error: 'Network error' }); }); @@ -184,15 +184,15 @@ test('Handles error with status text and message', async () => { const statusText = 'status'; const message = 'message'; - // @ts-ignore + // @ts-expect-error expect(await getClientErrorObject({ statusText, message })).toMatchObject({ error: statusText, }); - // @ts-ignore + // @ts-expect-error expect(await getClientErrorObject({ message })).toMatchObject({ error: message, }); - // @ts-ignore + // @ts-expect-error expect(await getClientErrorObject({})).toMatchObject({ error: 'An error occurred', }); diff --git a/superset-frontend/packages/superset-ui-core/test/query/normalizeOrderBy.test.ts b/superset-frontend/packages/superset-ui-core/test/query/normalizeOrderBy.test.ts index 57f234ebc85..0bc8da67e4f 100644 --- a/superset-frontend/packages/superset-ui-core/test/query/normalizeOrderBy.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/query/normalizeOrderBy.test.ts @@ -183,7 +183,7 @@ describe('normalizeOrderBy', () => { datasource: '5__table', viz_type: VizType.Table, time_range: '1 year ago : 2013', - // @ts-ignore + // @ts-expect-error orderby: [['count(*)', 'true']], }; expect(normalizeOrderBy(query)).not.toHaveProperty('orderby'); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatter.test.ts index e6353e856d0..6334c93d4e4 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatter.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatter.test.ts @@ -24,7 +24,7 @@ describe('TimeFormatter', () => { it('requires config.id', () => { expect( () => - // @ts-ignore -- intentionally pass invalid input + // @ts-expect-error -- intentionally pass invalid input new TimeFormatter({ formatFunc: () => 'test', }), @@ -33,7 +33,7 @@ describe('TimeFormatter', () => { it('requires config.formatFunc', () => { expect( () => - // @ts-ignore -- intentionally pass invalid input + // @ts-expect-error -- intentionally pass invalid input new TimeFormatter({ id: 'my_format', }), diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts index 57b659d7400..8ae78b49730 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/TimeFormatterRegistry.test.ts @@ -45,7 +45,7 @@ describe('TimeFormatterRegistry', () => { }); it('falls back to default format if format is null', () => { registry.setDefaultKey(TimeFormats.INTERNATIONAL_DATE); - // @ts-ignore + // @ts-expect-error const formatter = registry.get(null); expect(formatter.format(PREVIEW_TIME)).toEqual('14/02/2017'); }); diff --git a/superset-frontend/packages/superset-ui-core/test/time-format/factories/createD3TimeFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/time-format/factories/createD3TimeFormatter.test.ts index a47e0e2f5b4..22a45c9e797 100644 --- a/superset-frontend/packages/superset-ui-core/test/time-format/factories/createD3TimeFormatter.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/time-format/factories/createD3TimeFormatter.test.ts @@ -71,9 +71,9 @@ const thLocale: TimeLocaleDefinition = { describe('createD3TimeFormatter(config)', () => { it('requires config.formatString', () => { - // @ts-ignore + // @ts-expect-error expect(() => createD3TimeFormatter()).toThrow(); - // @ts-ignore + // @ts-expect-error expect(() => createD3TimeFormatter({})).toThrow(); }); describe('config.useLocalTime', () => { diff --git a/superset-frontend/packages/superset-ui-core/test/utils/getSelectedText.test.ts b/superset-frontend/packages/superset-ui-core/test/utils/getSelectedText.test.ts index 75682e9e747..7737cd36fb3 100644 --- a/superset-frontend/packages/superset-ui-core/test/utils/getSelectedText.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/utils/getSelectedText.test.ts @@ -27,7 +27,7 @@ test('Returns null if Selection object is null', () => { test('Returns selection text if Selection object is not null', () => { jest .spyOn(window, 'getSelection') - // @ts-ignore + // @ts-expect-error .mockImplementationOnce(() => ({ toString: () => 'test string' })); expect(getSelectedText()).toEqual('test string'); jest.restoreAllMocks(); diff --git a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/testData.ts b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/testData.ts index b5f12f34855..0d4daf339f9 100644 --- a/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/testData.ts +++ b/superset-frontend/packages/superset-ui-demo/storybook/stories/plugins/plugin-chart-table/testData.ts @@ -22,8 +22,8 @@ import { TableChartFormData, TableChartProps, } from '@superset-ui/plugin-chart-table'; -// @ts-ignore // eslint-disable-next-line import/extensions +// @ts-ignore -- TS6307: this file is outside the tsconfig project scope, @ts-expect-error does not suppress project-level errors import birthNamesJson from './birthNames.json'; export const birthNames = birthNamesJson as unknown as TableChartProps; diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.js b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.ts similarity index 62% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.js rename to superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.ts index bff582d62e0..f5fd3c29f88 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.js +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/Calendar.ts @@ -16,46 +16,48 @@ * specific language governing permissions and limitations * under the License. */ -import PropTypes from 'prop-types'; import { extent as d3Extent, range as d3Range } from 'd3-array'; import { select as d3Select } from 'd3-selection'; import { getSequentialSchemeRegistry } from '@superset-ui/core'; -import { t } from '@apache-superset/core/ui'; -import CalHeatMap from './vendor/cal-heatmap'; +import { SupersetTheme, t } from '@apache-superset/core/ui'; +import CalHeatMapImport from './vendor/cal-heatmap'; import { convertUTCTimestampToLocal } from './utils'; -const propTypes = { - data: PropTypes.shape({ - // Object hashed by metric name, - // then hashed by timestamp (in seconds, not milliseconds) as float - // the innermost value is count - // e.g. { count_distinct_something: { 1535034236.0: 3 } } - data: PropTypes.object, - domain: PropTypes.string, - range: PropTypes.number, - // timestamp in milliseconds - start: PropTypes.number, - subdomain: PropTypes.string, - }), - height: PropTypes.number, - // eslint-disable-next-line react/sort-prop-types - cellPadding: PropTypes.number, - // eslint-disable-next-line react/sort-prop-types - cellRadius: PropTypes.number, - // eslint-disable-next-line react/sort-prop-types - cellSize: PropTypes.number, - linearColorScheme: PropTypes.string, - showLegend: PropTypes.bool, - showMetricName: PropTypes.bool, - showValues: PropTypes.bool, - steps: PropTypes.number, - timeFormatter: PropTypes.func, - valueFormatter: PropTypes.func, - verboseMap: PropTypes.object, - theme: PropTypes.object, -}; +// The vendor file is @ts-nocheck, so its export lacks type info. +// Define a minimal constructor interface for use in this file. +interface CalHeatMapInstance { + init(config: Record): void; +} +const CalHeatMap = CalHeatMapImport as unknown as new () => CalHeatMapInstance; -function Calendar(element, props) { +interface CalendarData { + data: Record>; + domain: string; + range: number; + start: number; + subdomain: string; +} + +interface CalendarProps { + data: CalendarData; + height: number; + cellPadding?: number; + cellRadius?: number; + cellSize?: number; + domainGranularity: string; + linearColorScheme: string; + showLegend: boolean; + showMetricName: boolean; + showValues: boolean; + steps: number; + subdomainGranularity: string; + timeFormatter: (ts: number | string) => string; + valueFormatter: (value: number) => string; + verboseMap: Record; + theme: SupersetTheme; +} + +function Calendar(element: HTMLElement, props: CalendarProps) { const { data, height, @@ -82,7 +84,7 @@ function Calendar(element, props) { const div = container.append('div'); const subDomainTextFormat = showValues - ? (date, value) => valueFormatter(value) + ? (_date: Date, value: number) => valueFormatter(value) : null; const metricsData = data.data; @@ -95,11 +97,21 @@ function Calendar(element, props) { calContainer.text(`${METRIC_TEXT}: ${verboseMap[metric] || metric}`); } const timestamps = metricsData[metric]; - const extents = d3Extent(Object.keys(timestamps), key => timestamps[key]); - const step = (extents[1] - extents[0]) / (steps - 1); - const colorScale = getSequentialSchemeRegistry() - .get(linearColorScheme) - .createLinearScale(extents); + const rawExtents = d3Extent( + Object.keys(timestamps), + key => timestamps[key], + ); + // Guard against undefined extents (empty data) + const extents: [number, number] = + rawExtents[0] != null && rawExtents[1] != null + ? [rawExtents[0], rawExtents[1]] + : [0, 1]; + // Guard against division by zero when steps <= 1 + const step = steps > 1 ? (extents[1] - extents[0]) / (steps - 1) : 0; + const colorScheme = getSequentialSchemeRegistry().get(linearColorScheme); + const colorScale = colorScheme + ? colorScheme.createLinearScale(extents) + : (v: number) => '#ccc'; // fallback if scheme not found const legend = d3Range(steps).map(i => extents[0] + step * i); const legendColors = legend.map(x => colorScale(x)); @@ -138,6 +150,5 @@ function Calendar(element, props) { } Calendar.displayName = 'Calendar'; -Calendar.propTypes = propTypes; export default Calendar; diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.jsx b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.tsx similarity index 91% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.jsx rename to superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.tsx index f028b94b116..a14bc9b4a22 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/ReactCalendar.tsx @@ -16,15 +16,27 @@ * specific language governing permissions and limitations * under the License. */ -import PropTypes from 'prop-types'; import { reactify } from '@superset-ui/core'; import { styled, css, useTheme } from '@apache-superset/core/ui'; import { Global } from '@emotion/react'; import Component from './Calendar'; -const ReactComponent = reactify(Component); +// Type-erase the render function to allow flexible prop spreading in the wrapper. +// The Calendar render function has typed props, but the wrapper passes props via spread +// which TypeScript cannot verify at compile time. Props are validated at runtime. +const ReactComponent = reactify( + Component as unknown as ( + container: HTMLDivElement, + props: Record, + ) => void, +); -const Calendar = ({ className, ...otherProps }) => { +interface CalendarWrapperProps { + className?: string; + [key: string]: unknown; +} + +const Calendar = ({ className, ...otherProps }: CalendarWrapperProps) => { const theme = useTheme(); return (
@@ -88,15 +100,6 @@ const Calendar = ({ className, ...otherProps }) => { ); }; -Calendar.defaultProps = { - otherProps: {}, -}; - -Calendar.propTypes = { - className: PropTypes.string.isRequired, - otherProps: PropTypes.objectOf(PropTypes.any), -}; - export default styled(Calendar)` ${({ theme }) => ` .superset-legacy-chart-calendar { diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-calendar/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts similarity index 87% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts index 379a206f513..b20773ee70d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/transformProps.ts @@ -17,10 +17,10 @@ * under the License. */ -import { getNumberFormatter } from '@superset-ui/core'; +import { ChartProps, getNumberFormatter } from '@superset-ui/core'; import { getFormattedUTCTime } from './utils'; -export default function transformProps(chartProps) { +export default function transformProps(chartProps: ChartProps) { const { height, formData, queriesData, datasource } = chartProps; const { cellPadding, @@ -38,7 +38,8 @@ export default function transformProps(chartProps) { } = formData; const { verboseMap } = datasource; - const timeFormatter = ts => getFormattedUTCTime(ts, xAxisTimeFormat); + const timeFormatter = (ts: number | string) => + getFormattedUTCTime(ts, xAxisTimeFormat); const valueFormatter = getNumberFormatter(yAxisFormat); return { diff --git a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.js b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.ts similarity index 99% rename from superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.js rename to superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.ts index 3240dd7b741..1ccf62f996a 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/src/vendor/cal-heatmap.ts @@ -1,3 +1,4 @@ +// @ts-nocheck // [LICENSE TBD] /* Copied and altered from http://cal-heatmap.com/ , alterations around: * - tuning tooltips diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts similarity index 71% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts index 9fba63a88a5..66677a600a6 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-calendar/types/external.d.ts @@ -16,15 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { - const { height, width, formData, queriesData } = chartProps; - const { horizonColorScale, seriesHeight } = formData; - return { - colorScale: horizonColorScale, - data: queriesData[0].data, - height, - seriesHeight: parseInt(seriesHeight, 10), - width, - }; +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.ts similarity index 76% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js rename to superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.ts index 1d5ed45683f..62483c2d331 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.js +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/Chord.ts @@ -1,3 +1,4 @@ +// @ts-nocheck /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,24 +19,26 @@ */ /* eslint-disable no-param-reassign, react/sort-prop-types */ import d3 from 'd3'; -import PropTypes from 'prop-types'; import { getNumberFormatter, CategoricalColorNamespace, } from '@superset-ui/core'; -const propTypes = { - data: PropTypes.shape({ - matrix: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), - nodes: PropTypes.arrayOf(PropTypes.string), - }), - width: PropTypes.number, - height: PropTypes.number, - colorScheme: PropTypes.string, - numberFormat: PropTypes.string, -}; +interface ChordData { + matrix: number[][]; + nodes: string[]; +} -function Chord(element, props) { +interface ChordProps { + data: ChordData; + width: number; + height: number; + colorScheme: string; + numberFormat: string; + sliceId: number; +} + +function Chord(element: HTMLElement, props: ChordProps) { const { data, width, height, numberFormat, colorScheme, sliceId } = props; element.innerHTML = ''; @@ -49,7 +52,10 @@ function Chord(element, props) { const outerRadius = Math.min(width, height) / 2 - 10; const innerRadius = outerRadius - 24; - let chord; + // d3 v3 data-bound selections use any for the datum generic parameter + // because the d3 v3 typings cannot represent the actual chord layout data types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let chord: d3.Selection; const arc = d3.svg.arc().innerRadius(innerRadius).outerRadius(outerRadius); @@ -92,6 +98,7 @@ function Chord(element, props) { const groupPath = group .append('path') .attr('id', (d, i) => `group${i}`) + // @ts-expect-error -- d3 v3 arc layout is callable at runtime but not typed as Primitive .attr('d', arc) .style('fill', (d, i) => colorFn(nodes[i], sliceId)); @@ -104,9 +111,10 @@ function Chord(element, props) { .text((d, i) => nodes[i]); // Remove the labels that don't fit. :( groupText - .filter(function filter(d, i) { + .filter(function filter(this: SVGTextElement, d, i) { return ( - groupPath[0][i].getTotalLength() / 2 - 16 < this.getComputedTextLength() + (groupPath[0][i] as SVGPathElement).getTotalLength() / 2 - 16 < + this.getComputedTextLength() ); }) .remove(); @@ -122,6 +130,7 @@ function Chord(element, props) { chord.classed('fade', p => p !== d); }) .style('fill', d => colorFn(nodes[d.source.index], sliceId)) + // @ts-expect-error -- d3 v3 chord layout is callable at runtime but not typed as Primitive .attr('d', path); // Add an elaborate mouseover title for each chord. @@ -129,15 +138,14 @@ function Chord(element, props) { .append('title') .text( d => - `${nodes[d.source.index]} → ${nodes[d.target.index]}: ${f( + `${nodes[d.source.index]} \u2192 ${nodes[d.target.index]}: ${f( d.target.value, - )}\n${nodes[d.target.index]} → ${nodes[d.source.index]}: ${f( + )}\n${nodes[d.target.index]} \u2192 ${nodes[d.source.index]}: ${f( d.source.value, )}`, ); } Chord.displayName = 'Chord'; -Chord.propTypes = propTypes; export default Chord; diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.jsx b/superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.tsx similarity index 73% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.jsx rename to superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.tsx index fbefe58e3f7..040e67e6d3c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/ReactChord.tsx @@ -18,26 +18,29 @@ */ import { reactify } from '@superset-ui/core'; import { styled } from '@apache-superset/core/ui'; -import PropTypes from 'prop-types'; import Component from './Chord'; -const ReactComponent = reactify(Component); +// Type-erase the render function to allow flexible prop spreading in the wrapper. +// The Chord render function has typed props, but the wrapper passes props via spread +// which TypeScript cannot verify at compile time. Props are validated at runtime. +const ReactComponent = reactify( + Component as unknown as ( + container: HTMLDivElement, + props: Record, + ) => void, +); -const Chord = ({ className, ...otherProps }) => ( +interface ChordWrapperProps { + className?: string; + [key: string]: unknown; +} + +const Chord = ({ className, ...otherProps }: ChordWrapperProps) => (
); -Chord.defaultProps = { - otherProps: {}, -}; - -Chord.propTypes = { - className: PropTypes.string.isRequired, - otherProps: PropTypes.objectOf(PropTypes.any), -}; - export default styled(Chord)` ${({ theme }) => ` .superset-legacy-chart-chord svg #circle circle { diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-chord/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts similarity index 90% rename from superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts index 7503ff4ea1f..4b8fef75104 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/src/transformProps.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { +import { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { const { width, height, formData, queriesData } = chartProps; const { yAxisFormat, colorScheme, sliceId } = formData; diff --git a/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.ts new file mode 100644 index 00000000000..66677a600a6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-chord/types/external.d.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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.ts similarity index 69% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js rename to superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.ts index e8e14d6a44c..c4d94ff3a18 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/CountryMap.ts @@ -1,3 +1,4 @@ +// @ts-nocheck /** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file @@ -18,7 +19,6 @@ */ /* eslint-disable react/sort-prop-types */ import d3 from 'd3'; -import PropTypes from 'prop-types'; import { extent as d3Extent } from 'd3-array'; import { getNumberFormatter, @@ -27,25 +27,47 @@ import { } from '@superset-ui/core'; import countries, { countryOptions } from './countries'; -const propTypes = { - data: PropTypes.arrayOf( - PropTypes.shape({ - country_id: PropTypes.string, - metric: PropTypes.number, - }), - ), - width: PropTypes.number, - height: PropTypes.number, - country: PropTypes.string, - colorScheme: PropTypes.string, - linearColorScheme: PropTypes.string, - mapBaseUrl: PropTypes.string, - numberFormat: PropTypes.string, -}; +/** + * Escape HTML special characters to prevent XSS attacks + */ +function escapeHtml(text: string): string { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; +} -const maps = {}; +interface CountryMapDataItem { + country_id: string; + metric: number; +} -function CountryMap(element, props) { +interface GeoFeature { + properties: { + ISO: string; + ID_2?: string; + NAME_1?: string; + NAME_2?: string; + }; +} + +interface GeoData { + features: GeoFeature[]; +} + +interface CountryMapProps { + data: CountryMapDataItem[]; + width: number; + height: number; + country: string; + linearColorScheme: string; + numberFormat: string; + colorScheme: string; + sliceId: number; +} + +const maps: Record = {}; + +function CountryMap(element: HTMLElement, props: CountryMapProps) { const { data, width, @@ -59,18 +81,24 @@ function CountryMap(element, props) { const container = element; const format = getNumberFormatter(numberFormat); - const linearColorScale = getSequentialSchemeRegistry() - .get(linearColorScheme) - .createLinearScale(d3Extent(data, v => v.metric)); + const rawExtents = d3Extent(data, v => v.metric); + const extents: [number, number] = + rawExtents[0] != null && rawExtents[1] != null + ? [rawExtents[0], rawExtents[1]] + : [0, 1]; + const colorSchemeObj = getSequentialSchemeRegistry().get(linearColorScheme); + const linearColorScale = colorSchemeObj + ? colorSchemeObj.createLinearScale(extents) + : () => '#ccc'; // fallback if scheme not found const colorScale = CategoricalColorNamespace.getScale(colorScheme); - const colorMap = {}; + const colorMap: Record = {}; data.forEach(d => { colorMap[d.country_id] = colorScheme ? colorScale(d.country_id, sliceId) - : linearColorScale(d.metric); + : (linearColorScale(d.metric) ?? ''); }); - const colorFn = d => colorMap[d.properties.ISO] || 'none'; + const colorFn = (d: GeoFeature) => colorMap[d.properties.ISO] || 'none'; const path = d3.geo.path(); const div = d3.select(container); @@ -92,13 +120,13 @@ function CountryMap(element, props) { const mapLayer = g.append('g').classed('map-layer', true); const hoverPopup = div.append('div').attr('class', 'hover-popup'); - let centered; + let centered: GeoFeature | null; - const clicked = function clicked(d) { + const clicked = function clicked(d: GeoFeature) { const hasCenter = d && centered !== d; - let x; - let y; - let k; + let x: number; + let y: number; + let k: number; const halfWidth = width / 2; const halfHeight = height / 2; @@ -124,19 +152,21 @@ function CountryMap(element, props) { backgroundRect.on('click', clicked); - const getNameOfRegion = function getNameOfRegion(feature) { + const getNameOfRegion = function getNameOfRegion( + feature: GeoFeature, + ): string { if (feature && feature.properties) { if (feature.properties.ID_2) { - return feature.properties.NAME_2; + return feature.properties.NAME_2 || ''; } - return feature.properties.NAME_1; + return feature.properties.NAME_1 || ''; } return ''; }; - const mouseenter = function mouseenter(d) { + const mouseenter = function mouseenter(this: SVGPathElement, d: GeoFeature) { // Darken color - let c = colorFn(d); + let c: string = colorFn(d); if (c !== 'none') { c = d3.rgb(c).darker().toString(); } @@ -163,12 +193,12 @@ function CountryMap(element, props) { .style('left', `${position[0]}px`); }; - const mouseout = function mouseout() { + const mouseout = function mouseout(this: SVGPathElement) { d3.select(this).style('fill', colorFn); hoverPopup.style('display', 'none'); }; - function drawMap(mapData) { + function drawMap(mapData: GeoData) { const { features } = mapData; const center = d3.geo.centroid(mapData); const scale = 100; @@ -213,13 +243,21 @@ function CountryMap(element, props) { if (map) { drawMap(map); } else { - const url = countries[country]; - d3.json(url, (error, mapData) => { + const url = (countries as Record)[country]; + if (!url) { + const countryName = + countryOptions.find(x => x[0] === country)?.[1] || country; + d3.select(element).html( + `
No map data available for ${escapeHtml(countryName)}
`, + ); + return; + } + d3.json(url, (error: unknown, mapData: GeoData) => { if (error) { const countryName = countryOptions.find(x => x[0] === country)?.[1] || country; d3.select(element).html( - `
Could not load map data for ${countryName}
`, + `
Could not load map data for ${escapeHtml(countryName)}
`, ); } else { maps[country] = mapData; @@ -230,6 +268,5 @@ function CountryMap(element, props) { } CountryMap.displayName = 'CountryMap'; -CountryMap.propTypes = propTypes; export default CountryMap; diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.tsx similarity index 78% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx rename to superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.tsx index aa937377982..d1afe8289d9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/ReactCountryMap.tsx @@ -20,9 +20,25 @@ import { reactify } from '@superset-ui/core'; import { styled } from '@apache-superset/core/ui'; import Component from './CountryMap'; -const ReactComponent = reactify(Component); +// Type-erase the render function to allow flexible prop spreading in the wrapper. +// The CountryMap render function has typed props, but the wrapper passes props via spread +// which TypeScript cannot verify at compile time. Props are validated at runtime. +const ReactComponent = reactify( + Component as unknown as ( + container: HTMLDivElement, + props: Record, + ) => void, +); -const CountryMap = ({ className = '', ...otherProps }) => ( +interface CountryMapWrapperProps { + className?: string; + [key: string]: unknown; +} + +const CountryMap = ({ + className = '', + ...otherProps +}: CountryMapWrapperProps) => (
diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-country-map/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts similarity index 91% rename from superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts index 8789c3d2f34..946a5c9fd44 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/src/transformProps.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { +import { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { const { width, height, formData, queriesData } = chartProps; const { linearColorScheme, diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx index b73c5831c71..97628a5fb68 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/test/CountryMap.test.tsx @@ -22,7 +22,10 @@ import { render, fireEvent } from '@testing-library/react'; import d3 from 'd3'; import ReactCountryMap from '../src/ReactCountryMap'; -jest.spyOn(d3, 'json'); +// d3 v3 APIs have loose types; cast to allow jest mock operations +const d3Any = d3 as any; + +jest.spyOn(d3Any, 'json'); type Projection = ((...args: unknown[]) => void) & { scale: () => Projection; @@ -44,10 +47,10 @@ mockPath.bounds = jest.fn(() => [ ]); mockPath.centroid = jest.fn(() => [50, 50]); -jest.spyOn(d3.geo, 'path').mockImplementation(() => mockPath); +jest.spyOn(d3Any.geo, 'path').mockImplementation(() => mockPath); // Mock d3.geo.mercator -jest.spyOn(d3.geo, 'mercator').mockImplementation(() => { +jest.spyOn(d3Any.geo, 'mercator').mockImplementation(() => { const proj = (() => {}) as Projection; proj.scale = () => proj; proj.center = () => proj; @@ -56,7 +59,7 @@ jest.spyOn(d3.geo, 'mercator').mockImplementation(() => { }); // Mock d3.mouse -jest.spyOn(d3, 'mouse').mockReturnValue([100, 50]); +jest.spyOn(d3Any, 'mouse').mockReturnValue([100, 50]); const mockMapData = { type: 'FeatureCollection', @@ -77,7 +80,7 @@ describe('CountryMap (legacy d3)', () => { }); it('renders a map after d3.json loads data', async () => { - d3.json.mockImplementation((_url: string, cb: D3JsonCallback) => + d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) => cb(null, mockMapData), ); @@ -93,14 +96,14 @@ describe('CountryMap (legacy d3)', () => { />, ); - expect(d3.json).toHaveBeenCalledTimes(1); + expect(d3Any.json).toHaveBeenCalledTimes(1); const region = document.querySelector('path.region'); expect(region).not.toBeNull(); }); it('shows tooltip on mouseenter/mousemove/mouseout', async () => { - d3.json.mockImplementation((_url: string, cb: D3JsonCallback) => + d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) => cb(null, mockMapData), ); @@ -129,7 +132,7 @@ describe('CountryMap (legacy d3)', () => { }); it('shows tooltip on mouseenter/mousemove/mouseout', async () => { - d3.json.mockImplementation((_url: string, cb: D3JsonCallback) => + d3Any.json.mockImplementation((_url: string, cb: D3JsonCallback) => cb(null, mockMapData), ); diff --git a/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.ts new file mode 100644 index 00000000000..66677a600a6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-country-map/types/external.d.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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.jsx b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.tsx similarity index 75% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.jsx rename to superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.tsx index d3ebde9bd5e..0ec77b51b89 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/HorizonChart.tsx @@ -18,35 +18,34 @@ */ /* eslint-disable react/jsx-sort-default-props, react/sort-prop-types */ import { PureComponent } from 'react'; -import PropTypes from 'prop-types'; import { extent as d3Extent } from 'd3-array'; import { ensureIsArray } from '@superset-ui/core'; import { styled } from '@apache-superset/core/ui'; import HorizonRow, { DEFAULT_COLORS } from './HorizonRow'; -const propTypes = { - className: PropTypes.string, - width: PropTypes.number, - height: PropTypes.number, - seriesHeight: PropTypes.number, - data: PropTypes.arrayOf( - PropTypes.shape({ - key: PropTypes.arrayOf(PropTypes.string), - values: PropTypes.arrayOf( - PropTypes.shape({ - y: PropTypes.number, - }), - ), - }), - ).isRequired, - // number of bands in each direction (positive / negative) - bands: PropTypes.number, - colors: PropTypes.arrayOf(PropTypes.string), - colorScale: PropTypes.string, - mode: PropTypes.string, - offsetX: PropTypes.number, -}; -const defaultProps = { +interface DataValue { + y: number; +} + +interface DataSeries { + key: string[]; + values: DataValue[]; +} + +interface HorizonChartProps { + className?: string; + width?: number; + height?: number; + seriesHeight?: number; + data: DataSeries[]; + bands?: number; + colors?: string[]; + colorScale?: string; + mode?: string; + offsetX?: number; +} + +const defaultProps: Partial = { className: '', width: 800, height: 600, @@ -81,7 +80,9 @@ const StyledDiv = styled.div` `} `; -class HorizonChart extends PureComponent { +class HorizonChart extends PureComponent { + static defaultProps = defaultProps; + render() { const { className, @@ -96,13 +97,17 @@ class HorizonChart extends PureComponent { offsetX, } = this.props; - let yDomain; + let yDomain: [number, number] | undefined; if (colorScale === 'overall') { - const allValues = data.reduce( + const allValues = data.reduce( (acc, current) => acc.concat(current.values), [], ); - yDomain = d3Extent(allValues, d => d.y); + const rawExtent = d3Extent(allValues, d => d.y); + // Only set yDomain if we have valid min and max values + if (rawExtent[0] != null && rawExtent[1] != null) { + yDomain = [rawExtent[0], rawExtent[1]]; + } } return ( @@ -113,7 +118,7 @@ class HorizonChart extends PureComponent { > {data.map(row => ( = { className: '', width: 800, height: 20, @@ -66,7 +65,11 @@ const defaultProps = { yDomain: undefined, }; -class HorizonRow extends PureComponent { +class HorizonRow extends PureComponent { + static defaultProps = defaultProps; + + private canvas: HTMLCanvasElement | null = null; + componentDidMount() { this.drawChart(); } @@ -84,12 +87,12 @@ class HorizonRow extends PureComponent { const { data: rawData, yDomain, - width, - height, - bands, - colors, + width = 800, + height = 20, + bands = DEFAULT_COLORS.length >> 1, + colors = DEFAULT_COLORS, colorScale, - offsetX, + offsetX = 0, mode, } = this.props; @@ -99,6 +102,7 @@ class HorizonRow extends PureComponent { : rawData; const context = this.canvas.getContext('2d'); + if (!context) return; context.imageSmoothingEnabled = false; context.clearRect(0, 0, width, height); // Reset transform @@ -118,7 +122,8 @@ class HorizonRow extends PureComponent { } // Create y-scale - const [min, max] = yDomain || d3Extent(data, d => d.y); + const [min, max] = + yDomain || (d3Extent(data, d => d.y) as [number, number]); const y = scaleLinear() .domain([0, Math.max(-min, max)]) .range([0, height]); @@ -127,8 +132,8 @@ class HorizonRow extends PureComponent { // http://www.html5rocks.com/en/tutorials/canvas/performance/ let hasNegative = false; // draw positive bands - let value; - let bExtents; + let value: number; + let bExtents: number; for (let b = 0; b < bands; b += 1) { context.fillStyle = colors[bands + b]; @@ -146,9 +151,9 @@ class HorizonRow extends PureComponent { if (value !== undefined) { context.fillRect( offsetX + i * step, - y(value), + y(value)!, step + 1, - y(0) - y(value), + y(0)! - y(value)!, ); } } @@ -177,9 +182,9 @@ class HorizonRow extends PureComponent { } context.fillRect( offsetX + ii * step, - y(-value), + y(-value)!, step + 1, - y(0) - y(-value), + y(0)! - y(-value)!, ); } } @@ -205,7 +210,4 @@ class HorizonRow extends PureComponent { } } -HorizonRow.propTypes = propTypes; -HorizonRow.defaultProps = defaultProps; - export default HorizonRow; diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-horizon/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.ts new file mode 100644 index 00000000000..cab69e8c568 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/src/transformProps.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 { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { + const { height, width, formData, queriesData } = chartProps; + const { + horizon_color_scale: horizonColorScale, + series_height: seriesHeight, + } = formData; + + // Only include colorScale if defined, otherwise let defaultProps apply + return { + ...(horizonColorScale !== undefined && { + colorScale: horizonColorScale as string, + }), + data: queriesData[0].data, + height, + seriesHeight: parseInt(String(seriesHeight ?? 20), 10), + width, + }; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.ts new file mode 100644 index 00000000000..66677a600a6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-horizon/types/external.d.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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.jsx b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx similarity index 61% rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.jsx rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx index 943cf7174b9..361a27610c9 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/MapBox.tsx @@ -19,7 +19,6 @@ /* eslint-disable react/jsx-sort-default-props, react/sort-prop-types */ /* eslint-disable react/forbid-prop-types, react/require-default-props */ import { Component } from 'react'; -import PropTypes from 'prop-types'; import MapGL from 'react-map-gl'; import { WebMercatorViewport } from '@math.gl/web-mercator'; import ScatterPlotGlowOverlay from './ScatterPlotGlowOverlay'; @@ -29,24 +28,46 @@ const NOOP = () => {}; export const DEFAULT_MAX_ZOOM = 16; export const DEFAULT_POINT_RADIUS = 60; -const propTypes = { - width: PropTypes.number, - height: PropTypes.number, - aggregatorName: PropTypes.string, - clusterer: PropTypes.object, - globalOpacity: PropTypes.number, - hasCustomMetric: PropTypes.bool, - mapStyle: PropTypes.string, - mapboxApiKey: PropTypes.string.isRequired, - onViewportChange: PropTypes.func, - pointRadius: PropTypes.number, - pointRadiusUnit: PropTypes.string, - renderWhileDragging: PropTypes.bool, - rgb: PropTypes.array, - bounds: PropTypes.array, -}; +interface Viewport { + longitude: number; + latitude: number; + zoom: number; + isDragging?: boolean; +} -const defaultProps = { +interface Clusterer { + getClusters(bbox: number[], zoom: number): GeoJSONLocation[]; +} + +interface GeoJSONLocation { + geometry: { + coordinates: [number, number]; + }; + properties: Record; +} + +interface MapBoxProps { + width?: number; + height?: number; + aggregatorName?: string; + clusterer: Clusterer; // Required - used for getClusters() + globalOpacity?: number; + hasCustomMetric?: boolean; + mapStyle?: string; + mapboxApiKey: string; + onViewportChange?: (viewport: Viewport) => void; + pointRadius?: number; + pointRadiusUnit?: string; + renderWhileDragging?: boolean; + rgb?: (string | number)[]; + bounds?: [[number, number], [number, number]]; // May be undefined for empty datasets +} + +interface MapBoxState { + viewport: Viewport; +} + +const defaultProps: Partial = { width: 400, height: 400, globalOpacity: 1, @@ -55,19 +76,29 @@ const defaultProps = { pointRadiusUnit: 'Pixels', }; -class MapBox extends Component { - constructor(props) { +class MapBox extends Component { + static defaultProps = defaultProps; + + constructor(props: MapBoxProps) { super(props); - const { width, height, bounds } = this.props; + const { width = 400, height = 400, bounds } = this.props; // Get a viewport that fits the given bounds, which all marks to be clustered. // Derive lat, lon and zoom from this viewport. This is only done on initial // render as the bounds don't update as we pan/zoom in the current design. - const mercator = new WebMercatorViewport({ - width, - height, - }).fitBounds(bounds); - const { latitude, longitude, zoom } = mercator; + + let latitude = 0; + let longitude = 0; + let zoom = 1; + + // Guard against empty datasets where bounds may be undefined + if (bounds && bounds[0] && bounds[1]) { + const mercator = new WebMercatorViewport({ + width, + height, + }).fitBounds(bounds); + ({ latitude, longitude, zoom } = mercator); + } this.state = { viewport: { @@ -79,10 +110,10 @@ class MapBox extends Component { this.handleViewportChange = this.handleViewportChange.bind(this); } - handleViewportChange(viewport) { + handleViewportChange(viewport: Viewport) { this.setState({ viewport }); const { onViewportChange } = this.props; - onViewportChange(viewport); + onViewportChange!(viewport); } render() { @@ -109,14 +140,20 @@ class MapBox extends Component { // to an area outside of the original bounds, no additional queries are made to the backend to // retrieve additional data. // add this variable to widen the visible area - const offsetHorizontal = (width * 0.5) / 100; - const offsetVertical = (height * 0.5) / 100; - const bbox = [ - bounds[0][0] - offsetHorizontal, - bounds[0][1] - offsetVertical, - bounds[1][0] + offsetHorizontal, - bounds[1][1] + offsetVertical, - ]; + const offsetHorizontal = ((width ?? 400) * 0.5) / 100; + const offsetVertical = ((height ?? 400) * 0.5) / 100; + + // Guard against empty datasets where bounds may be undefined + const bbox = + bounds && bounds[0] && bounds[1] + ? [ + bounds[0][0] - offsetHorizontal, + bounds[0][1] - offsetVertical, + bounds[1][0] + offsetHorizontal, + bounds[1][1] + offsetVertical, + ] + : [-180, -90, 180, 90]; // Default to world bounds + const clusters = clusterer.getClusters(bbox, Math.round(viewport.zoom)); return ( @@ -139,8 +176,8 @@ class MapBox extends Component { globalOpacity={globalOpacity} compositeOperation="screen" renderWhileDragging={renderWhileDragging} - aggregation={hasCustomMetric ? aggregatorName : null} - lngLatAccessor={location => { + aggregation={hasCustomMetric ? aggregatorName : undefined} + lngLatAccessor={(location: GeoJSONLocation) => { const { coordinates } = location.geometry; return [coordinates[0], coordinates[1]]; @@ -151,7 +188,4 @@ class MapBox extends Component { } } -MapBox.propTypes = propTypes; -MapBox.defaultProps = defaultProps; - export default MapBox; diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.tsx similarity index 66% rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.tsx index e70862f1d9c..c946d3f2825 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/ScatterPlotGlowOverlay.tsx @@ -17,7 +17,6 @@ * under the License. */ /* eslint-disable react/require-default-props */ -import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { CanvasOverlay } from 'react-map-gl'; import { kmToPixels, MILES_PER_KM } from './utils/geo'; @@ -25,43 +24,71 @@ import roundDecimal from './utils/roundDecimal'; import luminanceFromRGB from './utils/luminanceFromRGB'; import 'mapbox-gl/dist/mapbox-gl.css'; -const propTypes = { - aggregation: PropTypes.string, - compositeOperation: PropTypes.string, - dotRadius: PropTypes.number, - globalOpacity: PropTypes.number, - lngLatAccessor: PropTypes.func, - locations: PropTypes.arrayOf(PropTypes.object).isRequired, - pointRadiusUnit: PropTypes.string, - renderWhileDragging: PropTypes.bool, - rgb: PropTypes.arrayOf( - PropTypes.oneOfType([PropTypes.string, PropTypes.number]), - ), - zoom: PropTypes.number, -}; +interface GeoJSONLocation { + geometry: { + coordinates: [number, number]; + }; + properties: Record; +} -const defaultProps = { +interface RedrawParams { + width: number; + height: number; + ctx: CanvasRenderingContext2D; + isDragging: boolean; + project: (lngLat: [number, number]) => [number, number]; +} + +interface DrawTextOptions { + fontHeight?: number; + label?: string | number; + radius?: number; + rgb?: (string | number)[]; + shadow?: boolean; +} + +interface ScatterPlotGlowOverlayProps { + aggregation?: string; + compositeOperation?: string; + dotRadius?: number; + globalOpacity?: number; + lngLatAccessor?: (location: GeoJSONLocation) => [number, number]; + locations: GeoJSONLocation[]; + pointRadiusUnit?: string; + renderWhileDragging?: boolean; + rgb?: (string | number)[]; + zoom?: number; + isDragging?: boolean; +} + +const defaultProps: Partial = { // Same as browser default. compositeOperation: 'source-over', dotRadius: 4, - lngLatAccessor: location => [location[0], location[1]], + lngLatAccessor: (location: GeoJSONLocation) => [ + location.geometry.coordinates[0], + location.geometry.coordinates[1], + ], renderWhileDragging: true, }; -const computeClusterLabel = (properties, aggregation) => { - const count = properties.point_count; +const computeClusterLabel = ( + properties: Record, + aggregation: string | undefined, +): number | string => { + const count = properties.point_count as number; if (!aggregation) { return count; } if (aggregation === 'sum' || aggregation === 'min' || aggregation === 'max') { - return properties[aggregation]; + return properties[aggregation] as number; } - const { sum } = properties; + const { sum } = properties as { sum: number }; const mean = sum / count; if (aggregation === 'mean') { return Math.round(100 * mean) / 100; } - const { squaredSum } = properties; + const { squaredSum } = properties as { squaredSum: number }; const variance = squaredSum / count - (sum / count) ** 2; if (aggregation === 'var') { return Math.round(100 * variance) / 100; @@ -74,13 +101,19 @@ const computeClusterLabel = (properties, aggregation) => { return count; }; -class ScatterPlotGlowOverlay extends PureComponent { - constructor(props) { +class ScatterPlotGlowOverlay extends PureComponent { + static defaultProps = defaultProps; + + constructor(props: ScatterPlotGlowOverlayProps) { super(props); this.redraw = this.redraw.bind(this); } - drawText(ctx, pixel, options = {}) { + drawText( + ctx: CanvasRenderingContext2D, + pixel: [number, number], + options: DrawTextOptions = {}, + ) { const IS_DARK_THRESHOLD = 110; const { fontHeight = 0, @@ -90,7 +123,11 @@ class ScatterPlotGlowOverlay extends PureComponent { shadow = false, } = options; const maxWidth = radius * 1.8; - const luminance = luminanceFromRGB(rgb[1], rgb[2], rgb[3]); + const luminance = luminanceFromRGB( + rgb[1] as number, + rgb[2] as number, + rgb[3] as number, + ); ctx.globalCompositeOperation = 'source-over'; ctx.fillStyle = luminance <= IS_DARK_THRESHOLD ? 'white' : 'black'; @@ -102,7 +139,7 @@ class ScatterPlotGlowOverlay extends PureComponent { ctx.shadowColor = luminance <= IS_DARK_THRESHOLD ? 'black' : ''; } - const textWidth = ctx.measureText(label).width; + const textWidth = ctx.measureText(String(label)).width; if (textWidth > maxWidth) { const scale = fontHeight / textWidth; ctx.font = `${scale * maxWidth}px sans-serif`; @@ -110,14 +147,15 @@ class ScatterPlotGlowOverlay extends PureComponent { const { compositeOperation } = this.props; - ctx.fillText(label, pixel[0], pixel[1]); - ctx.globalCompositeOperation = compositeOperation; + ctx.fillText(String(label), pixel[0], pixel[1]); + ctx.globalCompositeOperation = (compositeOperation ?? + 'source-over') as GlobalCompositeOperation; ctx.shadowBlur = 0; ctx.shadowColor = ''; } // Modified: https://github.com/uber/react-map-gl/blob/master/overlays/scatterplot.react.js - redraw({ width, height, ctx, isDragging, project }) { + redraw({ width, height, ctx, isDragging, project }: RedrawParams) { const { aggregation, compositeOperation, @@ -131,8 +169,8 @@ class ScatterPlotGlowOverlay extends PureComponent { zoom, } = this.props; - const radius = dotRadius; - const clusterLabelMap = []; + const radius = dotRadius ?? 4; + const clusterLabelMap: (number | string)[] = []; locations.forEach((location, i) => { if (location.properties.cluster) { @@ -141,9 +179,15 @@ class ScatterPlotGlowOverlay extends PureComponent { aggregation, ); } - }, this); + }); - const maxLabel = Math.max(...clusterLabelMap.filter(v => !Number.isNaN(v))); + const filteredLabels = clusterLabelMap.filter( + v => !Number.isNaN(v), + ) as number[]; + // Guard against empty array or zero max to prevent NaN from division + const maxLabel = + filteredLabels.length > 0 ? Math.max(...filteredLabels) : 1; + const safeMaxLabel = maxLabel > 0 ? maxLabel : 1; // Calculate min/max radius values for Pixels mode scaling let minRadiusValue = Infinity; @@ -166,12 +210,17 @@ class ScatterPlotGlowOverlay extends PureComponent { } ctx.clearRect(0, 0, width, height); - ctx.globalCompositeOperation = compositeOperation; + ctx.globalCompositeOperation = (compositeOperation ?? + 'source-over') as GlobalCompositeOperation; if ((renderWhileDragging || !isDragging) && locations) { - locations.forEach(function _forEach(location, i) { - const pixel = project(lngLatAccessor(location)); - const pixelRounded = [ + locations.forEach(function _forEach( + this: ScatterPlotGlowOverlay, + location: GeoJSONLocation, + i: number, + ) { + const pixel = project(lngLatAccessor!(location)) as [number, number]; + const pixelRounded: [number, number] = [ roundDecimal(pixel[0], 1), roundDecimal(pixel[1], 1), ]; @@ -185,8 +234,13 @@ class ScatterPlotGlowOverlay extends PureComponent { ctx.beginPath(); if (location.properties.cluster) { let clusterLabel = clusterLabelMap[i]; + // Validate clusterLabel is a finite number before using it for radius calculation + const numericLabel = Number(clusterLabel); + const safeNumericLabel = Number.isFinite(numericLabel) + ? numericLabel + : 0; const scaledRadius = roundDecimal( - (clusterLabel / maxLabel) ** 0.5 * radius, + (safeNumericLabel / safeMaxLabel) ** 0.5 * radius, 1, ); const fontHeight = roundDecimal(scaledRadius * 0.5, 1); @@ -202,11 +256,11 @@ class ScatterPlotGlowOverlay extends PureComponent { gradient.addColorStop( 1, - `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${0.8 * globalOpacity})`, + `rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${0.8 * (globalOpacity ?? 1)})`, ); gradient.addColorStop( 0, - `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, 0)`, + `rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, 0)`, ); ctx.arc( pixelRounded[0], @@ -218,15 +272,16 @@ class ScatterPlotGlowOverlay extends PureComponent { ctx.fillStyle = gradient; ctx.fill(); - if (Number.isFinite(parseFloat(clusterLabel))) { - if (clusterLabel >= 10000) { - clusterLabel = `${Math.round(clusterLabel / 1000)}k`; - } else if (clusterLabel >= 1000) { - clusterLabel = `${Math.round(clusterLabel / 100) / 10}k`; + if (Number.isFinite(safeNumericLabel)) { + let label: string | number = clusterLabel; + if (safeNumericLabel >= 10000) { + label = `${Math.round(safeNumericLabel / 1000)}k`; + } else if (safeNumericLabel >= 1000) { + label = `${Math.round(safeNumericLabel / 100) / 10}k`; } this.drawText(ctx, pixelRounded, { fontHeight, - label: clusterLabel, + label, radius: scaledRadius, rgb, shadow: true, @@ -234,23 +289,24 @@ class ScatterPlotGlowOverlay extends PureComponent { } } else { const defaultRadius = radius / 6; - const radiusProperty = location.properties.radius; - const pointMetric = location.properties.metric; - let pointRadius = - radiusProperty === null ? defaultRadius : radiusProperty; - let pointLabel; + const rawRadius = location.properties.radius; + const radiusProperty = + typeof rawRadius === 'number' ? rawRadius : null; + const pointMetric = location.properties.metric ?? null; + let pointRadius: number = radiusProperty ?? defaultRadius; + let pointLabel: string | number | undefined; - if (radiusProperty !== null) { - const pointLatitude = lngLatAccessor(location)[1]; + if (radiusProperty != null) { + const pointLatitude = lngLatAccessor!(location)[1]; if (pointRadiusUnit === 'Kilometers') { pointLabel = `${roundDecimal(pointRadius, 2)}km`; - pointRadius = kmToPixels(pointRadius, pointLatitude, zoom); + pointRadius = kmToPixels(pointRadius, pointLatitude, zoom ?? 0); } else if (pointRadiusUnit === 'Miles') { pointLabel = `${roundDecimal(pointRadius, 2)}mi`; pointRadius = kmToPixels( pointRadius * MILES_PER_KM, pointLatitude, - zoom, + zoom ?? 0, ); } else if (pointRadiusUnit === 'Pixels') { // Scale pixel values to a reasonable range (radius/6 to radius/3) @@ -300,9 +356,10 @@ class ScatterPlotGlowOverlay extends PureComponent { } if (pointMetric !== null) { - pointLabel = Number.isFinite(parseFloat(pointMetric)) - ? roundDecimal(pointMetric, 2) - : pointMetric; + const numericMetric = parseFloat(String(pointMetric)); + pointLabel = Number.isFinite(numericMetric) + ? roundDecimal(numericMetric, 2) + : String(pointMetric); } // Fall back to default points if pointRadius wasn't a numerical column @@ -317,7 +374,7 @@ class ScatterPlotGlowOverlay extends PureComponent { 0, Math.PI * 2, ); - ctx.fillStyle = `rgba(${rgb[1]}, ${rgb[2]}, ${rgb[3]}, ${globalOpacity})`; + ctx.fillStyle = `rgba(${rgb![1]}, ${rgb![2]}, ${rgb![3]}, ${globalOpacity})`; ctx.fill(); if (pointLabel !== undefined) { @@ -340,7 +397,4 @@ class ScatterPlotGlowOverlay extends PureComponent { } } -ScatterPlotGlowOverlay.propTypes = propTypes; -ScatterPlotGlowOverlay.defaultProps = defaultProps; - export default ScatterPlotGlowOverlay; diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/index.ts similarity index 92% rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/index.ts index af58b4e70b1..595387721da 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/index.js +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/index.ts @@ -31,8 +31,8 @@ const metadata = new ChartMetadata({ credits: ['https://www.mapbox.com/mapbox-gl-js/api/'], description: '', exampleGallery: [ - { url: example1, urlDark: example1Dark, description: t('Light mode') }, - { url: example2, urlDark: example2Dark, description: t('Dark mode') }, + { url: example1, urlDark: example1Dark, caption: t('Light mode') }, + { url: example2, urlDark: example2Dark, caption: t('Dark mode') }, ], name: t('MapBox'), tags: [ diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts similarity index 79% rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts index 14a5581926b..caaeb9b3f55 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/transformProps.ts @@ -16,12 +16,23 @@ * specific language governing permissions and limitations * under the License. */ -import Supercluster from 'supercluster'; +import Supercluster, { + type Options as SuperclusterOptions, +} from 'supercluster'; +import { ChartProps } from '@superset-ui/core'; import { DEFAULT_POINT_RADIUS, DEFAULT_MAX_ZOOM } from './MapBox'; const NOOP = () => {}; -export default function transformProps(chartProps) { +interface ClusterProperties { + metric: number; + sum: number; + squaredSum: number; + min: number; + max: number; +} + +export default function transformProps(chartProps: ChartProps) { const { width, height, formData, hooks, queriesData } = chartProps; const { onError = NOOP, setControlValue = NOOP } = hooks; const { bounds, geoJSON, hasCustomMetric, mapboxApiKey } = @@ -32,7 +43,6 @@ export default function transformProps(chartProps) { mapboxColor, mapboxStyle, pandasAggfunc, - pointRadius, pointRadiusUnit, renderWhileDragging, } = formData; @@ -45,24 +55,26 @@ export default function transformProps(chartProps) { return {}; } - const opts = { + const opts: SuperclusterOptions = { maxZoom: DEFAULT_MAX_ZOOM, radius: clusteringRadius, }; if (hasCustomMetric) { opts.initial = () => ({ + metric: 0, sum: 0, squaredSum: 0, min: Infinity, max: -Infinity, }); - opts.map = prop => ({ + opts.map = (prop: ClusterProperties) => ({ + metric: prop.metric, sum: prop.metric, squaredSum: prop.metric ** 2, min: prop.metric, max: prop.metric, }); - opts.reduce = (accu, prop) => { + opts.reduce = (accu: ClusterProperties, prop: ClusterProperties) => { // Temporarily disable param-reassignment linting to work with supercluster's api /* eslint-disable no-param-reassign */ accu.sum += prop.sum; @@ -85,7 +97,15 @@ export default function transformProps(chartProps) { hasCustomMetric, mapboxApiKey, mapStyle: mapboxStyle, - onViewportChange({ latitude, longitude, zoom }) { + onViewportChange({ + latitude, + longitude, + zoom, + }: { + latitude: number; + longitude: number; + zoom: number; + }) { setControlValue('viewport_longitude', longitude); setControlValue('viewport_latitude', latitude); setControlValue('viewport_zoom', zoom); diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/geo.js b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/geo.ts similarity index 92% rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/geo.js rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/geo.ts index d2e48a65f6d..cfb434db475 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/geo.js +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/geo.ts @@ -22,7 +22,11 @@ import roundDecimal from './roundDecimal'; export const EARTH_CIRCUMFERENCE_KM = 40075.16; export const MILES_PER_KM = 1.60934; -export function kmToPixels(kilometers, latitude, zoomLevel) { +export function kmToPixels( + kilometers: number, + latitude: number, + zoomLevel: number, +): number { // Algorithm from: http://wiki.openstreetmap.org/wiki/Zoom_levels const latitudeRad = latitude * (Math.PI / 180); // Seems like the zoomLevel is off by one diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/luminanceFromRGB.js b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/luminanceFromRGB.ts similarity index 92% rename from superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/luminanceFromRGB.js rename to superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/luminanceFromRGB.ts index 545d6b04ec7..3ab019130af 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/luminanceFromRGB.js +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/src/utils/luminanceFromRGB.ts @@ -21,7 +21,11 @@ export const LUMINANCE_RED_WEIGHT = 0.2126; export const LUMINANCE_GREEN_WEIGHT = 0.7152; export const LUMINANCE_BLUE_WEIGHT = 0.0722; -export default function luminanceFromRGB(r, g, b) { +export default function luminanceFromRGB( + r: number, + g: number, + b: number, +): number { // Formula: https://en.wikipedia.org/wiki/Relative_luminance return ( LUMINANCE_RED_WEIGHT * r + diff --git a/superset-frontend/plugins/legacy-plugin-chart-map-box/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-map-box/types/external.d.ts new file mode 100644 index 00000000000..89134501f25 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-map-box/types/external.d.ts @@ -0,0 +1,101 @@ +/** + * 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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} + +declare module 'supercluster' { + interface Options

, C = Record> { + minZoom?: number; + maxZoom?: number; + minPoints?: number; + radius?: number; + extent?: number; + nodeSize?: number; + log?: boolean; + initial?: () => C; + map?: (props: P) => C; + reduce?: (accumulated: C, props: C) => void; + } + + interface GeoJSONFeature { + type: string; + geometry: { + type: string; + coordinates: [number, number]; + }; + properties: Record; + } + + class Supercluster

, C = Record> { + constructor(options?: Options); + load(points: GeoJSONFeature[]): Supercluster; + getClusters(bbox: number[], zoom: number): GeoJSONFeature[]; + getTile(z: number, x: number, y: number): GeoJSONFeature[] | null; + getChildren(clusterId: number): GeoJSONFeature[]; + getLeaves( + clusterId: number, + limit?: number, + offset?: number, + ): GeoJSONFeature[]; + getClusterExpansionZoom(clusterId: number): number; + } + + export default Supercluster; + export { Options, GeoJSONFeature }; +} + +declare module 'react-map-gl' { + import { Component, ReactNode } from 'react'; + + interface MapGLProps { + width?: number; + height?: number; + latitude?: number; + longitude?: number; + zoom?: number; + mapStyle?: string; + mapboxApiAccessToken?: string; + onViewportChange?: Function; + preserveDrawingBuffer?: boolean; + children?: ReactNode; + [key: string]: unknown; + } + + export default class MapGL extends Component {} + + interface CanvasOverlayProps { + redraw: (params: { + width: number; + height: number; + ctx: CanvasRenderingContext2D; + isDragging: boolean; + project: (lngLat: [number, number]) => [number, number]; + }) => void; + } + + export class CanvasOverlay extends Component {} +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.jsx b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.tsx similarity index 86% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.jsx rename to superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.tsx index e3c4812a609..65f94ea66b7 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/PairedTTest.tsx @@ -17,20 +17,19 @@ * under the License. */ /* eslint-disable react/no-array-index-key */ -import PropTypes from 'prop-types'; import { PureComponent } from 'react'; import { styled } from '@apache-superset/core/ui'; -import TTestTable, { dataPropType } from './TTestTable'; +import TTestTable, { DataEntry } from './TTestTable'; -const propTypes = { - alpha: PropTypes.number, - className: PropTypes.string, - data: PropTypes.objectOf(dataPropType).isRequired, - groups: PropTypes.arrayOf(PropTypes.string).isRequired, - liftValPrec: PropTypes.number, - metrics: PropTypes.arrayOf(PropTypes.string).isRequired, - pValPrec: PropTypes.number, -}; +interface PairedTTestProps { + alpha: number; + className: string; + data: Record; + groups: string[]; + liftValPrec: number; + metrics: string[]; + pValPrec: number; +} const defaultProps = { alpha: 0.05, @@ -115,7 +114,9 @@ const StyledDiv = styled.div` `} `; -class PairedTTest extends PureComponent { +class PairedTTest extends PureComponent { + static defaultProps = defaultProps; + render() { const { className, metrics, groups, data, alpha, pValPrec, liftValPrec } = this.props; @@ -144,7 +145,4 @@ class PairedTTest extends PureComponent { } } -PairedTTest.propTypes = propTypes; -PairedTTest.defaultProps = defaultProps; - export default PairedTTest; diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.jsx b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx similarity index 82% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.jsx rename to superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx index 9bbaccc432e..abc2a2ccff2 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/TTestTable.tsx @@ -20,28 +20,31 @@ import dist from 'distributions'; import { Component } from 'react'; import { Table, Tr, Td, Thead, Th } from 'reactable'; -import PropTypes from 'prop-types'; -export const dataPropType = PropTypes.arrayOf( - PropTypes.shape({ - group: PropTypes.arrayOf(PropTypes.string), - values: PropTypes.arrayOf( - PropTypes.shape({ - x: PropTypes.number, - y: PropTypes.number, - }), - ), - }), -); +interface DataPointValue { + x: number; + y: number; +} -const propTypes = { - alpha: PropTypes.number, - data: dataPropType.isRequired, - groups: PropTypes.arrayOf(PropTypes.string).isRequired, - liftValPrec: PropTypes.number, - metric: PropTypes.string.isRequired, - pValPrec: PropTypes.number, -}; +export interface DataEntry { + group: string[]; + values: DataPointValue[]; +} + +interface TTestTableProps { + alpha: number; + data: DataEntry[]; + groups: string[]; + liftValPrec: number; + metric: string; + pValPrec: number; +} + +interface TTestTableState { + control: number; + liftValues: (string | number)[]; + pValues: (string | number)[]; +} const defaultProps = { alpha: 0.05, @@ -49,8 +52,10 @@ const defaultProps = { pValPrec: 6, }; -class TTestTable extends Component { - constructor(props) { +class TTestTable extends Component { + static defaultProps = defaultProps; + + constructor(props: TTestTableProps) { super(props); this.state = { control: 0, @@ -64,7 +69,7 @@ class TTestTable extends Component { this.computeTTest(control); // initially populate table } - getLiftStatus(row) { + getLiftStatus(row: number): string { const { control, liftValues } = this.state; // Get a css class name for coloring if (row === control) { @@ -75,10 +80,10 @@ class TTestTable extends Component { return 'invalid'; // infinite or NaN values } - return liftVal >= 0 ? 'true' : 'false'; // green on true, red on false + return Number(liftVal) >= 0 ? 'true' : 'false'; // green on true, red on false } - getPValueStatus(row) { + getPValueStatus(row: number): string { const { control, pValues } = this.state; if (row === control) { return 'control'; @@ -91,7 +96,7 @@ class TTestTable extends Component { return ''; // p-values won't normally be colored } - getSignificance(row) { + getSignificance(row: number): string | boolean { const { control, pValues } = this.state; const { alpha } = this.props; // Color significant as green, else red @@ -100,10 +105,10 @@ class TTestTable extends Component { } // p-values significant below set threshold - return pValues[row] <= alpha; + return Number(pValues[row]) <= alpha; } - computeLift(values, control) { + computeLift(values: DataPointValue[], control: DataPointValue[]): string { const { liftValPrec } = this.props; // Compute the lift value between two time series let sumValues = 0; @@ -116,7 +121,10 @@ class TTestTable extends Component { return (((sumValues - sumControl) / sumControl) * 100).toFixed(liftValPrec); } - computePValue(values, control) { + computePValue( + values: DataPointValue[], + control: DataPointValue[], + ): string | number { const { pValPrec } = this.props; // Compute the p-value from Student's t-test // between two time series @@ -147,12 +155,12 @@ class TTestTable extends Component { } } - computeTTest(control) { + computeTTest(control: number) { // Compute lift and p-values for each row // against the selected control const { data } = this.props; - const pValues = []; - const liftValues = []; + const pValues: (string | number)[] = []; + const liftValues: (string | number)[] = []; if (!data) { return; } @@ -242,10 +250,13 @@ class TTestTable extends Component { ); }); // When sorted ascending, 'control' will always be at top - const sortConfig = groups.concat([ + type SortConfigItem = + | string + | { column: string; sortFunction: (a: string, b: string) => number }; + const sortConfig: SortConfigItem[] = (groups as SortConfigItem[]).concat([ { column: 'pValue', - sortFunction: (a, b) => { + sortFunction: (a: string, b: string) => { if (a === 'control') { return -1; } @@ -258,7 +269,7 @@ class TTestTable extends Component { }, { column: 'liftValue', - sortFunction: (a, b) => { + sortFunction: (a: string, b: string) => { if (a === 'control') { return -1; } @@ -271,7 +282,7 @@ class TTestTable extends Component { }, { column: 'significant', - sortFunction: (a, b) => { + sortFunction: (a: string, b: string) => { if (a === 'control') { return -1; } @@ -296,7 +307,4 @@ class TTestTable extends Component { } } -TTestTable.propTypes = propTypes; -TTestTable.defaultProps = defaultProps; - export default TTestTable; diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts similarity index 80% rename from superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts index 73027a54a22..4decf04962c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/transformProps.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { +import { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { const { formData, queriesData } = chartProps; const { groupby, @@ -31,8 +33,9 @@ export default function transformProps(chartProps) { data: queriesData[0].data, groups: groupby, liftValPrec: parseInt(liftvaluePrecision, 10), - metrics: metrics.map(metric => - typeof metric === 'string' ? metric : metric.label, + metrics: (metrics as (string | { label: string })[]).map( + (metric: string | { label: string }) => + typeof metric === 'string' ? metric : metric.label, ), pValPrec: parseInt(pvaluePrecision, 10), }; diff --git a/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts new file mode 100644 index 00000000000..40dbbf9ccbd --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-paired-t-test/types/external.d.ts @@ -0,0 +1,83 @@ +/** + * 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. + */ +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} + +declare module 'distributions' { + class Studentt { + constructor(degreesOfFreedom: number); + cdf(x: number): number; + } + const dist: { + Studentt: typeof Studentt; + }; + export default dist; +} + +declare module 'reactable' { + import { ComponentType, ReactNode } from 'react'; + + interface TableProps { + className?: string; + id?: string; + sortable?: ( + | string + | { + column: string; + sortFunction: (a: string, b: string) => number; + } + )[]; + children?: ReactNode; + } + + interface TrProps { + className?: string; + onClick?: () => void; + children?: ReactNode; + } + + interface TdProps { + className?: string; + column?: string; + data?: string | number | boolean; + children?: ReactNode; + } + + interface ThProps { + column?: string; + children?: ReactNode; + } + + interface TheadProps { + children?: ReactNode; + } + + export const Table: ComponentType; + export const Tr: ComponentType; + export const Td: ComponentType; + export const Th: ComponentType; + export const Thead: ComponentType; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.js b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts similarity index 64% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.js rename to superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts index c2970979eb7..67ae1c2686c 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.js +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ParallelCoordinates.ts @@ -18,26 +18,46 @@ */ /* eslint-disable react/sort-prop-types */ import * as d3 from 'd3v3'; -import PropTypes from 'prop-types'; import { getSequentialSchemeRegistry } from '@superset-ui/core'; import parcoords from './vendor/parcoords/d3.parcoords'; import divgrid from './vendor/parcoords/divgrid'; -const propTypes = { - // Standard tabular data [{ fieldName1: value1, fieldName2: value2 }] - data: PropTypes.arrayOf(PropTypes.object), - width: PropTypes.number, - height: PropTypes.number, - colorMetric: PropTypes.string, - includeSeries: PropTypes.bool, - linearColorScheme: PropTypes.string, - metrics: PropTypes.arrayOf(PropTypes.string), - series: PropTypes.string, - showDatatable: PropTypes.bool, -}; +interface ParcoordChart { + width(w: number): ParcoordChart; + height(h: number): ParcoordChart; + color(c: Function): ParcoordChart; + alpha(a: number): ParcoordChart; + composite(c: string): ParcoordChart; + data(d: Record[]): ParcoordChart; + dimensions(cols: string[]): ParcoordChart; + types(t: Record): ParcoordChart; + render(): ParcoordChart; + createAxes(): ParcoordChart; + shadows(): ParcoordChart; + reorderable(): ParcoordChart; + brushMode(mode: string): ParcoordChart; + highlight(d: Record[]): void; + unhighlight(): void; + on(event: string, callback: Function): void; +} -function ParallelCoordinates(element, props) { +interface ParallelCoordinatesProps { + data: Record[]; + width: number; + height: number; + colorMetric: string; + includeSeries: boolean; + linearColorScheme: string; + metrics: string[]; + series: string; + showDatatable: boolean; +} + +function ParallelCoordinates( + element: HTMLElement, + props: ParallelCoordinatesProps, +) { const { data, width, @@ -52,7 +72,7 @@ function ParallelCoordinates(element, props) { const cols = includeSeries ? [series].concat(metrics) : metrics; - const ttypes = {}; + const ttypes: Record = {}; ttypes[series] = 'string'; metrics.forEach(v => { ttypes[v] = 'number'; @@ -61,9 +81,15 @@ function ParallelCoordinates(element, props) { const colorScale = colorMetric ? getSequentialSchemeRegistry() .get(linearColorScheme) - .createLinearScale(d3.extent(data, d => d[colorMetric])) + ?.createLinearScale( + d3.extent( + data, + (d: Record) => d[colorMetric] as number, + ), + ) : () => 'grey'; - const color = d => colorScale(d[colorMetric]); + const color = (d: Record) => + (colorScale as Function)(d[colorMetric]); const container = d3 .select(element) .classed('superset-legacy-chart-parallel-coordinates', true); @@ -75,7 +101,7 @@ function ParallelCoordinates(element, props) { .style('height', `${effHeight}px`) .classed('parcoords', true); - const chart = parcoords()(div.node()) + const chart = (parcoords()(div.node()) as unknown as ParcoordChart) .width(width) .color(color) .alpha(0.5) @@ -101,19 +127,19 @@ function ParallelCoordinates(element, props) { .classed('parcoords grid', true) .selectAll('.row') .on({ - mouseover(d) { + mouseover(d: Record) { chart.highlight([d]); }, mouseout: chart.unhighlight, }); // update data table on brush event - chart.on('brush', d => { + chart.on('brush', (d: Record[]) => { d3.select('.grid') .datum(d) .call(grid) .selectAll('.row') .on({ - mouseover(dd) { + mouseover(dd: Record) { chart.highlight([dd]); }, mouseout: chart.unhighlight, @@ -123,6 +149,5 @@ function ParallelCoordinates(element, props) { } ParallelCoordinates.displayName = 'ParallelCoordinates'; -ParallelCoordinates.propTypes = propTypes; export default ParallelCoordinates; diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx similarity index 88% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx rename to superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx index 7bd02eed42f..6f989daa1ae 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/ReactParallelCoordinates.tsx @@ -16,23 +16,30 @@ * specific language governing permissions and limitations * under the License. */ +import { type ComponentProps } from 'react'; import { reactify, addAlpha } from '@superset-ui/core'; import { styled } from '@apache-superset/core/ui'; -import PropTypes from 'prop-types'; import Component from './ParallelCoordinates'; const ReactComponent = reactify(Component); -const ParallelCoordinates = ({ className, ...otherProps }) => ( +interface ParallelCoordinatesWrapperProps { + className?: string; + [key: string]: unknown; +} + +const ParallelCoordinates = ({ + className, + ...otherProps +}: ParallelCoordinatesWrapperProps) => (

- + {/* Props are injected by the chart framework at runtime */} + )} + />
); -ParallelCoordinates.propTypes = { - className: PropTypes.string.isRequired, -}; - export default styled(ParallelCoordinates)` ${({ theme }) => ` .superset-legacy-chart-parallel-coordinates { diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts similarity index 84% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts index 5d9a1457533..afb4759bf88 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/transformProps.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { +import { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { const { width, height, formData, queriesData } = chartProps; const { includeSeries, @@ -33,7 +35,9 @@ export default function transformProps(chartProps) { data: queriesData[0].data, includeSeries, linearColorScheme, - metrics: metrics.map(m => m.label || m), + metrics: metrics.map((m: { label?: string } | string) => + typeof m === 'string' ? m : m.label || m, + ), colorMetric: secondaryMetric && secondaryMetric.label ? secondaryMetric.label diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.js b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts similarity index 99% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.js rename to superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts index 69652a8111c..e6c324daf1d 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.js +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/d3.parcoords.ts @@ -1,6 +1,7 @@ /* [LICENSE TBD] */ +// @ts-nocheck /* eslint-disable */ -export default function (config) { +export default function (config?) { var __ = { data: [], highlighted: [], diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.js b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts similarity index 96% rename from superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.js rename to superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts index 2d79761ebd5..92f86192120 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.js +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/src/vendor/parcoords/divgrid.ts @@ -1,7 +1,8 @@ /* [LICENSE TBD] */ +// @ts-nocheck /* eslint-disable */ // from http://bl.ocks.org/3687826 -export default function (config) { +export default function (config?) { var columns = []; var dg = function (selection) { diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts index e8026f2e2ac..91aa2e2c4b4 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3-parcoords.d.ts @@ -16,7 +16,31 @@ * specific language governing permissions and limitations * under the License. */ -declare module 'src/vendor/parcoords/d3.parcoords' { - const parcoords: any; +declare module './vendor/parcoords/d3.parcoords' { + function parcoords(config?: Record): ( + selection: Element | null, + ) => Record & { + width: Function; + height: Function; + color: Function; + alpha: Function; + composite: Function; + data: Function; + dimensions: Function; + types: Function; + render: Function; + createAxes: Function; + shadows: Function; + reorderable: Function; + brushMode: Function; + highlight: Function; + unhighlight: Function; + on: Function; + }; export default parcoords; } + +declare module './vendor/parcoords/divgrid' { + function divgrid(config?: Record): Function; + export default divgrid; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3.d.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3.d.ts index 34a150e873e..934fe411c68 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3.d.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/d3.d.ts @@ -17,6 +17,6 @@ * under the License. */ declare module 'd3' { - const d3: any; + const d3: Record; export = d3; } diff --git a/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts new file mode 100644 index 00000000000..3143530e5dc --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-parallel-coordinates/types/external.d.ts @@ -0,0 +1,33 @@ +/** + * 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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} + +declare module 'd3v3' { + const d3: Record; + export = d3; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.ts similarity index 84% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js rename to superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.ts index fddc0f928d8..db5d47726a8 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.js +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/Partition.ts @@ -17,27 +17,67 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck -- Legacy D3 visualization using d3-hierarchy v1 APIs without proper type definitions. +// Uses the same approach as NVD3Vis.ts and other heavily D3-dependent files. /* eslint no-param-reassign: [2, {"props": false}] */ import d3 from 'd3'; import PropTypes from 'prop-types'; -import { hierarchy } from 'd3-hierarchy'; +import { hierarchy, HierarchyNode } from 'd3-hierarchy'; import { getNumberFormatter, getTimeFormatter, CategoricalColorNamespace, } from '@superset-ui/core'; +interface PartitionDataNode { + name: string; + val: number; + children?: PartitionDataNode[]; +} + +interface PartitionNode extends HierarchyNode { + x: number; + dx: number; + y: number; + dy: number; + weight: number; + sum: number; + name: string; + disp: string | number; + color: string; + children?: PartitionNode[]; + parent: PartitionNode | null; +} + +interface IcicleProps { + data: PartitionDataNode[]; + width: number; + height: number; + colorScheme: string; + dateTimeFormat: string; + equalDateSize: boolean; + levels: string[]; + metrics: (string | Record)[]; + numberFormat: string; + partitionLimit: number; + partitionThreshold: number; + timeSeriesOption: string; + useLogScale: boolean; + useRichTooltip: boolean; + sliceId: number; +} + // Compute dx, dy, x, y for each node and // return an array of nodes in breadth-first order -function init(root) { - const flat = []; +function init(root: PartitionNode): PartitionNode[] { + const flat: PartitionNode[] = []; const dy = 1 / (root.height + 1); - let prev = null; - root.each(n => { + let prev: PartitionNode | null = null; + root.each((n: PartitionNode) => { n.y = dy * n.depth; n.dy = dy; if (n.parent) { - n.x = prev.depth === n.parent.depth ? 0 : prev.x + prev.dx; + n.x = prev!.depth === n.parent.depth ? 0 : prev!.x + prev!.dx; n.dx = (n.weight / n.parent.sum) * n.parent.dx; } else { n.x = 0; @@ -52,8 +92,11 @@ function init(root) { // Declare PropTypes for recursive data structures // https://github.com/facebook/react/issues/5676 -/* eslint-disable-next-line no-undef */ -const lazyFunction = f => () => f().apply(this, arguments); +// eslint-disable-next-line func-names +const lazyFunction = (f: () => Record) => + function (...args: unknown[]) { + return f().apply(this, args); + }; const leafType = PropTypes.shape({ name: PropTypes.string, val: PropTypes.number.isRequired, @@ -89,9 +132,9 @@ const propTypes = { useRichTooltip: PropTypes.bool, }; -function getAncestors(d) { - const ancestors = [d]; - let node = d; +function getAncestors(d: PartitionNode): PartitionNode[] { + const ancestors: PartitionNode[] = [d]; + let node: PartitionNode = d; while (node.parent) { ancestors.push(node.parent); node = node.parent; @@ -102,7 +145,7 @@ function getAncestors(d) { // This vis is based on // http://mbostock.github.io/d3/talk/20111018/partition.html -function Icicle(element, props) { +function Icicle(element: HTMLElement, props: IcicleProps): void { const { width, height, @@ -134,11 +177,11 @@ function Icicle(element, props) { div.selectAll('*').remove(); const tooltip = div.append('div').classed('partition-tooltip', true); - function hasDateNode(n) { + function hasDateNode(n: PartitionNode): boolean { return metrics.includes(n.data.name) && hasTime; } - function getCategory(depth) { + function getCategory(depth: number): string { if (!depth) { return 'Metric'; } @@ -149,7 +192,7 @@ function Icicle(element, props) { return levels[depth - (hasTime ? 2 : 1)]; } - function drawVis(i, dat) { + function drawVis(i: number, dat: PartitionDataNode[]): void { const datum = dat[i]; const w = width; const h = height / data.length; @@ -262,7 +305,10 @@ function Icicle(element, props) { : 1; }); - function positionAndPopulate(tip, d) { + function positionAndPopulate( + tip: ReturnType, + d: PartitionNode, + ): void { let t = ''; if (useRichTooltip) { const nodes = getAncestors(d); @@ -310,7 +356,7 @@ function Icicle(element, props) { let zoomY = h / 1; // Keep text centered in its division - function transform(d) { + function transform(d: PartitionNode): string { return `translate(8,${(d.dx * zoomY) / 2})`; } @@ -332,7 +378,7 @@ function Icicle(element, props) { }); // When clicking a subdivision, the vis will zoom into it - function click(d) { + function click(d: PartitionNode): boolean { if (!d.children) { if (d.parent) { // Clicking on the rightmost level should zoom in diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.jsx b/superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.tsx similarity index 78% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.jsx rename to superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.tsx index 77a15ffe5a0..8677fa1b805 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/ReactPartition.tsx @@ -20,9 +20,22 @@ import { reactify } from '@superset-ui/core'; import { styled } from '@apache-superset/core/ui'; import Component from './Partition'; -const ReactComponent = reactify(Component); +// Type-erase the render function to allow flexible prop spreading in the wrapper. +// The Partition render function has typed props, but the wrapper passes props via spread +// which TypeScript cannot verify at compile time. Props are validated at runtime. +const ReactComponent = reactify( + Component as unknown as ( + container: HTMLDivElement, + props: Record, + ) => void, +); -const Partition = ({ className, ...otherProps }) => ( +interface PartitionWrapperProps { + className?: string; + [key: string]: unknown; +} + +const Partition = ({ className, ...otherProps }: PartitionWrapperProps) => (
@@ -41,7 +54,7 @@ export default styled(Partition)` } .superset-legacy-chart-partition rect { - stroke: ${theme.borderColorSecondary}; + stroke: ${theme.colorBorderSecondary}; fill: ${theme.colorBgLayout}; fill-opacity: 80%; transition: fill-opacity 180ms linear; diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-partition/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts similarity index 87% rename from superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts index da58cd61608..1422701dab2 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/src/transformProps.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { +import { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { const { width, height, datasource, formData, queriesData } = chartProps; const { colorScheme, @@ -32,7 +34,7 @@ export default function transformProps(chartProps) { timeSeriesOption, sliceId, } = formData; - const { verboseMap } = datasource; + const { verboseMap = {} } = datasource; return { width, @@ -41,7 +43,7 @@ export default function transformProps(chartProps) { colorScheme, dateTimeFormat, equalDateSize, - levels: groupby.map(g => verboseMap[g] || g), + levels: groupby.map((g: string) => verboseMap[g] || g), metrics, numberFormat, partitionLimit: partitionLimit && parseInt(partitionLimit, 10), diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.jsx b/superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.tsx similarity index 95% rename from superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.jsx rename to superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.tsx index 0b93f3e476e..b878dc7b330 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/test/OptionDescription.test.tsx @@ -18,13 +18,14 @@ */ import '@testing-library/jest-dom'; import { screen, render, fireEvent, act } from '@superset-ui/core/spec'; +import type { ColumnMeta } from '@superset-ui/chart-controls'; import OptionDescription from '../src/OptionDescription'; const defaultProps = { option: { label: 'Some option', description: 'Description for some option', - }, + } as unknown as ColumnMeta, }; beforeEach(() => { diff --git a/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.ts new file mode 100644 index 00000000000..66677a600a6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-partition/types/external.d.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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.jsx b/superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.tsx similarity index 79% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.jsx rename to superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.tsx index 824897d26e0..6ac6afdf59f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/ReactRose.tsx @@ -21,9 +21,22 @@ import { styled, css } from '@apache-superset/core/ui'; import { Global } from '@emotion/react'; import Component from './Rose'; -const ReactComponent = reactify(Component); +// Type-erase the render function to allow flexible prop spreading in the wrapper. +// The Rose render function has typed props, but the wrapper passes props via spread +// which TypeScript cannot verify at compile time. Props are validated at runtime. +const ReactComponent = reactify( + Component as unknown as ( + container: HTMLDivElement, + props: Record, + ) => void, +); -const Rose = ({ className, ...otherProps }) => ( +interface RoseWrapperProps { + className?: string; + [key: string]: unknown; +} + +const Rose = ({ className, ...otherProps }: RoseWrapperProps) => (
css` diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.ts similarity index 89% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js rename to superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.ts index 93d402cb61b..4ead29792ce 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.js +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/Rose.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck /* eslint no-use-before-define: ["error", { "functions": false }] */ /* eslint-disable no-restricted-syntax */ /* eslint-disable react/sort-prop-types */ @@ -28,6 +29,42 @@ import { CategoricalColorNamespace, } from '@superset-ui/core'; +interface RoseDataEntry { + key: string[]; + name: string; + time: number; + value: number; + id: number; +} + +interface RoseData { + [timestamp: string]: RoseDataEntry[]; +} + +interface ArcDatum { + startAngle: number; + endAngle: number; + innerRadius: number; + outerRadius: number; + name?: string; + arcId?: number; + val?: number; + time?: number; + percent?: number; +} + +interface RoseProps { + data: RoseData; + width: number; + height: number; + colorScheme: string; + dateTimeFormat: string; + numberFormat: string; + useRichTooltip: boolean; + useAreaProportions: boolean; + sliceId: number; +} + const propTypes = { // Data is an object hashed by numeric value, perhaps timestamp data: PropTypes.objectOf( @@ -49,7 +86,7 @@ const propTypes = { colorScheme: PropTypes.string, }; -function copyArc(d) { +function copyArc(d: ArcDatum): ArcDatum { return { startAngle: d.startAngle, endAngle: d.endAngle, @@ -58,7 +95,7 @@ function copyArc(d) { }; } -function sortValues(a, b) { +function sortValues(a: RoseDataEntry, b: RoseDataEntry): number { if (a.value === b.value) { return a.name > b.name ? 1 : -1; } @@ -66,7 +103,7 @@ function sortValues(a, b) { return b.value - a.value; } -function Rose(element, props) { +function Rose(element: HTMLElement, props: RoseProps): void { const { data, width, @@ -106,14 +143,14 @@ function Rose(element, props) { const legendWrap = g.append('g').attr('class', 'legendWrap'); - function legendData(adatum) { - return adatum[times[0]].map((v, i) => ({ + function legendData(adatum: RoseData) { + return adatum[times[0]].map((v: RoseDataEntry, i: number) => ({ disabled: state.disabled[i], key: v.name, })); } - function tooltipData(d, i, adatum) { + function tooltipData(d: ArcDatum, i: number, adatum: RoseData) { const timeIndex = Math.floor(d.arcId / numGroups); const series = useRichTooltip ? adatum[times[timeIndex]] @@ -173,7 +210,7 @@ function Rose(element, props) { .attr('class', 'groupLabelsWrap'); // Compute inner and outer angles for each data point - function computeArcStates(adatum) { + function computeArcStates(adatum: RoseData) { // Find the max sum of values across all time let maxSum = 0; let grain = 0; @@ -198,7 +235,7 @@ function Rose(element, props) { // Compute proportion const P = maxRadius / maxSum; const Q = P * maxRadius; - const computeOuterRadius = (value, innerRadius) => + const computeOuterRadius = (value: number, innerRadius: number): number => useAreaProportions ? Math.sqrt(Q * value + innerRadius * innerRadius) : P * value + innerRadius; @@ -297,26 +334,26 @@ function Rose(element, props) { let arcSt = computeArcStates(datum); - function tween(target, resFunc) { - return function doTween(d) { + function tween(target: ArcDatum, resFunc: (d: ArcDatum) => string) { + return function doTween(d: ArcDatum) { const interpolate = d3.interpolate(copyArc(d), copyArc(target)); - return t => resFunc(Object.assign(d, interpolate(t))); + return (t: number) => resFunc(Object.assign(d, interpolate(t))); }; } - function arcTween(target) { - return tween(target, d => arc(d)); + function arcTween(target: ArcDatum) { + return tween(target, (d: ArcDatum) => arc(d)); } - function translateTween(target) { - return tween(target, d => `translate(${arc.centroid(d)})`); + function translateTween(target: ArcDatum) { + return tween(target, (d: ArcDatum) => `translate(${arc.centroid(d)})`); } // Grab the ID range of segments stand between // this segment and the edge of the circle - const segmentsToEdgeCache = {}; - function getSegmentsToEdge(arcId) { + const segmentsToEdgeCache: Record = {}; + function getSegmentsToEdge(arcId: number): [number, number] { if (segmentsToEdgeCache[arcId]) { return segmentsToEdgeCache[arcId]; } @@ -327,8 +364,8 @@ function Rose(element, props) { } // Get the IDs of all segments in a timeIndex - const segmentsInTimeCache = {}; - function getSegmentsInTime(arcId) { + const segmentsInTimeCache: Record = {}; + function getSegmentsInTime(arcId: number): [number, number] { if (segmentsInTimeCache[arcId]) { return segmentsInTimeCache[arcId]; } @@ -389,11 +426,11 @@ function Rose(element, props) { .attr('fill', d => colorFn(d.name, sliceId)) .attr('d', arc); - function mousemove() { + function mousemove(): void { tooltip(); } - function mouseover(b, i) { + function mouseover(this: Element, b: ArcDatum, i: number): void { tooltip.data(tooltipData(b, i, datum)).hidden(false); const $this = d3.select(this); $this.classed('hover', true); @@ -424,7 +461,7 @@ function Rose(element, props) { } } - function mouseout(b, i) { + function mouseout(this: Element, b: ArcDatum, i: number): void { tooltip.hidden(true); const $this = d3.select(this); $this.classed('hover', false); @@ -455,7 +492,7 @@ function Rose(element, props) { } } - function click(b, i) { + function click(b: ArcDatum, i: number): void { if (inTransition) { return; } @@ -577,7 +614,7 @@ function Rose(element, props) { } } - function updateActive() { + function updateActive(): void { const delay = d3.event.altKey ? 3000 : 300; legendWrap.datum(legendData(datum)).call(legend); const nArcSt = computeArcStates(datum); diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-rose/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts similarity index 91% rename from superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts index b907e40ecff..b37e26a8722 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/src/transformProps.ts @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -export default function transformProps(chartProps) { +import { ChartProps } from '@superset-ui/core'; + +export default function transformProps(chartProps: ChartProps) { const { width, height, formData, queriesData } = chartProps; const { colorScheme, diff --git a/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.ts new file mode 100644 index 00000000000..66677a600a6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-rose/types/external.d.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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.jsx b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.tsx similarity index 71% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.jsx rename to superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.tsx index ef972bb039e..53dca6da86f 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.jsx +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/ReactWorldMap.tsx @@ -16,14 +16,29 @@ * specific language governing permissions and limitations * under the License. */ -import PropTypes from 'prop-types'; import { reactify } from '@superset-ui/core'; import { styled, useTheme } from '@apache-superset/core/ui'; import WorldMap from './WorldMap'; -const ReactWorldMap = reactify(WorldMap); +// Type-erase the render function to allow flexible prop spreading in the wrapper. +// The WorldMap render function has typed props, but the wrapper passes props via spread +// which TypeScript cannot verify at compile time. Props are validated at runtime. +const ReactWorldMap = reactify( + WorldMap as unknown as ( + container: HTMLDivElement, + props: Record, + ) => void, +); -const WorldMapComponent = ({ className, ...otherProps }) => { +interface WorldMapComponentProps { + className: string; + [key: string]: unknown; +} + +const WorldMapComponent = ({ + className, + ...otherProps +}: WorldMapComponentProps) => { const theme = useTheme(); return (
@@ -32,10 +47,6 @@ const WorldMapComponent = ({ className, ...otherProps }) => { ); }; -WorldMapComponent.propTypes = { - className: PropTypes.string.isRequired, -}; - export default styled(WorldMapComponent)` .superset-legacy-chart-world-map { position: relative; diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts similarity index 86% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js rename to superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts index 74647194a61..f5c873b2ef2 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.js +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/WorldMap.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck /* eslint-disable react/sort-prop-types */ import d3 from 'd3'; import PropTypes from 'prop-types'; @@ -23,10 +24,62 @@ import { extent as d3Extent } from 'd3-array'; import { getSequentialSchemeRegistry, CategoricalColorNamespace, + ValueFormatter, } from '@superset-ui/core'; import Datamap from 'datamaps/dist/datamaps.all.min'; import { ColorBy } from './utils'; +interface WorldMapDataEntry { + country: string; + code: string; + latitude: number; + longitude: number; + name: string; + m1: number; + m2: number; +} + +interface ProcessedDataEntry extends WorldMapDataEntry { + radius: number; + fillColor: string; +} + +interface WorldMapFilterState { + selectedValues?: string[]; + [key: string]: unknown; +} + +export interface WorldMapProps { + countryFieldtype: string; + entity: string; + data: WorldMapDataEntry[]; + width: number; + height: number; + maxBubbleSize: number; + showBubbles: boolean; + linearColorScheme: string; + color: string; + colorBy: ColorBy; + colorScheme: string; + sliceId: number; + theme: Record; + onContextMenu: ( + x: number, + y: number, + payload: Record, + ) => void; + setDataMask: (dataMask: Record) => void; + inContextMenu: boolean; + filterState: WorldMapFilterState; + emitCrossFilters: boolean; + formatter: ValueFormatter; +} + +interface DatamapSource { + id?: string; + country?: string; +} + const propTypes = { data: PropTypes.arrayOf( PropTypes.shape({ @@ -51,7 +104,7 @@ const propTypes = { formatter: PropTypes.object, }; -function WorldMap(element, props) { +function WorldMap(element: HTMLElement, props: WorldMapProps): void { const { countryFieldtype, entity, @@ -108,12 +161,12 @@ function WorldMap(element, props) { })); } - const mapData = {}; + const mapData: Record = {}; processedData.forEach(d => { mapData[d.country] = d; }); - const getCrossFilterDataMask = source => { + const getCrossFilterDataMask = (source: DatamapSource) => { const selected = Object.values(filterState.selectedValues || {}); const key = source.id || source.country; const country = @@ -152,7 +205,7 @@ function WorldMap(element, props) { }; }; - const handleClick = source => { + const handleClick = (source: DatamapSource) => { if (!emitCrossFilters) { return; } @@ -166,7 +219,7 @@ function WorldMap(element, props) { } }; - const handleContextMenu = source => { + const handleContextMenu = (source: DatamapSource) => { const pointerEvent = d3.event; pointerEvent.preventDefault(); const key = source.id || source.country; diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.js rename to superset-frontend/plugins/legacy-plugin-chart-world-map/src/index.ts diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts similarity index 94% rename from superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js rename to superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts index 92be0f8b01c..0c541297fcb 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.js +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/src/transformProps.ts @@ -17,9 +17,9 @@ * under the License. */ import { rgb } from 'd3-color'; -import { getValueFormatter } from '@superset-ui/core'; +import { ChartProps, getValueFormatter } from '@superset-ui/core'; -export default function transformProps(chartProps) { +export default function transformProps(chartProps: ChartProps) { const { width, height, diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts index b7439406589..8ae4109ea41 100644 --- a/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/test/WorldMap.test.ts @@ -18,7 +18,45 @@ */ import d3 from 'd3'; +import { getNumberFormatter, ValueFormatter } from '@superset-ui/core'; import WorldMap from '../src/WorldMap'; +import { ColorBy } from '../src/utils'; + +interface WorldMapDataEntry { + country: string; + code: string; + latitude: number; + longitude: number; + name: string; + m1: number; + m2: number; +} + +interface WorldMapProps { + countryFieldtype: string; + entity: string; + data: WorldMapDataEntry[]; + width: number; + height: number; + maxBubbleSize: number; + showBubbles: boolean; + linearColorScheme: string; + color: string; + colorBy: ColorBy; + colorScheme: string; + sliceId: number; + theme: Record; + onContextMenu: ( + x: number, + y: number, + payload: Record, + ) => void; + setDataMask: (dataMask: Record) => void; + inContextMenu: boolean; + filterState: { selectedValues?: string[] }; + emitCrossFilters: boolean; + formatter: ValueFormatter; +} type MouseEventHandler = (this: HTMLElement) => void; @@ -56,12 +94,28 @@ jest.mock('datamaps/dist/datamaps.all.min', () => { }); let container: HTMLElement; -const mockFormatter = jest.fn(val => String(val)); +const formatter = getNumberFormatter(); -const baseProps = { +const baseProps: WorldMapProps = { data: [ - { country: 'USA', name: 'United States', m1: 100, m2: 200, code: 'US' }, - { country: 'CAN', name: 'Canada', m1: 50, m2: 100, code: 'CA' }, + { + country: 'USA', + name: 'United States', + m1: 100, + m2: 200, + code: 'US', + latitude: 37.0902, + longitude: -95.7129, + }, + { + country: 'CAN', + name: 'Canada', + m1: 50, + m2: 100, + code: 'CA', + latitude: 56.1304, + longitude: -106.3468, + }, ], width: 600, height: 400, @@ -69,7 +123,7 @@ const baseProps = { showBubbles: false, linearColorScheme: 'schemeRdYlBu', color: '#61B0B7', - colorBy: 'country', + colorBy: ColorBy.Country, colorScheme: 'supersetColors', sliceId: 123, theme: { @@ -85,7 +139,7 @@ const baseProps = { inContextMenu: false, filterState: { selectedValues: [] }, emitCrossFilters: false, - formatter: mockFormatter, + formatter, }; beforeEach(() => { @@ -143,7 +197,7 @@ test('stores original fill color on mouseover', () => { selectAll: jest.fn().mockReturnValue({ remove: jest.fn() }), }; - jest.spyOn(d3, 'select').mockReturnValue(mockD3Selection as any); + jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any); // Capture the mouseover handler mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => { @@ -198,7 +252,7 @@ test('restores original fill color on mouseout for country with data', () => { selectAll: jest.fn().mockReturnValue({ remove: jest.fn() }), }; - jest.spyOn(d3, 'select').mockReturnValue(mockD3Selection as any); + jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any); // Capture the mouseout handler mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => { @@ -254,7 +308,7 @@ test('restores default fill color on mouseout for country with no data', () => { selectAll: jest.fn().mockReturnValue({ remove: jest.fn() }), }; - jest.spyOn(d3, 'select').mockReturnValue(mockD3Selection as any); + jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any); mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => { if (event === 'mouseout') { @@ -296,7 +350,7 @@ test('does not handle mouse events when inContextMenu is true', () => { selectAll: jest.fn().mockReturnValue({ remove: jest.fn() }), }; - jest.spyOn(d3, 'select').mockReturnValue(mockD3Selection as any); + jest.spyOn(d3 as any, 'select').mockReturnValue(mockD3Selection as any); mockSvg.on.mockImplementation((event: string, handler: MouseEventHandler) => { if (event === 'mouseover') { diff --git a/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts b/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.ts new file mode 100644 index 00000000000..66677a600a6 --- /dev/null +++ b/superset-frontend/plugins/legacy-plugin-chart-world-map/types/external.d.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. + */ + +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx index 0fdc53f2b97..f0f4d34a959 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/Multi/Multi.tsx @@ -224,7 +224,7 @@ const DeckMulti = (props: DeckMultiProps) => { const createLayerFromData = useCallback( (subslice: JsonObject, json: JsonObject): Layer => - // @ts-ignore TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators. + // @ts-expect-error TODO(hainenber): define proper type for `form_data.viz_type` and call signature for functions in layerGenerators. layerGenerators[subslice.form_data.viz_type]({ formData: subslice.form_data, payload: json, diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx index 72faceb32a8..9fd68782549 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Grid/Grid.tsx @@ -105,9 +105,9 @@ export const getLayer: GetLayerType = function ({ : undefined, colorRange, outline: false, - // @ts-ignore + // @ts-expect-error getElevationValue: aggFunc, - // @ts-ignore + // @ts-expect-error getColorValue: colorAggFunc, ...commonLayerProps({ formData: fd, @@ -158,7 +158,7 @@ export const getHighlightLayer: GetLayerType = function ({ colorRange: [TRANSPARENT_COLOR_ARRAY, HIGHLIGHT_COLOR_ARRAY], colorAggregation: 'MAX', outline: false, - // @ts-ignore + // @ts-expect-error getElevationValue: aggFunc, getColorWeight: colorAggFunc, opacity: 1, diff --git a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx index 1f1e35f3dc9..4472444ebb4 100644 --- a/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx +++ b/superset-frontend/plugins/legacy-preset-chart-deckgl/src/layers/Hex/Hex.tsx @@ -106,9 +106,9 @@ export const getLayer: GetLayerType = function ({ : undefined, colorRange, outline: false, - // @ts-ignore + // @ts-expect-error getElevationValue: aggFunc, - // @ts-ignore + // @ts-expect-error getColorValue: colorAggFunc, ...commonLayerProps({ formData: fd, @@ -159,7 +159,7 @@ export const getHighlightLayer: GetLayerType = function ({ colorRange: [TRANSPARENT_COLOR_ARRAY, HIGHLIGHT_COLOR_ARRAY], colorAggregation: 'MAX', outline: false, - // @ts-ignore + // @ts-expect-error getElevationValue: aggFunc, getColorWeight: colorAggFunc, opacity: 1, diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bubble/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/Bullet/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/index.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/Compare/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts similarity index 99% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts index 1b1bd1fedf3..d3dc4acea32 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/NVD3Vis.ts @@ -1,3 +1,4 @@ +// @ts-nocheck -- legacy file heavily dependent on untyped d3 v3 and nvd3 APIs /* eslint-disable react/sort-prop-types */ /** * Licensed to the Apache Software Foundation (ASF) under one diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/PropTypes.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/PropTypes.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/PropTypes.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/PropTypes.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.tsx similarity index 98% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.tsx index d1f5e64edc1..368f3b2183a 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.jsx +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/ReactNVD3.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck -- legacy reactified component with untyped `this` context from reactify callbacks import { reactify } from '@superset-ui/core'; import { styled } from '@apache-superset/core/ui'; import PropTypes from 'prop-types'; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/TimePivot/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/index.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/preset.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts similarity index 88% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts index 36465e97a43..edcc82a7e18 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/transformProps.ts @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { VizType } from '@superset-ui/core'; +// @ts-nocheck -- legacy transformProps with loosely-typed formData from ChartProps +import { ChartProps, VizType } from '@superset-ui/core'; import isTruthy from './utils/isTruthy'; import { tokenizeToNumericArray, @@ -26,8 +27,21 @@ import { formatLabel } from './utils'; const NOOP = () => {}; -const grabD3Format = (datasource, targetMetric) => { - let foundFormatter; +interface DatasourceMetric { + d3format?: string; + metric_name?: string; +} + +interface NVD3Datasource { + metrics?: DatasourceMetric[]; + verboseMap?: Record; +} + +const grabD3Format = ( + datasource: NVD3Datasource | undefined, + targetMetric: string, +): string | undefined => { + let foundFormatter: string | undefined; const { metrics = [] } = datasource || {}; metrics.forEach(metric => { if (metric.d3format && metric.metric_name === targetMetric) { @@ -38,7 +52,7 @@ const grabD3Format = (datasource, targetMetric) => { return foundFormatter; }; -export default function transformProps(chartProps) { +export default function transformProps(chartProps: ChartProps) { const { width, height, diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.ts similarity index 99% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.ts index dccc9e4184a..9fdeeeafa61 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck -- legacy file heavily dependent on untyped d3 v3 and nvd3 APIs import d3 from 'd3'; import d3tip from 'd3-tip'; import dompurify from 'dompurify'; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.ts similarity index 94% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.ts index 2594a63633b..d06eafe1b23 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/utils/isTruthy.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -export default function isTruthy(obj) { +export default function isTruthy(obj: unknown): boolean { if (typeof obj === 'boolean') { return obj; } diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts similarity index 97% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts index e9ff8299fb7..19b2af126df 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/AnnotationTypes.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck -- vendor file; not fully typed import { t } from '@apache-superset/core/ui'; function extractTypes(metadata) { diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.ts similarity index 97% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.ts index c4c9c3264ec..098b3a73f2e 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/src/vendor/superset/exploreUtils.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +// @ts-nocheck -- vendor file; not fully typed /* eslint camelcase: 0 */ import URI from 'urijs'; import safeStringify from 'fast-safe-stringify'; diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.ts similarity index 96% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.ts index 137a20e6256..192d92e4d03 100644 --- a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.js +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils.test.ts @@ -128,11 +128,12 @@ describe('nvd3/utils', () => { }); it('returns a date formatter if format is smart_date', () => { const time = new Date(Date.UTC(2018, 10, 21, 22, 11)); + // @ts-expect-error -- getTimeOrNumberFormatter doesn't distinguish return types; accepts Date at runtime expect(getTimeOrNumberFormatter('smart_date')(time)).toBe('10:11'); }); it('returns a number formatter otherwise', () => { expect(getTimeOrNumberFormatter('.3s')(3000000)).toBe('3.00M'); - expect(getTimeOrNumberFormatter()(3000100)).toBe('3M'); + expect(getTimeOrNumberFormatter(undefined)(3000100)).toBe('3M'); }); }); diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/isTruthy.test.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/isTruthy.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/isTruthy.test.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/isTruthy.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/tokenize.test.js b/superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/tokenize.test.ts similarity index 100% rename from superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/tokenize.test.js rename to superset-frontend/plugins/legacy-preset-chart-nvd3/test/utils/tokenize.test.ts diff --git a/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts b/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts new file mode 100644 index 00000000000..cd1101eb3d0 --- /dev/null +++ b/superset-frontend/plugins/legacy-preset-chart-nvd3/types/external.d.ts @@ -0,0 +1,49 @@ +/** + * 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. + */ +declare module '*.png' { + const value: string; + export default value; +} + +declare module '*.jpg' { + const value: string; + export default value; +} + +declare module '*.jpeg' { + const value: string; + export default value; +} + +declare module 'd3' { + const d3: Record; + export default d3; +} + +declare module 'nvd3-fork' { + const nv: Record; + export default nv; +} + +declare module 'nvd3-fork/build/nv.d3.css'; + +declare module 'd3-tip' { + const d3tip: () => Record; + export default d3tip; +} diff --git a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts index d84fe3fddf6..a6772d1e13a 100644 --- a/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts +++ b/superset-frontend/plugins/plugin-chart-ag-grid-table/src/utils/formatValue.ts @@ -103,7 +103,7 @@ export const valueFormatter = ( }; export const valueGetter = (params: ValueGetterParams, col: InputColumn) => { - // @ts-ignore + // @ts-expect-error if (params?.colDef?.isMain) { const modifiedColId = `Main ${params.column.getColId()}`; return params.data[modifiedColId]; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx index 65e608bd757..4af5066efca 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/src/util/layerUtil.tsx @@ -133,7 +133,6 @@ export const createWfsLayer = async (wfsLayerConf: WfsLayerConf) => { return new VectorLayer({ source: wfsSource, - // @ts-ignore style: writeStyleResult?.output, }); }; diff --git a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts index d2912105e02..20c415df655 100644 --- a/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts +++ b/superset-frontend/plugins/plugin-chart-cartodiagram/test/util/layerUtil.test.ts @@ -72,10 +72,10 @@ describe('layerUtil', () => { const wfsLayer = await createWfsLayer(wfsLayerConf); const style = wfsLayer!.getStyle(); - // @ts-ignore + // @ts-expect-error expect(style!.length).toEqual(3); - // @ts-ignore upgrade `ol` package for better type of StyleLike type. + // @ts-expect-error upgrade `ol` package for better type of StyleLike type. const colorAtLayer = style![1].getImage().getFill().getColor(); expect(colorToExpect).toEqual(colorAtLayer); }); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx index df201008b1f..e66eda4e13d 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberViz.tsx @@ -192,8 +192,19 @@ function BigNumberVis({ const renderHeader = (maxHeight: number) => { const { bigNumber, width, colorThresholdFormatters, onContextMenu } = props; - // @ts-ignore - const text = bigNumber === null ? t('No data') : headerFormatter(bigNumber); + // Format bigNumber based on its type: null/undefined -> "No data", number -> format, else -> string + let text: string; + if (bigNumber === null || bigNumber === undefined) { + text = t('No data'); + } else if (typeof bigNumber === 'number') { + text = headerFormatter(bigNumber); + } else { + // For string/boolean/Date values, convert to number if possible, else show as string + const numValue = Number(bigNumber); + text = Number.isNaN(numValue) + ? String(bigNumber) + : headerFormatter(numValue); + } const hasThresholdColorFormatter = Array.isArray(colorThresholdFormatters) && diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts index 7b3337fdb11..a1e11c63fea 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BigNumber/BigNumberWithTrendline/transformProps.ts @@ -215,10 +215,13 @@ export default function transformProps( } } - if (data.length > 0) { - const reversedData = [...sortedData].reverse(); - // @ts-ignore - trendLineData = showTrendLine ? reversedData : undefined; + if (data.length > 0 && showTrendLine) { + // Filter out entries with null timestamps and reverse for chronological order + // TimeSeriesDatum requires [number, number | null] - timestamp must be non-null + const validData = sortedData.filter( + (d): d is [number, number | null] => d[0] !== null, + ); + trendLineData = [...validData].reverse(); } let className = ''; @@ -379,7 +382,7 @@ export default function transformProps( width, height, bigNumber, - // @ts-ignore + // @ts-expect-error bigNumberFallback, className, headerFormatter: yAxisFormatter, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts index d9754e3f759..b0aa1e35826 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/transformProps.ts @@ -202,7 +202,7 @@ export default function transformProps( tooltip: { ...getDefaultTooltip(refs), formatter: (param: CallbackDataParams) => { - // @ts-ignore + // @ts-expect-error const { value, name, @@ -239,7 +239,7 @@ export default function transformProps( }, }, }, - // @ts-ignore + // @ts-expect-error ...outlierData, ]; const addYAxisTitleOffset = diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts index 6a7e79d2297..84c9c0c5a18 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/BoxPlot/types.ts @@ -47,7 +47,7 @@ export type BoxPlotFormXTickLayout = | 'flat' | 'staggered'; -// @ts-ignore +// @ts-expect-error export const DEFAULT_FORM_DATA: BoxPlotQueryFormData = { ...DEFAULT_TITLE_FORM_DATA, }; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts index fd3cce40c08..05b5683e628 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts @@ -264,7 +264,6 @@ export default function transformProps( fontWeight: 'bold', }, }, - // @ts-ignore data: transformedData, }, ]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts index a03d0ec01f3..5c1183d1fc5 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts @@ -59,7 +59,7 @@ export interface EchartsFunnelChartProps extends BaseChartProps & { tooltip?: Pick; }; -// @ts-ignore +// @ts-expect-error export const DEFAULT_FORM_DATA: EchartsGraphFormData = { ...DEFAULT_LEGEND_FORM_DATA, source: '', diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx index 390b380675e..6dbe6d7e035 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/EchartsMixedTimeseries.tsx @@ -75,7 +75,6 @@ export default function EchartsMixedTimeseries({ return { dataMask: { extraFormData: { - // @ts-ignore filters: values.length === 0 ? [] diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts index 108d41396a9..393788313f8 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/index.ts @@ -82,7 +82,6 @@ export default class EchartsTimeseriesChartPlugin extends EchartsChartPlugin< ], queryObjectCount: 2, }, - // @ts-ignore transformProps, }); } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 67c434d4beb..907ed4803d4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -759,7 +759,6 @@ export default function transformProps( legendState, chartPadding, ), - // @ts-ignore data: series .filter( entry => diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index c5e363ecb47..3000c995892 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -93,7 +93,7 @@ export type EchartsMixedTimeseriesFormData = QueryFormData & { } & LegendFormData & TitleFormData; -// @ts-ignore +// @ts-expect-error export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = { ...DEFAULT_LEGEND_FORM_DATA, annotationLayers: [], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts index 45d9b1119fe..c869c6b5112 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts @@ -65,7 +65,7 @@ export interface EchartsPieChartProps extends BaseChartProps formData: EchartsPieFormData; } -// @ts-ignore +// @ts-expect-error export const DEFAULT_FORM_DATA: EchartsPieFormData = { ...DEFAULT_LEGEND_FORM_DATA, donut: false, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts index 82719354a98..c3d5dd6cee4 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Radar/types.ts @@ -65,7 +65,7 @@ export interface EchartsRadarChartProps extends BaseChartProps params.name, - // @ts-ignore + // @ts-expect-error emphasis: { backgroundColor: theme.colorPrimaryBgHover, }, @@ -564,7 +563,6 @@ export function transformEventAnnotation( show: false, color: theme.colorTextLabel, position: 'insideEndTop', - // @ts-ignore emphasis: { formatter: (params: CallbackDataParams) => params.name, fontWeight: 'bold', diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts index 0c0d5c3b87f..40f1b87fd43 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/BigNumber/transformProps.test.ts @@ -134,7 +134,7 @@ describe('BigNumberWithTrendline', () => { expect(transformed.bigNumberFallback).toBeNull(); // should successfully formatTime by granularity - // @ts-ignore + // @ts-expect-error expect(transformed.formatTime(new Date('2020-01-01'))).toStrictEqual( '2020-01-01 00:00:00', ); @@ -156,7 +156,7 @@ describe('BigNumberWithTrendline', () => { }, }; const transformed = transformProps(propsWithDatasource); - // @ts-ignore + // @ts-expect-error expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual( '1.23', ); @@ -182,7 +182,7 @@ describe('BigNumberWithTrendline', () => { }, }; const transformed = transformProps(propsWithDatasource); - // @ts-ignore + // @ts-expect-error expect(transformed.headerFormatter(transformed.bigNumber)).toStrictEqual( '$ 1.23', ); diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts index 7be75638686..4c560ea9ff3 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/controlPanel.test.ts @@ -52,7 +52,7 @@ const mockControls = ( return { controls: { - // @ts-ignore + // @ts-expect-error x_axis: { value: xAxisColumn, options: options, diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts index 560f5c19ea1..f69ac1f4d93 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/Timeseries/Scatter/transformProps.test.ts @@ -70,7 +70,6 @@ describe('Scatter Chart X-axis Time Formatting', () => { }); const transformedProps = transformProps( - // @ts-ignore chartProps as EchartsTimeseriesChartProps, ); @@ -92,7 +91,6 @@ describe('Scatter Chart X-axis Time Formatting', () => { }); const transformedProps = transformProps( - // @ts-ignore chartProps as EchartsTimeseriesChartProps, ); @@ -146,7 +144,6 @@ describe('Scatter Chart X-axis Number Formatting', () => { }); const transformedProps = transformProps( - // @ts-ignore chartProps as EchartsTimeseriesChartProps, ); @@ -169,7 +166,6 @@ describe('Scatter Chart X-axis Number Formatting', () => { }); const transformedProps = transformProps( - // @ts-ignore chartProps as EchartsTimeseriesChartProps, ); diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx index 1c2f5cbc05e..7a1fb79f7ad 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/PivotTableChart.tsx @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { useCallback, useMemo } from 'react'; +import { + useCallback, + useMemo, + type MouseEvent as ReactMouseEvent, +} from 'react'; import { MinusSquareOutlined, PlusSquareOutlined } from '@ant-design/icons'; import { t } from '@apache-superset/core'; import { @@ -501,7 +505,7 @@ export default function PivotTableChart(props: PivotTableProps) { const toggleFilter = useCallback( ( - e: MouseEvent, + e: ReactMouseEvent, value: string, filters: FilterType, pivotData: Record, @@ -598,10 +602,10 @@ export default function PivotTableChart(props: PivotTableProps) { const handleContextMenu = useCallback( ( - e: MouseEvent, - colKey: (string | number | boolean)[] | undefined, - rowKey: (string | number | boolean)[] | undefined, - dataPoint: { [key: string]: string }, + e: ReactMouseEvent, + colKey?: string[], + rowKey?: string[], + dataPoint?: { [key: string]: string }, ) => { if (onContextMenu) { e.preventDefault(); @@ -611,7 +615,7 @@ export default function PivotTableChart(props: PivotTableProps) { colKey.forEach((val, i) => { const col = cols[i]; const formatter = dateFormatters[col]; - const formattedVal = formatter?.(val as number) || String(val); + const formattedVal = formatter?.(Number(val)) || String(val); if (i > 0) { drillToDetailFilters.push({ col, @@ -627,7 +631,7 @@ export default function PivotTableChart(props: PivotTableProps) { rowKey.forEach((val, i) => { const col = rows[i]; const formatter = dateFormatters[col]; - const formattedVal = formatter?.(val as number) || String(val); + const formattedVal = formatter?.(Number(val)) || String(val); drillToDetailFilters.push({ col, op: '==', @@ -639,7 +643,9 @@ export default function PivotTableChart(props: PivotTableProps) { } onContextMenu(e.clientX, e.clientY, { drillToDetail: drillToDetailFilters, - crossFilter: getCrossFilterDataMask(dataPoint), + crossFilter: dataPoint + ? getCrossFilterDataMask(dataPoint) + : undefined, drillBy: dataPoint && { filters: [ { diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.tsx similarity index 85% rename from superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx rename to superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.tsx index aa8fbe4f5b5..9e5565b9f26 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.jsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/PivotTable.tsx @@ -19,14 +19,14 @@ import { PureComponent } from 'react'; import { TableRenderer } from './TableRenderers'; +import type { ComponentProps } from 'react'; -class PivotTable extends PureComponent { +type PivotTableProps = ComponentProps; + +class PivotTable extends PureComponent { render() { return ; } } -PivotTable.propTypes = TableRenderer.propTypes; -PivotTable.defaultProps = TableRenderer.defaultProps; - export default PivotTable; diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/Styles.js b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/Styles.ts similarity index 98% rename from superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/Styles.js rename to superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/Styles.ts index 4b9cbd4d4c8..dcc8ccb590b 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/Styles.js +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/Styles.ts @@ -19,7 +19,7 @@ import { css, styled } from '@apache-superset/core/ui'; -export const Styles = styled.div` +export const Styles = styled.div<{ isDashboardEditMode: boolean }>` ${({ theme, isDashboardEditMode }) => css` table.pvtTable { position: ${isDashboardEditMode ? 'inherit' : 'relative'}; diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx similarity index 74% rename from superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx rename to superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx index 489813de6cb..6e536641c39 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.jsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/TableRenderers.tsx @@ -17,7 +17,7 @@ * under the License. */ -import { Component } from 'react'; +import { Component, ReactNode, MouseEvent } from 'react'; import { safeHtmlSpan } from '@superset-ui/core'; import { t } from '@apache-superset/core/ui'; import PropTypes from 'prop-types'; @@ -27,7 +27,106 @@ import { FaSortUp as FaSortAsc } from '@react-icons/all-files/fa/FaSortUp'; import { PivotData, flatKey } from './utilities'; import { Styles } from './Styles'; -const parseLabel = value => { +interface CellColorFormatter { + column: string; + getColorFromValue(value: unknown): string | undefined; +} + +type ClickCallback = ( + e: MouseEvent, + value: unknown, + filters: Record, + pivotData: InstanceType, +) => void; + +type HeaderClickCallback = ( + e: MouseEvent, + value: string, + filters: Record, + pivotData: InstanceType, + isSubtotal: boolean, + isGrandTotal: boolean, +) => void; + +interface TableOptions { + rowTotals?: boolean; + colTotals?: boolean; + rowSubTotals?: boolean; + colSubTotals?: boolean; + clickCallback?: ClickCallback; + clickColumnHeaderCallback?: HeaderClickCallback; + clickRowHeaderCallback?: HeaderClickCallback; + highlightHeaderCellsOnHover?: boolean; + omittedHighlightHeaderGroups?: string[]; + highlightedHeaderCells?: Record; + cellColorFormatters?: Record; + dateFormatters?: Record string) | undefined>; +} + +interface SubtotalDisplay { + displayOnTop: boolean; + enabled?: boolean; + hideOnExpand: boolean; +} + +interface SubtotalOptions { + arrowCollapsed?: ReactNode; + arrowExpanded?: ReactNode; + colSubtotalDisplay?: Partial; + rowSubtotalDisplay?: Partial; +} + +interface TableRendererProps { + cols: string[]; + rows: string[]; + aggregatorName: string; + tableOptions: TableOptions; + subtotalOptions?: SubtotalOptions; + namesMapping?: Record; + onContextMenu: ( + e: MouseEvent, + colKey?: string[], + rowKey?: string[], + filters?: Record, + ) => void; + allowRenderHtml?: boolean; + [key: string]: unknown; +} + +interface TableRendererState { + collapsedRows: Record; + collapsedCols: Record; + sortingOrder: string[]; + activeSortColumn?: number | null; +} + +interface PivotSettings { + pivotData: InstanceType; + colAttrs: string[]; + rowAttrs: string[]; + colKeys: string[][]; + rowKeys: string[][]; + rowTotals: boolean; + colTotals: boolean; + arrowCollapsed: ReactNode; + arrowExpanded: ReactNode; + colSubtotalDisplay: SubtotalDisplay; + rowSubtotalDisplay: SubtotalDisplay; + cellCallbacks: Record void>>; + rowTotalCallbacks: Record void>; + colTotalCallbacks: Record void>; + grandTotalCallback: ((e: MouseEvent) => void) | null; + namesMapping: Record; + allowRenderHtml?: boolean; + visibleRowKeys?: string[][]; + visibleColKeys?: string[][]; + maxRowVisible?: number; + maxColVisible?: number; + rowAttrSpans?: number[][]; + colAttrSpans?: number[][]; +} + +const parseLabel = (value: unknown): string | number => { if (typeof value === 'string') { if (value === 'metric') return t('metric'); return value; @@ -38,21 +137,21 @@ const parseLabel = value => { return String(value); }; -function displayCell(value, allowRenderHtml) { +function displayCell(value: unknown, allowRenderHtml?: boolean): ReactNode { if (allowRenderHtml && typeof value === 'string') { return safeHtmlSpan(value); } return parseLabel(value); } function displayHeaderCell( - needToggle, - ArrowIcon, - onArrowClick, - value, - namesMapping, - allowRenderHtml, -) { - const name = namesMapping[value] || value; + needToggle: boolean, + ArrowIcon: ReactNode, + onArrowClick: ((e: MouseEvent) => void) | null, + value: unknown, + namesMapping: Record, + allowRenderHtml?: boolean, +): ReactNode { + const name = namesMapping[String(value)] || value; const parsedLabel = parseLabel(name); const labelContent = allowRenderHtml && typeof parsedLabel === 'string' @@ -62,9 +161,9 @@ function displayHeaderCell( {ArrowIcon} @@ -75,7 +174,16 @@ function displayHeaderCell( ); } -function sortHierarchicalObject(obj, objSort, rowPartialOnTop) { +interface HierarchicalNode { + currentVal?: number; + [key: string]: HierarchicalNode | number | undefined; +} + +function sortHierarchicalObject( + obj: Record, + objSort: string, + rowPartialOnTop: boolean | undefined, +): Map { // Performs a recursive sort of nested object structures. Sorts objects based on // their currentVal property. The function preserves the hierarchical structure // while sorting each level according to the specified criteria. @@ -93,11 +201,18 @@ function sortHierarchicalObject(obj, objSort, rowPartialOnTop) { return objSort === 'asc' ? valA - valB : valB - valA; }); - const result = new Map(); + const result = new Map(); sortedKeys.forEach(key => { const value = obj[key]; if (typeof value === 'object' && !Array.isArray(value)) { - result.set(key, sortHierarchicalObject(value, objSort, rowPartialOnTop)); + result.set( + key, + sortHierarchicalObject( + value as Record, + objSort, + rowPartialOnTop, + ), + ); } else { result.set(key, value); } @@ -106,21 +221,21 @@ function sortHierarchicalObject(obj, objSort, rowPartialOnTop) { } function convertToArray( - obj, - rowEnabled, - rowPartialOnTop, - maxRowIndex, - parentKeys = [], - result = [], + obj: Map, + rowEnabled: boolean | undefined, + rowPartialOnTop: boolean | undefined, + maxRowIndex: number, + parentKeys: string[] = [], + result: string[][] = [], flag = false, -) { +): string[][] { // Recursively flattens a hierarchical Map structure into an array of key paths. // Handles different rendering scenarios based on row grouping configurations and // depth limitations. The function supports complex hierarchy flattening with let updatedFlag = flag; const keys = Array.from(obj.keys()); - const getValue = key => obj.get(key); + const getValue = (key: string) => obj.get(key); keys.forEach(key => { if (key === 'currentVal') { @@ -131,9 +246,9 @@ function convertToArray( result.push(parentKeys.length > 0 ? [...parentKeys, key] : [key]); updatedFlag = true; } - if (typeof value === 'object' && !Array.isArray(value)) { + if (typeof value === 'object' && value !== null && !Array.isArray(value)) { convertToArray( - value, + value as Map, rowEnabled, rowPartialOnTop, maxRowIndex, @@ -157,8 +272,18 @@ function convertToArray( return result; } -export class TableRenderer extends Component { - constructor(props) { +export class TableRenderer extends Component< + TableRendererProps, + TableRendererState +> { + sortCache: Map; + cachedProps: TableRendererProps | null; + cachedBasePivotSettings: PivotSettings | null; + + static propTypes: Record; + static defaultProps: Record; + + constructor(props: TableRendererProps) { super(props); // We need state to record which entries are collapsed and which aren't. @@ -166,18 +291,20 @@ export class TableRenderer extends Component { // should be collapsed. this.state = { collapsedRows: {}, collapsedCols: {}, sortingOrder: [] }; this.sortCache = new Map(); + this.cachedProps = null; + this.cachedBasePivotSettings = null; this.clickHeaderHandler = this.clickHeaderHandler.bind(this); this.clickHandler = this.clickHandler.bind(this); } - getBasePivotSettings() { + getBasePivotSettings(): PivotSettings { // One-time extraction of pivot settings that we'll use throughout the render. const { props } = this; const colAttrs = props.cols; const rowAttrs = props.rows; - const tableOptions = { + const tableOptions: TableOptions = { rowTotals: true, colTotals: true, ...props.tableOptions, @@ -186,27 +313,30 @@ export class TableRenderer extends Component { const colTotals = tableOptions.colTotals || rowAttrs.length === 0; const namesMapping = props.namesMapping || {}; - const subtotalOptions = { + const subtotalOptions: Required< + Pick + > & + SubtotalOptions = { arrowCollapsed: '\u25B2', arrowExpanded: '\u25BC', ...props.subtotalOptions, }; - const colSubtotalDisplay = { + const colSubtotalDisplay: SubtotalDisplay = { displayOnTop: false, enabled: tableOptions.colSubTotals, hideOnExpand: false, ...subtotalOptions.colSubtotalDisplay, }; - const rowSubtotalDisplay = { + const rowSubtotalDisplay: SubtotalDisplay = { displayOnTop: false, enabled: tableOptions.rowSubTotals, hideOnExpand: false, ...subtotalOptions.rowSubtotalDisplay, }; - const pivotData = new PivotData(props, { + const pivotData = new PivotData(props as Record, { rowEnabled: rowSubtotalDisplay.enabled, colEnabled: colSubtotalDisplay.enabled, rowPartialOnTop: rowSubtotalDisplay.displayOnTop, @@ -217,10 +347,13 @@ export class TableRenderer extends Component { // Also pre-calculate all the callbacks for cells, etc... This is nice to have to // avoid re-calculations of the call-backs on cell expansions, etc... - const cellCallbacks = {}; - const rowTotalCallbacks = {}; - const colTotalCallbacks = {}; - let grandTotalCallback = null; + const cellCallbacks: Record< + string, + Record void> + > = {}; + const rowTotalCallbacks: Record void> = {}; + const colTotalCallbacks: Record void> = {}; + let grandTotalCallback: ((e: MouseEvent) => void) | null = null; if (tableOptions.clickCallback) { rowKeys.forEach(rowKey => { const flatRowKey = flatKey(rowKey); @@ -281,11 +414,15 @@ export class TableRenderer extends Component { }; } - clickHandler(pivotData, rowValues, colValues) { + clickHandler( + pivotData: InstanceType, + rowValues: string[], + colValues: string[], + ) { const colAttrs = this.props.cols; const rowAttrs = this.props.rows; const value = pivotData.getAggregator(rowValues, colValues).value(); - const filters = {}; + const filters: Record = {}; const colLimit = Math.min(colAttrs.length, colValues.length); for (let i = 0; i < colLimit; i += 1) { const attr = colAttrs[i]; @@ -300,26 +437,26 @@ export class TableRenderer extends Component { filters[attr] = rowValues[i]; } } - return e => - this.props.tableOptions.clickCallback(e, value, filters, pivotData); + const { clickCallback } = this.props.tableOptions; + return (e: MouseEvent) => clickCallback?.(e, value, filters, pivotData); } clickHeaderHandler( - pivotData, - values, - attrs, - attrIdx, - callback, + pivotData: InstanceType, + values: string[], + attrs: string[], + attrIdx: number, + callback: HeaderClickCallback | undefined, isSubtotal = false, isGrandTotal = false, ) { - const filters = {}; + const filters: Record = {}; for (let i = 0; i <= attrIdx; i += 1) { const attr = attrs[i]; filters[attr] = values[i]; } - return e => - callback( + return (e: MouseEvent) => + callback?.( e, values[attrIdx], filters, @@ -329,15 +466,17 @@ export class TableRenderer extends Component { ); } - collapseAttr(rowOrCol, attrIdx, allKeys) { - return e => { + collapseAttr(rowOrCol: boolean, attrIdx: number, allKeys: string[][]) { + return (e: MouseEvent) => { // Collapse an entire attribute. e.stopPropagation(); const keyLen = attrIdx + 1; - const collapsed = allKeys.filter(k => k.length === keyLen).map(flatKey); + const collapsed = allKeys + .filter((k: string[]) => k.length === keyLen) + .map(flatKey); - const updates = {}; - collapsed.forEach(k => { + const updates: Record = {}; + collapsed.forEach((k: string) => { updates[k] = true; }); @@ -353,13 +492,13 @@ export class TableRenderer extends Component { }; } - expandAttr(rowOrCol, attrIdx, allKeys) { - return e => { + expandAttr(rowOrCol: boolean, attrIdx: number, allKeys: string[][]) { + return (e: MouseEvent) => { // Expand an entire attribute. This implicitly implies expanding all of the // parents as well. It's a bit inefficient but ah well... e.stopPropagation(); - const updates = {}; - allKeys.forEach(k => { + const updates: Record = {}; + allKeys.forEach((k: string[]) => { for (let i = 0; i <= attrIdx; i += 1) { updates[flatKey(k.slice(0, i + 1))] = false; } @@ -377,8 +516,8 @@ export class TableRenderer extends Component { }; } - toggleRowKey(flatRowKey) { - return e => { + toggleRowKey(flatRowKey: string) { + return (e: MouseEvent) => { e.stopPropagation(); this.setState(state => ({ collapsedRows: { @@ -389,8 +528,8 @@ export class TableRenderer extends Component { }; } - toggleColKey(flatColKey) { - return e => { + toggleColKey(flatColKey: string) { + return (e: MouseEvent) => { e.stopPropagation(); this.setState(state => ({ collapsedCols: { @@ -401,7 +540,7 @@ export class TableRenderer extends Component { }; } - calcAttrSpans(attrArr, numAttrs) { + calcAttrSpans(attrArr: string[][], numAttrs: number) { // Given an array of attribute values (i.e. each element is another array with // the value at every level), compute the spans for every attribute value at // every level. The return value is a nested array of the same shape. It has @@ -410,7 +549,7 @@ export class TableRenderer extends Component { const spans = []; // Index of the last new value const li = Array(numAttrs).map(() => 0); - let lv = Array(numAttrs).map(() => null); + let lv: (string | null)[] = Array(numAttrs).map(() => null); for (let i = 0; i < attrArr.length; i += 1) { // Keep increasing span values as long as the last keys are the same. For // the rest, record spans of 1. Update the indices too. @@ -434,12 +573,16 @@ export class TableRenderer extends Component { return spans; } - getAggregatedData(pivotData, visibleColName, rowPartialOnTop) { + getAggregatedData( + pivotData: InstanceType, + visibleColName: string[], + rowPartialOnTop: boolean | undefined, + ) { // Transforms flat row keys into a hierarchical group structure where each level // represents a grouping dimension. For each row key path, it calculates the // aggregated value for the specified column and builds a nested object that // preserves the hierarchy while storing aggregation values at each level. - const groups = {}; + const groups: Record = {}; const rows = pivotData.rowKeys; rows.forEach(rowKey => { const aggValue = @@ -448,13 +591,17 @@ export class TableRenderer extends Component { if (rowPartialOnTop) { const parent = rowKey .slice(0, -1) - .reduce((acc, key) => (acc[key] ??= {}), groups); - parent[rowKey.at(-1)] = { currentVal: aggValue }; + .reduce( + (acc: Record, key: string) => + (acc[key] ??= {}) as Record, + groups, + ); + parent[rowKey.at(-1)!] = { currentVal: aggValue as number }; } else { - rowKey.reduce((acc, key) => { + rowKey.reduce((acc: Record, key: string) => { acc[key] = acc[key] || { currentVal: 0 }; - acc[key].currentVal = aggValue; - return acc[key]; + (acc[key] as HierarchicalNode).currentVal = aggValue as number; + return acc[key] as Record; }, groups); } }); @@ -462,11 +609,11 @@ export class TableRenderer extends Component { } sortAndCacheData( - groups, - sortOrder, - rowEnabled, - rowPartialOnTop, - maxRowIndex, + groups: Record, + sortOrder: string, + rowEnabled: boolean | undefined, + rowPartialOnTop: boolean | undefined, + maxRowIndex: number, ) { // Processes hierarchical data by first sorting it according to the specified order // and then converting the sorted structure into a flat array format. This function @@ -485,7 +632,12 @@ export class TableRenderer extends Component { ); } - sortData(columnIndex, visibleColKeys, pivotData, maxRowIndex) { + sortData( + columnIndex: number, + visibleColKeys: string[][], + pivotData: InstanceType, + maxRowIndex: number, + ) { // Handles column sorting with direction toggling (asc/desc) and implements // caching mechanism to avoid redundant sorting operations. When sorting the same // column multiple times, it cycles through sorting directions. Uses composite @@ -500,7 +652,10 @@ export class TableRenderer extends Component { newDirection = sortingOrder[columnIndex] === 'asc' ? 'desc' : 'asc'; } - const { rowEnabled, rowPartialOnTop } = pivotData.subtotals; + const { rowEnabled, rowPartialOnTop } = pivotData.subtotals as { + rowEnabled?: boolean; + rowPartialOnTop?: boolean; + }; newSortingOrder[columnIndex] = newDirection; const cacheKey = `${columnIndex}-${visibleColKeys.length}-${rowEnabled}-${rowPartialOnTop}-${newDirection}`; @@ -525,8 +680,8 @@ export class TableRenderer extends Component { newRowKeys = sortedRowKeys; } this.cachedBasePivotSettings = { - ...this.cachedBasePivotSettings, - rowKeys: newRowKeys, + ...this.cachedBasePivotSettings!, + rowKeys: newRowKeys!, }; return { @@ -536,7 +691,11 @@ export class TableRenderer extends Component { }); } - renderColHeaderRow(attrName, attrIdx, pivotSettings) { + renderColHeaderRow( + attrName: string, + attrIdx: number, + pivotSettings: PivotSettings, + ) { // Render a single row in the column header at the top of the pivot table. const { @@ -561,6 +720,10 @@ export class TableRenderer extends Component { dateFormatters, } = this.props.tableOptions; + if (!visibleColKeys || !colAttrSpans) { + return null; + } + const spaceCell = attrIdx === 0 && rowAttrs.length !== 0 ? (
; } - renderRowHeaderRow(pivotSettings) { + renderRowHeaderRow(pivotSettings: PivotSettings) { // Render just the attribute names of the rows (the actual attribute values // will show up in the individual rows). @@ -773,15 +933,15 @@ export class TableRenderer extends Component { {rowAttrs.map((r, i) => { const needLabelToggle = - rowSubtotalDisplay.enabled && i !== rowAttrs.length - 1; + rowSubtotalDisplay.enabled === true && i !== rowAttrs.length - 1; let arrowClickHandle = null; let subArrow = null; if (needLabelToggle) { arrowClickHandle = - i + 1 < maxRowVisible + i + 1 < maxRowVisible! ? this.collapseAttr(true, i, rowKeys) : this.expandAttr(true, i, rowKeys); - subArrow = i + 1 < maxRowVisible ? arrowExpanded : arrowCollapsed; + subArrow = i + 1 < maxRowVisible! ? arrowExpanded : arrowCollapsed; } return ( {rowCells}; } - renderTotalsRow(pivotSettings) { + renderTotalsRow(pivotSettings: PivotSettings) { // Render the final totals rows that has the totals for all the columns. const { @@ -1021,6 +1187,10 @@ export class TableRenderer extends Component { grandTotalCallback, } = pivotSettings; + if (!visibleColKeys) { + return null; + } + const totalLabelCell = (
@@ -600,7 +763,7 @@ export class TableRenderer extends Component { // Iterate through columns. Jump over duplicate values. let i = 0; while (i < visibleColKeys.length) { - let handleContextMenu; + let handleContextMenu: ((e: MouseEvent) => void) | undefined; const colKey = visibleColKeys[i]; const colSpan = attrIdx < colKey.length ? colAttrSpans[i][attrIdx] : 1; let colLabelClass = 'pvtColLabel'; @@ -609,7 +772,7 @@ export class TableRenderer extends Component { if (highlightHeaderCellsOnHover) { colLabelClass += ' hoverable'; } - handleContextMenu = e => + handleContextMenu = (e: MouseEvent) => this.props.onContextMenu(e, colKey, undefined, { [attrName]: colKey[attrIdx], }); @@ -621,14 +784,15 @@ export class TableRenderer extends Component { ) { colLabelClass += ' active'; } - const { maxRowVisible: maxRowIndex, maxColVisible } = pivotSettings; - const visibleSortIcon = maxColVisible - 1 === attrIdx; - const columnName = colKey[maxColVisible - 1]; + const maxRowIndex = pivotSettings.maxRowVisible!; + const mColVisible = pivotSettings.maxColVisible!; + const visibleSortIcon = mColVisible - 1 === attrIdx; + const columnName = colKey[mColVisible - 1]; const rowSpan = 1 + (attrIdx === colAttrs.length - 1 ? rowIncrSpan : 0); const flatColKey = flatKey(colKey.slice(0, attrIdx + 1)); const onArrowClick = needToggle ? this.toggleColKey(flatColKey) : null; - const getSortIcon = key => { + const getSortIcon = (key: number) => { const { activeSortColumn, sortingOrder } = this.state; if (activeSortColumn !== key) { @@ -651,11 +815,7 @@ export class TableRenderer extends Component { ); }; const headerCellFormattedValue = - dateFormatters && - dateFormatters[attrName] && - typeof dateFormatters[attrName] === 'function' - ? dateFormatters[attrName](colKey[attrIdx]) - : colKey[attrIdx]; + dateFormatters?.[attrName]?.(colKey[attrIdx]) ?? colKey[attrIdx]; attrValueCells.push( {cells}
@@ -820,7 +980,11 @@ export class TableRenderer extends Component { ); } - renderTableRow(rowKey, rowIdx, pivotSettings) { + renderTableRow( + rowKey: string[], + rowIdx: number, + pivotSettings: PivotSettings, + ) { // Render a single row in the pivot table. const { @@ -849,14 +1013,14 @@ export class TableRenderer extends Component { const flatRowKey = flatKey(rowKey); const colIncrSpan = colAttrs.length !== 0 ? 1 : 0; - const attrValueCells = rowKey.map((r, i) => { - let handleContextMenu; + const attrValueCells = rowKey.map((r: string, i: number) => { + let handleContextMenu: ((e: MouseEvent) => void) | undefined; let valueCellClassName = 'pvtRowLabel'; if (!omittedHighlightHeaderGroups.includes(rowAttrs[i])) { if (highlightHeaderCellsOnHover) { valueCellClassName += ' hoverable'; } - handleContextMenu = e => + handleContextMenu = (e: MouseEvent) => this.props.onContextMenu(e, undefined, rowKey, { [rowAttrs[i]]: r, }); @@ -868,20 +1032,18 @@ export class TableRenderer extends Component { ) { valueCellClassName += ' active'; } - const rowSpan = rowAttrSpans[rowIdx][i]; + const rowSpan = rowAttrSpans![rowIdx][i]; if (rowSpan > 0) { const flatRowKey = flatKey(rowKey.slice(0, i + 1)); const colSpan = 1 + (i === rowAttrs.length - 1 ? colIncrSpan : 0); const needRowToggle = - rowSubtotalDisplay.enabled && i !== rowAttrs.length - 1; + rowSubtotalDisplay.enabled === true && i !== rowAttrs.length - 1; const onArrowClick = needRowToggle ? this.toggleRowKey(flatRowKey) : null; const headerCellFormattedValue = - dateFormatters && dateFormatters[rowAttrs[i]] - ? dateFormatters[rowAttrs[i]](r) - : r; + dateFormatters?.[rowAttrs[i]]?.(r) ?? r; return ( ) : null; + if (!visibleColKeys) { + return null; + } + const rowClickHandlers = cellCallbacks[flatRowKey] || {}; - const valueCells = visibleColKeys.map(colKey => { + const valueCells = visibleColKeys.map((colKey: string[]) => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator(rowKey, colKey); const aggValue = agg.value(); const keys = [...rowKey, ...colKey]; - let backgroundColor; + let backgroundColor: string | undefined; if (cellColorFormatters) { Object.values(cellColorFormatters).forEach(cellColorFormatter => { if (Array.isArray(cellColorFormatter)) { @@ -1008,7 +1174,7 @@ export class TableRenderer extends Component { return
); - const totalValueCells = visibleColKeys.map(colKey => { + const totalValueCells = visibleColKeys.map((colKey: string[]) => { const flatColKey = flatKey(colKey); const agg = pivotData.getAggregator([], colKey); const aggValue = agg.value(); @@ -1071,7 +1241,7 @@ export class TableRenderer extends Component { role="gridcell" key="total" className="pvtGrandTotal pvtRowTotal" - onClick={grandTotalCallback} + onClick={grandTotalCallback || undefined} onContextMenu={e => this.props.onContextMenu(e, undefined, undefined)} > {displayCell(agg.format(aggValue, agg), this.props.allowRenderHtml)} @@ -1088,11 +1258,18 @@ export class TableRenderer extends Component { ); } - visibleKeys(keys, collapsed, numAttrs, subtotalDisplay) { + visibleKeys( + keys: string[][], + collapsed: Record, + numAttrs: number, + subtotalDisplay: SubtotalDisplay, + ) { return keys.filter( - key => + (key: string[]) => // Is the key hidden by one of its parents? - !key.some((k, j) => collapsed[flatKey(key.slice(0, j))]) && + !key.some( + (_k: string, j: number) => collapsed[flatKey(key.slice(0, j))], + ) && // Leaf key. (key.length === numAttrs || // Children hidden. Must show total. @@ -1113,11 +1290,14 @@ export class TableRenderer extends Component { render() { if (this.cachedProps !== this.props) { this.sortCache.clear(); - this.state.sortingOrder = []; - this.state.activeSortColumn = null; + // Reset sort state without using setState to avoid re-render during render. + // This is safe because the state is being synchronized with new props. + (this.state as TableRendererState).sortingOrder = []; + (this.state as TableRendererState).activeSortColumn = null; this.cachedProps = this.props; this.cachedBasePivotSettings = this.getBasePivotSettings(); } + const basePivotSettings = this.cachedBasePivotSettings!; const { colAttrs, rowAttrs, @@ -1127,7 +1307,7 @@ export class TableRenderer extends Component { rowSubtotalDisplay, colSubtotalDisplay, allowRenderHtml, - } = this.cachedBasePivotSettings; + } = basePivotSettings; // Need to account for exclusions to compute the effective row // and column keys. @@ -1144,28 +1324,28 @@ export class TableRenderer extends Component { colSubtotalDisplay, ); - const pivotSettings = { + const pivotSettings: PivotSettings = { visibleRowKeys, - maxRowVisible: Math.max(...visibleRowKeys.map(k => k.length)), + maxRowVisible: Math.max(...visibleRowKeys.map((k: string[]) => k.length)), visibleColKeys, - maxColVisible: Math.max(...visibleColKeys.map(k => k.length)), + maxColVisible: Math.max(...visibleColKeys.map((k: string[]) => k.length)), rowAttrSpans: this.calcAttrSpans(visibleRowKeys, rowAttrs.length), colAttrSpans: this.calcAttrSpans(visibleColKeys, colAttrs.length), allowRenderHtml, - ...this.cachedBasePivotSettings, + ...basePivotSettings, }; return ( - {colAttrs.map((c, j) => + {colAttrs.map((c: string, j: number) => this.renderColHeaderRow(c, j, pivotSettings), )} {rowAttrs.length !== 0 && this.renderRowHeaderRow(pivotSettings)} - {visibleRowKeys.map((r, i) => + {visibleRowKeys.map((r: string[], i: number) => this.renderTableRow(r, i, pivotSettings), )} {colTotals && this.renderTotalsRow(pivotSettings)} diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/index.js b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/index.ts similarity index 100% rename from superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/index.js rename to superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/index.ts diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts similarity index 61% rename from superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js rename to superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts index 100f2defa0e..f4991ba4e1f 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.js +++ b/superset-frontend/plugins/plugin-chart-pivot-table/src/react-pivottable/utilities.ts @@ -20,7 +20,45 @@ import PropTypes from 'prop-types'; import { t } from '@apache-superset/core/ui'; -const addSeparators = function (nStr, thousandsSep, decimalSep) { +type SortFunction = ( + a: string | number | null, + b: string | number | null, +) => number; +type Formatter = (x: number) => string; +type PivotRecord = Record; + +interface NumberFormatOptions { + digitsAfterDecimal?: number; + scaler?: number; + thousandsSep?: string; + decimalSep?: string; + prefix?: string; + suffix?: string; +} + +interface Aggregator { + push(record: PivotRecord): void; + value(): string | number | null; + format(x: string | number | null, agg?: Aggregator): string; + numInputs?: number; + getCurrencies?(): string[]; + isSubtotal?: boolean; + isRowSubtotal?: boolean; + isColSubtotal?: boolean; +} + +interface SubtotalOptions { + rowEnabled?: boolean; + colEnabled?: boolean; + rowPartialOnTop?: boolean; + colPartialOnTop?: boolean; +} + +const addSeparators = function ( + nStr: string, + thousandsSep: string, + decimalSep: string, +): string { const x = String(nStr).split('.'); let x1 = x[0]; const x2 = x.length > 1 ? decimalSep + x[1] : ''; @@ -31,7 +69,7 @@ const addSeparators = function (nStr, thousandsSep, decimalSep) { return x1 + x2; }; -const numberFormat = function (optsIn) { +const numberFormat = function (optsIn?: NumberFormatOptions): Formatter { const defaults = { digitsAfterDecimal: 2, scaler: 1, @@ -41,7 +79,7 @@ const numberFormat = function (optsIn) { suffix: '', }; const opts = { ...defaults, ...optsIn }; - return function (x) { + return function (x: number): string { if (Number.isNaN(x) || !Number.isFinite(x)) { return ''; } @@ -57,7 +95,7 @@ const numberFormat = function (optsIn) { const rx = /(\d+)|(\D+)/g; const rd = /\d/; const rz = /^0/; -const naturalSort = (as, bs) => { +const naturalSort: SortFunction = (as, bs) => { // nulls first if (bs !== null && as === null) { return -1; @@ -114,56 +152,68 @@ const naturalSort = (as, bs) => { } // special treatment for strings containing digits - a = a.match(rx); - b = b.match(rx); - while (a.length && b.length) { - const a1 = a.shift(); - const b1 = b.shift(); + const aArr = a.match(rx)!; + const bArr = b.match(rx)!; + while (aArr.length && bArr.length) { + const a1 = aArr.shift()!; + const b1 = bArr.shift()!; if (a1 !== b1) { if (rd.test(a1) && rd.test(b1)) { - return a1.replace(rz, '.0') - b1.replace(rz, '.0'); + return Number(a1.replace(rz, '.0')) - Number(b1.replace(rz, '.0')); } return a1 > b1 ? 1 : -1; } } - return a.length - b.length; + return aArr.length - bArr.length; }; -const sortAs = function (order) { - const mapping = {}; +const sortAs = function (order: (string | number)[]): SortFunction { + const mapping: Record = {}; // sort lowercased keys similarly - const lMapping = {}; - order.forEach((element, i) => { + const lMapping: Record = {}; + order.forEach((element: string | number, i: number) => { mapping[element] = i; if (typeof element === 'string') { lMapping[element.toLowerCase()] = i; } }); - return function (a, b) { - if (a in mapping && b in mapping) { - return mapping[a] - mapping[b]; + return function ( + a: string | number | null, + b: string | number | null, + ): number { + const aKey = a !== null ? String(a) : ''; + const bKey = b !== null ? String(b) : ''; + if (aKey in mapping && bKey in mapping) { + return mapping[aKey] - mapping[bKey]; } - if (a in mapping) { + if (aKey in mapping) { return -1; } - if (b in mapping) { + if (bKey in mapping) { return 1; } - if (a in lMapping && b in lMapping) { - return lMapping[a] - lMapping[b]; + if (aKey in lMapping && bKey in lMapping) { + return lMapping[aKey] - lMapping[bKey]; } - if (a in lMapping) { + if (aKey in lMapping) { return -1; } - if (b in lMapping) { + if (bKey in lMapping) { return 1; } return naturalSort(a, b); }; }; -const getSort = function (sorters, attr) { +const getSort = function ( + sorters: + | ((attr: string) => SortFunction | undefined) + | Record + | null + | undefined, + attr: string, +): SortFunction { if (sorters) { if (typeof sorters === 'function') { const sort = sorters(attr); @@ -186,13 +236,16 @@ const usFmtPct = numberFormat({ suffix: '%', }); -const fmtNonString = formatter => (x, aggregator) => - typeof x === 'string' ? x : formatter(x, aggregator); +const fmtNonString = + (formatter: Formatter) => + (x: string | number | null): string => + typeof x === 'string' ? x : formatter(x as number); /* * Aggregators track currencies via push() and expose them via getCurrencies() * for per-cell currency detection in AUTO mode. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ const baseAggregatorTemplates = { count(formatter = usFmtInt) { return () => @@ -210,18 +263,23 @@ const baseAggregatorTemplates = { }; }, - uniques(fn, formatter = usFmtInt) { - return function ([attr]) { + uniques(fn: (uniq: any[]) => any, formatter = usFmtInt) { + return function ([attr]: string[]) { return function () { return { - uniq: [], - currencySet: new Set(), - push(record) { + uniq: [] as any[], + currencySet: new Set(), + push(record: PivotRecord) { if (!Array.from(this.uniq).includes(record[attr])) { this.uniq.push(record[attr]); } - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } }, value() { @@ -238,19 +296,24 @@ const baseAggregatorTemplates = { }, sum(formatter = usFmt) { - return function ([attr]) { + return function ([attr]: string[]) { return function () { return { - sum: 0, - currencySet: new Set(), - push(record) { + sum: 0 as any, + currencySet: new Set(), + push(record: PivotRecord) { if (Number.isNaN(Number(record[attr]))) { this.sum = record[attr]; } else { - this.sum += parseFloat(record[attr]); + this.sum += parseFloat(String(record[attr])); } - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } }, value() { @@ -266,17 +329,17 @@ const baseAggregatorTemplates = { }; }, - extremes(mode, formatter = usFmt) { - return function ([attr]) { - return function (data) { + extremes(mode: string, formatter = usFmt) { + return function ([attr]: string[]) { + return function (data: any) { return { - val: null, - currencySet: new Set(), + val: null as any, + currencySet: new Set(), sorter: getSort( typeof data !== 'undefined' ? data.sorters : null, attr, ), - push(record) { + push(record: PivotRecord) { const x = record[attr]; if (['min', 'max'].includes(mode)) { const coercedValue = Number(x); @@ -288,24 +351,36 @@ const baseAggregatorTemplates = { ? x : this.val; } else { - this.val = Math[mode]( + const mathFn = mode === 'min' ? Math.min : Math.max; + this.val = mathFn( coercedValue, this.val !== null ? this.val : coercedValue, ); } } else if ( mode === 'first' && - this.sorter(x, this.val !== null ? this.val : x) <= 0 + this.sorter( + x as any, + this.val !== null ? this.val : (x as any), + ) <= 0 ) { this.val = x; } else if ( mode === 'last' && - this.sorter(x, this.val !== null ? this.val : x) >= 0 + this.sorter( + x as any, + this.val !== null ? this.val : (x as any), + ) >= 0 ) { this.val = x; } - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } }, value() { @@ -314,7 +389,7 @@ const baseAggregatorTemplates = { getCurrencies() { return Array.from(this.currencySet); }, - format(x) { + format(x: any) { if (typeof x === 'number') { return formatter(x); } @@ -326,24 +401,29 @@ const baseAggregatorTemplates = { }; }, - quantile(q, formatter = usFmt) { - return function ([attr]) { + quantile(q: number, formatter = usFmt) { + return function ([attr]: string[]) { return function () { return { - vals: [], - strMap: {}, - currencySet: new Set(), - push(record) { + vals: [] as number[], + strMap: {} as Record, + currencySet: new Set(), + push(record: PivotRecord) { const val = record[attr]; const x = Number(val); if (Number.isNaN(x)) { - this.strMap[val] = (this.strMap[val] || 0) + 1; + this.strMap[String(val)] = (this.strMap[String(val)] || 0) + 1; } else { this.vals.push(x); } - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } }, value() { @@ -355,16 +435,18 @@ const baseAggregatorTemplates = { } if (Object.keys(this.strMap).length) { - const values = Object.values(this.strMap).sort((a, b) => a - b); + const values = (Object.values(this.strMap) as number[]).sort( + (a: number, b: number) => a - b, + ); const middle = Math.floor(values.length / 2); const keys = Object.keys(this.strMap); return keys.length % 2 !== 0 ? keys[middle] - : (keys[middle - 1] + keys[middle]) / 2; + : (Number(keys[middle - 1]) + Number(keys[middle])) / 2; } - this.vals.sort((a, b) => a - b); + this.vals.sort((a: number, b: number) => a - b); const i = (this.vals.length - 1) * q; return (this.vals[Math.floor(i)] + this.vals[Math.ceil(i)]) / 2.0; }, @@ -379,21 +461,28 @@ const baseAggregatorTemplates = { }, runningStat(mode = 'mean', ddof = 1, formatter = usFmt) { - return function ([attr]) { + return function ([attr]: string[]) { return function () { return { n: 0.0, m: 0.0, s: 0.0, - strValue: null, - currencySet: new Set(), - push(record) { + strValue: null as string | null, + currencySet: new Set(), + push(record: PivotRecord) { const x = Number(record[attr]); if (Number.isNaN(x)) { this.strValue = - typeof record[attr] === 'string' ? record[attr] : this.strValue; - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + typeof record[attr] === 'string' + ? (record[attr] as string) + : this.strValue; + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } return; } @@ -404,8 +493,13 @@ const baseAggregatorTemplates = { const mNew = this.m + (x - this.m) / this.n; this.s += (x - this.m) * (x - mNew); this.m = mNew; - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } }, value() { @@ -442,21 +536,26 @@ const baseAggregatorTemplates = { }, sumOverSum(formatter = usFmt) { - return function ([num, denom]) { + return function ([num, denom]: string[]) { return function () { return { sumNum: 0, sumDenom: 0, - currencySet: new Set(), - push(record) { + currencySet: new Set(), + push(record: PivotRecord) { if (!Number.isNaN(Number(record[num]))) { - this.sumNum += parseFloat(record[num]); + this.sumNum += parseFloat(String(record[num])); } if (!Number.isNaN(Number(record[denom]))) { - this.sumDenom += parseFloat(record[denom]); + this.sumDenom += parseFloat(String(record[denom])); } - if (record.__currencyColumn && record[record.__currencyColumn]) { - this.currencySet.add(record[record.__currencyColumn]); + if ( + record.__currencyColumn && + record[record.__currencyColumn as string] + ) { + this.currencySet.add( + String(record[record.__currencyColumn as string]), + ); } }, value() { @@ -473,15 +572,19 @@ const baseAggregatorTemplates = { }; }, - fractionOf(wrapped, type = 'total', formatter = usFmtPct) { - return (...x) => - function (data, rowKey, colKey) { + fractionOf( + wrapped: (...args: any[]) => any, + type = 'total', + formatter = usFmtPct, + ) { + return (...x: any[]) => + function (data: any, rowKey: any, colKey: any) { return { selector: { total: [[], []], row: [rowKey, []], col: [[], colKey] }[ type ], inner: wrapped(...Array.from(x || []))(data, rowKey, colKey), - push(record) { + push(record: PivotRecord) { this.inner.push(record); }, format: fmtNonString(formatter), @@ -504,36 +607,40 @@ const baseAggregatorTemplates = { }; }, }; +/* eslint-enable @typescript-eslint/no-explicit-any */ const extendedAggregatorTemplates = { - countUnique(f) { - return baseAggregatorTemplates.uniques(x => x.length, f); + countUnique(f?: Formatter) { + return baseAggregatorTemplates.uniques((x: unknown[]) => x.length, f); }, - listUnique(s, f) { - return baseAggregatorTemplates.uniques(x => x.join(s), f || (x => x)); + listUnique(s: string, f?: Formatter) { + return baseAggregatorTemplates.uniques( + (x: unknown[]) => x.join(s), + f || (((x: unknown) => x) as unknown as Formatter), + ); }, - max(f) { + max(f?: Formatter) { return baseAggregatorTemplates.extremes('max', f); }, - min(f) { + min(f?: Formatter) { return baseAggregatorTemplates.extremes('min', f); }, - first(f) { + first(f?: Formatter) { return baseAggregatorTemplates.extremes('first', f); }, - last(f) { + last(f?: Formatter) { return baseAggregatorTemplates.extremes('last', f); }, - median(f) { + median(f?: Formatter) { return baseAggregatorTemplates.quantile(0.5, f); }, - average(f) { + average(f?: Formatter) { return baseAggregatorTemplates.runningStat('mean', 1, f); }, - var(ddof, f) { + var(ddof: number, f?: Formatter) { return baseAggregatorTemplates.runningStat('var', ddof, f); }, - stdev(ddof, f) { + stdev(ddof: number, f?: Formatter) { return baseAggregatorTemplates.runningStat('stdev', ddof, f); }, }; @@ -603,63 +710,92 @@ const mthNamesEn = [ 'Dec', ]; const dayNamesEn = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; -const zeroPad = number => `0${number}`.substr(-2, 2); // eslint-disable-line no-magic-numbers +const zeroPad = (number: number): string => `0${number}`.substr(-2, 2); // eslint-disable-line no-magic-numbers const derivers = { - bin(col, binWidth) { - return record => record[col] - (record[col] % binWidth); + bin(col: string, binWidth: number) { + return (record: PivotRecord) => + (record[col] as number) - ((record[col] as number) % binWidth); }, dateFormat( - col, - formatString, + col: string, + formatString: string, utcOutput = false, mthNames = mthNamesEn, dayNames = dayNamesEn, ) { const utc = utcOutput ? 'UTC' : ''; - return function (record) { - const date = new Date(Date.parse(record[col])); - if (Number.isNaN(date)) { + return function (record: PivotRecord) { + const date = new Date(Date.parse(String(record[col]))); + if (Number.isNaN(date.getTime())) { return ''; } - return formatString.replace(/%(.)/g, function (m, p) { - switch (p) { - case 'y': - return date[`get${utc}FullYear`](); - case 'm': - return zeroPad(date[`get${utc}Month`]() + 1); - case 'n': - return mthNames[date[`get${utc}Month`]()]; - case 'd': - return zeroPad(date[`get${utc}Date`]()); - case 'w': - return dayNames[date[`get${utc}Day`]()]; - case 'x': - return date[`get${utc}Day`](); - case 'H': - return zeroPad(date[`get${utc}Hours`]()); - case 'M': - return zeroPad(date[`get${utc}Minutes`]()); - case 'S': - return zeroPad(date[`get${utc}Seconds`]()); - default: - return `%${p}`; - } - }); + return formatString.replace( + /%(.)/g, + function (m: string, p: string): string { + switch (p) { + case 'y': + return String(date[`get${utc}FullYear`]()); + case 'm': + return zeroPad(date[`get${utc}Month`]() + 1); + case 'n': + return mthNames[date[`get${utc}Month`]()]; + case 'd': + return zeroPad(date[`get${utc}Date`]()); + case 'w': + return dayNames[date[`get${utc}Day`]()]; + case 'x': + return String(date[`get${utc}Day`]()); + case 'H': + return zeroPad(date[`get${utc}Hours`]()); + case 'M': + return zeroPad(date[`get${utc}Minutes`]()); + case 'S': + return zeroPad(date[`get${utc}Seconds`]()); + default: + return `%${p}`; + } + }, + ); }; }, }; // Given an array of attribute values, convert to a key that // can be used in objects. -const flatKey = attrVals => attrVals.join(String.fromCharCode(0)); +const flatKey = (attrVals: string[]): string => + attrVals.join(String.fromCharCode(0)); /* Data Model class */ class PivotData { - constructor(inputProps = {}, subtotals = {}) { + props: Record; + aggregator: (...args: unknown[]) => Aggregator; + formattedAggregators: + | Record Aggregator>> + | false; + tree: Record>; + rowKeys: string[][]; + colKeys: string[][]; + rowTotals: Record; + colTotals: Record; + allTotal: Aggregator; + subtotals: SubtotalOptions; + sorted: boolean; + + static forEachRecord: ( + input: unknown, + processRecord: (record: PivotRecord) => void, + ) => void; + static defaultProps: Record; + static propTypes: Record; + + constructor( + inputProps: Record = {}, + subtotals: SubtotalOptions = {}, + ) { this.props = { ...PivotData.defaultProps, ...inputProps }; this.processRecord = this.processRecord.bind(this); PropTypes.checkPropTypes( @@ -669,23 +805,38 @@ class PivotData { 'PivotData', ); - this.aggregator = this.props - .aggregatorsFactory(this.props.defaultFormatter) - [this.props.aggregatorName](this.props.vals); - this.formattedAggregators = - this.props.customFormatters && - Object.entries(this.props.customFormatters).reduce( - (acc, [key, columnFormatter]) => { - acc[key] = {}; - Object.entries(columnFormatter).forEach(([column, formatter]) => { - acc[key][column] = this.props - .aggregatorsFactory(formatter) - [this.props.aggregatorName](this.props.vals); - }); - return acc; - }, - {}, - ); + const aggregatorsFactory = this.props.aggregatorsFactory as ( + fmt: unknown, + ) => Record (...args: unknown[]) => Aggregator>; + const aggregatorName = this.props.aggregatorName as string; + const vals = this.props.vals as string[]; + this.aggregator = aggregatorsFactory(this.props.defaultFormatter)[ + aggregatorName + ](vals); + this.formattedAggregators = this.props.customFormatters + ? Object.entries( + this.props.customFormatters as Record< + string, + Record + >, + ).reduce( + ( + acc: Record< + string, + Record Aggregator> + >, + [key, columnFormatter], + ) => { + acc[key] = {}; + Object.entries(columnFormatter).forEach(([column, formatter]) => { + acc[key][column] = + aggregatorsFactory(formatter)[aggregatorName](vals); + }); + return acc; + }, + {}, + ) + : false; this.tree = {}; this.rowKeys = []; this.colKeys = []; @@ -699,29 +850,36 @@ class PivotData { PivotData.forEachRecord(this.props.data, this.processRecord); } - getFormattedAggregator(record, totalsKeys) { + getFormattedAggregator(record: PivotRecord, totalsKeys?: string[]) { if (!this.formattedAggregators) { return this.aggregator; } + const fmtAggs = this.formattedAggregators; const [groupName, groupValue] = Object.entries(record).find( - ([name, value]) => - this.formattedAggregators[name] && - this.formattedAggregators[name][value], + ([name, value]) => fmtAggs[name] && fmtAggs[name][String(value)], ) || []; if ( !groupName || !groupValue || - (totalsKeys && !totalsKeys.includes(groupValue)) + (totalsKeys && !totalsKeys.includes(String(groupValue))) ) { return this.aggregator; } - return this.formattedAggregators[groupName][groupValue] || this.aggregator; + return fmtAggs[groupName][String(groupValue)] || this.aggregator; } - arrSort(attrs, partialOnTop, reverse = false) { - const sortersArr = attrs.map(a => getSort(this.props.sorters, a)); - return function (a, b) { + arrSort(attrs: string[], partialOnTop: boolean | undefined, reverse = false) { + const sortersArr = attrs.map(a => + getSort( + this.props.sorters as + | ((attr: string) => SortFunction | undefined) + | Record + | null, + a, + ), + ); + return function (a: string[], b: string[]) { const limit = Math.min(a.length, b.length); for (let i = 0; i < limit; i += 1) { const sorter = sortersArr[i]; @@ -734,14 +892,16 @@ class PivotData { }; } - sortKeys() { + sortKeys(): void { if (!this.sorted) { this.sorted = true; - const v = (r, c) => this.getAggregator(r, c).value(); + const rows = this.props.rows as string[]; + const cols = this.props.cols as string[]; + const v = (r: string[], c: string[]) => this.getAggregator(r, c).value(); switch (this.props.rowOrder) { case 'key_z_to_a': this.rowKeys.sort( - this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop, true), + this.arrSort(rows, this.subtotals.rowPartialOnTop, true), ); break; case 'value_a_to_z': @@ -751,14 +911,12 @@ class PivotData { this.rowKeys.sort((a, b) => -naturalSort(v(a, []), v(b, []))); break; default: - this.rowKeys.sort( - this.arrSort(this.props.rows, this.subtotals.rowPartialOnTop), - ); + this.rowKeys.sort(this.arrSort(rows, this.subtotals.rowPartialOnTop)); } switch (this.props.colOrder) { case 'key_z_to_a': this.colKeys.sort( - this.arrSort(this.props.cols, this.subtotals.colPartialOnTop, true), + this.arrSort(cols, this.subtotals.colPartialOnTop, true), ); break; case 'value_a_to_z': @@ -768,32 +926,30 @@ class PivotData { this.colKeys.sort((a, b) => -naturalSort(v([], a), v([], b))); break; default: - this.colKeys.sort( - this.arrSort(this.props.cols, this.subtotals.colPartialOnTop), - ); + this.colKeys.sort(this.arrSort(cols, this.subtotals.colPartialOnTop)); } } } - getColKeys() { + getColKeys(): string[][] { this.sortKeys(); return this.colKeys; } - getRowKeys() { + getRowKeys(): string[][] { this.sortKeys(); return this.rowKeys; } - processRecord(record) { + processRecord(record: PivotRecord): void { // this code is called in a tight loop - const colKey = []; - const rowKey = []; - this.props.cols.forEach(col => { - colKey.push(col in record ? record[col] : 'null'); + const colKey: string[] = []; + const rowKey: string[] = []; + (this.props.cols as string[]).forEach((col: string) => { + colKey.push(col in record ? String(record[col]) : 'null'); }); - this.props.rows.forEach(row => { - rowKey.push(row in record ? record[row] : 'null'); + (this.props.rows as string[]).forEach((row: string) => { + rowKey.push(row in record ? String(record[row]) : 'null'); }); this.allTotal.push(record); @@ -860,7 +1016,7 @@ class PivotData { } } - getAggregator(rowKey, colKey) { + getAggregator(rowKey: string[], colKey: string[]): Aggregator { let agg; const flatRowKey = flatKey(rowKey); const flatColKey = flatKey(colKey); @@ -887,7 +1043,10 @@ class PivotData { } // can handle arrays or jQuery selections of tables -PivotData.forEachRecord = function (input, processRecord) { +PivotData.forEachRecord = function ( + input: unknown, + processRecord: (record: PivotRecord) => void, +) { if (Array.isArray(input)) { // array of objects return input.map(record => processRecord(record)); @@ -933,6 +1092,14 @@ PivotData.propTypes = { ]), }; +export type { + SortFunction, + Formatter, + PivotRecord, + Aggregator, + SubtotalOptions, +}; + export { aggregatorTemplates, aggregators, diff --git a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx index 29828720d4d..57bd4fcb7ca 100644 --- a/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx +++ b/superset-frontend/plugins/plugin-chart-pivot-table/test/react-pivottable/tableRenders.test.tsx @@ -17,6 +17,7 @@ * under the License. */ import { TableRenderer } from '../../src/react-pivottable/TableRenderers'; +import type { PivotData } from '../../src/react-pivottable/utilities'; let tableRenderer: TableRenderer; let mockGetAggregatedData: jest.Mock; @@ -102,14 +103,13 @@ const mockGroups = { }, }; -const createMockPivotData = (rowData: Record) => { - return { +const createMockPivotData = (rowData: Record) => + ({ rowKeys: Object.keys(rowData).map(key => key.split('.')), getAggregator: (rowKey: string[], colName: string) => ({ value: () => rowData[rowKey.join('.')], }), - }; -}; + }) as unknown as PivotData; test('should set initial ascending sort when no active sort column', () => { mockGetAggregatedData.mockReturnValue({ @@ -477,7 +477,7 @@ test('create hierarchical structure with subtotal at bottom', () => { }; const pivotData = createMockPivotData(rowData); - const result = tableRenderer.getAggregatedData(pivotData, 'Col1', false); + const result = tableRenderer.getAggregatedData(pivotData, ['Col1'], false); expect(result).toEqual({ A: { @@ -513,7 +513,7 @@ test('create hierarchical structure with subtotal at top', () => { }; const pivotData = createMockPivotData(rowData); - const result = tableRenderer.getAggregatedData(pivotData, 'Col1', true); + const result = tableRenderer.getAggregatedData(pivotData, ['Col1'], true); expect(result).toEqual({ A: { @@ -546,7 +546,7 @@ test('values ​​from the 3rd level of the hierarchy with a subtotal at the bo }; const pivotData = createMockPivotData(rowData); - const result = tableRenderer.getAggregatedData(pivotData, 'Col1', false); + const result = tableRenderer.getAggregatedData(pivotData, ['Col1'], false); expect(result).toEqual({ A: { @@ -574,7 +574,7 @@ test('values ​​from the 3rd level of the hierarchy with a subtotal at the to }; const pivotData = createMockPivotData(rowData); - const result = tableRenderer.getAggregatedData(pivotData, 'Col1', true); + const result = tableRenderer.getAggregatedData(pivotData, ['Col1'], true); expect(result).toEqual({ A: { diff --git a/superset-frontend/plugins/plugin-chart-table/test/sortAlphanumericCaseInsensitive.test.ts b/superset-frontend/plugins/plugin-chart-table/test/sortAlphanumericCaseInsensitive.test.ts index 356596ec210..663bcc0b6e0 100644 --- a/superset-frontend/plugins/plugin-chart-table/test/sortAlphanumericCaseInsensitive.test.ts +++ b/superset-frontend/plugins/plugin-chart-table/test/sortAlphanumericCaseInsensitive.test.ts @@ -80,7 +80,7 @@ const testData = [ describe('sortAlphanumericCaseInsensitive', () => { it('Sort rows', () => { const sorted = [...testData].sort((a, b) => - // @ts-ignore + // @ts-expect-error sortAlphanumericCaseInsensitive(a, b, 'col'), ); diff --git a/superset-frontend/spec/fixtures/mockNativeFilters.ts b/superset-frontend/spec/fixtures/mockNativeFilters.ts index 414935c8b36..a93801ccbdc 100644 --- a/superset-frontend/spec/fixtures/mockNativeFilters.ts +++ b/superset-frontend/spec/fixtures/mockNativeFilters.ts @@ -132,7 +132,7 @@ export const NATIVE_FILTER_ID = 'NATIVE_FILTER-p4LImrSgA'; export const singleNativeFiltersState = { filters: { [NATIVE_FILTER_ID]: { - id: [NATIVE_FILTER_ID], + id: NATIVE_FILTER_ID, name: 'eth', type: 'text', filterType: 'filter_select', diff --git a/superset-frontend/spec/helpers/testing-library.tsx b/superset-frontend/spec/helpers/testing-library.tsx index 151c5d54e75..21abd6a44d0 100644 --- a/superset-frontend/spec/helpers/testing-library.tsx +++ b/superset-frontend/spec/helpers/testing-library.tsx @@ -39,11 +39,11 @@ import { DndProvider } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import reducerIndex from 'spec/helpers/reducerIndex'; import { QueryParamProvider } from 'use-query-params'; +import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; import { configureStore, Store } from '@reduxjs/toolkit'; import { api } from 'src/hooks/apiResources/queryApi'; import userEvent from '@testing-library/user-event'; import { ExtensionsProvider } from 'src/extensions/ExtensionsContext'; -import { ReactRouter5Adapter } from 'use-query-params/adapters/react-router-5'; type Options = Omit & { useRedux?: boolean; diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.test.js b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts similarity index 81% rename from superset-frontend/src/SqlLab/actions/sqlLab.test.js rename to superset-frontend/src/SqlLab/actions/sqlLab.test.ts index ae547da87d3..e1570129cf3 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.test.js +++ b/superset-frontend/src/SqlLab/actions/sqlLab.test.ts @@ -16,16 +16,18 @@ * specific language governing permissions and limitations * under the License. */ -import sinon from 'sinon'; import fetchMock from 'fetch-mock'; import configureMockStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import type { ThunkDispatch } from 'redux-thunk'; +import type { AnyAction } from 'redux'; import { waitFor } from 'spec/helpers/testing-library'; import * as actions from 'src/SqlLab/actions/sqlLab'; +import type { QueryEditor, Table, SqlLabRootState } from 'src/SqlLab/types'; import { LOG_EVENT } from 'src/logger/actions'; import { defaultQueryEditor, - query, + query as queryFixture, initialState, queryId, } from 'src/SqlLab/fixtures'; @@ -33,8 +35,16 @@ import { SupersetClient, isFeatureEnabled } from '@superset-ui/core'; import { ADD_TOAST } from 'src/components/MessageToasts/actions'; import { ToastType } from '../../components/MessageToasts/types'; +const isFeatureEnabledMock = isFeatureEnabled as unknown as jest.Mock; +const query = { ...queryFixture, id: queryId } as any; +// Cast fixture to satisfy SqlLabRootState for getState callbacks in thunk tests +const typedInitialState = initialState as unknown as SqlLabRootState; + +type DispatchExts = ThunkDispatch; + const middlewares = [thunk]; -const mockStore = configureMockStore(middlewares); +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const mockStore = configureMockStore(middlewares); jest.mock('nanoid', () => ({ nanoid: () => 'abcd', @@ -54,7 +64,7 @@ describe('getUpToDateQuery', () => { test('should return the up to date query editor state', () => { const outOfUpdatedQueryEditor = { ...defaultQueryEditor, - schema: null, + schema: null as unknown as string, sql: 'SELECT ...', }; const queryEditor = { @@ -66,7 +76,7 @@ describe('getUpToDateQuery', () => { queryEditors: [queryEditor], unsavedQueryEditor: {}, }, - }; + } as unknown as SqlLabRootState; expect(actions.getUpToDateQuery(state, outOfUpdatedQueryEditor)).toEqual( queryEditor, ); @@ -85,12 +95,12 @@ describe('async actions', () => { name: 'Untitled Query 1', }; - let dispatch; + let dispatch: jest.Mock; const fetchQueryEndpoint = 'glob:*/api/v1/sqllab/results/*'; const runQueryEndpoint = 'glob:*/api/v1/sqllab/execute/'; beforeEach(() => { - dispatch = sinon.spy(); + dispatch = jest.fn(); fetchMock.removeRoute(fetchQueryEndpoint); fetchMock.get( fetchQueryEndpoint, @@ -118,7 +128,7 @@ describe('async actions', () => { const makeRequest = () => { const request = actions.saveQuery(query, queryId); - return request(dispatch, () => initialState); + return request(dispatch, () => typedInitialState, undefined); }; test('posts to the correct url', () => { @@ -134,11 +144,15 @@ describe('async actions', () => { const store = mockStore(initialState); return store.dispatch(actions.saveQuery(query, queryId)).then(() => { const call = fetchMock.callHistory.calls(saveQueryEndpoint)[0]; - const formData = JSON.parse(call.options.body); + const formData = JSON.parse(call.options.body as string); const mappedQueryToServer = actions.convertQueryToServer(query); + // The 'id' field is excluded from the POST payload since it's for new queries + expect(formData.id).toBeUndefined(); Object.keys(mappedQueryToServer).forEach(key => { - expect(formData[key]).toBeDefined(); + if (key !== 'id') { + expect(formData[key]).toBeDefined(); + } }); }); }); @@ -147,7 +161,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.callCount).toBe(2); + expect(dispatch.mock.calls.length).toBe(2); }); }); @@ -155,7 +169,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.args[0][0].type).toBe(actions.QUERY_EDITOR_SAVED); + expect(dispatch.mock.calls[0][0].type).toBe(actions.QUERY_EDITOR_SAVED); }); }); @@ -167,7 +181,7 @@ describe('async actions', () => { actions.QUERY_EDITOR_SAVED, actions.QUERY_EDITOR_SET_TITLE, ]; - return store.dispatch(actions.saveQuery(query)).then(() => { + return store.dispatch(actions.saveQuery(query, queryId)).then(() => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); @@ -193,7 +207,7 @@ describe('async actions', () => { test('posts to the correct url', async () => { const store = mockStore(initialState); - store.dispatch(actions.formatQuery(query, queryId)); + store.dispatch(actions.formatQuery(query as unknown as QueryEditor)); await waitFor(() => expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( 1, @@ -218,7 +232,9 @@ describe('async actions', () => { }; const store = mockStore(state); - store.dispatch(actions.formatQuery(queryEditorWithoutExtras)); + store.dispatch( + actions.formatQuery(queryEditorWithoutExtras as unknown as QueryEditor), + ); await waitFor(() => expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( @@ -227,7 +243,7 @@ describe('async actions', () => { ); const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call.options.body); + const body = JSON.parse(call.options.body as string); expect(body).toEqual({ sql: 'SELECT * FROM table' }); expect(body.database_id).toBeUndefined(); @@ -249,7 +265,9 @@ describe('async actions', () => { }; const store = mockStore(state); - store.dispatch(actions.formatQuery(queryEditorWithDb)); + store.dispatch( + actions.formatQuery(queryEditorWithDb as unknown as QueryEditor), + ); await waitFor(() => expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( @@ -258,7 +276,7 @@ describe('async actions', () => { ); const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call.options.body); + const body = JSON.parse(call.options.body as string); expect(body).toEqual({ sql: 'SELECT * FROM table', @@ -281,7 +299,11 @@ describe('async actions', () => { }; const store = mockStore(state); - store.dispatch(actions.formatQuery(queryEditorWithTemplateString)); + store.dispatch( + actions.formatQuery( + queryEditorWithTemplateString as unknown as QueryEditor, + ), + ); await waitFor(() => expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( @@ -290,7 +312,7 @@ describe('async actions', () => { ); const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call.options.body); + const body = JSON.parse(call.options.body as string); expect(body).toEqual({ sql: 'SELECT * FROM table WHERE id = {{ user_id }}', @@ -314,7 +336,11 @@ describe('async actions', () => { }; const store = mockStore(state); - store.dispatch(actions.formatQuery(queryEditorWithTemplateObject)); + store.dispatch( + actions.formatQuery( + queryEditorWithTemplateObject as unknown as QueryEditor, + ), + ); await waitFor(() => expect(fetchMock.callHistory.calls(formatQueryEndpoint)).toHaveLength( @@ -323,7 +349,7 @@ describe('async actions', () => { ); const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call.options.body); + const body = JSON.parse(call.options.body as string); expect(body).toEqual({ sql: 'SELECT * FROM table WHERE id = {{ user_id }}', @@ -390,7 +416,7 @@ describe('async actions', () => { ); const call = fetchMock.callHistory.calls(formatQueryEndpoint)[0]; - const body = JSON.parse(call.options.body); + const body = JSON.parse(call.options.body as string); expect(body.sql).toBe('SELECT * FROM updated_table'); expect(body.database_id).toBe(10); @@ -402,7 +428,7 @@ describe('async actions', () => { const makeRequest = () => { const store = mockStore(initialState); const request = actions.fetchQueryResults(query); - return request(dispatch, store.getState); + return request(dispatch, store.getState, undefined); }; test('makes the fetch request', () => { @@ -417,17 +443,21 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.args[0][0].type).toBe(actions.REQUEST_QUERY_RESULTS); + expect(dispatch.mock.calls[0][0].type).toBe( + actions.REQUEST_QUERY_RESULTS, + ); }); }); test.skip('parses large number result without losing precision', () => makeRequest().then(() => { expect(fetchMock.callHistory.calls(fetchQueryEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(2); - expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( - mockBigNumber, - ); + expect(dispatch.mock.calls.length).toBe(2); + expect( + dispatch.mock.calls[1][ + dispatch.mock.calls[1].length - 1 + ].results.data.toString(), + ).toBe(mockBigNumber); })); test('calls querySuccess on fetch success', () => { @@ -472,7 +502,7 @@ describe('async actions', () => { describe('runQuery without query params', () => { const makeRequest = () => { const request = actions.runQuery(query); - return request(dispatch, () => initialState); + return request(dispatch, () => typedInitialState, undefined); }; test('makes the fetch request', () => { @@ -487,17 +517,19 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.args[0][0].type).toBe(actions.START_QUERY); + expect(dispatch.mock.calls[0][0].type).toBe(actions.START_QUERY); }); }); test('parses large number result without losing precision', () => makeRequest().then(() => { expect(fetchMock.callHistory.calls(runQueryEndpoint)).toHaveLength(1); - expect(dispatch.callCount).toBe(2); - expect(dispatch.getCall(1).lastArg.results.data.toString()).toBe( - mockBigNumber, - ); + expect(dispatch.mock.calls.length).toBe(2); + expect( + dispatch.mock.calls[1][ + dispatch.mock.calls[1].length - 1 + ].results.data.toString(), + ).toBe(mockBigNumber); })); test('calls querySuccess on fetch success', () => { @@ -507,7 +539,7 @@ describe('async actions', () => { const expectedActionTypes = [actions.START_QUERY, actions.QUERY_SUCCESS]; const { dispatch } = store; const request = actions.runQuery(query); - return request(dispatch, () => initialState).then(() => { + return request(dispatch, () => typedInitialState, undefined).then(() => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); @@ -538,7 +570,7 @@ describe('async actions', () => { ]; const { dispatch } = store; const request = actions.runQuery(query); - return request(dispatch, () => initialState).then(() => { + return request(dispatch, () => typedInitialState, undefined).then(() => { const actions = store.getActions(); expect(actions.map(a => a.type)).toEqual(expectedActionTypes); expect(actions[1].payload.eventData.issue_codes).toEqual([1000, 1001]); @@ -551,18 +583,18 @@ describe('async actions', () => { const { location } = window; beforeAll(() => { - delete window.location; - window.location = new URL('http://localhost/sqllab/?foo=bar'); + delete (window as any).location; + (window as any).location = new URL('http://localhost/sqllab/?foo=bar'); }); afterAll(() => { - delete window.location; + delete (window as any).location; window.location = location; }); const makeRequest = () => { const request = actions.runQuery(query); - return request(dispatch, () => initialState); + return request(dispatch, () => typedInitialState, undefined); }; test('makes the fetch request', async () => { @@ -593,7 +625,7 @@ describe('async actions', () => { }; const store = mockStore(state); const request = actions.reRunQuery(query); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()[0].query.id).toEqual('abcd'); }); }); @@ -609,7 +641,7 @@ describe('async actions', () => { const makeRequest = () => { const request = actions.postStopQuery(baseQuery); - return request(dispatch); + return request(dispatch, () => typedInitialState, undefined); }; test('makes the fetch request', () => { @@ -624,7 +656,7 @@ describe('async actions', () => { expect.assertions(1); return makeRequest().then(() => { - expect(dispatch.getCall(0).args[0].type).toBe(actions.STOP_QUERY); + expect(dispatch.mock.calls[0][0].type).toBe(actions.STOP_QUERY); }); }); @@ -633,7 +665,7 @@ describe('async actions', () => { return makeRequest().then(() => { const call = fetchMock.callHistory.calls(stopQueryEndpoint)[0]; - const body = JSON.parse(call.options.body); + const body = JSON.parse(call.options.body as string); expect(body.client_id).toBe(baseQuery.id); }); }); @@ -677,7 +709,7 @@ describe('async actions', () => { }, ]; const request = actions.cloneQueryToNewTab(query, true); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()).toEqual(expectedActions); }); @@ -721,11 +753,11 @@ describe('async actions', () => { template_parameters: null, }; - const makeRequest = id => { - const request = actions.popSavedQuery(id); + const makeRequest = (id: string | number) => { + const request = actions.popSavedQuery(String(id)); const { dispatch } = store; - return request(dispatch, () => initialState); + return request(dispatch, () => typedInitialState, undefined); }; beforeEach(() => { @@ -740,7 +772,7 @@ describe('async actions', () => { test('calls API endpint with correct params', async () => { supersetClientGetSpy.mockResolvedValue({ json: { result: mockSavedQueryApiResponse }, - }); + } as any); await makeRequest(123); @@ -752,7 +784,7 @@ describe('async actions', () => { test('dispatches addQueryEditor with correct params on successful API call', async () => { supersetClientGetSpy.mockResolvedValue({ json: { result: mockSavedQueryApiResponse }, - }); + } as any); const expectedParams = { name: 'Query 1', @@ -777,7 +809,7 @@ describe('async actions', () => { }); test('should dispatch addDangerToast on API error', async () => { - supersetClientGetSpy.mockResolvedValue(new Error()); + supersetClientGetSpy.mockResolvedValue(new Error() as any); await makeRequest(1); @@ -838,7 +870,7 @@ describe('async actions', () => { schema: defaultQueryEditor.schema, autorun: false, queryLimit: - defaultQueryEditor.queryLimit || + (defaultQueryEditor as any).queryLimit || initialState.common.conf.DEFAULT_SQLLAB_LIMIT, inLocalStorage: true, loaded: true, @@ -846,7 +878,7 @@ describe('async actions', () => { }, ]; const request = actions.addNewQueryEditor(); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()).toEqual(expectedActions); }); }); @@ -971,13 +1003,13 @@ describe('async actions', () => { fetchMock.get(getExtraTableMetadataEndpoint, {}); beforeEach(() => { - isFeatureEnabled.mockImplementation( - feature => feature === 'SQLLAB_BACKEND_PERSISTENCE', + isFeatureEnabledMock.mockImplementation( + (feature: string) => feature === 'SQLLAB_BACKEND_PERSISTENCE', ); }); afterEach(() => { - isFeatureEnabled.mockRestore(); + isFeatureEnabledMock.mockRestore(); }); afterEach(() => fetchMock.clearHistory()); @@ -1146,7 +1178,7 @@ describe('async actions', () => { }, }); const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); - return request(store.dispatch, store.getState).then(() => { + return request(store.dispatch, store.getState, undefined).then(() => { expect(store.getActions()).toEqual(expectedActions); expect( fetchMock.callHistory.calls(updateTabStateEndpoint), @@ -1157,8 +1189,8 @@ describe('async actions', () => { // eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks describe('with backend persistence flag off', () => { test('does not update the tab state in the backend', () => { - isFeatureEnabled.mockImplementation( - feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), + isFeatureEnabledMock.mockImplementation( + (feature: string) => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), ); const store = mockStore({ @@ -1169,13 +1201,13 @@ describe('async actions', () => { }, }); const request = actions.queryEditorSetAndSaveSql(queryEditor, sql); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()).toEqual(expectedActions); expect( fetchMock.callHistory.calls(updateTabStateEndpoint), ).toHaveLength(0); - isFeatureEnabled.mockRestore(); + isFeatureEnabledMock.mockRestore(); }); }); }); @@ -1246,7 +1278,7 @@ describe('async actions', () => { catalogName, schemaName, ); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()[0]).toEqual( expect.objectContaining({ table: expect.objectContaining({ @@ -1284,7 +1316,7 @@ describe('async actions', () => { catalogName, schemaName, ); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()[0]).toEqual( expect.objectContaining({ @@ -1323,7 +1355,7 @@ describe('async actions', () => { catalogName, schemaName, ); - request(store.dispatch, store.getState); + request(store.dispatch, store.getState, undefined); expect(store.getActions()[0]).toEqual( expect.objectContaining({ @@ -1350,8 +1382,12 @@ describe('async actions', () => { const expectedActionTypes = [ actions.MERGE_TABLE, // syncTable ]; - const request = actions.syncTable(query, tableName, schemaName); - return request(store.dispatch, store.getState).then(() => { + const request = actions.syncTable( + query as any, + tableName as any, + schemaName, + ); + return request(store.dispatch, store.getState, undefined).then(() => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); @@ -1414,7 +1450,7 @@ describe('async actions', () => { catalog: catalogName, schema: schemaName, }); - return request(store.dispatch, store.getState).then(() => { + return request(store.dispatch, store.getState, undefined).then(() => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); @@ -1440,7 +1476,7 @@ describe('async actions', () => { }, true, ); - return request(store.dispatch, store.getState).then(() => { + return request(store.dispatch, store.getState, undefined).then(() => { expect(store.getActions().map(a => a.type)).toEqual( expectedActionTypes, ); @@ -1466,18 +1502,20 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.expandTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call.url && - call.url.includes('/tableschemaview/') && - call.url.includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(1); - }); + return store + .dispatch(actions.expandTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(1); + }); }); test('does not call backend when table is not initialized', () => { @@ -1491,19 +1529,21 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.expandTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - // Check all POST calls to find the expanded endpoint - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(0); - }); + return store + .dispatch(actions.expandTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + // Check all POST calls to find the expanded endpoint + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(0); + }); }); test('does not call backend when initialized is undefined', () => { @@ -1517,26 +1557,28 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.expandTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - // Check all POST calls to find the expanded endpoint - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call.url && - call.url.includes('/tableschemaview/') && - call.url.includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(0); - }); + return store + .dispatch(actions.expandTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + // Check all POST calls to find the expanded endpoint + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(0); + }); }); test('does not call backend when feature flag is off', () => { expect.assertions(2); - isFeatureEnabled.mockImplementation( - feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), + isFeatureEnabledMock.mockImplementation( + (feature: string) => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), ); const table = { id: 1, initialized: true }; @@ -1547,20 +1589,22 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.expandTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - // Check all POST calls to find the expanded endpoint - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call.url && - call.url.includes('/tableschemaview/') && - call.url.includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(0); - isFeatureEnabled.mockRestore(); - }); + return store + .dispatch(actions.expandTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + // Check all POST calls to find the expanded endpoint + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(0); + isFeatureEnabledMock.mockRestore(); + }); }); }); @@ -1577,18 +1621,20 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.collapseTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call.url && - call.url.includes('/tableschemaview/') && - call.url.includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(1); - }); + return store + .dispatch(actions.collapseTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(1); + }); }); test('does not call backend when table is not initialized', () => { @@ -1602,18 +1648,20 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.collapseTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call.url && - call.url.includes('/tableschemaview/') && - call.url.includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(0); - }); + return store + .dispatch(actions.collapseTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(0); + }); }); test('does not call backend when initialized is undefined', () => { @@ -1627,25 +1675,27 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.collapseTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call.url && - call.url.includes('/tableschemaview/') && - call.url.includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(0); - }); + return store + .dispatch(actions.collapseTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(0); + }); }); test('does not call backend when feature flag is off', () => { expect.assertions(2); - isFeatureEnabled.mockImplementation( - feature => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), + isFeatureEnabledMock.mockImplementation( + (feature: string) => !(feature === 'SQLLAB_BACKEND_PERSISTENCE'), ); const table = { id: 1, initialized: true }; @@ -1656,19 +1706,21 @@ describe('async actions', () => { table, }, ]; - return store.dispatch(actions.collapseTable(table)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - const expandedCalls = fetchMock.callHistory - .calls() - .filter( - call => - call[0] && - call[0].includes('/tableschemaview/') && - call[0].includes('/expanded'), - ); - expect(expandedCalls).toHaveLength(0); - isFeatureEnabled.mockRestore(); - }); + return store + .dispatch(actions.collapseTable(table as unknown as Table)) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + const expandedCalls = fetchMock.callHistory + .calls() + .filter( + call => + call.url && + call.url.includes('/tableschemaview/') && + call.url.includes('/expanded'), + ); + expect(expandedCalls).toHaveLength(0); + isFeatureEnabledMock.mockRestore(); + }); }); }); @@ -1685,12 +1737,14 @@ describe('async actions', () => { tables: [table], }, ]; - return store.dispatch(actions.removeTables([table])).then(() => { - expect(store.getActions()).toEqual(expectedActions); - expect( - fetchMock.callHistory.calls(updateTableSchemaEndpoint), - ).toHaveLength(1); - }); + return store + .dispatch(actions.removeTables([table] as unknown as Table[])) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(1); + }); }); test('deletes multiple tables and updates the table schema state in the backend', () => { @@ -1707,12 +1761,14 @@ describe('async actions', () => { tables, }, ]; - return store.dispatch(actions.removeTables(tables)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - expect( - fetchMock.callHistory.calls(updateTableSchemaEndpoint), - ).toHaveLength(2); - }); + return store + .dispatch(actions.removeTables(tables as unknown as Table[])) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(2); + }); }); test('only updates the initialized table schema state in the backend', () => { @@ -1726,12 +1782,14 @@ describe('async actions', () => { tables, }, ]; - return store.dispatch(actions.removeTables(tables)).then(() => { - expect(store.getActions()).toEqual(expectedActions); - expect( - fetchMock.callHistory.calls(updateTableSchemaEndpoint), - ).toHaveLength(1); - }); + return store + .dispatch(actions.removeTables(tables as unknown as Table[])) + .then(() => { + expect(store.getActions()).toEqual(expectedActions); + expect( + fetchMock.callHistory.calls(updateTableSchemaEndpoint), + ).toHaveLength(1); + }); }); }); diff --git a/superset-frontend/src/SqlLab/actions/sqlLab.ts b/superset-frontend/src/SqlLab/actions/sqlLab.ts index 99858a99e64..3fe01a03071 100644 --- a/superset-frontend/src/SqlLab/actions/sqlLab.ts +++ b/superset-frontend/src/SqlLab/actions/sqlLab.ts @@ -18,7 +18,8 @@ */ import { nanoid } from 'nanoid'; import rison from 'rison'; -import type { ThunkAction } from 'redux-thunk'; +import type { AnyAction } from 'redux'; +import type { ThunkAction, ThunkDispatch } from 'redux-thunk'; import type { QueryColumn, SupersetError } from '@superset-ui/core'; import { FeatureFlag, @@ -38,6 +39,7 @@ import { addWarningToast as addWarningToastAction, } from 'src/components/MessageToasts/actions'; import { LOG_ACTIONS_SQLLAB_FETCH_FAILED_QUERY } from 'src/logger/LogUtils'; +import type { BootstrapData } from 'src/types/bootstrapTypes'; import getBootstrapData from 'src/utils/getBootstrapData'; import { logEvent } from 'src/logger/actions'; import type { QueryEditor, SqlLabRootState, Table } from '../types'; @@ -223,20 +225,26 @@ export interface SqlLabAction { offline?: boolean; datasource?: unknown; clientId?: string; - result?: { remoteId: number }; + result?: Record; prepend?: boolean; - json?: { result: unknown }; + json?: Record; oldQueryId?: string; newQuery?: { id: string }; } +// Use AnyAction for ThunkAction/ThunkDispatch to maintain compatibility with +// redux-mock-store and standard Redux patterns. SqlLabAction is used for plain +// action creator return types where the shape is known. type SqlLabThunkAction = ThunkAction< R, SqlLabRootState, - unknown, - SqlLabAction + undefined, + AnyAction >; +type AppDispatch = ThunkDispatch; +type GetState = () => SqlLabRootState; + export const addInfoToast = addInfoToastAction; export const addSuccessToast = addSuccessToastAction; export const addDangerToast = addDangerToastAction; @@ -269,10 +277,8 @@ const fieldConverter = export const convertQueryToServer = fieldConverter(queryServerMapping); export const convertQueryToClient = fieldConverter(queryClientMapping); -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function getUpToDateQuery( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rootState: any, + rootState: SqlLabRootState, queryEditor: Partial, key?: string, ): QueryEditor { @@ -287,23 +293,23 @@ export function getUpToDateQuery( } as QueryEditor; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function resetState(data?: Record): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any, getState: any) => { +export function resetState(data?: Record): SqlLabThunkAction { + return (dispatch: AppDispatch, getState: GetState) => { const { common } = getState(); const initialState = getInitialState({ ...getBootstrapData(), common, - ...data, + ...(data as Partial), }); dispatch({ type: RESET_STATE, - sqlLabInitialState: initialState.sqlLab, + sqlLabInitialState: initialState.sqlLab as SqlLabRootState['sqlLab'], }); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - rehydratePersistedState(dispatch, initialState as any); + rehydratePersistedState( + dispatch, + initialState as unknown as SqlLabRootState, + ); }; } @@ -317,10 +323,10 @@ export function setEditorTabLastUpdate(timestamp: number): SqlLabAction { return { type: SET_EDITOR_TAB_LAST_UPDATE, timestamp }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function scheduleQuery(query: Record): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any) => +export function scheduleQuery( + query: Record, +): SqlLabThunkAction> { + return (dispatch: AppDispatch) => SupersetClient.post({ endpoint: '/api/v1/saved_query/', jsonPayload: query, @@ -340,10 +346,10 @@ export function scheduleQuery(query: Record): any { ); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function estimateQueryCost(queryEditor: QueryEditor): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any, getState: any) => { +export function estimateQueryCost( + queryEditor: QueryEditor, +): SqlLabThunkAction> { + return (dispatch: AppDispatch, getState: GetState) => { const { dbId, catalog, schema, sql, selectedText, templateParams } = getUpToDateQuery(getState(), queryEditor); const requestSql = selectedText || sql; @@ -465,14 +471,12 @@ export function requestQueryResults(query: Query): SqlLabAction { return { type: REQUEST_QUERY_RESULTS, query }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function fetchQueryResults( query: Query, displayLimit?: number, timeoutInMs?: number, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +): SqlLabThunkAction> { + return function (dispatch: AppDispatch, getState: GetState) { const { SQLLAB_QUERY_RESULT_TIMEOUT } = getState().common?.conf ?? {}; dispatch(requestQueryResults(query)); @@ -487,9 +491,9 @@ export function fetchQueryResults( parseMethod: 'json-bigint', ...(timeout && { timeout, signal: controller.signal }), }) - .then(({ json }) => - dispatch(querySuccess(query, json as SqlExecuteResponse)), - ) + .then(({ json }) => { + dispatch(querySuccess(query, json as SqlExecuteResponse)); + }) .catch(response => { controller.abort(); getClientErrorObject(response).then(error => { @@ -506,10 +510,11 @@ export function fetchQueryResults( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function runQuery(query: Query, runPreviewOnly?: boolean): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function runQuery( + query: Query, + runPreviewOnly?: boolean, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { dispatch(startQuery(query, runPreviewOnly)); const postPayload = { client_id: query.id, @@ -556,7 +561,6 @@ export function runQuery(query: Query, runPreviewOnly?: boolean): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function runQueryFromSqlEditor( database: Database | null, queryEditor: QueryEditor, @@ -564,9 +568,8 @@ export function runQueryFromSqlEditor( tempTable?: string, ctas?: boolean, ctasMethod?: string, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +): SqlLabThunkAction { + return function (dispatch: AppDispatch, getState: GetState) { const qe = getUpToDateQuery(getState(), queryEditor, queryEditor.id); const query: Query = { id: nanoid(11), @@ -589,19 +592,17 @@ export function runQueryFromSqlEditor( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function reRunQuery(query: Query): any { +export function reRunQuery(query: Query): SqlLabThunkAction { // run Query with a new id - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { + return function (dispatch: AppDispatch) { dispatch(runQuery({ ...query, id: nanoid(11) })); }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function postStopQuery(query: Query): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function postStopQuery( + query: Query, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { return SupersetClient.post({ endpoint: '/api/v1/query/stop', body: JSON.stringify({ client_id: query.id }), @@ -620,10 +621,8 @@ export function setDatabases(databases: Database[]): SqlLabAction { function migrateTable( table: Table, queryEditorId: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dispatch: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): Promise { + dispatch: AppDispatch, +): Promise { return SupersetClient.post({ endpoint: encodeURI('/tableschemaview/'), postPayload: { table: { ...table, queryEditorId } }, @@ -651,10 +650,8 @@ function migrateTable( function migrateQuery( queryId: string, queryEditorId: string, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - dispatch: any, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -): Promise { + dispatch: AppDispatch, +): Promise { return SupersetClient.post({ endpoint: encodeURI(`/tabstateview/${queryEditorId}/migrate_query`), postPayload: { queryId }, @@ -681,18 +678,17 @@ function migrateQuery( * stored in local storage will also be synchronized to the backend * through syncQueryEditor. */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function syncQueryEditor(queryEditor: QueryEditor): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +export function syncQueryEditor( + queryEditor: QueryEditor, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch, getState: GetState) { const { tables, queries } = getState().sqlLab; const localStorageTables = tables.filter( (table: Table) => table.inLocalStorage && table.queryEditorId === queryEditor.id, ); const localStorageQueries = Object.values(queries).filter( - (query: Query) => - query.inLocalStorage && query.sqlEditorId === queryEditor.id, + query => query.inLocalStorage && query.sqlEditorId === queryEditor.id, ); return SupersetClient.post({ endpoint: '/tabstateview/', @@ -748,20 +744,18 @@ export function addQueryEditor( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function addNewQueryEditor(): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +export function addNewQueryEditor(): SqlLabThunkAction { + return function (dispatch: AppDispatch, getState: GetState) { const { sqlLab: { queryEditors, tabHistory, unsavedQueryEditor, databases }, common, } = getState(); - const defaultDbId = common.conf.SQLLAB_DEFAULT_DBID; + const defaultDbId = common.conf.SQLLAB_DEFAULT_DBID as number | undefined; const activeQueryEditor = queryEditors.find( (qe: QueryEditor) => qe.id === tabHistory[tabHistory.length - 1], ); const dbIds = Object.values(databases).map( - (database: Database) => database.id, + (database: { id: number }) => database.id, ); const firstDbId = dbIds.length > 0 ? Math.min(...dbIds) : undefined; const { dbId, catalog, schema, queryLimit, autorun } = { @@ -797,16 +791,16 @@ export function addNewQueryEditor(): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function cloneQueryToNewTab(query: Query, autorun: boolean): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +export function cloneQueryToNewTab( + query: Query, + autorun: boolean, +): SqlLabThunkAction { + return function (dispatch: AppDispatch, getState: GetState) { const state = getState(); const { queryEditors, unsavedQueryEditor, tabHistory } = state.sqlLab; const sourceQueryEditor = { ...queryEditors.find( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (qe: any) => qe.id === tabHistory[tabHistory.length - 1], + (qe: QueryEditor) => qe.id === tabHistory[tabHistory.length - 1], ), ...(tabHistory[tabHistory.length - 1] === unsavedQueryEditor.id && unsavedQueryEditor), @@ -819,7 +813,6 @@ export function cloneQueryToNewTab(query: Query, autorun: boolean): any { autorun, sql: query.sql, queryLimit: sourceQueryEditor.queryLimit, - maxRow: sourceQueryEditor.maxRow, templateParams: sourceQueryEditor.templateParams, }; return dispatch(addQueryEditor(queryEditor)); @@ -840,16 +833,13 @@ export function setActiveQueryEditor(queryEditor: QueryEditor): SqlLabAction { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function switchQueryEditor(goBackward = false): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +export function switchQueryEditor(goBackward = false): SqlLabThunkAction { + return function (dispatch: AppDispatch, getState: GetState) { const { sqlLab } = getState(); const { queryEditors, tabHistory } = sqlLab; const qeid = tabHistory[tabHistory.length - 1]; const currentIndex = queryEditors.findIndex( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (qe: any) => qe.id === qeid, + (qe: QueryEditor) => qe.id === qeid, ); const nextIndex = goBackward ? currentIndex - 1 + queryEditors.length @@ -915,13 +905,11 @@ export function setTables(tableSchemas: TableSchema[]): SqlLabAction { return { type: SET_TABLES, tables }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function fetchQueryEditor( queryEditor: QueryEditor, displayLimit: number, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +): SqlLabThunkAction { + return function (dispatch: AppDispatch) { const queryEditorId = queryEditor.tabViewId ?? queryEditor.id; SupersetClient.get({ endpoint: encodeURI(`/tabstateview/${queryEditorId}`), @@ -981,13 +969,12 @@ export function removeQueryEditor(queryEditor: QueryEditor): SqlLabAction { return { type: REMOVE_QUERY_EDITOR, queryEditor }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function removeAllOtherQueryEditors(queryEditor: QueryEditor): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +export function removeAllOtherQueryEditors( + queryEditor: QueryEditor, +): SqlLabThunkAction { + return function (dispatch: AppDispatch, getState: GetState) { const { sqlLab } = getState(); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sqlLab.queryEditors?.forEach((otherQueryEditor: any) => { + sqlLab.queryEditors?.forEach((otherQueryEditor: QueryEditor) => { if (otherQueryEditor.id !== queryEditor.id) { dispatch(removeQueryEditor(otherQueryEditor)); } @@ -995,10 +982,8 @@ export function removeAllOtherQueryEditors(queryEditor: QueryEditor): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function removeQuery(query: Query): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function removeQuery(query: Query): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { const queryEditorId = query.sqlEditorId ?? query.id; const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) ? SupersetClient.delete({ @@ -1070,16 +1055,15 @@ export function queryEditorSetTitle( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function saveQuery(query: Partial, clientId: string): any { - // eslint-disable-next-line @typescript-eslint/no-unused-vars +export function saveQuery( + query: Partial, + clientId: string, +): SqlLabThunkAction | void>> { const { id: _id, ...payload } = convertQueryToServer( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - query as any, + query as Record, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any) => + return (dispatch: AppDispatch) => SupersetClient.post({ endpoint: '/api/v1/saved_query/', jsonPayload: convertQueryToServer(payload as Record), @@ -1103,42 +1087,38 @@ export function saveQuery(query: Partial, clientId: string): any { ); } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export const addSavedQueryToTabState = - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (queryEditor: QueryEditor, savedQuery: { remoteId: string }): any => - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (dispatch: any) => { - const queryEditorId = queryEditor.tabViewId ?? queryEditor.id; - const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) - ? SupersetClient.put({ - endpoint: `/tabstateview/${queryEditorId}`, - postPayload: { saved_query_id: savedQuery.remoteId }, - }) - : Promise.resolve(); - - return sync - .catch(() => { - dispatch(addDangerToast(t('Your query was not properly saved'))); + ( + queryEditor: QueryEditor, + savedQuery: { remoteId: string }, + ): SqlLabThunkAction> => + (dispatch: AppDispatch) => { + const queryEditorId = queryEditor.tabViewId ?? queryEditor.id; + const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) + ? SupersetClient.put({ + endpoint: `/tabstateview/${queryEditorId}`, + postPayload: { saved_query_id: savedQuery.remoteId }, }) - .then(() => { - dispatch(addSuccessToast(t('Your query was saved'))); - }); - }; + : Promise.resolve(); + + return sync + .catch(() => { + dispatch(addDangerToast(t('Your query was not properly saved'))); + }) + .then(() => { + dispatch(addSuccessToast(t('Your query was saved'))); + }); + }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function updateSavedQuery( query: Partial, clientId: string, -): any { - // eslint-disable-next-line @typescript-eslint/no-unused-vars +): SqlLabThunkAction> { const { id: _id, ...payload } = convertQueryToServer( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - query as any, + query as Record, ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any) => + return (dispatch: AppDispatch) => SupersetClient.put({ endpoint: `/api/v1/saved_query/${query.remoteId}`, jsonPayload: convertQueryToServer(payload as Record), @@ -1153,7 +1133,9 @@ export function updateSavedQuery( console.error(message, e); dispatch(addDangerToast(message)); }) - .then(() => dispatch(updateQueryEditor(query))); + .then(() => { + dispatch(updateQueryEditor(query)); + }); } export function queryEditorSetSql( @@ -1171,14 +1153,12 @@ export function queryEditorSetCursorPosition( return { type: QUERY_EDITOR_SET_CURSOR_POSITION, queryEditor, position }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function queryEditorSetAndSaveSql( targetQueryEditor: Partial, sql: string, queryId?: string, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +): SqlLabThunkAction> { + return function (dispatch: AppDispatch, getState: GetState) { const queryEditor = getUpToDateQuery(getState(), targetQueryEditor); // saved query and set tab state use this action dispatch(queryEditorSetSql(queryEditor, sql, queryId)); @@ -1203,10 +1183,10 @@ export function queryEditorSetAndSaveSql( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function formatQuery(queryEditor: QueryEditor): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +export function formatQuery( + queryEditor: QueryEditor, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch, getState: GetState) { const { sql, dbId, templateParams } = getUpToDateQuery( getState(), queryEditor, @@ -1276,16 +1256,14 @@ export function mergeTable( return { type: MERGE_TABLE, table, query, prepend }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function addTable( queryEditor: Partial, tableName: string, catalogName: string | null, schemaName: string, expanded = true, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +): SqlLabThunkAction { + return function (dispatch: AppDispatch, getState: GetState) { const { dbId } = getUpToDateQuery(getState(), queryEditor, queryEditor.id); const table = { dbId, @@ -1315,13 +1293,11 @@ interface NewTable { previewQueryId?: string; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function runTablePreviewQuery( newTable: NewTable, runPreviewOnly?: boolean, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any, getState: any) { +): SqlLabThunkAction> { + return function (dispatch: AppDispatch, getState: GetState) { const { sqlLab: { databases }, } = getState(); @@ -1375,14 +1351,12 @@ interface TableMetaData { indexes?: unknown[]; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any export function syncTable( table: Table, tableMetadata: TableMetaData, finalQueryEditorId?: string, -): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { const finalTable = { ...table, queryEditorId: finalQueryEditorId }; const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) ? SupersetClient.post({ @@ -1422,10 +1396,8 @@ export function changeDataPreviewId( return { type: CHANGE_DATA_PREVIEW_ID, oldQueryId, newQuery }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function reFetchQueryResults(query: Query): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function reFetchQueryResults(query: Query): SqlLabThunkAction { + return function (dispatch: AppDispatch) { const newQuery: Query = { id: nanoid(), dbId: query.dbId, @@ -1443,10 +1415,8 @@ export function reFetchQueryResults(query: Query): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function expandTable(table: Table): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function expandTable(table: Table): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) && table.initialized @@ -1471,10 +1441,10 @@ export function expandTable(table: Table): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function collapseTable(table: Table): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function collapseTable( + table: Table, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) && table.initialized @@ -1499,10 +1469,10 @@ export function collapseTable(table: Table): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function removeTables(tables: Table[]): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function removeTables( + tables: Table[], +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { const tablesToRemove = tables?.filter(Boolean) ?? []; const sync = isFeatureEnabled(FeatureFlag.SqllabBackendPersistence) ? Promise.all( @@ -1554,10 +1524,8 @@ export function persistEditorHeight( }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function popPermalink(key: string): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function popPermalink(key: string): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/${key}` }) .then(({ json }) => dispatch( @@ -1576,10 +1544,10 @@ export function popPermalink(key: string): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function popStoredQuery(urlId: string): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function popStoredQuery( + urlId: string, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { return SupersetClient.get({ endpoint: `/api/v1/sqllab/permalink/kv:${urlId}`, }) @@ -1599,10 +1567,10 @@ export function popStoredQuery(urlId: string): any { .catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY))); }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function popSavedQuery(saveQueryId: string): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function popSavedQuery( + saveQueryId: string, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { return SupersetClient.get({ endpoint: `/api/v1/saved_query/${saveQueryId}`, }) @@ -1626,10 +1594,8 @@ export function popSavedQuery(saveQueryId: string): any { .catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY))); }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function popQuery(queryId: string): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function popQuery(queryId: string): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { return SupersetClient.get({ endpoint: `/api/v1/query/${queryId}`, }) @@ -1648,10 +1614,11 @@ export function popQuery(queryId: string): any { .catch(() => dispatch(addDangerToast(ERR_MSG_CANT_LOAD_QUERY))); }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function popDatasourceQuery(datasourceKey: string, sql?: string): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return function (dispatch: any) { +export function popDatasourceQuery( + datasourceKey: string, + sql?: string, +): SqlLabThunkAction> { + return function (dispatch: AppDispatch) { const QUERY_TEXT = t('Query'); const datasetId = datasourceKey.split('__')[0]; @@ -1699,10 +1666,10 @@ interface VizOptions { templateParams?: string; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDatasource(vizOptions: VizOptions): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any) => { +export function createDatasource( + vizOptions: VizOptions, +): SqlLabThunkAction> { + return (dispatch: AppDispatch) => { dispatch(createDatasourceStarted()); const { dbId, catalog, schema, datasourceName, sql, templateParams } = vizOptions; @@ -1740,10 +1707,10 @@ export function createDatasource(vizOptions: VizOptions): any { }; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createCtasDatasource(vizOptions: Record): any { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (dispatch: any) => { +export function createCtasDatasource( + vizOptions: Record, +): SqlLabThunkAction> { + return (dispatch: AppDispatch) => { dispatch(createDatasourceStarted()); return SupersetClient.post({ endpoint: '/api/v1/dataset/get_or_create/', diff --git a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts index e54322cca18..9aa7dcdacfd 100644 --- a/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts +++ b/superset-frontend/src/SqlLab/components/EditorWrapper/useKeywords.test.ts @@ -230,14 +230,14 @@ test('returns column keywords among selected tables', async () => { }, ), ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any storeWithSqlLab.dispatch( addTable( - // eslint-disable-next-line @typescript-eslint/no-explicit-any { id: expectQueryEditorId } as any, expectTable, expectCatalog, expectSchema, - ), + ) as any, ); }); @@ -275,14 +275,14 @@ test('returns column keywords among selected tables', async () => { ); act(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any storeWithSqlLab.dispatch( addTable( - // eslint-disable-next-line @typescript-eslint/no-explicit-any { id: expectQueryEditorId } as any, unexpectedTable, expectCatalog, expectSchema, - ), + ) as any, ); }); diff --git a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx index d0436115325..0439157b055 100644 --- a/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx +++ b/superset-frontend/src/SqlLab/components/QueryAutoRefresh/QueryAutoRefresh.test.tsx @@ -72,13 +72,13 @@ describe('QueryAutoRefresh', () => { }); test('isQueryRunning returns false for invalid query', () => { - // @ts-ignore + // @ts-expect-error expect(isQueryRunning(null)).toBe(false); - // @ts-ignore + // @ts-expect-error expect(isQueryRunning(undefined)).toBe(false); - // @ts-ignore + // @ts-expect-error expect(isQueryRunning('I Should Be An Object')).toBe(false); - // @ts-ignore + // @ts-expect-error expect(isQueryRunning({ state: { badFormat: true } })).toBe(false); }); @@ -91,20 +91,19 @@ describe('QueryAutoRefresh', () => { }); test('shouldCheckForQueries is false for invalid inputs', () => { - // @ts-ignore + // @ts-expect-error expect(shouldCheckForQueries(null)).toBe(false); - // @ts-ignore + // @ts-expect-error expect(shouldCheckForQueries(undefined)).toBe(false); expect( - // @ts-ignore shouldCheckForQueries({ - // @ts-ignore + // @ts-expect-error '1234': null, - // @ts-ignore + // @ts-expect-error '23425': 'hello world', - // @ts-ignore + // @ts-expect-error '345': [], - // @ts-ignore + // @ts-expect-error '57346': undefined, }), ).toBe(false); @@ -174,7 +173,7 @@ describe('QueryAutoRefresh', () => { render( , diff --git a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx index be5a1e22fc3..20871fc029f 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/ResultSet.test.tsx @@ -286,7 +286,6 @@ describe('ResultSet', () => { store, ); - // @ts-ignore rerender(); expect(store.getActions()).toHaveLength(1); expect(store.getActions()[0].query.results).toEqual(cachedQuery.results); diff --git a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx index 3b751881031..4838752f221 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet/index.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet/index.tsx @@ -298,7 +298,7 @@ const ResultSet = ({ const force = false; const includeAppRoot = openInNewWindow; const url = mountExploreUrl( - null, + 'base', { [URL_PARAMS.formDataKey.name]: key, }, @@ -428,7 +428,10 @@ const ResultSet = ({ )} {canExportData && ( c.column_name), + )} wrapped={false} copyNode={