diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 61a6989ba27fd..9faf9de1611cc 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -8024,7 +8024,8 @@ const docTemplate = `{ "tailnet_pg_coordinator", "single_tailnet", "template_restart_requirement", - "deployment_health_page" + "deployment_health_page", + "template_parameters_insights" ], "x-enum-varnames": [ "ExperimentMoons", @@ -8032,7 +8033,8 @@ const docTemplate = `{ "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", "ExperimentTemplateRestartRequirement", - "ExperimentDeploymentHealthPage" + "ExperimentDeploymentHealthPage", + "ExperimentTemplateParametersInsights" ] }, "codersdk.Feature": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index 982ed816264c1..635457901db36 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7185,7 +7185,8 @@ "tailnet_pg_coordinator", "single_tailnet", "template_restart_requirement", - "deployment_health_page" + "deployment_health_page", + "template_parameters_insights" ], "x-enum-varnames": [ "ExperimentMoons", @@ -7193,7 +7194,8 @@ "ExperimentTailnetPGCoordinator", "ExperimentSingleTailnet", "ExperimentTemplateRestartRequirement", - "ExperimentDeploymentHealthPage" + "ExperimentDeploymentHealthPage", + "ExperimentTemplateParametersInsights" ] }, "codersdk.Feature": { diff --git a/codersdk/deployment.go b/codersdk/deployment.go index e714d9c1c34b5..fdc5b92af28de 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -1881,6 +1881,9 @@ const ( // Deployment health page ExperimentDeploymentHealthPage Experiment = "deployment_health_page" + // Template parameters insights + ExperimentTemplateParametersInsights Experiment = "template_parameters_insights" + // Add new experiments here! // ExperimentExample Experiment = "example" ) @@ -1891,6 +1894,7 @@ const ( // not be included here and will be essentially hidden. var ExperimentsAll = Experiments{ ExperimentDeploymentHealthPage, + ExperimentTemplateParametersInsights, } // Experiments is a list of experiments that are enabled for the deployment. diff --git a/docs/api/schemas.md b/docs/api/schemas.md index 2f09bfb6728ae..4ab6eceeef96d 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -2679,6 +2679,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `single_tailnet` | | `template_restart_requirement` | | `deployment_health_page` | +| `template_parameters_insights` | ## codersdk.Feature diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 7b9b79d69b3db..c01f4d94385ba 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -1586,6 +1586,7 @@ export type Experiment = | "moons" | "single_tailnet" | "tailnet_pg_coordinator" + | "template_parameters_insights" | "template_restart_requirement" | "workspace_actions" export const Experiments: Experiment[] = [ @@ -1593,6 +1594,7 @@ export const Experiments: Experiment[] = [ "moons", "single_tailnet", "tailnet_pg_coordinator", + "template_parameters_insights", "template_restart_requirement", "workspace_actions", ] diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx index 49678eef08375..4fb65c0b2b5f1 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.stories.tsx @@ -83,7 +83,376 @@ export const Loaded: Story = { seconds: 1020900, }, ], - parameters_usage: [], + parameters_usage: [ + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Compute instances", + type: "number", + description: "Let's set the expected number of instances.", + values: [ + { + value: "3", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Docker Image", + type: "string", + description: "Docker image for the development container", + values: [ + { + value: "ghcr.io/harrison-ai/coder-dev:base", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Very random string", + name: "Optional random string", + type: "string", + description: "This string is optional", + values: [ + { + value: "ksjdlkajs;djálskd'l ;a k;aosdk ;oaids ;li", + count: 1, + }, + { + value: "some other any string here", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Region", + type: "string", + description: "These are options.", + options: [ + { + name: "US Central", + description: "Select for central!", + value: "us-central1-a", + icon: "/icon/goland.svg", + }, + { + name: "US East", + description: "Select for east!", + value: "us-east1-a", + icon: "/icon/folder.svg", + }, + { + name: "US West", + description: "Select for west!", + value: "us-west2-a", + icon: "", + }, + ], + values: [ + { + value: "us-central1-a", + count: 1, + }, + { + value: "us-west2-a", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "Security groups", + type: "list(string)", + description: "Select appropriate security groups.", + values: [ + { + value: + '["Web Server Security Group","Database Security Group","Backend Security Group"]', + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Very random string", + name: "buggy-1", + type: "string", + description: "This string is buggy", + values: [ + { + value: "", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Force rebuild", + name: "force-rebuild", + type: "bool", + description: "Rebuild the project code", + values: [ + { + value: "false", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Location", + name: "location", + type: "string", + description: "What location should your workspace live in?", + options: [ + { + name: "US (Virginia)", + description: "", + value: "eastus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Virginia) 2", + description: "", + value: "eastus2", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Texas)", + description: "", + value: "southcentralus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Washington)", + description: "", + value: "westus2", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Arizona)", + description: "", + value: "westus3", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "US (Iowa)", + description: "", + value: "centralus", + icon: "/emojis/1f1fa-1f1f8.png", + }, + { + name: "Canada (Toronto)", + description: "", + value: "canadacentral", + icon: "/emojis/1f1e8-1f1e6.png", + }, + { + name: "Brazil (Sao Paulo)", + description: "", + value: "brazilsouth", + icon: "/emojis/1f1e7-1f1f7.png", + }, + { + name: "East Asia (Hong Kong)", + description: "", + value: "eastasia", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Southeast Asia (Singapore)", + description: "", + value: "southeastasia", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Australia (New South Wales)", + description: "", + value: "australiaeast", + icon: "/emojis/1f1e6-1f1fa.png", + }, + { + name: "China (Hebei)", + description: "", + value: "chinanorth3", + icon: "/emojis/1f1e8-1f1f3.png", + }, + { + name: "India (Pune)", + description: "", + value: "centralindia", + icon: "/emojis/1f1ee-1f1f3.png", + }, + { + name: "Japan (Tokyo)", + description: "", + value: "japaneast", + icon: "/emojis/1f1ef-1f1f5.png", + }, + { + name: "Korea (Seoul)", + description: "", + value: "koreacentral", + icon: "/emojis/1f1f0-1f1f7.png", + }, + { + name: "Europe (Ireland)", + description: "", + value: "northeurope", + icon: "/emojis/1f1ea-1f1fa.png", + }, + { + name: "Europe (Netherlands)", + description: "", + value: "westeurope", + icon: "/emojis/1f1ea-1f1fa.png", + }, + { + name: "France (Paris)", + description: "", + value: "francecentral", + icon: "/emojis/1f1eb-1f1f7.png", + }, + { + name: "Germany (Frankfurt)", + description: "", + value: "germanywestcentral", + icon: "/emojis/1f1e9-1f1ea.png", + }, + { + name: "Norway (Oslo)", + description: "", + value: "norwayeast", + icon: "/emojis/1f1f3-1f1f4.png", + }, + { + name: "Sweden (Gävle)", + description: "", + value: "swedencentral", + icon: "/emojis/1f1f8-1f1ea.png", + }, + { + name: "Switzerland (Zurich)", + description: "", + value: "switzerlandnorth", + icon: "/emojis/1f1e8-1f1ed.png", + }, + { + name: "Qatar (Doha)", + description: "", + value: "qatarcentral", + icon: "/emojis/1f1f6-1f1e6.png", + }, + { + name: "UAE (Dubai)", + description: "", + value: "uaenorth", + icon: "/emojis/1f1e6-1f1ea.png", + }, + { + name: "South Africa (Johannesburg)", + description: "", + value: "southafricanorth", + icon: "/emojis/1f1ff-1f1e6.png", + }, + { + name: "UK (London)", + description: "", + value: "uksouth", + icon: "/emojis/1f1ec-1f1e7.png", + }, + ], + values: [ + { + value: "brazilsouth", + count: 1, + }, + { + value: "switzerlandnorth", + count: 1, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "", + name: "mtojek_region", + type: "string", + description: "What region should your workspace live in?", + options: [ + { + name: "Los Angeles, CA", + description: "", + value: "Los Angeles, CA", + icon: "", + }, + { + name: "Moncks Corner, SC", + description: "", + value: "Moncks Corner, SC", + icon: "", + }, + { + name: "Eemshaven, NL", + description: "", + value: "Eemshaven, NL", + icon: "", + }, + ], + values: [ + { + value: "Los Angeles, CA", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "My Project ID", + name: "project_id", + type: "string", + description: "This is the Project ID.", + values: [ + { + value: "12345", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Force devcontainer rebuild", + name: "rebuild_devcontainer", + type: "bool", + description: "", + values: [ + { + value: "false", + count: 2, + }, + ], + }, + { + template_ids: ["7dd1d090-3e23-4ada-8894-3945affcad42"], + display_name: "Git Repo URL", + name: "repo_url", + type: "string", + description: + "See sample projects (https://github.com/microsoft/vscode-dev-containers#sample-projects)", + values: [ + { + value: "https://github.com/mtojek/coder", + count: 2, + }, + ], + }, + ], }, interval_reports: [ { diff --git a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx index d6161124d3540..77bf66a0759e9 100644 --- a/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx +++ b/site/src/pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx @@ -21,10 +21,17 @@ import { Loader } from "components/Loader/Loader" import { DAUsResponse, TemplateInsightsResponse, + TemplateParameterUsage, + TemplateParameterValue, UserLatencyInsightsResponse, } from "api/typesGenerated" import { ComponentProps } from "react" import { subDays, addHours, startOfHour } from "date-fns" +import { useDashboard } from "components/Dashboard/DashboardProvider" +import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined" +import Link from "@mui/material/Link" +import CheckCircleOutlined from "@mui/icons-material/CheckCircleOutlined" +import CancelOutlined from "@mui/icons-material/CancelOutlined" export default function TemplateInsightsPage() { const now = new Date() @@ -42,6 +49,10 @@ export default function TemplateInsightsPage() { queryKey: ["templates", template.id, "user-latency"], queryFn: () => getInsightsUserLatency(insightsFilter), }) + const dashboard = useDashboard() + const shouldDisplayParameters = + dashboard.experiments.includes("template_parameters_insights") || + process.env.NODE_ENV === "development" return ( <> @@ -51,6 +62,7 @@ export default function TemplateInsightsPage() { ) @@ -59,9 +71,11 @@ export default function TemplateInsightsPage() { export const TemplateInsightsPageView = ({ templateInsights, userLatency, + shouldDisplayParameters, }: { templateInsights: TemplateInsightsResponse | undefined userLatency: UserLatencyInsightsResponse | undefined + shouldDisplayParameters: boolean }) => { return ( + {shouldDisplayParameters && ( + + )} ) } @@ -261,6 +281,219 @@ const TemplateUsagePanel = ({ ) } +const TemplateParametersUsagePanel = ({ + data, + ...panelProps +}: PanelProps & { + data: TemplateInsightsResponse["report"]["parameters_usage"] | undefined +}) => { + return ( + + + Parameters usage + Last 7 days + + + {!data && } + {data && data.length === 0 && } + {data && + data.length > 0 && + data.map((parameter, parameterIndex) => { + const label = + parameter.display_name !== "" + ? parameter.display_name + : parameter.name + return ( + `1px solid ${theme.palette.divider}`, + width: (theme) => `calc(100% + ${theme.spacing(6)})`, + "&:first-child": { + borderTop: 0, + }, + }} + > + + {label} + theme.palette.text.secondary, + maxWidth: 400, + margin: 0, + }} + > + {parameter.description} + + + + {parameter.values + .sort((a, b) => b.count - a.count) + .map((usage, usageIndex) => ( + + + {usage.count} + + ))} + + + ) + })} + + + ) +} + +const ParameterUsageLabel = ({ + usage, + parameter, +}: { + usage: TemplateParameterValue + parameter: TemplateParameterUsage +}) => { + if (usage.value.trim() === "") { + return ( + theme.palette.text.secondary, + }} + > + Not set + + ) + } + + if (parameter.options) { + const option = parameter.options.find((o) => o.value === usage.value)! + const icon = option.icon + const label = option.name + + return ( + + {icon && ( + + + + )} + {label} + + ) + } + + if (usage.value.startsWith("http")) { + return ( + theme.palette.text.primary, + }} + > + + {usage.value} + + ) + } + + if (parameter.type === "list(string)") { + const values = JSON.parse(usage.value) as string[] + return ( + + {values.map((v, i) => { + return ( + theme.spacing(0.25, 1.5), + borderRadius: 999, + background: (theme) => theme.palette.divider, + whiteSpace: "nowrap", + }} + > + {v} + + ) + })} + + ) + } + + if (parameter.type === "bool") { + return ( + + {usage.value === "false" ? ( + <> + theme.palette.error.light, + }} + /> + False + + ) : ( + <> + theme.palette.success.light, + }} + /> + True + + )} + + ) + } + + return {usage.value} +} + const Panel = styled(Box)(({ theme }) => ({ borderRadius: theme.shape.borderRadius, border: `1px solid ${theme.palette.divider}`,