Compare commits

..

4 Commits

Author SHA1 Message Date
Evan
ff88f0dcdd test: fix deck.gl js_columns test to set the removed control explicitly
test_get_js_columns relied on the deck_path fixture's js_columns entry,
which was dropped when the "Extra data for JS" control was removed.
Set it explicitly in the test so the deprecated get_js_columns() method
is still exercised.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 18:34:15 -07:00
Evan Rusackas
b83e0a8400 chore(deckgl): remove js_columns (Extra data for JS) control
js_columns fed the now-removed JS eval controls and had no remaining
purpose: field-based tooltips already auto-include tooltip_contents
columns, and cross_filter_column is auto-included independently on the
Geojson/Polygon layers. Its only other effect was demoting matching
columns out of a feature's direct properties into extraProps, which
the crossFiltersDataMask extraProps fallback existed solely to
compensate for.

Removes the control from all 10 deck.gl layer control panels, dropping
the "Advanced" section entirely where it was the only control left
(Contour, Hex, Screengrid, Grid, Heatmap, Path, Scatter, Arc) and
keeping it on Geojson/Polygon for cross_filter_column. Also drops the
now-dead resolve_deck_gl_columns js_columns branch in the MCP chart
tool helpers.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-17 15:48:42 -07:00
Evan
bd544b8bcb fix: remove stale vm-browserify reference in storybook webpack config
vm-browserify was dropped from package.json alongside vm2, but
.storybook/main.mjs still called require.resolve('vm-browserify'),
breaking the storybook build. Mirror the vm: false fallback already
used in webpack.config.js.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 02:24:23 -07:00
Evan Rusackas
79e09ec1b3 chore: remove deck.gl JavaScript tooltip controls and ENABLE_JAVASCRIPT_CONTROLS
Removes the deck.gl JS tooltip/onclick-href/data-mutator controls and the
GeoJSON layer's label/icon JavaScript-mode generators, along with the
ENABLE_JAVASCRIPT_CONTROLS feature flag that gated them (closes #41045).
These let users write arbitrary JS, sandboxed via Node's vm module, to
customize deck.gl tooltips/click behavior/data transforms; the flag
defaulted off and saw negligible use.

Removes the now-dead sandboxedEval() runtime (utils/sandbox.ts), its
vm-browserify webpack polyfill and devDependency, and the underscore
dependency it alone pulled into the deck.gl plugin. Cleans up the
associated backend form_data-stripping mechanism, tests, fixtures, and
docs for the removed feature.

vm2 (the dependency this was originally suspected to pull in) turned out
to be unrelated: it was only a transitive dev-dependency of the
Cartodiagram plugin's geostyler dependency via typescript-json-schema,
which has since dropped it upstream (>=0.68.0). Forced that resolution
via an npm override, confirmed geostyler ships no code that touches
typescript-json-schema at runtime, and verified the Cartodiagram plugin
still builds and its full test suite passes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-16 22:24:24 -07:00
280 changed files with 1209 additions and 4310 deletions

View File

@@ -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,

View File

@@ -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

View File

@@ -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!"
}
]
}

View File

@@ -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.

View File

@@ -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;
}

View File

@@ -114,7 +114,7 @@ export default {
},
fallback: {
tty: false,
vm: require.resolve('vm-browserify')
vm: false,
}
},
plugins: [...config.plugins, ...filteredPlugins],

View File

@@ -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,

View File

@@ -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: {

File diff suppressed because it is too large Load Diff

View File

@@ -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"
},

View File

@@ -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",

View File

@@ -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"

View File

@@ -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`

View File

@@ -47,7 +47,7 @@ export interface MetricOptionProps {
showFormula?: boolean;
showType?: boolean;
url?: string;
labelRef?: RefObject<any | null>;
labelRef?: RefObject<any>;
shouldShowTooltip?: boolean;
}

View File

@@ -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 &&

View File

@@ -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';

View File

@@ -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": {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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}</>

View File

@@ -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>&nbsp;
<code>chartType=&quot;{loadingChartType}&quot;</code> &mdash;
{String(error)}
{error.toString()}
</div>
);
}

View File

@@ -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).

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 = [];

View File

@@ -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 };

View File

@@ -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}>

View File

@@ -57,7 +57,7 @@ export interface DropdownContainerProps {
/**
* Dropdown ref.
*/
dropdownRef?: RefObject<HTMLDivElement | null>;
dropdownRef?: RefObject<HTMLDivElement>;
/**
* Dropdown additional style properties.
*/

View File

@@ -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;

View File

@@ -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(

View File

@@ -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();
};

View File

@@ -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 =

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;

View File

@@ -542,7 +542,7 @@ const AsyncSelect = forwardRef(
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
) =>
dropDownRenderHelper(
originNode as Parameters<typeof dropDownRenderHelper>[0],
originNode,
isDropdownVisible,
isLoading,
fullSelectOptions.length,

View File

@@ -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,

View File

@@ -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,
},
}),

View File

@@ -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: () => {

View File

@@ -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,
});
});
});

View File

@@ -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;

View File

@@ -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';

View File

@@ -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),
);
});

View File

@@ -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 = () => {

View File

@@ -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[]) => {

View File

@@ -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);

View File

@@ -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];

View File

@@ -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',

View File

@@ -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(() => {

View File

@@ -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"

View File

@@ -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": "*"
}
}

View File

@@ -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"
}
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"
}
}

View File

@@ -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"
}
}

View File

@@ -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"

View File

@@ -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,
) => {

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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}

View File

@@ -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

View File

@@ -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,

View File

@@ -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"
}
}

View File

@@ -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"

View File

@@ -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);

View File

@@ -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,

View File

@@ -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(() => {

View File

@@ -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(

View File

@@ -44,7 +44,7 @@ export type EchartsStylesProps = {
export type Refs = {
echartRef?: Ref<EchartsHandler>;
divRef?: RefObject<HTMLDivElement | null>;
divRef?: RefObject<HTMLDivElement>;
};
export interface EchartsProps {

View File

@@ -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);
});

View File

@@ -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

View File

@@ -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",

View File

@@ -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>

View File

@@ -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",

View File

@@ -1541,3 +1541,4 @@ TableRenderer.propTypes = {
tableOptions: PropTypes.object,
onContextMenu: PropTypes.func,
};
TableRenderer.defaultProps = { ...PivotData.defaultProps, tableOptions: {} };

View File

@@ -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"

View File

@@ -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"

View File

@@ -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);

View File

@@ -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';

View File

@@ -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;
});

View File

@@ -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;

View File

@@ -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"

View File

@@ -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,

View File

@@ -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": {

View File

@@ -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);

View File

@@ -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']) => {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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(

View File

@@ -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: {

View File

@@ -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: '',
}}
/>
);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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',

View File

@@ -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