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:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user