fleet/frontend/pages/labels/components/DynamicLabelForm/DynamicLabelForm.tsx
Ian Littman 8e4e89f4e9
API + auth + UI changes for team labels (#37208)
Covers #36760, #36758.

# 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.

- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)

## 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)

- [ ] QA'd all new/changed functionality manually
2025-12-29 21:28:45 -06:00

156 lines
4.1 KiB
TypeScript

import React, { useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { IAceEditor } from "react-ace/lib/types";
import { validateQuery } from "components/forms/validators/validate_query";
import SQLEditor from "components/SQLEditor";
import Button from "components/buttons/Button";
import Icon from "components/Icon";
import LabelForm from "../LabelForm";
import { ILabelFormData } from "../LabelForm/LabelForm";
import PlatformField from "../PlatformField";
const baseClass = "dynamic-label-form";
export interface IDynamicLabelFormData {
name: string;
description: string;
query: string;
platform: string;
}
interface IDynamicLabelFormProps {
defaultName?: string;
defaultDescription?: string;
defaultQuery?: string;
defaultPlatform?: string;
showOpenSidebarButton?: boolean;
isEditing?: boolean;
onOpenSidebar?: () => void;
onOsqueryTableSelect?: (tableName: string) => void;
teamName: string | null;
onSave: (formData: IDynamicLabelFormData) => void;
onCancel: () => void;
}
const DynamicLabelForm = ({
defaultName = "",
defaultDescription = "",
defaultQuery = "",
defaultPlatform = "",
isEditing = false,
showOpenSidebarButton = false,
onOpenSidebar,
onOsqueryTableSelect,
teamName,
onSave,
onCancel,
}: IDynamicLabelFormProps) => {
const [query, setQuery] = useState(defaultQuery);
const [platform, setPlatform] = useState(defaultPlatform);
const [queryError, setQueryError] = useState<string | null>(null);
const debounceValidateSQL = useDebouncedCallback((queryString: string) => {
const { error } = validateQuery(queryString);
if (query === "" || error === "") {
setQueryError(null);
} else {
setQueryError(error);
}
}, 500);
const onQueryChange = (newQuery: string) => {
setQuery(newQuery);
debounceValidateSQL(newQuery);
};
const onSaveForm = (
labelFormData: ILabelFormData,
labelFormDataValid: boolean
) => {
const { error } = validateQuery(query);
if (error) {
setQueryError(error);
} else if (labelFormDataValid) {
// values from LabelForm component must be valid too
onSave({ ...labelFormData, query, platform });
}
};
const renderLabelComponent = (): JSX.Element | null => {
if (!showOpenSidebarButton) {
return null;
}
return (
<Button variant="inverse" onClick={onOpenSidebar}>
Schema
<Icon name="info" size="small" />
</Button>
);
};
const onLoad = (editor: IAceEditor) => {
editor.setOptions({
enableLinking: true,
enableMultiselect: false, // Disables command + click creating multiple cursors
});
// @ts-expect-error
// the string "linkClick" is not officially in the lib but we need it
editor.on("linkClick", (data) => {
const { type, value } = data.token;
if (type === "osquery-token" && onOsqueryTableSelect) {
return onOsqueryTableSelect(value);
}
return false;
});
};
const onChangePlatform = (value: string) => {
setPlatform(value);
};
return (
<div className={baseClass}>
<LabelForm
defaultName={defaultName}
defaultDescription={defaultDescription}
teamName={teamName}
onSave={onSaveForm}
onCancel={onCancel}
immutableFields={
teamName
? ["teams", "queries", "platforms"]
: ["queries", "platforms"]
}
additionalFields={
<>
<SQLEditor
error={queryError}
name="query"
onChange={onQueryChange}
value={query}
label="Query"
labelActionComponent={renderLabelComponent()}
readOnly={isEditing}
onLoad={onLoad}
wrapperClassName={`${baseClass}__text-editor-wrapper form-field`}
wrapEnabled
/>
<PlatformField
platform={platform}
isEditing={isEditing}
onChange={onChangePlatform}
/>
</>
}
/>
</div>
);
};
export default DynamicLabelForm;