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 |
Field | Tracked |
---|
created_at | true |
expires_at | true |
hashed_secret | false |
id | false |
ip_address | false |
last_used | true |
lifetime_seconds | false |
login_type | false |
scope | false |
token_name | false |
updated_at | false |
user_id | true |
|
| AuditOAuthConvertState
| Field | Tracked |
---|
created_at | true |
expires_at | true |
from_login_type | true |
to_login_type | true |
user_id | true |
|
-| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
+| Group
create, write, delete | Field | Tracked |
---|
avatar_url | true |
display_name | true |
id | true |
members | true |
name | true |
organization_id | false |
quota_allowance | true |
|
| GitSSHKey
create | Field | Tracked |
---|
created_at | false |
private_key | true |
public_key | true |
updated_at | false |
user_id | true |
|
| License
create, delete | Field | Tracked |
---|
exp | true |
id | false |
jwt | false |
uploaded_at | true |
uuid | true |
|
| Template
write, delete | Field | Tracked |
---|
active_version_id | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_username | false |
default_ttl | true |
deleted | false |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
inactivity_ttl | true |
locked_ttl | true |
max_ttl | true |
name | true |
organization_id | false |
provisioner | true |
restart_requirement_days_of_week | true |
restart_requirement_weeks | true |
updated_at | false |
user_acl | true |
|
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
[1mOptions[0m
- -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],
})