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
+10
View File
@@ -10,6 +10,7 @@ import (
"net/http"
"net/url"
"strings"
"unicode/utf8"
"golang.org/x/text/unicode/norm"
@@ -96,6 +97,9 @@ func (svc *Service) NewTeam(ctx context.Context, p fleet.TeamPayload) (*fleet.Te
if *p.Name == "" {
return nil, fleet.NewInvalidArgumentError("name", "may not be empty")
}
if utf8.RuneCountInString(*p.Name) > fleet.MaxTeamNameLength {
return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("may not exceed %d characters", fleet.MaxTeamNameLength))
}
if fleet.IsReservedTeamName(*p.Name) {
return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("%q is a reserved fleet name", *p.Name))
}
@@ -184,6 +188,9 @@ func (svc *Service) ModifyTeam(ctx context.Context, teamID uint, payload fleet.T
if *payload.Name == "" {
return nil, fleet.NewInvalidArgumentError("name", "may not be empty")
}
if utf8.RuneCountInString(*payload.Name) > fleet.MaxTeamNameLength {
return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("may not exceed %d characters", fleet.MaxTeamNameLength))
}
if fleet.IsReservedTeamName(*payload.Name) {
return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("%q is a reserved fleet name", *payload.Name))
}
@@ -1326,6 +1333,9 @@ func (svc *Service) ApplyTeamSpecs(ctx context.Context, specs []*fleet.TeamSpec,
if spec.Name == "" {
return nil, fleet.NewInvalidArgumentError("name", "name may not be empty")
}
if utf8.RuneCountInString(spec.Name) > fleet.MaxTeamNameLength {
return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("may not exceed %d characters", fleet.MaxTeamNameLength))
}
if fleet.IsReservedTeamName(spec.Name) {
return nil, fleet.NewInvalidArgumentError("name", fmt.Sprintf("%q is a reserved fleet name", spec.Name))
}
+48
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"fmt"
"log/slog"
"strings"
"testing"
@@ -134,6 +135,23 @@ func TestNewTeamNameValidation(t *testing.T) {
teamName: ptr.String("Engineering"),
wantName: "Engineering",
},
{
name: "name at max length is accepted",
teamName: new(strings.Repeat("a", fleet.MaxTeamNameLength)),
wantName: strings.Repeat("a", fleet.MaxTeamNameLength),
},
{
name: "name over max length is rejected",
teamName: new(strings.Repeat("a", fleet.MaxTeamNameLength+1)),
wantErr: fmt.Sprintf("may not exceed %d characters", fleet.MaxTeamNameLength),
},
{
// Guards against regressing to byte-based length checks, which
// would reject multibyte names that fit within the character cap.
name: "multibyte name at max character length is accepted",
teamName: new(strings.Repeat("日", fleet.MaxTeamNameLength)),
wantName: strings.Repeat("日", fleet.MaxTeamNameLength),
},
}
for _, tc := range testCases {
@@ -257,6 +275,21 @@ func TestModifyTeamNameValidation(t *testing.T) {
teamName: ptr.String("my team"),
wantName: "my team",
},
{
name: "name at max length is accepted",
teamName: new(strings.Repeat("a", fleet.MaxTeamNameLength)),
wantName: strings.Repeat("a", fleet.MaxTeamNameLength),
},
{
name: "name over max length is rejected",
teamName: new(strings.Repeat("a", fleet.MaxTeamNameLength+1)),
wantErr: fmt.Sprintf("may not exceed %d characters", fleet.MaxTeamNameLength),
},
{
name: "multibyte name at max character length is accepted",
teamName: new(strings.Repeat("日", fleet.MaxTeamNameLength)),
wantName: strings.Repeat("日", fleet.MaxTeamNameLength),
},
}
for _, tc := range testCases {
@@ -367,6 +400,21 @@ func TestApplyTeamSpecsNameValidation(t *testing.T) {
teamName: " Engineering ",
wantName: "Engineering",
},
{
name: "name at max length is accepted",
teamName: strings.Repeat("a", fleet.MaxTeamNameLength),
wantName: strings.Repeat("a", fleet.MaxTeamNameLength),
},
{
name: "name over max length is rejected",
teamName: strings.Repeat("a", fleet.MaxTeamNameLength+1),
wantErr: fmt.Sprintf("may not exceed %d characters", fleet.MaxTeamNameLength),
},
{
name: "multibyte name at max character length is accepted",
teamName: strings.Repeat("日", fleet.MaxTeamNameLength),
wantName: strings.Repeat("日", fleet.MaxTeamNameLength),
},
}
for _, tc := range testCases {