From d274f832b3bdb2f9e6dc8738ad540a4a9b2e28c1 Mon Sep 17 00:00:00 2001 From: Brett Kolodny Date: Tue, 26 Aug 2025 14:14:44 -0400 Subject: [PATCH 1/3] chore: improve scroll behavior of DashboardLayout wrapped pages (#19396) Updates the the `DashboardLayout` to create a singular scroll area between the top nav bar and the deployment banner on the bottom. Also improves the scroll behavior of the org settings pages. CleanShot 2025-08-18 at 13 53 01 https://github.com/user-attachments/assets/128be43d-433f-4a0f-af5b-bbfb7d646345 --- site/src/components/Sidebar/Sidebar.tsx | 5 +++-- site/src/modules/dashboard/DashboardLayout.tsx | 4 ++-- .../modules/management/OrganizationSettingsLayout.tsx | 6 +++--- site/src/modules/management/OrganizationSidebar.tsx | 2 +- .../modules/management/OrganizationSidebarLayout.tsx | 4 ++-- site/src/pages/AuditPage/AuditPageView.tsx | 2 +- .../pages/ConnectionLogPage/ConnectionLogPageView.tsx | 2 +- .../CreateTemplateGalleryPageView.tsx | 2 +- site/src/pages/GroupsPage/GroupsPage.tsx | 4 ++-- .../CustomRolesPage/CustomRolesPage.tsx | 4 ++-- .../IdpSyncPage/IdpSyncPage.tsx | 4 ++-- .../OrganizationMembersPage.tsx | 11 +---------- .../OrganizationMembersPageView.tsx | 2 +- .../OrganizationProvisionerJobsPageView.tsx | 4 ++-- .../OrganizationProvisionerKeysPageView.tsx | 2 +- .../OrganizationProvisionersPageView.tsx | 2 +- .../OrganizationSettingsPageView.tsx | 2 +- site/src/pages/TemplatePage/TemplateLayout.tsx | 6 +++--- site/src/pages/TemplatesPage/TemplatesPageView.tsx | 2 +- .../AppearancePage/AppearanceForm.tsx | 3 ++- 20 files changed, 33 insertions(+), 40 deletions(-) diff --git a/site/src/components/Sidebar/Sidebar.tsx b/site/src/components/Sidebar/Sidebar.tsx index ab289a7d7e0e8..4f626b8802354 100644 --- a/site/src/components/Sidebar/Sidebar.tsx +++ b/site/src/components/Sidebar/Sidebar.tsx @@ -5,10 +5,11 @@ import { cn } from "utils/cn"; interface SidebarProps { children?: ReactNode; + className?: string; } -export const Sidebar: FC = ({ children }) => { - return ; +export const Sidebar: FC = ({ className, children }) => { + return ; }; interface SidebarHeaderProps { diff --git a/site/src/modules/dashboard/DashboardLayout.tsx b/site/src/modules/dashboard/DashboardLayout.tsx index 1bbf5347e085e..1b3c5945b4c0d 100644 --- a/site/src/modules/dashboard/DashboardLayout.tsx +++ b/site/src/modules/dashboard/DashboardLayout.tsx @@ -23,10 +23,10 @@ export const DashboardLayout: FC = () => { {canViewDeployment && } -
+
-
+
}> diff --git a/site/src/modules/management/OrganizationSettingsLayout.tsx b/site/src/modules/management/OrganizationSettingsLayout.tsx index edbe759e0d5fb..46947c750bca6 100644 --- a/site/src/modules/management/OrganizationSettingsLayout.tsx +++ b/site/src/modules/management/OrganizationSettingsLayout.tsx @@ -91,7 +91,7 @@ const OrganizationSettingsLayout: FC = () => { organizationPermissions, }} > -
+
@@ -121,8 +121,8 @@ const OrganizationSettingsLayout: FC = () => { )} -
-
+
+
}> diff --git a/site/src/modules/management/OrganizationSidebar.tsx b/site/src/modules/management/OrganizationSidebar.tsx index 4f77348eefa93..ebcc5e13ce5bf 100644 --- a/site/src/modules/management/OrganizationSidebar.tsx +++ b/site/src/modules/management/OrganizationSidebar.tsx @@ -13,7 +13,7 @@ export const OrganizationSidebar: FC = () => { useOrganizationSettings(); return ( - + { return ( -
+
-
+
}> diff --git a/site/src/pages/AuditPage/AuditPageView.tsx b/site/src/pages/AuditPage/AuditPageView.tsx index f69e62581d202..ed19092c0a640 100644 --- a/site/src/pages/AuditPage/AuditPageView.tsx +++ b/site/src/pages/AuditPage/AuditPageView.tsx @@ -57,7 +57,7 @@ export const AuditPageView: FC = ({ const isEmpty = !isLoading && auditLogs?.length === 0; return ( - + diff --git a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx index fe3840d098aaa..0fcadf085f7ff 100644 --- a/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx +++ b/site/src/pages/ConnectionLogPage/ConnectionLogPageView.tsx @@ -56,7 +56,7 @@ export const ConnectionLogPageView: FC = ({ const isEmpty = !isLoading && connectionLogs?.length === 0; return ( - + diff --git a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx index 0ac220d4bcf67..0dfdb4a219504 100644 --- a/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx +++ b/site/src/pages/CreateTemplateGalleryPage/CreateTemplateGalleryPageView.tsx @@ -24,7 +24,7 @@ export const CreateTemplateGalleryPageView: FC< CreateTemplateGalleryPageViewProps > = ({ starterTemplatesByTag, error }) => { return ( - + diff --git a/site/src/pages/GroupsPage/GroupsPage.tsx b/site/src/pages/GroupsPage/GroupsPage.tsx index c5089cbad1e6b..64459955c91ec 100644 --- a/site/src/pages/GroupsPage/GroupsPage.tsx +++ b/site/src/pages/GroupsPage/GroupsPage.tsx @@ -76,7 +76,7 @@ const GroupsPage: FC = () => { } return ( - <> +
{helmet} { canCreateGroup={permissions.createGroup} groupsEnabled={groupsEnabled} /> - +
); }; diff --git a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx index ff197cc52aad6..92cfa5b404efa 100644 --- a/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/CustomRolesPage/CustomRolesPage.tsx @@ -58,7 +58,7 @@ const CustomRolesPage: FC = () => { } return ( - <> +
{pageTitle( @@ -116,7 +116,7 @@ const CustomRolesPage: FC = () => { }} /> </RequirePermission> - </> + </div> ); }; diff --git a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx index 59a086a024b9a..ea9604a385621 100644 --- a/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/IdpSyncPage/IdpSyncPage.tsx @@ -117,7 +117,7 @@ const IdpSyncPage: FC = () => { } return ( - <> + <div className="w-full max-w-screen-2xl pb-10"> {helmet} <div className="flex flex-col gap-12"> @@ -182,7 +182,7 @@ const IdpSyncPage: FC = () => { </Cond> </ChooseOne> </div> - </> + </div> ); }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx index f2c270cd929af..2e226f79f8066 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPage.tsx @@ -1,4 +1,3 @@ -import type { Interpolation, Theme } from "@emotion/react"; import { getErrorMessage } from "api/errors"; import { groupsByUserIdInOrganization } from "api/queries/groups"; import { @@ -156,9 +155,7 @@ const OrganizationMembersPage: FC = () => { </ul> </p> - <p css={styles.test}> - Are you sure you want to remove this member? - </p> + <p className="pb-5">Are you sure you want to remove this member?</p> </Stack> } /> @@ -166,10 +163,4 @@ const OrganizationMembersPage: FC = () => { ); }; -const styles = { - test: { - paddingBottom: 20, - }, -} satisfies Record<string, Interpolation<Theme>>; - export default OrganizationMembersPage; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx index 7f8ed8e92ea17..f720ba692d0ca 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationMembersPageView.tsx @@ -81,7 +81,7 @@ export const OrganizationMembersPageView: FC< updateMemberRoles, }) => { return ( - <div> + <div className="w-full max-w-screen-2xl pb-10"> <SettingsHeader> <SettingsHeaderTitle>Members</SettingsHeaderTitle> </SettingsHeader> diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx index 8b6a2a839b8af..f54cb163e3eea 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerJobsPage/OrganizationProvisionerJobsPageView.tsx @@ -99,7 +99,7 @@ const OrganizationProvisionerJobsPageView: FC< } return ( - <> + <div className="w-full max-w-screen-2xl pb-10"> <Helmet> <title> {pageTitle( @@ -227,7 +227,7 @@ const OrganizationProvisionerJobsPageView: FC< </TableBody> </Table> </section> - </> + </div> ); }; diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx index 6d5b1be3552ea..a8812cb603051 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionerKeysPage/OrganizationProvisionerKeysPageView.tsx @@ -45,7 +45,7 @@ export const OrganizationProvisionerKeysPageView: FC< OrganizationProvisionerKeysPageViewProps > = ({ showPaywall, provisionerKeyDaemons, error, onRetry }) => { return ( - <section> + <section className="w-full max-w-screen-2xl pb-10"> <SettingsHeader> <SettingsHeaderTitle>Provisioner Keys</SettingsHeaderTitle> <SettingsHeaderDescription> diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx index ac6e45aed24cf..386d87d8c1324 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPageView.tsx @@ -58,7 +58,7 @@ export const OrganizationProvisionersPageView: FC< onRetry, }) => { return ( - <section> + <section className="w-full max-w-screen-2xl pb-10"> <SettingsHeader> <SettingsHeaderTitle>Provisioners</SettingsHeaderTitle> <SettingsHeaderDescription> diff --git a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx index 16bc561efcc7d..a5891df618471 100644 --- a/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx +++ b/site/src/pages/OrganizationSettingsPage/OrganizationSettingsPageView.tsx @@ -68,7 +68,7 @@ export const OrganizationSettingsPageView: FC< const [isDeleting, setIsDeleting] = useState(false); return ( - <div> + <div className="w-full max-w-screen-2xl pb-10"> <SettingsHeader> <SettingsHeaderTitle>Settings</SettingsHeaderTitle> </SettingsHeader> diff --git a/site/src/pages/TemplatePage/TemplateLayout.tsx b/site/src/pages/TemplatePage/TemplateLayout.tsx index c6b9f81945f30..57fad23dc975f 100644 --- a/site/src/pages/TemplatePage/TemplateLayout.tsx +++ b/site/src/pages/TemplatePage/TemplateLayout.tsx @@ -108,7 +108,7 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ if (error || workspacePermissionsQuery.error) { return ( - <div css={{ margin: 16 }}> + <div className="p-4"> <ErrorAlert error={error} /> </div> ); @@ -119,7 +119,7 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ } return ( - <> + <div className="pb-12"> <TemplatePageHeader template={data.template} activeVersion={data.activeVersion} @@ -166,6 +166,6 @@ export const TemplateLayout: FC<PropsWithChildren> = ({ <Suspense fallback={<Loader />}>{children}</Suspense> </TemplateLayoutContext.Provider> </Margins> - </> + </div> ); }; diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index c8e391a7ebc2b..a37cb31232816 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -205,7 +205,7 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({ const isEmpty = templates && templates.length === 0; return ( - <Margins> + <Margins className="pb-12"> <PageHeader actions={ canCreateTemplates && ( diff --git a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx index 43db670850a49..aa10f315b6f2d 100644 --- a/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx +++ b/site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx @@ -21,6 +21,7 @@ import { terminalFontLabels, terminalFonts, } from "theme/constants"; +import { cn } from "utils/cn"; import { Section } from "../Section"; interface AppearanceFormProps { @@ -164,7 +165,7 @@ const AutoThemePreviewButton: FC<AutoThemePreviewButtonProps> = ({ onChange={onSelect} css={{ ...visuallyHidden }} /> - <label htmlFor={displayName} className={className}> + <label htmlFor={displayName} className={cn("relative", className)}> <ThemePreview css={{ // This half is absolute to not advance the layout (which would offset the second half) From f0cf0adcc87ccdc2d1f3d93ef6c1d79cd0ec71a0 Mon Sep 17 00:00:00 2001 From: Callum Styan <callumstyan@gmail.com> Date: Tue, 26 Aug 2025 11:14:53 -0700 Subject: [PATCH 2/3] feat: log additional known non-sensitive query param fields in the httpmw logger (#19532) Blink helped here but it's suggestion was to have a set map of sensitive fields based on predefined constants in various files, such as the api token string names. For now we'll add additional query param logging for fields we know are safe/that we want to log, such as query pagination/limit fields and ID list counts which may help identify P99 DB query latencies. --------- Signed-off-by: Callum Styan <callumstyan@gmail.com> --- coderd/httpmw/loggermw/logger.go | 61 ++++++++++++++++ .../httpmw/loggermw/logger_internal_test.go | 71 +++++++++++++++++++ 2 files changed, 132 insertions(+) diff --git a/coderd/httpmw/loggermw/logger.go b/coderd/httpmw/loggermw/logger.go index 8f21f9aa32123..37e15b3bfcf81 100644 --- a/coderd/httpmw/loggermw/logger.go +++ b/coderd/httpmw/loggermw/logger.go @@ -4,6 +4,9 @@ import ( "context" "fmt" "net/http" + "net/url" + "strconv" + "strings" "sync" "time" @@ -15,6 +18,59 @@ import ( "github.com/coder/coder/v2/coderd/tracing" ) +var ( + safeParams = []string{"page", "limit", "offset"} + countParams = []string{"ids", "template_ids"} +) + +func safeQueryParams(params url.Values) []slog.Field { + if len(params) == 0 { + return nil + } + + fields := make([]slog.Field, 0, len(params)) + for key, values := range params { + // Check if this parameter should be included + for _, pattern := range safeParams { + if strings.EqualFold(key, pattern) { + // Prepend query parameters in the log line to ensure we don't have issues with collisions + // in case any other internal logging fields already log fields with similar names + fieldName := "query_" + key + + // Log the actual values for non-sensitive parameters + if len(values) == 1 { + fields = append(fields, slog.F(fieldName, values[0])) + continue + } + fields = append(fields, slog.F(fieldName, values)) + } + } + // Some query params we just want to log the count of the params length + for _, pattern := range countParams { + if !strings.EqualFold(key, pattern) { + continue + } + count := 0 + + // Prepend query parameters in the log line to ensure we don't have issues with collisions + // in case any other internal logging fields already log fields with similar names + fieldName := "query_" + key + + // Count comma-separated values for CSV format + for _, v := range values { + if strings.Contains(v, ",") { + count += len(strings.Split(v, ",")) + continue + } + count++ + } + // For logging we always want strings + fields = append(fields, slog.F(fieldName+"_count", strconv.Itoa(count))) + } + } + return fields +} + func Logger(log slog.Logger) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { @@ -39,6 +95,11 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler { slog.F("start", start), ) + // Add safe query parameters to the log + if queryFields := safeQueryParams(r.URL.Query()); len(queryFields) > 0 { + httplog = httplog.With(queryFields...) + } + logContext := NewRequestLogger(httplog, r.Method, start) ctx := WithRequestLogger(r.Context(), logContext) diff --git a/coderd/httpmw/loggermw/logger_internal_test.go b/coderd/httpmw/loggermw/logger_internal_test.go index f372c665fda14..bf090464241a0 100644 --- a/coderd/httpmw/loggermw/logger_internal_test.go +++ b/coderd/httpmw/loggermw/logger_internal_test.go @@ -4,6 +4,7 @@ import ( "context" "net/http" "net/http/httptest" + "net/url" "slices" "strings" "sync" @@ -292,6 +293,76 @@ func TestRequestLogger_RouteParamsLogging(t *testing.T) { } } +func TestSafeQueryParams(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + params url.Values + expected map[string]interface{} + }{ + { + name: "safe parameters", + params: url.Values{ + "page": []string{"1"}, + "limit": []string{"10"}, + "filter": []string{"active"}, + "sort": []string{"name"}, + "offset": []string{"2"}, + "ids": []string{"some-id,another-id", "second-param"}, + "template_ids": []string{"some-id,another-id", "second-param"}, + }, + expected: map[string]interface{}{ + "query_page": "1", + "query_limit": "10", + "query_offset": "2", + "query_ids_count": "3", + "query_template_ids_count": "3", + }, + }, + { + name: "unknown/sensitive parameters", + params: url.Values{ + "token": []string{"secret-token"}, + "api_key": []string{"secret-key"}, + "coder_signed_app_token": []string{"jwt-token"}, + "coder_application_connect_api_key": []string{"encrypted-key"}, + "client_secret": []string{"oauth-secret"}, + "code": []string{"auth-code"}, + }, + expected: map[string]interface{}{}, + }, + { + name: "mixed parameters", + params: url.Values{ + "page": []string{"1"}, + "token": []string{"secret"}, + "filter": []string{"active"}, + }, + expected: map[string]interface{}{ + "query_page": "1", + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + fields := safeQueryParams(tt.params) + + // Convert fields to map for easier comparison + result := make(map[string]interface{}) + for _, field := range fields { + result[field.Name] = field.Value + } + + require.Equal(t, tt.expected, result) + }) + } +} + type fakeSink struct { entries []slog.SinkEntry newEntries chan slog.SinkEntry From 8083d9d5c87fbb7d7d8f018706a8d0769480378a Mon Sep 17 00:00:00 2001 From: Cian Johnston <cian@coder.com> Date: Tue, 26 Aug 2025 19:42:02 +0100 Subject: [PATCH 3/3] fix(cli): attach org option to task create (#19554) Attaches the org context options to the exp task create cmd --- cli/exp_taskcreate.go | 4 ++- cli/exp_taskcreate_test.go | 73 ++++++++++++++++++++++++++++---------- 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/cli/exp_taskcreate.go b/cli/exp_taskcreate.go index 40f45a903c85b..9125b86329746 100644 --- a/cli/exp_taskcreate.go +++ b/cli/exp_taskcreate.go @@ -23,7 +23,7 @@ func (r *RootCmd) taskCreate() *serpent.Command { taskInput string ) - return &serpent.Command{ + cmd := &serpent.Command{ Use: "create [template]", Short: "Create an experimental task", Middleware: serpent.Chain( @@ -123,4 +123,6 @@ func (r *RootCmd) taskCreate() *serpent.Command { return nil }, } + orgContext.AttachOptions(cmd) + return cmd } diff --git a/cli/exp_taskcreate_test.go b/cli/exp_taskcreate_test.go index 520838c53acca..121f22eb525f6 100644 --- a/cli/exp_taskcreate_test.go +++ b/cli/exp_taskcreate_test.go @@ -29,12 +29,13 @@ func TestTaskCreate(t *testing.T) { taskCreatedAt = time.Now() organizationID = uuid.New() + anotherOrganizationID = uuid.New() templateID = uuid.New() templateVersionID = uuid.New() templateVersionPresetID = uuid.New() ) - templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc { + templateAndVersionFoundHandler := func(t *testing.T, ctx context.Context, orgID uuid.UUID, templateName, templateVersionName, presetName, prompt string) http.HandlerFunc { t.Helper() return func(w http.ResponseWriter, r *http.Request) { @@ -42,14 +43,14 @@ func TestTaskCreate(t *testing.T) { case "/api/v2/users/me/organizations": httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{ {MinimalOrganization: codersdk.MinimalOrganization{ - ID: organizationID, + ID: orgID, }}, }) - case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", organizationID): + case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template/versions/my-template-version", orgID): httpapi.Write(ctx, w, http.StatusOK, codersdk.TemplateVersion{ ID: templateVersionID, }) - case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", organizationID): + case fmt.Sprintf("/api/v2/organizations/%s/templates/my-template", orgID): httpapi.Write(ctx, w, http.StatusOK, codersdk.Template{ ID: templateID, ActiveVersionID: templateVersionID, @@ -94,47 +95,47 @@ func TestTaskCreate(t *testing.T) { handler func(t *testing.T, ctx context.Context) http.HandlerFunc }{ { - args: []string{"my-template@my-template-version", "--input", "my custom prompt"}, + args: []string{"my-template@my-template-version", "--input", "my custom prompt", "--org", organizationID.String()}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt") }, }, { - args: []string{"my-template", "--input", "my custom prompt"}, + args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()}, env: []string{"CODER_TASK_TEMPLATE_VERSION=my-template-version"}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt") }, }, { - args: []string{"--input", "my custom prompt"}, + args: []string{"--input", "my custom prompt", "--org", organizationID.String()}, env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version"}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt") }, }, { - env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt"}, + env: []string{"CODER_TASK_TEMPLATE_NAME=my-template", "CODER_TASK_TEMPLATE_VERSION=my-template-version", "CODER_TASK_INPUT=my custom prompt", "CODER_ORGANIZATION=" + organizationID.String()}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "my-template-version", "", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "my-template-version", "", "my custom prompt") }, }, { - args: []string{"my-template", "--input", "my custom prompt"}, + args: []string{"my-template", "--input", "my custom prompt", "--org", organizationID.String()}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "", "", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "", "my custom prompt") }, }, { - args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset"}, + args: []string{"my-template", "--input", "my custom prompt", "--preset", "my-preset", "--org", organizationID.String()}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt") }, }, { @@ -142,14 +143,14 @@ func TestTaskCreate(t *testing.T) { env: []string{"CODER_TASK_PRESET_NAME=my-preset"}, expectOutput: fmt.Sprintf("The task %s has been created at %s!", cliui.Keyword("task-wild-goldfish-27"), cliui.Timestamp(taskCreatedAt)), handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt") }, }, { args: []string{"my-template", "--input", "my custom prompt", "--preset", "not-real-preset"}, expectError: `preset "not-real-preset" not found`, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { - return templateAndVersionFoundHandler(t, ctx, "my-template", "", "my-preset", "my custom prompt") + return templateAndVersionFoundHandler(t, ctx, organizationID, "my-template", "", "my-preset", "my custom prompt") }, }, { @@ -173,7 +174,7 @@ func TestTaskCreate(t *testing.T) { }, }, { - args: []string{"not-real-template", "--input", "my custom prompt"}, + args: []string{"not-real-template", "--input", "my custom prompt", "--org", organizationID.String()}, expectError: httpapi.ResourceNotFoundResponse.Message, handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { @@ -192,6 +193,40 @@ func TestTaskCreate(t *testing.T) { } }, }, + { + args: []string{"template-in-different-org", "--input", "my-custom-prompt", "--org", anotherOrganizationID.String()}, + expectError: httpapi.ResourceNotFoundResponse.Message, + handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v2/users/me/organizations": + httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{ + {MinimalOrganization: codersdk.MinimalOrganization{ + ID: anotherOrganizationID, + }}, + }) + case fmt.Sprintf("/api/v2/organizations/%s/templates/template-in-different-org", anotherOrganizationID): + httpapi.ResourceNotFound(w) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + } + }, + }, + { + args: []string{"no-org", "--input", "my-custom-prompt"}, + expectError: "Must select an organization with --org=<org_name>", + handler: func(t *testing.T, ctx context.Context) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/api/v2/users/me/organizations": + httpapi.Write(ctx, w, http.StatusOK, []codersdk.Organization{}) + default: + t.Errorf("unexpected path: %s", r.URL.Path) + } + } + }, + }, } for _, tt := range tests {