From ebe2d93e6eea222cedc65630f4132a706303eb8d Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Sun, 15 Oct 2023 20:51:16 +0000 Subject: [PATCH 01/13] added new option table type for experiments --- coderd/apidoc/docs.go | 8 + coderd/apidoc/swagger.json | 8 + coderd/database/queries.sql.go | 226 +++++++++--------- coderd/experiments.go | 11 +- codersdk/deployment.go | 22 +- docs/api/general.md | 6 + site/src/api/api.ts | 6 +- site/src/api/queries/experiments.ts | 9 +- site/src/api/typesGenerated.ts | 5 + .../DeploySettingsLayout/Option.tsx | 52 +++- .../DeploySettingsLayout/OptionsTable.tsx | 7 +- .../DeploySettingsLayout/optionValue.ts | 25 +- .../GeneralSettingsPage.tsx | 3 + .../GeneralSettingsPageView.tsx | 10 +- 14 files changed, 264 insertions(+), 134 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index ea5eae748b255..829c776fe9624 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -589,6 +589,14 @@ const docTemplate = `{ ], "summary": "Get experiments", "operationId": "get-experiments", + "parameters": [ + { + "type": "boolean", + "description": "All available experiments", + "name": "include_all", + "in": "query" + } + ], "responses": { "200": { "description": "OK", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5b908c90b176a..02471a18b0ed5 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -499,6 +499,14 @@ "tags": ["General"], "summary": "Get experiments", "operationId": "get-experiments", + "parameters": [ + { + "type": "boolean", + "description": "All available experiments", + "name": "include_all", + "in": "query" + } + ], "responses": { "200": { "description": "OK", diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 2d41e3e29e270..ae46a1348d1c0 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9748,6 +9748,119 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10708,116 +10821,3 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } - -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/coderd/experiments.go b/coderd/experiments.go index 1a8bb5ce1812a..4d66f97cd4f13 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/coder/coder/v2/coderd/httpapi" + "github.com/coder/coder/v2/codersdk" ) // @Summary Get experiments @@ -11,9 +12,17 @@ import ( // @Security CoderSessionToken // @Produce json // @Tags General +// @Param include_all query bool false "All available experiments" // @Success 200 {array} codersdk.Experiment // @Router /experiments [get] func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - httpapi.Write(ctx, rw, http.StatusOK, api.Experiments) + all := r.URL.Query().Has("include_all") + + if !all { + httpapi.Write(ctx, rw, http.StatusOK, api.Experiments) + return + } + + httpapi.Write(ctx, rw, http.StatusOK, codersdk.ExperimentsAll) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 195915b052ea4..aeea25b30b02e 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -2028,8 +2028,26 @@ func (e Experiments) Enabled(ex Experiment) bool { return false } -func (c *Client) Experiments(ctx context.Context) (Experiments, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) +type ExperimentOptions struct { + // All signifies that all experiments - rather than just those that are enabled - + // should be returned + IncludeAll bool `json:"include_all,omitempty"` +} + +// asRequestOption returns a function that can be used in (*Client).Request. +// It modifies the request query parameters. +func (o ExperimentOptions) asRequestOption() RequestOption { + return func(r *http.Request) { + q := r.URL.Query() + if o.IncludeAll { + q.Set("include_all", "true") + } + r.URL.RawQuery = q.Encode() + } +} + +func (c *Client) Experiments(ctx context.Context, opts ExperimentOptions) (Experiments, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil, opts.asRequestOption()) if err != nil { return nil, err } diff --git a/docs/api/general.md b/docs/api/general.md index 1362b6edcd280..acf1b3ca8ec1f 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -548,6 +548,12 @@ curl -X GET http://coder-server:8080/api/v2/experiments \ `GET /experiments` +### Parameters + +| Name | In | Type | Required | Description | +| ------------- | ----- | ------- | -------- | ------------------------- | +| `include_all` | query | boolean | false | All available experiments | + ### Example responses > 200 Response diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 32f88895429d4..67a2cb4c682b0 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -852,9 +852,11 @@ export const getEntitlements = async (): Promise => { } }; -export const getExperiments = async (): Promise => { +export const getExperiments = async ( + params?: TypesGen.ExperimentOptions, +): Promise => { try { - const response = await axios.get("/api/v2/experiments"); + const response = await axios.get("/api/v2/experiments", { params }); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index cc6a2a067fa1d..7ab53cd533ad2 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,5 +1,5 @@ import * as API from "api/api"; -import { Experiments } from "api/typesGenerated"; +import { Experiments, ExperimentOptions } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; export const experiments = () => { @@ -9,3 +9,10 @@ export const experiments = () => { getMetadataAsJSON("experiments") ?? API.getExperiments(), }; }; + +export const updatedExperiments = (params?: ExperimentOptions) => { + return { + queryKey: ["experiments"], + queryFn: async () => API.getExperiments(params), + }; +}; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 996eb300275bc..0dcdb1d1d1393 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -432,6 +432,11 @@ export interface Entitlements { readonly refreshed_at: string; } +// From codersdk/deployment.go +export interface ExperimentOptions { + readonly include_all?: boolean; +} + // From codersdk/deployment.go export type Experiments = Experiment[]; diff --git a/site/src/components/DeploySettingsLayout/Option.tsx b/site/src/components/DeploySettingsLayout/Option.tsx index 0b813157999f8..604f1d7490185 100644 --- a/site/src/components/DeploySettingsLayout/Option.tsx +++ b/site/src/components/DeploySettingsLayout/Option.tsx @@ -4,6 +4,7 @@ import Box, { BoxProps } from "@mui/material/Box"; import { useTheme } from "@mui/system"; import { DisabledBadge, EnabledBadge } from "./Badges"; import { css } from "@emotion/react"; +import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined"; export const OptionName: FC = (props) => { const { children } = props; @@ -38,7 +39,7 @@ export const OptionDescription: FC = (props) => { }; interface OptionValueProps { - children?: boolean | number | string | string[]; + children?: boolean | number | string | string[] | Record; } export const OptionValue: FC = (props) => { @@ -56,6 +57,15 @@ export const OptionValue: FC = (props) => { } `; + const listStyles = css` + margin: 0, + padding: 0, + listStylePosition: "inside", + display: "flex", + flexDirection: "column", + gap: theme.spacing(0.5), + `; + if (typeof children === "boolean") { return children ? : ; } @@ -72,18 +82,38 @@ export const OptionValue: FC = (props) => { return {children}; } + if (typeof children === "object" && !Array.isArray(children)) { + return ( +
    + {Object.entries(children).map(([option, isEnabled]) => ( +
  • + + {option} + {isEnabled && ( + theme.palette.success.light, + margin: (theme) => theme.spacing(0, 1), + }} + /> + )} + +
  • + ))} +
+ ); + } + if (Array.isArray(children)) { return ( -
    +
      {children.map((item) => (
    • {item} diff --git a/site/src/components/DeploySettingsLayout/OptionsTable.tsx b/site/src/components/DeploySettingsLayout/OptionsTable.tsx index b50e4f18242c0..a8920c2eddc02 100644 --- a/site/src/components/DeploySettingsLayout/OptionsTable.tsx +++ b/site/src/components/DeploySettingsLayout/OptionsTable.tsx @@ -19,7 +19,8 @@ import { optionValue } from "./optionValue"; const OptionsTable: FC<{ options: ClibaseOption[]; -}> = ({ options }) => { + additionalValues?: string[]; +}> = ({ options, additionalValues }) => { if (options.length === 0) { return

      No options to configure

      ; } @@ -95,7 +96,9 @@ const OptionsTable: FC<{ - {optionValue(option)} + + {optionValue(option, additionalValues)} + ); diff --git a/site/src/components/DeploySettingsLayout/optionValue.ts b/site/src/components/DeploySettingsLayout/optionValue.ts index 1976e52b4593b..4fd1b67a0c5fc 100644 --- a/site/src/components/DeploySettingsLayout/optionValue.ts +++ b/site/src/components/DeploySettingsLayout/optionValue.ts @@ -2,7 +2,10 @@ import { ClibaseOption } from "api/typesGenerated"; import { intervalToDuration, formatDuration } from "date-fns"; // optionValue is a helper function to format the value of a specific deployment options -export function optionValue(option: ClibaseOption) { +export function optionValue( + option: ClibaseOption, + additionalValues?: string[], +) { switch (option.name) { case "Max Token Lifetime": case "Session Duration": @@ -19,6 +22,26 @@ export function optionValue(option: ClibaseOption) { return Object.entries(option.value as Record).map( ([key, value]) => `"${key}"->"${value}"`, ); + case "Experiments": { + const experimentMap: Record | undefined = + additionalValues?.reduce( + (acc, v) => { + return { ...acc, [v]: false }; + }, + {} as Record, + ); + + for (const v of option.value) { + if ( + experimentMap && + Object.prototype.hasOwnProperty.call(experimentMap, v) + ) { + experimentMap[v] = true; + } + } + + return experimentMap; + } default: return option.value; } diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx index a36e7c4415e4a..3aa5fa0e1a29f 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx @@ -6,11 +6,13 @@ import { GeneralSettingsPageView } from "./GeneralSettingsPageView"; import { useQuery } from "react-query"; import { deploymentDAUs } from "api/queries/deployment"; import { entitlements } from "api/queries/entitlements"; +import { updatedExperiments } from "api/queries/experiments"; const GeneralSettingsPage: FC = () => { const { deploymentValues } = useDeploySettings(); const deploymentDAUsQuery = useQuery(deploymentDAUs()); const entitlementsQuery = useQuery(entitlements()); + const experimentsQuery = useQuery(updatedExperiments({ include_all: true })); return ( <> @@ -22,6 +24,7 @@ const GeneralSettingsPage: FC = () => { deploymentDAUs={deploymentDAUsQuery.data} deploymentDAUsError={deploymentDAUsQuery.error} entitlements={entitlementsQuery.data} + allExperiments={experimentsQuery.data} /> ); diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index 21623d813ee50..ffaf2236b46db 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -1,5 +1,10 @@ import Box from "@mui/material/Box"; -import { ClibaseOption, DAUsResponse, Entitlements } from "api/typesGenerated"; +import { + ClibaseOption, + DAUsResponse, + Entitlements, + Experiments, +} from "api/typesGenerated"; import { ErrorAlert } from "components/Alert/ErrorAlert"; import { ActiveUserChart, @@ -17,12 +22,14 @@ export type GeneralSettingsPageViewProps = { deploymentDAUs?: DAUsResponse; deploymentDAUsError: unknown; entitlements: Entitlements | undefined; + allExperiments: Experiments | undefined; }; export const GeneralSettingsPageView = ({ deploymentOptions, deploymentDAUs, deploymentDAUsError, entitlements, + allExperiments, }: GeneralSettingsPageViewProps): JSX.Element => { return ( <> @@ -57,6 +64,7 @@ export const GeneralSettingsPageView = ({ "Wildcard Access URL", "Experiments", )} + additionalValues={allExperiments} /> From 00ea98e05403e7077156e8ba70af6d59eec0ee0f Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Sun, 15 Oct 2023 21:37:54 +0000 Subject: [PATCH 02/13] added tests --- .../DeploySettingsLayout/optionValue.test.ts | 46 +++++++++++++++++-- .../DeploySettingsLayout/optionValue.ts | 2 +- .../GeneralSettingsPageView.stories.tsx | 38 ++++++++++++++- 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/site/src/components/DeploySettingsLayout/optionValue.test.ts b/site/src/components/DeploySettingsLayout/optionValue.test.ts index 890fa6a72c638..f9797b98f60d2 100644 --- a/site/src/components/DeploySettingsLayout/optionValue.test.ts +++ b/site/src/components/DeploySettingsLayout/optionValue.test.ts @@ -13,6 +13,7 @@ const defaultOption: ClibaseOption = { describe("optionValue", () => { it.each<{ option: ClibaseOption; + additionalValues?: string[]; expected: unknown; }>([ { @@ -67,7 +68,46 @@ describe("optionValue", () => { }, expected: [`"123"->"foo"`, `"456"->"bar"`, `"789"->"baz"`], }, - ])(`[$option.name]optionValue($option.value)`, ({ option, expected }) => { - expect(optionValue(option)).toEqual(expected); - }); + { + option: { + ...defaultOption, + name: "Experiments", + value: ["single_tailnet"], + }, + additionalValues: ["single_tailnet", "deployment_health_page"], + expected: { single_tailnet: true, deployment_health_page: false }, + }, + { + option: { + ...defaultOption, + name: "Experiments", + value: [], + }, + additionalValues: ["single_tailnet", "deployment_health_page"], + expected: { single_tailnet: false, deployment_health_page: false }, + }, + { + option: { + ...defaultOption, + name: "Experiments", + value: ["moons"], + }, + additionalValues: ["single_tailnet", "deployment_health_page"], + expected: { single_tailnet: false, deployment_health_page: false }, + }, + { + option: { + ...defaultOption, + name: "Experiments", + value: ["*"], + }, + additionalValues: ["single_tailnet", "deployment_health_page"], + expected: { single_tailnet: true, deployment_health_page: true }, + }, + ])( + `[$option.name]optionValue($option.value)`, + ({ option, expected, additionalValues }) => { + expect(optionValue(option, additionalValues)).toEqual(expected); + }, + ); }); diff --git a/site/src/components/DeploySettingsLayout/optionValue.ts b/site/src/components/DeploySettingsLayout/optionValue.ts index 4fd1b67a0c5fc..0b6ed8ef7b7cd 100644 --- a/site/src/components/DeploySettingsLayout/optionValue.ts +++ b/site/src/components/DeploySettingsLayout/optionValue.ts @@ -26,7 +26,7 @@ export function optionValue( const experimentMap: Record | undefined = additionalValues?.reduce( (acc, v) => { - return { ...acc, [v]: false }; + return { ...acc, [v]: option.value.includes("*") ? true : false }; }, {} as Record, ); diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx index 42416e828b685..0dad6c14cee17 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx @@ -34,12 +34,13 @@ const meta: Meta = { description: "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", flag: "experiments", - value: ["*", "moons", "single_tailnet", "deployment_health_page"], + value: ["single_tailnet"], flag_shorthand: "", hidden: false, }, ], deploymentDAUs: MockDeploymentDAUResponse, + allExperiments: ["single_tailnet", "deployment_health_page"], }, }; @@ -69,3 +70,38 @@ export const DAUError: Story = { }), }, }; + +export const allExperimentsEnabled: Story = { + args: { + deploymentOptions: [ + { + name: "Access URL", + description: + "The URL that users will use to access the Coder deployment.", + flag: "access-url", + flag_shorthand: "", + value: "https://dev.coder.com", + hidden: false, + }, + { + name: "Wildcard Access URL", + description: + 'Specifies the wildcard hostname to use for workspace applications in the form "*.example.com".', + flag: "wildcard-access-url", + flag_shorthand: "", + value: "*--apps.dev.coder.com", + hidden: false, + }, + { + name: "Experiments", + description: + "Enable one or more experiments. These are not ready for production. Separate multiple experiments with commas, or enter '*' to opt-in to all available experiments.", + flag: "experiments", + value: ["*"], + flag_shorthand: "", + hidden: false, + }, + ], + allExperiments: ["single_tailnet", "deployment_health_page"], + }, +}; From 7819735a9ac7e0add01f051fbef154a8ae963176 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Sun, 15 Oct 2023 21:57:22 +0000 Subject: [PATCH 03/13] fixed go tests --- coderd/experiments_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index 0f498e7e7cf2b..14a5c768c02e4 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -25,7 +25,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx) + experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) require.NoError(t, err) require.NotNil(t, experiments) require.Empty(t, experiments) @@ -44,7 +44,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx) + experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) require.NoError(t, err) require.NotNil(t, experiments) // Should be lower-cased. @@ -66,7 +66,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx) + experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) require.NoError(t, err) require.NotNil(t, experiments) require.ElementsMatch(t, codersdk.ExperimentsAll, experiments) @@ -88,7 +88,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx) + experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) require.NoError(t, err) require.NotNil(t, experiments) require.ElementsMatch(t, append(codersdk.ExperimentsAll, "danger"), experiments) @@ -112,7 +112,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.Experiments(ctx) + _, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) require.Error(t, err) require.ErrorContains(t, err, httpmw.SignedOutErrorMessage) }) From 776356ef83153e996b39f296b26c8faf343d5846 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 16 Oct 2023 14:17:17 +0000 Subject: [PATCH 04/13] added go test for new param --- coderd/experiments_test.go | 22 ++++++++++++++++++++++ codersdk/deployment.go | 3 ++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index 14a5c768c02e4..d9bfc23089554 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -116,4 +116,26 @@ func Test_Experiments(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, httpmw.SignedOutErrorMessage) }) + + t.Run("include_all query param", func(t *testing.T) { + t.Parallel() + cfg := coderdtest.DeploymentValues(t) + cfg.Experiments = []string{"foo", "BAR"} + codersdk.ExperimentsAll = []codersdk.Experiment{"bat", "fizz", "foo", "BAR"} + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: cfg, + }) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{IncludeAll: true}) + require.NoError(t, err) + require.NotNil(t, experiments) + require.ElementsMatch(t, []codersdk.Experiment{"bat", "fizz", "foo", "BAR"}, experiments) + + require.True(t, codersdk.Experiments{"foo", "BAR"}.Enabled("foo")) + require.True(t, codersdk.Experiments{"foo", "BAR"}.Enabled("BAR")) + }) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index aeea25b30b02e..afd4b9dbaab22 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -2013,12 +2013,13 @@ var ExperimentsAll = Experiments{ ExperimentSingleTailnet, } -// Experiments is a list of experiments that are enabled for the deployment. +// Experiments is a list of experiments. // Multiple experiments may be enabled at the same time. // Experiments are not safe for production use, and are not guaranteed to // be backwards compatible. They may be removed or renamed at any time. type Experiments []Experiment +// Returns a list of experiments that are enabled for the deployment. func (e Experiments) Enabled(ex Experiment) bool { for _, v := range e { if v == ex { From 9a2605a70ffe63517478d7f632b8419ec1f79c09 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 16 Oct 2023 14:23:49 +0000 Subject: [PATCH 05/13] removing query change --- coderd/database/queries.sql.go | 226 ++++++++++++++++----------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 31a2099826dc8..fc301d427fa8d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9755,119 +9755,6 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10828,3 +10715,116 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } + +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} From f13b815d178ebb169e0123ac101ae3354af04515 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 16 Oct 2023 14:42:50 +0000 Subject: [PATCH 06/13] clearing ExperimentsAll --- coderd/experiments_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index d9bfc23089554..aef783036e171 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -121,6 +121,7 @@ func Test_Experiments(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo", "BAR"} + inMemoryExperimentsAll := codersdk.ExperimentsAll codersdk.ExperimentsAll = []codersdk.Experiment{"bat", "fizz", "foo", "BAR"} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: cfg, @@ -137,5 +138,8 @@ func Test_Experiments(t *testing.T) { require.True(t, codersdk.Experiments{"foo", "BAR"}.Enabled("foo")) require.True(t, codersdk.Experiments{"foo", "BAR"}.Enabled("BAR")) + + // reset all experiments so other tests don't flake + codersdk.ExperimentsAll = inMemoryExperimentsAll }) } From aa65ff027d0655cc449327541842f91dcc9a59be Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 16 Oct 2023 15:11:48 +0000 Subject: [PATCH 07/13] dont mutate ExperimentsAll --- coderd/experiments_test.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index aef783036e171..5a6f2a9f894ba 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -121,8 +121,6 @@ func Test_Experiments(t *testing.T) { t.Parallel() cfg := coderdtest.DeploymentValues(t) cfg.Experiments = []string{"foo", "BAR"} - inMemoryExperimentsAll := codersdk.ExperimentsAll - codersdk.ExperimentsAll = []codersdk.Experiment{"bat", "fizz", "foo", "BAR"} client := coderdtest.New(t, &coderdtest.Options{ DeploymentValues: cfg, }) @@ -134,12 +132,6 @@ func Test_Experiments(t *testing.T) { experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{IncludeAll: true}) require.NoError(t, err) require.NotNil(t, experiments) - require.ElementsMatch(t, []codersdk.Experiment{"bat", "fizz", "foo", "BAR"}, experiments) - - require.True(t, codersdk.Experiments{"foo", "BAR"}.Enabled("foo")) - require.True(t, codersdk.Experiments{"foo", "BAR"}.Enabled("BAR")) - - // reset all experiments so other tests don't flake - codersdk.ExperimentsAll = inMemoryExperimentsAll + require.ElementsMatch(t, codersdk.ExperimentsAll, experiments) }) } From 242231630ec9481b4492140f2b0f73cf066cb843 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 16 Oct 2023 18:47:31 +0000 Subject: [PATCH 08/13] added new route for safe experiments --- coderd/experiments_test.go | 28 ++++--------------- site/src/api/api.ts | 19 ++++++++++--- site/src/api/queries/experiments.ts | 8 +++--- site/src/api/typesGenerated.ts | 10 +++---- .../GeneralSettingsPage.tsx | 6 ++-- .../GeneralSettingsPageView.stories.tsx | 4 +-- .../GeneralSettingsPageView.tsx | 6 ++-- 7 files changed, 37 insertions(+), 44 deletions(-) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index 5a6f2a9f894ba..0f498e7e7cf2b 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -25,7 +25,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) + experiments, err := client.Experiments(ctx) require.NoError(t, err) require.NotNil(t, experiments) require.Empty(t, experiments) @@ -44,7 +44,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) + experiments, err := client.Experiments(ctx) require.NoError(t, err) require.NotNil(t, experiments) // Should be lower-cased. @@ -66,7 +66,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) + experiments, err := client.Experiments(ctx) require.NoError(t, err) require.NotNil(t, experiments) require.ElementsMatch(t, codersdk.ExperimentsAll, experiments) @@ -88,7 +88,7 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) + experiments, err := client.Experiments(ctx) require.NoError(t, err) require.NotNil(t, experiments) require.ElementsMatch(t, append(codersdk.ExperimentsAll, "danger"), experiments) @@ -112,26 +112,8 @@ func Test_Experiments(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) defer cancel() - _, err := client.Experiments(ctx, codersdk.ExperimentOptions{}) + _, err := client.Experiments(ctx) require.Error(t, err) require.ErrorContains(t, err, httpmw.SignedOutErrorMessage) }) - - t.Run("include_all query param", func(t *testing.T) { - t.Parallel() - cfg := coderdtest.DeploymentValues(t) - cfg.Experiments = []string{"foo", "BAR"} - client := coderdtest.New(t, &coderdtest.Options{ - DeploymentValues: cfg, - }) - _ = coderdtest.CreateFirstUser(t, client) - - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() - - experiments, err := client.Experiments(ctx, codersdk.ExperimentOptions{IncludeAll: true}) - require.NoError(t, err) - require.NotNil(t, experiments) - require.ElementsMatch(t, codersdk.ExperimentsAll, experiments) - }) } diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 85f9a3399cdd8..dccdc383ccbf9 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -852,11 +852,9 @@ export const getEntitlements = async (): Promise => { } }; -export const getExperiments = async ( - params?: TypesGen.ExperimentOptions, -): Promise => { +export const getExperiments = async (): Promise => { try { - const response = await axios.get("/api/v2/experiments", { params }); + const response = await axios.get("/api/v2/experiments"); return response.data; } catch (error) { if (axios.isAxiosError(error) && error.response?.status === 404) { @@ -866,6 +864,19 @@ export const getExperiments = async ( } }; +export const getAvailableExperiments = + async (): Promise => { + try { + const response = await axios.get("/api/v2/experiments/available"); + return response.data; + } catch (error) { + if (axios.isAxiosError(error) && error.response?.status === 404) { + return { safe: [] }; + } + throw error; + } + }; + export const getExternalAuthProvider = async ( provider: string, ): Promise => { diff --git a/site/src/api/queries/experiments.ts b/site/src/api/queries/experiments.ts index 7ab53cd533ad2..c24941d79273b 100644 --- a/site/src/api/queries/experiments.ts +++ b/site/src/api/queries/experiments.ts @@ -1,5 +1,5 @@ import * as API from "api/api"; -import { Experiments, ExperimentOptions } from "api/typesGenerated"; +import { Experiments } from "api/typesGenerated"; import { getMetadataAsJSON } from "utils/metadata"; export const experiments = () => { @@ -10,9 +10,9 @@ export const experiments = () => { }; }; -export const updatedExperiments = (params?: ExperimentOptions) => { +export const availableExperiments = () => { return { - queryKey: ["experiments"], - queryFn: async () => API.getExperiments(params), + queryKey: ["availableExperiments"], + queryFn: async () => API.getAvailableExperiments(), }; }; diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 2e04230833f80..29fbd56f5e316 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -151,6 +151,11 @@ export interface AuthorizationRequest { // From codersdk/authorization.go export type AuthorizationResponse = Record; +// From codersdk/deployment.go +export interface AvailableExperiments { + readonly safe: Experiment[]; +} + // From codersdk/deployment.go export interface BuildInfoResponse { readonly external_url: string; @@ -433,11 +438,6 @@ export interface Entitlements { readonly refreshed_at: string; } -// From codersdk/deployment.go -export interface ExperimentOptions { - readonly include_all?: boolean; -} - // From codersdk/deployment.go export type Experiments = Experiment[]; diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx index 0deacc837385d..a4f45ae7a270f 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPage.tsx @@ -4,7 +4,7 @@ import { useQuery } from "react-query"; import { pageTitle } from "utils/page"; import { deploymentDAUs } from "api/queries/deployment"; import { entitlements } from "api/queries/entitlements"; -import { updatedExperiments } from "api/queries/experiments"; +import { availableExperiments } from "api/queries/experiments"; import { useDeploySettings } from "components/DeploySettingsLayout/DeploySettingsLayout"; import { GeneralSettingsPageView } from "./GeneralSettingsPageView"; @@ -12,7 +12,7 @@ const GeneralSettingsPage: FC = () => { const { deploymentValues } = useDeploySettings(); const deploymentDAUsQuery = useQuery(deploymentDAUs()); const entitlementsQuery = useQuery(entitlements()); - const experimentsQuery = useQuery(updatedExperiments({ include_all: true })); + const experimentsQuery = useQuery(availableExperiments()); return ( <> @@ -24,7 +24,7 @@ const GeneralSettingsPage: FC = () => { deploymentDAUs={deploymentDAUsQuery.data} deploymentDAUsError={deploymentDAUsQuery.error} entitlements={entitlementsQuery.data} - allExperiments={experimentsQuery.data} + safeExperiments={experimentsQuery.data?.safe ?? []} /> ); diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx index 0dad6c14cee17..2b3ec0afa13d5 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.stories.tsx @@ -40,7 +40,7 @@ const meta: Meta = { }, ], deploymentDAUs: MockDeploymentDAUResponse, - allExperiments: ["single_tailnet", "deployment_health_page"], + safeExperiments: ["single_tailnet", "deployment_health_page"], }, }; @@ -102,6 +102,6 @@ export const allExperimentsEnabled: Story = { hidden: false, }, ], - allExperiments: ["single_tailnet", "deployment_health_page"], + safeExperiments: ["single_tailnet", "deployment_health_page"], }, }; diff --git a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx index ffaf2236b46db..f5a3f2abf2c59 100644 --- a/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx +++ b/site/src/pages/DeploySettingsPage/GeneralSettingsPage/GeneralSettingsPageView.tsx @@ -22,14 +22,14 @@ export type GeneralSettingsPageViewProps = { deploymentDAUs?: DAUsResponse; deploymentDAUsError: unknown; entitlements: Entitlements | undefined; - allExperiments: Experiments | undefined; + safeExperiments: Experiments | undefined; }; export const GeneralSettingsPageView = ({ deploymentOptions, deploymentDAUs, deploymentDAUsError, entitlements, - allExperiments, + safeExperiments, }: GeneralSettingsPageViewProps): JSX.Element => { return ( <> @@ -64,7 +64,7 @@ export const GeneralSettingsPageView = ({ "Wildcard Access URL", "Experiments", )} - additionalValues={allExperiments} + additionalValues={safeExperiments} /> From 308dbb1bff3b689a3e1cdbe39de93707dde3cced Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Mon, 16 Oct 2023 18:47:51 +0000 Subject: [PATCH 09/13] added new route for safe experiments --- coderd/apidoc/docs.go | 34 +++++++++++++++++++++++------- coderd/apidoc/swagger.json | 30 +++++++++++++++++++------- coderd/coderd.go | 1 + coderd/experiments.go | 26 ++++++++++++++--------- codersdk/deployment.go | 33 +++++++++++++++-------------- docs/api/general.md | 43 ++++++++++++++++++++++++++++++++------ 6 files changed, 121 insertions(+), 46 deletions(-) diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 0333a91a34418..b5041179736d2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -587,16 +587,36 @@ const docTemplate = `{ "tags": [ "General" ], - "summary": "Get experiments", - "operationId": "get-experiments", - "parameters": [ + "summary": "Get enabled experiments", + "operationId": "get-enabled-experiments", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Experiment" + } + } + } + } + } + }, + "/experiments/available": { + "get": { + "security": [ { - "type": "boolean", - "description": "All available experiments", - "name": "include_all", - "in": "query" + "CoderSessionToken": [] } ], + "produces": [ + "application/json" + ], + "tags": [ + "General" + ], + "summary": "Get safe experiments", + "operationId": "get-safe-experiments", "responses": { "200": { "description": "OK", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 5380be808d1b0..6e2e5c0902ddb 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -497,16 +497,32 @@ ], "produces": ["application/json"], "tags": ["General"], - "summary": "Get experiments", - "operationId": "get-experiments", - "parameters": [ + "summary": "Get enabled experiments", + "operationId": "get-enabled-experiments", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/codersdk.Experiment" + } + } + } + } + } + }, + "/experiments/available": { + "get": { + "security": [ { - "type": "boolean", - "description": "All available experiments", - "name": "include_all", - "in": "query" + "CoderSessionToken": [] } ], + "produces": ["application/json"], + "tags": ["General"], + "summary": "Get safe experiments", + "operationId": "get-safe-experiments", "responses": { "200": { "description": "OK", diff --git a/coderd/coderd.go b/coderd/coderd.go index f301265cc5ad7..2210dcb3632d2 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -597,6 +597,7 @@ func New(options *Options) *API { }) r.Route("/experiments", func(r chi.Router) { r.Use(apiKeyMiddleware) + r.Get("/available", api.handleExperimentsSafe) r.Get("/", api.handleExperimentsGet) }) r.Get("/updatecheck", api.updateCheck) diff --git a/coderd/experiments.go b/coderd/experiments.go index 4d66f97cd4f13..abf155690f212 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -7,22 +7,28 @@ import ( "github.com/coder/coder/v2/codersdk" ) -// @Summary Get experiments -// @ID get-experiments +// @Summary Get enabled experiments +// @ID get-enabled-experiments // @Security CoderSessionToken // @Produce json // @Tags General -// @Param include_all query bool false "All available experiments" // @Success 200 {array} codersdk.Experiment // @Router /experiments [get] func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() - all := r.URL.Query().Has("include_all") - - if !all { - httpapi.Write(ctx, rw, http.StatusOK, api.Experiments) - return - } + httpapi.Write(ctx, rw, http.StatusOK, api.Experiments) +} - httpapi.Write(ctx, rw, http.StatusOK, codersdk.ExperimentsAll) +// @Summary Get safe experiments +// @ID get-safe-experiments +// @Security CoderSessionToken +// @Produce json +// @Tags General +// @Success 200 {array} codersdk.Experiment +// @Router /experiments/available [get] +func (api *API) handleExperimentsSafe(rw http.ResponseWriter, r *http.Request) { + ctx := r.Context() + httpapi.Write(ctx, rw, http.StatusOK, codersdk.AvailableExperiments{ + Safe: codersdk.ExperimentsAll, + }) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index afd4b9dbaab22..535c643de68b6 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -2029,26 +2029,27 @@ func (e Experiments) Enabled(ex Experiment) bool { return false } -type ExperimentOptions struct { - // All signifies that all experiments - rather than just those that are enabled - - // should be returned - IncludeAll bool `json:"include_all,omitempty"` +func (c *Client) Experiments(ctx context.Context) (Experiments, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil) + if err != nil { + return nil, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return nil, ReadBodyAsError(res) + } + var exp []Experiment + return exp, json.NewDecoder(res.Body).Decode(&exp) } -// asRequestOption returns a function that can be used in (*Client).Request. -// It modifies the request query parameters. -func (o ExperimentOptions) asRequestOption() RequestOption { - return func(r *http.Request) { - q := r.URL.Query() - if o.IncludeAll { - q.Set("include_all", "true") - } - r.URL.RawQuery = q.Encode() - } +// AvailableExperiments is an expandable type that returns all safe experiments +// available to be used with a deployment. +type AvailableExperiments struct { + Safe []Experiment `json:"safe"` } -func (c *Client) Experiments(ctx context.Context, opts ExperimentOptions) (Experiments, error) { - res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments", nil, opts.asRequestOption()) +func (c *Client) SafeExperiments(ctx context.Context) (Experiments, error) { + res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments/available", nil) if err != nil { return nil, err } diff --git a/docs/api/general.md b/docs/api/general.md index acf1b3ca8ec1f..577781136ef61 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -535,7 +535,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/stats \ To perform this operation, you must be authenticated. [Learn more](authentication.md). -## Get experiments +## Get enabled experiments ### Code samples @@ -548,11 +548,42 @@ curl -X GET http://coder-server:8080/api/v2/experiments \ `GET /experiments` -### Parameters +### Example responses + +> 200 Response + +```json +["moons"] +``` + +### Responses + +| Status | Meaning | Description | Schema | +| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------- | +| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Experiment](schemas.md#codersdkexperiment) | + +

      Response Schema

      + +Status Code **200** + +| Name | Type | Required | Restrictions | Description | +| -------------- | ----- | -------- | ------------ | ----------- | +| `[array item]` | array | false | | | + +To perform this operation, you must be authenticated. [Learn more](authentication.md). + +## Get safe experiments + +### Code samples + +```shell +# Example request using curl +curl -X GET http://coder-server:8080/api/v2/experiments/available \ + -H 'Accept: application/json' \ + -H 'Coder-Session-Token: API_KEY' +``` -| Name | In | Type | Required | Description | -| ------------- | ----- | ------- | -------- | ------------------------- | -| `include_all` | query | boolean | false | All available experiments | +`GET /experiments/available` ### Example responses @@ -568,7 +599,7 @@ curl -X GET http://coder-server:8080/api/v2/experiments \ | ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------- | | 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.Experiment](schemas.md#codersdkexperiment) | -

      Response Schema

      +

      Response Schema

      Status Code **200** From ad7a3d08d419c1546263555fbeb25c046158db48 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 17 Oct 2023 00:21:46 +0000 Subject: [PATCH 10/13] added test for new route --- coderd/experiments_test.go | 17 +++++++++++++++++ codersdk/deployment.go | 8 ++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/coderd/experiments_test.go b/coderd/experiments_test.go index 0f498e7e7cf2b..4288b9953fec6 100644 --- a/coderd/experiments_test.go +++ b/coderd/experiments_test.go @@ -116,4 +116,21 @@ func Test_Experiments(t *testing.T) { require.Error(t, err) require.ErrorContains(t, err, httpmw.SignedOutErrorMessage) }) + + t.Run("available experiments", func(t *testing.T) { + t.Parallel() + cfg := coderdtest.DeploymentValues(t) + client := coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: cfg, + }) + _ = coderdtest.CreateFirstUser(t, client) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + experiments, err := client.SafeExperiments(ctx) + require.NoError(t, err) + require.NotNil(t, experiments) + require.ElementsMatch(t, codersdk.ExperimentsAll, experiments.Safe) + }) } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 535c643de68b6..4622808853aa5 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -2048,16 +2048,16 @@ type AvailableExperiments struct { Safe []Experiment `json:"safe"` } -func (c *Client) SafeExperiments(ctx context.Context) (Experiments, error) { +func (c *Client) SafeExperiments(ctx context.Context) (AvailableExperiments, error) { res, err := c.Request(ctx, http.MethodGet, "/api/v2/experiments/available", nil) if err != nil { - return nil, err + return AvailableExperiments{}, err } defer res.Body.Close() if res.StatusCode != http.StatusOK { - return nil, ReadBodyAsError(res) + return AvailableExperiments{}, ReadBodyAsError(res) } - var exp []Experiment + var exp AvailableExperiments return exp, json.NewDecoder(res.Body).Decode(&exp) } From 82a513ba339beea242d829b51e71688b8efee739 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 17 Oct 2023 01:25:20 +0000 Subject: [PATCH 11/13] PR feedback --- .../DeploySettingsLayout/Option.tsx | 46 ++++++++++--------- .../DeploySettingsLayout/optionValue.ts | 9 ++-- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/site/src/components/DeploySettingsLayout/Option.tsx b/site/src/components/DeploySettingsLayout/Option.tsx index 604f1d7490185..6a8424675972c 100644 --- a/site/src/components/DeploySettingsLayout/Option.tsx +++ b/site/src/components/DeploySettingsLayout/Option.tsx @@ -85,28 +85,30 @@ export const OptionValue: FC = (props) => { if (typeof children === "object" && !Array.isArray(children)) { return (
        - {Object.entries(children).map(([option, isEnabled]) => ( -
      • - - {option} - {isEnabled && ( - theme.palette.success.light, - margin: (theme) => theme.spacing(0, 1), - }} - /> - )} - -
      • - ))} + {Object.entries(children) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([option, isEnabled]) => ( +
      • + + {option} + {isEnabled && ( + theme.palette.success.light, + margin: (theme) => theme.spacing(0, 1), + }} + /> + )} + +
      • + ))}
      ); } diff --git a/site/src/components/DeploySettingsLayout/optionValue.ts b/site/src/components/DeploySettingsLayout/optionValue.ts index 0b6ed8ef7b7cd..6221356075fbb 100644 --- a/site/src/components/DeploySettingsLayout/optionValue.ts +++ b/site/src/components/DeploySettingsLayout/optionValue.ts @@ -31,11 +31,12 @@ export function optionValue( {} as Record, ); + if (!experimentMap) { + break; + } + for (const v of option.value) { - if ( - experimentMap && - Object.prototype.hasOwnProperty.call(experimentMap, v) - ) { + if (Object.hasOwn(experimentMap, v)) { experimentMap[v] = true; } } From 42e8bd6da5e8a03e434075ba6f7c17a1546a9ce6 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 17 Oct 2023 01:58:20 +0000 Subject: [PATCH 12/13] altered design --- coderd/coderd.go | 2 +- coderd/experiments.go | 2 +- .../components/DeploySettingsLayout/Option.tsx | 18 +++++++++++++----- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/coderd/coderd.go b/coderd/coderd.go index 2210dcb3632d2..7ab2e578462b1 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -597,7 +597,7 @@ func New(options *Options) *API { }) r.Route("/experiments", func(r chi.Router) { r.Use(apiKeyMiddleware) - r.Get("/available", api.handleExperimentsSafe) + r.Get("/available", handleExperimentsSafe) r.Get("/", api.handleExperimentsGet) }) r.Get("/updatecheck", api.updateCheck) diff --git a/coderd/experiments.go b/coderd/experiments.go index abf155690f212..f7debd8c68bbb 100644 --- a/coderd/experiments.go +++ b/coderd/experiments.go @@ -26,7 +26,7 @@ func (api *API) handleExperimentsGet(rw http.ResponseWriter, r *http.Request) { // @Tags General // @Success 200 {array} codersdk.Experiment // @Router /experiments/available [get] -func (api *API) handleExperimentsSafe(rw http.ResponseWriter, r *http.Request) { +func handleExperimentsSafe(rw http.ResponseWriter, r *http.Request) { ctx := r.Context() httpapi.Write(ctx, rw, http.StatusOK, codersdk.AvailableExperiments{ Safe: codersdk.ExperimentsAll, diff --git a/site/src/components/DeploySettingsLayout/Option.tsx b/site/src/components/DeploySettingsLayout/Option.tsx index 6a8424675972c..9e4d529522a3e 100644 --- a/site/src/components/DeploySettingsLayout/Option.tsx +++ b/site/src/components/DeploySettingsLayout/Option.tsx @@ -60,7 +60,6 @@ export const OptionValue: FC = (props) => { const listStyles = css` margin: 0, padding: 0, - listStylePosition: "inside", display: "flex", flexDirection: "column", gap: theme.spacing(0.5), @@ -84,18 +83,26 @@ export const OptionValue: FC = (props) => { if (typeof children === "object" && !Array.isArray(children)) { return ( -
        +
          {Object.entries(children) .sort((a, b) => a[0].localeCompare(b[0])) .map(([option, isEnabled]) => ( -
        • +
        • - {option} {isEnabled && ( = (props) => { }} /> )} + {option}
        • ))} @@ -115,7 +123,7 @@ export const OptionValue: FC = (props) => { if (Array.isArray(children)) { return ( -
            +
              {children.map((item) => (
            • {item} From 9ed5f35ac939f2f8a67ce0d8b397e295a1304bc7 Mon Sep 17 00:00:00 2001 From: Kira Pilot Date: Tue, 17 Oct 2023 17:35:13 +0000 Subject: [PATCH 13/13] alias children --- .../DeploySettingsLayout/Option.tsx | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/site/src/components/DeploySettingsLayout/Option.tsx b/site/src/components/DeploySettingsLayout/Option.tsx index 9e4d529522a3e..e8c51eac1e5f1 100644 --- a/site/src/components/DeploySettingsLayout/Option.tsx +++ b/site/src/components/DeploySettingsLayout/Option.tsx @@ -43,7 +43,7 @@ interface OptionValueProps { } export const OptionValue: FC = (props) => { - const { children } = props; + const { children: value } = props; const theme = useTheme(); const optionStyles = css` @@ -61,41 +61,41 @@ export const OptionValue: FC = (props) => { margin: 0, padding: 0, display: "flex", - flexDirection: "column", + flex-direction: "column", gap: theme.spacing(0.5), `; - if (typeof children === "boolean") { - return children ? : ; + if (typeof value === "boolean") { + return value ? : ; } - if (typeof children === "number") { - return {children}; + if (typeof value === "number") { + return {value}; } - if (!children || children.length === 0) { + if (!value || value.length === 0) { return Not set; } - if (typeof children === "string") { - return {children}; + if (typeof value === "string") { + return {value}; } - if (typeof children === "object" && !Array.isArray(children)) { + if (typeof value === "object" && !Array.isArray(value)) { return (
                - {Object.entries(children) + {Object.entries(value) .sort((a, b) => a[0].localeCompare(b[0])) .map(([option, isEnabled]) => (
              • = (props) => { ); } - if (Array.isArray(children)) { + if (Array.isArray(value)) { return (
                  - {children.map((item) => ( + {value.map((item) => (
                • {item}
                • @@ -133,7 +133,7 @@ export const OptionValue: FC = (props) => { ); } - return {JSON.stringify(children)}; + return {JSON.stringify(value)}; }; interface OptionConfigProps extends BoxProps {