From 671f38f61749466b381ca67d43b4173df6e6a774 Mon Sep 17 00:00:00 2001 From: Leanid Astrakou Date: Mon, 15 Sep 2025 16:30:32 -0400 Subject: [PATCH 1/2] Add anchor resolution + combinator resolution to fix schema Signed-off-by: Leanid Astrakou --- src/renderer/components/common/JsonForms.tsx | 56 ++++++++++++++++++- src/renderer/components/common/Utils.tsx | 45 +++++++++++++++ .../components/stages/CachingService.tsx | 4 +- .../components/stages/Certificates.tsx | 4 +- src/renderer/components/stages/Security.tsx | 4 +- .../stages/installation/Installation.tsx | 4 +- 6 files changed, 107 insertions(+), 10 deletions(-) diff --git a/src/renderer/components/common/JsonForms.tsx b/src/renderer/components/common/JsonForms.tsx index b6b65b6e..e06b3ed3 100644 --- a/src/renderer/components/common/JsonForms.tsx +++ b/src/renderer/components/common/JsonForms.tsx @@ -17,6 +17,7 @@ import Radio from '@mui/material/Radio'; import RadioGroup from '@mui/material/RadioGroup'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormControl from '@mui/material/FormControl'; +import { sanitizeOldStyleAnchors } from './Utils'; // Creates a basic input element in the UI schema const createControl = (scope: string, label?: string) => ({ @@ -118,6 +119,7 @@ const findMatchingSchemaIndex = (formData: any, oneOfSchemas: any) => { }; const makeUISchema = (schema: any, base: string, formData: any): any => { + if (!schema || !formData) { return ""; } @@ -185,6 +187,54 @@ const makeUISchema = (schema: any, base: string, formData: any): any => { return createVerticalLayout(elements); // Return whole structure } +/* resolveCombinators takes a schema plus the current form data and collapses any oneOf/anyOf it finds. +For each combinator, it picks the option that best matches the data (or index 0 as a fallback), +then replaces the whole combinator node with that chosen subschema and continues recursively +through properties/items/$defs, etc. + +The result is schema without combinators, so JsonForms won’t render tabs as a result of multiple sources */ +const resolveCombinators = (schema: any, data: any): any => { + const s = JSON.parse(JSON.stringify(schema)); + + const pick = (arr: any[], dataSlice: any) => { + // reuse your findMatchingSchemaIndex or default to 0 + const i = findMatchingSchemaIndex(dataSlice ?? {}, arr); + return arr[i] || arr[0]; + }; + + const traverse = (node: any, dataSlice: any) => { + if (!node || typeof node !== 'object') return; + + if (Array.isArray(node.oneOf)) { + const chosen = pick(node.oneOf, dataSlice); + Object.keys(node).forEach(k => delete node[k]); + Object.assign(node, chosen); + } + if (Array.isArray(node.anyOf)) { + const chosen = pick(node.anyOf, dataSlice); + Object.keys(node).forEach(k => delete node[k]); + Object.assign(node, chosen); + } + + // descend into typical containers + if (node.properties && typeof node.properties === 'object') { + for (const [k, v] of Object.entries(node.properties)) { + traverse(v, dataSlice?.[k]); + } + } + for (const key of ['items','contains','if','then','else','not']) { + if (node[key]) traverse(node[key], dataSlice); + } + for (const key of ['allOf','anyOf','oneOf','prefixItems']) { + if (Array.isArray(node[key])) node[key].forEach((x: any) => traverse(x, dataSlice)); + } + if (node.$defs) Object.values(node.$defs).forEach((x: any) => traverse(x, undefined)); + }; + + traverse(s, data); + return s; +} + export default function JsonForm(props: any) { let {schema, onChange, formData} = props; @@ -209,6 +259,8 @@ export default function JsonForm(props: any) { onChange(formData, schemaIndex); } + const sanitizedSchema = resolveCombinators(sanitizeOldStyleAnchors(requiredSchema), formData); + return ( { schema.oneOf && @@ -234,8 +286,8 @@ export default function JsonForm(props: any) { } = new Set(); // track anchors by absolute "baseId#anchor" + const traverse = (node: any, baseId: string) => { + if (!node || typeof node !== 'object') return; + + // normalize old-style `"$id":"#name"` -> `$anchor` + if (typeof node.$id === 'string' && node.$id.startsWith('#')) { + const frag = node.$id.slice(1); + if (frag && !node.$anchor) node.$anchor = frag; + delete node.$id; + } + if (typeof node.$id === 'string' && node.$id && !node.$id.startsWith('#')) baseId = node.$id; + + if (typeof node.$anchor === 'string' && node.$anchor) { + const key = `${baseId}#${node.$anchor}`; + if (seen.has(key)) delete node.$anchor; + else seen.add(key); + } + + const keys = [ + 'properties','patternProperties','$defs','definitions', + 'items','prefixItems','contains','if','then','else','not','allOf','anyOf','oneOf','additionalProperties' + ]; + for (const k of keys) { + const ch = node[k]; + if (!ch) continue; + if (Array.isArray(ch)) ch.forEach(c => traverse(c, baseId)); + else if (typeof ch === 'object') { + if (k === 'properties' || k === 'patternProperties' || k === '$defs' || k === 'definitions') { + Object.values(ch).forEach((c: any) => traverse(c, baseId)); + } else traverse(ch, baseId); + } + } + }; + traverse(root, root.$id || ''); + return root; } \ No newline at end of file diff --git a/src/renderer/components/stages/CachingService.tsx b/src/renderer/components/stages/CachingService.tsx index ff131cf6..38d4aaef 100644 --- a/src/renderer/components/stages/CachingService.tsx +++ b/src/renderer/components/stages/CachingService.tsx @@ -27,7 +27,7 @@ import { getStageDetails, getSubStageDetails } from "../../../services/StageDeta import { getProgress, setVsamInitState, updateSubStepSkipStatus, getInstallationArguments, getVsamInitState, isInitializationStageComplete, getZoweMajorVersion } from "./progress/StageProgressStatus"; import { InitSubStepsState } from "../../../types/stateInterfaces"; import { alertEmitter } from "../Header"; -import { DEF_ZOWE_MAJOR_VERS, INIT_STAGE_LABEL, ajv } from "../common/Utils"; +import { sanitizeOldStyleAnchors, DEF_ZOWE_MAJOR_VERS, INIT_STAGE_LABEL, ajv } from "../common/Utils"; const CachingService = () => { @@ -70,7 +70,7 @@ const CachingService = () => { const [defaultErrorMessage] = useState("Please ensure that the volume, storage class & dataset values are accurate."); - const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(setupSchema)) + const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(sanitizeOldStyleAnchors(setupSchema))); useEffect(() => { stageStatusRef.current = stageStatus; diff --git a/src/renderer/components/stages/Certificates.tsx b/src/renderer/components/stages/Certificates.tsx index cd216abc..c34d0fe1 100644 --- a/src/renderer/components/stages/Certificates.tsx +++ b/src/renderer/components/stages/Certificates.tsx @@ -26,7 +26,7 @@ import { setActiveStep } from "./progress/activeStepSlice"; import { getStageDetails, getSubStageDetails } from "../../../services/StageDetails"; import { getProgress, setCertificateInitState, getCertificateInitState, updateSubStepSkipStatus, getInstallationArguments, isInitializationStageComplete } from "./progress/StageProgressStatus"; import { CertInitSubStepsState } from "../../../types/stateInterfaces"; -import { TYPE_YAML, TYPE_OUTPUT, INIT_STAGE_LABEL, CERTIFICATES_STAGE_LABEL, ajv, deepMerge } from "../common/Utils"; +import { sanitizeOldStyleAnchors, TYPE_YAML, TYPE_OUTPUT, INIT_STAGE_LABEL, CERTIFICATES_STAGE_LABEL, ajv, deepMerge } from "../common/Utils"; const Certificates = () => { @@ -59,7 +59,7 @@ const Certificates = () => { let timer: any; - const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(setupSchema)) + const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(sanitizeOldStyleAnchors(setupSchema))); useEffect(() => { stageStatusRef.current = stageStatus; diff --git a/src/renderer/components/stages/Security.tsx b/src/renderer/components/stages/Security.tsx index 4241e5e5..3975ffce 100644 --- a/src/renderer/components/stages/Security.tsx +++ b/src/renderer/components/stages/Security.tsx @@ -26,7 +26,7 @@ import { setActiveStep } from "./progress/activeStepSlice"; import { getStageDetails, getSubStageDetails } from "../../../services/StageDetails"; import { setProgress, getProgress, setSecurityInitState, getSecurityInitState, updateSubStepSkipStatus, getInstallationArguments, isInitializationStageComplete } from "./progress/StageProgressStatus"; import { InitSubStepsState } from "../../../types/stateInterfaces"; -import { JCL_UNIX_SCRIPT_OK, INIT_STAGE_LABEL, SECURITY_STAGE_LABEL, ajv, SERVER_COMMON } from '../common/Utils'; +import { sanitizeOldStyleAnchors, JCL_UNIX_SCRIPT_OK, INIT_STAGE_LABEL, SECURITY_STAGE_LABEL, ajv, SERVER_COMMON } from '../common/Utils'; import { alertEmitter } from "../Header"; const Security = () => { @@ -60,7 +60,7 @@ const Security = () => { const [connectionArgs] = useState(useAppSelector(selectConnectionArgs)); let timer: any; - const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(setupSchema)); + const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(sanitizeOldStyleAnchors(setupSchema))); useEffect(() => { stageStatusRef.current = stageStatus; diff --git a/src/renderer/components/stages/installation/Installation.tsx b/src/renderer/components/stages/installation/Installation.tsx index 7f131529..0e6c986b 100644 --- a/src/renderer/components/stages/installation/Installation.tsx +++ b/src/renderer/components/stages/installation/Installation.tsx @@ -24,7 +24,7 @@ import { alertEmitter } from "../../Header"; import { createTheme } from '@mui/material/styles'; import {stages} from "../../configuration-wizard/Wizard"; import { setActiveStep } from "../progress/activeStepSlice"; -import { TYPE_YAML, TYPE_OUTPUT, JCL_UNIX_SCRIPT_OK, FALLBACK_YAML, ajv, INIT_STAGE_LABEL, INSTALL_STAGE_LABEL} from '../../common/Utils'; +import { sanitizeOldStyleAnchors, TYPE_YAML, TYPE_OUTPUT, JCL_UNIX_SCRIPT_OK, FALLBACK_YAML, ajv, INIT_STAGE_LABEL, INSTALL_STAGE_LABEL} from '../../common/Utils'; import { getStageDetails, getSubStageDetails } from "../../../../services/StageDetails"; import { getProgress, setDatasetInstallationState, getDatasetInstallationState, getInstallationTypeStatus, updateSubStepSkipStatus, getInstallationArguments, datasetInstallationStatus, isInitializationStageComplete } from "../progress/StageProgressStatus"; import { DatasetInstallationState } from "../../../../types/stateInterfaces"; @@ -66,7 +66,7 @@ const Installation = () => { let timer: any; const [installationType] = useState(getInstallationTypeStatus().installationType); - const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(setupSchema)); + const [validate] = useState(() => ajv.getSchema("https://zowe.org/schemas/v3/server-base") || ajv.compile(sanitizeOldStyleAnchors(setupSchema))); useEffect(() => { stageStatusRef.current = stageStatus; From f62328608698a99a40a59a48017bdaa2d2115880 Mon Sep 17 00:00:00 2001 From: Leanid Astrakou Date: Thu, 18 Sep 2025 21:47:43 -0400 Subject: [PATCH 2/2] Updated names & comments for clarity Signed-off-by: Leanid Astrakou --- src/renderer/components/common/JsonForms.tsx | 37 +++++++++++--------- src/renderer/components/common/Stepper.tsx | 3 +- src/renderer/components/common/Utils.tsx | 19 ++++++---- 3 files changed, 35 insertions(+), 24 deletions(-) diff --git a/src/renderer/components/common/JsonForms.tsx b/src/renderer/components/common/JsonForms.tsx index e06b3ed3..e53a6026 100644 --- a/src/renderer/components/common/JsonForms.tsx +++ b/src/renderer/components/common/JsonForms.tsx @@ -188,51 +188,56 @@ const makeUISchema = (schema: any, base: string, formData: any): any => { } /* resolveCombinators takes a schema plus the current form data and collapses any oneOf/anyOf it finds. -For each combinator, it picks the option that best matches the data (or index 0 as a fallback), +For each combinator, it chooses the option that best matches the data (or index 0 as a fallback), then replaces the whole combinator node with that chosen subschema and continues recursively through properties/items/$defs, etc. The result is schema without combinators, so JsonForms won’t render tabs as a result of multiple sources */ const resolveCombinators = (schema: any, data: any): any => { - const s = JSON.parse(JSON.stringify(schema)); + const resolvedSchema = JSON.parse(JSON.stringify(schema)); - const pick = (arr: any[], dataSlice: any) => { - // reuse your findMatchingSchemaIndex or default to 0 - const i = findMatchingSchemaIndex(dataSlice ?? {}, arr); - return arr[i] || arr[0]; + const choose = (schemasArray: any[], formData: any) => { + // Find the matching schema for the given form data or default to 0 + return schemasArray[findMatchingSchemaIndex(formData ?? {}, schemasArray)] || schemasArray[0]; }; - const traverse = (node: any, dataSlice: any) => { + const traverse = (node: any, formData: any) => { if (!node || typeof node !== 'object') return; if (Array.isArray(node.oneOf)) { - const chosen = pick(node.oneOf, dataSlice); + const chosen = choose(node.oneOf, formData); Object.keys(node).forEach(k => delete node[k]); Object.assign(node, chosen); } if (Array.isArray(node.anyOf)) { - const chosen = pick(node.anyOf, dataSlice); + const chosen = choose(node.anyOf, formData); Object.keys(node).forEach(k => delete node[k]); Object.assign(node, chosen); } - // descend into typical containers + // descend down usual containerized tree if (node.properties && typeof node.properties === 'object') { for (const [k, v] of Object.entries(node.properties)) { - traverse(v, dataSlice?.[k]); + traverse(v, formData?.[k]); } } for (const key of ['items','contains','if','then','else','not']) { - if (node[key]) traverse(node[key], dataSlice); + if (node[key]) { + traverse(node[key], formData); + } } for (const key of ['allOf','anyOf','oneOf','prefixItems']) { - if (Array.isArray(node[key])) node[key].forEach((x: any) => traverse(x, dataSlice)); + if (Array.isArray(node[key])) { + node[key].forEach((x: any) => traverse(x, formData)); + } + } + if (node.$defs) { + Object.values(node.$defs).forEach((x: any) => traverse(x, undefined)); } - if (node.$defs) Object.values(node.$defs).forEach((x: any) => traverse(x, undefined)); }; - traverse(s, data); - return s; + traverse(resolvedSchema, data); + return resolvedSchema; } export default function JsonForm(props: any) { diff --git a/src/renderer/components/common/Stepper.tsx b/src/renderer/components/common/Stepper.tsx index 382ce77c..2dec07ca 100644 --- a/src/renderer/components/common/Stepper.tsx +++ b/src/renderer/components/common/Stepper.tsx @@ -347,6 +347,7 @@ export default function HorizontalLinearStepper({stages, initialization}:{stages } {stages[activeStep] && stages[activeStep].isSkippable && + !skipButtonDisabled(stages, activeStep, activeSubStep) && ( - } + )} {stages[activeStep] && stages[activeStep].nextButton &&