Fleet UI: Handle long fleet names across the Fleets UI (#49216)

## Issue
Closes #47290

Also implements the "Cap free-text `maxLength` to the backend column
length" pattern established in [#49041 (patterns.md
thread)](https://github.com/fleetdm/fleet/pull/49041/files#r3572648691).

## Description
Fleet name inputs had no `maxLength` cap and no service-layer length
check, so a name >255 chars failed with a raw MySQL `Data too long`
error, and several UI surfaces didn't handle long names gracefully. This
PR fixes all four manifestations called out in the bug, plus a related
label-overflow case on the host details page, and hardens adjacent name
inputs across the app.

**Frontend fixes for #47290:**
- Create/Rename fleet name inputs now cap at 255 characters (matches
`teams.name varchar(255)`).
- Fleets table Name column uses `LinkCell` with `tooltipTruncate` +
`className="w400"` so long names truncate with an ellipsis and full-name
tooltip instead of overflowing across the Hosts/Users columns.
- Fleet-detail page header (`.team-details__team-header`): h1 gets
`overflow: hidden; text-overflow: ellipsis; white-space: nowrap;`,
`__team-details` gets `min-width: 0; flex: 1`, and `.action-buttons`
gets `flex-shrink: 0` + `white-space: nowrap` on buttons so *Manage
enroll secrets / Rename / Delete* no longer wrap to a second line when
the fleet name is long.
- Manage enroll secrets modal body — `__description` gets
`overflow-wrap: anywhere; min-width: 0` so a long `<b>{fleet name}</b>`
wraps within the modal instead of spilling out the right edge.

**Backend fixes for #47290:**
- New `fleet.MaxTeamNameLength = 255` constant.
- `NewTeam`, `ModifyTeam`, and `ApplyTeamSpecs` now return
`fleet.NewInvalidArgumentError("name", "may not exceed 255 characters")`
instead of surfacing a raw `Data too long` MySQL error. Covers UI, API,
and GitOps entry points.

**Broader consistency pass (per [#49041
thread](https://github.com/fleetdm/fleet/pull/49041/files#r3572648691)):**
- New shared `MAX_ENTITY_CHAR_LENGTH = 255` constant in
`frontend/utilities/constants.tsx`.
- Refactored 8 existing files that had ad-hoc `NAME_MAX_LENGTH = 255` /
`MAX_LABEL_NAME_LENGTH = 255` locals to use it.
- Slotted it into 16 additional `InputField` name/description inputs
that were missing a cap (API user, custom variable, certificate, label
name + description, pack name + description, and all 5 CA forms —
CustomEST, CustomSCEP, Smallstep, Digicert, Hydrant).
- Pruned dead FE length validators that can no longer fire now that the
DOM cap enforces the limit (certificate modal, custom variable modal,
both label helpers, both category modals). Unusual/shorter caps (e.g.
`varchar(64)`, custom business rules) still keep their inline validators
— silent truncation is only appropriate for the common 255-char norm.

**Bonus:** fixed the long-label overflow on the host details Labels card
by capping the pill button `max-width` at 300px.

## Screenrecording



https://github.com/user-attachments/assets/b917b72e-7437-4d0c-a1a1-c49b4b1c28ba



https://github.com/user-attachments/assets/3a3efbb2-09d8-4f47-9fd4-f158b3453b9e



https://github.com/user-attachments/assets/73e5e022-dc93-4381-82b3-be9549d050e6

Latest - max width 300px long label:

<img width="1106" height="262" alt="Screenshot 2026-07-23 at 11 29
24 AM"
src="https://github.com/user-attachments/assets/741fddbd-f78d-4578-a025-bddf64a81c25"
/>


## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually

Test coverage:
- `CreateFleetModal.tests.tsx`, `RenameFleetModal.tests.tsx` — new case
per file asserting the name input's `maxLength === 255`.
- `AddCertificateModal.tests.tsx`, `Variables.tests.tsx` — the existing
"shows too-long error when pasting 256 chars" tests are now unreachable
via the DOM cap; converted to `maxLength === 255` assertions.
- `ee/server/service/teams_test.go` — `TestNewTeamNameValidation`,
`TestModifyTeamNameValidation`, and `TestApplyTeamSpecsNameValidation`
each get two new cases (accepts at the limit, rejects one over with the
expected error message).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Limited fleet, team, and other user-entered names and descriptions to
255 characters.
* Replaced database errors for oversized names with clear validation
messages.
* Prevented long fleet and label names from overflowing tables, headers,
modals, and host details.
  * Improved modal and dropdown layouts for long text.
* **Tests**
* Added coverage for character limits, boundary values, and multibyte
names.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
RachelElysia
2026-07-24 11:22:06 -04:00
committed by GitHub
parent 2d4136e0d8
commit 09fea47ce4
37 changed files with 192 additions and 160 deletions
@@ -2,13 +2,13 @@ import React, { useState } from "react";
import selfServiceCategoriesAPI from "services/entities/self_service_categories";
import { hasStatusKey } from "interfaces/errors";
import { MAX_ENTITY_CHAR_LENGTH } from "utilities/constants";
import Button from "components/buttons/Button";
import InputField from "components/forms/fields/InputField";
import Modal from "components/Modal";
const baseClass = "add-category-modal";
const NAME_MAX_LENGTH = 255;
interface IAddCategoryModalProps {
fleetId: number;
@@ -26,9 +26,7 @@ const AddCategoryModal = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const trimmedName = name.trim();
const isInvalid =
trimmedName.length === 0 || trimmedName.length > NAME_MAX_LENGTH;
const isDisabled = isInvalid || isSubmitting;
const isDisabled = trimmedName.length === 0 || isSubmitting;
const onNameChange = (value: string) => {
setName(value);
@@ -74,7 +72,7 @@ const AddCategoryModal = ({
error={error}
autofocus
ignore1password
inputOptions={{ maxLength: NAME_MAX_LENGTH }}
inputOptions={{ maxLength: MAX_ENTITY_CHAR_LENGTH }}
/>
<div className="modal-cta-wrap">
<Button type="submit" disabled={isDisabled} isLoading={isSubmitting}>
@@ -3,13 +3,13 @@ import React, { useState } from "react";
import selfServiceCategoriesAPI from "services/entities/self_service_categories";
import { hasStatusKey } from "interfaces/errors";
import { ISelfServiceCategory } from "interfaces/self_service_category";
import { MAX_ENTITY_CHAR_LENGTH } from "utilities/constants";
import Button from "components/buttons/Button";
import InputField from "components/forms/fields/InputField";
import Modal from "components/Modal";
const baseClass = "edit-category-modal";
const NAME_MAX_LENGTH = 255;
interface IEditCategoryModalProps {
category: ISelfServiceCategory;
@@ -27,10 +27,8 @@ const EditCategoryModal = ({
const [isSubmitting, setIsSubmitting] = useState(false);
const trimmedName = name.trim();
const isInvalid =
trimmedName.length === 0 || trimmedName.length > NAME_MAX_LENGTH;
const isUnchanged = trimmedName === category.name;
const isDisabled = isInvalid || isUnchanged || isSubmitting;
const isDisabled = trimmedName.length === 0 || isUnchanged || isSubmitting;
const onNameChange = (value: string) => {
setName(value);
@@ -75,7 +73,7 @@ const EditCategoryModal = ({
error={error}
autofocus
ignore1password
inputOptions={{ maxLength: NAME_MAX_LENGTH }}
inputOptions={{ maxLength: MAX_ENTITY_CHAR_LENGTH }}
/>
<div className="modal-cta-wrap">
<Button type="submit" disabled={isDisabled} isLoading={isSubmitting}>