Fleet UI: AI autofill policy description and resolution (#18646)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import React from "react";
|
||||
import classNames from "classnames";
|
||||
import Checkbox from "components/forms/fields/Checkbox";
|
||||
|
||||
interface IPlatformSelectorProps {
|
||||
@@ -11,6 +12,7 @@ interface IPlatformSelectorProps {
|
||||
setCheckWindows: (val: boolean) => void;
|
||||
setCheckLinux: (val: boolean) => void;
|
||||
setCheckChrome: (val: boolean) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const PlatformSelector = ({
|
||||
@@ -23,17 +25,23 @@ export const PlatformSelector = ({
|
||||
setCheckWindows,
|
||||
setCheckLinux,
|
||||
setCheckChrome,
|
||||
disabled = false,
|
||||
}: IPlatformSelectorProps): JSX.Element => {
|
||||
const baseClass = "platform-selector";
|
||||
|
||||
const labelClasses = classNames("form-field__label", {
|
||||
[`form-field__label--disabled`]: disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={`${parentClass}__${baseClass} ${baseClass} form-field`}>
|
||||
<span className="form-field__label">Checks on:</span>
|
||||
<span className={labelClasses}>Checks on:</span>
|
||||
<span className={`${baseClass}__checkboxes`}>
|
||||
<Checkbox
|
||||
value={checkDarwin}
|
||||
onChange={(value: boolean) => setCheckDarwin(value)}
|
||||
wrapperClassName={`${baseClass}__platform-checkbox-wrapper`}
|
||||
disabled={disabled}
|
||||
>
|
||||
macOS
|
||||
</Checkbox>
|
||||
@@ -41,6 +49,7 @@ export const PlatformSelector = ({
|
||||
value={checkWindows}
|
||||
onChange={(value: boolean) => setCheckWindows(value)}
|
||||
wrapperClassName={`${baseClass}__platform-checkbox-wrapper`}
|
||||
disabled={disabled}
|
||||
>
|
||||
Windows
|
||||
</Checkbox>
|
||||
@@ -48,6 +57,7 @@ export const PlatformSelector = ({
|
||||
value={checkLinux}
|
||||
onChange={(value: boolean) => setCheckLinux(value)}
|
||||
wrapperClassName={`${baseClass}__platform-checkbox-wrapper`}
|
||||
disabled={disabled}
|
||||
>
|
||||
Linux
|
||||
</Checkbox>
|
||||
@@ -55,6 +65,7 @@ export const PlatformSelector = ({
|
||||
value={checkChrome}
|
||||
onChange={(value: boolean) => setCheckChrome(value)}
|
||||
wrapperClassName={`${baseClass}__platform-checkbox-wrapper`}
|
||||
disabled={disabled}
|
||||
>
|
||||
ChromeOS
|
||||
</Checkbox>
|
||||
|
||||
@@ -7,4 +7,8 @@
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-field__label--disabled {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,6 +120,10 @@ class InputField extends Component {
|
||||
[`${baseClass}__textarea`]: type === "textarea",
|
||||
});
|
||||
|
||||
const inputWrapperClasses = classnames(inputWrapperClass, {
|
||||
[`input-field--disabled`]: disabled,
|
||||
});
|
||||
|
||||
const formFieldProps = pick(this.props, [
|
||||
"helpText",
|
||||
"label",
|
||||
@@ -144,7 +148,7 @@ class InputField extends Component {
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="textarea"
|
||||
className={inputWrapperClass}
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<textarea
|
||||
name={name}
|
||||
@@ -169,7 +173,11 @@ class InputField extends Component {
|
||||
});
|
||||
|
||||
return (
|
||||
<FormField {...formFieldProps} type="input" className={inputWrapperClass}>
|
||||
<FormField
|
||||
{...formFieldProps}
|
||||
type="input"
|
||||
className={inputWrapperClasses}
|
||||
>
|
||||
<div className={inputContainerClasses}>
|
||||
<input
|
||||
disabled={disabled}
|
||||
|
||||
@@ -21,13 +21,18 @@
|
||||
border-color: $core-vibrant-blue;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover &:not(.input-field--disabled) {
|
||||
box-shadow: none;
|
||||
border: 1px solid $core-vibrant-blue;
|
||||
}
|
||||
|
||||
&--disabled {
|
||||
color: $ui-fleet-black-50;
|
||||
|
||||
.form-field__label,
|
||||
.form-field__help-text {
|
||||
color: $ui-fleet-black-50;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from "react";
|
||||
import { COLORS, Colors } from "styles/var/colors";
|
||||
|
||||
interface ISparklesProps {
|
||||
color?: Colors;
|
||||
}
|
||||
|
||||
const Sparkles = ({ color = "core-fleet-blue" }: ISparklesProps) => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="17"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 16 17"
|
||||
>
|
||||
<path
|
||||
d="M9.03 2.767a.5.5 0 0 1 .939 0l1.342 3.627a.5.5 0 0 0 .295.295l3.627 1.342a.5.5 0 0 1 0 .938l-3.627 1.342a.5.5 0 0 0-.295.295l-1.342 3.627a.5.5 0 0 1-.938 0l-1.342-3.627a.5.5 0 0 0-.295-.295L3.767 8.969a.5.5 0 0 1 0-.938L7.394 6.69a.5.5 0 0 0 .295-.295l1.342-3.627ZM2.765 1.134a.25.25 0 0 1 .47 0l.535 1.448a.25.25 0 0 0 .148.148l1.448.536a.25.25 0 0 1 0 .468l-1.448.536a.25.25 0 0 0-.148.148l-.536 1.448a.25.25 0 0 1-.469 0L2.23 4.418a.25.25 0 0 0-.147-.148L.633 3.734a.25.25 0 0 1 0-.468l1.449-.536a.25.25 0 0 0 .147-.148l.536-1.448ZM2.765 11.134a.25.25 0 0 1 .47 0l.535 1.448a.25.25 0 0 0 .148.148l1.448.535a.25.25 0 0 1 0 .47l-1.448.535a.25.25 0 0 0-.148.148l-.536 1.448a.25.25 0 0 1-.469 0l-.536-1.448a.25.25 0 0 0-.147-.148l-1.449-.535a.25.25 0 0 1 0-.47l1.449-.535a.25.25 0 0 0 .147-.148l.536-1.448Z"
|
||||
fill={COLORS[color]}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default Sparkles;
|
||||
@@ -48,6 +48,7 @@ import Clock from "./Clock";
|
||||
import Copy from "./Copy";
|
||||
import Eye from "./Eye";
|
||||
import Pencil from "./Pencil";
|
||||
import Sparkles from "./Sparkles";
|
||||
import Text from "./Text";
|
||||
import Transfer from "./Transfer";
|
||||
import TrashCan from "./TrashCan";
|
||||
@@ -87,6 +88,7 @@ export const ICON_MAP = {
|
||||
eye: Eye,
|
||||
pencil: Pencil,
|
||||
search: Search,
|
||||
sparkles: Sparkles,
|
||||
text: Text,
|
||||
transfer: Transfer,
|
||||
trash: TrashCan,
|
||||
|
||||
@@ -13,11 +13,13 @@ export interface IPlatformSelector {
|
||||
getSelectedPlatforms: () => ("darwin" | "windows" | "linux" | "chrome")[];
|
||||
isAnyPlatformSelected: boolean;
|
||||
render: () => JSX.Element;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
const usePlatformSelector = (
|
||||
platformContext: SelectedPlatformString | null | undefined,
|
||||
baseClass = ""
|
||||
baseClass = "",
|
||||
disabled = false
|
||||
): IPlatformSelector => {
|
||||
const [checkDarwin, setCheckDarwin] = useState(false);
|
||||
const [checkWindows, setCheckWindows] = useState(false);
|
||||
@@ -69,9 +71,10 @@ const usePlatformSelector = (
|
||||
setCheckWindows={setCheckWindows}
|
||||
setCheckLinux={setCheckLinux}
|
||||
setCheckChrome={setCheckChrome}
|
||||
disabled={disabled}
|
||||
/>
|
||||
);
|
||||
}, [checkDarwin, checkWindows, checkLinux, checkChrome]);
|
||||
}, [checkDarwin, checkWindows, checkLinux, checkChrome, disabled]);
|
||||
|
||||
return {
|
||||
setSelectedPlatforms,
|
||||
|
||||
@@ -2,9 +2,7 @@ import React, { useCallback, useContext, useEffect, useState } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { InjectedRouter } from "react-router/lib/Router";
|
||||
import PATHS from "router/paths";
|
||||
import { noop, isEqual, uniqueId } from "lodash";
|
||||
|
||||
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
|
||||
import { noop, isEqual } from "lodash";
|
||||
|
||||
import { getNextLocationPath } from "utilities/helpers";
|
||||
|
||||
|
||||
@@ -61,6 +61,11 @@ describe("PolicyForm - component", () => {
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -117,6 +122,11 @@ describe("PolicyForm - component", () => {
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -172,7 +182,7 @@ describe("PolicyForm - component", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { container, user } = render(
|
||||
const { user } = render(
|
||||
<PolicyForm
|
||||
policyIdForEdit={mockPolicy.id}
|
||||
showOpenSchemaActionText={false}
|
||||
@@ -189,6 +199,11 @@ describe("PolicyForm - component", () => {
|
||||
onOpenSchemaSidebar={jest.fn()}
|
||||
renderLiveQueryWarning={jest.fn()}
|
||||
backendValidators={{}}
|
||||
onClickAutofillDescription={jest.fn()}
|
||||
onClickAutofillResolution={jest.fn()}
|
||||
isFetchingAutofillDescription={false}
|
||||
isFetchingAutofillResolution={false}
|
||||
resetAiAutofillData={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@ import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Spinner from "components/Spinner";
|
||||
import Icon from "components/Icon/Icon";
|
||||
import AutoSizeInputField from "components/forms/fields/AutoSizeInputField";
|
||||
import PremiumFeatureIconWithTooltip from "components/PremiumFeatureIconWithTooltip";
|
||||
import SaveNewPolicyModal from "../SaveNewPolicyModal";
|
||||
|
||||
const baseClass = "policy-form";
|
||||
@@ -50,6 +49,11 @@ interface IPolicyFormProps {
|
||||
onOpenSchemaSidebar: () => void;
|
||||
renderLiveQueryWarning: () => JSX.Element | null;
|
||||
backendValidators: { [key: string]: string };
|
||||
isFetchingAutofillDescription: boolean;
|
||||
isFetchingAutofillResolution: boolean;
|
||||
onClickAutofillDescription: () => Promise<void>;
|
||||
onClickAutofillResolution: () => Promise<void>;
|
||||
resetAiAutofillData: () => void;
|
||||
}
|
||||
|
||||
const validateQuerySQL = (query: string) => {
|
||||
@@ -80,6 +84,11 @@ const PolicyForm = ({
|
||||
onOpenSchemaSidebar,
|
||||
renderLiveQueryWarning,
|
||||
backendValidators,
|
||||
isFetchingAutofillDescription,
|
||||
isFetchingAutofillResolution,
|
||||
onClickAutofillDescription,
|
||||
onClickAutofillResolution,
|
||||
resetAiAutofillData,
|
||||
}: IPolicyFormProps): JSX.Element => {
|
||||
const [errors, setErrors] = useState<{ [key: string]: any }>({}); // string | null | undefined or boolean | undefined
|
||||
const [isSaveNewPolicyModalOpen, setIsSaveNewPolicyModalOpen] = useState(
|
||||
@@ -135,9 +144,13 @@ const PolicyForm = ({
|
||||
setCompatiblePlatforms,
|
||||
} = platformCompatibility;
|
||||
|
||||
const platformSelectorDisabled =
|
||||
isFetchingAutofillDescription || isFetchingAutofillResolution;
|
||||
|
||||
const platformSelector = usePlatformSelector(
|
||||
lastEditedQueryPlatform,
|
||||
baseClass
|
||||
baseClass,
|
||||
platformSelectorDisabled
|
||||
);
|
||||
|
||||
const {
|
||||
@@ -155,6 +168,8 @@ const PolicyForm = ({
|
||||
DEFAULT_POLICIES.find((p) => p.name === lastEditedQueryName);
|
||||
|
||||
const disabledLiveQuery = config?.server_settings.live_query_disabled;
|
||||
const aiFeaturesDisabled =
|
||||
config?.server_settings.ai_features_disabled || false;
|
||||
|
||||
useEffect(() => {
|
||||
if (isNewTemplatePolicy) {
|
||||
@@ -199,8 +214,9 @@ const PolicyForm = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onChangePolicy = (sqlString: string) => {
|
||||
const onChangePolicySql = (sqlString: string) => {
|
||||
setLastEditedQueryBody(sqlString);
|
||||
resetAiAutofillData(); // Allows retry of AI autofill API if the SQL has changed
|
||||
};
|
||||
|
||||
const onInputKeypress = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
@@ -569,7 +585,7 @@ const PolicyForm = ({
|
||||
name="query editor"
|
||||
onLoad={onLoad}
|
||||
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
|
||||
onChange={onChangePolicy}
|
||||
onChange={onChangePolicySql}
|
||||
handleSubmit={promptSavePolicy}
|
||||
wrapEnabled
|
||||
focus={!isEditMode}
|
||||
@@ -662,6 +678,11 @@ const PolicyForm = ({
|
||||
backendValidators={backendValidators}
|
||||
platformSelector={platformSelector}
|
||||
isUpdatingPolicy={isUpdatingPolicy}
|
||||
aiFeaturesDisabled={aiFeaturesDisabled}
|
||||
isFetchingAutofillDescription={isFetchingAutofillDescription}
|
||||
isFetchingAutofillResolution={isFetchingAutofillResolution}
|
||||
onClickAutofillDescription={onClickAutofillDescription}
|
||||
onClickAutofillResolution={onClickAutofillResolution}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -184,6 +184,19 @@
|
||||
font-size: $x-small;
|
||||
}
|
||||
|
||||
&__autofill-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
.autofill-tooltip-wrapper {
|
||||
display: flex; // Required for vertical centering
|
||||
}
|
||||
|
||||
.autofill-button-tooltip {
|
||||
font-weight: $regular;
|
||||
}
|
||||
}
|
||||
|
||||
&__button-wrap {
|
||||
&--tooltip {
|
||||
display: flex;
|
||||
|
||||
+116
-23
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useContext, useEffect } from "react";
|
||||
import React, { useState, useContext, useEffect, useCallback } from "react";
|
||||
import { size } from "lodash";
|
||||
import classNames from "classnames";
|
||||
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
@@ -14,6 +15,7 @@ import Checkbox from "components/forms/fields/Checkbox";
|
||||
import TooltipWrapper from "components/TooltipWrapper";
|
||||
import Button from "components/buttons/Button";
|
||||
import Modal from "components/Modal";
|
||||
import Icon from "components/Icon";
|
||||
import ReactTooltip from "react-tooltip";
|
||||
import { COLORS } from "styles/var/colors";
|
||||
|
||||
@@ -25,6 +27,11 @@ export interface ISaveNewPolicyModalProps {
|
||||
backendValidators: { [key: string]: string };
|
||||
platformSelector: IPlatformSelector;
|
||||
isUpdatingPolicy: boolean;
|
||||
aiFeaturesDisabled?: boolean;
|
||||
isFetchingAutofillDescription: boolean;
|
||||
isFetchingAutofillResolution: boolean;
|
||||
onClickAutofillDescription: () => Promise<void>;
|
||||
onClickAutofillResolution: () => Promise<void>;
|
||||
}
|
||||
|
||||
const validatePolicyName = (name: string) => {
|
||||
@@ -46,6 +53,11 @@ const SaveNewPolicyModal = ({
|
||||
backendValidators,
|
||||
platformSelector,
|
||||
isUpdatingPolicy,
|
||||
aiFeaturesDisabled,
|
||||
isFetchingAutofillDescription,
|
||||
isFetchingAutofillResolution,
|
||||
onClickAutofillDescription,
|
||||
onClickAutofillResolution,
|
||||
}: ISaveNewPolicyModalProps): JSX.Element => {
|
||||
const { isPremiumTier } = useContext(AppContext);
|
||||
const {
|
||||
@@ -53,24 +65,27 @@ const SaveNewPolicyModal = ({
|
||||
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 [name, setName] = useState(lastEditedQueryName);
|
||||
const [description, setDescription] = useState(lastEditedQueryDescription);
|
||||
const [resolution, setResolution] = useState(lastEditedQueryResolution);
|
||||
const [critical, setCritical] = useState(lastEditedQueryCritical);
|
||||
const [errors, setErrors] = useState<{ [key: string]: string }>(
|
||||
backendValidators
|
||||
);
|
||||
|
||||
const disableSave = !platformSelector.isAnyPlatformSelected;
|
||||
const disableForm =
|
||||
isFetchingAutofillDescription || isFetchingAutofillResolution;
|
||||
const disableSave = !platformSelector.isAnyPlatformSelected || disableForm;
|
||||
|
||||
useDeepEffect(() => {
|
||||
if (name) {
|
||||
if (lastEditedQueryName) {
|
||||
setErrors({});
|
||||
}
|
||||
}, [name]);
|
||||
}, [lastEditedQueryName]);
|
||||
|
||||
useEffect(() => {
|
||||
setErrors(backendValidators);
|
||||
@@ -84,7 +99,9 @@ const SaveNewPolicyModal = ({
|
||||
.join(",") as SelectedPlatformString;
|
||||
setLastEditedQueryPlatform(newPlatformString);
|
||||
|
||||
const { valid: validName, errors: newErrors } = validatePolicyName(name);
|
||||
const { valid: validName, errors: newErrors } = validatePolicyName(
|
||||
lastEditedQueryName
|
||||
);
|
||||
setErrors({
|
||||
...errors,
|
||||
...newErrors,
|
||||
@@ -92,16 +109,88 @@ const SaveNewPolicyModal = ({
|
||||
|
||||
if (!disableSave && validName) {
|
||||
onCreatePolicy({
|
||||
description,
|
||||
name,
|
||||
description: lastEditedQueryDescription,
|
||||
name: lastEditedQueryName,
|
||||
query: queryValue,
|
||||
resolution,
|
||||
resolution: lastEditedQueryResolution,
|
||||
platform: newPlatformString,
|
||||
critical,
|
||||
critical: lastEditedQueryCritical,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderAutofillButton = useCallback(
|
||||
(labelName: "Description" | "Resolution") => {
|
||||
const isFetchingButton =
|
||||
(labelName === "Description" && isFetchingAutofillDescription) ||
|
||||
(labelName === "Resolution" && isFetchingAutofillResolution);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-tip
|
||||
data-for={`autofill-button-${labelName}`}
|
||||
// Tooltip shows except when fetching AI autofill
|
||||
data-tip-disable={disableForm}
|
||||
className="autofill-tooltip-wrapper"
|
||||
>
|
||||
<Button
|
||||
variant="text-icon"
|
||||
disabled={aiFeaturesDisabled || disableForm}
|
||||
onClick={
|
||||
labelName === "Description"
|
||||
? onClickAutofillDescription
|
||||
: onClickAutofillResolution
|
||||
}
|
||||
>
|
||||
{isFetchingButton ? (
|
||||
"Thinking..."
|
||||
) : (
|
||||
<>
|
||||
<Icon name="sparkles" /> Autofill
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<ReactTooltip
|
||||
className="autofill-button-tooltip"
|
||||
place="top"
|
||||
effect="solid"
|
||||
backgroundColor={COLORS["tooltip-bg"]}
|
||||
id={`autofill-button-${labelName}`}
|
||||
data-html
|
||||
>
|
||||
{aiFeaturesDisabled ? (
|
||||
"AI features are disabled in organization settings"
|
||||
) : (
|
||||
<>
|
||||
Policy queries (SQL) will be sent to the LLM. Fleet doesn't
|
||||
use this data to train models.
|
||||
</>
|
||||
)}
|
||||
</ReactTooltip>
|
||||
</>
|
||||
);
|
||||
},
|
||||
[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"
|
||||
@@ -115,40 +204,44 @@ const SaveNewPolicyModal = ({
|
||||
>
|
||||
<InputField
|
||||
name="name"
|
||||
onChange={(value: string) => setName(value)}
|
||||
value={name}
|
||||
onChange={(value: string) => setLastEditedQueryName(value)}
|
||||
value={lastEditedQueryName}
|
||||
error={errors.name}
|
||||
inputClassName={`${baseClass}__policy-save-modal-name`}
|
||||
label="Name"
|
||||
helpText="What yes or no question does your policy ask about your hosts?"
|
||||
autofocus
|
||||
ignore1password
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<InputField
|
||||
name="description"
|
||||
onChange={(value: string) => setDescription(value)}
|
||||
value={description}
|
||||
onChange={(value: string) => setLastEditedQueryDescription(value)}
|
||||
value={lastEditedQueryDescription}
|
||||
inputClassName={`${baseClass}__policy-save-modal-description`}
|
||||
label="Description"
|
||||
label={renderAutofillLabel("Description")}
|
||||
type="textarea"
|
||||
disabled={disableForm}
|
||||
/>
|
||||
<InputField
|
||||
name="resolution"
|
||||
onChange={(value: string) => setResolution(value)}
|
||||
value={resolution}
|
||||
onChange={(value: string) => setLastEditedQueryResolution(value)}
|
||||
value={lastEditedQueryResolution}
|
||||
inputClassName={`${baseClass}__policy-save-modal-resolution`}
|
||||
label="Resolution"
|
||||
label={renderAutofillLabel("Resolution")}
|
||||
type="textarea"
|
||||
helpText="What steps should an end user take to resolve a host that fails this policy? (optional)"
|
||||
disabled={disableForm}
|
||||
/>
|
||||
{platformSelector.render()}
|
||||
{isPremiumTier && (
|
||||
<div className="critical-checkbox-wrapper">
|
||||
<Checkbox
|
||||
name="critical-policy"
|
||||
onChange={(value: boolean) => setCritical(value)}
|
||||
value={critical}
|
||||
onChange={(value: boolean) => setLastEditedQueryCritical(value)}
|
||||
value={lastEditedQueryCritical}
|
||||
isLeftLabel
|
||||
disabled={disableForm}
|
||||
>
|
||||
<TooltipWrapper
|
||||
tipContent={
|
||||
|
||||
@@ -3,6 +3,7 @@ import { InjectedRouter } from "react-router/lib/Router";
|
||||
|
||||
import globalPoliciesAPI from "services/entities/global_policies";
|
||||
import teamPoliciesAPI from "services/entities/team_policies";
|
||||
import autofillAPI, { IAutofillPolicy } from "services/entities/autofill";
|
||||
import { AppContext } from "context/app";
|
||||
import { PolicyContext } from "context/policy";
|
||||
import { NotificationContext } from "context/notification";
|
||||
@@ -64,6 +65,8 @@ const QueryEditor = ({
|
||||
lastEditedQueryCritical,
|
||||
lastEditedQueryPlatform,
|
||||
policyTeamId,
|
||||
setLastEditedQueryDescription,
|
||||
setLastEditedQueryResolution,
|
||||
} = useContext(PolicyContext);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -79,6 +82,68 @@ const QueryEditor = ({
|
||||
const [backendValidators, setBackendValidators] = useState<{
|
||||
[key: string]: string;
|
||||
}>({});
|
||||
const [
|
||||
policyAutofillData,
|
||||
setPolicyAutofillData,
|
||||
] = useState<IAutofillPolicy | null>(null);
|
||||
const [policyAutofillErrors, setPolicyAutofillErrors] = useState<any>({});
|
||||
const [
|
||||
isFetchingAutofillDescription,
|
||||
setIsFetchingAutofillDescription,
|
||||
] = useState(false);
|
||||
const [
|
||||
isFetchingAutofillResolution,
|
||||
setIsFetchingAutofillResolution,
|
||||
] = useState(false);
|
||||
|
||||
const onClickAutofillDescription = async () => {
|
||||
// When AI autofill data exists already, fill out section clicked with data
|
||||
if (policyAutofillData) {
|
||||
setLastEditedQueryDescription(policyAutofillData.description);
|
||||
} else {
|
||||
// Show thinking state and fetch data from API
|
||||
setIsFetchingAutofillDescription(true);
|
||||
|
||||
try {
|
||||
const autofillResponse = await autofillAPI.getPolicyInterpretationFromSQL(
|
||||
lastEditedQueryBody
|
||||
);
|
||||
|
||||
setPolicyAutofillData(autofillResponse);
|
||||
// Only fill out section that was clicked to be fetched
|
||||
setLastEditedQueryDescription(autofillResponse.description);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
renderFlash("error", "Couldn't autofill policy data.");
|
||||
setPolicyAutofillErrors(error);
|
||||
}
|
||||
setIsFetchingAutofillDescription(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onClickAutofillResolution = async () => {
|
||||
// When AI autofill data exists already, fill out section clicked with data
|
||||
if (policyAutofillData) {
|
||||
setLastEditedQueryResolution(policyAutofillData.resolution);
|
||||
} else {
|
||||
// Show thinking state and fetch data from API
|
||||
setIsFetchingAutofillResolution(true);
|
||||
|
||||
try {
|
||||
const autofillResponse = await autofillAPI.getPolicyInterpretationFromSQL(
|
||||
lastEditedQueryBody
|
||||
);
|
||||
setPolicyAutofillData(autofillResponse);
|
||||
// Only fill out section that was clicked to be fetched
|
||||
setLastEditedQueryResolution(autofillResponse.resolution);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
renderFlash("error", "Couldn't autofill policy data.");
|
||||
setPolicyAutofillErrors(error);
|
||||
}
|
||||
setIsFetchingAutofillResolution(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onCreatePolicy = debounce(async (formData: IPolicyFormData) => {
|
||||
if (policyTeamId) {
|
||||
@@ -199,6 +264,11 @@ const QueryEditor = ({
|
||||
isTeamMaintainer={isTeamMaintainer}
|
||||
isTeamObserver={isTeamObserver}
|
||||
isUpdatingPolicy={isUpdatingPolicy}
|
||||
isFetchingAutofillDescription={isFetchingAutofillDescription}
|
||||
isFetchingAutofillResolution={isFetchingAutofillResolution}
|
||||
onClickAutofillDescription={onClickAutofillDescription}
|
||||
onClickAutofillResolution={onClickAutofillResolution}
|
||||
resetAiAutofillData={() => setPolicyAutofillData(null)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
// import sendRequest from "services";
|
||||
import { sendRequest } from "services/mock_service/service/service";
|
||||
import endpoints from "utilities/endpoints";
|
||||
|
||||
export type IAutofillPolicy = {
|
||||
description: string;
|
||||
resolution: string;
|
||||
};
|
||||
|
||||
export default {
|
||||
getPolicyInterpretationFromSQL: (sql: string): Promise<IAutofillPolicy> => {
|
||||
const { AUTOFILL_POLICIES } = endpoints;
|
||||
|
||||
return sendRequest("POST", AUTOFILL_POLICIES, sql);
|
||||
},
|
||||
};
|
||||
@@ -8,7 +8,7 @@ import RESPONSES from "./responses";
|
||||
|
||||
type IResponses = Record<string, Record<string, Record<string, unknown>>>;
|
||||
|
||||
const DELAY = 1000;
|
||||
const DELAY = 5000;
|
||||
|
||||
const ENDPOINT = "/latest/fleet";
|
||||
|
||||
@@ -54,6 +54,7 @@ const REQUEST_RESPONSE_MAPPINGS: IResponses = {
|
||||
team_id: null,
|
||||
platform: "linux",
|
||||
},
|
||||
"autofill/policies": RESPONSES.aiAutofillPolicy,
|
||||
},
|
||||
} as IResponses;
|
||||
|
||||
|
||||
@@ -10591,6 +10591,13 @@ const globalQuery6 = { query: globalQueries.queries[6] };
|
||||
const teamQuery1 = { query: teamQueries.queries[0] };
|
||||
const teamQuery2 = { query: teamQueries.queries[1] };
|
||||
|
||||
const aiAutofillPolicy = {
|
||||
description:
|
||||
"The firewall is not enabled, exposing the laptop to potential security threats such as unauthorized access, data breaches, and malware attacks.",
|
||||
resolution:
|
||||
"The maintenance will probably involve investigation of the firewall status, troubleshooting any issues found, and enabling the firewall to ensure the laptop's security.",
|
||||
};
|
||||
|
||||
export default {
|
||||
count,
|
||||
hosts,
|
||||
@@ -10606,4 +10613,5 @@ export default {
|
||||
teamQueries,
|
||||
teamQuery1,
|
||||
teamQuery2,
|
||||
aiAutofillPolicy,
|
||||
};
|
||||
|
||||
@@ -129,6 +129,9 @@ export default {
|
||||
SOFTWARE_VERSION: (id: number) =>
|
||||
`/${API_VERSION}/fleet/software/versions/${id}`,
|
||||
|
||||
// AI endpoints
|
||||
AUTOFILL_POLICIES: `/${API_VERSION}/fleet/autofill/policies`,
|
||||
|
||||
SSO: `/v1/fleet/sso`,
|
||||
STATUS_LABEL_COUNTS: `/${API_VERSION}/fleet/host_summary`,
|
||||
STATUS_LIVE_QUERY: `/${API_VERSION}/fleet/status/live_query`,
|
||||
|
||||
Reference in New Issue
Block a user