fleet/frontend/pages/labels/components/LabelForm/LabelForm.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

117 lines
3 KiB
TypeScript

import React, { ReactNode, useState } from "react";
import validate_presence from "components/forms/validators/validate_presence";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import Button from "components/buttons/Button";
import TeamNameField from "../TeamNameField/TeamNameField";
export interface ILabelFormData {
name: string;
description: string;
}
interface ILabelFormProps {
defaultName?: string;
defaultDescription?: string;
additionalFields?: ReactNode;
isUpdatingLabel?: boolean;
teamName: string | null;
onCancel: () => void;
immutableFields: string[];
onSave: (formData: ILabelFormData, isValid: boolean) => void;
}
const baseClass = "label-form";
const generateDescriptionHelpText = (immutableFields: string[]) => {
if (immutableFields.length === 0) {
return "";
}
const SUFFIX =
"are immutable. To make changes, delete this label and create a new one.";
return immutableFields.length === 1
? `Label ${immutableFields[0]} ${SUFFIX}`
: `Label ${immutableFields
.slice(0, -1)
.join(", ")} and ${immutableFields.pop()} ${SUFFIX}`;
};
const LabelForm = ({
defaultName = "",
defaultDescription = "",
additionalFields,
isUpdatingLabel,
teamName,
onCancel,
onSave,
immutableFields,
}: ILabelFormProps) => {
const [name, setName] = useState(defaultName);
const [description, setDescription] = useState(defaultDescription);
const [nameError, setNameError] = useState<string | null>("");
const onNameChange = (value: string) => {
setName(value);
setNameError(null);
};
const onDescriptionChange = (value: string) => {
setDescription(value);
};
const onSubmitForm = (evt: React.FormEvent) => {
evt.preventDefault();
let isFormValid = true;
if (!validate_presence(name)) {
setNameError("Label name must be present");
isFormValid = false;
}
onSave({ name, description }, isFormValid);
};
return (
<form className={`${baseClass}__wrapper`} onSubmit={onSubmitForm}>
<InputField
error={nameError}
name="name"
onChange={onNameChange}
value={name}
inputClassName={`${baseClass}__label-title`}
label="Name"
placeholder="Label name"
/>
<InputField
name="description"
onChange={onDescriptionChange}
value={description}
inputClassName={`${baseClass}__label-description`}
label="Description"
type="textarea"
placeholder="Label description (optional)"
/>
{immutableFields.length > 0 ? (
<span className={`${baseClass}__help-text`}>
{generateDescriptionHelpText(immutableFields)}
</span>
) : null}
{teamName ? <TeamNameField name={teamName} /> : null}
{additionalFields}
<div className="button-wrap">
<Button onClick={onCancel} variant="inverse">
Cancel
</Button>
<Button type="submit" isLoading={isUpdatingLabel}>
Save
</Button>
</div>
</form>
);
};
export default LabelForm;