diff --git a/cli/cliui/output.go b/cli/cliui/output.go index b875e19d15..65f6171c2c 100644 --- a/cli/cliui/output.go +++ b/cli/cliui/output.go @@ -83,6 +83,12 @@ func (f *OutputFormatter) Format(ctx context.Context, data any) (string, error) return "", xerrors.Errorf("unknown output format %q", f.formatID) } +// FormatID will return the ID of the format selected by `--output`. +// If no flag is present, it returns the 'default' formatter. +func (f *OutputFormatter) FormatID() string { + return f.formatID +} + type tableFormat struct { defaultColumns []string allColumns []string diff --git a/cli/list.go b/cli/list.go index 1a578c8873..083d32c6e8 100644 --- a/cli/list.go +++ b/cli/list.go @@ -112,7 +112,7 @@ func (r *RootCmd) list() *serpent.Command { return err } - if len(res) == 0 { + if len(res) == 0 && formatter.FormatID() != cliui.JSONFormat().ID() { pretty.Fprintf(inv.Stderr, cliui.DefaultStyles.Prompt, "No workspaces found! Create one:\n") _, _ = fmt.Fprintln(inv.Stderr) _, _ = fmt.Fprintln(inv.Stderr, " "+pretty.Sprint(cliui.DefaultStyles.Code, "coder create ")) diff --git a/cli/list_test.go b/cli/list_test.go index 37f2f36f79..a70c70babf 100644 --- a/cli/list_test.go +++ b/cli/list_test.go @@ -74,4 +74,30 @@ func TestList(t *testing.T) { require.NoError(t, json.Unmarshal(out.Bytes(), &workspaces)) require.Len(t, workspaces, 1) }) + + t.Run("NoWorkspacesJSON", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + owner := coderdtest.CreateFirstUser(t, client) + member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID) + + inv, root := clitest.New(t, "list", "--output=json") + clitest.SetupConfig(t, member, root) + + ctx, cancelFunc := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancelFunc() + + stdout := bytes.NewBuffer(nil) + stderr := bytes.NewBuffer(nil) + inv.Stdout = stdout + inv.Stderr = stderr + err := inv.WithContext(ctx).Run() + require.NoError(t, err) + + var workspaces []codersdk.Workspace + require.NoError(t, json.Unmarshal(stdout.Bytes(), &workspaces)) + require.Len(t, workspaces, 0) + + require.Len(t, stderr.Bytes(), 0) + }) } diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 2e48634c7d..6f09a0482d 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -3055,6 +3055,16 @@ const docTemplate = `{ "name": "limit", "in": "query" }, + { + "type": "array", + "format": "uuid", + "items": { + "type": "string" + }, + "description": "Filter results by job IDs", + "name": "ids", + "in": "query" + }, { "enum": [ "pending", @@ -3075,6 +3085,12 @@ const docTemplate = `{ "description": "Filter results by status", "name": "status", "in": "query" + }, + { + "type": "object", + "description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})", + "name": "tags", + "in": "query" } ], "responses": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 0e03555da4..db682394ca 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -2683,6 +2683,16 @@ "name": "limit", "in": "query" }, + { + "type": "array", + "format": "uuid", + "items": { + "type": "string" + }, + "description": "Filter results by job IDs", + "name": "ids", + "in": "query" + }, { "enum": [ "pending", @@ -2703,6 +2713,12 @@ "description": "Filter results by status", "name": "status", "in": "query" + }, + { + "type": "object", + "description": "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})", + "name": "tags", + "in": "query" } ], "responses": { diff --git a/coderd/coderd.go b/coderd/coderd.go index 8ff8c05ee7..2b62d96b56 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -1304,7 +1304,7 @@ func New(options *Options) *API { func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if !api.Authorize(r, policy.ActionRead, rbac.ResourceDebugInfo) { - httpapi.ResourceNotFound(rw) + httpapi.Forbidden(rw) return } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 21c4023371..780a180f1f 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -4170,6 +4170,9 @@ func (q *FakeQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePosition if len(arg.IDs) > 0 && !slices.Contains(arg.IDs, job.ID) { continue } + if len(arg.Tags) > 0 && !tagsSubset(job.Tags, arg.Tags) { + continue + } row := database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerRow{ ProvisionerJob: rowQP.ProvisionerJob, diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2d7fe83296..d8c2b3a77d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -6472,6 +6472,7 @@ WHERE ($1::uuid IS NULL OR pj.organization_id = $1) AND (COALESCE(array_length($2::uuid[], 1), 0) = 0 OR pj.id = ANY($2::uuid[])) AND (COALESCE(array_length($3::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY($3::provisioner_job_status[])) + AND ($4::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, $4::tagset)) GROUP BY pj.id, qp.queue_position, @@ -6486,13 +6487,14 @@ GROUP BY ORDER BY pj.created_at DESC LIMIT - $4::int + $5::int ` type GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams struct { OrganizationID uuid.NullUUID `db:"organization_id" json:"organization_id"` IDs []uuid.UUID `db:"ids" json:"ids"` Status []ProvisionerJobStatus `db:"status" json:"status"` + Tags StringMap `db:"tags" json:"tags"` Limit sql.NullInt32 `db:"limit" json:"limit"` } @@ -6515,6 +6517,7 @@ func (q *sqlQuerier) GetProvisionerJobsByOrganizationAndStatusWithQueuePositionA arg.OrganizationID, pq.Array(arg.IDs), pq.Array(arg.Status), + arg.Tags, arg.Limit, ) if err != nil { diff --git a/coderd/database/queries/provisionerjobs.sql b/coderd/database/queries/provisionerjobs.sql index fedcc630a1..9888fb11df 100644 --- a/coderd/database/queries/provisionerjobs.sql +++ b/coderd/database/queries/provisionerjobs.sql @@ -158,6 +158,7 @@ WHERE (sqlc.narg('organization_id')::uuid IS NULL OR pj.organization_id = @organization_id) AND (COALESCE(array_length(@ids::uuid[], 1), 0) = 0 OR pj.id = ANY(@ids::uuid[])) AND (COALESCE(array_length(@status::provisioner_job_status[], 1), 0) = 0 OR pj.job_status = ANY(@status::provisioner_job_status[])) + AND (@tags::tagset = 'null'::tagset OR provisioner_tagset_contains(pj.tags::tagset, @tags::tagset)) GROUP BY pj.id, qp.queue_position, diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 492aa50eeb..b12187e682 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -72,7 +72,9 @@ func (api *API) provisionerJob(rw http.ResponseWriter, r *http.Request) { // @Tags Organizations // @Param organization path string true "Organization ID" format(uuid) // @Param limit query int false "Page limit" +// @Param ids query []string false "Filter results by job IDs" format(uuid) // @Param status query codersdk.ProvisionerJobStatus false "Filter results by status" enums(pending,running,succeeded,canceling,canceled,failed) +// @Param tags query object false "Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'})" // @Success 200 {array} codersdk.ProvisionerJob // @Router /organizations/{organization}/provisionerjobs [get] func (api *API) provisionerJobs(rw http.ResponseWriter, r *http.Request) { @@ -103,6 +105,10 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt p := httpapi.NewQueryParamParser() limit := p.PositiveInt32(qp, 50, "limit") status := p.Strings(qp, nil, "status") + if ids == nil { + ids = p.UUIDs(qp, nil, "ids") + } + tagsRaw := p.String(qp, "", "tags") p.ErrorExcessParams(qp) if len(p.Errors) > 0 { httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ @@ -112,11 +118,23 @@ func (api *API) handleAuthAndFetchProvisionerJobs(rw http.ResponseWriter, r *htt return nil, false } + tags := database.StringMap{} + if tagsRaw != "" { + if err := tags.Scan([]byte(tagsRaw)); err != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{ + Message: "Invalid tags query parameter", + Detail: err.Error(), + }) + return nil, false + } + } + jobs, err := api.Database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisioner(ctx, database.GetProvisionerJobsByOrganizationAndStatusWithQueuePositionAndProvisionerParams{ OrganizationID: uuid.NullUUID{UUID: org.ID, Valid: true}, Status: slice.StringEnums[database.ProvisionerJobStatus](status), Limit: sql.NullInt32{Int32: limit, Valid: limit > 0}, IDs: ids, + Tags: tags, }) if err != nil { if httpapi.Is404Error(err) { diff --git a/coderd/provisionerjobs_test.go b/coderd/provisionerjobs_test.go index 1c832d6825..6ec8959102 100644 --- a/coderd/provisionerjobs_test.go +++ b/coderd/provisionerjobs_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "encoding/json" + "strconv" "testing" "time" @@ -65,9 +66,10 @@ func TestProvisionerJobs(t *testing.T) { }) // Add more jobs than the default limit. - for range 60 { + for i := range 60 { dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ OrganizationID: owner.OrganizationID, + Tags: database.StringMap{"count": strconv.Itoa(i)}, }) } @@ -132,6 +134,16 @@ func TestProvisionerJobs(t *testing.T) { require.Len(t, jobs, 50) }) + t.Run("IDs", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + IDs: []uuid.UUID{workspace.LatestBuild.Job.ID, version.Job.ID}, + }) + require.NoError(t, err) + require.Len(t, jobs, 2) + }) + t.Run("Status", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) @@ -142,6 +154,16 @@ func TestProvisionerJobs(t *testing.T) { require.Len(t, jobs, 1) }) + t.Run("Tags", func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitMedium) + jobs, err := templateAdminClient.OrganizationProvisionerJobs(ctx, owner.OrganizationID, &codersdk.OrganizationProvisionerJobsOptions{ + Tags: map[string]string{"count": "1"}, + }) + require.NoError(t, err) + require.Len(t, jobs, 1) + }) + t.Run("Limit", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitMedium) diff --git a/codersdk/organizations.go b/codersdk/organizations.go index a6bacd2798..98afd98fed 100644 --- a/codersdk/organizations.go +++ b/codersdk/organizations.go @@ -346,7 +346,9 @@ func (c *Client) OrganizationProvisionerDaemons(ctx context.Context, organizatio type OrganizationProvisionerJobsOptions struct { Limit int + IDs []uuid.UUID Status []ProvisionerJobStatus + Tags map[string]string } func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID uuid.UUID, opts *OrganizationProvisionerJobsOptions) ([]ProvisionerJob, error) { @@ -355,9 +357,19 @@ func (c *Client) OrganizationProvisionerJobs(ctx context.Context, organizationID if opts.Limit > 0 { qp.Add("limit", strconv.Itoa(opts.Limit)) } + if len(opts.IDs) > 0 { + qp.Add("ids", joinSliceStringer(opts.IDs)) + } if len(opts.Status) > 0 { qp.Add("status", joinSlice(opts.Status)) } + if len(opts.Tags) > 0 { + tagsRaw, err := json.Marshal(opts.Tags) + if err != nil { + return nil, xerrors.Errorf("marshal tags: %w", err) + } + qp.Add("tags", string(tagsRaw)) + } } res, err := c.Request(ctx, http.MethodGet, @@ -401,6 +413,14 @@ func joinSlice[T ~string](s []T) string { return strings.Join(ss, ",") } +func joinSliceStringer[T fmt.Stringer](s []T) string { + var ss []string + for _, v := range s { + ss = append(ss, v.String()) + } + return strings.Join(ss, ",") +} + // CreateTemplateVersion processes source-code and optionally associates the version with a template. // Executing without a template is useful for validating source-code. func (c *Client) CreateTemplateVersion(ctx context.Context, organizationID uuid.UUID, req CreateTemplateVersionRequest) (TemplateVersion, error) { diff --git a/docs/admin/monitoring/notifications/index.md b/docs/admin/monitoring/notifications/index.md index a7eeab44d4..eb077e13b3 100644 --- a/docs/admin/monitoring/notifications/index.md +++ b/docs/admin/monitoring/notifications/index.md @@ -3,43 +3,54 @@ Notifications are sent by Coder in response to specific internal events, such as a workspace being deleted or a user being created. +Available events may differ between versions. +For a list of all events, visit your Coder deployment's +`https://coder.example.com/deployment/notifications`. + ## Event Types Notifications are sent in response to internal events, to alert the affected -user(s) of this event. Currently we support the following list of events: +user(s) of the event. + +Coder supports the following list of events: ### Workspace Events -_These notifications are sent to the workspace owner._ +These notifications are sent to the workspace owner: -- Workspace Deleted -- Workspace Manual Build Failure -- Workspace Automatic Build Failure -- Workspace Automatically Updated -- Workspace Dormant -- Workspace Marked For Deletion +- Workspace created +- Workspace deleted +- Workspace manual build failure +- Workspace automatic build failure +- Workspace manually updated +- Workspace automatically updated +- Workspace marked as dormant +- Workspace marked for deletion ### User Events -_These notifications are sent to users with **owner** and **user admin** roles._ +These notifications sent to users with **owner** and **user admin** roles: -- User Account Created -- User Account Deleted -- User Account Suspended -- User Account Activated -- _(coming soon) User Password Reset_ -- _(coming soon) User Email Verification_ +- User account created +- User account deleted +- User account suspended +- User account activated -_These notifications are sent to the user themselves._ +These notifications sent to users themselves: -- User Account Suspended -- User Account Activated +- User account suspended +- User account activated +- User password reset (One-time passcode) ### Template Events -_These notifications are sent to users with **template admin** roles._ +These notifications are sent to users with **template admin** roles: -- Template Deleted +- Template deleted +- Template deprecated +- Report: Workspace builds failed for template + - This notification is delivered as part of a weekly cron job and summarizes + the failed builds for a given template. ## Configuration diff --git a/docs/admin/monitoring/notifications/slack.md b/docs/admin/monitoring/notifications/slack.md index e7cad847fa..4b9810d9fb 100644 --- a/docs/admin/monitoring/notifications/slack.md +++ b/docs/admin/monitoring/notifications/slack.md @@ -89,11 +89,11 @@ To build the server to receive webhooks and interact with Slack: return res.status(400).send("Error: request body is missing"); } - const { title, body } = req.body; - if (!title || !body) { - return res - .status(400) - .send('Error: missing fields: "title", or "body"'); + const { title_markdown, body_markdown } = req.body; + if (!title_markdown || !body_markdown) { + return res + .status(400) + .send('Error: missing fields: "title_markdown", or "body_markdown"'); } const payload = req.body.payload; @@ -119,11 +119,11 @@ To build the server to receive webhooks and interact with Slack: blocks: [ { type: "header", - text: { type: "plain_text", text: title }, + text: { type: "mrkdwn", text: title_markdown }, }, { type: "section", - text: { type: "mrkdwn", text: body }, + text: { type: "mrkdwn", text: body_markdown }, }, ], }; diff --git a/docs/admin/monitoring/notifications/teams.md b/docs/admin/monitoring/notifications/teams.md index 0b874a997c..477ebcb714 100644 --- a/docs/admin/monitoring/notifications/teams.md +++ b/docs/admin/monitoring/notifications/teams.md @@ -67,10 +67,10 @@ The process of setting up a Teams workflow consists of three key steps: } } }, - "title": { + "title_markdown": { "type": "string" }, - "body": { + "body_markdown": { "type": "string" } } @@ -108,11 +108,11 @@ The process of setting up a Teams workflow consists of three key steps: }, { "type": "TextBlock", - "text": "**@{replace(body('Parse_JSON')?['title'], '"', '\"')}**" + "text": "**@{replace(body('Parse_JSON')?['title_markdown'], '"', '\"')}**" }, { "type": "TextBlock", - "text": "@{replace(body('Parse_JSON')?['body'], '"', '\"')}", + "text": "@{replace(body('Parse_JSON')?['body_markdown'], '"', '\"')}", "wrap": true }, { diff --git a/docs/admin/templates/managing-templates/schedule.md b/docs/admin/templates/managing-templates/schedule.md index 89185f7fa7..584bd025d5 100644 --- a/docs/admin/templates/managing-templates/schedule.md +++ b/docs/admin/templates/managing-templates/schedule.md @@ -122,7 +122,7 @@ stopped due to the policy at the start of the user's quiet hours. ![User schedule settings](../../../images/admin/templates/schedule/user-quiet-hours.png) Admins can define the default quiet hours for all users with the -`--default-quiet-hours-schedule` flag or `CODER_DEFAULT_QUIET_HOURS_SCHEDULE` +[CODER_QUIET_HOURS_DEFAULT_SCHEDULE](../../../reference/cli/server.md#--default-quiet-hours-schedule) environment variable. The value should be a cron expression such as `CRON_TZ=America/Chicago 30 2 * * *` which would set the default quiet hours to 2:30 AM in the America/Chicago timezone. The cron schedule can only have a diff --git a/docs/reference/api/organizations.md b/docs/reference/api/organizations.md index 08fceb2e29..8c49f33e31 100644 --- a/docs/reference/api/organizations.md +++ b/docs/reference/api/organizations.md @@ -359,11 +359,13 @@ curl -X GET http://coder-server:8080/api/v2/organizations/{organization}/provisi ### Parameters -| Name | In | Type | Required | Description | -|----------------|-------|--------------|----------|--------------------------| -| `organization` | path | string(uuid) | true | Organization ID | -| `limit` | query | integer | false | Page limit | -| `status` | query | string | false | Filter results by status | +| Name | In | Type | Required | Description | +|----------------|-------|--------------|----------|------------------------------------------------------------------------------------| +| `organization` | path | string(uuid) | true | Organization ID | +| `limit` | query | integer | false | Page limit | +| `ids` | query | array(uuid) | false | Filter results by job IDs | +| `status` | query | string | false | Filter results by status | +| `tags` | query | object | false | Provisioner tags to filter by (JSON of the form {'tag1':'value1','tag2':'value2'}) | #### Enumerated Values diff --git a/examples/templates/kubernetes/main.tf b/examples/templates/kubernetes/main.tf index 0ba6ba33b7..e1fdb12cbe 100644 --- a/examples/templates/kubernetes/main.tf +++ b/examples/templates/kubernetes/main.tf @@ -278,8 +278,9 @@ resource "kubernetes_deployment" "main" { } spec { security_context { - run_as_user = 1000 - fs_group = 1000 + run_as_user = 1000 + fs_group = 1000 + run_as_non_root = true } container { diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 09541c9767..50b45ccd4d 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1438,7 +1438,9 @@ export interface OrganizationMemberWithUserData extends OrganizationMember { // From codersdk/organizations.go export interface OrganizationProvisionerJobsOptions { readonly Limit: number; + readonly IDs: readonly string[]; readonly Status: readonly ProvisionerJobStatus[]; + readonly Tags: Record; } // From codersdk/idpsync.go diff --git a/site/src/components/Button/Button.stories.tsx b/site/src/components/Button/Button.stories.tsx index 3dc5001064..ceeb395cf8 100644 --- a/site/src/components/Button/Button.stories.tsx +++ b/site/src/components/Button/Button.stories.tsx @@ -96,6 +96,7 @@ export const DestructiveSmall: Story = { export const IconButtonDefault: Story = { args: { variant: "default", + size: "icon", children: , }, }; @@ -103,6 +104,7 @@ export const IconButtonDefault: Story = { export const IconButtonOutline: Story = { args: { variant: "outline", + size: "icon", children: , }, }; @@ -110,6 +112,7 @@ export const IconButtonOutline: Story = { export const IconButtonSubtle: Story = { args: { variant: "subtle", + size: "icon", children: , }, }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index 1cc009e1f4..72737a92c3 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -1,14 +1,6 @@ -import type { Interpolation, Theme } from "@emotion/react"; import PersonAdd from "@mui/icons-material/PersonAdd"; import LoadingButton from "@mui/lab/LoadingButton"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; import { getErrorMessage } from "api/errors"; -import type { GroupsByUserId } from "api/queries/groups"; import type { Group, OrganizationMemberWithUserData, @@ -28,6 +20,13 @@ import { } from "components/MoreMenu/MoreMenu"; import { SettingsHeader } from "components/SettingsHeader/SettingsHeader"; import { Stack } from "components/Stack/Stack"; +import { + Table, + TableBody, + TableCell, + TableHeader, + TableRow, +} from "components/Table/Table"; import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete"; import { UserGroupsCell } from "pages/UsersPage/UsersTable/UserGroupsCell"; import { type FC, useState } from "react"; @@ -80,83 +79,80 @@ export const OrganizationMembersPageView: FC< onSubmit={addMember} /> )} - - - - - - User - - - Roles - - - - - - Groups - - - - - - - - {members?.map((member) => ( - - - - } - title={member.name || member.username} - subtitle={member.email} - /> - - { - try { - await updateMemberRoles(member, roles); - displaySuccess("Roles updated successfully."); - } catch (error) { - displayError( - getErrorMessage(error, "Failed to update roles."), - ); - } - }} +
+ + + User + + + Roles + + + + + + Groups + + + + + + + + {members?.map((member) => ( + + + + } + title={member.name || member.username} + subtitle={member.email} /> - - - {member.user_id !== me.id && canEditMembers && ( - - - - - - removeMember(member)} - > - Remove - - - - )} - - - ))} - -
-
+ + { + try { + await updateMemberRoles(member, roles); + displaySuccess("Roles updated successfully."); + } catch (error) { + displayError( + getErrorMessage(error, "Failed to update roles."), + ); + } + }} + /> + + + {member.user_id !== me.id && canEditMembers && ( + + + + + + removeMember(member)} + > + Remove + + + + )} + + + ))} + + ); @@ -190,7 +186,7 @@ const AddOrganizationMember: FC = ({ > { setSelectedUser(newValue); @@ -210,17 +206,3 @@ const AddOrganizationMember: FC = ({ ); }; - -const styles = { - role: (theme) => ({ - backgroundColor: theme.roles.notice.background, - borderColor: theme.roles.notice.outline, - }), - globalRole: (theme) => ({ - backgroundColor: theme.roles.inactive.background, - borderColor: theme.roles.inactive.outline, - }), - autoComplete: { - width: 300, - }, -} satisfies Record>; diff --git a/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx index d7d3c100ac..64e059b413 100644 --- a/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx +++ b/site/src/pages/OrganizationSettingsPage/UserTable/EditRolesButton.tsx @@ -1,9 +1,8 @@ -import type { Interpolation, Theme } from "@emotion/react"; import UserIcon from "@mui/icons-material/PersonOutline"; import Checkbox from "@mui/material/Checkbox"; -import IconButton from "@mui/material/IconButton"; import Tooltip from "@mui/material/Tooltip"; import type { SlimRole } from "api/typesGenerated"; +import { Button } from "components/Button/Button"; import { HelpTooltip, HelpTooltipContent, @@ -12,13 +11,11 @@ import { HelpTooltipTrigger, } from "components/HelpTooltip/HelpTooltip"; import { EditSquare } from "components/Icons/EditSquare"; -import { Stack } from "components/Stack/Stack"; import { Popover, PopoverContent, PopoverTrigger, } from "components/deprecated/Popover/Popover"; -import { type ClassName, useClassName } from "hooks/useClassName"; import type { FC } from "react"; const roleDescriptions: Record = { @@ -47,23 +44,23 @@ const Option: FC = ({ onChange, }) => { return ( -