fleet/frontend/pages/hosts/ManageHostsPage/components/RunScriptBatchModal/helpers.ts
Scott Gress e985d20b1d
UI for scheduling batch scripts (#31885)
# Details

This PR merges the feature branch for the scheduled scripts UI into
main. This includes the following previously-approved PRs:

* https://github.com/fleetdm/fleet/pull/31750
* https://github.com/fleetdm/fleet/pull/31604
* https://github.com/fleetdm/fleet/pull/31797


# 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

- [X] Added/updated automated tests
- [X] Where appropriate, [automated tests simulate multiple hosts and
test for host
isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing)
(updates to one hosts's records do not affect another)

- [X] QA'd all new/changed functionality manually

---------

Co-authored-by: jacobshandling <61553566+jacobshandling@users.noreply.github.com>
Co-authored-by: Jacob Shandling <jacob@fleetdm.com>
2025-08-14 10:10:45 -05:00

154 lines
4.2 KiB
TypeScript

import { parse, isValid } from "date-fns";
import { IRunScriptBatchModalScheduleFormData } from "./RunScriptBatchModal";
// TODO: create a validator abstraction for this and the other form validation files
export interface IRunScriptBatchModalFormValidation {
isValid: boolean;
date?: { isValid: boolean; message?: string };
time?: { isValid: boolean; message?: string };
}
type IMessageFunc = (formData: IRunScriptBatchModalScheduleFormData) => string;
type IValidationMessage = string | IMessageFunc;
type IFormValidationKey = keyof Omit<
IRunScriptBatchModalScheduleFormData,
"isValid"
>;
interface IValidation {
name: string;
isValid: (
formData: IRunScriptBatchModalScheduleFormData,
validations?: IRunScriptBatchModalFormValidation
) => boolean;
message?: IValidationMessage;
}
type IFormValidations = Record<
IFormValidationKey,
{ validations: IValidation[] }
>;
const FORM_VALIDATIONS: IFormValidations = {
date: {
validations: [
{
name: "required",
isValid: (formData: IRunScriptBatchModalScheduleFormData) => {
return formData.date.length > 0;
},
},
{
name: "validDate",
isValid: (formData: IRunScriptBatchModalScheduleFormData) => {
if (!formData.date.match(/^\d{4}-\d{2}-\d{2}$/)) {
return false;
}
const parsedDate = parse(formData.date, "yyyy-MM-dd", new Date());
return isValid(parsedDate);
},
message: "Date (UTC) must have valid format",
},
{
name: "notInPast",
isValid: (formData: IRunScriptBatchModalScheduleFormData) => {
const now = new Date();
const parsedDate = parse(
`${formData.date} 23:59:59.999`,
"yyyy-MM-dd HH:mm:ss.SSS",
now
);
return parsedDate >= now;
},
message: `Date (UTC) cannot be in the past`,
},
],
},
time: {
validations: [
{
name: "required",
isValid: (formData: IRunScriptBatchModalScheduleFormData) => {
return formData.time.length > 0;
},
},
{
name: "validTime",
isValid: (formData: IRunScriptBatchModalScheduleFormData) => {
if (!formData.time.match(/^\d{2}:\d{2}$/)) {
return false;
}
const parsedDate = parse(
`1982-10-13 ${formData.time}`,
"yyyy-MM-dd HH:mm",
new Date()
);
return isValid(parsedDate);
},
message: "Time (UTC) must have valid format",
},
{
name: "notInPast",
isValid: (
formData: IRunScriptBatchModalScheduleFormData,
validations?: IRunScriptBatchModalFormValidation
) => {
if (validations?.date?.isValid === false) {
return true; // If date is invalid, skip time validation
}
const parsedDate = parse(
`${formData.date} ${formData.time}`,
"yyyy-MM-dd HH:mm",
new Date()
);
return parsedDate >= new Date();
},
message: `Time (UTC) cannot be in the past`,
},
],
},
};
const getErrorMessage = (
formData: IRunScriptBatchModalScheduleFormData,
message?: IValidationMessage
) => {
if (message === undefined || typeof message === "string") {
return message;
}
return message(formData);
};
export const validateFormData = (
formData: IRunScriptBatchModalScheduleFormData,
runMode: "run_now" | "schedule" = "run_now"
) => {
const formValidation: IRunScriptBatchModalFormValidation = {
isValid: true,
};
if (runMode === "run_now") {
return formValidation; // No validation needed for run now
}
Object.keys(FORM_VALIDATIONS).forEach((key) => {
const objKey = key as keyof typeof FORM_VALIDATIONS;
const failedValidation = FORM_VALIDATIONS[objKey].validations.find(
(validation) => !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;
};