Fleet UI: AI autofill policy description and resolution (#18646)

This commit is contained in:
RachelElysia
2024-05-02 11:57:00 -05:00
committed by Victor Lyuboslavsky
parent 4946c96e77
commit 85b15758f7
17 changed files with 334 additions and 38 deletions
@@ -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 {
+25
View File
@@ -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;
+2
View File
@@ -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,
+5 -2
View File
@@ -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;
@@ -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&apos;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>
);
+16
View File
@@ -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,
};
+3
View File
@@ -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`,