This is an automated email from the ASF dual-hosted git repository. betodealmeida pushed a commit to branch oauth-during-db-creation in repository https://gitbox.apache.org/repos/asf/superset.git
commit 7a8d2c585f6bfb92072bd074a9bee238eefdd7bc Author: Beto Dealmeida <[email protected]> AuthorDate: Wed May 13 15:14:51 2026 -0400 feat(semantic layers): form for semantic layer with single semantic view --- .../features/semanticLayers/jsonFormsHelpers.tsx | 91 +++++++++++++++++++++- .../semanticViews/AddSemanticViewModal.tsx | 38 ++++++++- 2 files changed, 123 insertions(+), 6 deletions(-) diff --git a/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx b/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx index 85c7b891dbc..7368f61ceec 100644 --- a/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx +++ b/superset-frontend/src/features/semanticLayers/jsonFormsHelpers.tsx @@ -255,14 +255,98 @@ const EnumNamesRenderer = withJsonFormsControlProps(EnumNamesControl); const enumNamesEntry = { // Rank 5: higher than the default string renderer (2–3) so this fires // whenever x-enumNames is present, regardless of the underlying type. + // Array-of-enum schemas are handled by ``multiEnumEntry`` below — this + // renderer only targets scalar string/number controls. tester: rankWith( 5, + and( + schemaMatches(s => { + const names = (s as Record<string, unknown>)['x-enumNames']; + return Array.isArray(names) && (names as unknown[]).length > 0; + }), + schemaMatches( + s => (s as Record<string, unknown>)?.type !== 'array', + ), + ), + ), + renderer: EnumNamesRenderer, +}; + +/** + * Renderer for ``{type: 'array', items: {enum: [...]}}`` schemas. Renders + * a single Antd Select with ``mode="multiple"`` (tag-style multi-select), + * matching the natural expectation of a "pick several from a list" control. + * + * Without this, the default ``PrimitiveArrayControl`` from the upstream + * library renders an "Add …" button that creates one single-select per + * element — visually wrong for an enum multi-select and unable to display + * ``items.x-enumNames`` labels. + * + * The renderer is dynamic-aware: when the host form is refreshing the + * schema (e.g. compatible options narrowing as the user picks), the Select + * shows a loading indicator without becoming disabled, so the user can + * continue editing while options refresh. + */ +function MultiEnumControl(props: ControlProps) { + const { refreshingSchema } = props.config ?? {}; + const arraySchema = props.schema as Record<string, unknown>; + const itemsSchema = + (arraySchema.items as Record<string, unknown>) ?? + ({} as Record<string, unknown>); + + const enumValues = (itemsSchema.enum as unknown[]) ?? []; + const enumNames = + (itemsSchema['x-enumNames'] as string[]) ?? enumValues.map(String); + + const options = enumValues.map((value, index) => ({ + value: value as string | number, + label: enumNames[index] ?? String(value), + })); + + const value = Array.isArray(props.data) ? (props.data as unknown[]) : []; + + const tooltip = (props.uischema?.options as Record<string, unknown>) + ?.tooltip as string | undefined; + + return ( + <Form.Item label={props.label} tooltip={tooltip}> + <Select + mode="multiple" + value={value as (string | number)[]} + onChange={next => props.handleChange(props.path, next)} + options={options} + style={{ width: '100%' }} + disabled={!props.enabled} + loading={!!refreshingSchema} + allowClear + optionFilterProp="label" + placeholder={ + (props.uischema?.options as Record<string, unknown>) + ?.placeholderText as string | undefined + } + /> + </Form.Item> + ); +} +const MultiEnumRenderer = withJsonFormsControlProps(MultiEnumControl); +const multiEnumEntry = { + // Rank 35: must beat upstream ``PrimitiveArrayRenderer`` (rank 30) so an + // ``array``/``items.enum`` schema renders as one Antd multi-select tag + // box instead of the "Add" repeater pattern that PrimitiveArray uses. + tester: rankWith( + 35, schemaMatches(s => { - const names = (s as Record<string, unknown>)['x-enumNames']; - return Array.isArray(names) && (names as unknown[]).length > 0; + const schema = s as Record<string, unknown>; + if (schema?.type !== 'array') return false; + const items = schema.items as Record<string, unknown> | undefined; + return ( + !!items && + Array.isArray(items.enum) && + (items.enum as unknown[]).length > 0 + ); }), ), - renderer: EnumNamesRenderer, + renderer: MultiEnumRenderer, }; export const renderers = [ @@ -271,6 +355,7 @@ export const renderers = [ constEntry, readOnlyEntry, enumNamesEntry, + multiEnumEntry, dynamicFieldEntry, ]; diff --git a/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx b/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx index e85900c0b92..14bb9e42191 100644 --- a/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx +++ b/superset-frontend/src/features/semanticViews/AddSemanticViewModal.tsx @@ -254,7 +254,9 @@ export default function AddSemanticViewModal({ !schema?.properties || Object.keys(schema.properties).length === 0 ) { - // No runtime config needed — fetch views right away + // Preserve top-level runtime metadata (e.g. x-singleView) even when + // there are no form fields, then fetch views right away. + applyRuntimeSchema(schema); fetchViews(uuid, {}, gen); } else { applyRuntimeSchema(schema); @@ -456,6 +458,32 @@ export default function AddSemanticViewModal({ const viewsDisabled = loadingViews || (!loadingViews && availableViews.length === 0); + // When ``x-singleView: true`` the runtime form fully describes a single + // semantic view (e.g. a MetricFlow cube). Hide the picker and auto-select + // whatever ``get_semantic_views`` returned so the Add button can fire + // without an extra user click. + const singleViewMode = + (runtimeSchema as Record<string, unknown> | null)?.['x-singleView'] === + true; + + useEffect(() => { + if (!singleViewMode) return; + const namesToAdd = availableViews + .filter(v => !v.already_added) + .map(v => v.name) + .sort((a, b) => a.localeCompare(b)) + .slice(0, 1); + setSelectedViewNames(prev => { + if ( + prev.length === namesToAdd.length && + prev.every((n, i) => n === namesToAdd[i]) + ) { + return prev; + } + return namesToAdd; + }); + }, [singleViewMode, availableViews]); + return ( <StandardModal show={show} @@ -511,8 +539,12 @@ export default function AddSemanticViewModal({ </> )} - {/* Semantic Views — always visible once a layer is selected */} - {selectedLayerUuid && !loadingRuntime && ( + {/* Semantic Views — always visible once a layer is selected, unless + the runtime schema declares ``x-singleView: true``: extensions + (e.g. MetricFlow cubes) whose runtime form fully describes a + single view set that flag so the picker disappears and the + view is auto-selected when ``get_semantic_views`` returns it. */} + {selectedLayerUuid && !loadingRuntime && !singleViewMode && ( <ModalFormField label={t('Semantic Views')}> <Select ariaLabel={t('Semantic views')}
