feat: extend workspace build reasons to track connection types (#18827)

This PR introduces new build reason values to identify what type of
connection triggered a workspace build, helping to troubleshoot
workspace-related issues.

## Database Migration
Added migration 000349_extend_workspace_build_reason.up.sql that extends
the build_reason enum with new values:
```
dashboard, cli, ssh_connection, vscode_connection, jetbrains_connection
```

## Implementation
The build reason is specified through the API when creating new
workspace builds:

- Dashboard: Automatically sets reason to `dashboard` when users start
workspaces via the web interface
- CLI `start` command: Sets reason to `cli` when workspaces are started
via the command line
- CLI `ssh` command: Sets reason to ssh_connection when workspaces are
started due to SSH connections
- VS Code connections: Will be set to `vscode_connection` by the VS Code
extension through CLI hidden flag
(https://github.com/coder/vscode-coder/pull/550)
- JetBrains connections: Will be set to `jetbrains_connection` by the
Jetbrains Toolbox
(https://github.com/coder/coder-jetbrains-toolbox/pull/150) and
Jetbrains Gateway extension
(https://github.com/coder/jetbrains-coder/pull/561)

## UI Changes:
* Tooltip with reason in Build history
<img width="309" height="457" alt="image"
src="https://github.com/user-attachments/assets/bde8440b-bf3b-49a1-a244-ed7e8eb9763c"
/>

* Reason in Audit Logs Row tooltip
<img width="906" height="237" alt="image"
src="https://github.com/user-attachments/assets/ebbb62c7-cf07-4398-afbf-323c83fb6426"
/>

<img width="909" height="188" alt="image"
src="https://github.com/user-attachments/assets/1ddbab07-44bf-4dee-8867-b4e2cd56ae96"
/>
This commit is contained in:
Kacper Sawicki
2025-07-22 13:11:27 +02:00
committed by GitHub
parent 0ebd4356a0
commit 482463c51a
25 changed files with 388 additions and 36 deletions
+15 -1
View File
@@ -145,9 +145,11 @@ func parseParameterMapFile(parameterFile string) (map[string]string, error) {
return parameterMap, nil
}
// buildFlags contains options relating to troubleshooting provisioner jobs.
// buildFlags contains options relating to troubleshooting provisioner jobs
// and setting the reason for the workspace build.
type buildFlags struct {
provisionerLogDebug bool
reason string
}
func (bf *buildFlags) cliOptions() []serpent.Option {
@@ -160,5 +162,17 @@ This is useful for troubleshooting build issues.`,
Value: serpent.BoolOf(&bf.provisionerLogDebug),
Hidden: true,
},
{
Flag: "reason",
Description: `Sets the reason for the workspace build (cli, vscode_connection, jetbrains_connection).`,
Value: serpent.EnumOf(
&bf.reason,
string(codersdk.BuildReasonCLI),
string(codersdk.BuildReasonVSCodeConnection),
string(codersdk.BuildReasonJetbrainsConnection),
),
Default: string(codersdk.BuildReasonCLI),
Hidden: true,
},
}
}
+3 -1
View File
@@ -873,7 +873,9 @@ func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *
// It's possible for a workspace build to fail due to the template requiring starting
// workspaces with the active version.
_, _ = fmt.Fprintf(inv.Stderr, "Workspace was stopped, starting workspace to allow connecting to %q...\n", workspace.Name)
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceStart)
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{
reason: string(codersdk.BuildReasonSSHConnection),
}, WorkspaceStart)
if cerr, ok := codersdk.AsError(err); ok {
switch cerr.StatusCode() {
case http.StatusConflict:
+3
View File
@@ -169,6 +169,9 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
if buildFlags.provisionerLogDebug {
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
}
if buildFlags.reason != "" {
wbr.Reason = codersdk.CreateWorkspaceBuildReason(buildFlags.reason)
}
return wbr, nil
}
+36
View File
@@ -477,3 +477,39 @@ func TestStart_NoWait(t *testing.T) {
pty.ExpectMatch("workspace has been started in no-wait mode")
_ = testutil.TryReceive(ctx, t, doneChan)
}
func TestStart_WithReason(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
// Prepare user, template, workspace
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
// Stop the workspace
build := coderdtest.CreateWorkspaceBuild(t, member, workspace, database.WorkspaceTransitionStop)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
// Start the workspace with reason
inv, root := clitest.New(t, "start", workspace.Name, "--reason", "cli")
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
pty.ExpectMatch("workspace has been started")
_ = testutil.TryReceive(ctx, t, doneChan)
workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
require.Equal(t, codersdk.BuildReasonCLI, workspace.LatestBuild.Reason)
}
+44 -2
View File
@@ -11448,13 +11448,23 @@ const docTemplate = `{
"initiator",
"autostart",
"autostop",
"dormancy"
"dormancy",
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"BuildReasonInitiator",
"BuildReasonAutostart",
"BuildReasonAutostop",
"BuildReasonDormancy"
"BuildReasonDormancy",
"BuildReasonDashboard",
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
]
},
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
@@ -12070,6 +12080,23 @@ const docTemplate = `{
}
}
},
"codersdk.CreateWorkspaceBuildReason": {
"type": "string",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"CreateWorkspaceBuildReasonDashboard",
"CreateWorkspaceBuildReasonCLI",
"CreateWorkspaceBuildReasonSSHConnection",
"CreateWorkspaceBuildReasonVSCodeConnection",
"CreateWorkspaceBuildReasonJetbrainsConnection"
]
},
"codersdk.CreateWorkspaceBuildRequest": {
"type": "object",
"required": [
@@ -12094,6 +12121,21 @@ const docTemplate = `{
"description": "Orphan may be set for the Destroy transition.",
"type": "boolean"
},
"reason": {
"description": "Reason sets the reason for the workspace build.",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.CreateWorkspaceBuildReason"
}
]
},
"rich_parameter_values": {
"description": "ParameterValues are optional. It will write params to the 'workspace' scope.\nThis will overwrite any existing parameters with the same name.\nThis will not delete old params not included in this list.",
"type": "array",
+49 -2
View File
@@ -10179,12 +10179,27 @@
},
"codersdk.BuildReason": {
"type": "string",
"enum": ["initiator", "autostart", "autostop", "dormancy"],
"enum": [
"initiator",
"autostart",
"autostop",
"dormancy",
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"BuildReasonInitiator",
"BuildReasonAutostart",
"BuildReasonAutostop",
"BuildReasonDormancy"
"BuildReasonDormancy",
"BuildReasonDashboard",
"BuildReasonCLI",
"BuildReasonSSHConnection",
"BuildReasonVSCodeConnection",
"BuildReasonJetbrainsConnection"
]
},
"codersdk.ChangePasswordWithOneTimePasscodeRequest": {
@@ -10758,6 +10773,23 @@
}
}
},
"codersdk.CreateWorkspaceBuildReason": {
"type": "string",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"x-enum-varnames": [
"CreateWorkspaceBuildReasonDashboard",
"CreateWorkspaceBuildReasonCLI",
"CreateWorkspaceBuildReasonSSHConnection",
"CreateWorkspaceBuildReasonVSCodeConnection",
"CreateWorkspaceBuildReasonJetbrainsConnection"
]
},
"codersdk.CreateWorkspaceBuildRequest": {
"type": "object",
"required": ["transition"],
@@ -10778,6 +10810,21 @@
"description": "Orphan may be set for the Destroy transition.",
"type": "boolean"
},
"reason": {
"description": "Reason sets the reason for the workspace build.",
"enum": [
"dashboard",
"cli",
"ssh_connection",
"vscode_connection",
"jetbrains_connection"
],
"allOf": [
{
"$ref": "#/definitions/codersdk.CreateWorkspaceBuildReason"
}
]
},
"rich_parameter_values": {
"description": "ParameterValues are optional. It will write params to the 'workspace' scope.\nThis will overwrite any existing parameters with the same name.\nThis will not delete old params not included in this list.",
"type": "array",
+6 -1
View File
@@ -51,7 +51,12 @@ CREATE TYPE build_reason AS ENUM (
'autostop',
'dormancy',
'failedstop',
'autodelete'
'autodelete',
'dashboard',
'cli',
'ssh_connection',
'vscode_connection',
'jetbrains_connection'
);
CREATE TYPE connection_status AS ENUM (
@@ -0,0 +1 @@
-- It's not possible to delete enum values.
@@ -0,0 +1,5 @@
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'dashboard';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'cli';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'ssh_connection';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'vscode_connection';
ALTER TYPE build_reason ADD VALUE IF NOT EXISTS 'jetbrains_connection';
+22 -7
View File
@@ -349,12 +349,17 @@ func AllAutomaticUpdatesValues() []AutomaticUpdates {
type BuildReason string
const (
BuildReasonInitiator BuildReason = "initiator"
BuildReasonAutostart BuildReason = "autostart"
BuildReasonAutostop BuildReason = "autostop"
BuildReasonDormancy BuildReason = "dormancy"
BuildReasonFailedstop BuildReason = "failedstop"
BuildReasonAutodelete BuildReason = "autodelete"
BuildReasonInitiator BuildReason = "initiator"
BuildReasonAutostart BuildReason = "autostart"
BuildReasonAutostop BuildReason = "autostop"
BuildReasonDormancy BuildReason = "dormancy"
BuildReasonFailedstop BuildReason = "failedstop"
BuildReasonAutodelete BuildReason = "autodelete"
BuildReasonDashboard BuildReason = "dashboard"
BuildReasonCli BuildReason = "cli"
BuildReasonSshConnection BuildReason = "ssh_connection"
BuildReasonVscodeConnection BuildReason = "vscode_connection"
BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection"
)
func (e *BuildReason) Scan(src interface{}) error {
@@ -399,7 +404,12 @@ func (e BuildReason) Valid() bool {
BuildReasonAutostop,
BuildReasonDormancy,
BuildReasonFailedstop,
BuildReasonAutodelete:
BuildReasonAutodelete,
BuildReasonDashboard,
BuildReasonCli,
BuildReasonSshConnection,
BuildReasonVscodeConnection,
BuildReasonJetbrainsConnection:
return true
}
return false
@@ -413,6 +423,11 @@ func AllBuildReasonValues() []BuildReason {
BuildReasonDormancy,
BuildReasonFailedstop,
BuildReasonAutodelete,
BuildReasonDashboard,
BuildReasonCli,
BuildReasonSshConnection,
BuildReasonVscodeConnection,
BuildReasonJetbrainsConnection,
}
}
+7 -1
View File
@@ -329,13 +329,15 @@ func (api *API) workspaceBuildByBuildNumber(rw http.ResponseWriter, r *http.Requ
func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
apiKey := httpmw.APIKey(r)
workspace := httpmw.WorkspaceParam(r)
var createBuild codersdk.CreateWorkspaceBuildRequest
if !httpapi.Read(ctx, rw, r, &createBuild) {
return
}
builder := wsbuilder.New(workspace, database.WorkspaceTransition(createBuild.Transition), *api.BuildUsageChecker.Load()).
transition := database.WorkspaceTransition(createBuild.Transition)
builder := wsbuilder.New(workspace, transition, *api.BuildUsageChecker.Load()).
Initiator(apiKey.UserID).
RichParameterValues(createBuild.RichParameterValues).
LogLevel(string(createBuild.LogLevel)).
@@ -343,6 +345,10 @@ func (api *API) postWorkspaceBuilds(rw http.ResponseWriter, r *http.Request) {
Experiments(api.Experiments).
TemplateVersionPresetID(createBuild.TemplateVersionPresetID)
if transition == database.WorkspaceTransitionStart && createBuild.Reason != "" {
builder = builder.Reason(database.BuildReason(createBuild.Reason))
}
var (
previousWorkspaceBuild database.WorkspaceBuild
workspaceBuild *database.WorkspaceBuild
+24
View File
@@ -1808,6 +1808,30 @@ func TestPostWorkspaceBuild(t *testing.T) {
assert.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
}
})
t.Run("WithReason", func(t *testing.T) {
t.Parallel()
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
_ = closeDaemon.Close()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: template.ActiveVersionID,
Transition: codersdk.WorkspaceTransitionStart,
Reason: codersdk.CreateWorkspaceBuildReasonDashboard,
})
require.NoError(t, err)
require.Equal(t, codersdk.BuildReasonDashboard, build.Reason)
})
}
func TestWorkspaceBuildTimings(t *testing.T) {
+10
View File
@@ -49,6 +49,16 @@ const (
// BuildReasonDormancy "dormancy" is used when a build to stop a workspace is triggered due to inactivity (dormancy).
// The initiator id/username in this case is the workspace owner and can be ignored.
BuildReasonDormancy BuildReason = "dormancy"
// BuildReasonDashboard "dashboard" is used when a build to start a workspace is triggered by the dashboard.
BuildReasonDashboard BuildReason = "dashboard"
// BuildReasonCLI "cli" is used when a build to start a workspace is triggered by the CLI.
BuildReasonCLI BuildReason = "cli"
// BuildReasonSSHConnection "ssh_connection" is used when a build to start a workspace is triggered by an SSH connection.
BuildReasonSSHConnection BuildReason = "ssh_connection"
// BuildReasonVSCodeConnection "vscode_connection" is used when a build to start a workspace is triggered by a VS Code connection.
BuildReasonVSCodeConnection BuildReason = "vscode_connection"
// BuildReasonJetbrainsConnection "jetbrains_connection" is used when a build to start a workspace is triggered by a JetBrains connection.
BuildReasonJetbrainsConnection BuildReason = "jetbrains_connection"
)
// WorkspaceBuild is an at-point representation of a workspace state.
+12
View File
@@ -99,6 +99,16 @@ const (
ProvisionerLogLevelDebug ProvisionerLogLevel = "debug"
)
type CreateWorkspaceBuildReason string
const (
CreateWorkspaceBuildReasonDashboard CreateWorkspaceBuildReason = "dashboard"
CreateWorkspaceBuildReasonCLI CreateWorkspaceBuildReason = "cli"
CreateWorkspaceBuildReasonSSHConnection CreateWorkspaceBuildReason = "ssh_connection"
CreateWorkspaceBuildReasonVSCodeConnection CreateWorkspaceBuildReason = "vscode_connection"
CreateWorkspaceBuildReasonJetbrainsConnection CreateWorkspaceBuildReason = "jetbrains_connection"
)
// CreateWorkspaceBuildRequest provides options to update the latest workspace build.
type CreateWorkspaceBuildRequest struct {
TemplateVersionID uuid.UUID `json:"template_version_id,omitempty" format:"uuid"`
@@ -116,6 +126,8 @@ type CreateWorkspaceBuildRequest struct {
LogLevel ProvisionerLogLevel `json:"log_level,omitempty" validate:"omitempty,oneof=debug"`
// TemplateVersionPresetID is the ID of the template version preset to use for the build.
TemplateVersionPresetID uuid.UUID `json:"template_version_preset_id,omitempty" format:"uuid"`
// Reason sets the reason for the workspace build.
Reason CreateWorkspaceBuildReason `json:"reason,omitempty" validate:"omitempty,oneof=dashboard cli ssh_connection vscode_connection jetbrains_connection"`
}
type WorkspaceOptions struct {
+1
View File
@@ -1762,6 +1762,7 @@ curl -X POST http://coder-server:8080/api/v2/workspaces/{workspace}/builds \
"dry_run": true,
"log_level": "debug",
"orphan": true,
"reason": "dashboard",
"rich_parameter_values": [
{
"name": "string",
+42 -12
View File
@@ -1044,12 +1044,17 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
#### Enumerated Values
| Value |
|-------------|
| `initiator` |
| `autostart` |
| `autostop` |
| `dormancy` |
| Value |
|------------------------|
| `initiator` |
| `autostart` |
| `autostop` |
| `dormancy` |
| `dashboard` |
| `cli` |
| `ssh_connection` |
| `vscode_connection` |
| `jetbrains_connection` |
## codersdk.ChangePasswordWithOneTimePasscodeRequest
@@ -1689,6 +1694,24 @@ This is required on creation to enable a user-flow of validating a template work
| `user_status` | [codersdk.UserStatus](#codersdkuserstatus) | false | | User status defaults to UserStatusDormant. |
| `username` | string | true | | |
## codersdk.CreateWorkspaceBuildReason
```json
"dashboard"
```
### Properties
#### Enumerated Values
| Value |
|------------------------|
| `dashboard` |
| `cli` |
| `ssh_connection` |
| `vscode_connection` |
| `jetbrains_connection` |
## codersdk.CreateWorkspaceBuildRequest
```json
@@ -1696,6 +1719,7 @@ This is required on creation to enable a user-flow of validating a template work
"dry_run": true,
"log_level": "debug",
"orphan": true,
"reason": "dashboard",
"rich_parameter_values": [
{
"name": "string",
@@ -1718,6 +1742,7 @@ This is required on creation to enable a user-flow of validating a template work
| `dry_run` | boolean | false | | |
| `log_level` | [codersdk.ProvisionerLogLevel](#codersdkprovisionerloglevel) | false | | Log level changes the default logging verbosity of a provider ("info" if empty). |
| `orphan` | boolean | false | | Orphan may be set for the Destroy transition. |
| `reason` | [codersdk.CreateWorkspaceBuildReason](#codersdkcreateworkspacebuildreason) | false | | Reason sets the reason for the workspace build. |
| `rich_parameter_values` | array of [codersdk.WorkspaceBuildParameter](#codersdkworkspacebuildparameter) | false | | Rich parameter values are optional. It will write params to the 'workspace' scope. This will overwrite any existing parameters with the same name. This will not delete old params not included in this list. |
| `state` | array of integer | false | | |
| `template_version_id` | string | false | | |
@@ -1726,12 +1751,17 @@ This is required on creation to enable a user-flow of validating a template work
#### Enumerated Values
| Property | Value |
|--------------|----------|
| `log_level` | `debug` |
| `transition` | `start` |
| `transition` | `stop` |
| `transition` | `delete` |
| Property | Value |
|--------------|------------------------|
| `log_level` | `debug` |
| `reason` | `dashboard` |
| `reason` | `cli` |
| `reason` | `ssh_connection` |
| `reason` | `vscode_connection` |
| `reason` | `jetbrains_connection` |
| `transition` | `start` |
| `transition` | `stop` |
| `transition` | `delete` |
## codersdk.CreateWorkspaceProxyRequest
+1
View File
@@ -1272,6 +1272,7 @@ class ApiMethods {
template_version_id: templateVersionId,
log_level: logLevel,
rich_parameter_values: buildParameters,
reason: "dashboard",
});
};
+32 -1
View File
@@ -275,13 +275,27 @@ export interface BuildInfoResponse {
}
// From codersdk/workspacebuilds.go
export type BuildReason = "autostart" | "autostop" | "dormancy" | "initiator";
export type BuildReason =
| "autostart"
| "autostop"
| "cli"
| "dashboard"
| "dormancy"
| "initiator"
| "jetbrains_connection"
| "ssh_connection"
| "vscode_connection";
export const BuildReasons: BuildReason[] = [
"autostart",
"autostop",
"cli",
"dashboard",
"dormancy",
"initiator",
"jetbrains_connection",
"ssh_connection",
"vscode_connection",
];
// From codersdk/client.go
@@ -530,6 +544,22 @@ export interface CreateUserRequestWithOrgs {
readonly organization_ids: readonly string[];
}
// From codersdk/workspaces.go
export type CreateWorkspaceBuildReason =
| "cli"
| "dashboard"
| "jetbrains_connection"
| "ssh_connection"
| "vscode_connection";
export const CreateWorkspaceBuildReasons: CreateWorkspaceBuildReason[] = [
"cli",
"dashboard",
"jetbrains_connection",
"ssh_connection",
"vscode_connection",
];
// From codersdk/workspaces.go
export interface CreateWorkspaceBuildRequest {
readonly template_version_id?: string;
@@ -540,6 +570,7 @@ export interface CreateWorkspaceBuildRequest {
readonly rich_parameter_values?: readonly WorkspaceBuildParameter[];
readonly log_level?: ProvisionerLogLevel;
readonly template_version_preset_id?: string;
readonly reason?: CreateWorkspaceBuildReason;
}
// From codersdk/workspaceproxy.go
@@ -1,11 +1,15 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import type { WorkspaceBuild } from "api/typesGenerated";
import { BuildIcon } from "components/BuildIcon/BuildIcon";
import { InfoIcon } from "lucide-react";
import { createDayString } from "utils/createDayString";
import {
buildReasonLabels,
getDisplayWorkspaceBuildInitiatedBy,
getDisplayWorkspaceBuildStatus,
systemBuildReasons,
} from "utils/workspace";
export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
@@ -29,6 +33,9 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
textOverflow: "ellipsis",
overflow: "hidden",
whiteSpace: "nowrap",
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<span css={{ textTransform: "capitalize" }}>{build.transition}</span>{" "}
@@ -36,6 +43,17 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
<span css={{ fontWeight: 500 }}>
{getDisplayWorkspaceBuildInitiatedBy(build)}
</span>
{!systemBuildReasons.includes(build.reason) &&
build.transition === "start" && (
<Tooltip title={buildReasonLabels[build.reason]}>
<InfoIcon
css={(theme) => ({
color: theme.palette.info.light,
})}
className="size-icon-xs -mt-px"
/>
</Tooltip>
)}
</div>
<div
css={{
@@ -2,6 +2,7 @@ import Link from "@mui/material/Link";
import type { AuditLog } from "api/typesGenerated";
import { type FC, useMemo } from "react";
import { Link as RouterLink } from "react-router-dom";
import { systemBuildReasons } from "utils/workspace";
interface BuildAuditDescriptionProps {
auditLog: AuditLog;
@@ -14,7 +15,7 @@ export const BuildAuditDescription: FC<BuildAuditDescriptionProps> = ({
// workspaces can be started/stopped/deleted by a user, or kicked off automatically by Coder
const user =
auditLog.additional_fields?.build_reason &&
auditLog.additional_fields?.build_reason !== "initiator"
systemBuildReasons.includes(auditLog.additional_fields?.build_reason)
? "Coder automatically"
: auditLog.user
? auditLog.user.username.trim()
@@ -3,7 +3,7 @@ import Collapse from "@mui/material/Collapse";
import Link from "@mui/material/Link";
import TableCell from "@mui/material/TableCell";
import Tooltip from "@mui/material/Tooltip";
import type { AuditLog } from "api/typesGenerated";
import type { AuditLog, BuildReason } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { Stack } from "components/Stack/Stack";
@@ -14,6 +14,7 @@ import { NetworkIcon } from "lucide-react";
import { type FC, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import userAgentParser from "ua-parser-js";
import { buildReasonLabels } from "utils/workspace";
import { AuditLogDescription } from "./AuditLogDescription/AuditLogDescription";
import { AuditLogDiff } from "./AuditLogDiff/AuditLogDiff";
import {
@@ -166,12 +167,20 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
</Link>
</div>
)}
{auditLog.additional_fields?.reason && (
<div>
<h4 css={styles.auditLogInfoHeader}>Reason:</h4>
<div>{auditLog.additional_fields?.reason}</div>
</div>
)}
{auditLog.additional_fields?.build_reason &&
auditLog.action === "start" && (
<div>
<h4 css={styles.auditLogInfoHeader}>Reason:</h4>
<div>
{
buildReasonLabels[
auditLog.additional_fields
.build_reason as BuildReason
]
}
</div>
</div>
)}
</div>
}
>
@@ -203,6 +212,20 @@ export const AuditLogRow: FC<AuditLogRowProps> = ({
</strong>
</span>
)}
{auditLog.additional_fields?.build_reason &&
auditLog.action === "start" && (
<span css={styles.auditLogInfo}>
<span>Reason: </span>
<strong>
{
buildReasonLabels[
auditLog.additional_fields
.build_reason as BuildReason
]
}
</strong>
</span>
)}
</Stack>
)}
</Stack>
@@ -67,6 +67,7 @@ test("Submit the workspace settings page successfully", async () => {
// Assert that the API calls were made with the correct data
await waitFor(() => {
expect(postWorkspaceBuildSpy).toHaveBeenCalledWith(MockWorkspace.id, {
reason: "dashboard",
transition: "start",
rich_parameter_values: [
{ name: MockTemplateVersionParameter1.name, value: "new-value" },
@@ -35,6 +35,7 @@ const WorkspaceParametersPage: FC = () => {
API.postWorkspaceBuild(workspace.id, {
transition: "start",
rich_parameter_values: buildParameters,
reason: "dashboard",
}),
onSuccess: () => {
navigate(`/${workspace.owner_name}/${workspace.name}`);
@@ -149,6 +149,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
transition: "start",
template_version_id: templateVersionId,
rich_parameter_values: buildParameters,
reason: "dashboard",
}),
onSuccess: () => {
navigate(`/@${workspace.owner_name}/${workspace.name}`);
+22
View File
@@ -78,6 +78,11 @@ export const getDisplayWorkspaceBuildInitiatedBy = (
): string | undefined => {
switch (build.reason) {
case "initiator":
case "dashboard":
case "cli":
case "ssh_connection":
case "vscode_connection":
case "jetbrains_connection":
return build.initiator_name;
case "autostart":
case "autostop":
@@ -87,6 +92,23 @@ export const getDisplayWorkspaceBuildInitiatedBy = (
return undefined;
};
export const systemBuildReasons = ["autostart", "autostop", "dormancy"];
export const buildReasonLabels: Record<TypesGen.BuildReason, string> = {
// User build reasons
initiator: "API",
dashboard: "Dashboard",
cli: "CLI",
ssh_connection: "SSH Connection",
vscode_connection: "VSCode Connection",
jetbrains_connection: "JetBrains Connection",
// System build reasons
autostart: "Autostart",
autostop: "Autostop",
dormancy: "Dormancy",
};
const getWorkspaceBuildDurationInSeconds = (
build: TypesGen.WorkspaceBuild,
): number | undefined => {