From 04e5495eede4261c7483ea4f17517b5466fc94bc Mon Sep 17 00:00:00 2001 From: Sean O'Keeffe Date: Thu, 3 Sep 2026 11:21:16 +0100 Subject: [PATCH 1/2] EDM-5152: Improve fleet and device Updates step UX and copy Update the fleet and device Updates wizard with an UpdateMode radio group, clearer rollout and maintenance copy, and corrected rollout policy patch logic for timeout-only policies. Co-authored-by: Celia Amador Made-with: Cursor --- libs/i18n/locales/en/translation.json | 23 +-- .../EditDeviceWizard/EditDeviceWizard.tsx | 9 +- .../steps/DeviceUpdateStep.tsx | 60 ++++--- .../steps/ReviewUpdatePolicy.tsx | 4 +- .../Device/EditDeviceWizard/utils.ts | 7 +- .../Fleet/CreateFleet/fleetSpecUtils.ts | 6 +- .../Fleet/CreateFleet/steps/ReviewStep.tsx | 8 +- .../CreateFleet/steps/UpdatePolicyStep.css | 7 +- .../CreateFleet/steps/UpdatePolicyStep.tsx | 151 +++++++++++------- .../src/components/Fleet/CreateFleet/utils.ts | 36 ++--- .../src/components/form/CheckboxField.tsx | 5 +- .../src/components/form/RadioField.tsx | 4 +- .../src/components/form/validations.ts | 22 +-- libs/ui-components/src/types/deviceSpec.ts | 13 +- libs/ui-components/src/utils/patch.ts | 32 ++-- 15 files changed, 226 insertions(+), 161 deletions(-) diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 05d6286b63..384bba64c3 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -967,12 +967,12 @@ "This device uses package-based OS management. System image configuration is not available for this device.": "This device uses package-based OS management. System image configuration is not available for this device.", "System image is managed by Software Catalog": "System image is managed by Software Catalog", "Must be a reference to a bootable container image (such as \"quay.io//my-rhel-with-fc-agent:\"). If you do not want to manage your OS from Edge management, leave this field empty.": "Must be a reference to a bootable container image (such as \"quay.io//my-rhel-with-fc-agent:\"). If you do not want to manage your OS from Edge management, leave this field empty.", - "Use basic configurations": "Use basic configurations", - "Advanced configurations": "Advanced configurations", - "Update policies": "Update policies", - "Update policies allow you to control when updates should be downloaded and applied.": "Update policies allow you to control when updates should be downloaded and applied.", - "Default update policy": "Default update policy", - "The device will download and apply updates as soon as they are available.": "The device will download and apply updates as soon as they are available.", + "Choose how updates are delivered to this device.": "Choose how updates are delivered to this device.", + "Update behavior": "Update behavior", + "Immediate updates": "Immediate updates", + "Devices receive updates as soon as they are available.": "Devices receive updates as soon as they are available.", + "Customize maintenance window and scheduling": "Customize maintenance window and scheduling", + "Set maintenance windows to control when updates are downloaded and applied.": "Set maintenance windows to control when updates are downloaded and applied.", "Device alias": "Device alias", "Device labels": "Device labels", "Unnamed": "Unnamed", @@ -1098,12 +1098,15 @@ "Device selector": "Device selector", "Rollout policy": "Rollout policy", "Disruption budget": "Disruption budget", - "Set rollout policies": "Set rollout policies", - "Rollout policies allow you to control the order of updates for the fleet devices.": "Rollout policies allow you to control the order of updates for the fleet devices.", + "Maintenance windows": "Maintenance windows", + "Set rollout order": "Set rollout order", + "Rollout order controls in what sequence fleet devices are updated.": "Rollout order controls in what sequence fleet devices are updated.", "Set disruption budget": "Set disruption budget", "Disruption budget allows you to limit the number of similar devices that may be updating simultaneously.": "Disruption budget allows you to limit the number of similar devices that may be updating simultaneously.", - "Set update policies": "Set update policies", - "All the devices that are part of this fleet will receive updates as soon as they are available.": "All the devices that are part of this fleet will receive updates as soon as they are available.", + "Set maintenance windows": "Set maintenance windows", + "Maintenance windows control when devices may download and install updates.": "Maintenance windows control when devices may download and install updates.", + "Choose how updates are delivered to devices in this fleet.": "Choose how updates are delivered to devices in this fleet.", + "Set rollout order, disruption budget, or maintenance windows.": "Set rollout order, disruption budget, or maintenance windows.", "Group devices by label keys": "Group devices by label keys", "The disruption budget will be applied to all group combinations independently.": "The disruption budget will be applied to all group combinations independently.", "Add label key": "Add label key", diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx index 5bbe8fb997..7f71a31b26 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/EditDeviceWizard.tsx @@ -14,7 +14,7 @@ import { import { type Device } from '@flightctl/types'; import { getUpdatePolicyValues } from '../../Fleet/CreateFleet/fleetSpecUtils'; -import { type EditDeviceFormValues } from './../../../types/deviceSpec'; +import { type EditDeviceFormValues, UpdateMode } from './../../../types/deviceSpec'; import { getErrorMessage } from '../../../utils/error'; import { fromAPILabel } from '../../../utils/labels'; import { getEditDisabledReason } from '../../../utils/devices'; @@ -111,11 +111,8 @@ const EditDeviceWizard = () => { applications: getApplicationValues(device.spec), systemdUnits: getSystemdUnitsValues(device.spec), registerMicroShift, - updatePolicy: { - ...updatePolicyValues, - isAdvanced: true, - }, - useBasicUpdateConfig: !updatePolicyValues.isAdvanced, + updatePolicy: updatePolicyValues, + updateMode: updatePolicyValues.isCustomized ? UpdateMode.Customized : UpdateMode.Default, }} validationSchema={getValidationSchema(t)} validateOnMount diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx index 359572feb6..2022f98b87 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx @@ -1,14 +1,15 @@ import * as React from 'react'; -import { Alert, Title } from '@patternfly/react-core'; +import { FormGroup, Stack, StackItem, Title } from '@patternfly/react-core'; import { type FormikErrors, useFormikContext } from 'formik'; import { useTranslation } from '../../../../hooks/useTranslation'; import UpdateStepUpdatePolicy from '../../../Fleet/CreateFleet/steps/UpdateStepUpdatePolicy'; +import { CustomizedUpdatesSectionBody } from '../../../Fleet/CreateFleet/steps/UpdatePolicyStep'; import FlightCtlForm from '../../../form/FlightCtlForm'; -import { type DeviceSpecConfigFormValues } from '../../../../types/deviceSpec'; -import { FormGroupWithHelperText } from '../../../common/WithHelperText'; -import CheckboxField from '../../../form/CheckboxField'; +import { type DeviceSpecConfigFormValues, UpdateMode } from '../../../../types/deviceSpec'; +import RadioField from '../../../form/RadioField'; +import { DefaultHelperText } from '../../../form/FieldHelperText'; export const deviceUpdatePolicyStepId = 'update-policy'; @@ -18,27 +19,44 @@ const UpdatePolicyStep = ({ isReadOnly }: { isReadOnly?: boolean }) => { const { t } = useTranslation(); const { - values: { useBasicUpdateConfig }, + values: { updateMode }, } = useFormikContext(); return ( - - {!useBasicUpdateConfig ? ( - <> - {t('Advanced configurations')} - - - - - ) : ( - - {t('The device will download and apply updates as soon as they are available.')} - - )} + + + + {t('Updates')} + + + + + + + + + + } + /> + + + ); }; diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ReviewUpdatePolicy.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ReviewUpdatePolicy.tsx index dafaaf61ee..2e62c9b58a 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ReviewUpdatePolicy.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/ReviewUpdatePolicy.tsx @@ -9,14 +9,14 @@ import LabelsView from '../../../common/LabelsView'; export const ReviewUpdateRolloutPolicy = ({ rolloutPolicy }: { rolloutPolicy: RolloutPolicyForm }) => { const { t } = useTranslation(); - return rolloutPolicy.isAdvanced + return rolloutPolicy.isCustomized ? t('{{ count }} batches have been defined', { count: rolloutPolicy.batches.length }) : '-'; }; export const ReviewUpdateDisruptionBudget = ({ disruptionBudget }: { disruptionBudget: DisruptionBudgetForm }) => { const { t } = useTranslation(); - if (!disruptionBudget.isAdvanced) { + if (!disruptionBudget.isCustomized) { return '-'; } diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/utils.ts b/libs/ui-components/src/components/Device/EditDeviceWizard/utils.ts index 179cb15a37..8e2d28533c 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/utils.ts +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/utils.ts @@ -11,7 +11,7 @@ import { } from '../../form/validations'; import { getDeviceLabelPatches, getStringListPatches, getUpdatePolicyPatches } from '../../../utils/patch'; import { type Device, type PatchRequest } from '@flightctl/types'; -import { type EditDeviceFormValues, type UpdatePolicyForm } from './../../../types/deviceSpec'; +import { type EditDeviceFormValues, UpdateMode, type UpdatePolicyForm } from './../../../types/deviceSpec'; import { ACMCrdConfig, ACMImportConfig, @@ -30,8 +30,7 @@ export const getValidationSchema = (t: TFunction) => labels: validLabelsSchema(t), configTemplates: validConfigTemplatesSchema(t), applications: validApplicationsSchema(t), - updatePolicy: - !values.useBasicUpdateConfig && values.updatePolicy.isAdvanced ? validUpdatePolicySchema(t) : Yup.object(), + updatePolicy: values.updateMode === UpdateMode.Customized ? validUpdatePolicySchema(t) : Yup.object(), }), ); @@ -78,7 +77,7 @@ export const getDevicePatches = (currentDevice: Device, updatedDevice: EditDevic // Updates const updatesPatches = getUpdatePolicyPatches('/spec/updatePolicy', currentDevice.spec?.updatePolicy, { ...updatedDevice.updatePolicy, - isAdvanced: !updatedDevice.useBasicUpdateConfig, + isCustomized: updatedDevice.updateMode === UpdateMode.Customized, } as Required); allPatches = allPatches.concat(updatesPatches); diff --git a/libs/ui-components/src/components/Fleet/CreateFleet/fleetSpecUtils.ts b/libs/ui-components/src/components/Fleet/CreateFleet/fleetSpecUtils.ts index f55e9c54fc..e3312869f6 100644 --- a/libs/ui-components/src/components/Fleet/CreateFleet/fleetSpecUtils.ts +++ b/libs/ui-components/src/components/Fleet/CreateFleet/fleetSpecUtils.ts @@ -40,7 +40,7 @@ export const getRolloutPolicyValues = (fleetSpec?: FleetSpec): RolloutPolicyForm // If the policy does not specify the timeout, we set the backend's default as the field is required in the UI const updateTimeout = fleetSpec?.rolloutPolicy?.defaultUpdateTimeout || `${DEFAULT_BACKEND_UPDATE_TIMEOUT_MINUTES}m`; return { - isAdvanced: batches.length > 0, + isCustomized: batches.length > 0, batches: batches.length ? batches : [getEmptyInitializedBatch()], updateTimeout: timeUtils.durationToMinutes(updateTimeout), }; @@ -50,7 +50,7 @@ export const getDisruptionBudgetValues = (fleetSpec?: FleetSpec) => { const budget = fleetSpec?.rolloutPolicy?.disruptionBudget || {}; const groupLabels = budget.groupBy || []; return { - isAdvanced: Boolean(groupLabels.length > 0 || budget.minAvailable || budget.maxUnavailable), + isCustomized: Boolean(groupLabels.length > 0 || budget.minAvailable || budget.maxUnavailable), groupBy: groupLabels, minAvailable: budget.minAvailable, maxUnavailable: budget.maxUnavailable, @@ -72,7 +72,7 @@ export const getUpdatePolicyValues = (updateSpec?: DeviceUpdatePolicySpec): Upda : updateSpec?.updateSchedule?.startGraceDuration; return { - isAdvanced: Boolean(updateSpec?.downloadSchedule?.at || updateSpec?.updateSchedule?.at), + isCustomized: Boolean(updateSpec?.downloadSchedule?.at || updateSpec?.updateSchedule?.at), downloadAndInstallDiffer: !isEqual, downloadStartsAt, downloadEndsAt: timeUtils.getEndTime(downloadStartsAt, downloadStartGraceDuration), diff --git a/libs/ui-components/src/components/Fleet/CreateFleet/steps/ReviewStep.tsx b/libs/ui-components/src/components/Fleet/CreateFleet/steps/ReviewStep.tsx index 6ce5ec6309..e0ea2ef40d 100644 --- a/libs/ui-components/src/components/Fleet/CreateFleet/steps/ReviewStep.tsx +++ b/libs/ui-components/src/components/Fleet/CreateFleet/steps/ReviewStep.tsx @@ -11,7 +11,7 @@ import { import { useFormikContext } from 'formik'; import { useTranslation } from '../../../../hooks/useTranslation'; -import { type FleetFormValues } from '../../../../types/deviceSpec'; +import { type FleetFormValues, UpdateMode } from '../../../../types/deviceSpec'; import LabelsView from '../../../common/LabelsView'; import { toAPILabel } from '../../../../utils/labels'; import RepositorySourceList from '../../../Repository/RepositoryDetails/RepositorySourceList'; @@ -88,9 +88,9 @@ const ReviewStep = ({ error }: { error?: unknown }) => { )} - {!values.useBasicUpdateConfig && ( + {values.updateMode === UpdateMode.Customized && ( <> - {values.rolloutPolicy.isAdvanced && ( + {values.rolloutPolicy.isCustomized && ( {t('Rollout policy')} @@ -98,7 +98,7 @@ const ReviewStep = ({ error }: { error?: unknown }) => { )} - {values.disruptionBudget.isAdvanced && ( + {values.disruptionBudget.isCustomized && ( {t('Disruption budget')} diff --git a/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.css b/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.css index 422e420725..c5be48196a 100644 --- a/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.css +++ b/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.css @@ -1,4 +1,5 @@ -.fctl-update-policy--advanced .pf-v6-c-check { - /* Prevents Patternfly from moving these checkboxes too high relative to the center of their label */ - --pf-v6-c-check__input--TranslateY: 0.7rem; +.fctl-update-policy--customize-options { + padding-inline-start: var(--pf-t--global--spacer--md); + border-inline-start: var(--pf-t--global--border--width--control--default) solid + var(--pf-t--global--border--color--default); } diff --git a/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx b/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx index 1fcef6faac..a371729fe2 100644 --- a/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx +++ b/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx @@ -1,16 +1,17 @@ import * as React from 'react'; -import { Alert, FormSection } from '@patternfly/react-core'; +import { FormGroup, List, ListItem, Stack, StackItem, Title } from '@patternfly/react-core'; import { type FormikErrors, useFormikContext } from 'formik'; import { useTranslation } from '../../../../hooks/useTranslation'; -import LabelWithHelperText from '../../../common/WithHelperText'; -import { type FleetFormValues } from '../../../../types/deviceSpec'; +import { type FleetFormValues, UpdateMode } from '../../../../types/deviceSpec'; import FlightCtlForm from '../../../form/FlightCtlForm'; +import CheckboxField from '../../../form/CheckboxField'; +import { DefaultHelperText } from '../../../form/FieldHelperText'; +import RadioField from '../../../form/RadioField'; import UpdateStepRolloutPolicy from './UpdateStepRolloutPolicy'; import UpdateStepDisruptionBudget from './UpdateStepDisruptionBudget'; import UpdateStepUpdatePolicy from './UpdateStepUpdatePolicy'; -import CheckboxField from '../../../form/CheckboxField'; import './UpdatePolicyStep.css'; @@ -19,64 +20,106 @@ export const updatePolicyStepId = 'update-policy'; export const isUpdatePolicyStepValid = (errors: FormikErrors) => !errors.rolloutPolicy && !errors.disruptionBudget && !errors.updatePolicy; -const UpdatePolicyStep = ({ isReadOnly }: { isReadOnly: boolean }) => { +export const CustomizedUpdatesSectionBody = ({ + updateMode, + children, +}: React.PropsWithChildren<{ updateMode: UpdateMode }>) => { + const { t } = useTranslation(); + if (updateMode === UpdateMode.Default) { + return null; + } + + return ( +
+ {children} +
+ ); +}; + +const CustomizedUpdatesSection = ({ isReadOnly }: { isReadOnly: boolean }) => { const { t } = useTranslation(); const { - values: { useBasicUpdateConfig, rolloutPolicy, disruptionBudget, updatePolicy }, + values: { rolloutPolicy, disruptionBudget, updatePolicy }, } = useFormikContext(); return ( - - - {!useBasicUpdateConfig ? ( - - {/* Rollout policies */} - - } - isDisabled={isReadOnly} - body={rolloutPolicy.isAdvanced && } - /> + + + } + /> + + + } + /> + + + } + /> + + + ); +}; - {/* Disruption budget */} - - } - isDisabled={isReadOnly} - body={disruptionBudget.isAdvanced && } - /> +const UpdatePolicyStep = ({ isReadOnly }: { isReadOnly: boolean }) => { + const { t } = useTranslation(); - {/* Update (and download) policies */} - - } - isDisabled={isReadOnly} - body={updatePolicy.isAdvanced && } - /> - - ) : ( - - {t('All the devices that are part of this fleet will receive updates as soon as they are available.')} - - )} + const { + values: { updateMode }, + } = useFormikContext(); + + return ( + + + + + {t('Updates')} + + + + + + + + + + } + /> + + + ); }; diff --git a/libs/ui-components/src/components/Fleet/CreateFleet/utils.ts b/libs/ui-components/src/components/Fleet/CreateFleet/utils.ts index f98730f8ff..245c65b747 100644 --- a/libs/ui-components/src/components/Fleet/CreateFleet/utils.ts +++ b/libs/ui-components/src/components/Fleet/CreateFleet/utils.ts @@ -36,11 +36,12 @@ import { toApiApplication, } from '../../Device/EditDeviceWizard/deviceSpecUtils'; import { getDisruptionBudgetValues, getRolloutPolicyValues, getUpdatePolicyValues } from './fleetSpecUtils'; -import { type FleetFormValues, type UpdatePolicyForm } from '../../../types/deviceSpec'; +import { type FleetFormValues, UpdateMode, type UpdatePolicyForm } from '../../../types/deviceSpec'; export const getValidationSchema = (t: TFunction) => { - return Yup.lazy((values: FleetFormValues) => - Yup.object({ + return Yup.lazy((values: FleetFormValues) => { + const hasCustomUpdates = values.updateMode === UpdateMode.Customized; + return Yup.object({ name: validKubernetesDnsSubdomain(t, { isRequired: true }), osSpec: validOsFormValue(t, { isFleet: true }), fleetLabels: validLabelsSchema(t), @@ -49,17 +50,12 @@ export const getValidationSchema = (t: TFunction) => { applications: validApplicationsSchema(t), systemdUnits: systemdUnitListValidationSchema(t), rolloutPolicy: - !values.useBasicUpdateConfig && values.rolloutPolicy?.isAdvanced - ? validFleetRolloutPolicySchema(t) - : Yup.object(), + hasCustomUpdates && values.rolloutPolicy?.isCustomized ? validFleetRolloutPolicySchema(t) : Yup.object(), disruptionBudget: - !values.useBasicUpdateConfig && values.disruptionBudget?.isAdvanced - ? validFleetDisruptionBudgetSchema(t) - : Yup.object(), - updatePolicy: - !values.useBasicUpdateConfig && values.updatePolicy?.isAdvanced ? validUpdatePolicySchema(t) : Yup.object(), - }), - ); + hasCustomUpdates && values.disruptionBudget?.isCustomized ? validFleetDisruptionBudgetSchema(t) : Yup.object(), + updatePolicy: hasCustomUpdates && values.updatePolicy?.isCustomized ? validUpdatePolicySchema(t) : Yup.object(), + }); + }); }; export const getFleetPatches = (currentFleet: Fleet, updatedFleet: FleetFormValues) => { @@ -142,7 +138,7 @@ export const getFleetPatches = (currentFleet: Fleet, updatedFleet: FleetFormValu currentFleet.spec.template.spec.updatePolicy, { ...updatedFleet.updatePolicy, - isAdvanced: !updatedFleet.useBasicUpdateConfig && updatedFleet.updatePolicy.isAdvanced, + isCustomized: updatedFleet.updateMode === UpdateMode.Customized && updatedFleet.updatePolicy.isCustomized, } as Required, ); allPatches = allPatches.concat(updatePolicyPatches); @@ -190,11 +186,11 @@ export const getFleetResource = (values: FleetFormValues): Fleet => { if (values.registerMicroShift) { fleet.spec.template.spec.config?.push(ACMCrdConfig, ACMImportConfig, MicroshiftRegistrationHook); } - if (!values.useBasicUpdateConfig) { - if (values.rolloutPolicy.isAdvanced || values.disruptionBudget.isAdvanced) { + if (values.updateMode === UpdateMode.Customized) { + if (values.rolloutPolicy.isCustomized || values.disruptionBudget.isCustomized) { fleet.spec.rolloutPolicy = getRolloutPolicyData(values); } - if (values.updatePolicy.isAdvanced) { + if (values.updatePolicy.isCustomized) { fleet.spec.template.spec.updatePolicy = updatePolicyFormToApi(values.updatePolicy as Required); } } @@ -207,6 +203,8 @@ export const getInitialValues = (fleet?: Fleet): FleetFormValues => { const rolloutPolicy = getRolloutPolicyValues(fleet.spec); const disruptionBudget = getDisruptionBudgetValues(fleet.spec); const updatePolicy = getUpdatePolicyValues(fleet.spec.template?.spec?.updatePolicy); + + const isCustomUpdateMode = rolloutPolicy.isCustomized || disruptionBudget.isCustomized || updatePolicy.isCustomized; return { name: fleet.metadata.name || '', labels: Object.keys(fleet.spec.selector?.matchLabels || {}).map((key) => ({ @@ -225,7 +223,7 @@ export const getInitialValues = (fleet?: Fleet): FleetFormValues => { rolloutPolicy, disruptionBudget, updatePolicy, - useBasicUpdateConfig: !rolloutPolicy.isAdvanced && !disruptionBudget.isAdvanced && !updatePolicy.isAdvanced, + updateMode: isCustomUpdateMode ? UpdateMode.Customized : UpdateMode.Default, }; } @@ -241,6 +239,6 @@ export const getInitialValues = (fleet?: Fleet): FleetFormValues => { rolloutPolicy: getRolloutPolicyValues(undefined), disruptionBudget: getDisruptionBudgetValues(undefined), updatePolicy: getUpdatePolicyValues(undefined), - useBasicUpdateConfig: true, + updateMode: UpdateMode.Default, }; }; diff --git a/libs/ui-components/src/components/form/CheckboxField.tsx b/libs/ui-components/src/components/form/CheckboxField.tsx index 9a9f6fe348..4315561678 100644 --- a/libs/ui-components/src/components/form/CheckboxField.tsx +++ b/libs/ui-components/src/components/form/CheckboxField.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useField } from 'formik'; import { Checkbox, type CheckboxProps, FormGroup } from '@patternfly/react-core'; -import ErrorHelperText, { DefaultHelperText } from './FieldHelperText'; +import ErrorHelperText from './FieldHelperText'; interface BaseCheckboxProps extends Omit { name: string; @@ -34,7 +34,7 @@ export const CheckboxFieldGroupValidation = ({ onChangeCustom, ...props }: BaseC ); }; -const CheckboxField = ({ onChangeCustom, helperText, children, ...props }: CheckboxFieldProps) => { +const CheckboxField = ({ onChangeCustom, children, ...props }: CheckboxFieldProps) => { const [{ value, ...rest }, meta, { setValue, setTouched }] = useField({ name: props.name, }); @@ -53,7 +53,6 @@ const CheckboxField = ({ onChangeCustom, helperText, children, ...props }: Check - {value && children} diff --git a/libs/ui-components/src/components/form/RadioField.tsx b/libs/ui-components/src/components/form/RadioField.tsx index e304a1dd71..920998481c 100644 --- a/libs/ui-components/src/components/form/RadioField.tsx +++ b/libs/ui-components/src/components/form/RadioField.tsx @@ -31,7 +31,7 @@ const RadioField = ({ if (noDefaultOnChange) { return; } - await setValue(checkedValue || true, true); + await setValue(checkedValue !== undefined ? checkedValue : true, true); await setTouched(true); } }; @@ -45,7 +45,7 @@ const RadioField = ({ {...props} id={fieldId} onChange={onChange} - isChecked={checkedValue ? field.value === checkedValue : !!field.value} + isChecked={checkedValue !== undefined ? field.value === checkedValue : !!field.value} /> diff --git a/libs/ui-components/src/components/form/validations.ts b/libs/ui-components/src/components/form/validations.ts index a14234ffe4..fac9220620 100644 --- a/libs/ui-components/src/components/form/validations.ts +++ b/libs/ui-components/src/components/form/validations.ts @@ -1122,7 +1122,7 @@ export const validApplicationsSchema = (t: TFunction) => { export const validFleetRolloutPolicySchema = (t: TFunction) => { return Yup.object() .shape({ - isAdvanced: Yup.boolean().required(), + isCustomized: Yup.boolean().required(), updateTimeout: Yup.number() .required('Update timeout is required') .test('not-decimal', t('Cannot be decimal'), isInteger), @@ -1183,8 +1183,8 @@ export const validFleetRolloutPolicySchema = (t: TFunction) => { }; const requiredDownloadTimes = (t: TFunction, isStartTime: boolean) => - Yup.string().when(['isAdvanced', 'downloadAndInstallDiffer'], ([isAdvanced, downloadAndInstallDiffer]) => { - if (!isAdvanced) { + Yup.string().when(['isCustomized', 'downloadAndInstallDiffer'], ([isCustomized, downloadAndInstallDiffer]) => { + if (!isCustomized) { return Yup.string(); } if (downloadAndInstallDiffer) { @@ -1202,8 +1202,8 @@ const requiredDownloadTimes = (t: TFunction, isStartTime: boolean) => }); const requiredInstallTimes = (t: TFunction, isStartTime: boolean) => - Yup.string().when(['isAdvanced', 'downloadAndInstallDiffer'], ([isAdvanced, downloadAndInstallDiffer]) => { - if (isAdvanced && downloadAndInstallDiffer) { + Yup.string().when(['isCustomized', 'downloadAndInstallDiffer'], ([isCustomized, downloadAndInstallDiffer]) => { + if (isCustomized && downloadAndInstallDiffer) { return Yup.string() .required(isStartTime ? t('Installing start time is required') : t('Installing end time is required')) .matches(TIME_VALUE_REGEXP, t('Time must be in hh:mm with 24-hour format', { nsSeparator: '|' })); @@ -1212,8 +1212,8 @@ const requiredInstallTimes = (t: TFunction, isStartTime: boolean) => }); const requiredStartGraceDuration = (t: TFunction, isInstallField: boolean = false) => - Yup.string().when(['isAdvanced', 'downloadAndInstallDiffer'], ([isAdvanced, downloadAndInstallDiffer]) => { - if (!isAdvanced || (isInstallField && !downloadAndInstallDiffer)) { + Yup.string().when(['isCustomized', 'downloadAndInstallDiffer'], ([isCustomized, downloadAndInstallDiffer]) => { + if (!isCustomized || (isInstallField && !downloadAndInstallDiffer)) { return Yup.string(); } return Yup.string() @@ -1244,9 +1244,9 @@ const updateWeekDaysSchema = (t: TFunction) => export const validUpdatePolicySchema = (t: TFunction) => { return Yup.object().shape({ - isAdvanced: Yup.boolean().required(), + isCustomized: Yup.boolean().required(), downloadAndInstallDiffer: Yup.boolean().required(), - // Fields are flattened so "isAdvanced" can be used for validating them + // Fields are flattened so "isCustomized" can be used for validating them downloadStartsAt: requiredDownloadTimes(t, true), downloadEndsAt: requiredDownloadTimes(t, false), downloadStartGraceDuration: requiredStartGraceDuration(t, false), @@ -1265,7 +1265,7 @@ export const validUpdatePolicySchema = (t: TFunction) => { export const validFleetDisruptionBudgetSchema = (t: TFunction) => { return Yup.object() .shape({ - isAdvanced: Yup.boolean().required(), + isCustomized: Yup.boolean().required(), minAvailable: Yup.number().test('not-decimal', t('Number of devices cannot be decimal'), isInteger), maxUnavailable: Yup.number().test('not-decimal', t('Number of devices cannot be decimal'), isInteger), groupBy: validGroupLabelKeysSchema(t), @@ -1274,7 +1274,7 @@ export const validFleetDisruptionBudgetSchema = (t: TFunction) => { 'has-min-or-max', t('At least one of minimum available or maximum unavailable devices is required.'), (value: DisruptionBudgetForm) => - !(value.isAdvanced && value.minAvailable === undefined && value.maxUnavailable === undefined), + !(value.isCustomized && value.minAvailable === undefined && value.maxUnavailable === undefined), ); }; diff --git a/libs/ui-components/src/types/deviceSpec.ts b/libs/ui-components/src/types/deviceSpec.ts index cca40adbac..9bae115f57 100644 --- a/libs/ui-components/src/types/deviceSpec.ts +++ b/libs/ui-components/src/types/deviceSpec.ts @@ -257,6 +257,11 @@ export type SystemdUnitFormValue = { exists: boolean; }; +export enum UpdateMode { + Default = 'default', + Customized = 'customized', +} + export type DeviceSpecConfigFormValues = { osSpec?: ImageOrCatalogItemRefSpec; configTemplates: SpecConfigTemplate[]; @@ -264,7 +269,7 @@ export type DeviceSpecConfigFormValues = { systemdUnits: SystemdUnitFormValue[]; updatePolicy: UpdatePolicyForm; registerMicroShift: boolean; - useBasicUpdateConfig: boolean; + updateMode: UpdateMode; }; export type EditDeviceFormValues = DeviceSpecConfigFormValues & { @@ -295,17 +300,17 @@ export type BatchForm = { }; export type RolloutPolicyForm = { - isAdvanced: boolean; + isCustomized: boolean; updateTimeout: number; batches: BatchForm[]; }; export type DisruptionBudgetForm = DisruptionBudget & { - isAdvanced: boolean; + isCustomized: boolean; }; export type UpdatePolicyForm = { - isAdvanced: boolean; + isCustomized: boolean; downloadAndInstallDiffer: boolean; downloadStartsAt?: string; downloadEndsAt?: string; diff --git a/libs/ui-components/src/utils/patch.ts b/libs/ui-components/src/utils/patch.ts index d0bfcddbd2..fe8aa82a42 100644 --- a/libs/ui-components/src/utils/patch.ts +++ b/libs/ui-components/src/utils/patch.ts @@ -17,6 +17,7 @@ import { type DisruptionBudgetForm, type FleetFormValues, type RolloutPolicyForm, + UpdateMode, type UpdatePolicyForm, } from '../types/deviceSpec'; import { getUpdateCronExpression, localDeviceTimezone } from './time'; @@ -181,9 +182,9 @@ export const getUpdatePolicyPatches = ( currentPolicy: DeviceUpdatePolicySpec | undefined, form: Required, ): PatchRequest => { - // Switching from basic mode to advanced or viceversa + // Switching from default mode to customized mode or viceversa if (!currentPolicy) { - return form.isAdvanced + return form.isCustomized ? ([ { op: 'add', @@ -192,7 +193,7 @@ export const getUpdatePolicyPatches = ( }, ] as PatchRequest) : []; - } else if (!form.isAdvanced) { + } else if (!form.isCustomized) { return [ { op: 'remove', @@ -201,7 +202,7 @@ export const getUpdatePolicyPatches = ( ] as PatchRequest; } - // Making changes to existing advaced settings + // Making changes to existing customized settings const updatePatches: PatchRequest = []; const { downloadSchedule: newDownloadSched, updateSchedule: newInstallSched } = updatePolicyFormToApi(form); if (!schedulesAreEqual(currentPolicy.downloadSchedule, newDownloadSched)) { @@ -246,11 +247,11 @@ export const getUpdatePolicyPatches = ( export const getRolloutPolicyData = ({ rolloutPolicy, disruptionBudget }: FleetFormValues) => { const newRolloutPolicy: RolloutPolicy = {}; - if (rolloutPolicy.isAdvanced) { + if (rolloutPolicy.isCustomized) { newRolloutPolicy.defaultUpdateTimeout = toApiDuration(rolloutPolicy.updateTimeout); newRolloutPolicy.deviceSelection = toApiDeviceSelection(rolloutPolicy); } - if (disruptionBudget.isAdvanced) { + if (disruptionBudget.isCustomized) { newRolloutPolicy.disruptionBudget = toApiDisruptionBudget(disruptionBudget); } return newRolloutPolicy; @@ -263,14 +264,15 @@ export const getRolloutPolicyPatches = ( const currentBatches = currentPolicy?.deviceSelection?.sequence || []; const currentDisruption = currentPolicy?.disruptionBudget; - const hadAdvancedSettings = currentBatches.length > 0 || !!currentDisruption; - const wantsAdvancedSettings = - !fleetValues.useBasicUpdateConfig && - (fleetValues.rolloutPolicy.isAdvanced || fleetValues.disruptionBudget.isAdvanced); + const hadCustomSettings = + currentPolicy?.defaultUpdateTimeout !== undefined || currentBatches.length > 0 || !!currentDisruption; + const wantsCustomSettings = + fleetValues.updateMode === UpdateMode.Customized && + (fleetValues.rolloutPolicy.isCustomized || fleetValues.disruptionBudget.isCustomized); const updatedPolicy = fleetValues.rolloutPolicy; - if (hadAdvancedSettings !== wantsAdvancedSettings) { - return wantsAdvancedSettings + if (hadCustomSettings !== wantsCustomSettings) { + return wantsCustomSettings ? [ { op: 'add', @@ -287,7 +289,7 @@ export const getRolloutPolicyPatches = ( } const patches: PatchRequest = []; - if (fleetValues.rolloutPolicy.isAdvanced) { + if (fleetValues.rolloutPolicy.isCustomized) { // The timeout will be always expressed in minutes if ((currentPolicy?.defaultUpdateTimeout || '') !== (updatedPolicy.updateTimeout || '')) { appendJSONPatch({ @@ -339,7 +341,7 @@ export const getRolloutPolicyPatches = ( }); } - if (fleetValues.disruptionBudget.isAdvanced) { + if (fleetValues.disruptionBudget.isCustomized) { const hasMinChanged = (currentDisruption?.minAvailable || '') !== (fleetValues.disruptionBudget.minAvailable || ''); const hasMaxChanged = (currentDisruption?.maxUnavailable || '') !== (fleetValues.disruptionBudget.maxUnavailable || ''); @@ -354,7 +356,7 @@ export const getRolloutPolicyPatches = ( path: '/spec/rolloutPolicy/disruptionBudget', patches, originalValue: currentDisruption, - newValue: fleetValues.disruptionBudget.isAdvanced + newValue: fleetValues.disruptionBudget.isCustomized ? toApiDisruptionBudget(fleetValues.disruptionBudget) : undefined, }); From e5bbee88bd1ddacdb0a1b35e50f1026a209b81fe Mon Sep 17 00:00:00 2001 From: Celia Amador Date: Thu, 3 Sep 2026 12:35:07 +0200 Subject: [PATCH 2/2] Distinguish label for Fleets and Devices Made-with: Cursor --- libs/i18n/locales/en/translation.json | 1 + .../EditDeviceWizard/steps/DeviceUpdateStep.tsx | 2 +- .../Fleet/CreateFleet/steps/UpdatePolicyStep.tsx | 11 ++++++++--- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 384bba64c3..f76f83d63e 100644 --- a/libs/i18n/locales/en/translation.json +++ b/libs/i18n/locales/en/translation.json @@ -1098,6 +1098,7 @@ "Device selector": "Device selector", "Rollout policy": "Rollout policy", "Disruption budget": "Disruption budget", + "Customize updates": "Customize updates", "Maintenance windows": "Maintenance windows", "Set rollout order": "Set rollout order", "Rollout order controls in what sequence fleet devices are updated.": "Rollout order controls in what sequence fleet devices are updated.", diff --git a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx index 2022f98b87..0ebf2d5c6c 100644 --- a/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx +++ b/libs/ui-components/src/components/Device/EditDeviceWizard/steps/DeviceUpdateStep.tsx @@ -49,7 +49,7 @@ const UpdatePolicyStep = ({ isReadOnly }: { isReadOnly?: boolean }) => { checkedValue={UpdateMode.Customized} isDisabled={isReadOnly} body={ - + } diff --git a/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx b/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx index a371729fe2..8dc92feb9f 100644 --- a/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx +++ b/libs/ui-components/src/components/Fleet/CreateFleet/steps/UpdatePolicyStep.tsx @@ -22,15 +22,20 @@ export const isUpdatePolicyStepValid = (errors: FormikErrors) = export const CustomizedUpdatesSectionBody = ({ updateMode, + isFleet, children, -}: React.PropsWithChildren<{ updateMode: UpdateMode }>) => { +}: React.PropsWithChildren<{ updateMode: UpdateMode; isFleet: boolean }>) => { const { t } = useTranslation(); if (updateMode === UpdateMode.Default) { return null; } return ( -
+
{children}
); @@ -112,7 +117,7 @@ const UpdatePolicyStep = ({ isReadOnly }: { isReadOnly: boolean }) => { checkedValue={UpdateMode.Customized} isDisabled={isReadOnly} body={ - + }