Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 59 additions & 2 deletions src/renderer/components/common/JsonForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand Down Expand Up @@ -118,6 +119,7 @@ const findMatchingSchemaIndex = (formData: any, oneOfSchemas: any) => {
};

const makeUISchema = (schema: any, base: string, formData: any): any => {

if (!schema || !formData) {
return "";
}
Expand Down Expand Up @@ -185,6 +187,59 @@ 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 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 resolvedSchema = JSON.parse(JSON.stringify(schema));

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Check that this method makes a decision based on different types of objects covered by combinator

};

const traverse = (node: any, formData: any) => {
if (!node || typeof node !== 'object') return;

if (Array.isArray(node.oneOf)) {
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 = choose(node.anyOf, formData);
Object.keys(node).forEach(k => delete node[k]);
Object.assign(node, chosen);
}

// descend down usual containerized tree
if (node.properties && typeof node.properties === 'object') {
for (const [k, v] of Object.entries(node.properties)) {
traverse(v, formData?.[k]);
}
}
for (const key of ['items','contains','if','then','else','not']) {
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, formData));
}
}
if (node.$defs) {
Object.values(node.$defs).forEach((x: any) => traverse(x, undefined));
}
};

traverse(resolvedSchema, data);
return resolvedSchema;
}

export default function JsonForm(props: any) {
let {schema, onChange, formData} = props;

Expand All @@ -209,6 +264,8 @@ export default function JsonForm(props: any) {
onChange(formData, schemaIndex);
}

const sanitizedSchema = resolveCombinators(sanitizeOldStyleAnchors(requiredSchema), formData);

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few sentences summary of overall AJV/schema context for this issue woudl be good


return (
<ThemeProvider theme={jsonFormTheme}>
{ schema.oneOf &&
Expand All @@ -234,8 +291,8 @@ export default function JsonForm(props: any) {

}
<JsonForms
schema={requiredSchema}
uischema={makeUISchema(requiredSchema, '/', formData)}
schema={sanitizedSchema}
uischema={makeUISchema(sanitizedSchema, '/', formData)}
data={formData}
renderers={materialRenderers}
cells={materialCells}
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/components/common/Stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,7 @@ export default function HorizontalLinearStepper({stages, initialization}:{stages
</Button>
}
{stages[activeStep] && stages[activeStep].isSkippable &&
!skipButtonDisabled(stages, activeStep, activeSubStep) && (
<Button
disabled={skipButtonDisabled(stages, activeStep, activeSubStep)}
variant="contained"
Expand All @@ -355,7 +356,7 @@ export default function HorizontalLinearStepper({stages, initialization}:{stages
>
Skip {stages[activeStep] && stages[activeStep].subStages ? stages[activeStep].subStages[activeSubStep].label : stages[activeStep]? stages[activeStep].label: ''}
</Button>
}
)}
{stages[activeStep] && stages[activeStep].nextButton &&
<Button
disabled={!isNextStepEnabled}
Expand Down
50 changes: 50 additions & 0 deletions src/renderer/components/common/Utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2344,4 +2344,54 @@ export const FALLBACK_YAML = {
"port": 7553
}
}
}

/* sanitizeOldStyleAnchors deep-clones, then recursively traverses the JSON Schema tree
Starting at root node, it converts legacy fragment "$id": "#name" into "$anchor": "name",
tracks the current base $id as it descends (calls itself on each child schema (properties, items, oneOf, etc.)),
and removes duplicate $anchors under the same base $id. It only touches anchors (and that legacy $id form), leaving $refs and everything else intact

So Ajv won’t throw the “reference resolves to more than one schema” error when trying to merge old-style & new style Zowe schema */
export function sanitizeOldStyleAnchors(schema: any) {
const root = JSON.parse(JSON.stringify(schema));
const traversedAnchors: Set<string> = 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 (traversedAnchors.has(key)) {
delete node.$anchor;
}
else traversedAnchors.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;
}
4 changes: 2 additions & 2 deletions src/renderer/components/stages/CachingService.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {

Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/stages/Certificates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {

Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/stages/Security.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/components/stages/installation/Installation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
Loading