mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: display the AI add-on column in the UI on the Users and Organization Members tables (#23291)
## Summary Adds an entitlement-gated **AI add-on** column to both the **Users** table and the **Organization Members** table. When `ai_governance_user_limit` is entitled, each row shows whether the user is consuming an AI seat. ## Background The AI governance add-on tracks which users are consuming AI seats. Admins need visibility into per-user seat consumption directly from the user management tables. This change surfaces that information through both the site-wide Users table and the per-organization Members table, gated behind the `ai_governance_user_limit` entitlement so the column only appears when the feature is licensed. ## Implementation ### Backend - **New SQL query** `GetUserAISeatStates` (`coderd/database/queries/aiseatstate.sql`) — returns user IDs consuming an AI seat, derived from: - Users with entries in `aibridge_interceptions` (AI Bridge usage) - Users who own workspaces with `has_ai_task = true` builds (AI Tasks usage) - **SDK types** — added `has_ai_seat: boolean` to `codersdk.User` and `codersdk.OrganizationMemberWithUserData` - **Handler wiring** — both the Users list endpoint (`coderd/users.go`) and all Members endpoints (`coderd/members.go`) query AI seat state per page of user IDs and populate the response field - **dbauthz** — per-user `ActionRead` checks on `ResourceUserObject` ### Frontend - **Shared `AISeatCell` component** (`site/src/modules/users/AISeatCell.tsx`) — green `CircleCheck` for consuming, gray `X` for non-consuming - **`TableColumnHelpTooltip`** — extended with `ai_addon` variant with tooltip: *"Users with access to AI features like AI Bridge, Boundary, or Tasks who are actively consuming a seat."* - **Column visibility** gated behind `useFeatureVisibility().ai_governance_user_limit` ## Validation - Backend: dbauthz full method suite (`TestMethodTestSuite`) passes including new `GetUserAISeatStates` test - Backend: `TestGetUsers`, `TestUsersFilter`, CLI golden file tests pass - Frontend: 7/7 tests pass across `UsersPage.test.tsx` and `OrganizationMembersPage.test.tsx` (column visibility gating both directions) - `go build ./coderd/...` compiles clean - `pnpm --dir site run lint:types` passes - `make gen` clean ## Risks - **Pagination performance**: The AI seat query is scoped to the current page's user IDs (not a full table scan), keeping it efficient for paginated views. - **Semantic scope**: The workspace-side AI seat derivation uses "any build with `has_ai_task = true`" rather than "latest build only". If the product intent is latest-build-only, this can be tightened in a follow-up. --- _Generated with `mux` • Model: `anthropic:claude-opus-4-6` • Thinking: `xhigh` • Cost: `$27.25`_ <!-- mux-attribution: model=anthropic:claude-opus-4-6 thinking=xhigh costs=27.25 -->
This commit is contained in:
Generated
+51
-46
@@ -36,6 +36,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
||||
"organization_id": "string"
|
||||
}
|
||||
],
|
||||
"has_ai_seat": true,
|
||||
"is_service_account": true,
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "",
|
||||
@@ -68,28 +69,29 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------|------------------------------------------------------|----------|--------------|-------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string | false | | |
|
||||
| `» created_at` | string(date-time) | false | | |
|
||||
| `» email` | string | false | | |
|
||||
| `» global_roles` | array | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string | false | | |
|
||||
| `» is_service_account` | boolean | false | | |
|
||||
| `» last_seen_at` | string(date-time) | false | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» roles` | array | false | | |
|
||||
| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `» updated_at` | string(date-time) | false | | |
|
||||
| `» user_created_at` | string(date-time) | false | | |
|
||||
| `» user_id` | string(uuid) | false | | |
|
||||
| `» user_updated_at` | string(date-time) | false | | |
|
||||
| `» username` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|------------------------|------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» avatar_url` | string | false | | |
|
||||
| `» created_at` | string(date-time) | false | | |
|
||||
| `» email` | string | false | | |
|
||||
| `» global_roles` | array | false | | |
|
||||
| `»» display_name` | string | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string | false | | |
|
||||
| `» has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
|
||||
| `» is_service_account` | boolean | false | | |
|
||||
| `» last_seen_at` | string(date-time) | false | | |
|
||||
| `» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» organization_id` | string(uuid) | false | | |
|
||||
| `» roles` | array | false | | |
|
||||
| `» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `» updated_at` | string(date-time) | false | | |
|
||||
| `» user_created_at` | string(date-time) | false | | |
|
||||
| `» user_id` | string(uuid) | false | | |
|
||||
| `» user_updated_at` | string(date-time) | false | | |
|
||||
| `» username` | string | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
@@ -595,6 +597,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/members
|
||||
"organization_id": "string"
|
||||
}
|
||||
],
|
||||
"has_ai_seat": true,
|
||||
"is_service_account": true,
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "",
|
||||
@@ -802,6 +805,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat
|
||||
"organization_id": "string"
|
||||
}
|
||||
],
|
||||
"has_ai_seat": true,
|
||||
"is_service_account": true,
|
||||
"last_seen_at": "2019-08-24T14:15:22Z",
|
||||
"login_type": "",
|
||||
@@ -836,30 +840,31 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/paginat
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-------------------------|------------------------------------------------------|----------|--------------|-------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» count` | integer | false | | |
|
||||
| `» members` | array | false | | |
|
||||
| `»» avatar_url` | string | false | | |
|
||||
| `»» created_at` | string(date-time) | false | | |
|
||||
| `»» email` | string | false | | |
|
||||
| `»» global_roles` | array | false | | |
|
||||
| `»»» display_name` | string | false | | |
|
||||
| `»»» name` | string | false | | |
|
||||
| `»»» organization_id` | string | false | | |
|
||||
| `»» is_service_account` | boolean | false | | |
|
||||
| `»» last_seen_at` | string(date-time) | false | | |
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string(uuid) | false | | |
|
||||
| `»» roles` | array | false | | |
|
||||
| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `»» updated_at` | string(date-time) | false | | |
|
||||
| `»» user_created_at` | string(date-time) | false | | |
|
||||
| `»» user_id` | string(uuid) | false | | |
|
||||
| `»» user_updated_at` | string(date-time) | false | | |
|
||||
| `»» username` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
|-------------------------|------------------------------------------------------|----------|--------------|--------------------------------------------------------------------------------------------------|
|
||||
| `[array item]` | array | false | | |
|
||||
| `» count` | integer | false | | |
|
||||
| `» members` | array | false | | |
|
||||
| `»» avatar_url` | string | false | | |
|
||||
| `»» created_at` | string(date-time) | false | | |
|
||||
| `»» email` | string | false | | |
|
||||
| `»» global_roles` | array | false | | |
|
||||
| `»»» display_name` | string | false | | |
|
||||
| `»»» name` | string | false | | |
|
||||
| `»»» organization_id` | string | false | | |
|
||||
| `»» has_ai_seat` | boolean | false | | Has ai seat intentionally omits omitempty so the API always includes the field, even when false. |
|
||||
| `»» is_service_account` | boolean | false | | |
|
||||
| `»» last_seen_at` | string(date-time) | false | | |
|
||||
| `»» login_type` | [codersdk.LoginType](schemas.md#codersdklogintype) | false | | |
|
||||
| `»» name` | string | false | | |
|
||||
| `»» organization_id` | string(uuid) | false | | |
|
||||
| `»» roles` | array | false | | |
|
||||
| `»» status` | [codersdk.UserStatus](schemas.md#codersdkuserstatus) | false | | |
|
||||
| `»» updated_at` | string(date-time) | false | | |
|
||||
| `»» user_created_at` | string(date-time) | false | | |
|
||||
| `»» user_id` | string(uuid) | false | | |
|
||||
| `»» user_updated_at` | string(date-time) | false | | |
|
||||
| `»» username` | string | false | | |
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
|
||||
Reference in New Issue
Block a user