diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go
index d487af5691bca..0a1c9fcbeaf87 100644
--- a/cli/exp_mcp.go
+++ b/cli/exp_mcp.go
@@ -585,10 +585,10 @@ func (s *mcpServer) startWatcher(ctx context.Context, inv *serpent.Invocation) {
case event := <-eventsCh:
switch ev := event.(type) {
case agentapi.EventStatusChange:
- // If the screen is stable, assume complete.
+ // If the screen is stable, report idle.
state := codersdk.WorkspaceAppStatusStateWorking
if ev.Status == agentapi.StatusStable {
- state = codersdk.WorkspaceAppStatusStateComplete
+ state = codersdk.WorkspaceAppStatusStateIdle
}
err := s.queue.Push(taskReport{
state: state,
diff --git a/cli/exp_mcp_test.go b/cli/exp_mcp_test.go
index 08d6fbc4e2ce6..bcfafb0204874 100644
--- a/cli/exp_mcp_test.go
+++ b/cli/exp_mcp_test.go
@@ -900,7 +900,7 @@ func TestExpMcpReporter(t *testing.T) {
{
event: makeStatusEvent(agentapi.StatusStable),
expected: &codersdk.WorkspaceAppStatus{
- State: codersdk.WorkspaceAppStatusStateComplete,
+ State: codersdk.WorkspaceAppStatusStateIdle,
Message: "doing work",
URI: "https://dev.coder.com",
},
@@ -948,7 +948,7 @@ func TestExpMcpReporter(t *testing.T) {
{
event: makeStatusEvent(agentapi.StatusStable),
expected: &codersdk.WorkspaceAppStatus{
- State: codersdk.WorkspaceAppStatusStateComplete,
+ State: codersdk.WorkspaceAppStatusStateIdle,
Message: "oops",
URI: "",
},
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index 1d175333c1271..6844d166d8f1d 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -18081,11 +18081,13 @@ const docTemplate = `{
"type": "string",
"enum": [
"working",
+ "idle",
"complete",
"failure"
],
"x-enum-varnames": [
"WorkspaceAppStatusStateWorking",
+ "WorkspaceAppStatusStateIdle",
"WorkspaceAppStatusStateComplete",
"WorkspaceAppStatusStateFailure"
]
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index 9d00a7ba34c30..35d39cfbe3839 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -16522,9 +16522,10 @@
},
"codersdk.WorkspaceAppStatusState": {
"type": "string",
- "enum": ["working", "complete", "failure"],
+ "enum": ["working", "idle", "complete", "failure"],
"x-enum-varnames": [
"WorkspaceAppStatusStateWorking",
+ "WorkspaceAppStatusStateIdle",
"WorkspaceAppStatusStateComplete",
"WorkspaceAppStatusStateFailure"
]
diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql
index 2a94ef0fe7b4e..17b16abc5715c 100644
--- a/coderd/database/dump.sql
+++ b/coderd/database/dump.sql
@@ -324,7 +324,8 @@ CREATE TYPE workspace_app_open_in AS ENUM (
CREATE TYPE workspace_app_status_state AS ENUM (
'working',
'complete',
- 'failure'
+ 'failure',
+ 'idle'
);
CREATE TYPE workspace_transition AS ENUM (
diff --git a/coderd/database/migrations/000340_workspace_app_status_idle.down.sql b/coderd/database/migrations/000340_workspace_app_status_idle.down.sql
new file mode 100644
index 0000000000000..a5d2095b1cd4a
--- /dev/null
+++ b/coderd/database/migrations/000340_workspace_app_status_idle.down.sql
@@ -0,0 +1,15 @@
+-- It is not possible to delete a value from an enum, so we have to recreate it.
+CREATE TYPE old_workspace_app_status_state AS ENUM ('working', 'complete', 'failure');
+
+-- Convert the new "idle" state into "complete". This means we lose some
+-- information when downgrading, but this is necessary to swap to the old enum.
+UPDATE workspace_app_statuses SET state = 'complete' WHERE state = 'idle';
+
+-- Swap to the old enum.
+ALTER TABLE workspace_app_statuses
+ALTER COLUMN state TYPE old_workspace_app_status_state
+USING (state::text::old_workspace_app_status_state);
+
+-- Drop the new enum and rename the old one to the final name.
+DROP TYPE workspace_app_status_state;
+ALTER TYPE old_workspace_app_status_state RENAME TO workspace_app_status_state;
diff --git a/coderd/database/migrations/000340_workspace_app_status_idle.up.sql b/coderd/database/migrations/000340_workspace_app_status_idle.up.sql
new file mode 100644
index 0000000000000..1630e3580f45c
--- /dev/null
+++ b/coderd/database/migrations/000340_workspace_app_status_idle.up.sql
@@ -0,0 +1 @@
+ALTER TYPE workspace_app_status_state ADD VALUE IF NOT EXISTS 'idle';
diff --git a/coderd/database/models.go b/coderd/database/models.go
index 6a571ffc1d0d4..ce65c4c559d65 100644
--- a/coderd/database/models.go
+++ b/coderd/database/models.go
@@ -2628,6 +2628,7 @@ const (
WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working"
WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete"
WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure"
+ WorkspaceAppStatusStateIdle WorkspaceAppStatusState = "idle"
)
func (e *WorkspaceAppStatusState) Scan(src interface{}) error {
@@ -2669,7 +2670,8 @@ func (e WorkspaceAppStatusState) Valid() bool {
switch e {
case WorkspaceAppStatusStateWorking,
WorkspaceAppStatusStateComplete,
- WorkspaceAppStatusStateFailure:
+ WorkspaceAppStatusStateFailure,
+ WorkspaceAppStatusStateIdle:
return true
}
return false
@@ -2680,6 +2682,7 @@ func AllWorkspaceAppStatusStateValues() []WorkspaceAppStatusState {
WorkspaceAppStatusStateWorking,
WorkspaceAppStatusStateComplete,
WorkspaceAppStatusStateFailure,
+ WorkspaceAppStatusStateIdle,
}
}
diff --git a/coderd/dynamicparameters/render.go b/coderd/dynamicparameters/render.go
index b6a77c4704225..8e7df929505f1 100644
--- a/coderd/dynamicparameters/render.go
+++ b/coderd/dynamicparameters/render.go
@@ -2,9 +2,11 @@ package dynamicparameters
import (
"context"
+ "database/sql"
"io/fs"
"log/slog"
"sync"
+ "time"
"github.com/google/uuid"
"golang.org/x/sync/errgroup"
@@ -105,9 +107,24 @@ func (r *loader) loadData(ctx context.Context, db database.Store) error {
if r.terraformValues == nil {
values, err := db.GetTemplateVersionTerraformValues(ctx, r.templateVersion.ID)
- if err != nil {
+ if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
return xerrors.Errorf("template version terraform values: %w", err)
}
+
+ if xerrors.Is(err, sql.ErrNoRows) {
+ // If the row does not exist, return zero values.
+ //
+ // Older template versions (prior to dynamic parameters) will be missing
+ // this row, and we can assume the 'ProvisionerdVersion' "" (unknown).
+ values = database.TemplateVersionTerraformValue{
+ TemplateVersionID: r.templateVersionID,
+ UpdatedAt: time.Time{},
+ CachedPlan: nil,
+ CachedModuleFiles: uuid.NullUUID{},
+ ProvisionerdVersion: "",
+ }
+ }
+
r.terraformValues = &values
}
diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go
index ed3f554a89b75..8282eb9e7d01f 100644
--- a/coderd/workspaceagents.go
+++ b/coderd/workspaceagents.go
@@ -359,7 +359,10 @@ func (api *API) patchWorkspaceAgentAppStatus(rw http.ResponseWriter, r *http.Req
}
switch req.State {
- case codersdk.WorkspaceAppStatusStateComplete, codersdk.WorkspaceAppStatusStateFailure, codersdk.WorkspaceAppStatusStateWorking: // valid states
+ case codersdk.WorkspaceAppStatusStateComplete,
+ codersdk.WorkspaceAppStatusStateFailure,
+ codersdk.WorkspaceAppStatusStateWorking,
+ codersdk.WorkspaceAppStatusStateIdle: // valid states
default:
httpapi.Write(ctx, rw, http.StatusBadRequest, codersdk.Response{
Message: "Invalid state provided.",
diff --git a/codersdk/toolsdk/toolsdk.go b/codersdk/toolsdk/toolsdk.go
index bb1649efa1993..3b992124005ac 100644
--- a/codersdk/toolsdk/toolsdk.go
+++ b/codersdk/toolsdk/toolsdk.go
@@ -191,7 +191,7 @@ Bad Tasks
Use the "state" field to indicate your progress. Periodically report
progress with state "working" to keep the user updated. It is not possible to send too many updates!
-ONLY report a "complete" or "failure" state if you have FULLY completed the task.
+ONLY report an "idle" or "failure" state if you have FULLY completed the task.
`,
Schema: aisdk.Schema{
Properties: map[string]any{
@@ -205,10 +205,10 @@ ONLY report a "complete" or "failure" state if you have FULLY completed the task
},
"state": map[string]any{
"type": "string",
- "description": "The state of your task. This can be one of the following: working, complete, or failure. Select the state that best represents your current progress.",
+ "description": "The state of your task. This can be one of the following: working, idle, or failure. Select the state that best represents your current progress.",
"enum": []string{
string(codersdk.WorkspaceAppStatusStateWorking),
- string(codersdk.WorkspaceAppStatusStateComplete),
+ string(codersdk.WorkspaceAppStatusStateIdle),
string(codersdk.WorkspaceAppStatusStateFailure),
},
},
diff --git a/codersdk/workspaceapps.go b/codersdk/workspaceapps.go
index 556b3adb27b2e..6e95377bbaf42 100644
--- a/codersdk/workspaceapps.go
+++ b/codersdk/workspaceapps.go
@@ -19,6 +19,7 @@ type WorkspaceAppStatusState string
const (
WorkspaceAppStatusStateWorking WorkspaceAppStatusState = "working"
+ WorkspaceAppStatusStateIdle WorkspaceAppStatusState = "idle"
WorkspaceAppStatusStateComplete WorkspaceAppStatusState = "complete"
WorkspaceAppStatusStateFailure WorkspaceAppStatusState = "failure"
)
diff --git a/docs/admin/integrations/dx-data-cloud.md b/docs/admin/integrations/dx-data-cloud.md
new file mode 100644
index 0000000000000..62055d69f5f1a
--- /dev/null
+++ b/docs/admin/integrations/dx-data-cloud.md
@@ -0,0 +1,87 @@
+# DX Data Cloud
+
+[DX](https://getdx.com) is a developer intelligence platform used by engineering
+leaders and platform engineers.
+
+DX uses metadata attributes to assign information to individual users.
+While it's common to segment users by `role`, `level`, or `geo`, it’s become increasingly
+common to use DX attributes to better understand usage and adoption of tools.
+
+You can create a `Coder` attribute in DX to segment and analyze the impact of Coder usage on a developer’s work, including:
+
+- Understanding the needs of power users or low Coder usage across the org
+- Correlate Coder usage with qualitative and quantitative engineering metrics,
+ such as PR throughput, deployment frequency, deep work, dev environment toil, and more.
+- Personalize user experiences
+
+## Requirements
+
+- A DX subscription
+- Access to Coder user data through the Coder CLI, Coder API, an IdP, or an existing Coder-DX integration
+- Coordination with your DX Customer Success Manager
+
+## Extract Your Coder User List
+
+
+
+You can use the Coder CLI, Coder API, or your Identity Provider (IdP) to extract your list of users.
+
+If your organization already uses the Coder-DX integration, you can find a list of active Coder users directly within DX.
+
+### CLI
+
+Use `users list` to export the list of users to a CSV file:
+
+```shell
+coder users list > users.csv
+```
+
+Visit the [users list](../../reference/cli/users_list.md) documentation for more options.
+
+### API
+
+Use [get users](../../reference/api/users.md#get-users):
+
+```bash
+curl -X GET http://coder-server:8080/api/v2/users \
+ -H 'Accept: application/json' \
+ -H 'Coder-Session-Token: API_KEY'
+```
+
+To export the results to a CSV file, you can use the `jq` tool to process the JSON response:
+
+```bash
+curl -X GET http://coder-server:8080/api/v2/users \
+ -H 'Accept: application/json' \
+ -H 'Coder-Session-Token: API_KEY' | \
+ jq -r '.users | (map(keys) | add | unique) as $cols | $cols, (.[] | [.[$cols[]]] | @csv)' > users.csv
+```
+
+Visit the [get users](../../reference/api/users.md#get-users) documentation for more options.
+
+### IdP
+
+If your organization uses a centralized IdP to manage user accounts, you can extract user data directly from your IdP.
+
+This is particularly useful if you need additional user attributes managed within your IdP.
+
+
+
+## Contact your DX Customer Success Manager
+
+Provide the file to your dedicated DX Customer Success Manager (CSM).
+
+Your CSM will import the CSV of individuals using Coder, as well as usage frequency (if applicable) into DX to create a `Coder` attribute.
+
+After the attribute is uploaded, you'll have a Coder filter option within your DX reports allowing you to:
+
+- Perform cohort analysis (Coder user vs non-user)
+- Understand unique behaviors and patterns across your Coder users
+- Run a [study](https://getdx.com/studies/) or setup a [PlatformX](https://getdx.com/platformx/) event for deeper analysis
+
+## Related Resources
+
+- [DX Data Cloud Documentation](https://help.getdx.com/en/)
+- [Coder CLI](../../reference/cli/users.md)
+- [Coder API](../../reference/api/users.md)
+- [PlatformX Integration](./platformx.md)
diff --git a/docs/install/other/index.md b/docs/install/other/index.md
index f727e5c34bf55..1aa6d195bcdbb 100644
--- a/docs/install/other/index.md
+++ b/docs/install/other/index.md
@@ -1,6 +1,6 @@
# Alternate install methods
-Coder has a number of alternate unofficial install methods. Contributions are
+Coder has a number of alternate unofficial installation methods. Contributions are
welcome!
| Platform Name | Status | Documentation |
@@ -9,7 +9,7 @@ welcome!
| Terraform (GKE, AKS, LKE, DOKS, IBMCloud K8s, OVHCloud K8s, Scaleway K8s Kapsule) | Unofficial | [GitHub: coder-oss-terraform](https://github.com/ElliotG/coder-oss-tf) |
| Fly.io | Unofficial | [Blog: Run Coder on Fly.io](https://coder.com/blog/remote-developer-environments-on-fly-io) |
| Garden.io | Unofficial | [GitHub: garden-coder-example](https://github.com/garden-io/garden-coder-example) |
-| Railway.app | Unofficial | [Blog: Run Coder on Railway.app](https://coder.com/blog/deploy-coder-on-railway-app) |
+| Railway.com | Unofficial | [Run Coder on Railway.com](https://railway.com/deploy/coder) |
| Heroku | Unofficial | [Docs: Deploy Coder on Heroku](https://github.com/coder/packages/blob/main/heroku/README.md) |
| Render | Unofficial | [Docs: Deploy Coder on Render](https://github.com/coder/packages/blob/main/render/README.md) |
| Snapcraft | Unofficial | [Get it from the Snap Store](https://snapcraft.io/coder) |
diff --git a/docs/manifest.json b/docs/manifest.json
index 2aa9cb0ead9ce..c01eb828ac3a1 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -697,6 +697,11 @@
"description": "Integrate Coder with DX PlatformX",
"path": "./admin/integrations/platformx.md"
},
+ {
+ "title": "DX Data Cloud",
+ "description": "Tag Coder Users with DX Data Cloud",
+ "path": "./admin/integrations/dx-data-cloud.md"
+ },
{
"title": "Hashicorp Vault",
"description": "Integrate Coder with Hashicorp Vault",
diff --git a/docs/reference/api/builds.md b/docs/reference/api/builds.md
index 2a0e4b2ede1a0..c47b89d0bbba1 100644
--- a/docs/reference/api/builds.md
+++ b/docs/reference/api/builds.md
@@ -933,6 +933,7 @@ Status Code **200**
| `sharing_level` | `organization` |
| `sharing_level` | `public` |
| `state` | `working` |
+| `state` | `idle` |
| `state` | `complete` |
| `state` | `failure` |
| `lifecycle_state` | `created` |
@@ -1695,6 +1696,7 @@ Status Code **200**
| `sharing_level` | `organization` |
| `sharing_level` | `public` |
| `state` | `working` |
+| `state` | `idle` |
| `state` | `complete` |
| `state` | `failure` |
| `lifecycle_state` | `created` |
diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md
index 8f548478e27a6..e0999f6bb3778 100644
--- a/docs/reference/api/schemas.md
+++ b/docs/reference/api/schemas.md
@@ -9686,6 +9686,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
| Value |
|------------|
| `working` |
+| `idle` |
| `complete` |
| `failure` |
diff --git a/docs/reference/api/templates.md b/docs/reference/api/templates.md
index d695be4122951..85e865d8b4b37 100644
--- a/docs/reference/api/templates.md
+++ b/docs/reference/api/templates.md
@@ -2557,6 +2557,7 @@ Status Code **200**
| `sharing_level` | `organization` |
| `sharing_level` | `public` |
| `state` | `working` |
+| `state` | `idle` |
| `state` | `complete` |
| `state` | `failure` |
| `lifecycle_state` | `created` |
@@ -3233,6 +3234,7 @@ Status Code **200**
| `sharing_level` | `organization` |
| `sharing_level` | `public` |
| `state` | `working` |
+| `state` | `idle` |
| `state` | `complete` |
| `state` | `failure` |
| `lifecycle_state` | `created` |
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index 98338c24bb2d8..d536ac3a0fe5e 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -3622,11 +3622,16 @@ export interface WorkspaceAppStatus {
}
// From codersdk/workspaceapps.go
-export type WorkspaceAppStatusState = "complete" | "failure" | "working";
+export type WorkspaceAppStatusState =
+ | "complete"
+ | "failure"
+ | "idle"
+ | "working";
export const WorkspaceAppStatusStates: WorkspaceAppStatusState[] = [
"complete",
"failure",
+ "idle",
"working",
];
diff --git a/site/src/modules/apps/AppStatusStateIcon.tsx b/site/src/modules/apps/AppStatusStateIcon.tsx
index 829a8288235de..f713f49ed24b0 100644
--- a/site/src/modules/apps/AppStatusStateIcon.tsx
+++ b/site/src/modules/apps/AppStatusStateIcon.tsx
@@ -5,6 +5,7 @@ import {
CircleAlertIcon,
CircleCheckIcon,
HourglassIcon,
+ SquareIcon,
TriangleAlertIcon,
} from "lucide-react";
import type { FC } from "react";
@@ -26,6 +27,10 @@ export const AppStatusStateIcon: FC = ({
const className = cn(["size-4 shrink-0", customClassName]);
switch (state) {
+ case "idle":
+ return (
+
+ );
case "complete":
return (
diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx
index d95444e658d64..0e229467b994b 100644
--- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx
+++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.stories.tsx
@@ -39,6 +39,26 @@ export const Working: Story = {
},
};
+export const Idle: Story = {
+ args: {
+ status: {
+ ...MockWorkspaceAppStatus,
+ state: "idle",
+ message: "Done for now",
+ },
+ },
+};
+
+export const NoMessage: Story = {
+ args: {
+ status: {
+ ...MockWorkspaceAppStatus,
+ state: "idle",
+ message: "",
+ },
+ },
+};
+
export const LongMessage: Story = {
args: {
status: {
diff --git a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx
index 0b999f54402a8..587ae9f5b062f 100644
--- a/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx
+++ b/site/src/modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus.tsx
@@ -5,6 +5,7 @@ import {
TooltipProvider,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
+import capitalize from "lodash/capitalize";
import { AppStatusStateIcon } from "modules/apps/AppStatusStateIcon";
import { cn } from "utils/cn";
@@ -25,6 +26,7 @@ export const WorkspaceAppStatus = ({
);
}
+ const message = status.message || capitalize(status.state);
return (
- {status.message}
+ {message}
diff --git a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx
index 1a02d50d06dcf..d6b56fd06e24f 100644
--- a/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx
+++ b/site/src/pages/TemplateSettingsPage/TemplateGeneralSettingsPage/TemplateSettingsForm.tsx
@@ -10,6 +10,7 @@ import {
} from "api/typesGenerated";
import { PremiumBadge } from "components/Badges/Badges";
import { Button } from "components/Button/Button";
+import { FeatureStageBadge } from "components/FeatureStageBadge/FeatureStageBadge";
import {
FormFields,
FormFooter,
@@ -17,6 +18,7 @@ import {
HorizontalForm,
} from "components/Form/Form";
import { IconField } from "components/IconField/IconField";
+import { Link } from "components/Link/Link";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import {
@@ -25,6 +27,7 @@ import {
} from "components/StackLabel/StackLabel";
import { type FormikTouched, useFormik } from "formik";
import type { FC } from "react";
+import { docs } from "utils/docs";
import {
displayNameValidator,
getFormHelpers,
@@ -230,21 +233,37 @@ export const TemplateSettingsForm: FC = ({
size="small"
id="use_classic_parameter_flow"
name="use_classic_parameter_flow"
- checked={form.values.use_classic_parameter_flow}
- onChange={form.handleChange}
+ checked={!form.values.use_classic_parameter_flow}
+ onChange={(event) =>
+ form.setFieldValue(
+ "use_classic_parameter_flow",
+ !event.currentTarget.checked,
+ )
+ }
disabled={false}
/>
}
label={
- Use classic workspace creation form
+
+ Enable dynamic parameters for workspace creation
+
+
-
- Show the original workspace creation form and workspace
- parameters settings form without dynamic parameters or live
- updates. Recommended if your provisioners aren't updated or
- the dynamic form causes issues.
-
+
+ The new workspace form allows you to design your template
+ with new form types and identity-aware conditional
+ parameters. The form will only present options that are
+ compatible and available.
+