michael-s-molina commented on code in PR #37868: URL: https://github.com/apache/superset/pull/37868#discussion_r2792984439
########## superset-frontend/src/explore/components/controls/JSEditorControl.tsx: ########## @@ -0,0 +1,104 @@ +/** + * 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 { useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import ControlHeader, { + ControlHeaderProps, +} from 'src/explore/components/ControlHeader'; +import { styled } from '@apache-superset/core'; +import { + ControlComponentProps, + safeParseEChartOptions, + EChartOptionsParseError, +} from '@superset-ui/chart-controls'; +import { EditorHost } from 'src/core/editors'; +import { useDebounceValue } from 'src/hooks/useDebounceValue'; + +const Container = styled.div` + border: 1px solid ${({ theme }) => theme.colorBorder}; + border-radius: ${({ theme }) => theme.borderRadius}px; + overflow: hidden; +`; + +const ErrorMessage = styled.div` + color: ${({ theme }) => theme.colorErrorText}; +`; + +export default function JSEditorControl({ + name, + label, + description, + renderTrigger, + hovered, + tooltipOnClick, + onChange, + value, +}: ControlHeaderProps & ControlComponentProps<string>) { + const debouncedValue = useDebounceValue(value); + // safeParseEChartOptions; + const error = useMemo(() => { + try { + safeParseEChartOptions(debouncedValue ?? ''); + return null; + } catch (err) { + if (err instanceof EChartOptionsParseError) { + return err; + } + throw err; + } + }, [debouncedValue]); + const headerProps = { + name, + label: label ?? name, + description, + renderTrigger, + validationErrors: error?.message ? [error.message] : undefined, + hovered, + tooltipOnClick, + }; + + return ( + <> + <ControlHeader {...headerProps} /> Review Comment: Can we add an info tooltip about this control? Some things that I think would be interesting: - A link to [ECharts API docs](https://echarts.apache.org/en/option.html#title) - An explanation of how properties that conflict with Explore controls as resolved. I wonder which one wins: - If it's the ECharts control, then we might have a situation where users might report bugs that Explore controls are not working because they don't see that a property was overridden. - If it's the Explore control, then users might think the ECharts control is bugged because the change will not work. We might need a strategy to validate/warn users when there's a conflict. ########## superset-frontend/packages/superset-ui-chart-controls/src/utils/echartOptionsSchema.ts: ########## @@ -0,0 +1,838 @@ +/** + * 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. + */ + +/** + * Unified ECharts Options Schema + * + * This file serves as the single source of truth for: + * 1. Runtime validation (Zod schema) + * 2. TypeScript types (inferred from Zod) + * + * Reference: https://echarts.apache.org/en/option.html + */ + +import { z } from 'zod'; Review Comment: Can you move ECharts-related files to `plugins/plugin-chart-echarts`? The control definition is fine but schema and parsing should leave there. You can import them using `@superset-ui/plugin-chart-echarts` in the editor. ########## superset-frontend/packages/superset-core/src/api/contributions.ts: ########## @@ -91,57 +91,29 @@ export interface EditorContribution { /** * Supported editor languages. */ -export type EditorLanguage = 'sql' | 'json' | 'yaml' | 'markdown' | 'css'; - -/** - * Valid locations within SQL Lab. - */ -export type SqlLabLocation = - | 'leftSidebar' - | 'rightSidebar' - | 'panels' - | 'editor' - | 'statusBar' - | 'results' - | 'queryHistory'; - -/** - * Nested structure for view contributions by scope and location. - * @example - * { - * sqllab: { - * panels: [{ id: "my-ext.panel", name: "My Panel" }], - * leftSidebar: [{ id: "my-ext.sidebar", name: "My Sidebar" }] - * } - * } - */ -export interface ViewContributions { - sqllab?: Partial<Record<SqlLabLocation, ViewContribution[]>>; -} - -/** - * Nested structure for menu contributions by scope and location. - * @example - * { - * sqllab: { - * editor: { primary: [...], secondary: [...] } - * } - * } - */ -export interface MenuContributions { - sqllab?: Partial<Record<SqlLabLocation, MenuContribution>>; -} +export type EditorLanguage = + | 'sql' + | 'json' + | 'yaml' + | 'markdown' + | 'css' + | 'javascript' + | string; Review Comment: We should not make this open ended. ```suggestion | 'javascript'; ``` ########## superset-frontend/package.json: ########## @@ -232,7 +232,8 @@ "use-query-params": "^2.2.2", "uuid": "^13.0.0", "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", - "yargs": "^17.7.2" + "yargs": "^17.7.2", + "zod": "^4.3.6" Review Comment: We need to also add `acorn` as an explicit dependency. ########## superset-frontend/src/explore/components/controls/JSEditorControl.tsx: ########## @@ -0,0 +1,104 @@ +/** + * 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 { useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import ControlHeader, { + ControlHeaderProps, +} from 'src/explore/components/ControlHeader'; +import { styled } from '@apache-superset/core'; +import { + ControlComponentProps, + safeParseEChartOptions, + EChartOptionsParseError, +} from '@superset-ui/chart-controls'; +import { EditorHost } from 'src/core/editors'; +import { useDebounceValue } from 'src/hooks/useDebounceValue'; + +const Container = styled.div` + border: 1px solid ${({ theme }) => theme.colorBorder}; + border-radius: ${({ theme }) => theme.borderRadius}px; + overflow: hidden; +`; + +const ErrorMessage = styled.div` + color: ${({ theme }) => theme.colorErrorText}; +`; + +export default function JSEditorControl({ + name, + label, + description, + renderTrigger, + hovered, + tooltipOnClick, + onChange, + value, +}: ControlHeaderProps & ControlComponentProps<string>) { + const debouncedValue = useDebounceValue(value); + // safeParseEChartOptions; Review Comment: ```suggestion ``` ########## superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx: ########## @@ -455,6 +455,18 @@ const order_by_cols: SharedControlConfig<'SelectControl'> = { resetOnHide: false, }; +const echart_options: SharedControlConfig<'JSEditorControl'> = { + type: 'JSEditorControl', + label: t('EChart Options (JS object literals)'), Review Comment: General comment for file names, field names and descriptions: It should be ECharts not EChart. ########## superset-frontend/src/explore/components/controls/JSEditorControl.tsx: ########## @@ -0,0 +1,104 @@ +/** + * 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 { useMemo } from 'react'; +import AutoSizer from 'react-virtualized-auto-sizer'; +import ControlHeader, { + ControlHeaderProps, +} from 'src/explore/components/ControlHeader'; +import { styled } from '@apache-superset/core'; +import { + ControlComponentProps, + safeParseEChartOptions, + EChartOptionsParseError, +} from '@superset-ui/chart-controls'; +import { EditorHost } from 'src/core/editors'; +import { useDebounceValue } from 'src/hooks/useDebounceValue'; + +const Container = styled.div` + border: 1px solid ${({ theme }) => theme.colorBorder}; + border-radius: ${({ theme }) => theme.borderRadius}px; + overflow: hidden; +`; + +const ErrorMessage = styled.div` + color: ${({ theme }) => theme.colorErrorText}; +`; + +export default function JSEditorControl({ + name, + label, + description, + renderTrigger, + hovered, + tooltipOnClick, + onChange, + value, +}: ControlHeaderProps & ControlComponentProps<string>) { + const debouncedValue = useDebounceValue(value); + // safeParseEChartOptions; + const error = useMemo(() => { + try { + safeParseEChartOptions(debouncedValue ?? ''); + return null; + } catch (err) { + if (err instanceof EChartOptionsParseError) { + return err; + } + throw err; + } + }, [debouncedValue]); + const headerProps = { + name, + label: label ?? name, + description, + renderTrigger, + validationErrors: error?.message ? [error.message] : undefined, + hovered, + tooltipOnClick, + }; + + return ( + <> + <ControlHeader {...headerProps} /> + <Container> + <AutoSizer disableHeight> + {({ width }) => ( + <EditorHost + id="echart-js-editor" + value={value ?? ''} + onChange={val => onChange?.(val)} + language="javascript" + tabSize={2} + lineNumbers + width={`${width}px`} + height="250px" + /> + )} + </AutoSizer> + </Container> + {error && ( + <ErrorMessage> + {error.validationErrors.map(err => ( + <div key={err}>{err}</div> Review Comment: Using the error message string as the key will produce duplicate keys if two identical error messages exist. Use the index or a unique identifier instead. ########## superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/sharedControls.tsx: ########## @@ -455,6 +455,18 @@ const order_by_cols: SharedControlConfig<'SelectControl'> = { resetOnHide: false, }; +const echart_options: SharedControlConfig<'JSEditorControl'> = { + type: 'JSEditorControl', + label: t('EChart Options (JS object literals)'), + description: t( + 'A JavaScript object that adheres to the ECharts options specification. ' + + '(i.e. { title: { text: "My Chart" }, tooltip: { trigger: "item" } })', Review Comment: +1 ########## superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts: ########## @@ -839,8 +841,23 @@ export default function transformProps( const onFocusedSeries = (seriesName: string | null) => { focusedSeries = seriesName; }; + + let mergedEchartOptions; + try { + // Parse custom EChart options safely using AST analysis + // This replaces the unsafe `new Function()` approach with a secure parser + // that only allows static data structures (no function callbacks) + const customEchartOptions = safeParseEChartOptions(_echartOptions); + mergedEchartOptions = mergeCustomEChartOptions( + echartOptions, + customEchartOptions, + ); + } catch (_) { Review Comment: This catches all errors silently, not just `EChartOptionsParseError`. If `mergeCustomEChartOptions` has an unexpected bug, it would be invisible. ########## superset-frontend/packages/superset-ui-chart-controls/src/utils/echartOptionsSchema.ts: ########## @@ -0,0 +1,838 @@ +/** + * 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. + */ + +/** + * Unified ECharts Options Schema + * + * This file serves as the single source of truth for: + * 1. Runtime validation (Zod schema) + * 2. TypeScript types (inferred from Zod) + * + * Reference: https://echarts.apache.org/en/option.html + */ + +import { z } from 'zod'; + +// ============================================================================= +// Common Schemas +// ============================================================================= + +/** Color value - hex, rgb, rgba, or named color */ +const colorSchema = z.string(); + +/** Numeric or percentage string (e.g., '50%') */ +const numberOrPercentSchema = z.union([z.number(), z.string()]); + +/** Line type */ +const lineTypeSchema = z.union([ + z.enum(['solid', 'dashed', 'dotted']), + z.array(z.number()), +]); + +/** Font weight */ +const fontWeightSchema = z.union([ + z.enum(['normal', 'bold', 'bolder', 'lighter']), + z.number().min(100).max(900), +]); + +/** Font style */ +const fontStyleSchema = z.enum(['normal', 'italic', 'oblique']); + +/** Symbol type */ +const symbolTypeSchema = z.string(); + +// ============================================================================= +// Text Style Schema +// ============================================================================= + +export const textStyleSchema = z + .object({ + color: colorSchema.optional(), + fontStyle: fontStyleSchema.optional(), + fontWeight: fontWeightSchema.optional(), + fontFamily: z.string().optional(), + fontSize: z.number().optional(), + lineHeight: z.number().optional(), + backgroundColor: z + .union([colorSchema, z.literal('transparent')]) + .optional(), + borderColor: colorSchema.optional(), + borderWidth: z.number().optional(), + borderType: lineTypeSchema.optional(), + borderRadius: z.union([z.number(), z.array(z.number())]).optional(), + padding: z.union([z.number(), z.array(z.number())]).optional(), + shadowColor: colorSchema.optional(), + shadowBlur: z.number().optional(), + shadowOffsetX: z.number().optional(), + shadowOffsetY: z.number().optional(), + width: numberOrPercentSchema.optional(), + height: numberOrPercentSchema.optional(), + textBorderColor: colorSchema.optional(), + textBorderWidth: z.number().optional(), + textShadowColor: colorSchema.optional(), + textShadowBlur: z.number().optional(), + textShadowOffsetX: z.number().optional(), + textShadowOffsetY: z.number().optional(), + overflow: z.enum(['none', 'truncate', 'break', 'breakAll']).optional(), + ellipsis: z.string().optional(), + align: z.enum(['left', 'center', 'right']).optional(), + verticalAlign: z.enum(['top', 'middle', 'bottom']).optional(), + }) + .catchall(z.unknown()); Review Comment: This allows any unknown properties on textStyle objects, partially bypassing schema validation. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected] --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
