Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@red-hat-developer-hub/backstage-plugin-orchestrator-form-react': patch
'@red-hat-developer-hub/backstage-plugin-orchestrator': patch
---

Exclude omitFromWorkflowInput fields from execution payloads and add a review
toggle to show hidden parameters.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { OrchestratorFormContextProps } from '@red-hat-developer-hub/backstage-p
import { TranslationFunction } from '../hooks/useTranslation';
import extractStaticDefaults from '../utils/extractStaticDefaults';
import generateUiSchema from '../utils/generateUiSchema';
import { pruneFormData } from '../utils/pruneFormData';
import { omitFromWorkflowInput, pruneFormData } from '../utils/pruneFormData';
import { StepperContextProvider } from '../utils/StepperContext';
import OrchestratorFormWrapper from './OrchestratorFormWrapper';
import ReviewStep from './ReviewStep';
Expand Down Expand Up @@ -149,10 +149,14 @@ const OrchestratorForm = ({
return pruneFormData(formData, schema);
}, [formData, schema]);

const workflowInputData = useMemo(() => {
return omitFromWorkflowInput(prunedFormData, schema);
}, [prunedFormData, schema]);

const _handleExecute = useCallback(() => {
// Use pruned data for execution to avoid submitting stale properties
handleExecute(prunedFormData);
}, [prunedFormData, handleExecute]);
handleExecute(workflowInputData);
}, [workflowInputData, handleExecute]);

const onSubmit = useCallback(
(_formData: JsonObject) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@
* limitations under the License.
*/

import { useMemo } from 'react';
import { useMemo, useState } from 'react';

import { Content } from '@backstage/core-components';
import { JsonObject } from '@backstage/types';

import Alert from '@mui/material/Alert';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import FormControlLabel from '@mui/material/FormControlLabel';
import Paper from '@mui/material/Paper';
import Switch from '@mui/material/Switch';
import Typography from '@mui/material/Typography';
import type { JSONSchema7 } from 'json-schema';
import { get } from 'lodash';
import { makeStyles } from 'tss-react/mui';
Expand Down Expand Up @@ -60,6 +63,12 @@ const useStyles = makeStyles()(theme => ({
hiddenFieldsAlert: {
marginBottom: theme.spacing(2),
},
hiddenFieldsAction: {
marginLeft: theme.spacing(2),
},
hiddenFieldsText: {
fontSize: theme.typography.body1.fontSize,
},
}));

/**
Expand Down Expand Up @@ -115,9 +124,12 @@ const ReviewStep = ({

const { classes } = useStyles();
const { handleBack } = useStepperContext();
const [showHiddenFields, setShowHiddenFields] = useState(false);
const displayData = useMemo<JsonObject>(() => {
return generateReviewTableData(schema, data);
}, [schema, data]);
return generateReviewTableData(schema, data, {
includeHiddenFields: showHiddenFields,
});
}, [schema, data, showHiddenFields]);

const showHiddenFieldsNote = useMemo(() => {
return hasHiddenFields(schema);
Expand All @@ -127,8 +139,32 @@ const ReviewStep = ({
<Content noPadding>
<Paper square elevation={0} className={classes.paper}>
{showHiddenFieldsNote && (
<Alert severity="info" className={classes.hiddenFieldsAlert}>
{t('reviewStep.hiddenFieldsNote')}
<Alert
severity="info"
className={classes.hiddenFieldsAlert}
action={
<FormControlLabel
className={classes.hiddenFieldsAction}
control={
<Switch
checked={showHiddenFields}
onChange={event =>
setShowHiddenFields(event.target.checked)
}
color="primary"
/>
}
label={
<Typography className={classes.hiddenFieldsText}>
{t('reviewStep.showHiddenParameters')}
</Typography>
}
/>
}
>
<Typography className={classes.hiddenFieldsText}>
{t('reviewStep.hiddenFieldsNote')}
</Typography>
</Alert>
)}
<NestedReviewTable data={displayData} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,38 @@ describe('mapSchemaToData', () => {
expect(result).toEqual(expectedResult);
});

it('should include hidden fields when includeHiddenFields is true', () => {
const schema: JSONSchema7 = {
type: 'object',
properties: {
visibleField: {
type: 'string',
title: 'Visible Field',
},
hiddenField: {
type: 'string',
title: 'Hidden Field',
'ui:hidden': true,
} as JSONSchema7,
},
};

const data = {
visibleField: 'shown',
hiddenField: 'should appear',
};

const expectedResult = {
'Visible Field': 'shown',
'Hidden Field': 'should appear',
};

const result = generateReviewTableData(schema, data, {
includeHiddenFields: true,
});
expect(result).toEqual(expectedResult);
});

it('should exclude nested hidden fields from review table', () => {
const schema: JSONSchema7 = {
type: 'object',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export function processSchema(
value: JsonValue | undefined,
schema: JSONSchema7,
formState: JsonObject,
includeHiddenFields: boolean,
): JsonObject {
const parsedSchema = new JSONSchema(schema);
const definitionInSchema =
Expand All @@ -43,7 +44,7 @@ export function processSchema(
if (definitionInSchema) {
// Skip hidden fields in the review table
const uiHidden = definitionInSchema['ui:hidden'];
if (uiHidden !== undefined) {
if (!includeHiddenFields && uiHidden !== undefined) {
// Handle both static boolean and condition objects
const hiddenCondition = uiHidden as HiddenCondition;
const isHidden = evaluateHiddenCondition(hiddenCondition, formState);
Expand All @@ -63,7 +64,13 @@ export function processSchema(
const curKey = key ? `${key}/${nestedKey}` : nestedKey;
return {
...prev,
...processSchema(curKey, _nestedValue, schema, formState),
...processSchema(
curKey,
_nestedValue,
schema,
formState,
includeHiddenFields,
),
};
},
{},
Expand All @@ -84,9 +91,16 @@ export function processSchema(
function generateReviewTableData(
schema: JSONSchema7,
data: JsonObject,
options?: { includeHiddenFields?: boolean },
): JsonObject {
schema.title = '';
const result = processSchema('', data, schema, data);
const result = processSchema(
'',
data,
schema,
data,
options?.includeHiddenFields ?? false,
);
return result[''] as JsonObject;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { JsonObject } from '@backstage/types';

import { JSONSchema7 } from 'json-schema';

import { pruneFormData } from './pruneFormData';
import { omitFromWorkflowInput, pruneFormData } from './pruneFormData';

describe('pruneFormData', () => {
describe('Basic Property Handling', () => {
Expand Down Expand Up @@ -726,4 +726,63 @@ describe('pruneFormData', () => {
expect(result.step1).not.toHaveProperty('advancedField2');
});
});

describe('omitFromWorkflowInput', () => {
it('should remove properties marked with omitFromWorkflowInput', () => {
const schema: JSONSchema7 = {
type: 'object',
properties: {
visible: { type: 'string' },
hidden: {
type: 'string',
omitFromWorkflowInput: true,
} as JSONSchema7,
},
};

const formData = {
visible: 'keep',
hidden: 'omit',
};

const result = omitFromWorkflowInput(formData, schema);

expect(result).toEqual({ visible: 'keep' });
expect(result).not.toHaveProperty('hidden');
});

it('should remove nested properties marked with omitFromWorkflowInput', () => {
const schema: JSONSchema7 = {
type: 'object',
properties: {
step: {
type: 'object',
properties: {
visible: { type: 'string' },
secret: {
type: 'string',
omitFromWorkflowInput: true,
} as JSONSchema7,
},
},
},
};

const formData = {
step: {
visible: 'keep',
secret: 'omit',
},
};

const result = omitFromWorkflowInput(formData, schema);

expect(result).toEqual({
step: {
visible: 'keep',
},
});
expect(result.step).not.toHaveProperty('secret');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ import { JsonObject, JsonValue } from '@backstage/types';

import type { JSONSchema7 } from 'json-schema';

type WorkflowInputSchema = JSONSchema7 & {
omitFromWorkflowInput?: boolean;
};

/**
* Resolves $ref references in a schema by looking in $defs
*/
Expand All @@ -40,6 +44,10 @@ function resolveSchema(
return schema;
}

function shouldOmitFromWorkflowInput(schema: JSONSchema7): boolean {
return (schema as WorkflowInputSchema).omitFromWorkflowInput === true;
}

/**
* Checks if a property's enum value matches the form data
*/
Expand Down Expand Up @@ -304,3 +312,80 @@ export function pruneFormData(

return pruned;
}

/**
* Removes fields marked with omitFromWorkflowInput from the form data.
* This keeps the review UI intact while excluding data from execution payloads.
*/
export function omitFromWorkflowInput(
formData: JsonObject,
schema: JSONSchema7,
rootSchema?: JSONSchema7,
): JsonObject {
const root = rootSchema || schema;
const filtered: JsonObject = {};

for (const [key, value] of Object.entries(formData)) {
if (value === undefined) continue;

let propSchema = schema.properties?.[key];
if (typeof propSchema === 'boolean' || !propSchema) {
filtered[key] = value as JsonValue;
continue;
}

propSchema = resolveSchema(propSchema as JSONSchema7, root);
if (shouldOmitFromWorkflowInput(propSchema)) {
continue;
}

if (
propSchema.type === 'object' &&
typeof value === 'object' &&
value !== null &&
!Array.isArray(value)
) {
filtered[key] = omitFromWorkflowInput(
value as JsonObject,
propSchema as JSONSchema7,
root,
);
continue;
}

if (propSchema.type === 'array' && Array.isArray(value)) {
const itemsSchema =
typeof propSchema.items === 'object' && propSchema.items
? resolveSchema(propSchema.items as JSONSchema7, root)
: undefined;

if (itemsSchema && shouldOmitFromWorkflowInput(itemsSchema)) {
continue;
}

if (
itemsSchema &&
itemsSchema.type === 'object' &&
itemsSchema.properties
) {
filtered[key] = value.map(item => {
if (typeof item === 'object' && item !== null) {
return omitFromWorkflowInput(
item as JsonObject,
itemsSchema as JSONSchema7,
root,
);
}
return item as JsonValue;
});
} else {
filtered[key] = value as JsonValue;
}
continue;
}

filtered[key] = value as JsonValue;
}

return filtered;
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ readonly "tooltips.workflowDown": string;
readonly "tooltips.suspended": string;
readonly "tooltips.userNotAuthorizedAbort": string;
readonly "reviewStep.hiddenFieldsNote": string;
readonly "reviewStep.showHiddenParameters": string;
readonly "permissions.accessDenied": string;
readonly "permissions.accessDeniedDescription": string;
readonly "permissions.requiredPermission": string;
Expand All @@ -185,7 +186,7 @@ export const orchestratorTranslations: TranslationResource<"plugin.orchestrator"
// src/components/catalogComponents/CatalogTab.d.ts:1:22 - (ae-undocumented) Missing documentation for "IsOrchestratorCatalogTabAvailable".
// src/components/catalogComponents/CatalogTab.d.ts:2:22 - (ae-undocumented) Missing documentation for "OrchestratorCatalogTab".
// src/translations/index.d.ts:2:22 - (ae-undocumented) Missing documentation for "orchestratorTranslations".
// src/translations/ref.d.ts:200:22 - (ae-undocumented) Missing documentation for "orchestratorTranslationRef".
// src/translations/ref.d.ts:201:22 - (ae-undocumented) Missing documentation for "orchestratorTranslationRef".

// (No @packageDocumentation comment for this package)

Expand Down
Loading