From 1c49916e9b75341d5c2c7e386fe53b39602f6712 Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Wed, 22 Jun 2022 03:31:42 +0000 Subject: [PATCH 1/5] fix: Use WebSockets to stream workspace build logs This was using a streaming HTTP request before, which didn't work on my version of Chrome. This method seemed less reliable and standard than a WebSocket, so figured switching would be best. --- coderd/provisionerjobs.go | 37 ++++++------ codersdk/provisionerdaemons.go | 28 ++++++++-- scripts/build_go_matrix.sh | 4 +- scripts/develop.sh | 6 +- site/src/api/api.ts | 19 +------ .../WorkspaceBuildPage.test.tsx | 33 ++++++----- .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 2 +- .../WorkspaceBuildPageView.tsx | 4 +- site/src/testHelpers/handlers.ts | 3 + .../workspaceBuild/workspaceBuildXService.ts | 56 ++++++++++++++----- 10 files changed, 119 insertions(+), 73 deletions(-) diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index f10ba0358aa2c..d67ee8689186a 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -11,6 +11,7 @@ import ( "time" "github.com/google/uuid" + "nhooyr.io/websocket" "cdr.dev/slog" @@ -98,12 +99,28 @@ func (api *API) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job return } + api.websocketWaitMutex.Lock() + api.websocketWaitGroup.Add(1) + api.websocketWaitMutex.Unlock() + defer api.websocketWaitGroup.Done() + conn, err := websocket.Accept(rw, r, nil) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "Failed to accept websocket.", + Detail: err.Error(), + }) + return + } + + ctx, wsNetConn := websocketNetConn(r.Context(), conn, websocket.MessageText) + defer wsNetConn.Close() // Also closes conn. + bufferedLogs := make(chan database.ProvisionerJobLog, 128) closeSubscribe, err := api.Pubsub.Subscribe(provisionerJobLogsChannel(job.ID), func(ctx context.Context, message []byte) { var logs []database.ProvisionerJobLog err := json.Unmarshal(message, &logs) if err != nil { - api.Logger.Warn(r.Context(), fmt.Sprintf("invalid provisioner job log on channel %q: %s", provisionerJobLogsChannel(job.ID), err.Error())) + api.Logger.Warn(ctx, fmt.Sprintf("invalid provisioner job log on channel %q: %s", provisionerJobLogsChannel(job.ID), err.Error())) return } @@ -113,7 +130,7 @@ func (api *API) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job default: // If this overflows users could miss logs streaming. This can happen // if a database request takes a long amount of time, and we get a lot of logs. - api.Logger.Warn(r.Context(), "provisioner job log overflowing channel") + api.Logger.Warn(ctx, "provisioner job log overflowing channel") } } }) @@ -126,7 +143,7 @@ func (api *API) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job } defer closeSubscribe() - provisionerJobLogs, err := api.Database.GetProvisionerLogsByIDBetween(r.Context(), database.GetProvisionerLogsByIDBetweenParams{ + provisionerJobLogs, err := api.Database.GetProvisionerLogsByIDBetween(ctx, database.GetProvisionerLogsByIDBetweenParams{ JobID: job.ID, CreatedAfter: after, CreatedBefore: before, @@ -142,17 +159,8 @@ func (api *API) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job return } - // "follow" uses the ndjson format to stream data. - // See: https://canjs.com/doc/can-ndjson-stream.html - rw.Header().Set("Content-Type", "application/stream+json") - rw.WriteHeader(http.StatusOK) - if flusher, ok := rw.(http.Flusher); ok { - flusher.Flush() - } - // The Go stdlib JSON encoder appends a newline character after message write. - encoder := json.NewEncoder(rw) - + encoder := json.NewEncoder(wsNetConn) for _, provisionerJobLog := range provisionerJobLogs { err = encoder.Encode(convertProvisionerJobLog(provisionerJobLog)) if err != nil { @@ -171,9 +179,6 @@ func (api *API) provisionerJobLogs(rw http.ResponseWriter, r *http.Request, job if err != nil { return } - if flusher, ok := rw.(http.Flusher); ok { - flusher.Flush() - } case <-ticker.C: job, err := api.Database.GetProvisionerJobByID(r.Context(), job.ID) if err != nil { diff --git a/codersdk/provisionerdaemons.go b/codersdk/provisionerdaemons.go index 5027d30ab9d60..584f4a6d6d102 100644 --- a/codersdk/provisionerdaemons.go +++ b/codersdk/provisionerdaemons.go @@ -6,11 +6,16 @@ import ( "encoding/json" "fmt" "net/http" + "net/http/cookiejar" "net/url" "strconv" "time" "github.com/google/uuid" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "github.com/coder/coder/coderd/httpmw" ) type LogSource string @@ -106,17 +111,30 @@ func (c *Client) provisionerJobLogsAfter(ctx context.Context, path string, after if !after.IsZero() { afterQuery = fmt.Sprintf("&after=%d", after.UTC().UnixMilli()) } - res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("%s?follow%s", path, afterQuery), nil) + followURL, err := c.URL.Parse(fmt.Sprintf("%s?follow%s", path, afterQuery)) if err != nil { return nil, err } - if res.StatusCode != http.StatusOK { - defer res.Body.Close() + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(followURL, []*http.Cookie{{ + Name: httpmw.SessionTokenKey, + Value: c.SessionToken, + }}) + httpClient := &http.Client{ + Jar: jar, + } + conn, res, err := websocket.Dial(ctx, followURL.String(), &websocket.DialOptions{ + HTTPClient: httpClient, + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { return nil, readBodyAsError(res) } - logs := make(chan ProvisionerJobLog) - decoder := json.NewDecoder(res.Body) + decoder := json.NewDecoder(websocket.NetConn(ctx, conn, websocket.MessageText)) go func() { defer close(logs) var log ProvisionerJobLog diff --git a/scripts/build_go_matrix.sh b/scripts/build_go_matrix.sh index 3082393d523e2..a60bc4719d9db 100755 --- a/scripts/build_go_matrix.sh +++ b/scripts/build_go_matrix.sh @@ -193,7 +193,7 @@ for spec in "${specs[@]}"; do --os "$spec_os" \ --arch "$spec_arch" \ --output "$spec_output_binary" \ - "${build_args[@]}" + "${build_args[@]}" & log log @@ -227,3 +227,5 @@ for spec in "${specs[@]}"; do log fi done + +wait diff --git a/scripts/develop.sh b/scripts/develop.sh index 3b4cc5c0d5376..851b635f935f1 100755 --- a/scripts/develop.sh +++ b/scripts/develop.sh @@ -19,16 +19,16 @@ echo '== Without these binaries, workspaces will fail to start!' cd "${PROJECT_ROOT}" trap 'kill 0' SIGINT - CODERV2_HOST=http://127.0.0.1:3000 INSPECT_XSTATE=true yarn --cwd=./site dev & + CODER_HOST=http://127.0.0.1:3000 INSPECT_XSTATE=true yarn --cwd=./site dev & go run -tags embed cmd/coder/main.go server --in-memory --tunnel & # Just a minor sleep to ensure the first user was created to make the member. - sleep 2 + sleep 5 # create the first user, the admin go run cmd/coder/main.go login http://127.0.0.1:3000 --username=admin --email=admin@coder.com --password=password || true # || yes to always exit code 0. If this fails, whelp. - go run cmd/coder/main.go users create --email=member@coder.com --username=member --password="${CODER_DEV_ADMIN_PASSWORD}" || true + go run cmd/coder/main.go users create --email=member@coder.com --username=member --password=password || true wait ) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index 3fd40d3c81cb4..c7a43fa7c61e8 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -1,5 +1,4 @@ import axios, { AxiosRequestHeaders } from "axios" -import ndjsonStream from "can-ndjson-stream" import * as Types from "./types" import { WorkspaceBuildTransition } from "./types" import * as TypesGen from "./typesGenerated" @@ -280,25 +279,11 @@ export const getWorkspaceBuildByNumber = async ( return response.data } -export const getWorkspaceBuildLogs = async (buildname: string): Promise => { - const response = await axios.get(`/api/v2/workspacebuilds/${buildname}/logs`) +export const getWorkspaceBuildLogs = async (buildname: string, before: Date): Promise => { + const response = await axios.get(`/api/v2/workspacebuilds/${buildname}/logs?before=`+before.getTime()) return response.data } -export const streamWorkspaceBuildLogs = async ( - buildname: string, -): Promise> => { - // Axios does not support HTTP stream in the browser - // https://github.com/axios/axios/issues/1474 - // So we are going to use window.fetch and return a "stream" reader - const reader = await window - .fetch(`/api/v2/workspacebuilds/${buildname}/logs?follow=true`) - .then((res) => ndjsonStream(res.body)) - .then((stream) => stream.getReader()) - - return reader -} - export const putWorkspaceExtension = async ( workspaceId: string, extendWorkspaceRequest: TypesGen.PutExtendWorkspaceRequest, diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index 6d1d8b4e9777b..b035389395751 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx @@ -1,26 +1,31 @@ import { screen } from "@testing-library/react" -import * as API from "../../api/api" -import { MockWorkspace, MockWorkspaceBuild, renderWithAuth } from "../../testHelpers/renderHelpers" +import WS from "jest-websocket-mock" +import { + MockWorkspace, + MockWorkspaceBuild, + renderWithAuth, +} from "../../testHelpers/renderHelpers" import { WorkspaceBuildPage } from "./WorkspaceBuildPage" describe("WorkspaceBuildPage", () => { it("renders the stats and logs", async () => { - jest.spyOn(API, "streamWorkspaceBuildLogs").mockResolvedValueOnce({ - read() { - return Promise.resolve({ - value: undefined, - done: true, - }) - }, - releaseLock: jest.fn(), - closed: Promise.resolve(undefined), - cancel: jest.fn(), - }) + const server = new WS("ws://localhost/api/v2/workspacebuilds/" + MockWorkspaceBuild.id + "/logs") renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, path: "/@:username/:workspace/builds/:buildNumber", }) - + await server.connected + const log = { + id: "70459334-4878-4bda-a546-98eee166c4c6", + created_at: "2022-05-19T16:46:02.283Z", + log_source: "provisioner_daemon", + log_level: "info", + stage: "Another stage", + output: "", + } + server.send(JSON.stringify(log)) await screen.findByText(MockWorkspaceBuild.workspace_name) + await screen.findByText(log.stage) + server.close() }) }) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 9d570db458dfe..970aa3bff362f 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -8,7 +8,7 @@ import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView" export const WorkspaceBuildPage: FC = () => { const { username, workspace: workspaceName, buildNumber } = useParams() - const [buildState] = useMachine(workspaceBuildMachine, { context: { username, workspaceName, buildNumber } }) + const [buildState] = useMachine(workspaceBuildMachine, { context: { username, workspaceName, buildNumber, timeCursor: new Date() } }) const { logs, build } = buildState.context const isWaitingForLogs = !buildState.matches("logs.loaded") diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index 5dd1e0a47c813..0a6d194a62312 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -17,7 +17,7 @@ export interface WorkspaceBuildPageViewProps { isWaitingForLogs: boolean } -export const WorkspaceBuildPageView: FC = ({ logs, build, isWaitingForLogs }) => { +export const WorkspaceBuildPageView: FC = ({ logs, build }) => { return ( @@ -27,7 +27,7 @@ export const WorkspaceBuildPageView: FC = ({ logs, {build && } {!logs && } - {logs && } + {logs && } ) diff --git a/site/src/testHelpers/handlers.ts b/site/src/testHelpers/handlers.ts index 5144a45cc301c..142d685ea2923 100644 --- a/site/src/testHelpers/handlers.ts +++ b/site/src/testHelpers/handlers.ts @@ -124,4 +124,7 @@ export const handlers = [ rest.patch("/api/v2/workspacebuilds/:workspaceBuildId/cancel", (req, res, ctx) => { return res(ctx.status(200), ctx.json(M.MockCancellationMessage)) }), + rest.get("/api/v2/workspacebuilds/:workspaceBuildId/logs", (req, res, ctx) => { + return res(ctx.status(200), ctx.json(M.MockWorkspaceBuildLogs)) + }), ] diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts index 325ecf92424c3..59326ac7a6f55 100644 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts @@ -8,6 +8,8 @@ type LogsContext = { workspaceName: string buildNumber: string buildId: string + // Used to reference logs before + after. + timeCursor: Date build?: WorkspaceBuild getBuildError?: Error | unknown // Logs @@ -33,6 +35,9 @@ export const workspaceBuildMachine = createMachine( getWorkspaceBuild: { data: WorkspaceBuild } + getLogs: { + data: ProvisionerJobLog[] + } }, }, tsTypes: {} as import("./workspaceBuildXService.typegen").Typegen0, @@ -54,8 +59,18 @@ export const workspaceBuildMachine = createMachine( }, idle: {}, logs: { - initial: "watchingLogs", + initial: "gettingExistentLogs", states: { + gettingExistentLogs: { + invoke: { + id: "getLogs", + src: "getLogs", + onDone: { + actions: ["assignLogs"], + target: "watchingLogs", + }, + }, + }, watchingLogs: { id: "watchingLogs", invoke: { @@ -94,6 +109,10 @@ export const workspaceBuildMachine = createMachine( clearGetBuildError: assign({ getBuildError: (_) => undefined, }), + // Logs + assignLogs: assign({ + logs: (_, event) => event.data, + }), addLog: assign({ logs: (context, event) => { const previousLogs = context.logs ?? [] @@ -103,21 +122,30 @@ export const workspaceBuildMachine = createMachine( }, services: { getWorkspaceBuild: (ctx) => API.getWorkspaceBuildByNumber(ctx.username, ctx.workspaceName, ctx.buildNumber), + getLogs: async (ctx) => API.getWorkspaceBuildLogs(ctx.buildId, ctx.timeCursor), streamWorkspaceBuildLogs: (ctx) => async (callback) => { - const reader = await API.streamWorkspaceBuildLogs(ctx.buildId) - - // Watching for the stream - // eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition - while (true) { - const { value, done } = await reader.read() - - if (done) { + return new Promise((resolve, reject) => { + const proto = location.protocol === "https:" ? "wss:" : "ws:" + const socket = new WebSocket( + `${proto}//${location.host}/api/v2/workspacebuilds/${ctx.buildId}/logs?follow=true&after=` + + ctx.timeCursor.getTime(), + ) + socket.binaryType = "blob" + socket.addEventListener("message", (event) => { + callback({ type: "ADD_LOG", log: JSON.parse(event.data) }) + }) + socket.addEventListener("error", () => { + reject(new Error("socket errored")) + }) + socket.addEventListener("open", () => { + resolve() + }) + socket.addEventListener("close", () => { + // When the socket closes, logs have finished streaming! callback("NO_MORE_LOGS") - break - } - - callback({ type: "ADD_LOG", log: value }) - } + resolve() + }) + }) }, }, }, From 700b59129ea7ecd8036c33d6ee0bd9c3e02f2fcc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 22 Jun 2022 08:02:53 -0500 Subject: [PATCH 2/5] Update site/src/xServices/workspaceBuild/workspaceBuildXService.ts Co-authored-by: Abhineet Jain --- site/src/xServices/workspaceBuild/workspaceBuildXService.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts index 59326ac7a6f55..9c8a419360f88 100644 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts @@ -127,8 +127,7 @@ export const workspaceBuildMachine = createMachine( return new Promise((resolve, reject) => { const proto = location.protocol === "https:" ? "wss:" : "ws:" const socket = new WebSocket( - `${proto}//${location.host}/api/v2/workspacebuilds/${ctx.buildId}/logs?follow=true&after=` + - ctx.timeCursor.getTime(), + `${proto}//${location.host}/api/v2/workspacebuilds/${ctx.buildId}/logs?follow=true&after=${ctx.timeCursor.getTime()}`, ) socket.binaryType = "blob" socket.addEventListener("message", (event) => { From f46d188b266e326f60cc4db7e8852b67fd324ed4 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 22 Jun 2022 08:03:06 -0500 Subject: [PATCH 3/5] Update site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx Co-authored-by: Abhineet Jain --- site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx index b035389395751..e56789ef1cf4f 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx @@ -9,7 +9,7 @@ import { WorkspaceBuildPage } from "./WorkspaceBuildPage" describe("WorkspaceBuildPage", () => { it("renders the stats and logs", async () => { - const server = new WS("ws://localhost/api/v2/workspacebuilds/" + MockWorkspaceBuild.id + "/logs") + const server = new WS(`ws://localhost/api/v2/workspacebuilds/${MockWorkspaceBuild.id}/logs`) renderWithAuth(, { route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}/builds/${MockWorkspace.latest_build.build_number}`, path: "/@:username/:workspace/builds/:buildNumber", From 23134e474ffaad9c0041f9ad75093dd5821dfa40 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 22 Jun 2022 08:03:12 -0500 Subject: [PATCH 4/5] Update site/src/api/api.ts Co-authored-by: Abhineet Jain --- site/src/api/api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/api/api.ts b/site/src/api/api.ts index c7a43fa7c61e8..58732f076f59e 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -280,7 +280,7 @@ export const getWorkspaceBuildByNumber = async ( } export const getWorkspaceBuildLogs = async (buildname: string, before: Date): Promise => { - const response = await axios.get(`/api/v2/workspacebuilds/${buildname}/logs?before=`+before.getTime()) + const response = await axios.get(`/api/v2/workspacebuilds/${buildname}/logs?before=${before.getTime()}`) return response.data } From 58e060f91e885c35559940832116a4f9885c377b Mon Sep 17 00:00:00 2001 From: kylecarbs Date: Wed, 22 Jun 2022 13:06:24 +0000 Subject: [PATCH 5/5] Remove unused prop --- .../WorkspaceBuildLogs.stories.tsx | 6 ------ .../WorkspaceBuildLogs/WorkspaceBuildLogs.tsx | 15 +++------------ .../WorkspaceBuildPage/WorkspaceBuildPage.tsx | 7 ++++--- .../WorkspaceBuildPage/WorkspaceBuildPageView.tsx | 3 +-- .../workspaceBuild/workspaceBuildXService.ts | 7 ------- 5 files changed, 8 insertions(+), 30 deletions(-) diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx index 05d5bcfe8da30..71886186d2804 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx @@ -13,9 +13,3 @@ export const Example = Template.bind({}) Example.args = { logs: MockWorkspaceBuildLogs, } - -export const Loading = Template.bind({}) -Loading.args = { - logs: MockWorkspaceBuildLogs, - isWaitingForLogs: true, -} diff --git a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx index b5e438c4b97c2..8f52d623ec525 100644 --- a/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx +++ b/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx @@ -1,4 +1,3 @@ -import CircularProgress from "@material-ui/core/CircularProgress" import { makeStyles } from "@material-ui/core/styles" import dayjs from "dayjs" import { FC } from "react" @@ -40,17 +39,16 @@ const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => { export interface WorkspaceBuildLogsProps { logs: ProvisionerJobLog[] - isWaitingForLogs: boolean } -export const WorkspaceBuildLogs: FC = ({ logs, isWaitingForLogs }) => { +export const WorkspaceBuildLogs: FC = ({ logs }) => { const groupedLogsByStage = groupLogsByStage(logs) const stages = Object.keys(groupedLogsByStage) const styles = useStyles() return (
- {stages.map((stage, stageIndex) => { + {stages.map((stage) => { const logs = groupedLogsByStage[stage] const isEmpty = logs.every((log) => log.output === "") const lines = logs.map((log) => ({ @@ -58,15 +56,12 @@ export const WorkspaceBuildLogs: FC = ({ logs, isWaitin output: log.output, })) const duration = getStageDurationInSeconds(logs) - const isLastStage = stageIndex === stages.length - 1 - const shouldDisplaySpinner = isWaitingForLogs && isLastStage - const shouldDisplayDuration = !isWaitingForLogs && duration + const shouldDisplayDuration = duration !== undefined return (
{stage}
- {shouldDisplaySpinner && } {shouldDisplayDuration && (
{duration} {Language.seconds} @@ -109,8 +104,4 @@ const useStyles = makeStyles((theme) => ({ padding: theme.spacing(2), paddingLeft: theme.spacing(4), }, - - spinner: { - marginLeft: "auto", - }, })) diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 970aa3bff362f..fdb39e06d21ea 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -8,9 +8,10 @@ import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView" export const WorkspaceBuildPage: FC = () => { const { username, workspace: workspaceName, buildNumber } = useParams() - const [buildState] = useMachine(workspaceBuildMachine, { context: { username, workspaceName, buildNumber, timeCursor: new Date() } }) + const [buildState] = useMachine(workspaceBuildMachine, { + context: { username, workspaceName, buildNumber, timeCursor: new Date() }, + }) const { logs, build } = buildState.context - const isWaitingForLogs = !buildState.matches("logs.loaded") return ( <> @@ -18,7 +19,7 @@ export const WorkspaceBuildPage: FC = () => { {build ? pageTitle(`Build #${build.build_number} ยท ${build.workspace_name}`) : ""} - + ) } diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx index 0a6d194a62312..2734a9d82a75a 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPageView.tsx @@ -14,7 +14,6 @@ const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => { export interface WorkspaceBuildPageViewProps { logs: ProvisionerJobLog[] | undefined build: WorkspaceBuild | undefined - isWaitingForLogs: boolean } export const WorkspaceBuildPageView: FC = ({ logs, build }) => { @@ -27,7 +26,7 @@ export const WorkspaceBuildPageView: FC = ({ logs, {build && } {!logs && } - {logs && } + {logs && } ) diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts index 9c8a419360f88..c6078a72791f0 100644 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts @@ -21,9 +21,6 @@ type LogsEvent = type: "ADD_LOG" log: ProvisionerJobLog } - | { - type: "NO_MORE_LOGS" - } export const workspaceBuildMachine = createMachine( { @@ -86,9 +83,6 @@ export const workspaceBuildMachine = createMachine( ADD_LOG: { actions: "addLog", }, - NO_MORE_LOGS: { - target: "logs.loaded", - }, }, }, }, @@ -141,7 +135,6 @@ export const workspaceBuildMachine = createMachine( }) socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! - callback("NO_MORE_LOGS") resolve() }) })