From d7614a4b026cbec988d69f8790218eff996b1e15 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 12 Feb 2025 16:43:05 +0100 Subject: [PATCH 01/13] fix: display error on deleted workspace build (#16536) Fixes: https://github.com/coder/coder/issues/15058 --- .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 1 + .../WorkspaceBuildPage/WorkspaceBuildPageView.tsx | 15 +++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 5ef847fd27..a8c77d948f 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -49,6 +49,7 @@ export const WorkspaceBuildPage: FC = () => { diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index fc3be6649b..9e6decaf7f 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -5,7 +5,9 @@ import type { WorkspaceBuild, } from "api/typesGenerated"; import { Alert } from "components/Alert/Alert"; +import { ErrorAlert } from "components/Alert/ErrorAlert"; import { Loader } from "components/Loader/Loader"; +import { Margins } from "components/Margins/Margins"; import { FullWidthPageHeader, PageHeaderSubtitle, @@ -48,6 +50,7 @@ const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => { export interface WorkspaceBuildPageViewProps { logs: ProvisionerJobLog[] | undefined; build: WorkspaceBuild | undefined; + buildError?: unknown; builds: WorkspaceBuild[] | undefined; activeBuildNumber: number; } @@ -55,6 +58,7 @@ export interface WorkspaceBuildPageViewProps { export const WorkspaceBuildPageView: FC = ({ logs, build, + buildError, builds, activeBuildNumber, }) => { @@ -64,6 +68,17 @@ export const WorkspaceBuildPageView: FC = ({ defaultValue: "build", }); + if (buildError) { + return ( + + + + ); + } + if (!build) { return ; } From f65051966cc7c8d6a137177ff4f428744bfa5e5b Mon Sep 17 00:00:00 2001 From: Andrey Date: Wed, 12 Feb 2025 16:58:33 +0100 Subject: [PATCH 02/13] feat: add run_as_non_root=True to Kubernetes Starter template (#16512) This document sounds like `run_as_non_root=True` should be enabled for workspaces. https://coder.com/docs/install/kubernetes#kubernetes-security-reference > All containers must run as non-root user > - Control plane - ... > - Workspaces - Workspace pod UID is [set in the Terraform template here](https://github.com/coder/coder/blob/f57ce97b5aadd825ddb9a9a129bb823a3725252b/examples/templates/kubernetes/main.tf#L274-L276), and are not required to run as root. Administrators of the Kubernetes of a cluster I am working on have added a security check on it, and prevent creating pods, without `run_as_non_root=True`. So, I need to set it every time I create a template. According to the docs used with `run_as_user=1000` it should not have negative effects and could be safely added. https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/ --- examples/templates/kubernetes/main.tf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 { From f1c26050b1d85cf096bde7f4d144e17d4b18236f Mon Sep 17 00:00:00 2001 From: brettkolodny Date: Wed, 12 Feb 2025 14:28:15 -0500 Subject: [PATCH 03/13] fix: add correct size to storybook icon buttons (#16539) Jaayden and I noticed that the icon button in storybooks didn't look correct. This PR fixes that by adding the correct size argument. --- site/src/components/Button/Button.stories.tsx | 3 +++ 1 file changed, 3 insertions(+) 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: , }, }; From ea1358ce76cc045b72ce717fae8ff0b35995ddc4 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Wed, 12 Feb 2025 19:59:18 +0000 Subject: [PATCH 04/13] chore: update table component and styles (#16541) - migrate styles to tailwind - migrate to new Table component --- .../OrganizationMembersPageView.tsx | 180 ++++++++---------- .../UserTable/UserRoleCell.tsx | 7 +- .../UsersPage/UsersTable/UserGroupsCell.tsx | 12 +- 3 files changed, 88 insertions(+), 111 deletions(-) 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/UserRoleCell.tsx b/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx index 88f66af485..4c350f6ffb 100644 --- a/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx +++ b/site/src/pages/OrganizationSettingsPage/UserTable/UserRoleCell.tsx @@ -14,11 +14,10 @@ * users like that, though, know that it will be painful */ import { type Interpolation, type Theme, useTheme } from "@emotion/react"; -import Stack from "@mui/material/Stack"; -import TableCell from "@mui/material/TableCell"; import Tooltip from "@mui/material/Tooltip"; import type { LoginType, SlimRole } from "api/typesGenerated"; import { Pill } from "components/Pill/Pill"; +import { TableCell } from "components/Table/Table"; import { Popover, PopoverContent, @@ -59,7 +58,7 @@ export const UserRoleCell: FC = ({ return ( - +
{canEditUsers && ( = ({ {extraRoles.length > 0 && } - +
); }; diff --git a/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx b/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx index 00f135abb2..c7c4586c0e 100644 --- a/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx +++ b/site/src/pages/UsersPage/UsersTable/UserGroupsCell.tsx @@ -2,11 +2,10 @@ import { useTheme } from "@emotion/react"; import GroupIcon from "@mui/icons-material/Group"; import List from "@mui/material/List"; import ListItem from "@mui/material/ListItem"; -import TableCell from "@mui/material/TableCell"; import type { Group } from "api/typesGenerated"; import { Avatar } from "components/Avatar/Avatar"; import { OverflowY } from "components/OverflowY/OverflowY"; -import { Stack } from "components/Stack/Stack"; +import { TableCell } from "components/Table/Table"; import { Popover, PopoverContent, @@ -40,12 +39,9 @@ export const UserGroupsCell: FC = ({ userGroups }) => { color: "inherit", lineHeight: "1", }} + type="button" > - +
= ({ userGroups }) => { {userGroups.length} Group{userGroups.length !== 1 && "s"} - +
From d52d2397ea2c2fdb8636b966e3f45a0e26b7529d Mon Sep 17 00:00:00 2001 From: Jullian Pepito Date: Wed, 12 Feb 2025 13:16:42 -0800 Subject: [PATCH 05/13] docs: fix link to CODER_QUIET_HOURS_DEFAULT_SCHEDULE in schedule doc (#16545) Corrects incorrect reference to env variable `CODER_DEFAULT_QUIET_HOURS_SCHEDULE`. Changes to `CODER_QUIET_HOURS_DEFAULT_SCHEDULE`. Also hyperlinks to the server flag (similar to `CODER_ALLOW_CUSTOM_QUIET_HOURS`) --- docs/admin/templates/managing-templates/schedule.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 981cf8c33354372dc369cac0f2cd8b2b7beefe38 Mon Sep 17 00:00:00 2001 From: Yevhenii Shcherbina Date: Thu, 13 Feb 2025 10:13:20 -0500 Subject: [PATCH 06/13] fix: display the correct response for coder list (#16547) Closes https://github.com/coder/coder/issues/16312 We intend to modify the behavior of the CLI handler based on the specified output format. However, the output format is currently only accessible within the `OutputFormatter` structure. Therefore, I propose extending `OutputFormatter` by introducing a public `FormatID` method, which will allow us to retrieve the format identifier and use it to customize the behavior of the CLI handler accordingly. --- cli/cliui/output.go | 6 ++++++ cli/list.go | 2 +- cli/list_test.go | 26 ++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) 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) + }) } From ade0a53ddbc7144390fdd505653789bff2b0e9c1 Mon Sep 17 00:00:00 2001 From: Edward Angert Date: Thu, 13 Feb 2025 10:35:05 -0500 Subject: [PATCH 07/13] docs: add markdown fields in webhook payloads (#16542) These changes were made in #14931 but didn't make it into the restructured docs Co-authored-by: EdwardAngert <17991901+EdwardAngert@users.noreply.github.com> --- docs/admin/monitoring/notifications/slack.md | 14 +++++++------- docs/admin/monitoring/notifications/teams.md | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) 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 }, { From e38bd27183c300e65634d1c5ae915050ea10c057 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 13 Feb 2025 18:24:27 +0200 Subject: [PATCH 08/13] feat(coderd): add support for provisioner job id and tag filter (#16556) This change adds to new filters to the provisionerjobs endpoint, id (array) and tags (map). Updates #15084 Updates #15192 Related #16532 --- coderd/apidoc/docs.go | 16 ++++++++++++++ coderd/apidoc/swagger.json | 16 ++++++++++++++ coderd/database/dbmem/dbmem.go | 3 +++ coderd/database/queries.sql.go | 5 ++++- coderd/database/queries/provisionerjobs.sql | 1 + coderd/provisionerjobs.go | 18 ++++++++++++++++ coderd/provisionerjobs_test.go | 24 ++++++++++++++++++++- codersdk/organizations.go | 20 +++++++++++++++++ docs/reference/api/organizations.md | 12 ++++++----- site/src/api/typesGenerated.ts | 2 ++ 10 files changed, 110 insertions(+), 7 deletions(-) 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/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/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/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 From 00e76b881fd7a37d15151c547be2359e520f32f5 Mon Sep 17 00:00:00 2001 From: Jaayden Halko Date: Thu, 13 Feb 2025 19:45:38 +0000 Subject: [PATCH 09/13] chore: migrate to tailwind (#16543) Moving styles to Tailwind --- .../UserTable/EditRolesButton.tsx | 120 ++++-------------- 1 file changed, 24 insertions(+), 96 deletions(-) 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 ( -