Compare commits

...

8 Commits

Author SHA1 Message Date
Evan Rusackas
87aea31236 fix(chart): restore JSX.Element return type on ChartDataProvider
The earlier pivot to `ReactNode` broke tsc with TS2786 — `ReactNode`
widens to `undefined`, which isn't assignable to the
`ReactElement<any, any> | null` type TypeScript requires for a JSX
component. This failed the lint-frontend and sharded-jest-tests (2)
CI checks.

Keep the render-prop `children` signature permissive (`ReactNode`)
but wrap each `children(...)` call in a Fragment so the component
itself returns `JSX.Element | null`. No unsafe casts, and consumers
can still return strings / fragments / arrays from the render prop.
2026-04-23 01:36:37 -07:00
Evan Rusackas
ed58fc51e0 address review: correct stale comment on data-fetch effect
Previous comment claimed changing callback props would trigger a
refetch, but the dependency array only includes [formData, sliceId].
Update the comment to describe the actual behavior (which matches
the original class component's componentDidUpdate) and explain why
exhaustive-deps is disabled.
2026-04-22 13:57:24 -07:00
Evan Rusackas
9a446677e3 address review: return ReactNode from ChartDataProvider
The children prop is typed as returning ReactNode (which covers
strings, fragments, arrays, etc.), so casting the return to
JSX.Element was unsafe. Change the component's return type to
ReactNode and drop the casts.
2026-04-22 13:56:49 -07:00
Evan Rusackas
e4eb0fc33d address review: drop redundant useTheme + theme prop in StatefulChart
SuperChart is exported via withTheme(...), so the theme flows in from
context automatically. The explicit useTheme() call and theme={theme}
prop are redundant, and the cast on useTheme from @emotion/react
(rather than the guarded @apache-superset/core/theme hook) was an
unnecessary type workaround.
2026-04-22 13:55:41 -07:00
Evan Rusackas
a9771609c3 Merge branch 'master' into chore/fc-05-chart-data-provider 2026-04-19 15:25:42 -04:00
Evan Rusackas
d820f16173 fix(imports): rewrite stale @apache-superset/core bare and api/core imports to correct subpaths 2026-04-17 11:35:19 -07:00
Evan Rusackas
393c018681 fix(imports): rewrite stale @apache-superset/core/ui to current subpaths
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:56:20 -07:00
Evan Rusackas
a5f0c31e05 chore(lint): convert ChartDataProvider and StatefulChart to function components
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-17 10:34:37 -07:00

View File

@@ -17,8 +17,7 @@
* under the License.
*/
/* eslint react/sort-comp: 'off' */
import { PureComponent, ReactNode } from 'react';
import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react';
import {
SupersetClientInterface,
RequestConfig,
@@ -67,103 +66,112 @@ export type ChartDataProviderState = {
error?: ProvidedProps['error'];
};
class ChartDataProvider extends PureComponent<
ChartDataProviderProps,
ChartDataProviderState
> {
readonly chartClient: ChartClient;
function ChartDataProvider({
children,
client,
formData,
sliceId,
loadDatasource,
onError,
onLoaded,
formDataRequestOptions,
datasourceRequestOptions,
queryRequestOptions,
}: ChartDataProviderProps): JSX.Element | null {
const [state, setState] = useState<ChartDataProviderState>({
status: 'uninitialized',
});
constructor(props: ChartDataProviderProps) {
super(props);
this.state = { status: 'uninitialized' };
this.chartClient = new ChartClient({ client: props.client });
}
const chartClient = useMemo(() => new ChartClient({ client }), [client]);
componentDidMount() {
this.handleFetchData();
}
const extractSliceIdAndFormData = useCallback(
(): SliceIdAndOrFormData =>
formData ? { formData } : { sliceId: sliceId as number },
[formData, sliceId],
);
componentDidUpdate(prevProps: ChartDataProviderProps) {
const { formData, sliceId } = this.props;
if (formData !== prevProps.formData || sliceId !== prevProps.sliceId) {
this.handleFetchData();
const handleReceiveData = useCallback(
(payload?: Payload) => {
if (onLoaded) onLoaded(payload);
setState({ payload, status: 'loaded' });
},
[onLoaded],
);
const handleError = useCallback(
(error: ProvidedProps['error']) => {
if (onError) onError(error);
setState({ error, status: 'error' });
},
[onError],
);
const handleFetchData = useCallback(() => {
setState({ status: 'loading' });
try {
chartClient
.loadFormData(extractSliceIdAndFormData(), formDataRequestOptions)
.then(loadedFormData =>
Promise.all([
loadDatasource
? chartClient.loadDatasource(
loadedFormData.datasource,
datasourceRequestOptions,
)
: Promise.resolve(undefined),
chartClient.loadQueryData(loadedFormData, queryRequestOptions),
]).then(
([datasource, queriesData]) =>
({
datasource,
formData: loadedFormData,
queriesData,
}) as Payload,
),
)
.then(handleReceiveData)
.catch(handleError);
} catch (error) {
handleError(error as Error);
}
}
}, [
chartClient,
extractSliceIdAndFormData,
formDataRequestOptions,
loadDatasource,
datasourceRequestOptions,
queryRequestOptions,
handleReceiveData,
handleError,
]);
private extractSliceIdAndFormData() {
const { formData, sliceId } = this.props;
return formData ? { formData } : { sliceId: sliceId as number };
}
// Fetch on mount and only refetch when formData or sliceId changes.
// This preserves the original class component's componentDidUpdate
// semantics (which compared only formData and sliceId). Other
// fetch-related inputs referenced by handleFetchData (callbacks and
// request option props) are intentionally excluded from the dependency
// array, so the exhaustive-deps rule is suppressed here.
useEffect(() => {
handleFetchData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formData, sliceId]);
private handleFetchData = () => {
const {
loadDatasource,
formDataRequestOptions,
datasourceRequestOptions,
queryRequestOptions,
} = this.props;
const { status, payload, error } = state;
this.setState({ status: 'loading' }, () => {
try {
this.chartClient
.loadFormData(
this.extractSliceIdAndFormData(),
formDataRequestOptions,
)
.then(formData =>
Promise.all([
loadDatasource
? this.chartClient.loadDatasource(
formData.datasource,
datasourceRequestOptions,
)
: Promise.resolve(undefined),
this.chartClient.loadQueryData(formData, queryRequestOptions),
]).then(
([datasource, queriesData]) =>
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
({
datasource,
formData,
queriesData,
}) as Payload,
),
)
.then(this.handleReceiveData)
.catch(this.handleError);
} catch (error) {
this.handleError(error as Error);
}
});
};
private handleReceiveData = (payload?: Payload) => {
const { onLoaded } = this.props;
if (onLoaded) onLoaded(payload);
this.setState({ payload, status: 'loaded' });
};
private handleError = (error: ProvidedProps['error']) => {
const { onError } = this.props;
if (onError) onError(error);
this.setState({ error, status: 'error' });
};
render() {
const { children } = this.props;
const { status, payload, error } = this.state;
switch (status) {
case 'loading':
return children({ loading: true });
case 'loaded':
return children({ payload });
case 'error':
return children({ error });
case 'uninitialized':
default:
return null;
}
// Wrap the children result in a Fragment so the component's return type
// 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) {
case 'loading':
return <>{children({ loading: true })}</>;
case 'loaded':
return <>{children({ payload })}</>;
case 'error':
return <>{children({ error })}</>;
case 'uninitialized':
default:
return null;
}
}