From 323559ba82c6057c0b78ac2d058ff035707e5e2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=82=B1=E3=82=A4=E3=83=A9?= Date: Fri, 7 Feb 2025 11:41:22 -0700 Subject: [PATCH 001/830] feat: show warning on unrecognized idp group and role mapping claims (#16485) --- site/src/api/api.ts | 2 +- .../IdpOrgSyncPageView.stories.tsx | 10 ++- .../IdpOrgSyncPage/IdpOrgSyncPageView.tsx | 3 +- .../IdpSyncPage/IdpGroupSyncForm.tsx | 71 +++++++++++++++---- .../IdpSyncPage/IdpRoleSyncForm.tsx | 66 +++++++++++++---- .../IdpSyncPage/IdpSyncPage.tsx | 20 +++++- .../IdpSyncPage/IdpSyncPageView.stories.tsx | 71 ++++++++++++------- .../IdpSyncPage/IdpSyncPageView.tsx | 9 ++- site/src/testHelpers/entities.ts | 4 +- 9 files changed, 191 insertions(+), 65 deletions(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 5a314ddde151a..43051961fa7e7 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -804,7 +804,7 @@ class ApiMethods { ) => { const params = new URLSearchParams(); params.set("claimField", field); - const response = await this.axios.get( + const response = await this.axios.get( `/api/v2/organizations/${organization}/settings/idpsync/field-values?${params}`, ); return response.data; diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx index 78842737e5baf..430fce3a2ee05 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.stories.tsx @@ -1,5 +1,5 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { userEvent, within } from "@storybook/test"; +import { expect, userEvent, within } from "@storybook/test"; import { MockOrganization, MockOrganization2, @@ -45,10 +45,16 @@ export const MissingGroups: Story = { }, }; -export const MissingClaim: Story = { +export const MissingClaims: Story = { args: { claimFieldValues: [], }, + play: async ({ canvasElement }) => { + const user = userEvent.setup(); + const warning = canvasElement.querySelector(".lucide-triangle-alert")!; + expect(warning).not.toBe(null); + await user.hover(warning); + }, }; export const AssignDefaultOrgWarningDialog: Story = { diff --git a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx index f6822ba0a60ef..bdcc65b89aaba 100644 --- a/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx +++ b/site/src/pages/DeploymentSettingsPage/IdpOrgSyncPage/IdpOrgSyncPageView.tsx @@ -1,4 +1,3 @@ -import { TooltipProvider } from "@radix-ui/react-tooltip"; import type { Organization, OrganizationSyncSettings, @@ -30,7 +29,6 @@ import { type Option, } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { Spinner } from "components/Spinner/Spinner"; -import { Stack } from "components/Stack/Stack"; import { Switch } from "components/Switch/Switch"; import { Table, @@ -42,6 +40,7 @@ import { import { Tooltip, TooltipContent, + TooltipProvider, TooltipTrigger, } from "components/Tooltip/Tooltip"; import { useFormik } from "formik"; diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx index 2f1c0be7fa602..9d63baf180fbc 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpGroupSyncForm.tsx @@ -22,8 +22,14 @@ import { } from "components/MultiSelectCombobox/MultiSelectCombobox"; import { Spinner } from "components/Spinner/Spinner"; import { Switch } from "components/Switch/Switch"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "components/Tooltip/Tooltip"; import { useFormik } from "formik"; -import { Plus, Trash } from "lucide-react"; +import { Plus, Trash, TriangleAlert } from "lucide-react"; import { type FC, useId, useState } from "react"; import { docs } from "utils/docs"; import { isUUID } from "utils/uuid"; @@ -32,16 +38,6 @@ import { ExportPolicyButton } from "./ExportPolicyButton"; import { IdpMappingTable } from "./IdpMappingTable"; import { IdpPillList } from "./IdpPillList"; -interface IdpGroupSyncFormProps { - groupSyncSettings: GroupSyncSettings; - groupsMap: Map; - groups: Group[]; - groupMappingCount: number; - legacyGroupMappingCount: number; - organization: Organization; - onSubmit: (data: GroupSyncSettings) => void; -} - const groupSyncValidationSchema = Yup.object({ field: Yup.string().trim(), regex_filter: Yup.string().trim(), @@ -65,15 +61,27 @@ const groupSyncValidationSchema = Yup.object({ .default({}), }); -export const IdpGroupSyncForm = ({ +interface IdpGroupSyncFormProps { + groupSyncSettings: GroupSyncSettings; + claimFieldValues: readonly string[] | undefined; + groupsMap: Map; + groups: Group[]; + groupMappingCount: number; + legacyGroupMappingCount: number; + organization: Organization; + onSubmit: (data: GroupSyncSettings) => void; +} + +export const IdpGroupSyncForm: FC = ({ groupSyncSettings, + claimFieldValues, groupMappingCount, legacyGroupMappingCount, groups, groupsMap, organization, onSubmit, -}: IdpGroupSyncFormProps) => { +}) => { const form = useFormik({ initialValues: { field: groupSyncSettings?.field ?? "", @@ -270,6 +278,7 @@ export const IdpGroupSyncForm = ({ @@ -288,6 +297,7 @@ export const IdpGroupSyncForm = ({ @@ -303,17 +313,48 @@ export const IdpGroupSyncForm = ({ interface GroupRowProps { idpGroup: string; + exists: boolean | undefined; coderGroup: readonly string[]; onDelete: (idpOrg: string) => void; } -const GroupRow: FC = ({ idpGroup, coderGroup, onDelete }) => { +const GroupRow: FC = ({ + idpGroup, + exists = true, + coderGroup, + onDelete, +}) => { return ( - {idpGroup} + +
+ {idpGroup} + {!exists && ( + + + + + + + This value has not be seen in the specified claim field + before. You might want to check your IdP configuration and + ensure that this value is not misspelled. + + + + )} +
+
+ + From d52d2397ea2c2fdb8636b966e3f45a0e26b7529d Mon Sep 17 00:00:00 2001 From: Jullian Pepito Date: Wed, 12 Feb 2025 13:16:42 -0800 Subject: [PATCH 029/830] 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 89185f7fa7df7..584bd025d5aa2 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 030/830] 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 b875e19d154c3..65f6171c2c962 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 1a578c887371b..083d32c6e8fa1 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 37f2f36f79278..a70c70babf437 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 031/830] 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 e7cad847faad4..4b9810d9fbe86 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 0b874a997c54a..477ebcb714603 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 032/830] 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 2e48634c7de13..6f09a0482dbd1 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 0e03555da4720..db682394ca04a 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 21c40233718ef..780a180f1ff35 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 2d7fe83296deb..d8c2b3a77dacf 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 fedcc630a1687..9888fb11dfc3b 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 492aa50eeb7f9..b12187e682efa 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 1c832d6825c6f..6ec8959102fa5 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 a6bacd2798043..98afd98feda2a 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 08fceb2e29d82..8c49f33e31ce3 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 09541c9767b89..50b45ccd4d22f 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 033/830] 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 d7d3c100acd73..64e059b4134f6 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 ( -