Skip to content

Commit

Permalink
Merge pull request #241 from zowe/update-jsonforms
Browse files Browse the repository at this point in the history
 Update JSON Forms
  • Loading branch information
skurnevich authored Sep 3, 2024
2 parents b02c5a4 + 151e9e9 commit e8960c5
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 186 deletions.
3 changes: 1 addition & 2 deletions src/actions/InstallationHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,11 +249,10 @@ class Installation {
await this.getExampleYamlAndSchemas(connectionArgs, installationArgs).then((res: IResponse) => {
if(res.status){
if(res.details.mergedYaml != undefined){
yamlObj = res.details.mergedYaml
yamlObj = res.details.mergedYaml;
}
}
})

return {status: download.status && uploadYaml.status && upload.status && unpax.status, details: {message: 'Zowe unpax successful.', mergedYaml: yamlObj}};
} catch (error) {
return {status: false, details: error.message};
Expand Down
250 changes: 174 additions & 76 deletions src/renderer/components/common/JsonForms.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,42 +13,141 @@ import { JsonForms } from '@jsonforms/react';
import { materialRenderers, materialCells } from '@jsonforms/material-renderers';
import { ThemeProvider } from '@mui/material/styles';
import jsonFormTheme from '../../jsonFormsTheme';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';

// Creates a basic input element in the UI schema
const createControl = (scope: string, label?: string) => ({
type: 'Control',
label: label ? label.charAt(0).toUpperCase() + label.slice(1).toLowerCase() : undefined,
scope, // Scope specifies the JSON path
});

// createGroup generates a group object, used to group together multiple form elements - can optionally include a rule (like visibility)
const createGroup = (label: string, elements: any[], rule?: any) => ({
type: 'Group',
label: `\n${label}`,
elements: elements || [],
...(rule && {rule}),
});

// createVerticalLayout generates a layout object that arranges its child elements vertically.
const createVerticalLayout = (elements: any[]) => ({
type: 'VerticalLayout',
elements,
});

// Same as above, but arranges its child elements horizontally.
const createHorizontalLayout = (elements: any[]) => ({
type: 'HorizontalLayout',
elements,
});

// To handle the "if", "else", and "then" in the schema
const conditionalSchema = (schema: any, formData: any, prop: any): boolean=> {
if(schema.if && schema.then && schema.else){
const ifProp = Object.keys(schema.if.properties)[0];
const ifPropValue = schema.if.properties[ifProp].const.toLowerCase();
const thenProp = schema.then.required[0].toLowerCase();
const elseProp = schema.else.required[0].toLowerCase();

if(formData && formData[ifProp]) {
const formDataPropValue = formData[ifProp].toLowerCase();
if( (formDataPropValue == ifPropValue && prop == elseProp) || (formDataPropValue != ifPropValue && prop == thenProp) ) {
return true;
}
}
return false;
}
return false;
}

const getDefaultFormData = (schema: any, formData: any) => {
if (schema.oneOf) {
schema = schema.oneOf[0];
}
if (schema && schema.properties) {
const defaultFormData = { ...formData };
Object.keys(schema.properties).forEach((property) => {
if (schema.properties[property].type && schema.properties[property].default !== undefined || schema.properties[property].type === 'object') {
// If the property is an object, recursively set default values
if (schema.properties[property].type === 'object') {
defaultFormData[property] = getDefaultFormData(
schema.properties[property],
defaultFormData[property] || {}
);
} else {
defaultFormData[property] = schema.properties[property].default;
}
}
});
return defaultFormData;
}
return null;
};

// Function to return form data based on the attributes present in the schema
const filterFormData = (data: { [key: string]: any }, schema: any) => {
if (!data) {
return null;
}
const filteredData: { [key: string]: any } = {};
const schemaProperties = schema?.properties || {};

Object.keys(data).forEach(key => {
if (key in schemaProperties) {
filteredData[key] = data[key];
}
});

return filteredData;
};

// Function to find the matching schema for the given form data
const findMatchingSchemaIndex = (formData: any, oneOfSchemas: any) => {
for (let i = 0; i < oneOfSchemas.length; i++) {
const subSchema = oneOfSchemas[i];
const filteredData = filterFormData(formData, subSchema);
if (filteredData && JSON.stringify(filteredData) === JSON.stringify(formData)) {
return i;
}
}
return 0; // Default to the first schema if no match is found
};

const makeUISchema = (schema: any, base: string, formData: any): any => {
if (!schema || !formData) {
return "";
}

if(schema.oneOf) {
const schemaIndex = findMatchingSchemaIndex(formData, schema.oneOf)
schema = schema.oneOf[schemaIndex];
}

const properties = Object.keys(schema?.properties);

// Creates a basic input element in the UI schema
const createControl = (scope: string) => ({
type: 'Control',
scope, // Scope specifies the JSON path
});
// Map each property in the JSON schema to an appropriate UI element based on its type and structure.
const elements = properties.map((prop: any) => {

// createGroup generates a group object, used to group together multiple form elements - can optionally include a rule (like visibility)
const createGroup = (label: string, elements: any[], rule?: any) => ({
type: 'Group',
label: `\n${label}`,
elements: elements || [],
...(rule && {rule}),
});
const propertySchema = schema.properties[prop];

// createVerticalLayout generates a layout object that arranges its child elements vertically.
const createVerticalLayout = (elements: any[]) => ({
type: 'VerticalLayout',
elements,
});
if (propertySchema.anyOf && propertySchema.anyOf.length > 0) {
let matchingPropertyIndex = findMatchingSchemaIndex(formData[prop], propertySchema.anyOf);
let selectedProperty = propertySchema.anyOf[matchingPropertyIndex];

// Same as above, but arranges its child elements horizontally.
const createHorizontalLayout = (elements: any[]) => ({
type: 'HorizontalLayout',
elements,
});
if (selectedProperty.type === 'null') {
matchingPropertyIndex = matchingPropertyIndex > 0 ? matchingPropertyIndex-1 : propertySchema.anyOf.length-1;
}

// Map each property in the JSON schema to an appropriate UI element based on its type and structure.
const elements = properties.map((prop: any) => {
if (schema.properties[prop].type === 'object') {
if (propertySchema.type !== 'object') {
return createControl(`#/properties${base}${prop}/anyOf[${matchingPropertyIndex}]`, prop);
}
}

if (propertySchema.type === 'object') {
// Create a group with a hide rule if patternProperties are present or conditional hiding is required.
if (schema.properties[prop].patternProperties || (schema.if && conditionalSchema(schema, formData, prop))) {
return createGroup(prop, [], {
Expand Down Expand Up @@ -86,64 +185,63 @@ const makeUISchema = (schema: any, base: string, formData: any): any => {
return createVerticalLayout(elements); // Return whole structure
}

// To handle the "if", "else", and "then" in the schema
const conditionalSchema = (schema: any, formData: any, prop: any): boolean=> {
if(schema.if && schema.then && schema.else){
const ifProp = Object.keys(schema.if.properties)[0];
const ifPropValue = schema.if.properties[ifProp].const.toLowerCase();
const thenProp = schema.then.required[0].toLowerCase();
const elseProp = schema.else.required[0].toLowerCase();

if(formData && formData[ifProp]) {
const formDataPropValue = formData[ifProp].toLowerCase();
if( (formDataPropValue == ifPropValue && prop == elseProp) || (formDataPropValue != ifPropValue && prop == thenProp) ) {
return true;
}
}
return false;
}
return false;
}

const getDefaultFormData = (schema: any, formData: any) => {
if (schema && schema.properties) {
const defaultFormData = { ...formData };
Object.keys(schema.properties).forEach((property) => {
if (schema.properties[property].type && schema.properties[property].default !== undefined || schema.properties[property].type === 'object') {
// If the property is an object, recursively set default values
if (schema.properties[property].type === 'object') {
defaultFormData[property] = getDefaultFormData(
schema.properties[property],
defaultFormData[property] || {}
);
} else {
defaultFormData[property] = schema.properties[property].default;
}
}
});
return defaultFormData;
}
return null;
};

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

const isFormDataEmpty = formData === null || formData === undefined || Object.keys(formData).length < 1;
formData = isFormDataEmpty ? getDefaultFormData(schema, {}) : formData;

const formDataToUse = isFormDataEmpty ? getDefaultFormData(schema, {}) : formData;
const [selectedSchemaIndex, setSelectedSchemaIndex] = schema.oneOf ? useState(findMatchingSchemaIndex(formData, schema.oneOf)) : useState(0);
const [requiredSchema, setRequiredSchema] = useState(schema);

useEffect(() => {
if (schema?.oneOf) {
setRequiredSchema(schema.oneOf[selectedSchemaIndex]);
formData = filterFormData(formData, schema.oneOf[selectedSchemaIndex]);
}
}, []);

const handleSchemaChange = (event: any) => {
const schemaIndex = parseInt(event.target.value);
setSelectedSchemaIndex(schemaIndex);
setRequiredSchema(schema.oneOf[schemaIndex]);
formData = filterFormData(formData, schema.oneOf[schemaIndex]);
onChange(formData, schemaIndex);
}

return (
<ThemeProvider theme={jsonFormTheme}>
<JsonForms
schema={schema}
uischema={makeUISchema(schema, '/', formData)}
data={formDataToUse}
renderers={materialRenderers}
cells={materialCells}
config={{showUnfocusedDescription: true}}
onChange={({ data, errors }) => { onChange(data) }}
/>
{ schema.oneOf &&
<div>
<FormControl>
<RadioGroup
row
name="controlled-radio-buttons-group"
value={selectedSchemaIndex}
onChange={handleSchemaChange}
>
{ schema.oneOf.map((schemaOption: any, index: number) => (
<FormControlLabel
key={index}
value={index}
control={<Radio />}
label={schemaOption.name ? schemaOption.name : `Option ${index+1}`}
/>
))}
</RadioGroup>
</FormControl>
</div>

}
<JsonForms
schema={requiredSchema}
uischema={makeUISchema(requiredSchema, '/', formData)}
data={formData}
renderers={materialRenderers}
cells={materialCells}
config={{showUnfocusedDescription: true}}
onChange={({ data, errors }) => { schema.oneOf ? onChange(data, selectedSchemaIndex) : onChange(data) }}
/>
</ThemeProvider>
);
}
38 changes: 23 additions & 15 deletions src/renderer/components/stages/Certificates.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,11 @@ const Certificates = () => {

const dispatch = useAppDispatch();
const [schema, setLocalSchema] = useState(useAppSelector(selectSchema));
const [yaml, setLocalYaml] = useState(useAppSelector(selectYaml));
const yaml = useAppSelector(selectYaml);
const [connectionArgs] = useState(useAppSelector(selectConnectionArgs));
const [installationArgs] = useState(getInstallationArguments());
const [setupSchema] = useState(schema?.properties?.zowe?.properties?.setup?.properties?.certificate);
const [setupYaml, setSetupYaml] = useState(yaml?.zowe?.setup?.certificate);
const setupYaml = yaml?.zowe?.setup?.certificate;
const [verifyCerts, setVerifyCerts] = useState(yaml?.zowe?.verifyCertificates ?? "STRICT");
const [isFormInit, setIsFormInit] = useState(false);
const [editorVisible, setEditorVisible] = useState(false);
Expand Down Expand Up @@ -188,7 +188,6 @@ const Certificates = () => {
if(res.details.updatedYaml != undefined){
const updatedCerts = res.details.updatedYaml.zowe?.certificate;
const updatedYaml = {...yaml, zowe: {...yaml.zowe, certificate: updatedCerts}};
setSetupYaml(res.details.updatedYaml.zowe?.setup.certificate);
window.electron.ipcRenderer.setConfig(updatedYaml);
dispatch(setYaml(updatedYaml));
}
Expand Down Expand Up @@ -218,7 +217,7 @@ const Certificates = () => {
setEditorVisible(!editorVisible);
};

const handleFormChange = (data: any) => {
const handleFormChange = (data: any, schemaOption?: any) => {
if(data?.zowe?.verifyCertificates){
setVerifyCerts(data.zowe.verifyCertificates);
}
Expand All @@ -228,24 +227,34 @@ const Certificates = () => {
if (newData) {
if(validate) {
validate(newData);

if(validate.errors) {
const errPath = validate.errors[0].schemaPath;
const errMsg = validate.errors[0].message;
setStageConfig(false, errPath+' '+errMsg, newData);
const { schemaPath, message } = validate.errors[0];
let errorText = `${schemaPath} ${message}`;

if(schemaOption !== undefined && schemaOption !== null) {
validate.errors.forEach(err => {
if (err.schemaPath.includes(schemaOption)) {
errorText = err.message;
}
})
}

setStageConfig(false, errorText, newData);
} else {
window.electron.ipcRenderer.setConfig({...yaml, zowe: {...yaml.zowe, setup: {...yaml.zowe.setup, certificate: newData}}});
setLocalYaml({...yaml, zowe: {...yaml.zowe, setup: {...yaml.zowe.setup, certificate: newData}}});
setStageConfig(true, '', newData);
}

const updatedYaml = {...yaml, zowe: {...yaml.zowe, setup: {...yaml.zowe.setup, certificate: newData}}};
window.electron.ipcRenderer.setConfig(updatedYaml);
dispatch(setYaml(updatedYaml));
}
}
};


const setStageConfig = (isValid: boolean, errorMsg: string, data: any) => {
setIsFormValid(isValid);
setFormError(errorMsg);
setSetupYaml(data);
}

return (
Expand All @@ -264,10 +273,9 @@ const Certificates = () => {
setStageConfig(true, '', newData);
}
}/> }
<Box sx={{ width: '60vw' }} onBlur={async () => dispatch(setYaml((await window.electron.ipcRenderer.getConfig()).details ?? yaml))}>
<Box sx={{ width: '60vw' }}>
{!isFormValid && <div style={{color: 'red', fontSize: 'small', marginBottom: '20px'}}>{formError}</div>}
<JsonForm schema={setupSchema} onChange={handleFormChange} formData={setupYaml}/>
{/* <JsonForm schema={verifyCertsSchema} onChange={handleVerifyCertsChange} formData={verifyCertsYaml}/> */}
<JsonForm schema={{type: "object", ...setupSchema}} onChange={handleFormChange} formData={setupYaml}/>
<p style={{fontSize: "24px"}}>Verify Certificates</p>
<FormControl variant="standard" sx={{ m: 1, minWidth: 120 }}>
<Select
Expand All @@ -278,7 +286,7 @@ const Certificates = () => {
dispatchActions(false);
const newConfig = {...yaml, zowe: {...yaml?.zowe, verifyCertificates: e.target.value, setup: {...yaml.zowe.setup}}};
window.electron.ipcRenderer.setConfig(newConfig);
setLocalYaml(newConfig)
dispatch(setYaml(newConfig));
setVerifyCerts(e.target.value);
}}
>
Expand Down
Loading

0 comments on commit e8960c5

Please sign in to comment.