mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat(site): add agent connection timings (#15276)
Local preview: <img width="1260" alt="Screenshot 2024-10-29 at 16 16 01" src="https://github.com/user-attachments/assets/10fdb20d-1f2a-4b0a-a8a1-171050ee620d"> Close https://github.com/coder/internal/issues/116 --------- Co-authored-by: Danny Kopping <danny@coder.com>
This commit is contained in:
co-authored by
Danny Kopping
parent
18ef954a03
commit
e232aee011
Generated
+60
-2
@@ -9020,6 +9020,28 @@ const docTemplate = `{
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentConnectionTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"stage": {
|
||||
"$ref": "#/definitions/codersdk.TimingStage"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"workspace_agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_agent_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentScriptTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -9034,7 +9056,7 @@ const docTemplate = `{
|
||||
"type": "integer"
|
||||
},
|
||||
"stage": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.TimingStage"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
@@ -9042,6 +9064,12 @@ const docTemplate = `{
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_agent_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -12170,7 +12198,7 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
},
|
||||
"stage": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.TimingStage"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
@@ -13473,6 +13501,29 @@ const docTemplate = `{
|
||||
"TemplateVersionWarningUnsupportedWorkspaces"
|
||||
]
|
||||
},
|
||||
"codersdk.TimingStage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"init",
|
||||
"plan",
|
||||
"graph",
|
||||
"apply",
|
||||
"start",
|
||||
"stop",
|
||||
"cron",
|
||||
"connect"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TimingStageInit",
|
||||
"TimingStagePlan",
|
||||
"TimingStageGraph",
|
||||
"TimingStageApply",
|
||||
"TimingStageStart",
|
||||
"TimingStageStop",
|
||||
"TimingStageCron",
|
||||
"TimingStageConnect"
|
||||
]
|
||||
},
|
||||
"codersdk.TokenConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -14806,7 +14857,14 @@ const docTemplate = `{
|
||||
"codersdk.WorkspaceBuildTimings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_connection_timings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AgentConnectionTiming"
|
||||
}
|
||||
},
|
||||
"agent_script_timings": {
|
||||
"description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AgentScriptTiming"
|
||||
|
||||
Generated
+60
-2
@@ -7992,6 +7992,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentConnectionTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ended_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"stage": {
|
||||
"$ref": "#/definitions/codersdk.TimingStage"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"workspace_agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_agent_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.AgentScriptTiming": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -8006,7 +8028,7 @@
|
||||
"type": "integer"
|
||||
},
|
||||
"stage": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.TimingStage"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
@@ -8014,6 +8036,12 @@
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_agent_id": {
|
||||
"type": "string"
|
||||
},
|
||||
"workspace_agent_name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -10986,7 +11014,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"stage": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/codersdk.TimingStage"
|
||||
},
|
||||
"started_at": {
|
||||
"type": "string",
|
||||
@@ -12228,6 +12256,29 @@
|
||||
"enum": ["UNSUPPORTED_WORKSPACES"],
|
||||
"x-enum-varnames": ["TemplateVersionWarningUnsupportedWorkspaces"]
|
||||
},
|
||||
"codersdk.TimingStage": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"init",
|
||||
"plan",
|
||||
"graph",
|
||||
"apply",
|
||||
"start",
|
||||
"stop",
|
||||
"cron",
|
||||
"connect"
|
||||
],
|
||||
"x-enum-varnames": [
|
||||
"TimingStageInit",
|
||||
"TimingStagePlan",
|
||||
"TimingStageGraph",
|
||||
"TimingStageApply",
|
||||
"TimingStageStart",
|
||||
"TimingStageStop",
|
||||
"TimingStageCron",
|
||||
"TimingStageConnect"
|
||||
]
|
||||
},
|
||||
"codersdk.TokenConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -13475,7 +13526,14 @@
|
||||
"codersdk.WorkspaceBuildTimings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"agent_connection_timings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AgentConnectionTiming"
|
||||
}
|
||||
},
|
||||
"agent_script_timings": {
|
||||
"description": "TODO: Consolidate agent-related timing metrics into a single struct when\nupdating the API version",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.AgentScriptTiming"
|
||||
|
||||
@@ -2894,13 +2894,15 @@ func (s *MethodTestSuite) TestSystemFunctions() {
|
||||
})
|
||||
rows := []database.GetWorkspaceAgentScriptTimingsByBuildIDRow{
|
||||
{
|
||||
StartedAt: timing.StartedAt,
|
||||
EndedAt: timing.EndedAt,
|
||||
Stage: timing.Stage,
|
||||
ScriptID: timing.ScriptID,
|
||||
ExitCode: timing.ExitCode,
|
||||
Status: timing.Status,
|
||||
DisplayName: script.DisplayName,
|
||||
StartedAt: timing.StartedAt,
|
||||
EndedAt: timing.EndedAt,
|
||||
Stage: timing.Stage,
|
||||
ScriptID: timing.ScriptID,
|
||||
ExitCode: timing.ExitCode,
|
||||
Status: timing.Status,
|
||||
DisplayName: script.DisplayName,
|
||||
WorkspaceAgentID: agent.ID,
|
||||
WorkspaceAgentName: agent.Name,
|
||||
},
|
||||
}
|
||||
check.Args(build.ID).Asserts(rbac.ResourceSystem, policy.ActionRead).Returns(rows)
|
||||
|
||||
@@ -5899,15 +5899,31 @@ func (q *FakeQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Contex
|
||||
break
|
||||
}
|
||||
}
|
||||
if script.ID == uuid.Nil {
|
||||
return nil, xerrors.Errorf("script with ID %s not found", t.ScriptID)
|
||||
}
|
||||
|
||||
var agent database.WorkspaceAgent
|
||||
for _, a := range agents {
|
||||
if a.ID == script.WorkspaceAgentID {
|
||||
agent = a
|
||||
break
|
||||
}
|
||||
}
|
||||
if agent.ID == uuid.Nil {
|
||||
return nil, xerrors.Errorf("agent with ID %s not found", t.ScriptID)
|
||||
}
|
||||
|
||||
rows = append(rows, database.GetWorkspaceAgentScriptTimingsByBuildIDRow{
|
||||
ScriptID: t.ScriptID,
|
||||
StartedAt: t.StartedAt,
|
||||
EndedAt: t.EndedAt,
|
||||
ExitCode: t.ExitCode,
|
||||
Stage: t.Stage,
|
||||
Status: t.Status,
|
||||
DisplayName: script.DisplayName,
|
||||
ScriptID: t.ScriptID,
|
||||
StartedAt: t.StartedAt,
|
||||
EndedAt: t.EndedAt,
|
||||
ExitCode: t.ExitCode,
|
||||
Stage: t.Stage,
|
||||
Status: t.Status,
|
||||
DisplayName: script.DisplayName,
|
||||
WorkspaceAgentID: agent.ID,
|
||||
WorkspaceAgentName: agent.Name,
|
||||
})
|
||||
}
|
||||
return rows, nil
|
||||
|
||||
@@ -11454,7 +11454,11 @@ func (q *sqlQuerier) GetWorkspaceAgentMetadata(ctx context.Context, arg GetWorks
|
||||
}
|
||||
|
||||
const getWorkspaceAgentScriptTimingsByBuildID = `-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
|
||||
SELECT workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status, workspace_agent_scripts.display_name
|
||||
SELECT
|
||||
workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at, workspace_agent_script_timings.ended_at, workspace_agent_script_timings.exit_code, workspace_agent_script_timings.stage, workspace_agent_script_timings.status,
|
||||
workspace_agent_scripts.display_name,
|
||||
workspace_agents.id as workspace_agent_id,
|
||||
workspace_agents.name as workspace_agent_name
|
||||
FROM workspace_agent_script_timings
|
||||
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
|
||||
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
|
||||
@@ -11464,13 +11468,15 @@ WHERE workspace_builds.id = $1
|
||||
`
|
||||
|
||||
type GetWorkspaceAgentScriptTimingsByBuildIDRow struct {
|
||||
ScriptID uuid.UUID `db:"script_id" json:"script_id"`
|
||||
StartedAt time.Time `db:"started_at" json:"started_at"`
|
||||
EndedAt time.Time `db:"ended_at" json:"ended_at"`
|
||||
ExitCode int32 `db:"exit_code" json:"exit_code"`
|
||||
Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"`
|
||||
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
ScriptID uuid.UUID `db:"script_id" json:"script_id"`
|
||||
StartedAt time.Time `db:"started_at" json:"started_at"`
|
||||
EndedAt time.Time `db:"ended_at" json:"ended_at"`
|
||||
ExitCode int32 `db:"exit_code" json:"exit_code"`
|
||||
Stage WorkspaceAgentScriptTimingStage `db:"stage" json:"stage"`
|
||||
Status WorkspaceAgentScriptTimingStatus `db:"status" json:"status"`
|
||||
DisplayName string `db:"display_name" json:"display_name"`
|
||||
WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"`
|
||||
WorkspaceAgentName string `db:"workspace_agent_name" json:"workspace_agent_name"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context, id uuid.UUID) ([]GetWorkspaceAgentScriptTimingsByBuildIDRow, error) {
|
||||
@@ -11490,6 +11496,8 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context
|
||||
&i.Stage,
|
||||
&i.Status,
|
||||
&i.DisplayName,
|
||||
&i.WorkspaceAgentID,
|
||||
&i.WorkspaceAgentName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -303,7 +303,11 @@ VALUES
|
||||
RETURNING workspace_agent_script_timings.*;
|
||||
|
||||
-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
|
||||
SELECT workspace_agent_script_timings.*, workspace_agent_scripts.display_name
|
||||
SELECT
|
||||
workspace_agent_script_timings.*,
|
||||
workspace_agent_scripts.display_name,
|
||||
workspace_agents.id as workspace_agent_id,
|
||||
workspace_agents.name as workspace_agent_name
|
||||
FROM workspace_agent_script_timings
|
||||
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
|
||||
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
|
||||
|
||||
@@ -957,15 +957,29 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild)
|
||||
return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agent script timings: %w", err)
|
||||
}
|
||||
|
||||
resources, err := api.Database.GetWorkspaceResourcesByJobID(ctx, build.JobID)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace resources: %w", err)
|
||||
}
|
||||
resourceIDs := make([]uuid.UUID, 0, len(resources))
|
||||
for _, resource := range resources {
|
||||
resourceIDs = append(resourceIDs, resource.ID)
|
||||
}
|
||||
agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(ctx, resourceIDs)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return codersdk.WorkspaceBuildTimings{}, xerrors.Errorf("fetching workspace agents: %w", err)
|
||||
}
|
||||
|
||||
res := codersdk.WorkspaceBuildTimings{
|
||||
ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)),
|
||||
AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)),
|
||||
ProvisionerTimings: make([]codersdk.ProvisionerTiming, 0, len(provisionerTimings)),
|
||||
AgentScriptTimings: make([]codersdk.AgentScriptTiming, 0, len(agentScriptTimings)),
|
||||
AgentConnectionTimings: make([]codersdk.AgentConnectionTiming, 0, len(agents)),
|
||||
}
|
||||
|
||||
for _, t := range provisionerTimings {
|
||||
res.ProvisionerTimings = append(res.ProvisionerTimings, codersdk.ProvisionerTiming{
|
||||
JobID: t.JobID,
|
||||
Stage: string(t.Stage),
|
||||
Stage: codersdk.TimingStage(t.Stage),
|
||||
Source: t.Source,
|
||||
Action: t.Action,
|
||||
Resource: t.Resource,
|
||||
@@ -975,12 +989,23 @@ func (api *API) buildTimings(ctx context.Context, build database.WorkspaceBuild)
|
||||
}
|
||||
for _, t := range agentScriptTimings {
|
||||
res.AgentScriptTimings = append(res.AgentScriptTimings, codersdk.AgentScriptTiming{
|
||||
StartedAt: t.StartedAt,
|
||||
EndedAt: t.EndedAt,
|
||||
ExitCode: t.ExitCode,
|
||||
Stage: string(t.Stage),
|
||||
Status: string(t.Status),
|
||||
DisplayName: t.DisplayName,
|
||||
StartedAt: t.StartedAt,
|
||||
EndedAt: t.EndedAt,
|
||||
ExitCode: t.ExitCode,
|
||||
Stage: codersdk.TimingStage(t.Stage),
|
||||
Status: string(t.Status),
|
||||
DisplayName: t.DisplayName,
|
||||
WorkspaceAgentID: t.WorkspaceAgentID.String(),
|
||||
WorkspaceAgentName: t.WorkspaceAgentName,
|
||||
})
|
||||
}
|
||||
for _, agent := range agents {
|
||||
res.AgentConnectionTimings = append(res.AgentConnectionTimings, codersdk.AgentConnectionTiming{
|
||||
WorkspaceAgentID: agent.ID.String(),
|
||||
WorkspaceAgentName: agent.Name,
|
||||
StartedAt: agent.CreatedAt,
|
||||
Stage: codersdk.TimingStageConnect,
|
||||
EndedAt: agent.FirstConnectedAt.Time,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1183,8 +1183,9 @@ func TestPostWorkspaceBuild(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:paralleltest
|
||||
func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup the test environment with a template and version
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
client := coderdtest.New(t, &coderdtest.Options{
|
||||
@@ -1237,10 +1238,13 @@ func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("NonExistentBuild", func(t *testing.T) {
|
||||
// When: fetching an inexistent build
|
||||
t.Parallel()
|
||||
|
||||
// Given: a non-existent build
|
||||
buildID := uuid.New()
|
||||
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
_, err := client.WorkspaceBuildTimings(ctx, buildID)
|
||||
@@ -1250,10 +1254,13 @@ func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
require.Contains(t, err.Error(), "not found")
|
||||
})
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("EmptyTimings", func(t *testing.T) {
|
||||
// When: fetching timings for a build with no timings
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with no timings
|
||||
build := makeBuild()
|
||||
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
@@ -1264,25 +1271,27 @@ func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
require.Empty(t, res.AgentScriptTimings)
|
||||
})
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("ProvisionerTimings", func(t *testing.T) {
|
||||
// When: fetching timings for a build with provisioner timings
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with provisioner timings
|
||||
build := makeBuild()
|
||||
provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5)
|
||||
|
||||
// Then: return a response with the expected timings
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.ProvisionerTimings, 5)
|
||||
|
||||
// Then: return a response with the expected timings
|
||||
require.Len(t, res.ProvisionerTimings, 5)
|
||||
for i := range res.ProvisionerTimings {
|
||||
timingRes := res.ProvisionerTimings[i]
|
||||
genTiming := provisionerTimings[i]
|
||||
require.Equal(t, genTiming.Resource, timingRes.Resource)
|
||||
require.Equal(t, genTiming.Action, timingRes.Action)
|
||||
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
|
||||
require.Equal(t, string(genTiming.Stage), string(timingRes.Stage))
|
||||
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
|
||||
require.Equal(t, genTiming.Source, timingRes.Source)
|
||||
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
|
||||
@@ -1290,9 +1299,10 @@ func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("AgentScriptTimings", func(t *testing.T) {
|
||||
// When: fetching timings for a build with agent script timings
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with agent script timings
|
||||
build := makeBuild()
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
@@ -1305,27 +1315,31 @@ func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
})
|
||||
agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5)
|
||||
|
||||
// Then: return a response with the expected timings
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res.AgentScriptTimings, 5)
|
||||
|
||||
// Then: return a response with the expected timings
|
||||
require.Len(t, res.AgentScriptTimings, 5)
|
||||
for i := range res.AgentScriptTimings {
|
||||
timingRes := res.AgentScriptTimings[i]
|
||||
genTiming := agentScriptTimings[i]
|
||||
require.Equal(t, genTiming.ExitCode, timingRes.ExitCode)
|
||||
require.Equal(t, string(genTiming.Status), timingRes.Status)
|
||||
require.Equal(t, string(genTiming.Stage), timingRes.Stage)
|
||||
require.Equal(t, string(genTiming.Stage), string(timingRes.Stage))
|
||||
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
|
||||
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
|
||||
require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID)
|
||||
require.Equal(t, agent.Name, timingRes.WorkspaceAgentName)
|
||||
}
|
||||
})
|
||||
|
||||
//nolint:paralleltest
|
||||
t.Run("NoAgentScripts", func(t *testing.T) {
|
||||
// When: fetching timings for a build with no agent scripts
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with no agent scripts
|
||||
build := makeBuild()
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
@@ -1334,29 +1348,88 @@ func TestWorkspaceBuildTimings(t *testing.T) {
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
|
||||
// Then: return a response with empty agent script timings
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: return a response with empty agent script timings
|
||||
require.Empty(t, res.AgentScriptTimings)
|
||||
})
|
||||
|
||||
// Some workspaces might not have agents. It is improbable, but possible.
|
||||
//nolint:paralleltest
|
||||
t.Run("NoAgents", func(t *testing.T) {
|
||||
// When: fetching timings for a build with no agents
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with no agents
|
||||
build := makeBuild()
|
||||
dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
|
||||
// Then: return a response with empty agent script timings
|
||||
// trigger build
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: return a response with empty agent script timings
|
||||
require.Empty(t, res.AgentScriptTimings)
|
||||
require.Empty(t, res.AgentConnectionTimings)
|
||||
})
|
||||
|
||||
t.Run("AgentConnectionTimings", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with an agent
|
||||
build := makeBuild()
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: return a response with the expected timings
|
||||
require.Len(t, res.AgentConnectionTimings, 1)
|
||||
for i := range res.ProvisionerTimings {
|
||||
timingRes := res.AgentConnectionTimings[i]
|
||||
require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID)
|
||||
require.Equal(t, agent.Name, timingRes.WorkspaceAgentName)
|
||||
require.NotEmpty(t, timingRes.StartedAt)
|
||||
require.NotEmpty(t, timingRes.EndedAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MultipleAgents", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: a build with multiple agents
|
||||
build := makeBuild()
|
||||
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||||
JobID: build.JobID,
|
||||
})
|
||||
agents := make([]database.WorkspaceAgent, 5)
|
||||
for i := range agents {
|
||||
agents[i] = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||||
ResourceID: resource.ID,
|
||||
})
|
||||
}
|
||||
|
||||
// When: fetching timings for the build
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
t.Cleanup(cancel)
|
||||
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Then: return a response with the expected timings
|
||||
require.Len(t, res.AgentConnectionTimings, 5)
|
||||
})
|
||||
}
|
||||
|
||||
+43
-14
@@ -175,28 +175,57 @@ func (c *Client) WorkspaceBuildParameters(ctx context.Context, build uuid.UUID)
|
||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||
}
|
||||
|
||||
type TimingStage string
|
||||
|
||||
const (
|
||||
// Based on ProvisionerJobTimingStage
|
||||
TimingStageInit TimingStage = "init"
|
||||
TimingStagePlan TimingStage = "plan"
|
||||
TimingStageGraph TimingStage = "graph"
|
||||
TimingStageApply TimingStage = "apply"
|
||||
// Based on WorkspaceAgentScriptTimingStage
|
||||
TimingStageStart TimingStage = "start"
|
||||
TimingStageStop TimingStage = "stop"
|
||||
TimingStageCron TimingStage = "cron"
|
||||
// Custom timing stage to represent the time taken to connect to an agent
|
||||
TimingStageConnect TimingStage = "connect"
|
||||
)
|
||||
|
||||
type ProvisionerTiming struct {
|
||||
JobID uuid.UUID `json:"job_id" format:"uuid"`
|
||||
StartedAt time.Time `json:"started_at" format:"date-time"`
|
||||
EndedAt time.Time `json:"ended_at" format:"date-time"`
|
||||
Stage string `json:"stage"`
|
||||
Source string `json:"source"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
JobID uuid.UUID `json:"job_id" format:"uuid"`
|
||||
StartedAt time.Time `json:"started_at" format:"date-time"`
|
||||
EndedAt time.Time `json:"ended_at" format:"date-time"`
|
||||
Stage TimingStage `json:"stage"`
|
||||
Source string `json:"source"`
|
||||
Action string `json:"action"`
|
||||
Resource string `json:"resource"`
|
||||
}
|
||||
|
||||
type AgentScriptTiming struct {
|
||||
StartedAt time.Time `json:"started_at" format:"date-time"`
|
||||
EndedAt time.Time `json:"ended_at" format:"date-time"`
|
||||
ExitCode int32 `json:"exit_code"`
|
||||
Stage string `json:"stage"`
|
||||
Status string `json:"status"`
|
||||
DisplayName string `json:"display_name"`
|
||||
StartedAt time.Time `json:"started_at" format:"date-time"`
|
||||
EndedAt time.Time `json:"ended_at" format:"date-time"`
|
||||
ExitCode int32 `json:"exit_code"`
|
||||
Stage TimingStage `json:"stage"`
|
||||
Status string `json:"status"`
|
||||
DisplayName string `json:"display_name"`
|
||||
WorkspaceAgentID string `json:"workspace_agent_id"`
|
||||
WorkspaceAgentName string `json:"workspace_agent_name"`
|
||||
}
|
||||
|
||||
type AgentConnectionTiming struct {
|
||||
StartedAt time.Time `json:"started_at" format:"date-time"`
|
||||
EndedAt time.Time `json:"ended_at" format:"date-time"`
|
||||
Stage TimingStage `json:"stage"`
|
||||
WorkspaceAgentID string `json:"workspace_agent_id"`
|
||||
WorkspaceAgentName string `json:"workspace_agent_name"`
|
||||
}
|
||||
|
||||
type WorkspaceBuildTimings struct {
|
||||
ProvisionerTimings []ProvisionerTiming `json:"provisioner_timings"`
|
||||
AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"`
|
||||
// TODO: Consolidate agent-related timing metrics into a single struct when
|
||||
// updating the API version
|
||||
AgentScriptTimings []AgentScriptTiming `json:"agent_script_timings"`
|
||||
AgentConnectionTimings []AgentConnectionTiming `json:"agent_connection_timings"`
|
||||
}
|
||||
|
||||
func (c *Client) WorkspaceBuildTimings(ctx context.Context, build uuid.UUID) (WorkspaceBuildTimings, error) {
|
||||
|
||||
Generated
+14
-3
@@ -1016,14 +1016,25 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_connection_timings": [
|
||||
{
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
],
|
||||
"agent_script_timings": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"exit_code": 0,
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"status": "string"
|
||||
"status": "string",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
],
|
||||
"provisioner_timings": [
|
||||
@@ -1033,7 +1044,7 @@ curl -X GET http://coder-server:8080/api/v2/workspacebuilds/{workspacebuild}/tim
|
||||
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
|
||||
"resource": "string",
|
||||
"source": "string",
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z"
|
||||
}
|
||||
]
|
||||
|
||||
Generated
+86
-27
@@ -349,6 +349,28 @@
|
||||
| --------- | ------ | -------- | ------------ | ----------- |
|
||||
| `license` | string | true | | |
|
||||
|
||||
## codersdk.AgentConnectionTiming
|
||||
|
||||
```json
|
||||
{
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------- | -------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `ended_at` | string | false | | |
|
||||
| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | |
|
||||
| `started_at` | string | false | | |
|
||||
| `workspace_agent_id` | string | false | | |
|
||||
| `workspace_agent_name` | string | false | | |
|
||||
|
||||
## codersdk.AgentScriptTiming
|
||||
|
||||
```json
|
||||
@@ -356,22 +378,26 @@
|
||||
"display_name": "string",
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"exit_code": 0,
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"status": "string"
|
||||
"status": "string",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ------- | -------- | ------------ | ----------- |
|
||||
| `display_name` | string | false | | |
|
||||
| `ended_at` | string | false | | |
|
||||
| `exit_code` | integer | false | | |
|
||||
| `stage` | string | false | | |
|
||||
| `started_at` | string | false | | |
|
||||
| `status` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------- | -------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `display_name` | string | false | | |
|
||||
| `ended_at` | string | false | | |
|
||||
| `exit_code` | integer | false | | |
|
||||
| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | |
|
||||
| `started_at` | string | false | | |
|
||||
| `status` | string | false | | |
|
||||
| `workspace_agent_id` | string | false | | |
|
||||
| `workspace_agent_name` | string | false | | |
|
||||
|
||||
## codersdk.AgentSubsystem
|
||||
|
||||
@@ -4359,22 +4385,22 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
|
||||
"resource": "string",
|
||||
"source": "string",
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------ | ------ | -------- | ------------ | ----------- |
|
||||
| `action` | string | false | | |
|
||||
| `ended_at` | string | false | | |
|
||||
| `job_id` | string | false | | |
|
||||
| `resource` | string | false | | |
|
||||
| `source` | string | false | | |
|
||||
| `stage` | string | false | | |
|
||||
| `started_at` | string | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------------ | -------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `action` | string | false | | |
|
||||
| `ended_at` | string | false | | |
|
||||
| `job_id` | string | false | | |
|
||||
| `resource` | string | false | | |
|
||||
| `source` | string | false | | |
|
||||
| `stage` | [codersdk.TimingStage](#codersdktimingstage) | false | | |
|
||||
| `started_at` | string | false | | |
|
||||
|
||||
## codersdk.ProxyHealthReport
|
||||
|
||||
@@ -5717,6 +5743,27 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o
|
||||
| ------------------------ |
|
||||
| `UNSUPPORTED_WORKSPACES` |
|
||||
|
||||
## codersdk.TimingStage
|
||||
|
||||
```json
|
||||
"init"
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value |
|
||||
| --------- |
|
||||
| `init` |
|
||||
| `plan` |
|
||||
| `graph` |
|
||||
| `apply` |
|
||||
| `start` |
|
||||
| `stop` |
|
||||
| `cron` |
|
||||
| `connect` |
|
||||
|
||||
## codersdk.TokenConfig
|
||||
|
||||
```json
|
||||
@@ -7381,14 +7428,25 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_connection_timings": [
|
||||
{
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
],
|
||||
"agent_script_timings": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"exit_code": 0,
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"status": "string"
|
||||
"status": "string",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
],
|
||||
"provisioner_timings": [
|
||||
@@ -7398,7 +7456,7 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
|
||||
"resource": "string",
|
||||
"source": "string",
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z"
|
||||
}
|
||||
]
|
||||
@@ -7407,10 +7465,11 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ---------------------- | ----------------------------------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | |
|
||||
| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | |
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------------------- | ------------------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------- |
|
||||
| `agent_connection_timings` | array of [codersdk.AgentConnectionTiming](#codersdkagentconnectiontiming) | false | | |
|
||||
| `agent_script_timings` | array of [codersdk.AgentScriptTiming](#codersdkagentscripttiming) | false | | Agent script timings Consolidate agent-related timing metrics into a single struct when updating the API version |
|
||||
| `provisioner_timings` | array of [codersdk.ProvisionerTiming](#codersdkprovisionertiming) | false | | |
|
||||
|
||||
## codersdk.WorkspaceConnectionLatencyMS
|
||||
|
||||
|
||||
Generated
+14
-3
@@ -1641,14 +1641,25 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \
|
||||
|
||||
```json
|
||||
{
|
||||
"agent_connection_timings": [
|
||||
{
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
],
|
||||
"agent_script_timings": [
|
||||
{
|
||||
"display_name": "string",
|
||||
"ended_at": "2019-08-24T14:15:22Z",
|
||||
"exit_code": 0,
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z",
|
||||
"status": "string"
|
||||
"status": "string",
|
||||
"workspace_agent_id": "string",
|
||||
"workspace_agent_name": "string"
|
||||
}
|
||||
],
|
||||
"provisioner_timings": [
|
||||
@@ -1658,7 +1669,7 @@ curl -X GET http://coder-server:8080/api/v2/workspaces/{workspace}/timings \
|
||||
"job_id": "453bd7d7-5355-4d6d-a38e-d9e7eb218c3f",
|
||||
"resource": "string",
|
||||
"source": "string",
|
||||
"stage": "string",
|
||||
"stage": "init",
|
||||
"started_at": "2019-08-24T14:15:22Z"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -57,9 +57,18 @@ export const infiniteWorkspaceBuilds = (
|
||||
};
|
||||
};
|
||||
|
||||
export const workspaceBuildTimings = (workspaceBuildId: string) => {
|
||||
// We use readyAgentsCount to invalidate the query when an agent connects
|
||||
export const workspaceBuildTimings = (
|
||||
workspaceBuildId: string,
|
||||
readyAgentsCount: number,
|
||||
) => {
|
||||
return {
|
||||
queryKey: ["workspaceBuilds", workspaceBuildId, "timings"],
|
||||
queryKey: [
|
||||
"workspaceBuilds",
|
||||
workspaceBuildId,
|
||||
"timings",
|
||||
{ readyAgentsCount },
|
||||
],
|
||||
queryFn: () => API.workspaceBuildTimings(workspaceBuildId),
|
||||
};
|
||||
};
|
||||
|
||||
Generated
+18
-2
@@ -32,14 +32,25 @@ export interface AddLicenseRequest {
|
||||
readonly license: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export interface AgentConnectionTiming {
|
||||
readonly started_at: string;
|
||||
readonly ended_at: string;
|
||||
readonly stage: TimingStage;
|
||||
readonly workspace_agent_id: string;
|
||||
readonly workspace_agent_name: string;
|
||||
}
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export interface AgentScriptTiming {
|
||||
readonly started_at: string;
|
||||
readonly ended_at: string;
|
||||
readonly exit_code: number;
|
||||
readonly stage: string;
|
||||
readonly stage: TimingStage;
|
||||
readonly status: string;
|
||||
readonly display_name: string;
|
||||
readonly workspace_agent_id: string;
|
||||
readonly workspace_agent_name: string;
|
||||
}
|
||||
|
||||
// From codersdk/templates.go
|
||||
@@ -1104,7 +1115,7 @@ export interface ProvisionerTiming {
|
||||
readonly job_id: string;
|
||||
readonly started_at: string;
|
||||
readonly ended_at: string;
|
||||
readonly stage: string;
|
||||
readonly stage: TimingStage;
|
||||
readonly source: string;
|
||||
readonly action: string;
|
||||
readonly resource: string;
|
||||
@@ -1986,6 +1997,7 @@ export interface WorkspaceBuildParameter {
|
||||
export interface WorkspaceBuildTimings {
|
||||
readonly provisioner_timings: Readonly<Array<ProvisionerTiming>>;
|
||||
readonly agent_script_timings: Readonly<Array<AgentScriptTiming>>;
|
||||
readonly agent_connection_timings: Readonly<Array<AgentConnectionTiming>>;
|
||||
}
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
@@ -2226,6 +2238,10 @@ export const TemplateRoles: TemplateRole[] = ["", "admin", "use"]
|
||||
export type TemplateVersionWarning = "UNSUPPORTED_WORKSPACES"
|
||||
export const TemplateVersionWarnings: TemplateVersionWarning[] = ["UNSUPPORTED_WORKSPACES"]
|
||||
|
||||
// From codersdk/workspacebuilds.go
|
||||
export type TimingStage = "apply" | "connect" | "cron" | "graph" | "init" | "plan" | "start" | "stop"
|
||||
export const TimingStages: TimingStage[] = ["apply", "connect", "cron", "graph", "init", "plan", "start", "stop"]
|
||||
|
||||
// From codersdk/workspaces.go
|
||||
export type UsageAppName = "jetbrains" | "reconnecting-pty" | "ssh" | "vscode"
|
||||
export const UsageAppNames: UsageAppName[] = ["jetbrains", "reconnecting-pty", "ssh", "vscode"]
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { css } from "@emotion/css";
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
import { type Theme, useTheme } from "@emotion/react";
|
||||
import { type FC, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Bar } from "./Chart/Bar";
|
||||
import {
|
||||
Chart,
|
||||
@@ -30,7 +27,7 @@ import {
|
||||
makeTicks,
|
||||
mergeTimeRanges,
|
||||
} from "./Chart/utils";
|
||||
import type { StageCategory } from "./StagesChart";
|
||||
import type { Stage } from "./StagesChart";
|
||||
|
||||
type ResourceTiming = {
|
||||
name: string;
|
||||
@@ -40,14 +37,12 @@ type ResourceTiming = {
|
||||
};
|
||||
|
||||
export type ResourcesChartProps = {
|
||||
category: StageCategory;
|
||||
stage: string;
|
||||
stage: Stage;
|
||||
timings: ResourceTiming[];
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const ResourcesChart: FC<ResourcesChartProps> = ({
|
||||
category,
|
||||
stage,
|
||||
timings,
|
||||
onBack,
|
||||
@@ -71,11 +66,11 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
|
||||
<ChartBreadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: category.name,
|
||||
label: stage.section,
|
||||
onClick: onBack,
|
||||
},
|
||||
{
|
||||
label: stage,
|
||||
label: stage.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -89,7 +84,7 @@ export const ResourcesChart: FC<ResourcesChartProps> = ({
|
||||
<ChartContent>
|
||||
<YAxis>
|
||||
<YAxisSection>
|
||||
<YAxisHeader>{stage} stage</YAxisHeader>
|
||||
<YAxisHeader>{stage.name} stage</YAxisHeader>
|
||||
<YAxisLabels>
|
||||
{visibleTimings.map((t) => (
|
||||
<YAxisLabel key={t.name} id={encodeURIComponent(t.name)}>
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
makeTicks,
|
||||
mergeTimeRanges,
|
||||
} from "./Chart/utils";
|
||||
import type { StageCategory } from "./StagesChart";
|
||||
import type { Stage } from "./StagesChart";
|
||||
|
||||
type ScriptTiming = {
|
||||
name: string;
|
||||
@@ -37,14 +37,12 @@ type ScriptTiming = {
|
||||
};
|
||||
|
||||
export type ScriptsChartProps = {
|
||||
category: StageCategory;
|
||||
stage: string;
|
||||
stage: Stage;
|
||||
timings: ScriptTiming[];
|
||||
onBack: () => void;
|
||||
};
|
||||
|
||||
export const ScriptsChart: FC<ScriptsChartProps> = ({
|
||||
category,
|
||||
stage,
|
||||
timings,
|
||||
onBack,
|
||||
@@ -66,11 +64,11 @@ export const ScriptsChart: FC<ScriptsChartProps> = ({
|
||||
<ChartBreadcrumbs
|
||||
breadcrumbs={[
|
||||
{
|
||||
label: category.name,
|
||||
label: stage.section,
|
||||
onClick: onBack,
|
||||
},
|
||||
{
|
||||
label: stage,
|
||||
label: stage.name,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -84,7 +82,7 @@ export const ScriptsChart: FC<ScriptsChartProps> = ({
|
||||
<ChartContent>
|
||||
<YAxis>
|
||||
<YAxisSection>
|
||||
<YAxisHeader>{stage} stage</YAxisHeader>
|
||||
<YAxisHeader>{stage.name} stage</YAxisHeader>
|
||||
<YAxisLabels>
|
||||
{visibleTimings.map((t) => (
|
||||
<YAxisLabel key={t.name} id={encodeURIComponent(t.name)}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import ErrorSharp from "@mui/icons-material/ErrorSharp";
|
||||
import InfoOutlined from "@mui/icons-material/InfoOutlined";
|
||||
import type { TimingStage } from "api/typesGenerated";
|
||||
import type { FC } from "react";
|
||||
import { Bar, ClickableBar } from "./Chart/Bar";
|
||||
import { Blocks } from "./Chart/Blocks";
|
||||
@@ -28,118 +29,34 @@ import {
|
||||
mergeTimeRanges,
|
||||
} from "./Chart/utils";
|
||||
|
||||
export type StageCategory = {
|
||||
name: string;
|
||||
id: "provisioning" | "workspaceBoot";
|
||||
};
|
||||
|
||||
const stageCategories: StageCategory[] = [
|
||||
{
|
||||
name: "provisioning",
|
||||
id: "provisioning",
|
||||
},
|
||||
{
|
||||
name: "workspace boot",
|
||||
id: "workspaceBoot",
|
||||
},
|
||||
] as const;
|
||||
|
||||
export type Stage = {
|
||||
name: string;
|
||||
categoryID: StageCategory["id"];
|
||||
/**
|
||||
* The name is used to identify the stage.
|
||||
*/
|
||||
name: TimingStage;
|
||||
/**
|
||||
* The value to display in the stage label. This can differ from the stage
|
||||
* name to provide more context or clarity.
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* The section is used to group stages together.
|
||||
*/
|
||||
section: string;
|
||||
/**
|
||||
* The tooltip is used to provide additional information about the stage.
|
||||
*/
|
||||
tooltip: Omit<TooltipProps, "children">;
|
||||
};
|
||||
|
||||
export const stages: Stage[] = [
|
||||
{
|
||||
name: "init",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform initialization</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Download providers & modules.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plan",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform plan</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Compare state of desired vs actual resources and compute changes to
|
||||
be made.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "graph",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform graph</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
List all resources in plan, used to update coderd database.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "apply",
|
||||
categoryID: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform apply</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Execute Terraform plan to create/modify/delete resources into
|
||||
desired states.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start",
|
||||
categoryID: "workspaceBoot",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Start</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Scripts executed when the agent is starting.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
type StageTiming = {
|
||||
name: string;
|
||||
/**
|
||||
stage: Stage;
|
||||
/**
|
||||
* Represents the number of resources included in this stage that can be
|
||||
* inspected. This value is used to display individual blocks within the bar,
|
||||
* indicating that the stage consists of multiple resource time blocks.
|
||||
*/
|
||||
visibleResources: number;
|
||||
/**
|
||||
* Represents the category of the stage. This value is used to group stages
|
||||
* together in the chart. For example, all provisioning stages are grouped
|
||||
* together.
|
||||
*/
|
||||
categoryID: StageCategory["id"];
|
||||
/**
|
||||
* Represents the time range of the stage. This value is used to calculate the
|
||||
* duration of the stage and to position the stage within the chart. This can
|
||||
@@ -155,7 +72,7 @@ type StageTiming = {
|
||||
|
||||
export type StagesChartProps = {
|
||||
timings: StageTiming[];
|
||||
onSelectStage: (timing: StageTiming, category: StageCategory) => void;
|
||||
onSelectStage: (stage: Stage) => void;
|
||||
};
|
||||
|
||||
export const StagesChart: FC<StagesChartProps> = ({
|
||||
@@ -167,27 +84,28 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
);
|
||||
const totalTime = calcDuration(totalRange);
|
||||
const [ticks, scale] = makeTicks(totalTime);
|
||||
const sections = Array.from(new Set(timings.map((t) => t.stage.section)));
|
||||
|
||||
return (
|
||||
<Chart>
|
||||
<ChartContent>
|
||||
<YAxis>
|
||||
{stageCategories.map((c) => {
|
||||
const stagesInCategory = stages.filter(
|
||||
(s) => s.categoryID === c.id,
|
||||
);
|
||||
{sections.map((section) => {
|
||||
const stages = timings
|
||||
.filter((t) => t.stage.section === section)
|
||||
.map((t) => t.stage);
|
||||
|
||||
return (
|
||||
<YAxisSection key={c.id}>
|
||||
<YAxisHeader>{c.name}</YAxisHeader>
|
||||
<YAxisSection key={section}>
|
||||
<YAxisHeader>{section}</YAxisHeader>
|
||||
<YAxisLabels>
|
||||
{stagesInCategory.map((stage) => (
|
||||
{stages.map((stage) => (
|
||||
<YAxisLabel
|
||||
key={stage.name}
|
||||
id={encodeURIComponent(stage.name)}
|
||||
>
|
||||
<span css={styles.stageLabel}>
|
||||
{stage.name}
|
||||
{stage.label}
|
||||
<Tooltip {...stage.tooltip}>
|
||||
<InfoOutlined css={styles.info} />
|
||||
</Tooltip>
|
||||
@@ -201,19 +119,19 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
</YAxis>
|
||||
|
||||
<XAxis ticks={ticks} scale={scale}>
|
||||
{stageCategories.map((category) => {
|
||||
{sections.map((section) => {
|
||||
const stageTimings = timings.filter(
|
||||
(t) => t.categoryID === category.id,
|
||||
(t) => t.stage.section === section,
|
||||
);
|
||||
return (
|
||||
<XAxisSection key={category.id}>
|
||||
<XAxisSection key={section}>
|
||||
{stageTimings.map((t) => {
|
||||
// If the stage has no timing data, we just want to render an empty row
|
||||
if (t.range === undefined) {
|
||||
return (
|
||||
<XAxisRow
|
||||
key={t.name}
|
||||
yAxisLabelId={encodeURIComponent(t.name)}
|
||||
key={t.stage.name}
|
||||
yAxisLabelId={encodeURIComponent(t.stage.name)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -223,18 +141,18 @@ export const StagesChart: FC<StagesChartProps> = ({
|
||||
|
||||
return (
|
||||
<XAxisRow
|
||||
key={t.name}
|
||||
yAxisLabelId={encodeURIComponent(t.name)}
|
||||
key={t.stage.name}
|
||||
yAxisLabelId={encodeURIComponent(t.stage.name)}
|
||||
>
|
||||
{/** We only want to expand stages with more than one resource */}
|
||||
{t.visibleResources > 1 ? (
|
||||
<ClickableBar
|
||||
aria-label={`View ${t.name} details`}
|
||||
aria-label={`View ${t.stage.label} details`}
|
||||
scale={scale}
|
||||
value={value}
|
||||
offset={offset}
|
||||
onClick={() => {
|
||||
onSelectStage(t, category);
|
||||
onSelectStage(t.stage);
|
||||
}}
|
||||
>
|
||||
{t.error && (
|
||||
@@ -281,3 +199,103 @@ const styles = {
|
||||
cursor: "pointer",
|
||||
}),
|
||||
} satisfies Record<string, Interpolation<Theme>>;
|
||||
|
||||
export const provisioningStages: Stage[] = [
|
||||
{
|
||||
name: "init",
|
||||
label: "init",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform initialization</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Download providers & modules.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "plan",
|
||||
label: "plan",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform plan</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Compare state of desired vs actual resources and compute changes to
|
||||
be made.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "graph",
|
||||
label: "graph",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform graph</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
List all resources in plan, used to update coderd database.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "apply",
|
||||
label: "apply",
|
||||
section: "provisioning",
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Terraform apply</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Execute Terraform plan to create/modify/delete resources into
|
||||
desired states.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const agentStages = (section: string): Stage[] => {
|
||||
return [
|
||||
{
|
||||
name: "connect",
|
||||
label: "connect",
|
||||
section,
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Connect</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Establish an RPC connection with the control plane.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "start",
|
||||
label: "run startup scripts",
|
||||
section,
|
||||
tooltip: {
|
||||
title: (
|
||||
<>
|
||||
<TooltipTitle>Run startup scripts</TooltipTitle>
|
||||
<TooltipShortDescription>
|
||||
Execute each agent startup script.
|
||||
</TooltipShortDescription>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ const meta: Meta<typeof WorkspaceTimings> = {
|
||||
defaultIsOpen: true,
|
||||
provisionerTimings: WorkspaceTimingsResponse.provisioner_timings,
|
||||
agentScriptTimings: WorkspaceTimingsResponse.agent_script_timings,
|
||||
agentConnectionTimings: WorkspaceTimingsResponse.agent_connection_timings,
|
||||
},
|
||||
parameters: {
|
||||
chromatic,
|
||||
@@ -32,6 +33,7 @@ export const Loading: Story = {
|
||||
args: {
|
||||
provisionerTimings: undefined,
|
||||
agentScriptTimings: undefined,
|
||||
agentConnectionTimings: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -45,7 +47,7 @@ export const ClickToOpen: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const user = userEvent.setup();
|
||||
const canvas = within(canvasElement);
|
||||
await user.click(canvas.getByRole("button"));
|
||||
await user.click(canvas.getByText("Build timeline", { exact: false }));
|
||||
await canvas.findByText("provisioning");
|
||||
},
|
||||
};
|
||||
@@ -58,9 +60,9 @@ export const ClickToClose: Story = {
|
||||
const user = userEvent.setup();
|
||||
const canvas = within(canvasElement);
|
||||
await canvas.findByText("provisioning");
|
||||
await user.click(canvas.getByText("Provisioning time", { exact: false }));
|
||||
await user.click(canvas.getByText("Build timeline", { exact: false }));
|
||||
await waitFor(() =>
|
||||
expect(canvas.getByText("workspace boot")).not.toBeVisible(),
|
||||
expect(canvas.queryByText("workspace boot")).not.toBeInTheDocument(),
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -96,7 +98,7 @@ export const NavigateToStartStage: Story = {
|
||||
const user = userEvent.setup();
|
||||
const canvas = within(canvasElement);
|
||||
const detailsButton = canvas.getByRole("button", {
|
||||
name: "View start details",
|
||||
name: "View run startup scripts details",
|
||||
});
|
||||
await user.click(detailsButton);
|
||||
await canvas.findByText("Startup Script");
|
||||
|
||||
@@ -4,19 +4,27 @@ import KeyboardArrowUp from "@mui/icons-material/KeyboardArrowUp";
|
||||
import Button from "@mui/material/Button";
|
||||
import Collapse from "@mui/material/Collapse";
|
||||
import Skeleton from "@mui/material/Skeleton";
|
||||
import type { AgentScriptTiming, ProvisionerTiming } from "api/typesGenerated";
|
||||
import type {
|
||||
AgentConnectionTiming,
|
||||
AgentScriptTiming,
|
||||
ProvisionerTiming,
|
||||
} from "api/typesGenerated";
|
||||
import { type FC, useState } from "react";
|
||||
import { type TimeRange, calcDuration, mergeTimeRanges } from "./Chart/utils";
|
||||
import { ResourcesChart, isCoderResource } from "./ResourcesChart";
|
||||
import { ScriptsChart } from "./ScriptsChart";
|
||||
import { type StageCategory, StagesChart, stages } from "./StagesChart";
|
||||
import {
|
||||
type Stage,
|
||||
StagesChart,
|
||||
agentStages,
|
||||
provisioningStages,
|
||||
} from "./StagesChart";
|
||||
|
||||
type TimingView =
|
||||
| { name: "default" }
|
||||
| {
|
||||
name: "detailed";
|
||||
stage: string;
|
||||
category: StageCategory;
|
||||
stage: Stage;
|
||||
filter: string;
|
||||
};
|
||||
|
||||
@@ -24,20 +32,37 @@ type WorkspaceTimingsProps = {
|
||||
defaultIsOpen?: boolean;
|
||||
provisionerTimings: readonly ProvisionerTiming[] | undefined;
|
||||
agentScriptTimings: readonly AgentScriptTiming[] | undefined;
|
||||
agentConnectionTimings: readonly AgentConnectionTiming[] | undefined;
|
||||
};
|
||||
|
||||
export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
provisionerTimings = [],
|
||||
agentScriptTimings = [],
|
||||
agentConnectionTimings = [],
|
||||
defaultIsOpen = false,
|
||||
}) => {
|
||||
const [view, setView] = useState<TimingView>({ name: "default" });
|
||||
const timings = [...provisionerTimings, ...agentScriptTimings];
|
||||
const timings = [
|
||||
...provisionerTimings,
|
||||
...agentScriptTimings,
|
||||
...agentConnectionTimings,
|
||||
];
|
||||
const [isOpen, setIsOpen] = useState(defaultIsOpen);
|
||||
const isLoading = timings.length === 0;
|
||||
|
||||
// All stages
|
||||
const agentStageLabels = Array.from(
|
||||
new Set(
|
||||
agentConnectionTimings.map((t) => `agent (${t.workspace_agent_name})`),
|
||||
),
|
||||
);
|
||||
const stages = [
|
||||
...provisioningStages,
|
||||
...agentStageLabels.flatMap((a) => agentStages(a)),
|
||||
];
|
||||
|
||||
const displayProvisioningTime = () => {
|
||||
const totalRange = mergeTimeRanges(timings.map(extractRange));
|
||||
const totalRange = mergeTimeRanges(timings.map(toTimeRange));
|
||||
const totalDuration = calcDuration(totalRange);
|
||||
return humanizeDuration(totalDuration);
|
||||
};
|
||||
@@ -81,7 +106,7 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
const stageRange =
|
||||
stageTimings.length === 0
|
||||
? undefined
|
||||
: mergeTimeRanges(stageTimings.map(extractRange));
|
||||
: mergeTimeRanges(stageTimings.map(toTimeRange));
|
||||
|
||||
// Prevent users from inspecting internal coder resources in
|
||||
// provisioner timings.
|
||||
@@ -93,67 +118,63 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
});
|
||||
|
||||
return {
|
||||
stage: s,
|
||||
range: stageRange,
|
||||
name: s.name,
|
||||
categoryID: s.categoryID,
|
||||
visibleResources: visibleResources.length,
|
||||
error: stageTimings.some(
|
||||
(t) => "status" in t && t.status === "exit_failure",
|
||||
),
|
||||
};
|
||||
})}
|
||||
onSelectStage={(t, category) => {
|
||||
onSelectStage={(stage) => {
|
||||
setView({
|
||||
stage,
|
||||
name: "detailed",
|
||||
stage: t.name,
|
||||
category,
|
||||
filter: "",
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view.name === "detailed" &&
|
||||
view.category.id === "provisioning" && (
|
||||
<ResourcesChart
|
||||
timings={provisionerTimings
|
||||
.filter((t) => t.stage === view.stage)
|
||||
.map((t) => {
|
||||
return {
|
||||
range: extractRange(t),
|
||||
{view.name === "detailed" && (
|
||||
<>
|
||||
{view.stage.section === "provisioning" && (
|
||||
<ResourcesChart
|
||||
timings={provisionerTimings
|
||||
.filter((t) => t.stage === view.stage.name)
|
||||
.map((t) => ({
|
||||
range: toTimeRange(t),
|
||||
name: t.resource,
|
||||
source: t.source,
|
||||
action: t.action,
|
||||
};
|
||||
})}
|
||||
category={view.category}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
}))}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{view.name === "detailed" &&
|
||||
view.category.id === "workspaceBoot" && (
|
||||
<ScriptsChart
|
||||
timings={agentScriptTimings
|
||||
.filter((t) => t.stage === view.stage)
|
||||
.map((t) => {
|
||||
return {
|
||||
range: extractRange(t),
|
||||
name: t.display_name,
|
||||
status: t.status,
|
||||
exitCode: t.exit_code,
|
||||
};
|
||||
})}
|
||||
category={view.category}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{view.stage.name === "start" && (
|
||||
<ScriptsChart
|
||||
timings={agentScriptTimings
|
||||
.filter((t) => t.stage === view.stage.name)
|
||||
.map((t) => {
|
||||
return {
|
||||
range: toTimeRange(t),
|
||||
name: t.display_name,
|
||||
status: t.status,
|
||||
exitCode: t.exit_code,
|
||||
};
|
||||
})}
|
||||
stage={view.stage}
|
||||
onBack={() => {
|
||||
setView({ name: "default" });
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Collapse>
|
||||
)}
|
||||
@@ -161,9 +182,10 @@ export const WorkspaceTimings: FC<WorkspaceTimingsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const extractRange = (
|
||||
timing: ProvisionerTiming | AgentScriptTiming,
|
||||
): TimeRange => {
|
||||
const toTimeRange = (timing: {
|
||||
started_at: string;
|
||||
ended_at: string;
|
||||
}): TimeRange => {
|
||||
return {
|
||||
startedAt: new Date(timing.started_at),
|
||||
endedAt: new Date(timing.ended_at),
|
||||
|
||||
@@ -355,6 +355,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Startup Script",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650915Z",
|
||||
@@ -363,6 +365,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Dotfiles",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650715Z",
|
||||
@@ -371,6 +375,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Personalize",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650512Z",
|
||||
@@ -379,6 +385,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "install_slackme",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650659Z",
|
||||
@@ -387,6 +395,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Coder Login",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650666Z",
|
||||
@@ -395,6 +405,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "File Browser",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.652425Z",
|
||||
@@ -403,6 +415,8 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "code-server",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
{
|
||||
started_at: "2024-10-14T11:30:56.650423Z",
|
||||
@@ -411,6 +425,17 @@ export const WorkspaceTimingsResponse: WorkspaceBuildTimings = {
|
||||
stage: "start",
|
||||
status: "ok",
|
||||
display_name: "Git Clone",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
],
|
||||
agent_connection_timings: [
|
||||
{
|
||||
started_at: "2024-10-14T11:30:55.650423Z",
|
||||
ended_at: "2024-10-14T11:30:56.650423Z",
|
||||
stage: "connect",
|
||||
workspace_agent_id: "86fd4143-d95f-4602-b464-1149ede62269",
|
||||
workspace_agent_name: "dev",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -267,8 +267,9 @@ export const Workspace: FC<WorkspaceProps> = ({
|
||||
)}
|
||||
|
||||
<WorkspaceTimings
|
||||
agentScriptTimings={timings?.agent_script_timings}
|
||||
provisionerTimings={timings?.provisioner_timings}
|
||||
agentScriptTimings={timings?.agent_script_timings}
|
||||
agentConnectionTimings={timings?.agent_connection_timings}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -158,8 +158,11 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
|
||||
const cancelBuildMutation = useMutation(cancelBuild(workspace, queryClient));
|
||||
|
||||
// Build Timings. Fetch build timings only when the build job is completed.
|
||||
const readyAgents = workspace.latest_build.resources
|
||||
.flatMap((r) => r.agents)
|
||||
.filter((a) => a && a.lifecycle_state !== "starting");
|
||||
const timingsQuery = useQuery({
|
||||
...workspaceBuildTimings(workspace.latest_build.id),
|
||||
...workspaceBuildTimings(workspace.latest_build.id, readyAgents.length),
|
||||
enabled: Boolean(workspace.latest_build.job.completed_at),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user