diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index d5ccfb06dfc47..e7709c9ed353b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7263,6 +7263,9 @@ const docTemplate = `{ "avatar_url": { "type": "string" }, + "display_name": { + "type": "string" + }, "name": { "type": "string" }, @@ -8247,6 +8250,9 @@ const docTemplate = `{ "avatar_url": { "type": "string" }, + "display_name": { + "type": "string" + }, "id": { "type": "string", "format": "uuid" diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 69b3e1f6a5453..a033cab29da1a 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -6471,6 +6471,9 @@ "avatar_url": { "type": "string" }, + "display_name": { + "type": "string" + }, "name": { "type": "string" }, @@ -7403,6 +7406,9 @@ "avatar_url": { "type": "string" }, + "display_name": { + "type": "string" + }, "id": { "type": "string", "format": "uuid" diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 8e29bae4349fb..626e8336719d4 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -3402,6 +3402,7 @@ func (q *FakeQuerier) InsertAllUsersGroup(ctx context.Context, orgID uuid.UUID) return q.InsertGroup(ctx, database.InsertGroupParams{ ID: orgID, Name: database.AllUsersGroup, + DisplayName: "", OrganizationID: orgID, }) } @@ -3521,6 +3522,7 @@ func (q *FakeQuerier) InsertGroup(_ context.Context, arg database.InsertGroupPar group := database.Group{ ID: arg.ID, Name: arg.Name, + DisplayName: arg.DisplayName, OrganizationID: arg.OrganizationID, AvatarURL: arg.AvatarURL, QuotaAllowance: arg.QuotaAllowance, @@ -4327,6 +4329,7 @@ func (q *FakeQuerier) UpdateGroupByID(_ context.Context, arg database.UpdateGrou for i, group := range q.groups { if group.ID == arg.ID { + group.DisplayName = arg.DisplayName group.Name = arg.Name group.AvatarURL = arg.AvatarURL group.QuotaAllowance = arg.QuotaAllowance diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 6b123cf4f3677..35e6986849bac 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -272,9 +272,11 @@ func OrganizationMember(t testing.TB, db database.Store, orig database.Organizat } func Group(t testing.TB, db database.Store, orig database.Group) database.Group { + name := takeFirst(orig.Name, namesgenerator.GetRandomName(1)) group, err := db.InsertGroup(genCtx, database.InsertGroupParams{ ID: takeFirst(orig.ID, uuid.New()), - Name: takeFirst(orig.Name, namesgenerator.GetRandomName(1)), + Name: name, + DisplayName: takeFirst(orig.DisplayName, name), OrganizationID: takeFirst(orig.OrganizationID, uuid.New()), AvatarURL: takeFirst(orig.AvatarURL, "https://logo.example.com"), QuotaAllowance: takeFirst(orig.QuotaAllowance, 0), diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 0bf80bbfb536a..aea6fcda116de 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -295,9 +295,12 @@ CREATE TABLE groups ( name text NOT NULL, organization_id uuid NOT NULL, avatar_url text DEFAULT ''::text NOT NULL, - quota_allowance integer DEFAULT 0 NOT NULL + quota_allowance integer DEFAULT 0 NOT NULL, + display_name text DEFAULT ''::text NOT NULL ); +COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.'; + CREATE TABLE licenses ( id integer NOT NULL, uploaded_at timestamp with time zone NOT NULL, diff --git a/coderd/database/migrations/000144_group_display_name.down.sql b/coderd/database/migrations/000144_group_display_name.down.sql new file mode 100644 index 0000000000000..b04850449fedc --- /dev/null +++ b/coderd/database/migrations/000144_group_display_name.down.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE groups + DROP COLUMN display_name; + +COMMIT; diff --git a/coderd/database/migrations/000144_group_display_name.up.sql b/coderd/database/migrations/000144_group_display_name.up.sql new file mode 100644 index 0000000000000..a812ad8aa34c3 --- /dev/null +++ b/coderd/database/migrations/000144_group_display_name.up.sql @@ -0,0 +1,8 @@ +BEGIN; + +ALTER TABLE groups + ADD COLUMN display_name TEXT NOT NULL DEFAULT ''; + +COMMENT ON COLUMN groups.display_name IS 'Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string.'; + +COMMIT; diff --git a/coderd/database/models.go b/coderd/database/models.go index 58083303b05d7..6af46e2d57126 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -1492,6 +1492,8 @@ type Group struct { OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` AvatarURL string `db:"avatar_url" json:"avatar_url"` QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` + // Display name is a custom, human-friendly group name that user can set. This is not required to be unique and can be the empty string. + DisplayName string `db:"display_name" json:"display_name"` } type GroupMember struct { diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 87d86e9621fbb..d875697aa68ce 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1180,7 +1180,7 @@ func (q *sqlQuerier) DeleteGroupByID(ctx context.Context, id uuid.UUID) error { const getGroupByID = `-- name: GetGroupByID :one SELECT - id, name, organization_id, avatar_url, quota_allowance + id, name, organization_id, avatar_url, quota_allowance, display_name FROM groups WHERE @@ -1198,13 +1198,14 @@ func (q *sqlQuerier) GetGroupByID(ctx context.Context, id uuid.UUID) (Group, err &i.OrganizationID, &i.AvatarURL, &i.QuotaAllowance, + &i.DisplayName, ) return i, err } const getGroupByOrgAndName = `-- name: GetGroupByOrgAndName :one SELECT - id, name, organization_id, avatar_url, quota_allowance + id, name, organization_id, avatar_url, quota_allowance, display_name FROM groups WHERE @@ -1229,13 +1230,14 @@ func (q *sqlQuerier) GetGroupByOrgAndName(ctx context.Context, arg GetGroupByOrg &i.OrganizationID, &i.AvatarURL, &i.QuotaAllowance, + &i.DisplayName, ) return i, err } const getGroupsByOrganizationID = `-- name: GetGroupsByOrganizationID :many SELECT - id, name, organization_id, avatar_url, quota_allowance + id, name, organization_id, avatar_url, quota_allowance, display_name FROM groups WHERE @@ -1259,6 +1261,7 @@ func (q *sqlQuerier) GetGroupsByOrganizationID(ctx context.Context, organization &i.OrganizationID, &i.AvatarURL, &i.QuotaAllowance, + &i.DisplayName, ); err != nil { return nil, err } @@ -1280,7 +1283,7 @@ INSERT INTO groups ( organization_id ) VALUES - ($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance + ($1, 'Everyone', $1) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name ` // We use the organization_id as the id @@ -1295,6 +1298,7 @@ func (q *sqlQuerier) InsertAllUsersGroup(ctx context.Context, organizationID uui &i.OrganizationID, &i.AvatarURL, &i.QuotaAllowance, + &i.DisplayName, ) return i, err } @@ -1303,17 +1307,19 @@ const insertGroup = `-- name: InsertGroup :one INSERT INTO groups ( id, name, + display_name, organization_id, avatar_url, quota_allowance ) VALUES - ($1, $2, $3, $4, $5) RETURNING id, name, organization_id, avatar_url, quota_allowance + ($1, $2, $3, $4, $5, $6) RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name ` type InsertGroupParams struct { ID uuid.UUID `db:"id" json:"id"` Name string `db:"name" json:"name"` + DisplayName string `db:"display_name" json:"display_name"` OrganizationID uuid.UUID `db:"organization_id" json:"organization_id"` AvatarURL string `db:"avatar_url" json:"avatar_url"` QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` @@ -1323,6 +1329,7 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr row := q.db.QueryRowContext(ctx, insertGroup, arg.ID, arg.Name, + arg.DisplayName, arg.OrganizationID, arg.AvatarURL, arg.QuotaAllowance, @@ -1334,6 +1341,7 @@ func (q *sqlQuerier) InsertGroup(ctx context.Context, arg InsertGroupParams) (Gr &i.OrganizationID, &i.AvatarURL, &i.QuotaAllowance, + &i.DisplayName, ) return i, err } @@ -1343,15 +1351,17 @@ UPDATE groups SET name = $1, - avatar_url = $2, - quota_allowance = $3 + display_name = $2, + avatar_url = $3, + quota_allowance = $4 WHERE - id = $4 -RETURNING id, name, organization_id, avatar_url, quota_allowance + id = $5 +RETURNING id, name, organization_id, avatar_url, quota_allowance, display_name ` type UpdateGroupByIDParams struct { Name string `db:"name" json:"name"` + DisplayName string `db:"display_name" json:"display_name"` AvatarURL string `db:"avatar_url" json:"avatar_url"` QuotaAllowance int32 `db:"quota_allowance" json:"quota_allowance"` ID uuid.UUID `db:"id" json:"id"` @@ -1360,6 +1370,7 @@ type UpdateGroupByIDParams struct { func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDParams) (Group, error) { row := q.db.QueryRowContext(ctx, updateGroupByID, arg.Name, + arg.DisplayName, arg.AvatarURL, arg.QuotaAllowance, arg.ID, @@ -1371,6 +1382,7 @@ func (q *sqlQuerier) UpdateGroupByID(ctx context.Context, arg UpdateGroupByIDPar &i.OrganizationID, &i.AvatarURL, &i.QuotaAllowance, + &i.DisplayName, ) return i, err } diff --git a/coderd/database/queries/groups.sql b/coderd/database/queries/groups.sql index a80a4c22567bb..e1ee6635a5fe0 100644 --- a/coderd/database/queries/groups.sql +++ b/coderd/database/queries/groups.sql @@ -34,12 +34,13 @@ AND INSERT INTO groups ( id, name, + display_name, organization_id, avatar_url, quota_allowance ) VALUES - ($1, $2, $3, $4, $5) RETURNING *; + ($1, $2, $3, $4, $5, $6) RETURNING *; -- We use the organization_id as the id -- for simplicity since all users is @@ -57,11 +58,12 @@ VALUES UPDATE groups SET - name = $1, - avatar_url = $2, - quota_allowance = $3 + name = @name, + display_name = @display_name, + avatar_url = @avatar_url, + quota_allowance = @quota_allowance WHERE - id = $4 + id = @id RETURNING *; -- name: DeleteGroupByID :exec diff --git a/codersdk/groups.go b/codersdk/groups.go index 3d9495eb074c2..c04267e4e0eb2 100644 --- a/codersdk/groups.go +++ b/codersdk/groups.go @@ -12,6 +12,7 @@ import ( type CreateGroupRequest struct { Name string `json:"name"` + DisplayName string `json:"display_name"` AvatarURL string `json:"avatar_url"` QuotaAllowance int `json:"quota_allowance"` } @@ -19,6 +20,7 @@ type CreateGroupRequest struct { type Group struct { ID uuid.UUID `json:"id" format:"uuid"` Name string `json:"name"` + DisplayName string `json:"display_name"` OrganizationID uuid.UUID `json:"organization_id" format:"uuid"` Members []User `json:"members"` AvatarURL string `json:"avatar_url"` @@ -98,6 +100,7 @@ type PatchGroupRequest struct { AddUsers []string `json:"add_users"` RemoveUsers []string `json:"remove_users"` Name string `json:"name"` + DisplayName *string `json:"display_name"` AvatarURL *string `json:"avatar_url"` QuotaAllowance *int `json:"quota_allowance"` } diff --git a/docs/admin/audit-logs.md b/docs/admin/audit-logs.md index 27ccbb763ab2a..9c29dbeee85d6 100644 --- a/docs/admin/audit-logs.md +++ b/docs/admin/audit-logs.md @@ -13,7 +13,7 @@ We track the following resources: | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | APIKey
login, logout, register, create, delete |
FieldTracked
created_attrue
expires_attrue
hashed_secretfalse
idfalse
ip_addressfalse
last_usedtrue
lifetime_secondsfalse
login_typefalse
scopefalse
token_namefalse
updated_atfalse
user_idtrue
| | AuditOAuthConvertState
|
FieldTracked
created_attrue
expires_attrue
from_login_typetrue
to_login_typetrue
user_idtrue
| -| Group
create, write, delete |
FieldTracked
avatar_urltrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| +| Group
create, write, delete |
FieldTracked
avatar_urltrue
display_nametrue
idtrue
memberstrue
nametrue
organization_idfalse
quota_allowancetrue
| | GitSSHKey
create |
FieldTracked
created_atfalse
private_keytrue
public_keytrue
updated_atfalse
user_idtrue
| | License
create, delete |
FieldTracked
exptrue
idfalse
jwtfalse
uploaded_attrue
uuidtrue
| | Template
write, delete |
FieldTracked
active_version_idtrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_usernamefalse
default_ttltrue
deletedfalse
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
inactivity_ttltrue
locked_ttltrue
max_ttltrue
nametrue
organization_idfalse
provisionertrue
restart_requirement_days_of_weektrue
restart_requirement_weekstrue
updated_atfalse
user_acltrue
| diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md index 03610ba53651f..244487a5a73ce 100644 --- a/docs/api/enterprise.md +++ b/docs/api/enterprise.md @@ -174,6 +174,7 @@ curl -X GET http://coder-server:8080/api/v2/groups/{group} \ ```json { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -234,6 +235,7 @@ curl -X DELETE http://coder-server:8080/api/v2/groups/{group} \ ```json { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -294,6 +296,7 @@ curl -X PATCH http://coder-server:8080/api/v2/groups/{group} \ ```json { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -429,6 +432,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups [ { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -470,6 +474,7 @@ Status Code **200** | --------------------- | ---------------------------------------------------- | -------- | ------------ | ----------- | | `[array item]` | array | false | | | | `» avatar_url` | string | false | | | +| `» display_name` | string | false | | | | `» id` | string(uuid) | false | | | | `» members` | array | false | | | | `»» avatar_url` | string(uri) | false | | | @@ -521,6 +526,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups ```json { "avatar_url": "string", + "display_name": "string", "name": "string", "quota_allowance": 0 } @@ -540,6 +546,7 @@ curl -X POST http://coder-server:8080/api/v2/organizations/{organization}/groups ```json { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -601,6 +608,7 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/groups/ ```json { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -1171,6 +1179,7 @@ curl -X GET http://coder-server:8080/api/v2/templates/{template}/acl/available \ "groups": [ { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -1234,6 +1243,7 @@ Status Code **200** | `[array item]` | array | false | | | | `» groups` | array | false | | | | `»» avatar_url` | string | false | | | +| `»» display_name` | string | false | | | | `»» id` | string(uuid) | false | | | | `»» members` | array | false | | | | `»»» avatar_url` | string(uri) | false | | | diff --git a/docs/api/schemas.md b/docs/api/schemas.md index b9d5e3d1b78a1..ae1a2c7700750 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -765,6 +765,7 @@ "groups": [ { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -1418,6 +1419,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { "avatar_url": "string", + "display_name": "string", "name": "string", "quota_allowance": 0 } @@ -1428,6 +1430,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | Name | Type | Required | Restrictions | Description | | ----------------- | ------- | -------- | ------------ | ----------- | | `avatar_url` | string | false | | | +| `display_name` | string | false | | | | `name` | string | false | | | | `quota_allowance` | integer | false | | | @@ -2930,6 +2933,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in ```json { "avatar_url": "string", + "display_name": "string", "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", "members": [ { @@ -2961,6 +2965,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | Name | Type | Required | Restrictions | Description | | ----------------- | --------------------------------------- | -------- | ------------ | ----------- | | `avatar_url` | string | false | | | +| `display_name` | string | false | | | | `id` | string | false | | | | `members` | array of [codersdk.User](#codersdkuser) | false | | | | `name` | string | false | | | diff --git a/docs/cli/groups_create.md b/docs/cli/groups_create.md index 5158cf20dd094..dd51ed7233a9a 100644 --- a/docs/cli/groups_create.md +++ b/docs/cli/groups_create.md @@ -20,3 +20,12 @@ coder groups create [flags] | Environment | $CODER_AVATAR_URL | Set an avatar for a group. + +### --display-name + +| | | +| ----------- | -------------------------------- | +| Type | string | +| Environment | $CODER_DISPLAY_NAME | + +Optional human friendly name for the group. diff --git a/docs/cli/groups_edit.md b/docs/cli/groups_edit.md index 8de7b567bf949..da8788806367a 100644 --- a/docs/cli/groups_edit.md +++ b/docs/cli/groups_edit.md @@ -28,6 +28,15 @@ Add users to the group. Accepts emails or IDs. Update the group avatar. +### --display-name + +| | | +| ----------- | -------------------------------- | +| Type | string | +| Environment | $CODER_DISPLAY_NAME | + +Optional human friendly name for the group. + ### -n, --name | | | diff --git a/docs/cli/groups_list.md b/docs/cli/groups_list.md index 3766de273abf4..5f9e184f3995d 100644 --- a/docs/cli/groups_list.md +++ b/docs/cli/groups_list.md @@ -14,12 +14,12 @@ coder groups list [flags] ### -c, --column -| | | -| ------- | ---------------------------------------------------- | -| Type | string-array | -| Default | name,organization id,members,avatar url | +| | | +| ------- | ----------------------------------------------------------------- | +| Type | string-array | +| Default | name,display name,organization id,members,avatar url | -Columns to display in table output. Available columns: name, organization id, members, avatar url. +Columns to display in table output. Available columns: name, display name, organization id, members, avatar url. ### -o, --output diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index cea72f7c703cb..c93557e52cd79 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -151,6 +151,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ &database.AuditableGroup{}: { "id": ActionTrack, "name": ActionTrack, + "display_name": ActionTrack, "organization_id": ActionIgnore, // Never changes. "avatar_url": ActionTrack, "quota_allowance": ActionTrack, diff --git a/enterprise/cli/groupcreate.go b/enterprise/cli/groupcreate.go index c7c63d469ad0c..d80c1d441e69d 100644 --- a/enterprise/cli/groupcreate.go +++ b/enterprise/cli/groupcreate.go @@ -12,7 +12,11 @@ import ( ) func (r *RootCmd) groupCreate() *clibase.Cmd { - var avatarURL string + var ( + avatarURL string + displayName string + ) + client := new(codersdk.Client) cmd := &clibase.Cmd{ Use: "create ", @@ -30,8 +34,9 @@ func (r *RootCmd) groupCreate() *clibase.Cmd { } group, err := client.CreateGroup(ctx, org.ID, codersdk.CreateGroupRequest{ - Name: inv.Args[0], - AvatarURL: avatarURL, + Name: inv.Args[0], + DisplayName: displayName, + AvatarURL: avatarURL, }) if err != nil { return xerrors.Errorf("create group: %w", err) @@ -50,6 +55,12 @@ func (r *RootCmd) groupCreate() *clibase.Cmd { Env: "CODER_AVATAR_URL", Value: clibase.StringOf(&avatarURL), }, + { + Flag: "display-name", + Description: `Optional human friendly name for the group.`, + Env: "CODER_DISPLAY_NAME", + Value: clibase.StringOf(&displayName), + }, } return cmd diff --git a/enterprise/cli/groupedit.go b/enterprise/cli/groupedit.go index 0b8abae1dad0e..9ef6a3100658d 100644 --- a/enterprise/cli/groupedit.go +++ b/enterprise/cli/groupedit.go @@ -15,10 +15,11 @@ import ( func (r *RootCmd) groupEdit() *clibase.Cmd { var ( - avatarURL string - name string - addUsers []string - rmUsers []string + avatarURL string + name string + displayName string + addUsers []string + rmUsers []string ) client := new(codersdk.Client) cmd := &clibase.Cmd{ @@ -52,6 +53,10 @@ func (r *RootCmd) groupEdit() *clibase.Cmd { req.AvatarURL = &avatarURL } + if inv.ParsedFlags().Lookup("display-name").Changed { + req.DisplayName = &displayName + } + userRes, err := client.Users(ctx, codersdk.UsersRequest{}) if err != nil { return xerrors.Errorf("get users: %w", err) @@ -90,6 +95,12 @@ func (r *RootCmd) groupEdit() *clibase.Cmd { Description: "Update the group avatar.", Value: clibase.StringOf(&avatarURL), }, + { + Flag: "display-name", + Description: `Optional human friendly name for the group.`, + Env: "CODER_DISPLAY_NAME", + Value: clibase.StringOf(&displayName), + }, { Flag: "add-users", FlagShorthand: "a", diff --git a/enterprise/cli/grouplist.go b/enterprise/cli/grouplist.go index f164589c49e21..51c36c4ba8ac9 100644 --- a/enterprise/cli/grouplist.go +++ b/enterprise/cli/grouplist.go @@ -67,6 +67,7 @@ type groupTableRow struct { // For table output: Name string `json:"-" table:"name,default_sort"` + DisplayName string `json:"-" table:"display_name"` OrganizationID uuid.UUID `json:"-" table:"organization_id"` Members []string `json:"-" table:"members"` AvatarURL string `json:"-" table:"avatar_url"` @@ -81,6 +82,7 @@ func groupsToRows(groups ...codersdk.Group) []groupTableRow { } rows = append(rows, groupTableRow{ Name: group.Name, + DisplayName: group.DisplayName, OrganizationID: group.OrganizationID, AvatarURL: group.AvatarURL, Members: members, diff --git a/enterprise/cli/testdata/coder_groups_create_--help.golden b/enterprise/cli/testdata/coder_groups_create_--help.golden index 1cc883ab83aa0..d661a25b8740e 100644 --- a/enterprise/cli/testdata/coder_groups_create_--help.golden +++ b/enterprise/cli/testdata/coder_groups_create_--help.golden @@ -6,5 +6,8 @@ Create a user group -u, --avatar-url string, $CODER_AVATAR_URL Set an avatar for a group. + --display-name string, $CODER_DISPLAY_NAME + Optional human friendly name for the group. + --- Run `coder --help` for a list of global options. diff --git a/enterprise/cli/testdata/coder_groups_edit_--help.golden b/enterprise/cli/testdata/coder_groups_edit_--help.golden index a265ef521508b..b5743bc4fc260 100644 --- a/enterprise/cli/testdata/coder_groups_edit_--help.golden +++ b/enterprise/cli/testdata/coder_groups_edit_--help.golden @@ -9,6 +9,9 @@ Edit a user group -u, --avatar-url string Update the group avatar. + --display-name string, $CODER_DISPLAY_NAME + Optional human friendly name for the group. + -n, --name string Update the group name. diff --git a/enterprise/cli/testdata/coder_groups_list_--help.golden b/enterprise/cli/testdata/coder_groups_list_--help.golden index 99c20f5be3c9a..fafa537205fad 100644 --- a/enterprise/cli/testdata/coder_groups_list_--help.golden +++ b/enterprise/cli/testdata/coder_groups_list_--help.golden @@ -3,9 +3,9 @@ Usage: coder groups list [flags] List user groups Options - -c, --column string-array (default: name,organization id,members,avatar url) - Columns to display in table output. Available columns: name, - organization id, members, avatar url. + -c, --column string-array (default: name,display name,organization id,members,avatar url) + Columns to display in table output. Available columns: name, display + name, organization id, members, avatar url. -o, --output string (default: table) Output format. Available formats: table, json. diff --git a/enterprise/coderd/groups.go b/enterprise/coderd/groups.go index 79fba137f81e8..b6f126e1f62e0 100644 --- a/enterprise/coderd/groups.go +++ b/enterprise/coderd/groups.go @@ -56,6 +56,7 @@ func (api *API) postGroupByOrganization(rw http.ResponseWriter, r *http.Request) group, err := api.Database.InsertGroup(ctx, database.InsertGroupParams{ ID: uuid.New(), Name: req.Name, + DisplayName: req.DisplayName, OrganizationID: org.ID, AvatarURL: req.AvatarURL, QuotaAllowance: int32(req.QuotaAllowance), @@ -177,6 +178,7 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { ID: group.ID, AvatarURL: group.AvatarURL, Name: group.Name, + DisplayName: group.DisplayName, QuotaAllowance: group.QuotaAllowance, } @@ -190,6 +192,9 @@ func (api *API) patchGroup(rw http.ResponseWriter, r *http.Request) { if req.QuotaAllowance != nil { updateGroupParams.QuotaAllowance = int32(*req.QuotaAllowance) } + if req.DisplayName != nil { + updateGroupParams.DisplayName = *req.DisplayName + } group, err = tx.UpdateGroupByID(ctx, updateGroupParams) if err != nil { @@ -395,9 +400,11 @@ func convertGroup(g database.Group, users []database.User) codersdk.Group { for _, user := range users { orgs[user.ID] = []uuid.UUID{g.OrganizationID} } + return codersdk.Group{ ID: g.ID, Name: g.Name, + DisplayName: g.DisplayName, OrganizationID: g.OrganizationID, AvatarURL: g.AvatarURL, QuotaAllowance: int(g.QuotaAllowance), diff --git a/enterprise/coderd/groups_test.go b/enterprise/coderd/groups_test.go index 322ee17c36cd4..5999fa47b1f6d 100644 --- a/enterprise/coderd/groups_test.go +++ b/enterprise/coderd/groups_test.go @@ -37,6 +37,7 @@ func TestCreateGroup(t *testing.T) { require.Equal(t, "hi", group.Name) require.Equal(t, "https://example.com", group.AvatarURL) require.Empty(t, group.Members) + require.Empty(t, group.DisplayName) require.NotEqual(t, uuid.Nil.String(), group.ID.String()) }) @@ -124,11 +125,13 @@ func TestPatchGroup(t *testing.T) { codersdk.FeatureTemplateRBAC: 1, }, }}) + const displayName = "foobar" ctx := testutil.Context(t, testutil.WaitLong) group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ Name: "hi", AvatarURL: "https://example.com", QuotaAllowance: 10, + DisplayName: "", }) require.NoError(t, err) require.Equal(t, 10, group.QuotaAllowance) @@ -137,8 +140,41 @@ func TestPatchGroup(t *testing.T) { Name: "bye", AvatarURL: ptr.Ref("https://google.com"), QuotaAllowance: ptr.Ref(20), + DisplayName: ptr.Ref(displayName), }) require.NoError(t, err) + require.Equal(t, displayName, group.DisplayName) + require.Equal(t, "bye", group.Name) + require.Equal(t, "https://google.com", group.AvatarURL) + require.Equal(t, 20, group.QuotaAllowance) + }) + + t.Run("DisplayNameUnchanged", func(t *testing.T) { + t.Parallel() + + client, user := coderdenttest.New(t, &coderdenttest.Options{LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{ + codersdk.FeatureTemplateRBAC: 1, + }, + }}) + const displayName = "foobar" + ctx := testutil.Context(t, testutil.WaitLong) + group, err := client.CreateGroup(ctx, user.OrganizationID, codersdk.CreateGroupRequest{ + Name: "hi", + AvatarURL: "https://example.com", + QuotaAllowance: 10, + DisplayName: displayName, + }) + require.NoError(t, err) + require.Equal(t, 10, group.QuotaAllowance) + + group, err = client.PatchGroup(ctx, group.ID, codersdk.PatchGroupRequest{ + Name: "bye", + AvatarURL: ptr.Ref("https://google.com"), + QuotaAllowance: ptr.Ref(20), + }) + require.NoError(t, err) + require.Equal(t, displayName, group.DisplayName) require.Equal(t, "bye", group.Name) require.Equal(t, "https://google.com", group.AvatarURL) require.Equal(t, 20, group.QuotaAllowance) diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index e374e46e192f1..ab6d5f699c452 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -174,6 +174,7 @@ export interface CreateFirstUserResponse { // From codersdk/groups.go export interface CreateGroupRequest { readonly name: string + readonly display_name: string readonly avatar_url: string readonly quota_allowance: number } @@ -506,6 +507,7 @@ export interface GitSSHKey { export interface Group { readonly id: string readonly name: string + readonly display_name: string readonly organization_id: string readonly members: User[] readonly avatar_url: string @@ -666,6 +668,7 @@ export interface PatchGroupRequest { readonly add_users: string[] readonly remove_users: string[] readonly name: string + readonly display_name?: string readonly avatar_url?: string readonly quota_allowance?: number } diff --git a/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx b/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx index 5a1e2f08aa45e..127d970c9ee56 100644 --- a/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx +++ b/site/src/components/UserOrGroupAutocomplete/UserOrGroupAutocomplete.tsx @@ -71,7 +71,7 @@ export const UserOrGroupAutocomplete: React.FC< }} isOptionEqualToValue={(option, value) => option.id === value.id} getOptionLabel={(option) => - isGroup(option) ? option.name : option.email + isGroup(option) ? option.display_name || option.name : option.email } renderOption={(props, option) => { const isOptionGroup = isGroup(option) @@ -79,7 +79,11 @@ export const UserOrGroupAutocomplete: React.FC< return ( diff --git a/site/src/pages/GroupsPage/CreateGroupPageView.tsx b/site/src/pages/GroupsPage/CreateGroupPageView.tsx index b5b132c1f794c..a2936a5924ae3 100644 --- a/site/src/pages/GroupsPage/CreateGroupPageView.tsx +++ b/site/src/pages/GroupsPage/CreateGroupPageView.tsx @@ -29,6 +29,7 @@ export const CreateGroupPageView: FC = ({ const form = useFormik({ initialValues: { name: "", + display_name: "", avatar_url: "", quota_allowance: 0, }, @@ -51,6 +52,17 @@ export const CreateGroupPageView: FC = ({ fullWidth label="Name" /> + { return ( <> - {pageTitle(group?.name ?? "Loading...")} + + {pageTitle((group?.display_name || group?.name) ?? "Loading...")} + @@ -127,13 +130,18 @@ export const GroupPage: React.FC = () => { } > - {group?.name} + + {group?.display_name || group?.name} + - {group?.members.length} members + {/* Show the name if it differs from the display name. */} + {group?.display_name && group?.display_name !== group?.name + ? group?.name + : ""}{" "} - + { }} /> + + diff --git a/site/src/pages/GroupsPage/GroupsPageView.stories.tsx b/site/src/pages/GroupsPage/GroupsPageView.stories.tsx index cc5456207ed6b..c82baac4d5c01 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.stories.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.stories.tsx @@ -25,6 +25,13 @@ WithGroups.args = { isTemplateRBACEnabled: true, } +export const WithDisplayGroup = Template.bind({}) +WithGroups.args = { + groups: [{ ...MockGroup, name: "front-end" }], + canCreateGroup: true, + isTemplateRBACEnabled: true, +} + export const EmptyGroup = Template.bind({}) EmptyGroup.args = { groups: [], diff --git a/site/src/pages/GroupsPage/GroupsPageView.tsx b/site/src/pages/GroupsPage/GroupsPageView.tsx index b14bc7fdf6e85..1acb5e79cf769 100644 --- a/site/src/pages/GroupsPage/GroupsPageView.tsx +++ b/site/src/pages/GroupsPage/GroupsPageView.tsx @@ -137,11 +137,11 @@ export const GroupsPageView: FC = ({ } - title={group.name} + title={group.display_name || group.name} subtitle={`${group.members.length} members`} /> diff --git a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx index f5c85958942f0..fd5c3d4e3fd0c 100644 --- a/site/src/pages/GroupsPage/SettingsGroupPageView.tsx +++ b/site/src/pages/GroupsPage/SettingsGroupPageView.tsx @@ -15,6 +15,7 @@ import { Stack } from "components/Stack/Stack" type FormData = { name: string + display_name: string avatar_url: string quota_allowance: number } @@ -34,6 +35,7 @@ const UpdateGroupForm: FC<{ const form = useFormik({ initialValues: { name: group.name, + display_name: group.display_name, avatar_url: group.avatar_url, quota_allowance: group.quota_allowance, }, @@ -55,7 +57,17 @@ const UpdateGroupForm: FC<{ fullWidth label="Name" /> - + form.setFieldValue("avatar_url", value)} /> - } - title={group.name} + title={group.display_name || group.name} subtitle={getGroupSubtitle(group)} /> diff --git a/site/src/testHelpers/entities.ts b/site/src/testHelpers/entities.ts index 930263fe2049c..f3212d8ea9d76 100644 --- a/site/src/testHelpers/entities.ts +++ b/site/src/testHelpers/entities.ts @@ -1661,6 +1661,7 @@ export const MockWorkspaceQuota: TypesGen.WorkspaceQuota = { export const MockGroup: TypesGen.Group = { id: "fbd2116a-8961-4954-87ae-e4575bd29ce0", name: "Front-End", + display_name: "Front-End", avatar_url: "https://example.com", organization_id: MockOrganization.id, members: [MockUser, MockUser2], diff --git a/site/src/utils/groups.ts b/site/src/utils/groups.ts index f96a9f3545cd6..9afc048ce7cef 100644 --- a/site/src/utils/groups.ts +++ b/site/src/utils/groups.ts @@ -3,6 +3,7 @@ import { Group } from "api/typesGenerated" export const everyOneGroup = (organizationId: string): Group => ({ id: organizationId, name: "Everyone", + display_name: "", organization_id: organizationId, members: [], avatar_url: "", diff --git a/site/src/xServices/groups/editGroupXService.ts b/site/src/xServices/groups/editGroupXService.ts index 25104dbb62ca1..ef919b7db72d3 100644 --- a/site/src/xServices/groups/editGroupXService.ts +++ b/site/src/xServices/groups/editGroupXService.ts @@ -23,7 +23,12 @@ export const editGroupMachine = createMachine( }, events: {} as { type: "UPDATE" - data: { name: string; avatar_url: string; quota_allowance: number } + data: { + display_name: string + name: string + avatar_url: string + quota_allowance: number + } }, }, tsTypes: {} as import("./editGroupXService.typegen").Typegen0, diff --git a/site/src/xServices/groups/groupXService.ts b/site/src/xServices/groups/groupXService.ts index 66d9b482d1022..d9aa63fa23f4f 100644 --- a/site/src/xServices/groups/groupXService.ts +++ b/site/src/xServices/groups/groupXService.ts @@ -183,6 +183,7 @@ export const groupMachine = createMachine( return patchGroup(group.id, { name: "", + display_name: "", add_users: [userId], remove_users: [], }) @@ -194,6 +195,7 @@ export const groupMachine = createMachine( return patchGroup(group.id, { name: "", + display_name: "", add_users: [], remove_users: [userId], })