mirror of
https://github.com/apache/superset.git
synced 2026-07-18 20:55:47 +00:00
Compare commits
4 Commits
msyavuz/ch
...
chore/remo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ff88f0dcdd | ||
|
|
b83e0a8400 | ||
|
|
bd544b8bcb | ||
|
|
79e09ec1b3 |
20
UPDATING.md
20
UPDATING.md
@@ -24,6 +24,26 @@ assists people when migrating to a new version.
|
||||
|
||||
## Next
|
||||
|
||||
### Removed deck.gl JavaScript tooltip/data-mutator controls and ENABLE_JAVASCRIPT_CONTROLS
|
||||
|
||||
The `ENABLE_JAVASCRIPT_CONTROLS` feature flag and the deck.gl chart controls it gated
|
||||
(`js_tooltip`, `js_onclick_href`, `js_data_mutator`, and the GeoJSON layer's label/icon
|
||||
JavaScript-mode generators) have been removed. These controls let users write arbitrary
|
||||
JavaScript, sandboxed via Node's `vm` module, to customize deck.gl tooltips, click
|
||||
behavior, and data transforms; the flag defaulted off and the feature saw negligible use.
|
||||
|
||||
The deck.gl "Extra data for JS" control (`js_columns`) has also been removed. It only
|
||||
ever existed to feed extra columns into the JavaScript controls above; deck.gl's
|
||||
built-in field-based tooltips and cross-filtering already pull in any columns they need
|
||||
via `tooltip_contents`/`cross_filter_column`, so this control had no remaining purpose.
|
||||
Any chart layer whose "Advanced" control panel section only contained this control no
|
||||
longer has an "Advanced" section.
|
||||
|
||||
Any saved charts with these fields set will simply ignore them going forward and fall back
|
||||
to deck.gl's built-in field-based tooltips (`tooltip_contents`/`tooltip_template`) and
|
||||
native click/cross-filter behavior. No migration is required; the fields are dropped
|
||||
silently on next save.
|
||||
|
||||
### Owners, dashboard roles, and RLS roles replaced by Subjects
|
||||
|
||||
Superset now uses subject-based access assignments for dashboards, charts, datasets,
|
||||
|
||||
@@ -135,7 +135,7 @@ Superset sends an HTTP POST with `Content-Type: application/json`:
|
||||
}
|
||||
```
|
||||
|
||||
When a report includes file attachments (CSV, Excel, PDF, or PNG screenshots), the request is sent as `multipart/form-data` instead. In that case, each top-level payload field (`name`, `text`, `description`, `url`) becomes its own form field, and nested structures like `header` are serialized as a JSON-encoded string in their own field. Every attachment is added as a repeated form field named `files`:
|
||||
When a report includes file attachments (CSV, PDF, or PNG screenshots), the request is sent as `multipart/form-data` instead. In that case, each top-level payload field (`name`, `text`, `description`, `url`) becomes its own form field, and nested structures like `header` are serialized as a JSON-encoded string in their own field. Every attachment is added as a repeated form field named `files`:
|
||||
|
||||
```
|
||||
POST /webhook HTTP/1.1
|
||||
|
||||
6
docs/static/feature-flags.json
vendored
6
docs/static/feature-flags.json
vendored
@@ -405,12 +405,6 @@
|
||||
"default": true,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Enable drill-to-detail functionality in charts"
|
||||
},
|
||||
{
|
||||
"name": "ENABLE_JAVASCRIPT_CONTROLS",
|
||||
"default": false,
|
||||
"lifecycle": "deprecated",
|
||||
"description": "Allow JavaScript in chart controls. WARNING: XSS security vulnerability!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -96,26 +96,6 @@ describe("guest token refresh", () => {
|
||||
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("falls back to default timing for a completely malformed token", () => {
|
||||
const timing = getGuestTokenRefreshTiming("not-a-jwt");
|
||||
|
||||
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("falls back to default timing for an empty string token", () => {
|
||||
const timing = getGuestTokenRefreshTiming("");
|
||||
|
||||
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("falls back to default timing for a token with invalid base64 payload", () => {
|
||||
const timing = getGuestTokenRefreshTiming(
|
||||
"header.!!!invalid-base64!!!.signature",
|
||||
);
|
||||
|
||||
expect(timing).toBe(DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("exposes a positive retry delay for failed token refreshes", () => {
|
||||
// The refresh loop reschedules itself after this delay when a fetch
|
||||
// fails or times out, so it must be a sane positive value.
|
||||
|
||||
@@ -25,20 +25,16 @@ export const DEFAULT_TOKEN_REFRESH_RETRY_MS = 10000; // wait before retrying a f
|
||||
|
||||
// when do we refresh the guest token?
|
||||
export function getGuestTokenRefreshTiming(currentGuestToken: string) {
|
||||
try {
|
||||
const parsedJwt = jwtDecode<Record<string, any>>(currentGuestToken);
|
||||
// if exp is int, it is in seconds, but Date() takes milliseconds
|
||||
const exp = new Date(
|
||||
/[^0-9\.]/g.test(parsedJwt.exp)
|
||||
? parsedJwt.exp
|
||||
: parseFloat(parsedJwt.exp) * 1000,
|
||||
);
|
||||
const isValidDate = exp.toString() !== "Invalid Date";
|
||||
const ttl = isValidDate
|
||||
? Math.max(MIN_REFRESH_WAIT_MS, exp.getTime() - Date.now())
|
||||
: DEFAULT_TOKEN_EXP_MS;
|
||||
return ttl - REFRESH_TIMING_BUFFER_MS;
|
||||
} catch {
|
||||
return DEFAULT_TOKEN_EXP_MS - REFRESH_TIMING_BUFFER_MS;
|
||||
}
|
||||
const parsedJwt = jwtDecode<Record<string, any>>(currentGuestToken);
|
||||
// if exp is int, it is in seconds, but Date() takes milliseconds
|
||||
const exp = new Date(
|
||||
/[^0-9\.]/g.test(parsedJwt.exp)
|
||||
? parsedJwt.exp
|
||||
: parseFloat(parsedJwt.exp) * 1000,
|
||||
);
|
||||
const isValidDate = exp.toString() !== "Invalid Date";
|
||||
const ttl = isValidDate
|
||||
? Math.max(MIN_REFRESH_WAIT_MS, exp.getTime() - Date.now())
|
||||
: DEFAULT_TOKEN_EXP_MS;
|
||||
return ttl - REFRESH_TIMING_BUFFER_MS;
|
||||
}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default {
|
||||
},
|
||||
fallback: {
|
||||
tty: false,
|
||||
vm: require.resolve('vm-browserify')
|
||||
vm: false,
|
||||
}
|
||||
},
|
||||
plugins: [...config.plugins, ...filteredPlugins],
|
||||
|
||||
@@ -17,14 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useRef,
|
||||
ReactNode,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import { useState, useEffect, useCallback, useRef, ReactNode } from 'react';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import {
|
||||
SupersetClient,
|
||||
@@ -61,7 +54,7 @@ export default function VerifyCORS({
|
||||
host,
|
||||
method,
|
||||
postPayload,
|
||||
}: Props): ReactElement {
|
||||
}: Props): JSX.Element {
|
||||
const [didVerify, setDidVerify] = useState(false);
|
||||
const [error, setError] = useState<Error | SupersetApiError | undefined>(
|
||||
undefined,
|
||||
|
||||
@@ -75,7 +75,7 @@ module.exports = {
|
||||
// @ant-design/colors and @ant-design/fast-color are allowed through because
|
||||
// @ant-design/icons >= 6.3 deep-imports the ESM build of @ant-design/colors
|
||||
// from its CJS output, so babel-jest must transform those files.
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|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/*.|@x0k/.*|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-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact|react-resize-detector|react-markdown|vfile|vfile-message|trim-lines|trough|is-plain-obj)',
|
||||
'node_modules/(?!@ant-design/(colors|fast-color)|@formatjs/.*|d3-(array|interpolate|color|time|scale|time-format|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/*.|@x0k/.*|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-.*|(?!geostyler)lodash|react-error-boundary|react-json-tree|react-base16-styling|lodash-es|rbush|quickselect|react-diff-viewer-continued|storybook/*.|json-stringify-pretty-compact)',
|
||||
],
|
||||
preset: 'ts-jest',
|
||||
transform: {
|
||||
|
||||
700
superset-frontend/package-lock.json
generated
700
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -205,19 +205,20 @@
|
||||
"ol": "^10.9.0",
|
||||
"query-string": "9.4.1",
|
||||
"re-resizable": "^6.11.2",
|
||||
"react": "^19.2.0",
|
||||
"react": "^18.3.0",
|
||||
"react-arborist": "^3.13.2",
|
||||
"react-checkbox-tree": "^2.0.1",
|
||||
"react-checkbox-tree": "^1.8.0",
|
||||
"react-diff-viewer-continued": "^4.2.2",
|
||||
"react-dnd": "^11.1.3",
|
||||
"react-dnd-html5-backend": "^11.1.3",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-google-recaptcha": "^3.1.0",
|
||||
"react-intersection-observer": "^10.0.3",
|
||||
"react-json-tree": "^0.20.0",
|
||||
"react-lines-ellipsis": "^0.16.1",
|
||||
"react-loadable": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-resize-detector": "^12.3.0",
|
||||
"react-resize-detector": "^9.1.1",
|
||||
"react-reverse-portal": "^2.3.0",
|
||||
"react-router-dom": "^5.3.4",
|
||||
"react-search-input": "^0.11.3",
|
||||
@@ -262,7 +263,7 @@
|
||||
"@babel/types": "^7.29.7",
|
||||
"@emotion/babel-plugin": "^11.13.5",
|
||||
"@emotion/jest": "^11.14.2",
|
||||
"@formatjs/intl-durationformat": "^0.10.16",
|
||||
"@formatjs/intl-durationformat": "^0.10.15",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.1",
|
||||
"@playwright/test": "^1.61.1",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.6.2",
|
||||
@@ -274,9 +275,9 @@
|
||||
"@swc/core": "^1.15.43",
|
||||
"@swc/plugin-emotion": "^14.15.0",
|
||||
"@swc/plugin-transform-imports": "^12.5.0",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "^12.8.3",
|
||||
"@types/content-disposition": "^0.5.9",
|
||||
"@types/dom-to-image": "^2.6.7",
|
||||
@@ -287,9 +288,10 @@
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mousetrap": "^1.6.15",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.2.0",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"@types/react": "^18.3.0",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-loadable": "^5.5.11",
|
||||
"@types/react-redux": "^7.1.10",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-transition-group": "^4.4.12",
|
||||
"@types/react-window": "^1.8.8",
|
||||
@@ -365,7 +367,6 @@
|
||||
"tsx": "^4.23.0",
|
||||
"typescript": "5.4.5",
|
||||
"unzipper": "^0.12.5",
|
||||
"vm-browserify": "^1.1.2",
|
||||
"wait-on": "^9.0.10",
|
||||
"webpack": "^5.108.4",
|
||||
"webpack-bundle-analyzer": "^5.3.0",
|
||||
@@ -379,7 +380,7 @@
|
||||
"ace-builds": "^1.41.0",
|
||||
"core-js": "^3.38.1",
|
||||
"handlebars": "^4.7.8",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-ace": "^10.1.0",
|
||||
"regenerator-runtime": "^0.14.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -387,53 +388,9 @@
|
||||
"npm": "^11.13.0"
|
||||
},
|
||||
"overrides": {
|
||||
"react-router-dom": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"react-table": {
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"react-redux": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"react-virtualized-auto-sizer": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"@reduxjs/toolkit": {
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"react-ultimate-pagination": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"@visx/axis": {
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"@visx/grid": {
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"@visx/responsive": {
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"@visx/scale": {
|
||||
"react": "^19.2.0"
|
||||
},
|
||||
"@visx/tooltip": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"@visx/xychart": {
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
},
|
||||
"uuid": "$uuid",
|
||||
"@great-expectations/jsonforms-antd-renderers": {
|
||||
"antd": "$antd",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0"
|
||||
"antd": "$antd"
|
||||
},
|
||||
"core-js": "^3.38.1",
|
||||
"dompurify": "^3.4.11",
|
||||
@@ -467,6 +424,7 @@
|
||||
"jest-util": "^30.4.0",
|
||||
"jest-circus": "^30.4.0",
|
||||
"jest-environment-node": "^30.4.0",
|
||||
"typescript-json-schema": "^0.68.0",
|
||||
"@babel/eslint-parser": {
|
||||
"eslint": "$eslint"
|
||||
},
|
||||
|
||||
@@ -89,11 +89,12 @@
|
||||
"typescript": "^5.0.0",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*"
|
||||
},
|
||||
@@ -104,8 +105,9 @@
|
||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||
"@fontsource/inter": "^5.2.6",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*",
|
||||
"lodash": "^4.18.1",
|
||||
"antd": "^6.0.0",
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
"@ant-design/icons": "^5.6.1 || ^6.0.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"ace-builds": "^1.4.14",
|
||||
"brace": "^0.11.1",
|
||||
"memoize-one": "^6.0.0",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -36,7 +36,7 @@ import { SQLPopover } from './SQLPopover';
|
||||
export type ColumnOptionProps = {
|
||||
column: ColumnMeta;
|
||||
showType?: boolean;
|
||||
labelRef?: RefObject<any | null>;
|
||||
labelRef?: RefObject<any>;
|
||||
};
|
||||
|
||||
const StyleOverrides = styled.span`
|
||||
|
||||
@@ -47,7 +47,7 @@ export interface MetricOptionProps {
|
||||
showFormula?: boolean;
|
||||
showType?: boolean;
|
||||
url?: string;
|
||||
labelRef?: RefObject<any | null>;
|
||||
labelRef?: RefObject<any>;
|
||||
shouldShowTooltip?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ const TooltipSection = ({
|
||||
</TooltipSectionWrapper>
|
||||
);
|
||||
|
||||
export const isLabelTruncated = (labelRef?: RefObject<any | null>): boolean =>
|
||||
export const isLabelTruncated = (labelRef?: RefObject<any>): boolean =>
|
||||
!!(labelRef?.current?.scrollWidth > labelRef?.current?.clientWidth);
|
||||
|
||||
export const getColumnLabelText = (column: ColumnMeta): string =>
|
||||
@@ -92,7 +92,7 @@ export const getColumnTypeTooltipNode = (column: ColumnMeta): ReactNode => {
|
||||
|
||||
export const getColumnTooltipNode = (
|
||||
column: ColumnMeta,
|
||||
labelRef?: RefObject<any | null>,
|
||||
labelRef?: RefObject<any>,
|
||||
): ReactNode => {
|
||||
if (
|
||||
(!column.column_name || !column.verbose_name) &&
|
||||
@@ -121,7 +121,7 @@ type MetricType = Omit<Metric, 'id' | 'uuid'> & { label?: string };
|
||||
|
||||
export const getMetricTooltipNode = (
|
||||
metric: MetricType,
|
||||
labelRef?: RefObject<any | null>,
|
||||
labelRef?: RefObject<any>,
|
||||
): ReactNode => {
|
||||
if (
|
||||
!metric.verbose_name &&
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactElement, ReactNode, ComponentType } from 'react';
|
||||
import { ReactElement, ReactNode, ReactText, ComponentType } from 'react';
|
||||
|
||||
import type {
|
||||
AdhocColumn,
|
||||
@@ -39,9 +39,6 @@ import type {
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import { sharedControls, sharedControlComponents } from './shared-controls';
|
||||
|
||||
// React 19 removed the ReactText type.
|
||||
type ReactText = string | number;
|
||||
|
||||
export type { Metric } from '@superset-ui/core';
|
||||
export type { ControlComponentProps } from './shared-controls/components/types';
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
"react-error-boundary": "^6.1.2",
|
||||
"react-js-cron": "^6.0.2",
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-resize-detector": "^12.3.0",
|
||||
"react-resize-detector": "^7.1.2",
|
||||
"react-syntax-highlighter": "^16.1.1",
|
||||
"react-ultimate-pagination": "^1.3.2",
|
||||
"regenerator-runtime": "^0.14.1",
|
||||
@@ -92,17 +92,19 @@
|
||||
"@emotion/cache": "^11.4.0",
|
||||
"@emotion/react": "^11.4.1",
|
||||
"@emotion/styled": "^11.14.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-loadable": "*",
|
||||
"@types/react-window": "^1.8.8",
|
||||
"@types/tinycolor2": "*",
|
||||
"antd": "^6.0.0",
|
||||
"nanoid": "^5.0.9",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0",
|
||||
"react-loadable": "^5.5.0",
|
||||
"tinycolor2": "*"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
||||
@@ -17,14 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
SupersetClientInterface,
|
||||
RequestConfig,
|
||||
@@ -84,7 +77,7 @@ function ChartDataProvider({
|
||||
formDataRequestOptions,
|
||||
datasourceRequestOptions,
|
||||
queryRequestOptions,
|
||||
}: ChartDataProviderProps): ReactElement | null {
|
||||
}: ChartDataProviderProps): JSX.Element | null {
|
||||
const [state, setState] = useState<ChartDataProviderState>({
|
||||
status: 'uninitialized',
|
||||
});
|
||||
@@ -166,7 +159,7 @@ function ChartDataProvider({
|
||||
const { status, payload, error } = state;
|
||||
|
||||
// Wrap the children result in a Fragment so the component's return type
|
||||
// stays `ReactElement | null` (which TypeScript requires for JSX components)
|
||||
// stays `JSX.Element | null` (which TypeScript requires for JSX components)
|
||||
// while still letting consumers return any ReactNode (strings, fragments,
|
||||
// arrays, null, etc.) from the render prop.
|
||||
switch (status) {
|
||||
|
||||
@@ -186,8 +186,8 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
const [error, setError] = useState<Error>();
|
||||
const [formData, setFormData] = useState<QueryFormData>();
|
||||
|
||||
const chartClientRef = useRef<ChartClient | null>(null);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
const chartClientRef = useRef<ChartClient>();
|
||||
const abortControllerRef = useRef<AbortController>();
|
||||
|
||||
// Initialize chart client
|
||||
if (!chartClientRef.current) {
|
||||
@@ -325,7 +325,7 @@ export default function StatefulChart(props: StatefulChartProps) {
|
||||
}, []);
|
||||
|
||||
// Combined effect for all prop changes and lifecycle
|
||||
const prevPropsRef = useRef<StatefulChartProps | null>(null);
|
||||
const prevPropsRef = useRef<StatefulChartProps>();
|
||||
useEffect(() => {
|
||||
const currentProps = props;
|
||||
const prevProps = prevPropsRef.current;
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
useCallback,
|
||||
useMemo,
|
||||
useRef,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@@ -77,9 +76,9 @@ export type Props = Omit<SuperChartCoreProps, 'chartProps'> &
|
||||
/** Prop for form plugins using superchart */
|
||||
showOverflow?: boolean;
|
||||
/** Prop for popovercontainer ref */
|
||||
parentRef?: RefObject<any | null>;
|
||||
parentRef?: RefObject<any>;
|
||||
/** Prop for chart ref */
|
||||
inputRef?: RefObject<any | null>;
|
||||
inputRef?: RefObject<any>;
|
||||
/** Chart width */
|
||||
height?: number | string;
|
||||
/** Chart height */
|
||||
@@ -126,7 +125,7 @@ function SuperChart({
|
||||
height = 400,
|
||||
width = '100%',
|
||||
...rest
|
||||
}: Props): ReactElement {
|
||||
}: Props): JSX.Element {
|
||||
/**
|
||||
* SuperChart's core ref
|
||||
*/
|
||||
@@ -265,7 +264,7 @@ function SuperChart({
|
||||
({ data }) => !data || (Array.isArray(data) && data.length === 0),
|
||||
));
|
||||
|
||||
let chart: ReactElement;
|
||||
let chart: JSX.Element;
|
||||
if (noResultQueries) {
|
||||
chart = noResults ? (
|
||||
<>{noResults}</>
|
||||
|
||||
@@ -42,10 +42,10 @@ function IDENTITY<T>(x: T) {
|
||||
return x;
|
||||
}
|
||||
|
||||
const EMPTY = Object.assign(() => null, { preload: () => Promise.resolve() });
|
||||
const EMPTY = () => null;
|
||||
|
||||
interface LoadingProps {
|
||||
error?: unknown;
|
||||
error: { toString(): string };
|
||||
}
|
||||
|
||||
interface LoadedModules {
|
||||
@@ -204,7 +204,7 @@ const SuperChartCore = forwardRef<SuperChartCoreRef, Props>(
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<strong>{t('ERROR')}</strong>
|
||||
<code>chartType="{loadingChartType}"</code> —
|
||||
{String(error)}
|
||||
{error.toString()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,21 +26,11 @@ import {
|
||||
} from 'react';
|
||||
import type {
|
||||
ComponentType,
|
||||
WeakValidationMap,
|
||||
ForwardRefExoticComponent,
|
||||
PropsWithoutRef,
|
||||
RefAttributes,
|
||||
} from 'react';
|
||||
import type { Validator } from 'prop-types';
|
||||
|
||||
// React 19 dropped `WeakValidationMap` from its type exports; keep the original
|
||||
// shape so `renderFn.propTypes` still type-checks.
|
||||
type WeakValidationMap<T> = {
|
||||
[K in keyof T]?: null extends T[K]
|
||||
? Validator<T[K] | null | undefined>
|
||||
: undefined extends T[K]
|
||||
? Validator<T[K] | null | undefined>
|
||||
: Validator<T[K]>;
|
||||
};
|
||||
|
||||
// TODO: Note that id and className can collide between Props and ReactifyProps
|
||||
// leading to (likely) unexpected behaviors. We should either require Props to not
|
||||
@@ -79,24 +69,6 @@ export type ReactifiedComponent<Props> = ForwardRefExoticComponent<
|
||||
PropsWithoutRef<Props & ReactifyProps> & RefAttributes<ReactifiedComponentRef>
|
||||
>;
|
||||
|
||||
// Mirrors React's own `defaultProps` rule: a default fills in only where the
|
||||
// caller passed `undefined`.
|
||||
function applyDefaultProps<P extends object>(
|
||||
props: P,
|
||||
defaultProps?: object,
|
||||
): P {
|
||||
if (!defaultProps) {
|
||||
return props;
|
||||
}
|
||||
const merged = { ...props } as Record<string, unknown>;
|
||||
Object.entries(defaultProps).forEach(([key, value]) => {
|
||||
if (merged[key] === undefined) {
|
||||
merged[key] = value;
|
||||
}
|
||||
});
|
||||
return merged as P;
|
||||
}
|
||||
|
||||
// Return the widest public type that covers "use it as a React component" so
|
||||
// TypeScript JSX callers and `ComponentType<...>`-typed variables still compile;
|
||||
// callers with explicit `ComponentClass<...>` annotations must widen to
|
||||
@@ -105,18 +77,11 @@ function applyDefaultProps<P extends object>(
|
||||
export default function reactify<Props extends object>(
|
||||
renderFn: RenderFuncType<Props>,
|
||||
callbacks?: LifeCycleCallbacks,
|
||||
): ComponentType<Props & ReactifyProps> & {
|
||||
// Copied off `renderFn` for introspection. React 19 no longer applies these
|
||||
// itself — `applyDefaultProps` does.
|
||||
defaultProps?: Partial<Props & ReactifyProps>;
|
||||
} {
|
||||
): ComponentType<Props & ReactifyProps> {
|
||||
const ReactifiedComponent = forwardRef<
|
||||
ReactifiedComponentRef,
|
||||
Props & ReactifyProps
|
||||
>(function ReactifiedComponent(incomingProps, ref) {
|
||||
// React 19 no longer applies `defaultProps` to function components, so the
|
||||
// defaults declared on `renderFn` are merged here instead.
|
||||
const props = applyDefaultProps(incomingProps, renderFn.defaultProps);
|
||||
>(function ReactifiedComponent(props, ref) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// Keep the latest props available to the unmount callback — legacy
|
||||
// consumers read values off `this.props` (e.g. ReactNVD3 uses id).
|
||||
|
||||
@@ -101,7 +101,7 @@ export interface ChartPropsConfig {
|
||||
/** is the chart refreshing its contents */
|
||||
isRefreshing?: boolean;
|
||||
/** chart ref */
|
||||
inputRef?: RefObject<any | null>;
|
||||
inputRef?: RefObject<any>;
|
||||
/** Theme object */
|
||||
theme: SupersetTheme;
|
||||
/* legend index */
|
||||
@@ -152,7 +152,7 @@ export default class ChartProps<FormData extends RawFormData = RawFormData> {
|
||||
|
||||
isRefreshing?: boolean;
|
||||
|
||||
inputRef?: RefObject<any | null>;
|
||||
inputRef?: RefObject<any>;
|
||||
|
||||
inContextMenu?: boolean;
|
||||
|
||||
|
||||
@@ -238,8 +238,8 @@ export function AsyncAceEditor(
|
||||
|
||||
// Move autocomplete popup to the nearest parent container with data-ace-container
|
||||
useEffect(() => {
|
||||
const editorInstance = (ref as React.RefObject<AceEditor | null>)
|
||||
?.current?.editor;
|
||||
const editorInstance = (ref as React.RefObject<AceEditor>)?.current
|
||||
?.editor;
|
||||
if (!editorInstance) return;
|
||||
|
||||
const editorContainer = editorInstance.container;
|
||||
|
||||
@@ -16,14 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
Children,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
Fragment,
|
||||
forwardRef,
|
||||
Ref,
|
||||
} from 'react';
|
||||
import { Children, ReactElement, Fragment, forwardRef, Ref } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { Button as AntdButton } from 'antd';
|
||||
import { useTheme } from '@apache-superset/core/theme';
|
||||
@@ -171,9 +164,7 @@ function ButtonInner(props: ButtonProps, ref: Ref<HTMLElement>) {
|
||||
color,
|
||||
} = resolvedStyleMap[buttonStyle ?? 'primary'] ?? BUTTON_STYLE_MAP.primary;
|
||||
|
||||
// React 19 types `ReactElement['props']` as `unknown`, so spell out the
|
||||
// shape read below.
|
||||
const element = children as ReactElement<{ children?: ReactNode }>;
|
||||
const element = children as ReactElement;
|
||||
|
||||
let renderedChildren = [];
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ComponentType } from 'react';
|
||||
import { DatePicker as AntdDatePicker } from 'antd';
|
||||
import { css } from '@apache-superset/core/theme';
|
||||
import type { DatePickerProps, RangePickerProps } from './types';
|
||||
@@ -30,7 +29,6 @@ export const DatePicker = (props: DatePickerProps) => (
|
||||
/>
|
||||
);
|
||||
|
||||
export const RangePicker: ComponentType<RangePickerProps> =
|
||||
AntdDatePicker.RangePicker;
|
||||
export const { RangePicker } = AntdDatePicker;
|
||||
|
||||
export type { DatePickerProps, RangePickerProps };
|
||||
|
||||
@@ -16,12 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
cloneElement,
|
||||
type FocusEventHandler,
|
||||
type KeyboardEventHandler,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import { ReactElement, cloneElement } from 'react';
|
||||
|
||||
import { Dropdown as AntdDropdown, DropdownProps } from 'antd';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
@@ -103,16 +98,10 @@ export const MenuDotsDropdown = ({
|
||||
|
||||
export const NoAnimationDropdown = (props: NoAnimationDropdownProps) => {
|
||||
const { children, onBlur, onKeyDown, ...rest } = props;
|
||||
const childrenWithProps = cloneElement(
|
||||
children as ReactElement<{
|
||||
onBlur?: FocusEventHandler;
|
||||
onKeyDown?: KeyboardEventHandler;
|
||||
}>,
|
||||
{
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
},
|
||||
);
|
||||
const childrenWithProps = cloneElement(children as ReactElement, {
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
});
|
||||
|
||||
return (
|
||||
<AntdDropdown autoFocus overlayStyle={props.overlayStyle} {...rest}>
|
||||
|
||||
@@ -57,7 +57,7 @@ export interface DropdownContainerProps {
|
||||
/**
|
||||
* Dropdown ref.
|
||||
*/
|
||||
dropdownRef?: RefObject<HTMLDivElement | null>;
|
||||
dropdownRef?: RefObject<HTMLDivElement>;
|
||||
/**
|
||||
* Dropdown additional style properties.
|
||||
*/
|
||||
|
||||
@@ -24,9 +24,7 @@ import { BaseIconComponent } from './BaseIcon';
|
||||
|
||||
const AsyncIcon = forwardRef<HTMLSpanElement, IconType>((props, ref) => {
|
||||
const [, setLoaded] = useState(false);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const ImportedSVG = useRef<FC<SVGProps<SVGSVGElement>>>();
|
||||
const { fileName, customIcons, iconSize, iconColor, viewBox, ...restProps } =
|
||||
props;
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type React from 'react';
|
||||
import { fireEvent, render } from '@superset-ui/core/spec';
|
||||
|
||||
import { Icons } from '../Icons';
|
||||
@@ -58,11 +57,7 @@ test('preserves a custom icon color (antd v6 Tag icon-style regression)', () =>
|
||||
|
||||
// test stories from the storybook!
|
||||
test('renders all the storybook gallery variants', () => {
|
||||
// Cast the storybook fn — its render signature changed in storybook 8
|
||||
const GalleryComponent = LabelGallery as unknown as React.FC<
|
||||
Record<string, never>
|
||||
>;
|
||||
const { container } = render(<GalleryComponent />);
|
||||
const { container } = render(<LabelGallery />);
|
||||
const nonInteractiveLabelCount = 4;
|
||||
const renderedLabelCount = options.length * 2 + nonInteractiveLabelCount;
|
||||
expect(container.querySelectorAll('.ant-tag')).toHaveLength(
|
||||
|
||||
@@ -43,14 +43,14 @@ const TWO_DAYS_AGO = '2 days ago';
|
||||
|
||||
const runWithBarCollapsed = async (func: Function) => {
|
||||
const spy = jest.spyOn(resizeDetector, 'useResizeDetector');
|
||||
let width = 0;
|
||||
spy.mockImplementation(((props: any) => {
|
||||
let width: number;
|
||||
spy.mockImplementation(props => {
|
||||
if (props?.onResize && !width) {
|
||||
width = 80;
|
||||
props.onResize({ width, height: 0, entry: null as unknown });
|
||||
props.onResize(width);
|
||||
}
|
||||
return { ref: () => {} };
|
||||
}) as never);
|
||||
return { ref: { current: undefined } };
|
||||
});
|
||||
await func();
|
||||
spy.mockRestore();
|
||||
};
|
||||
|
||||
@@ -17,10 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import {
|
||||
useResizeDetector,
|
||||
type OnResizeCallback,
|
||||
} from 'react-resize-detector';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { uniqWith } from 'lodash-es';
|
||||
import { styled } from '@apache-superset/core/theme';
|
||||
import { Tooltip } from '../Tooltip';
|
||||
@@ -184,7 +181,7 @@ export interface MetadataBarProps {
|
||||
* This process is important to make sure the new type is reviewed by the design team, improving Superset consistency.
|
||||
*/
|
||||
const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
|
||||
const [width, setWidth] = useState<number | undefined>(undefined);
|
||||
const [width, setWidth] = useState<number>();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const uniqueItems = uniqWith(items, (a, b) => a.type === b.type);
|
||||
const sortedItems = uniqueItems.sort((a, b) => ORDER[a.type] - ORDER[b.type]);
|
||||
@@ -196,9 +193,8 @@ const MetadataBar = ({ items, tooltipPlacement = 'top' }: MetadataBarProps) => {
|
||||
throw new Error('The maximum number of items for the metadata bar is 6.');
|
||||
}
|
||||
|
||||
const onResize = useCallback<OnResizeCallback>(
|
||||
payload => {
|
||||
const width = payload.width ?? undefined;
|
||||
const onResize = useCallback(
|
||||
(width: number | undefined) => {
|
||||
// Calculates the breakpoint width to collapse the bar.
|
||||
// The last item does not have a space, so we subtract SPACE_BETWEEN_ITEMS from the total.
|
||||
const breakpoint =
|
||||
|
||||
@@ -54,7 +54,7 @@ export interface ModalProps {
|
||||
/** @deprecated Use styles.body instead */
|
||||
bodyStyle?: CSSProperties;
|
||||
styles?: { body?: CSSProperties; [key: string]: CSSProperties | undefined };
|
||||
openerRef?: React.RefObject<HTMLElement | null>;
|
||||
openerRef?: React.RefObject<HTMLElement>;
|
||||
}
|
||||
|
||||
export interface StyledModalProps {
|
||||
|
||||
@@ -17,14 +17,13 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ModalTrigger } from '.';
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
interface IModalTriggerProps {
|
||||
triggerNode: ReactElement;
|
||||
triggerNode: JSX.Element;
|
||||
dialogClassName?: string;
|
||||
modalTitle?: string;
|
||||
modalBody?: ReactElement;
|
||||
modalFooter?: ReactElement;
|
||||
modalBody?: JSX.Element;
|
||||
modalFooter?: JSX.Element;
|
||||
beforeOpen?: () => void;
|
||||
onExit?: () => void;
|
||||
isButton?: boolean;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { Key, ReactElement } from 'react';
|
||||
import { Key } from 'react';
|
||||
import cx from 'classnames';
|
||||
import { css, useTheme } from '@apache-superset/core/theme';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
@@ -29,7 +29,7 @@ export interface OptionProps {
|
||||
}
|
||||
|
||||
export type OnChangeHandler = (key: Key) => void;
|
||||
export type RenderElementHandler = (option: OptionProps) => ReactElement;
|
||||
export type RenderElementHandler = (option: OptionProps) => JSX.Element;
|
||||
|
||||
export interface PopoverDropdownProps {
|
||||
id: string;
|
||||
|
||||
@@ -542,7 +542,7 @@ const AsyncSelect = forwardRef(
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) =>
|
||||
dropDownRenderHelper(
|
||||
originNode as Parameters<typeof dropDownRenderHelper>[0],
|
||||
originNode,
|
||||
isDropdownVisible,
|
||||
isLoading,
|
||||
fullSelectOptions.length,
|
||||
|
||||
@@ -583,10 +583,10 @@ const Select = forwardRef(
|
||||
const isLoading = loading ?? false;
|
||||
|
||||
const popupRender = (
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement | null> },
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) =>
|
||||
dropDownRenderHelper(
|
||||
originNode as Parameters<typeof dropDownRenderHelper>[0],
|
||||
originNode,
|
||||
isDropdownVisible,
|
||||
isLoading,
|
||||
fullSelectOptions.length,
|
||||
|
||||
@@ -153,20 +153,14 @@ export const getSuffixIcon = (
|
||||
return <Icons.DownOutlined iconSize="s" aria-label="down" />;
|
||||
};
|
||||
|
||||
type FlattenOptionsProps = {
|
||||
flattenOptions?: Array<Record<string, any>>;
|
||||
};
|
||||
|
||||
export const dropDownRenderHelper = (
|
||||
originNode: ReactElement<FlattenOptionsProps> & {
|
||||
ref?: RefObject<HTMLElement | null>;
|
||||
},
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
isDropdownVisible: boolean,
|
||||
isLoading: boolean | undefined,
|
||||
optionsLength: number,
|
||||
helperText: string | undefined,
|
||||
errorComponent?: ReactElement,
|
||||
bulkSelectComponents?: ReactElement,
|
||||
errorComponent?: JSX.Element,
|
||||
bulkSelectComponents?: JSX.Element,
|
||||
) => {
|
||||
if (!isDropdownVisible) {
|
||||
originNode.ref?.current?.scrollTo({ top: 0 });
|
||||
@@ -178,19 +172,17 @@ export const dropDownRenderHelper = (
|
||||
return errorComponent;
|
||||
}
|
||||
|
||||
const originProps = originNode.props;
|
||||
|
||||
// remap for accessibility for proper item count
|
||||
const accessibilityNode = {
|
||||
...originNode,
|
||||
props: {
|
||||
...originProps,
|
||||
flattenOptions: ensureIsArray(originProps.flattenOptions).map(
|
||||
...originNode.props,
|
||||
flattenOptions: ensureIsArray(originNode.props.flattenOptions).map(
|
||||
(opt: Record<string, any>, idx: number) => ({
|
||||
...opt,
|
||||
data: {
|
||||
...opt.data,
|
||||
'aria-setsize': originProps.flattenOptions?.length || 0,
|
||||
'aria-setsize': originNode.props.flattenOptions?.length || 0,
|
||||
'aria-posinset': idx + 1,
|
||||
},
|
||||
}),
|
||||
|
||||
@@ -23,10 +23,7 @@ import {
|
||||
TableProps as AntTableProps,
|
||||
} from 'antd/es/table';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
useResizeDetector,
|
||||
type OnResizeCallback,
|
||||
} from 'react-resize-detector';
|
||||
import { useResizeDetector } from 'react-resize-detector';
|
||||
import { useEffect, useRef, useState, useCallback, CSSProperties } from 'react';
|
||||
import { VariableSizeGrid as Grid } from 'react-window';
|
||||
import { safeHtmlSpan } from '@superset-ui/core';
|
||||
@@ -87,8 +84,8 @@ const VirtualTable = <RecordType extends object>(
|
||||
allowHTML = false,
|
||||
} = props;
|
||||
const [tableWidth, setTableWidth] = useState<number>(0);
|
||||
const onResize = useCallback<OnResizeCallback>(payload => {
|
||||
setTableWidth(payload.width ?? 0);
|
||||
const onResize = useCallback((width?: number) => {
|
||||
setTableWidth(width ?? 0);
|
||||
}, []);
|
||||
const { ref } = useResizeDetector({ onResize });
|
||||
const theme = useTheme();
|
||||
@@ -129,8 +126,8 @@ const VirtualTable = <RecordType extends object>(
|
||||
(lastColumn.width as number) + Math.floor(tableWidth - totalWidth);
|
||||
}
|
||||
|
||||
const gridRef = useRef<any | null>(null);
|
||||
const [connectObject] = useState<Record<string, unknown>>(() => {
|
||||
const gridRef = useRef<any>();
|
||||
const [connectObject] = useState<any>(() => {
|
||||
const obj = {};
|
||||
Object.defineProperty(obj, 'scrollLeft', {
|
||||
get: () => {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { fireEvent, render, screen } from '@superset-ui/core/spec';
|
||||
import { fireEvent, render } from '@superset-ui/core/spec';
|
||||
import Tabs, { EditableTabs, LineEditableTabs } from './Tabs';
|
||||
|
||||
const defaultItems = [
|
||||
@@ -163,14 +163,12 @@ describe('Tabs', () => {
|
||||
expect(onEditMock).toHaveBeenCalledWith(expect.any(String), 'remove');
|
||||
});
|
||||
|
||||
test('renders with editable-card defaults', () => {
|
||||
render(
|
||||
<EditableTabs
|
||||
items={[{ key: '1', label: 'Tab 1', children: 'one' }]}
|
||||
/>,
|
||||
);
|
||||
const container = screen.getByText('Tab 1').closest('.ant-tabs');
|
||||
expect(container).toHaveClass('ant-tabs-editable-card');
|
||||
test('should have default props set correctly', () => {
|
||||
expect(EditableTabs.defaultProps?.type).toBe('editable-card');
|
||||
expect(EditableTabs.defaultProps?.animated).toEqual({
|
||||
inkBar: true,
|
||||
tabPane: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import type { ComponentProps, FC } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { css, styled } from '@apache-superset/core/theme';
|
||||
import { Tabs as AntdTabs, TabsProps as AntdTabsProps } from 'antd';
|
||||
import { Icons } from '@superset-ui/core/components/Icons';
|
||||
@@ -132,23 +132,19 @@ const StyledEditableTabs = styled(StyledTabs)`
|
||||
const StyledCloseOutlined = styled(Icons.CloseOutlined)`
|
||||
color: ${({ theme }) => theme.colorIcon};
|
||||
`;
|
||||
const EditableTabsBase: FC<TabsProps> = ({
|
||||
type = 'editable-card',
|
||||
animated = { inkBar: true, tabPane: false },
|
||||
...rest
|
||||
}: TabsProps) => (
|
||||
<StyledEditableTabs type={type} animated={animated} {...rest} />
|
||||
);
|
||||
|
||||
const EditableTabPane: FC<ComponentProps<typeof StyledTabPane>> = ({
|
||||
closeIcon = <StyledCloseOutlined iconSize="s" role="button" tabIndex={0} />,
|
||||
...rest
|
||||
}) => <StyledTabPane closeIcon={closeIcon} {...rest} />;
|
||||
|
||||
export const EditableTabs = Object.assign(EditableTabsBase, {
|
||||
TabPane: EditableTabPane,
|
||||
export const EditableTabs = Object.assign(StyledEditableTabs, {
|
||||
TabPane: StyledTabPane,
|
||||
});
|
||||
|
||||
EditableTabs.defaultProps = {
|
||||
type: 'editable-card',
|
||||
animated: { inkBar: true, tabPane: false },
|
||||
};
|
||||
|
||||
EditableTabs.TabPane.defaultProps = {
|
||||
closeIcon: <StyledCloseOutlined iconSize="s" role="button" tabIndex={0} />,
|
||||
};
|
||||
|
||||
export const StyledLineEditableTabs = styled(EditableTabs)`
|
||||
&.ant-tabs-card > .ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0 ${({ theme }) => theme.sizeUnit * 4}px;
|
||||
|
||||
@@ -42,7 +42,7 @@ interface TelemetryPixelProps {
|
||||
* @param {string} props.sha - The SHA of Superset that's currently in use.
|
||||
* @param {string} props.build - The build of Superset that's currently in use.
|
||||
* @param {boolean} props.enabled - Runtime opt-out switch; when false the pixel is not rendered.
|
||||
* @returns {ReactElement | null} The rendered TelemetryPixel component.
|
||||
* @returns {JSX.Element | null} The rendered TelemetryPixel component.
|
||||
*/
|
||||
|
||||
const PIXEL_ID = '0d3461e1-abb1-4691-a0aa-5ed50de66af0';
|
||||
|
||||
@@ -122,10 +122,6 @@ test('applies dark theme when background is dark', () => {
|
||||
expect(theme.foregroundColor).toBe('#ffffff');
|
||||
});
|
||||
|
||||
// The props AgGridReact was last rendered with.
|
||||
const agGridProps = () =>
|
||||
(AgGridReact as unknown as jest.Mock).mock.calls.at(-1)?.[0];
|
||||
|
||||
test('forwards ref to AgGridReact', () => {
|
||||
const ref = createRef<AgGridReact>();
|
||||
|
||||
@@ -137,14 +133,13 @@ test('forwards ref to AgGridReact', () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
// React 19 hands `ref` to the component as a regular prop rather than as a
|
||||
// second argument, so assert on the props object itself.
|
||||
expect(agGridProps()).toEqual(
|
||||
// Check that AgGridReact was called with the ref
|
||||
expect(AgGridReact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rowData: mockRowData,
|
||||
columnDefs: mockColumnDefs,
|
||||
ref,
|
||||
}),
|
||||
expect.any(Object), // ref is passed as second argument
|
||||
);
|
||||
});
|
||||
|
||||
@@ -165,7 +160,7 @@ test('passes all props through to AgGridReact', () => {
|
||||
|
||||
// onGridReady and onFirstDataRendered are intercepted by the component to expose
|
||||
// the grid API on the container element; the wrapped function is passed instead.
|
||||
expect(agGridProps()).toEqual(
|
||||
expect(AgGridReact).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
rowData: mockRowData,
|
||||
columnDefs: mockColumnDefs,
|
||||
@@ -174,6 +169,7 @@ test('passes all props through to AgGridReact', () => {
|
||||
pagination: true,
|
||||
paginationPageSize: 10,
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ export function Timer({
|
||||
const [clockStr, setClockStr] = useState(
|
||||
startTime && endTime ? fDuration(startTime, endTime) : '00:00:00.00',
|
||||
);
|
||||
const timer = useRef<ReturnType<typeof setInterval> | undefined>(undefined);
|
||||
const timer = useRef<ReturnType<typeof setInterval>>();
|
||||
|
||||
useEffect(() => {
|
||||
const stopTimer = () => {
|
||||
|
||||
@@ -20,8 +20,8 @@ import { useEffect, useRef, useState, RefObject } from 'react';
|
||||
|
||||
export function useElementOnScreen<T extends Element>(
|
||||
options: IntersectionObserverInit,
|
||||
): [RefObject<T | null>, boolean] {
|
||||
const containerRef = useRef<T | null>(null);
|
||||
): [RefObject<T>, boolean] {
|
||||
const containerRef = useRef<T>(null);
|
||||
const [isSticky, setIsSticky] = useState(false);
|
||||
|
||||
const callback = (entries: IntersectionObserverEntry[]) => {
|
||||
|
||||
@@ -38,9 +38,9 @@ export const truncationCSS = css`
|
||||
*/
|
||||
const useCSSTextTruncation = <T extends HTMLElement>(
|
||||
{ isVertical, isHorizontal } = { isVertical: false, isHorizontal: true },
|
||||
): [RefObject<T | null>, boolean] => {
|
||||
): [RefObject<T>, boolean] => {
|
||||
const [isTruncated, setIsTruncated] = useState(true);
|
||||
const ref = useRef<T | null>(null);
|
||||
const ref = useRef<T>(null);
|
||||
const [offsetWidth, setOffsetWidth] = useState(0);
|
||||
const [scrollWidth, setScrollWidth] = useState(0);
|
||||
const [offsetHeight, setOffsetHeight] = useState(0);
|
||||
|
||||
@@ -30,10 +30,10 @@ const genElements = (
|
||||
offsetWidth: number | undefined,
|
||||
childNodes: any = [],
|
||||
) => {
|
||||
const elementRef: RefObject<Partial<HTMLElement> | null> = {
|
||||
const elementRef: RefObject<Partial<HTMLElement>> = {
|
||||
current: { scrollWidth, clientWidth, childNodes },
|
||||
};
|
||||
const plusRef: RefObject<Partial<HTMLElement> | null> = {
|
||||
const plusRef: RefObject<Partial<HTMLElement>> = {
|
||||
current: { offsetWidth },
|
||||
};
|
||||
return [elementRef, plusRef];
|
||||
|
||||
@@ -47,8 +47,6 @@ export enum FeatureFlag {
|
||||
EnableAdvancedDataTypes = 'ENABLE_ADVANCED_DATA_TYPES',
|
||||
EnableExtensions = 'ENABLE_EXTENSIONS',
|
||||
EnableViewers = 'ENABLE_VIEWERS',
|
||||
/** @deprecated */
|
||||
EnableJavascriptControls = 'ENABLE_JAVASCRIPT_CONTROLS',
|
||||
EnableTemplateProcessing = 'ENABLE_TEMPLATE_PROCESSING',
|
||||
EscapeMarkdownHtml = 'ESCAPE_MARKDOWN_HTML',
|
||||
EstimateQueryCost = 'ESTIMATE_QUERY_COST',
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
import { ComponentType, ReactElement } from 'react';
|
||||
import { ComponentType } from 'react';
|
||||
import { render as renderTestComponent, screen } from '@testing-library/react';
|
||||
import createLoadableRenderer, {
|
||||
LoadableRenderer as LoadableRendererType,
|
||||
@@ -29,8 +29,8 @@ describe('createLoadableRenderer', () => {
|
||||
return <div className="test-component">test</div>;
|
||||
}
|
||||
let loadChartSuccess = jest.fn(() => Promise.resolve(TestComponent));
|
||||
let render: (loaded: { Chart: ComponentType }) => ReactElement;
|
||||
let loading: () => ReactElement;
|
||||
let render: (loaded: { Chart: ComponentType }) => JSX.Element;
|
||||
let loading: () => JSX.Element;
|
||||
let LoadableRenderer: LoadableRendererType<{}>;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -30,12 +30,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"d3": "^3.5.17",
|
||||
"prop-types": "^15.8.1"
|
||||
"prop-types": "^15.8.1",
|
||||
"react": "^19.2.7"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.2.0 || ^19.0.0"
|
||||
"@apache-superset/core": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,6 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -36,6 +36,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -39,6 +39,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@apache-superset/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,6 +44,6 @@
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -68,7 +68,7 @@ const getSortIcon = (sortState: SortState[], colId: string | null) => {
|
||||
const autoOpenFilterAndFocus = async (
|
||||
column: Column,
|
||||
api: GridApi,
|
||||
filterRef: React.RefObject<HTMLDivElement | null>,
|
||||
filterRef: React.RefObject<HTMLDivElement>,
|
||||
setFilterVisible: (visible: boolean) => void,
|
||||
lastFilteredInputPosition?: FilterInputPosition,
|
||||
) => {
|
||||
|
||||
@@ -16,19 +16,12 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import {
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
cloneElement,
|
||||
type ReactElement,
|
||||
type Ref,
|
||||
} from 'react';
|
||||
import { useEffect, useRef, useState, cloneElement } from 'react';
|
||||
import { PopoverContainer, PopoverWrapper } from '../../styles';
|
||||
|
||||
interface Props {
|
||||
content: React.ReactNode;
|
||||
children: ReactElement<{ ref?: Ref<HTMLElement> }>;
|
||||
children: React.ReactElement;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -24,10 +24,9 @@ import {
|
||||
memo,
|
||||
FunctionComponent,
|
||||
useState,
|
||||
FormEvent,
|
||||
ChangeEvent,
|
||||
useEffect,
|
||||
type RefObject,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
|
||||
import { Constants, ThemedAgGridReact } from '@superset-ui/core/components';
|
||||
@@ -103,14 +102,14 @@ export interface AgGridTableProps {
|
||||
handleCellClicked: (event: CellClickedEvent) => void;
|
||||
handleSelectionChanged: (event: SelectionChangedEvent) => void;
|
||||
filters?: Record<string, DataRecordValue[]> | null;
|
||||
renderTimeComparisonDropdown: () => ReactElement | null;
|
||||
renderTimeComparisonDropdown: () => JSX.Element | null;
|
||||
cleanedTotals: DataRecord;
|
||||
showTotals: boolean;
|
||||
width: number;
|
||||
onColumnStateChange?: (state: AgGridChartStateWithMetadata) => void;
|
||||
onFilterChanged?: (filterModel: Record<string, any>) => void;
|
||||
metricColumns?: string[];
|
||||
gridRef?: RefObject<AgGridReact | null>;
|
||||
gridRef?: RefObject<AgGridReact>;
|
||||
chartState?: AgGridChartState;
|
||||
}
|
||||
|
||||
@@ -247,8 +246,7 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
|
||||
}, []);
|
||||
|
||||
const onFilterTextBoxChanged = useCallback(
|
||||
(event: FormEvent<HTMLInputElement>) => {
|
||||
const value = (event.target as HTMLInputElement).value;
|
||||
({ target: { value } }: ChangeEvent<HTMLInputElement>) => {
|
||||
if (serverPagination) {
|
||||
setSearchValue(value);
|
||||
debouncedSearch(value);
|
||||
|
||||
@@ -23,14 +23,7 @@ import {
|
||||
getTimeFormatterForGranularity,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
useMemo,
|
||||
type ReactElement,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
||||
import {
|
||||
@@ -446,7 +439,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[setDataMask, serverPagination],
|
||||
);
|
||||
|
||||
const renderTimeComparisonVisibility = (): ReactElement => (
|
||||
const renderTimeComparisonVisibility = (): JSX.Element => (
|
||||
<TimeComparisonVisibility
|
||||
comparisonColumns={comparisonColumns}
|
||||
selectedComparisonColumns={selectedComparisonColumns}
|
||||
|
||||
@@ -120,7 +120,7 @@ async function detectLastFilteredInput(
|
||||
* Gets complete filter state including SQL conversion and input position detection.
|
||||
*/
|
||||
export async function getCompleteFilterState(
|
||||
gridRef: RefObject<AgGridReact | null>,
|
||||
gridRef: RefObject<AgGridReact>,
|
||||
metricColumns: string[],
|
||||
): Promise<FilterState> {
|
||||
// Capture activeElement before any async operations to detect which input
|
||||
|
||||
@@ -24,7 +24,7 @@ import { FILTER_INPUT_POSITIONS } from '../../src/consts';
|
||||
describe('filterStateManager', () => {
|
||||
describe('getCompleteFilterState', () => {
|
||||
test('should return empty state when gridRef.current is null', async () => {
|
||||
const gridRef = { current: null } as RefObject<AgGridReact | null>;
|
||||
const gridRef = { current: null } as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('filterStateManager', () => {
|
||||
test('should return empty state when gridRef.current.api is undefined', async () => {
|
||||
const gridRef = {
|
||||
current: { api: undefined } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -103,7 +103,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, ['SUM(revenue)']);
|
||||
|
||||
@@ -138,7 +138,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
// Mock activeElement
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
@@ -182,7 +182,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
// Mock activeElement
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
@@ -217,7 +217,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
// Mock activeElement as something outside the filter
|
||||
const outsideElement = document.createElement('div');
|
||||
@@ -272,7 +272,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
// Mock activeElement in age filter
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
@@ -306,7 +306,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -324,7 +324,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -356,7 +356,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -378,7 +378,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -422,7 +422,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
writable: true,
|
||||
@@ -449,7 +449,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -469,7 +469,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
const result = await getCompleteFilterState(gridRef, []);
|
||||
|
||||
@@ -509,7 +509,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
// Mock activeElement in join AND operator
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
@@ -556,7 +556,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
// Mock activeElement in join OR operator
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
@@ -598,7 +598,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
writable: true,
|
||||
@@ -641,7 +641,7 @@ describe('filterStateManager', () => {
|
||||
|
||||
const gridRef = {
|
||||
current: { api: mockApi } as any,
|
||||
} as RefObject<AgGridReact | null>;
|
||||
} as RefObject<AgGridReact>;
|
||||
|
||||
Object.defineProperty(document, 'activeElement', {
|
||||
writable: true,
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"geostyler-wfs-parser": "^3.0.1",
|
||||
"ol": "^10.8.0",
|
||||
"polished": "*",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@
|
||||
"dayjs": "^1.11.21",
|
||||
"echarts": "*",
|
||||
"memoize-one": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -75,12 +75,8 @@ export default function EchartsMixedTimeseries({
|
||||
return {
|
||||
dataMask: {
|
||||
extraFormData: {
|
||||
// An empty lookup result means the clicked series could not be
|
||||
// resolved through the label map; emitting column filters anyway
|
||||
// would produce bogus `IS NULL` clauses (an empty array makes the
|
||||
// `every` below vacuously true), so clear the filters instead.
|
||||
filters:
|
||||
values.length === 0 || groupbyValues.length === 0
|
||||
values.length === 0
|
||||
? []
|
||||
: currentGroupBy.map((col, idx) => {
|
||||
const val: DataRecordValue[] = groupbyValues.map(v => {
|
||||
@@ -152,11 +148,10 @@ export default function EchartsMixedTimeseries({
|
||||
const drillToDetailFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
const drillByFilters: BinaryQueryObjectFilterClause[] = [];
|
||||
const isFirst = isFirstQuery(seriesIndex);
|
||||
const currentGroupBy = isFirst ? formData.groupby : formData.groupbyB;
|
||||
const seriesValues = (isFirst ? labelMap : labelMapB)[seriesName] || [];
|
||||
// Label map values may carry metric/offset labels ahead of the
|
||||
// dimension values — anchor from the tail, like getCrossFilterDataMask.
|
||||
const metricsCount = seriesValues.length - currentGroupBy.length;
|
||||
const values = [
|
||||
...(eventParams.name ? [eventParams.name] : []),
|
||||
...((isFirst ? labelMap : labelMapB)[eventParams.seriesName] || []),
|
||||
];
|
||||
if (data && xAxis.type === AxisType.Time) {
|
||||
drillToDetailFilters.push({
|
||||
col:
|
||||
@@ -169,39 +164,31 @@ export default function EchartsMixedTimeseries({
|
||||
formattedVal: xValueFormatter(data[0]),
|
||||
});
|
||||
}
|
||||
if (
|
||||
data &&
|
||||
xAxis.type === AxisType.Category &&
|
||||
eventParams.name != null
|
||||
) {
|
||||
[
|
||||
...(data && xAxis.type === AxisType.Category ? [xAxis.label] : []),
|
||||
...(isFirst ? formData.groupby : formData.groupbyB),
|
||||
].forEach((dimension, i) =>
|
||||
drillToDetailFilters.push({
|
||||
col: xAxis.label,
|
||||
col: dimension,
|
||||
op: '==',
|
||||
val: eventParams.name,
|
||||
formattedVal: String(eventParams.name),
|
||||
});
|
||||
}
|
||||
if (metricsCount >= 0) {
|
||||
currentGroupBy.forEach((dimension, i) => {
|
||||
const value = seriesValues[metricsCount + i];
|
||||
drillToDetailFilters.push({
|
||||
col: dimension,
|
||||
op: '==',
|
||||
val: value,
|
||||
formattedVal: String(value),
|
||||
});
|
||||
val: values[i],
|
||||
formattedVal: String(values[i]),
|
||||
}),
|
||||
);
|
||||
|
||||
[...(isFirst ? formData.groupby : formData.groupbyB)].forEach(
|
||||
(dimension, i) =>
|
||||
drillByFilters.push({
|
||||
col: dimension,
|
||||
op: '==',
|
||||
val: value,
|
||||
formattedVal: formatSeriesName(value, {
|
||||
val: values[i],
|
||||
formattedVal: formatSeriesName(values[i], {
|
||||
timeFormatter: getTimeFormatter(formData.dateFormat),
|
||||
numberFormatter: getNumberFormatter(formData.numberFormat),
|
||||
coltype: coltypeMapping?.[getColumnLabel(dimension)],
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
const hasCrossFilter =
|
||||
(isFirst && groupby.length > 0) || (!isFirst && groupbyB.length > 0);
|
||||
|
||||
|
||||
@@ -146,14 +146,10 @@ export default function transformProps(
|
||||
columnFormats = {},
|
||||
currencyCodeColumn,
|
||||
} = datasource;
|
||||
// "raw" because these are keyed by the backend column labels; the maps
|
||||
// returned to the component are re-keyed by the rendered series names below.
|
||||
const { label_map: rawLabelMap, detected_currency: backendDetectedCurrency } =
|
||||
const { label_map: labelMap, detected_currency: backendDetectedCurrency } =
|
||||
queriesData[0] as TimeseriesChartDataResponseResult;
|
||||
const {
|
||||
label_map: rawLabelMapB,
|
||||
detected_currency: backendDetectedCurrencyB,
|
||||
} = queriesData[1] as TimeseriesChartDataResponseResult;
|
||||
const { label_map: labelMapB, detected_currency: backendDetectedCurrencyB } =
|
||||
queriesData[1] as TimeseriesChartDataResponseResult;
|
||||
const data1 = (queriesData[0].data || []) as TimeseriesDataRecord[];
|
||||
const data2 = (queriesData[1].data || []) as TimeseriesDataRecord[];
|
||||
const annotationData = getAnnotationData(chartProps);
|
||||
@@ -442,16 +438,6 @@ export default function transformProps(
|
||||
const array = ensureIsArray(chartProps.rawFormData?.time_compare);
|
||||
const inverted = invert(verboseMap);
|
||||
|
||||
// The rendered ECharts series names are display names that can diverge from
|
||||
// the backend `label_map` keys: the metric display name is prepended when
|
||||
// dimensions are present, query identifiers may be appended, and verbose
|
||||
// names replace the raw column labels. Cross-filtering and drill lookups in
|
||||
// EchartsMixedTimeseries resolve the clicked series name through the label
|
||||
// map, so expose maps re-keyed by the rendered series names to keep those
|
||||
// lookups working (#41622).
|
||||
const displayLabelMap: Record<string, string[]> = {};
|
||||
const displayLabelMapB: Record<string, string[]> = {};
|
||||
|
||||
rawSeriesA.forEach(entry => {
|
||||
const entryName = String(entry.name || '');
|
||||
const seriesName = inverted[entryName] || entryName;
|
||||
@@ -471,19 +457,13 @@ export default function transformProps(
|
||||
// When no groupby, format as just the entry name with optional query identifier
|
||||
displayName = showQueryIdentifiers ? `${entryName} (Query A)` : entryName;
|
||||
}
|
||||
|
||||
const labelMapValues = rawLabelMap?.[seriesName];
|
||||
if (labelMapValues) {
|
||||
displayLabelMap[displayName] = labelMapValues;
|
||||
}
|
||||
|
||||
const axisFormatterConfig = getAxisFormatterConfig(yAxisIndex);
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
axisFormatterConfig.customFormatters,
|
||||
axisFormatterConfig.formatter,
|
||||
metrics,
|
||||
labelMapValues?.[0],
|
||||
labelMap?.[seriesName]?.[0],
|
||||
!!contributionMode,
|
||||
);
|
||||
|
||||
@@ -534,6 +514,7 @@ export default function transformProps(
|
||||
rawSeriesB.forEach(entry => {
|
||||
const entryName = String(entry.name || '');
|
||||
const seriesEntry = inverted[entryName] || entryName;
|
||||
const seriesName = `${seriesEntry} (1)`;
|
||||
const colorScaleKey = getOriginalSeries(seriesEntry, array);
|
||||
|
||||
let displayName: string;
|
||||
@@ -550,19 +531,13 @@ export default function transformProps(
|
||||
// When no groupby, format as just the entry name with optional query identifier
|
||||
displayName = showQueryIdentifiers ? `${entryName} (Query B)` : entryName;
|
||||
}
|
||||
|
||||
const labelMapValuesB = rawLabelMapB?.[seriesEntry];
|
||||
if (labelMapValuesB) {
|
||||
displayLabelMapB[displayName] = labelMapValuesB;
|
||||
}
|
||||
|
||||
const axisFormatterConfig = getAxisFormatterConfig(yAxisIndexB);
|
||||
|
||||
const seriesFormatter = getFormatter(
|
||||
axisFormatterConfig.customFormatters,
|
||||
axisFormatterConfig.formatter,
|
||||
metricsB,
|
||||
labelMapValuesB?.[0],
|
||||
labelMapB?.[seriesName]?.[0],
|
||||
!!contributionMode,
|
||||
);
|
||||
|
||||
@@ -834,15 +809,15 @@ export default function transformProps(
|
||||
.filter(key => keys.includes(key))
|
||||
.forEach(key => {
|
||||
const value = forecastValues[key];
|
||||
// The tooltip key is the rendered series name; resolve it through
|
||||
// the display-keyed maps, whose values lead with the raw metric
|
||||
// label both with and without dimensions. Fall back to the
|
||||
// verbose-name inversion for series absent from the maps.
|
||||
// if there are no dimensions, key is a verbose name of a metric,
|
||||
// otherwise it is a comma separated string where the first part is metric name
|
||||
let formatterKey;
|
||||
if (primarySeries.has(key)) {
|
||||
formatterKey = displayLabelMap[key]?.[0] ?? inverted[key];
|
||||
formatterKey =
|
||||
groupby.length === 0 ? inverted[key] : labelMap[key]?.[0];
|
||||
} else {
|
||||
formatterKey = displayLabelMapB[key]?.[0] ?? inverted[key];
|
||||
formatterKey =
|
||||
groupbyB.length === 0 ? inverted[key] : labelMapB[key]?.[0];
|
||||
}
|
||||
const tooltipFormatter = getFormatter(
|
||||
customFormatters,
|
||||
@@ -937,8 +912,8 @@ export default function transformProps(
|
||||
echartOptions: mergedEchartOptions,
|
||||
setDataMask,
|
||||
emitCrossFilters,
|
||||
labelMap: displayLabelMap,
|
||||
labelMapB: displayLabelMapB,
|
||||
labelMap,
|
||||
labelMapB,
|
||||
groupby,
|
||||
groupbyB,
|
||||
seriesBreakdown: rawSeriesA.length,
|
||||
|
||||
@@ -66,9 +66,7 @@ export default function EchartsTimeseries({
|
||||
const echartRef = useRef<EchartsHandler | null>(null);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
refs.echartRef = echartRef;
|
||||
const clickTimer = useRef<ReturnType<typeof setTimeout> | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const clickTimer = useRef<ReturnType<typeof setTimeout>>();
|
||||
const extraControlRef = useRef<HTMLDivElement>(null);
|
||||
const [extraControlHeight, setExtraControlHeight] = useState(0);
|
||||
useEffect(() => {
|
||||
|
||||
@@ -165,7 +165,7 @@ function Echart(
|
||||
refs.divRef = divRef;
|
||||
}
|
||||
const [didMount, setDidMount] = useState(false);
|
||||
const chartRef = useRef<EChartsType | null>(null);
|
||||
const chartRef = useRef<EChartsType>();
|
||||
const previousQueryEventHandlers = useRef<QueryEventHandlers>([]);
|
||||
const currentSelection = useMemo(
|
||||
() => Object.keys(selectedValues) || [],
|
||||
@@ -174,7 +174,7 @@ function Echart(
|
||||
const previousSelection = useRef<string[]>([]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
getEchartInstance: () => chartRef.current ?? undefined,
|
||||
getEchartInstance: () => chartRef.current,
|
||||
}));
|
||||
|
||||
const locale = useSelector(
|
||||
|
||||
@@ -44,7 +44,7 @@ export type EchartsStylesProps = {
|
||||
|
||||
export type Refs = {
|
||||
echartRef?: Ref<EchartsHandler>;
|
||||
divRef?: RefObject<HTMLDivElement | null>;
|
||||
divRef?: RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export interface EchartsProps {
|
||||
|
||||
@@ -1,223 +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.
|
||||
*/
|
||||
import { render } from '@testing-library/react';
|
||||
import {
|
||||
ChartDataResponseResult,
|
||||
DataRecord,
|
||||
VizType,
|
||||
} from '@superset-ui/core';
|
||||
import { GenericDataType } from '@apache-superset/core/common';
|
||||
import transformProps from '../../src/MixedTimeseries/transformProps';
|
||||
import EchartsMixedTimeseries from '../../src/MixedTimeseries/EchartsMixedTimeseries';
|
||||
import {
|
||||
DEFAULT_FORM_DATA,
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps,
|
||||
} from '../../src/MixedTimeseries/types';
|
||||
import Echart from '../../src/components/Echart';
|
||||
import { createEchartsTimeseriesTestChartProps } from '../helpers';
|
||||
|
||||
jest.mock('../../src/components/Echart', () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => null),
|
||||
}));
|
||||
|
||||
const mockedEchart = jest.mocked(Echart);
|
||||
|
||||
const ts1 = 1704067200000;
|
||||
const ts2 = 1704153600000;
|
||||
|
||||
/**
|
||||
* Backend-shaped fixture: flattened column names keep the metric label, so
|
||||
* label_map values lead with the metric label ahead of the dimension value.
|
||||
*/
|
||||
function createQueryData(): ChartDataResponseResult {
|
||||
const rows = [
|
||||
{ ds: ts1, 'sum__num, boy': 1, 'sum__num, girl': 2 },
|
||||
{ ds: ts2, 'sum__num, boy': 3, 'sum__num, girl': 4 },
|
||||
];
|
||||
return {
|
||||
annotation_data: null,
|
||||
cache_key: null,
|
||||
cache_timeout: null,
|
||||
cached_dttm: null,
|
||||
queried_dttm: null,
|
||||
data: rows as DataRecord[],
|
||||
colnames: ['ds', 'sum__num, boy', 'sum__num, girl'],
|
||||
coltypes: [
|
||||
GenericDataType.Temporal,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
],
|
||||
error: null,
|
||||
is_cached: false,
|
||||
query: '',
|
||||
rowcount: rows.length,
|
||||
sql_rowcount: rows.length,
|
||||
stacktrace: null,
|
||||
status: 'success',
|
||||
from_dttm: null,
|
||||
to_dttm: null,
|
||||
label_map: {
|
||||
ds: ['ds'],
|
||||
'sum__num, boy': ['sum__num', 'boy'],
|
||||
'sum__num, girl': ['sum__num', 'girl'],
|
||||
},
|
||||
} as ChartDataResponseResult;
|
||||
}
|
||||
|
||||
function setup(
|
||||
formDataOverrides: Partial<EchartsMixedTimeseriesFormData> = {},
|
||||
) {
|
||||
const queryData = createQueryData();
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
defaultFormData: DEFAULT_FORM_DATA as EchartsMixedTimeseriesFormData,
|
||||
defaultVizType: 'mixed_timeseries',
|
||||
defaultQueriesData: [queryData, queryData],
|
||||
formData: {
|
||||
colorScheme: 'bnbColors',
|
||||
metrics: ['sum__num'],
|
||||
metricsB: ['sum__num'],
|
||||
groupby: ['gender'],
|
||||
groupbyB: ['gender'],
|
||||
x_axis: 'ds',
|
||||
viz_type: VizType.MixedTimeseries,
|
||||
...formDataOverrides,
|
||||
},
|
||||
queriesData: [queryData, queryData],
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
const setDataMask = jest.fn();
|
||||
const onContextMenu = jest.fn();
|
||||
const onFocusedSeries = jest.fn();
|
||||
render(
|
||||
<EchartsMixedTimeseries
|
||||
{...transformed}
|
||||
setDataMask={setDataMask}
|
||||
onContextMenu={onContextMenu}
|
||||
onFocusedSeries={onFocusedSeries}
|
||||
emitCrossFilters
|
||||
/>,
|
||||
);
|
||||
const lastCall = mockedEchart.mock.calls[mockedEchart.mock.calls.length - 1];
|
||||
const { eventHandlers } = lastCall[0] as any;
|
||||
return { eventHandlers, setDataMask, onContextMenu, onFocusedSeries };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
mockedEchart.mockClear();
|
||||
});
|
||||
|
||||
test('EchartsMixedTimeseries click emits cross-filter with tail-anchored dimension values', () => {
|
||||
const { eventHandlers, setDataMask } = setup();
|
||||
|
||||
eventHandlers.click({ seriesName: 'sum__num, boy', seriesIndex: 0 });
|
||||
|
||||
expect(setDataMask).toHaveBeenCalledTimes(1);
|
||||
const dataMask = setDataMask.mock.calls[0][0];
|
||||
// label_map values are ['sum__num', 'boy'] — the metric label must be
|
||||
// skipped and the dimension value emitted.
|
||||
expect(dataMask.extraFormData.filters).toEqual([
|
||||
{ col: 'gender', op: 'IN', val: ['boy'] },
|
||||
]);
|
||||
expect(dataMask.filterState.selectedValues).toEqual(['sum__num, boy']);
|
||||
});
|
||||
|
||||
test('EchartsMixedTimeseries click clears filters when the series misses the label map', () => {
|
||||
const { eventHandlers, setDataMask } = setup();
|
||||
|
||||
eventHandlers.click({ seriesName: 'not a series', seriesIndex: 0 });
|
||||
|
||||
expect(setDataMask).toHaveBeenCalledTimes(1);
|
||||
const dataMask = setDataMask.mock.calls[0][0];
|
||||
// An unresolvable series must not emit bogus IS NULL filters (#41622).
|
||||
expect(dataMask.extraFormData.filters).toEqual([]);
|
||||
expect(dataMask.filterState.value).toBeNull();
|
||||
});
|
||||
|
||||
test('EchartsMixedTimeseries context menu drills with tail-anchored dimension values', async () => {
|
||||
const { eventHandlers, onContextMenu } = setup();
|
||||
|
||||
await eventHandlers.contextmenu({
|
||||
data: [ts1, 1],
|
||||
seriesName: 'sum__num, boy',
|
||||
seriesIndex: 0,
|
||||
name: '',
|
||||
event: { stop: jest.fn(), event: { clientX: 11, clientY: 22 } },
|
||||
});
|
||||
|
||||
expect(onContextMenu).toHaveBeenCalledTimes(1);
|
||||
const [x, y, payload] = onContextMenu.mock.calls[0];
|
||||
expect(x).toBe(11);
|
||||
expect(y).toBe(22);
|
||||
expect(payload.drillToDetail).toEqual([
|
||||
expect.objectContaining({ col: 'ds', op: '==', val: ts1 }),
|
||||
expect.objectContaining({
|
||||
col: 'gender',
|
||||
op: '==',
|
||||
val: 'boy',
|
||||
formattedVal: 'boy',
|
||||
}),
|
||||
]);
|
||||
expect(payload.drillBy.filters).toEqual([
|
||||
expect.objectContaining({ col: 'gender', op: '==', val: 'boy' }),
|
||||
]);
|
||||
expect(payload.drillBy.groupbyFieldName).toBe('groupby');
|
||||
expect(payload.crossFilter.dataMask.extraFormData.filters).toEqual([
|
||||
{ col: 'gender', op: 'IN', val: ['boy'] },
|
||||
]);
|
||||
});
|
||||
|
||||
test('EchartsMixedTimeseries context menu emits the category x-axis filter', async () => {
|
||||
const { eventHandlers, onContextMenu } = setup({
|
||||
xAxisForceCategorical: true,
|
||||
});
|
||||
|
||||
await eventHandlers.contextmenu({
|
||||
data: ['boy-cat', 1],
|
||||
seriesName: 'sum__num, girl',
|
||||
seriesIndex: 1,
|
||||
name: 'boy-cat',
|
||||
event: { stop: jest.fn(), event: { clientX: 0, clientY: 0 } },
|
||||
});
|
||||
|
||||
const payload = onContextMenu.mock.calls[0][2];
|
||||
expect(payload.drillToDetail).toEqual([
|
||||
expect.objectContaining({
|
||||
col: 'ds',
|
||||
op: '==',
|
||||
val: 'boy-cat',
|
||||
formattedVal: 'boy-cat',
|
||||
}),
|
||||
expect.objectContaining({ col: 'gender', op: '==', val: 'girl' }),
|
||||
]);
|
||||
});
|
||||
|
||||
test('EchartsMixedTimeseries hover focuses and unfocuses the series', () => {
|
||||
const { eventHandlers, onFocusedSeries } = setup();
|
||||
|
||||
eventHandlers.mouseover({ seriesName: 'sum__num, boy' });
|
||||
expect(onFocusedSeries).toHaveBeenLastCalledWith('sum__num, boy');
|
||||
|
||||
eventHandlers.mouseout();
|
||||
expect(onFocusedSeries).toHaveBeenLastCalledWith(null);
|
||||
});
|
||||
@@ -777,198 +777,6 @@ test('xAxisForceCategorical forces Category axis regardless of Numeric coltype',
|
||||
expect(xAxis.type).toBe(AxisType.Category);
|
||||
});
|
||||
|
||||
// labelMap/labelMapB must be keyed by the rendered series names or the
|
||||
// cross-filter/drill lookups in EchartsMixedTimeseries miss (#41622);
|
||||
// see the re-key comment in transformProps.ts.
|
||||
test('cross-filter label maps are keyed by the rendered series names', () => {
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
|
||||
defaultQueriesData: queriesData,
|
||||
formData: { ...formData, showQueryIdentifiers: false },
|
||||
queriesData,
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
|
||||
// The backend label_map is keyed by the flattened column names
|
||||
// ("boy"/"girl") while the rendered series are "sum__num, boy" etc.
|
||||
expect(transformed.labelMap).toEqual({
|
||||
'sum__num, boy': ['boy'],
|
||||
'sum__num, girl': ['girl'],
|
||||
});
|
||||
expect(transformed.labelMapB).toEqual({
|
||||
'sum__num, boy': ['boy'],
|
||||
'sum__num, girl': ['girl'],
|
||||
});
|
||||
});
|
||||
|
||||
test('cross-filter label maps resolve every rendered series name', () => {
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
|
||||
defaultQueriesData: queriesData,
|
||||
formData: { ...formData, showQueryIdentifiers: true },
|
||||
queriesData,
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
|
||||
const names = (transformed.echartOptions.series as SeriesOption[]).map(
|
||||
series => String(series.name),
|
||||
);
|
||||
expect(names).toHaveLength(4);
|
||||
names
|
||||
.slice(0, transformed.seriesBreakdown)
|
||||
.forEach(name => expect(transformed.labelMap[name]).toBeDefined());
|
||||
names
|
||||
.slice(transformed.seriesBreakdown)
|
||||
.forEach(name => expect(transformed.labelMapB[name]).toBeDefined());
|
||||
});
|
||||
|
||||
test('cross-filter label maps resolve verbose series names to raw label_map values', () => {
|
||||
const verboseRows = [
|
||||
{ ds: 599616000000, sum__num: 1 },
|
||||
{ ds: 599916000000, sum__num: 3 },
|
||||
];
|
||||
const verboseQueryData = createTestQueryData(verboseRows, {
|
||||
label_map: { ds: ['ds'], sum__num: ['sum__num'] },
|
||||
});
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
|
||||
defaultQueriesData: [verboseQueryData, verboseQueryData],
|
||||
formData: { ...formData, groupby: [], groupbyB: [] },
|
||||
queriesData: [verboseQueryData, verboseQueryData],
|
||||
datasource: {
|
||||
verboseMap: { sum__num: 'Total Births' },
|
||||
},
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
|
||||
// rebaseForecastDatum renames data columns to their verbose names, so the
|
||||
// rendered series is "Total Births" while label_map stays keyed by
|
||||
// "sum__num" — the display-keyed map bridges the two.
|
||||
expect(transformed.labelMap['Total Births']).toEqual(['sum__num']);
|
||||
expect(transformed.labelMapB['Total Births']).toEqual(['sum__num']);
|
||||
});
|
||||
|
||||
test('tooltip resolves per-metric formats through the display-keyed label map', () => {
|
||||
// Multi-metric so getCustomFormatter cannot short-circuit on a single
|
||||
// saved metric: the formatter key must come from resolving the rendered
|
||||
// series name through the display-keyed map.
|
||||
const rows = [{ ds: 599616000000, 'sum__num, boy': 0.5, 'avg__num, boy': 1 }];
|
||||
const queryData = createTestQueryData(rows, {
|
||||
colnames: ['ds', 'sum__num, boy', 'avg__num, boy'],
|
||||
coltypes: [
|
||||
GenericDataType.Temporal,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
],
|
||||
label_map: {
|
||||
ds: ['ds'],
|
||||
'sum__num, boy': ['sum__num', 'boy'],
|
||||
'avg__num, boy': ['avg__num', 'boy'],
|
||||
},
|
||||
});
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
|
||||
defaultQueriesData: [queryData, queryData],
|
||||
formData: {
|
||||
...formData,
|
||||
metrics: ['sum__num', 'avg__num'],
|
||||
x_axis: 'ds',
|
||||
yAxisFormat: undefined,
|
||||
},
|
||||
queriesData: [queryData, queryData],
|
||||
datasource: {
|
||||
columnFormats: { sum__num: '.2%' },
|
||||
},
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
|
||||
const formatter = (transformed.echartOptions.tooltip as any).formatter as (
|
||||
params: unknown,
|
||||
) => string;
|
||||
const html = formatter({
|
||||
value: [599616000000, 0.5],
|
||||
seriesId: 'sum__num, boy',
|
||||
marker: '',
|
||||
color: '#333',
|
||||
});
|
||||
|
||||
expect(html).toContain('50.00%');
|
||||
});
|
||||
|
||||
test('tooltip resolves per-metric formats for secondary-query series', () => {
|
||||
const rowsA = [
|
||||
{ ds: 599616000000, 'sum__num, boy': 0.5, 'avg__num, boy': 1 },
|
||||
];
|
||||
const queryDataA = createTestQueryData(rowsA, {
|
||||
colnames: ['ds', 'sum__num, boy', 'avg__num, boy'],
|
||||
coltypes: [
|
||||
GenericDataType.Temporal,
|
||||
GenericDataType.Numeric,
|
||||
GenericDataType.Numeric,
|
||||
],
|
||||
label_map: {
|
||||
ds: ['ds'],
|
||||
'sum__num, boy': ['sum__num', 'boy'],
|
||||
'avg__num, boy': ['avg__num', 'boy'],
|
||||
},
|
||||
});
|
||||
const rowsB = [{ ds: 599616000000, 'count__num, boy': 2.5 }];
|
||||
const queryDataB = createTestQueryData(rowsB, {
|
||||
colnames: ['ds', 'count__num, boy'],
|
||||
coltypes: [GenericDataType.Temporal, GenericDataType.Numeric],
|
||||
label_map: {
|
||||
ds: ['ds'],
|
||||
'count__num, boy': ['count__num', 'boy'],
|
||||
},
|
||||
});
|
||||
const chartProps = createEchartsTimeseriesTestChartProps<
|
||||
EchartsMixedTimeseriesFormData,
|
||||
EchartsMixedTimeseriesProps
|
||||
>({
|
||||
...MIXED_TIMESERIES_CHART_PROPS_DEFAULTS,
|
||||
defaultQueriesData: [queryDataA, queryDataB],
|
||||
formData: {
|
||||
...formData,
|
||||
metrics: ['sum__num', 'avg__num'],
|
||||
metricsB: ['count__num', 'max__num'],
|
||||
x_axis: 'ds',
|
||||
yAxisFormat: undefined,
|
||||
yAxisFormatSecondary: undefined,
|
||||
yAxisIndex: 0,
|
||||
yAxisIndexB: 1,
|
||||
},
|
||||
queriesData: [queryDataA, queryDataB],
|
||||
datasource: {
|
||||
columnFormats: { count__num: '.1f' },
|
||||
},
|
||||
});
|
||||
const transformed = transformProps(chartProps);
|
||||
|
||||
const formatter = (transformed.echartOptions.tooltip as any).formatter as (
|
||||
params: unknown,
|
||||
) => string;
|
||||
const html = formatter({
|
||||
value: [599616000000, 2.5],
|
||||
seriesId: 'count__num, boy',
|
||||
marker: '',
|
||||
color: '#333',
|
||||
});
|
||||
|
||||
expect(html).toContain('2.5');
|
||||
});
|
||||
|
||||
test('temporal x coltype wires the time formatter and Time axis', () => {
|
||||
// Regression guard: the happy path for mixed-timeseries charts. Ensures
|
||||
// Temporal coltype still routes through the TimeFormatter so the time axis
|
||||
|
||||
@@ -40,9 +40,9 @@
|
||||
"handlebars": "^4.7.8",
|
||||
"lodash": "^4.18.1",
|
||||
"dayjs": "^1.11.21",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-ace": "^14.0.1",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-ace": "^10.1.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jest": "^30.0.0",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { ReactNode, ReactElement } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
interface ControlHeaderProps {
|
||||
children: ReactNode;
|
||||
@@ -24,7 +24,7 @@ interface ControlHeaderProps {
|
||||
|
||||
export const ControlHeader = ({
|
||||
children,
|
||||
}: ControlHeaderProps): ReactElement => (
|
||||
}: ControlHeaderProps): JSX.Element => (
|
||||
<div className="ControlHeader">
|
||||
<div className="pull-left">{children}</div>
|
||||
</div>
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
"@superset-ui/core": "*",
|
||||
"lodash": "^4.18.1",
|
||||
"prop-types": "*",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/types": "^7.29.7",
|
||||
|
||||
@@ -1541,3 +1541,4 @@ TableRenderer.propTypes = {
|
||||
tableOptions: PropTypes.object,
|
||||
onContextMenu: PropTypes.func,
|
||||
};
|
||||
TableRenderer.defaultProps = { ...PivotData.defaultProps, tableOptions: {} };
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -40,14 +40,14 @@
|
||||
"@apache-superset/core": "*",
|
||||
"@superset-ui/chart-controls": "*",
|
||||
"@superset-ui/core": "*",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/dom": "^9.3.4",
|
||||
"@testing-library/jest-dom": "*",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@testing-library/user-event": "*",
|
||||
"@types/react": "*",
|
||||
"match-sorter": "^8.2.0",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
DragEvent,
|
||||
useEffect,
|
||||
useMemo,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
import { typedMemo, usePrevious } from '@superset-ui/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
@@ -80,10 +79,10 @@ export interface DataTableProps<D extends object> extends TableOptions<D> {
|
||||
noResults?: string | ((filterString: string) => ReactNode);
|
||||
sticky?: boolean;
|
||||
rowCount: number;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement | null>;
|
||||
wrapperRef?: MutableRefObject<HTMLDivElement>;
|
||||
onColumnOrderChange?: () => void;
|
||||
renderGroupingHeaders?: () => ReactElement;
|
||||
renderTimeComparisonDropdown?: () => ReactElement;
|
||||
renderGroupingHeaders?: () => JSX.Element;
|
||||
renderTimeComparisonDropdown?: () => JSX.Element;
|
||||
handleSortByChange: (sortBy: SortByItem[]) => void;
|
||||
sortByFromParent: SortByItem[];
|
||||
manualSearch?: boolean;
|
||||
@@ -139,7 +138,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
onFilteredDataChange,
|
||||
onFilteredRowsChange,
|
||||
...moreUseTableOptions
|
||||
}: DataTableProps<D>): ReactElement {
|
||||
}: DataTableProps<D>): JSX.Element {
|
||||
const tableHooks: PluginHook<D>[] = [
|
||||
useGlobalFilter,
|
||||
useSortBy,
|
||||
@@ -344,7 +343,7 @@ export default typedMemo(function DataTable<D extends object>({
|
||||
if (!columns || columns.length === 0) {
|
||||
return (
|
||||
wrapStickyTable ? wrapStickyTable(getNoResults) : getNoResults()
|
||||
) as ReactElement;
|
||||
) as JSX.Element;
|
||||
}
|
||||
|
||||
const shouldRenderFooter = columns.some(x => !!x.Footer);
|
||||
|
||||
@@ -28,7 +28,6 @@ import {
|
||||
ComponentPropsWithRef,
|
||||
CSSProperties,
|
||||
UIEventHandler,
|
||||
type JSX,
|
||||
} from 'react';
|
||||
import { TableInstance, Hooks } from 'react-table';
|
||||
import { useTheme, css } from '@apache-superset/core/theme';
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function useMountedMemo<T>(
|
||||
factory: () => T,
|
||||
deps?: unknown[],
|
||||
): T | undefined {
|
||||
const mounted = useRef<typeof factory | null>(null);
|
||||
const mounted = useRef<typeof factory>();
|
||||
useLayoutEffect(() => {
|
||||
mounted.current = factory;
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
useEffect,
|
||||
useRef,
|
||||
ReactElement,
|
||||
} from 'react';
|
||||
|
||||
import {
|
||||
@@ -766,7 +765,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[comparisonLabels],
|
||||
);
|
||||
|
||||
const renderTimeComparisonDropdown = (): ReactElement => {
|
||||
const renderTimeComparisonDropdown = (): JSX.Element => {
|
||||
const allKey = comparisonColumns[0].key;
|
||||
const handleOnClick = (data: any) => {
|
||||
const { key } = data;
|
||||
@@ -873,7 +872,7 @@ export default function TableChart<D extends DataRecord = DataRecord>(
|
||||
[visibleColumnsMeta, getHeaderColumns, isUsingTimeComparison],
|
||||
);
|
||||
|
||||
const renderGroupingHeaders = (): ReactElement => {
|
||||
const renderGroupingHeaders = (): JSX.Element => {
|
||||
// TODO: Make use of ColumnGroup to render the aditional headers
|
||||
const headers: any = [];
|
||||
let currentColumnIndex = 0;
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"@superset-ui/core": "*",
|
||||
"@types/lodash": "*",
|
||||
"@types/react": "*",
|
||||
"react": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3-cloud": "^1.2.9"
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
*/
|
||||
import { isCustomControlItem } from '@superset-ui/chart-controls';
|
||||
import controlPanel from '../src/plugin/controlPanel';
|
||||
import React, { type ReactElement } from 'react';
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
const isNameControl = (
|
||||
item: unknown,
|
||||
|
||||
@@ -51,7 +51,6 @@
|
||||
"prop-types": "^15.8.1",
|
||||
"react-map-gl": "^8.1.1",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"underscore": "^1.13.7",
|
||||
"urijs": "^1.19.11",
|
||||
"xss": "^1.0.15",
|
||||
"lodash-es": "^4.18.1"
|
||||
@@ -59,7 +58,6 @@
|
||||
"devDependencies": {
|
||||
"@types/mapbox__geojson-extent": "^1.0.3",
|
||||
"@types/ngeohash": "^0.6.8",
|
||||
"@types/underscore": "^1.13.0",
|
||||
"@types/urijs": "^1.19.26"
|
||||
},
|
||||
"peerDependencies": {
|
||||
@@ -68,8 +66,8 @@
|
||||
"@superset-ui/core": "*",
|
||||
"dayjs": "^1.11.21",
|
||||
"mapbox-gl": ">=1.0.0",
|
||||
"react": "^18.3.0 || ^19.0.0",
|
||||
"react-dom": "^18.3.0 || ^19.0.0"
|
||||
"react": "^18.3.0",
|
||||
"react-dom": "^18.3.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"mapbox-gl": {
|
||||
|
||||
@@ -40,7 +40,6 @@ import type { Layer } from '@deck.gl/core';
|
||||
import Legend from './components/Legend';
|
||||
import { hexToRGB } from './utils/colors';
|
||||
import { getMapboxApiKey } from './utils/mapbox';
|
||||
import sandboxedEval from './utils/sandbox';
|
||||
import fitViewport, { Viewport } from './utils/fitViewport';
|
||||
import {
|
||||
DeckGLContainerHandle,
|
||||
@@ -173,12 +172,6 @@ const CategoricalDeckGLContainer = (props: CategoricalDeckGLContainerProps) => {
|
||||
// Add colors from categories or fixed color
|
||||
features = addColor(features, fd, selectedColorScheme);
|
||||
|
||||
// Apply user defined data mutator if defined
|
||||
if (fd.js_data_mutator) {
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
features = jsFnMutator(features);
|
||||
}
|
||||
|
||||
// Show only categories selected in the legend
|
||||
if (fd.dimension) {
|
||||
features = features.filter(d => categories[d.cat_color]?.enabled);
|
||||
|
||||
@@ -112,7 +112,7 @@ export const DeckGLContainer = memo(
|
||||
}, [props.layers]);
|
||||
|
||||
const isCustomTooltip = (content: ReactNode): boolean =>
|
||||
isValidElement<{ 'data-tooltip-type'?: string }>(content) &&
|
||||
isValidElement(content) &&
|
||||
content.props?.['data-tooltip-type'] === 'custom';
|
||||
|
||||
const renderTooltip = (tooltipState: TooltipProps['tooltip']) => {
|
||||
|
||||
@@ -116,7 +116,7 @@ const selectDataMask = createSelector(
|
||||
);
|
||||
|
||||
const DeckMulti = (props: DeckMultiProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle | null>(null);
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
|
||||
const dataMask = useSelector(selectDataMask);
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ export function createDeckGLComponent(
|
||||
) {
|
||||
// Higher order component
|
||||
return memo((props: DeckGLComponentProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle | null>(null);
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
const prevFormData = usePrevious(props.formData);
|
||||
const prevFilterState = usePrevious(props.filterState);
|
||||
const prevPayload = usePrevious(props.payload);
|
||||
|
||||
@@ -32,19 +32,12 @@ export interface DeckArcFormData extends SqlaFormData {
|
||||
start_spatial: SpatialFormData['spatial'];
|
||||
end_spatial: SpatialFormData['spatial'];
|
||||
dimension?: string;
|
||||
js_columns?: string[];
|
||||
tooltip_contents?: unknown[];
|
||||
tooltip_template?: string;
|
||||
}
|
||||
|
||||
export default function buildQuery(formData: DeckArcFormData) {
|
||||
const {
|
||||
start_spatial,
|
||||
end_spatial,
|
||||
dimension,
|
||||
js_columns,
|
||||
tooltip_contents,
|
||||
} = formData;
|
||||
const { start_spatial, end_spatial, dimension, tooltip_contents } = formData;
|
||||
|
||||
if (!start_spatial || !end_spatial) {
|
||||
throw new Error(
|
||||
@@ -66,13 +59,6 @@ export default function buildQuery(formData: DeckArcFormData) {
|
||||
columns = [...columns, dimension];
|
||||
}
|
||||
|
||||
const jsColumns = ensureIsArray(js_columns || []);
|
||||
jsColumns.forEach(col => {
|
||||
if (!columns.includes(col)) {
|
||||
columns.push(col);
|
||||
}
|
||||
});
|
||||
|
||||
columns = addTooltipColumnsToQuery(columns, tooltip_contents);
|
||||
|
||||
let filters = addSpatialNullFilters(
|
||||
|
||||
@@ -31,10 +31,6 @@ import {
|
||||
import {
|
||||
filterNulls,
|
||||
autozoom,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsTooltip,
|
||||
jsOnclickHref,
|
||||
legendFormat,
|
||||
legendPosition,
|
||||
viewport,
|
||||
@@ -166,15 +162,6 @@ const config: ControlPanelConfig = {
|
||||
[legendFormat],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -126,10 +126,6 @@ export const ArcChartViz = ({
|
||||
stroke_width: strokeWidth,
|
||||
legend_position: 'tr',
|
||||
legend_format: null,
|
||||
js_columns: [],
|
||||
js_data_mutator: '',
|
||||
js_tooltip: '',
|
||||
js_onclick_href: '',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -17,11 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import { ChartProps } from '@superset-ui/core';
|
||||
import {
|
||||
processSpatialData,
|
||||
addJsColumnsToExtraProps,
|
||||
DataRecord,
|
||||
} from '../spatialUtils';
|
||||
import { processSpatialData, DataRecord } from '../spatialUtils';
|
||||
import {
|
||||
createBaseTransformResult,
|
||||
getRecordsFromQuery,
|
||||
@@ -43,7 +39,6 @@ function processArcData(
|
||||
startSpatial: DeckArcFormData['start_spatial'],
|
||||
endSpatial: DeckArcFormData['end_spatial'],
|
||||
dimension?: string,
|
||||
jsColumns?: string[],
|
||||
): ArcPoint[] {
|
||||
if (!startSpatial || !endSpatial || !records.length) {
|
||||
return [];
|
||||
@@ -52,9 +47,7 @@ function processArcData(
|
||||
const startFeatures = processSpatialData(records, startSpatial);
|
||||
const endFeatures = processSpatialData(records, endSpatial);
|
||||
const excludeKeys = new Set(
|
||||
['__timestamp', dimension, ...(jsColumns || [])].filter(
|
||||
(key): key is string => key != null,
|
||||
),
|
||||
['__timestamp', dimension].filter((key): key is string => key != null),
|
||||
);
|
||||
|
||||
return records
|
||||
@@ -72,8 +65,6 @@ function processArcData(
|
||||
extraProps: {},
|
||||
};
|
||||
|
||||
arcPoint = addJsColumnsToExtraProps(arcPoint, record, jsColumns);
|
||||
|
||||
if (dimension && record[dimension] != null) {
|
||||
arcPoint.cat_color = String(record[dimension]);
|
||||
}
|
||||
@@ -92,8 +83,7 @@ function processArcData(
|
||||
|
||||
export default function transformProps(chartProps: ChartProps) {
|
||||
const { rawFormData: formData } = chartProps;
|
||||
const { start_spatial, end_spatial, dimension, js_columns } =
|
||||
formData as DeckArcFormData;
|
||||
const { start_spatial, end_spatial, dimension } = formData as DeckArcFormData;
|
||||
|
||||
const records = getRecordsFromQuery(chartProps.queriesData);
|
||||
const features = processArcData(
|
||||
@@ -101,7 +91,6 @@ export default function transformProps(chartProps: ChartProps) {
|
||||
start_spatial,
|
||||
end_spatial,
|
||||
dimension,
|
||||
js_columns,
|
||||
);
|
||||
|
||||
return createBaseTransformResult(chartProps, features);
|
||||
|
||||
@@ -21,7 +21,6 @@ import { PolygonLayer } from '@deck.gl/layers';
|
||||
import { Position } from '@deck.gl/core';
|
||||
import { t } from '@apache-superset/core/translation';
|
||||
import { commonLayerProps } from '../common';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { GetLayerType, createDeckGLComponent } from '../../factory';
|
||||
import { ColorType } from '../../types';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
@@ -69,13 +68,8 @@ export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
emitCrossFilters,
|
||||
}) {
|
||||
const fd = formData;
|
||||
const {
|
||||
aggregation = 'SUM',
|
||||
js_data_mutator: jsFnMutator,
|
||||
contours: rawContours,
|
||||
cellSize = '200',
|
||||
} = fd;
|
||||
let data = payload.data.features;
|
||||
const { aggregation = 'SUM', contours: rawContours, cellSize = '200' } = fd;
|
||||
const data = payload.data.features;
|
||||
|
||||
// Store original data for tooltip access
|
||||
const originalDataMap = new Map();
|
||||
@@ -113,12 +107,6 @@ export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
},
|
||||
);
|
||||
|
||||
if (jsFnMutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutatorFunction = sandboxedEval(fd.js_data_mutator);
|
||||
data = jsFnMutatorFunction(data);
|
||||
}
|
||||
|
||||
// Create wrapper for tooltip content that adds nearby points
|
||||
const tooltipContentGenerator = (o: any) => {
|
||||
// Find nearby points based on hover coordinate
|
||||
@@ -179,7 +167,7 @@ export const getLayer: GetLayerType<ContourLayer> = function ({
|
||||
cellSize: safeCellSize,
|
||||
aggregation: aggregation.toUpperCase(),
|
||||
getPosition: (d: { position: number[]; weight: number }) =>
|
||||
d.position as unknown as Position,
|
||||
d.position as Position,
|
||||
getWeight: (d: { weight: number }) => d.weight || 0,
|
||||
...commonLayerProps({
|
||||
formData: fd,
|
||||
|
||||
@@ -25,10 +25,6 @@ import { validateNonEmpty } from '@superset-ui/core';
|
||||
import {
|
||||
autozoom,
|
||||
filterNulls,
|
||||
jsColumns,
|
||||
jsDataMutator,
|
||||
jsOnclickHref,
|
||||
jsTooltip,
|
||||
mapboxStyle,
|
||||
maplibreStyle,
|
||||
mapProvider,
|
||||
@@ -114,15 +110,6 @@ const config: ControlPanelConfig = {
|
||||
],
|
||||
],
|
||||
},
|
||||
{
|
||||
label: t('Advanced'),
|
||||
controlSetRows: [
|
||||
[jsColumns],
|
||||
[jsDataMutator],
|
||||
[jsTooltip],
|
||||
[jsOnclickHref],
|
||||
],
|
||||
},
|
||||
],
|
||||
controlOverrides: {
|
||||
size: {
|
||||
|
||||
@@ -27,9 +27,7 @@ import { render } from '@testing-library/react';
|
||||
import { SqlaFormData } from '@superset-ui/core';
|
||||
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
|
||||
import DeckGLGeoJson, {
|
||||
computeGeoJsonTextOptionsFromJsOutput,
|
||||
computeGeoJsonTextOptionsFromFormData,
|
||||
computeGeoJsonIconOptionsFromJsOutput,
|
||||
computeGeoJsonIconOptionsFromFormData,
|
||||
getPoints,
|
||||
getLayer,
|
||||
@@ -61,26 +59,6 @@ jest.mock('react-map-gl/maplibre', () => ({
|
||||
useControl: () => null,
|
||||
}));
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromJsOutput extracts valid text options from the input object', () => {
|
||||
const input = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
invalidOption: true,
|
||||
};
|
||||
const expectedOutput = {
|
||||
getText: 'name',
|
||||
getTextColor: [1, 2, 3, 255],
|
||||
};
|
||||
expect(computeGeoJsonTextOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonTextOptionsFromFormData computes text options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
label_property_name: 'name',
|
||||
@@ -105,28 +83,6 @@ test('computeGeoJsonTextOptionsFromFormData computes text options based on form
|
||||
expect(actualOutput.getText(sampleFeature)).toBe('Test');
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput returns an empty object for non-object input', () => {
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(null)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(42)).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput([1, 2, 3])).toEqual({});
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput('string')).toEqual({});
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromJsOutput extracts valid icon options from the input object', () => {
|
||||
const input = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
invalidOption: false,
|
||||
};
|
||||
|
||||
const expectedOutput = {
|
||||
getIcon: 'icon_name',
|
||||
getIconColor: [1, 2, 3, 255],
|
||||
};
|
||||
|
||||
expect(computeGeoJsonIconOptionsFromJsOutput(input)).toEqual(expectedOutput);
|
||||
});
|
||||
|
||||
test('computeGeoJsonIconOptionsFromFormData computes icon options based on form data', () => {
|
||||
const formData: SqlaFormData = {
|
||||
icon_url: 'https://example.com/icon.png',
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
DeckGLContainerStyledWrapper,
|
||||
} from '../../DeckGLContainer';
|
||||
import { hexToRGB } from '../../utils/colors';
|
||||
import sandboxedEval from '../../utils/sandbox';
|
||||
import { commonLayerProps } from '../common';
|
||||
import TooltipRow from '../../TooltipRow';
|
||||
import fitViewport, { Viewport } from '../../utils/fitViewport';
|
||||
@@ -146,52 +145,6 @@ const getFillColor = (feature: JsonObject, filterStateValue: unknown[]) => {
|
||||
};
|
||||
const getLineColor = (feature: JsonObject) => feature?.properties?.strokeColor;
|
||||
|
||||
const isObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
|
||||
export const computeGeoJsonTextOptionsFromJsOutput = (
|
||||
output: unknown,
|
||||
): Partial<GeoJsonLayerProps> => {
|
||||
if (!isObject(output)) return {};
|
||||
|
||||
// Properties sourced from:
|
||||
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-2
|
||||
const options: (keyof GeoJsonLayerProps)[] = [
|
||||
'getText',
|
||||
'getTextColor',
|
||||
'getTextAngle',
|
||||
'getTextSize',
|
||||
'getTextAnchor',
|
||||
'getTextAlignmentBaseline',
|
||||
'getTextPixelOffset',
|
||||
'getTextBackgroundColor',
|
||||
'getTextBorderColor',
|
||||
'getTextBorderWidth',
|
||||
'textSizeUnits',
|
||||
'textSizeScale',
|
||||
'textSizeMinPixels',
|
||||
'textSizeMaxPixels',
|
||||
'textCharacterSet',
|
||||
'textFontFamily',
|
||||
'textFontWeight',
|
||||
'textLineHeight',
|
||||
'textMaxWidth',
|
||||
'textWordBreak',
|
||||
'textBackground',
|
||||
'textBackgroundPadding',
|
||||
'textOutlineColor',
|
||||
'textOutlineWidth',
|
||||
'textBillboard',
|
||||
'textFontSettings',
|
||||
];
|
||||
|
||||
const allEntries = Object.entries(output);
|
||||
const validEntries = allEntries.filter(([k]) =>
|
||||
options.includes(k as keyof GeoJsonLayerProps),
|
||||
);
|
||||
return Object.fromEntries(validEntries);
|
||||
};
|
||||
|
||||
export const computeGeoJsonTextOptionsFromFormData = (
|
||||
fd: SqlaFormData,
|
||||
): Partial<GeoJsonLayerProps> => {
|
||||
@@ -205,36 +158,6 @@ export const computeGeoJsonTextOptionsFromFormData = (
|
||||
};
|
||||
};
|
||||
|
||||
export const computeGeoJsonIconOptionsFromJsOutput = (
|
||||
output: unknown,
|
||||
): Partial<GeoJsonLayerProps> => {
|
||||
if (!isObject(output)) return {};
|
||||
|
||||
// Properties sourced from:
|
||||
// https://deck.gl/docs/api-reference/layers/geojson-layer#pointtype-options-1
|
||||
const options: (keyof GeoJsonLayerProps)[] = [
|
||||
'getIcon',
|
||||
'getIconSize',
|
||||
'getIconColor',
|
||||
'getIconAngle',
|
||||
'getIconPixelOffset',
|
||||
'iconSizeUnits',
|
||||
'iconSizeScale',
|
||||
'iconSizeMinPixels',
|
||||
'iconSizeMaxPixels',
|
||||
'iconAtlas',
|
||||
'iconMapping',
|
||||
'iconBillboard',
|
||||
'iconAlphaCutoff',
|
||||
];
|
||||
|
||||
const allEntries = Object.entries(output);
|
||||
const validEntries = allEntries.filter(([k]) =>
|
||||
options.includes(k as keyof GeoJsonLayerProps),
|
||||
);
|
||||
return Object.fromEntries(validEntries);
|
||||
};
|
||||
|
||||
export const computeGeoJsonIconOptionsFromFormData = (
|
||||
fd: SqlaFormData,
|
||||
): Partial<GeoJsonLayerProps> => ({
|
||||
@@ -288,13 +211,6 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
features = [];
|
||||
recurseGeoJson(payload.data, propOverrides);
|
||||
|
||||
let processedFeatures = features;
|
||||
if (fd.js_data_mutator) {
|
||||
// Applying user defined data mutator if defined
|
||||
const jsFnMutator = sandboxedEval(fd.js_data_mutator);
|
||||
processedFeatures = jsFnMutator(features) as ProcessedFeature[];
|
||||
}
|
||||
|
||||
let pointType = 'circle';
|
||||
if (fd.enable_labels) {
|
||||
pointType = `${pointType}+text`;
|
||||
@@ -303,33 +219,17 @@ export const getLayer: GetLayerType<GeoJsonLayer> = function ({
|
||||
pointType = `${pointType}+icon`;
|
||||
}
|
||||
|
||||
let labelOpts: Partial<GeoJsonLayerProps> = {};
|
||||
if (fd.enable_labels) {
|
||||
if (fd.enable_label_javascript_mode) {
|
||||
const generator = sandboxedEval(fd.label_javascript_config_generator);
|
||||
if (typeof generator === 'function') {
|
||||
labelOpts = computeGeoJsonTextOptionsFromJsOutput(generator());
|
||||
}
|
||||
} else {
|
||||
labelOpts = computeGeoJsonTextOptionsFromFormData(fd);
|
||||
}
|
||||
}
|
||||
const labelOpts: Partial<GeoJsonLayerProps> = fd.enable_labels
|
||||
? computeGeoJsonTextOptionsFromFormData(fd)
|
||||
: {};
|
||||
|
||||
let iconOpts: Partial<GeoJsonLayerProps> = {};
|
||||
if (fd.enable_icons) {
|
||||
if (fd.enable_icon_javascript_mode) {
|
||||
const generator = sandboxedEval(fd.icon_javascript_config_generator);
|
||||
if (typeof generator === 'function') {
|
||||
iconOpts = computeGeoJsonIconOptionsFromJsOutput(generator());
|
||||
}
|
||||
} else {
|
||||
iconOpts = computeGeoJsonIconOptionsFromFormData(fd);
|
||||
}
|
||||
}
|
||||
const iconOpts: Partial<GeoJsonLayerProps> = fd.enable_icons
|
||||
? computeGeoJsonIconOptionsFromFormData(fd)
|
||||
: {};
|
||||
|
||||
return new GeoJsonLayer({
|
||||
id: `geojson-layer-${fd.slice_id}` as const,
|
||||
data: processedFeatures,
|
||||
data: features,
|
||||
extruded: fd.extruded,
|
||||
filled: fd.filled,
|
||||
stroked: fd.stroked,
|
||||
@@ -394,7 +294,7 @@ export function getPoints(data?: Point[]) {
|
||||
}
|
||||
|
||||
const DeckGLGeoJson = (props: DeckGLGeoJsonProps) => {
|
||||
const containerRef = useRef<DeckGLContainerHandle | null>(null);
|
||||
const containerRef = useRef<DeckGLContainerHandle>();
|
||||
const setTooltip = useCallback((tooltip: TooltipProps['tooltip']) => {
|
||||
const { current } = containerRef;
|
||||
if (current) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user