fleet/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/helpers.tsx
Scott Gress 04685db892
Auto software update frontend (#37677)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #35459

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [ ] Added/updated automated tests
working on these
- [X] QA'd all new/changed functionality manually

## Screenshots

| Option does not appear for FMA apps |
| --- |
| <img width="723" height="419" alt="image"
src="https://github.com/user-attachments/assets/f9f1328e-e38c-452c-b06e-337a69c13e71"
/> |

| Option does not appear for custom packages |
| --- |
| <img width="731" height="416" alt="image"
src="https://github.com/user-attachments/assets/3de78f15-d7ce-45c7-875f-a250fc00a160"
/> |

| Option does not appear for macOS VPP apps |
| --- |
| <img width="725" height="454" alt="image"
src="https://github.com/user-attachments/assets/07dcb074-f57d-4cc4-a746-20b80c821fb6"
/> |

| Option appears iOS VPP apps |
| --- |
| <img width="727" height="420" alt="image"
src="https://github.com/user-attachments/assets/ec4ce503-0300-437c-b3f2-248928fcfe7b"
/> |

| Option appears iPadOS VPP apps |
| --- |
| <img width="727" height="422" alt="image"
src="https://github.com/user-attachments/assets/0030c6cc-3d93-480c-af93-740fca4d5b57"
/> |

| Form with auto-updates disabled |
| --- |
| <img width="668" height="517" alt="image"
src="https://github.com/user-attachments/assets/d59a7ba4-dc83-4a80-ba94-0befc7635f05"
/> |

| Start / end time validation |
| --- |
| <img width="668" height="679" alt="image"
src="https://github.com/user-attachments/assets/939fd09a-76f6-42de-9c71-fe4982f3f84b"
/> |

| Maintenance window length validation |
| --- |
| <img width="664" height="681" alt="image"
src="https://github.com/user-attachments/assets/a2eab676-5166-42a9-9043-2565014e33cb"
/> |

| Badge and banner appears after saving |
| --- |
| <img width="766" height="529" alt="image"
src="https://github.com/user-attachments/assets/48d89e1d-4430-4dd7-b8e6-d5b04ebad47f"
/> |

---------

Co-authored-by: Gabriel Hernandez <ghernandez345@gmail.com>
Co-authored-by: Nico <32375741+nulmete@users.noreply.github.com>
2026-01-05 10:43:26 -06:00

190 lines
5.4 KiB
TypeScript

import { ISoftwareAutoUpdateConfigFormData } from "./EditAutoUpdateConfigModal";
export interface ISoftwareAutoUpdateConfigInputValidation {
isValid: boolean;
message?: string;
}
export interface ISoftwareAutoUpdateConfigFormValidation {
isValid: boolean;
autoUpdateStartTime?: ISoftwareAutoUpdateConfigInputValidation;
autoUpdateEndTime?: ISoftwareAutoUpdateConfigInputValidation;
targets?: ISoftwareAutoUpdateConfigInputValidation;
windowLength?: ISoftwareAutoUpdateConfigInputValidation;
}
type IMessageFunc = (formData: ISoftwareAutoUpdateConfigFormData) => string;
type IValidationMessage = string | IMessageFunc;
type IFormValidationKey = keyof Omit<
ISoftwareAutoUpdateConfigFormValidation,
"isValid"
>;
interface IValidation {
name: string;
isValid: (
formData: ISoftwareAutoUpdateConfigFormData,
validations?: ISoftwareAutoUpdateConfigFormValidation
) => boolean;
message?: IValidationMessage;
}
type IFormValidations = Record<
IFormValidationKey,
{ validations: IValidation[] }
>;
const validateTimeFormat = (time: string): boolean => {
if (!time.match(/^[0-9]{2}:[0-9]{2}$/)) {
return false;
}
const [hours, minutes] = time.split(":").map(Number);
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
return false;
}
return true;
};
const validateWindowLength = (
formData: ISoftwareAutoUpdateConfigFormData,
validations?: ISoftwareAutoUpdateConfigFormValidation
) => {
if (
formData.autoUpdateStartTime.length === 0 ||
formData.autoUpdateEndTime.length === 0 ||
!validations?.autoUpdateStartTime ||
!validations.autoUpdateStartTime.isValid ||
!validations.autoUpdateEndTime ||
!validations.autoUpdateEndTime.isValid
) {
return true; // Skip this validation if startTime is invalid
}
const [startHours, startMinutes] = formData.autoUpdateStartTime
.split(":")
.map(Number);
const [endHours, endMinutes] = formData.autoUpdateEndTime
.split(":")
.map(Number);
const startTotalMinutes = startHours * 60 + startMinutes;
const endTotalMinutes = endHours * 60 + endMinutes;
return (
endTotalMinutes < startTotalMinutes ||
endTotalMinutes - startTotalMinutes >= 60
);
};
const FORM_VALIDATIONS: IFormValidations = {
autoUpdateStartTime: {
validations: [
{
name: "required",
isValid: (formData: ISoftwareAutoUpdateConfigFormData) => {
return formData.autoUpdateStartTime.length > 0;
},
message: `Earliest start time is required`,
},
{
name: "valid",
isValid: (formData: ISoftwareAutoUpdateConfigFormData) => {
if (formData.autoUpdateStartTime.length === 0) {
return true; // Skip this validation if startTime is empty
}
return validateTimeFormat(formData.autoUpdateStartTime);
},
message: `Use HH:MM format (24-hour clock)`,
},
],
},
autoUpdateEndTime: {
validations: [
{
name: "required",
isValid: (formData: ISoftwareAutoUpdateConfigFormData) => {
return formData.autoUpdateEndTime.length > 0;
},
message: `Latest start time is required`,
},
{
name: "valid",
isValid: (formData: ISoftwareAutoUpdateConfigFormData) => {
if (formData.autoUpdateEndTime.length === 0) {
return true; // Skip this validation if endTime is empty
}
return validateTimeFormat(formData.autoUpdateEndTime);
},
message: `Use HH:MM format (24-hour clock)`,
},
],
},
targets: {
validations: [
{
name: "custom_labels_selected",
isValid: (formData: ISoftwareAutoUpdateConfigFormData) => {
return (
formData.targetType !== "Custom" ||
Object.values(formData.labelTargets).filter((v) => v).length > 0
);
},
message: `At least one label target must be selected`,
},
],
},
windowLength: {
validations: [
{
name: "minimum_length",
isValid: validateWindowLength,
message: `Update window must be at least 60 minutes long`,
},
],
},
};
const getErrorMessage = (
formData: ISoftwareAutoUpdateConfigFormData,
message?: IValidationMessage
) => {
if (message === undefined || typeof message === "string") {
return message;
}
return message(formData);
};
export const validateFormData = (
formData: ISoftwareAutoUpdateConfigFormData,
isSaving = false
) => {
const formValidation: ISoftwareAutoUpdateConfigFormValidation = {
isValid: true,
};
// If auto updates are not enabled, skip further validations.
Object.keys(FORM_VALIDATIONS).forEach((key) => {
if (!formData.autoUpdateEnabled && key !== "targets") {
return;
}
const objKey = key as keyof typeof FORM_VALIDATIONS;
const failedValidation = FORM_VALIDATIONS[objKey].validations.find(
(validation) => {
if (!isSaving && validation.name === "required") {
return false; // Skip this validation if not saving
}
return !validation.isValid(formData, formValidation);
}
);
if (!failedValidation) {
formValidation[objKey] = {
isValid: true,
};
} else {
formValidation.isValid = false;
formValidation[objKey] = {
isValid: false,
message: getErrorMessage(formData, failedValidation.message),
};
}
});
return formValidation;
};