API + auth + UI changes for team labels - #37208
Conversation
Turns out, we have a lot to unwind here so this doesn't compile yet
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #37208 +/- ##
==========================================
- Coverage 65.88% 65.87% -0.02%
==========================================
Files 2360 2361 +1
Lines 187285 187383 +98
Branches 8006 8017 +11
==========================================
+ Hits 123402 123441 +39
- Misses 52605 52645 +40
- Partials 11278 11297 +19
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Zed + Opus 4.5. Prompt below: Try to run `make test` and fix compile errors where the interface no longer matches the implementation until the compile completes. Don't change the associated interfaces, and use commit 5ad3c18 as a guide for the sort of changes that need to be made.
Courtesy Junie. Prompt: Using PlatformField.tsx as a pattern, give me a text field component (not an input, not editable) for "Team name". Include this in both ManualLabelForm and DynamicLabelForm, showing the component only if a new teamName prop (nullable string, required) is truthy. Pass this property all the way from LabelForm. There, add team_name to ILabel as an optional nullable string, and pass that to the DynamicLabelForm and ManualLabelForm components. TODO: A bunch of manual fixes, but figured I'd split those into a commit to show what's mine/what the robot did.
…scription, include teams reference
Backend isn't built here yet
… controls on label edit page and manage labels action menu
Used Zed + Opus 4.5 with the prompt: I'm getting test failures on frontend tests (`make test-js`) because I'm pretty sure real server requests are leaking through HTTP mocks. Fix the issues. It made some other changes that weren't relevant for fixing the problem, so I reverted those prior to committing.
|
@lucasmrod Going to stack additional tests in another PR since this one's massive. Added TODOs on where I believe we want more test coverage. Adding that checklist to the parent issue momentarily. |
lucasmrod
left a comment
There was a problem hiding this comment.
Overall LGTM!
Publishing review while I review test code.
| action == create | ||
| } | ||
|
|
||
| # Team admins, maintainers, and gitops can create global labels |
There was a problem hiding this comment.
(Not something to fix now) At some point in the future we need to revisit this. Team folks should not be able to configure queries that run on ALL hosts.
There was a problem hiding this comment.
I think we have visibility checks elsewhere to avoid this, maybe?
| return nil | ||
| } | ||
|
|
||
| // Get the user from the context. |
There was a problem hiding this comment.
Fail early if !ok || user == nil || user.User == nil?
There was a problem hiding this comment.
The previous code let user-less viewers through, so I assumed we needed to keep that behavior. But looking at code paths again, it doesn't look like we ever actually call this somewhere we don't have a ViewerContext injected, so fair enough.
| if hasWriteRoleAnywhere() && label.AuthorID != nil && *label.AuthorID == user.ID { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Why check authorship of a team label?
There was a problem hiding this comment.
For consistency, I suppose. But I'm fine with nuking this, since label authorship editability is a holdover from shimming team-ish functionality into global labels.
| rows, err := tx.QueryContext(ctx, hostTeamCheckSql, args...) | ||
| if err != nil { | ||
| return ctxerr.Wrap(ctx, err, "execute host team membership check query") | ||
| } | ||
|
|
||
| rows.Next() | ||
| var hostCountOnWrongTeam int | ||
| if err := rows.Scan(&hostCountOnWrongTeam); err != nil { | ||
| return ctxerr.Wrap(ctx, err, "check host team membership") | ||
| } | ||
| if err := rows.Err(); err != nil { | ||
| return ctxerr.Wrap(ctx, err, "check host team membership") | ||
| } | ||
| if err := rows.Close(); err != nil { //nolint:sqlclosecheck | ||
| return ctxerr.Wrap(ctx, err, "close result set for host team membership") | ||
| } |
There was a problem hiding this comment.
| rows, err := tx.QueryContext(ctx, hostTeamCheckSql, args...) | |
| if err != nil { | |
| return ctxerr.Wrap(ctx, err, "execute host team membership check query") | |
| } | |
| rows.Next() | |
| var hostCountOnWrongTeam int | |
| if err := rows.Scan(&hostCountOnWrongTeam); err != nil { | |
| return ctxerr.Wrap(ctx, err, "check host team membership") | |
| } | |
| if err := rows.Err(); err != nil { | |
| return ctxerr.Wrap(ctx, err, "check host team membership") | |
| } | |
| if err := rows.Close(); err != nil { //nolint:sqlclosecheck | |
| return ctxerr.Wrap(ctx, err, "close result set for host team membership") | |
| } | |
| var hostCountOnWrongTeam int | |
| if err := tx.QueryRowxContext(ctx, hostTeamCheckSql, args...).Scan(&hostCountOnWrongTeam); err != nil { | |
| return ctxerr.Wrap(ctx, err, "execute host team membership check query") | |
| } |
There was a problem hiding this comment.
Thanks for calling this out. Somehow missed that QueryRowxContext was a thing.
| SELECT l.*, | ||
| (SELECT COUNT(1) | ||
| FROM label_membership lm | ||
| JOIN hosts h ON (lm.host_id = h.id) WHERE label_id = l.id AND %s |
There was a problem hiding this comment.
Nit:
| JOIN hosts h ON (lm.host_id = h.id) WHERE label_id = l.id AND %s | |
| JOIN hosts h ON (lm.host_id = h.id) WHERE lm.label_id = l.id AND %s |
| } | ||
|
|
||
| func verifyLabelsToAssociate(ctx context.Context, ds fleet.Datastore, entityTeamID *uint, labelNames []string) error { | ||
| func verifyLabelsToAssociate(ctx context.Context, ds fleet.Datastore, entityTeamID *uint, labelNames []string, user *fleet.User) error { |
There was a problem hiding this comment.
Let's return an error early if user == nil.
There was a problem hiding this comment.
Fair enough. Will see if tests break on this, given that there are a fair number of paths that land here and ViewerContext doesn't guarantee a user.
There was a problem hiding this comment.
See TODOs (linking for referencing in future PRs)
| } catch (error) { | ||
| renderFlash( | ||
| "error", | ||
| (error as { status: number }).status === 409 |
There was a problem hiding this comment.
I'm wondering if this 409 check might now or in the future return false negatives?
I think we're trying to use a better error handling pattern such as:
const duplicateEntryReason = getErrorReason(err, {
reasonIncludes: "Duplicate entry",
});
There was a problem hiding this comment.
Status code should be fully reliable here, and tbh is preferable to a substring match on error message contents. For a generic 400/422, given that we don't hand back error codes, I get why we're doing substring matches, but this kinda goes back to what I mentioned in ~August of last year about having error messages that we don't have to substring to decipher.
| (isTeamAdmin(currentUser, label.team_id) || | ||
| isTeamMaintainer(currentUser, label.team_id))) | ||
| ); | ||
| }; |
| <LabelForm | ||
| defaultName={defaultName} | ||
| defaultDescription={defaultDescription} | ||
| teamName={teamName} |
There was a problem hiding this comment.
Just confirming, this feature is not supporting adding teams to labels via the UI, only via gitops, but can view that it's a team label in the UI, correct? @iansltx
There was a problem hiding this comment.
That's correct. Editing team labels is in scope, creating them is not. Team is also an immutable property, and will remain immutable when we add the ability to add team labels via the UI, at least for now, since there are so many things that have to be recalculated if moving a label from one team to another (making a team specific label global is less painful).
| readOnly={isEditing} | ||
| onLoad={onLoad} | ||
| wrapperClassName={`${baseClass}__text-editor-wrapper form-field`} | ||
| helpText={isEditing ? IMMUTABLE_QUERY_HELP_TEXT : ""} |
There was a problem hiding this comment.
I'm not quite sure this was removed on purpose? This branch has the query immutable, but it looks like you can change it with no help text saying otherwise that it's read only.
Screen.Recording.2025-12-29.at.1.42.12.PM.mov
There was a problem hiding this comment.
Nm I see it's up top.
Maybe quick iteration to make this look disabled, also it's missing the copy button. Don't want to block this PR for those improvements cc: design @marko-lisica
There was a problem hiding this comment.
Label query editability bug is preexisting; just checked Dogfood. Definitely missed that when clicking around when testing this though.
Re: help text, the help text is right below the description, which matches this Figma.
There was a problem hiding this comment.
#34124 actually covers the missing disabled state on the query editor.
| .slice(0, -1) | ||
| .join(", ")} and ${immutableFields.pop()} ${SUFFIX}`; | ||
| }; | ||
|
|
RachelElysia
left a comment
There was a problem hiding this comment.
Approving so we won't need to re-review from BE, but will need an immediate FE followup PR to address this copy bug:
https://github.com/fleetdm/fleet/pull/37208/files#r2651557303
|
Building follow-up PRs (split into BE/FE) to address feedback. Merging this as tests currently pass, and per earlier discussion today. Once additional feedback is in I'll create a cherry-pick into 4.79RC with everything. |

Covers #36760, #36758.
Checklist for submitter
If some of the following don't apply, delete the relevant line.
Changes file added for user-visible changes in
changes/,orbit/changes/oree/fleetd-chrome/changes.See Changes files for more information.
Input data is properly validated,
SELECT *is avoided, SQL injection is prevented (using placeholders for values in statements)Testing
Added/updated automated tests
Where appropriate, automated tests simulate multiple hosts and test for host isolation (updates to one hosts's records do not affect another)
QA'd all new/changed functionality manually