diff --git a/libs/i18n/locales/en/translation.json b/libs/i18n/locales/en/translation.json index 05d6286b6..f76f83d63 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,16 @@ "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.", + "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.", "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 5bbe8fb99..7f71a31b2 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 359572feb..0ebf2d5c6 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 dafaaf61e..2e62c9b58 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 179cb15a3..8e2d28533 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 f55e9c54f..e3312869f 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 6ce5ec630..e0ea2ef40 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 422e42072..c5be48196 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 1fcef6faa..8dc92feb9 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,111 @@ 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, + isFleet, + children, +}: React.PropsWithChildren<{ updateMode: UpdateMode; isFleet: boolean }>) => { + 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 f98730f8f..245c65b74 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 9a9f6fe34..431556167 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 e304a1dd7..920998481 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 a14234ffe..fac922062 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 cca40adba..9bae115f5 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 d0bfcddbd..fe8aa82a4 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, });