fleet/frontend/pages/policies/PolicyPage/components/SaveNewPolicyModal/SaveNewPolicyModal.tsx
Ian Littman 2891904f31
🤖 Switch InputField + InputFieldWithIcon JSX components to TS, add more test coverage, fix Storybook build (#43307)
Zed + Opus 4.6; prompt: Convert the InputField JSX component to
TypeScript and remove the ts-ignore directives that we no longer need
after doing so.

- [x] Changes file added
- [x] Automated tests updated
2026-04-09 08:41:48 -05:00

359 lines
11 KiB
TypeScript

import React, { useState, useContext, useEffect, useCallback } from "react";
import { size } from "lodash";
import classNames from "classnames";
import CUSTOM_TARGET_OPTIONS from "pages/policies/helpers";
import { AppContext } from "context/app";
import { PolicyContext } from "context/policy";
import { IPlatformSelector } from "hooks/usePlatformSelector";
import { ILabelSummary } from "interfaces/label";
import { IPolicyFormData } from "interfaces/policy";
import { CommaSeparatedPlatformString } from "interfaces/platform";
import useDeepEffect from "hooks/useDeepEffect";
import InputField from "components/forms/fields/InputField";
import Checkbox from "components/forms/fields/Checkbox";
import TooltipWrapper from "components/TooltipWrapper";
import Button from "components/buttons/Button";
import Modal from "components/Modal";
import TargetLabelSelector from "components/TargetLabelSelector";
import Icon from "components/Icon";
export interface ISaveNewPolicyModalProps {
baseClass: string;
queryValue: string;
onCreatePolicy: (formData: IPolicyFormData) => void;
setIsSaveNewPolicyModalOpen: (isOpen: boolean) => void;
backendValidators: { [key: string]: string };
platformSelector: IPlatformSelector;
isUpdatingPolicy: boolean;
aiFeaturesDisabled?: boolean;
isFetchingAutofillDescription: boolean;
isFetchingAutofillResolution: boolean;
onClickAutofillDescription: () => Promise<void>;
onClickAutofillResolution: () => Promise<void>;
labels: ILabelSummary[];
}
const validatePolicyName = (name: string) => {
const errors: { [key: string]: string } = {};
if (!name) {
errors.name = "Policy name must be present";
}
const valid = !size(errors);
return { valid, errors };
};
const SaveNewPolicyModal = ({
baseClass,
queryValue,
onCreatePolicy,
setIsSaveNewPolicyModalOpen,
backendValidators,
platformSelector,
isUpdatingPolicy,
aiFeaturesDisabled,
isFetchingAutofillDescription,
isFetchingAutofillResolution,
onClickAutofillDescription,
onClickAutofillResolution,
labels,
}: ISaveNewPolicyModalProps): JSX.Element => {
const { isPremiumTier } = useContext(AppContext);
const {
lastEditedQueryName,
lastEditedQueryDescription,
lastEditedQueryResolution,
lastEditedQueryCritical,
setLastEditedQueryName,
setLastEditedQueryPlatform,
// TODO: Keep last edited query platform from resetting when cancelling out of modal and clicking save again
setLastEditedQueryDescription,
setLastEditedQueryResolution,
setLastEditedQueryCritical,
} = useContext(PolicyContext);
const [errors, setErrors] = useState<{ [key: string]: string }>(
backendValidators
);
const [selectedTargetType, setSelectedTargetType] = useState("All hosts");
const [selectedCustomTarget, setSelectedCustomTarget] = useState(
"labelsIncludeAny"
);
const [selectedLabels, setSelectedLabels] = useState({});
const onSelectLabel = ({
name: labelName,
value,
}: {
name: string;
value: boolean;
}) => {
setSelectedLabels({
...selectedLabels,
[labelName]: value,
});
};
const disableForm =
isFetchingAutofillDescription || isFetchingAutofillResolution;
const disableSave =
!platformSelector.isAnyPlatformSelected ||
disableForm ||
(selectedTargetType === "Custom" &&
!Object.entries(selectedLabels).some(([, value]) => {
return value;
}));
useDeepEffect(() => {
if (lastEditedQueryName) {
setErrors({});
}
}, [lastEditedQueryName]);
useEffect(() => {
setErrors(backendValidators);
}, [backendValidators]);
const handleSavePolicy = (evt: React.MouseEvent<HTMLFormElement>) => {
evt.preventDefault();
const newPlatformString = platformSelector
.getSelectedPlatforms()
.join(",") as CommaSeparatedPlatformString;
setLastEditedQueryPlatform(newPlatformString);
const { valid: validName, errors: newErrors } = validatePolicyName(
lastEditedQueryName
);
setErrors({
...errors,
...newErrors,
});
if (!disableSave && validName) {
onCreatePolicy({
description: lastEditedQueryDescription,
name: lastEditedQueryName,
query: queryValue,
resolution: lastEditedQueryResolution,
platform: newPlatformString,
critical: lastEditedQueryCritical,
labels_include_any:
selectedTargetType === "Custom" &&
selectedCustomTarget === "labelsIncludeAny"
? Object.entries(selectedLabels)
.filter(([, selected]) => selected)
.map(([labelName]) => labelName)
: [],
labels_exclude_any:
selectedTargetType === "Custom" &&
selectedCustomTarget === "labelsExcludeAny"
? Object.entries(selectedLabels)
.filter(([, selected]) => selected)
.map(([labelName]) => labelName)
: [],
});
}
};
const renderAutofillButton = useCallback(
(labelName: "Description" | "Resolution") => {
const isFetchingButton =
(labelName === "Description" && isFetchingAutofillDescription) ||
(labelName === "Resolution" && isFetchingAutofillResolution);
return (
<TooltipWrapper
tipContent={
aiFeaturesDisabled ? (
"AI features are disabled in organization settings"
) : (
<>
Policy queries (SQL) will be sent to a <br />
large language model (LLM). Fleet <br />
doesn&apos;t use this data to train models.
</>
)
}
position="top"
disableTooltip={disableForm}
underline={false}
>
<div className="autofill-tooltip-wrapper">
<Button
variant="inverse"
disabled={aiFeaturesDisabled || disableForm}
onClick={
labelName === "Description"
? onClickAutofillDescription
: onClickAutofillResolution
}
size="small"
>
{isFetchingButton ? (
"Thinking..."
) : (
<>
<Icon name="sparkles" /> Autofill
</>
)}
</Button>
</div>
</TooltipWrapper>
);
},
[isFetchingAutofillDescription, isFetchingAutofillResolution, disableForm]
);
const renderAutofillLabel = useCallback(
(labelName: "Description" | "Resolution") => {
const labelClassName = classNames(`${baseClass}__autofill-label`, {
[`${baseClass}__label--${labelName}`]: !!labelName,
});
return (
<div className={labelClassName}>
{labelName}
{renderAutofillButton(labelName)}
</div>
);
},
[renderAutofillButton]
);
return (
<Modal
title="Save policy"
onExit={() => setIsSaveNewPolicyModalOpen(false)}
>
<>
<form
onSubmit={handleSavePolicy}
className={`${baseClass}__save-modal-form`}
autoComplete="off"
>
<InputField
name="name"
onChange={(value: string) => setLastEditedQueryName(value)}
value={lastEditedQueryName}
error={errors.name}
inputClassName={`${baseClass}__policy-save-modal-name`}
label="Name"
autofocus
ignore1password
disabled={disableForm}
/>
<InputField
name="description"
onChange={(value: string) => setLastEditedQueryDescription(value)}
value={lastEditedQueryDescription}
inputClassName={`${baseClass}__policy-save-modal-description`}
label={renderAutofillLabel("Description")}
helpText="How does this policy's failure put the organization at risk?"
type="textarea"
disabled={disableForm}
/>
<InputField
name="resolution"
onChange={(value: string) => setLastEditedQueryResolution(value)}
value={lastEditedQueryResolution}
inputClassName={`${baseClass}__policy-save-modal-resolution`}
label={renderAutofillLabel("Resolution")}
type="textarea"
helpText="If this policy fails, what should the end user expect?"
disabled={disableForm}
/>
{platformSelector.render()}
{isPremiumTier && (
<TargetLabelSelector
selectedTargetType={selectedTargetType}
selectedCustomTarget={selectedCustomTarget}
customTargetOptions={CUSTOM_TARGET_OPTIONS}
onSelectCustomTarget={setSelectedCustomTarget}
selectedLabels={selectedLabels}
className={`${baseClass}__target`}
onSelectTargetType={setSelectedTargetType}
onSelectLabel={onSelectLabel}
labels={labels || []}
customHelpText={
<span className="form-field__help-text">
Policy will target hosts on selected platforms that{" "}
<b>have any</b> of these labels:
</span>
}
suppressTitle
disableOptions={disableForm}
/>
)}
{isPremiumTier && (
<div className="critical-checkbox-wrapper">
<Checkbox
name="critical-policy"
onChange={(value: boolean) => setLastEditedQueryCritical(value)}
value={lastEditedQueryCritical}
disabled={disableForm}
>
<TooltipWrapper
tipContent={
<p>
If automations are turned on, this information is
included. If Okta conditional access is configured, end
users can never bypass critical policies.
</p>
}
>
Critical
</TooltipWrapper>
</Checkbox>
</div>
)}
<div className="modal-cta-wrap">
<TooltipWrapper
tipContent={
<>
Select the platforms this
<br />
policy will be checked on
<br />
to save the policy.
</>
}
tooltipClass={`${baseClass}__button--modal-save-tooltip`}
position="top"
disableTooltip={!disableSave}
underline={false}
showArrow
tipOffset={8}
>
<span className={`${baseClass}__button-wrap--modal-save`}>
<Button
type="submit"
onClick={handleSavePolicy}
disabled={disableSave}
className="save-policy-loading"
isLoading={isUpdatingPolicy}
>
Save
</Button>
</span>
</TooltipWrapper>
<Button
className={`${baseClass}__button--modal-cancel`}
onClick={() => setIsSaveNewPolicyModalOpen(false)}
variant="inverse"
>
Cancel
</Button>
</div>
</form>
</>
</Modal>
);
};
export default SaveNewPolicyModal;