diff --git a/site/src/api/api.ts b/site/src/api/api.ts index ef50daad9508a..7c1e397222d03 100644 --- a/site/src/api/api.ts +++ b/site/src/api/api.ts @@ -792,11 +792,11 @@ export const getWorkspaceBuildByNumber = async ( }; export const getWorkspaceBuildLogs = async ( - buildname: string, + buildId: string, before: Date, ): Promise => { const response = await axios.get( - `/api/v2/workspacebuilds/${buildname}/logs?before=${before.getTime()}`, + `/api/v2/workspacebuilds/${buildId}/logs?before=${before.getTime()}`, ); return response.data; }; @@ -1432,8 +1432,8 @@ export const watchWorkspaceAgentLogs = ( type WatchBuildLogsByBuildIdOptions = { after?: number; onMessage: (log: TypesGen.ProvisionerJobLog) => void; - onDone: () => void; - onError: (error: Error) => void; + onDone?: () => void; + onError?: (error: Error) => void; }; export const watchBuildLogsByBuildId = ( buildId: string, @@ -1454,12 +1454,12 @@ export const watchBuildLogsByBuildId = ( onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog), ); socket.addEventListener("error", () => { - onError(new Error("Connection for logs failed.")); + onError && onError(new Error("Connection for logs failed.")); socket.close(); }); socket.addEventListener("close", () => { // When the socket closes, logs have finished streaming! - onDone(); + onDone && onDone(); }); return socket; }; diff --git a/site/src/api/queries/workspaceBuilds.ts b/site/src/api/queries/workspaceBuilds.ts new file mode 100644 index 0000000000000..207e7374f39f5 --- /dev/null +++ b/site/src/api/queries/workspaceBuilds.ts @@ -0,0 +1,13 @@ +import * as API from "api/api"; + +export const workspaceBuildByNumber = ( + username: string, + workspaceName: string, + buildNumber: number, +) => { + return { + queryKey: [username, workspaceName, "workspaceBuild", buildNumber], + queryFn: () => + API.getWorkspaceBuildByNumber(username, workspaceName, buildNumber), + }; +}; diff --git a/site/src/hooks/useWorkspaceBuildLogs.ts b/site/src/hooks/useWorkspaceBuildLogs.ts new file mode 100644 index 0000000000000..71b76099f5291 --- /dev/null +++ b/site/src/hooks/useWorkspaceBuildLogs.ts @@ -0,0 +1,39 @@ +import { watchBuildLogsByBuildId } from "api/api"; +import { ProvisionerJobLog } from "api/typesGenerated"; +import { displayError } from "components/GlobalSnackbar/utils"; +import { useState, useEffect } from "react"; + +// buildId is optional because sometimes the build is not loaded yet +export const useWorkspaceBuildLogs = (buildId?: string) => { + const [logs, setLogs] = useState(); + useEffect(() => { + if (!buildId) { + return; + } + + // Every time this hook is called reset the values + setLogs(undefined); + + const socket = watchBuildLogsByBuildId(buildId, { + // Retrieve all the logs + after: -1, + onMessage: (log) => { + setLogs((previousLogs) => { + if (!previousLogs) { + return [log]; + } + return [...previousLogs, log]; + }); + }, + onError: () => { + displayError("Error on getting the build logs"); + }, + }); + + return () => { + socket.close(); + }; + }, [buildId]); + + return logs; +}; diff --git a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx index 7e06018c47cf2..d4056f2e76459 100644 --- a/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx +++ b/site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx @@ -1,13 +1,13 @@ -import { useMachine } from "@xstate/react"; -import { FC, useEffect } from "react"; +import { FC } from "react"; import { Helmet } from "react-helmet-async"; import { useParams } from "react-router-dom"; -import { pageTitle } from "../../utils/page"; -import { workspaceBuildMachine } from "../../xServices/workspaceBuild/workspaceBuildXService"; +import { pageTitle } from "utils/page"; import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView"; import { useQuery } from "@tanstack/react-query"; import { getWorkspaceBuilds } from "api/api"; import dayjs from "dayjs"; +import { workspaceBuildByNumber } from "api/queries/workspaceBuilds"; +import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; export const WorkspaceBuildPage: FC = () => { const params = useParams() as { @@ -18,10 +18,11 @@ export const WorkspaceBuildPage: FC = () => { const workspaceName = params.workspace; const buildNumber = Number(params.buildNumber); const username = params.username.replace("@", ""); - const [buildState, send] = useMachine(workspaceBuildMachine, { - context: { username, workspaceName, buildNumber, timeCursor: new Date() }, + const wsBuildQuery = useQuery({ + ...workspaceBuildByNumber(username, workspaceName, buildNumber), + keepPreviousData: true, }); - const { logs, build } = buildState.context; + const build = wsBuildQuery.data; const { data: builds } = useQuery({ queryKey: ["builds", username, build?.workspace_id], queryFn: () => { @@ -32,10 +33,7 @@ export const WorkspaceBuildPage: FC = () => { }, enabled: Boolean(build), }); - - useEffect(() => { - send("RESET", { buildNumber, timeCursor: new Date() }); - }, [buildNumber, send]); + const logs = useWorkspaceBuildLogs(build?.id); return ( <> diff --git a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx index 24ce38f0804f3..1316a743c4c52 100644 --- a/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceReadyPage.tsx @@ -1,4 +1,4 @@ -import { useActor, useMachine } from "@xstate/react"; +import { useActor } from "@xstate/react"; import { useDashboard } from "components/Dashboard/DashboardProvider"; import dayjs from "dayjs"; import { useFeatureVisibility } from "hooks/useFeatureVisibility"; @@ -28,12 +28,12 @@ import { ConfirmDialog, ConfirmDialogProps, } from "components/Dialogs/ConfirmDialog/ConfirmDialog"; -import { workspaceBuildMachine } from "xServices/workspaceBuild/workspaceBuildXService"; import * as TypesGen from "api/typesGenerated"; import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection"; import { templateVersion, templateVersions } from "api/queries/templates"; import { Alert } from "components/Alert/Alert"; import { Stack } from "components/Stack/Stack"; +import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; interface WorkspaceReadyPageProps { workspaceState: StateFrom; @@ -91,7 +91,7 @@ export const WorkspaceReadyPage = ({ enabled: workspace.outdated, }); - const buildLogs = useBuildLogs(workspace); + const buildLogs = useWorkspaceBuildLogs(workspace.latest_build.id); const shouldDisplayBuildLogs = hasJobError(workspace) || ["canceling", "deleting", "pending", "starting", "stopping"].includes( @@ -285,22 +285,3 @@ const WarningDialog: FC< > = (props) => { return ; }; - -const useBuildLogs = (workspace: TypesGen.Workspace) => { - const buildNumber = workspace.latest_build.build_number; - const [buildState, buildSend] = useMachine(workspaceBuildMachine, { - context: { - buildNumber, - username: workspace.owner_name, - workspaceName: workspace.name, - timeCursor: new Date(), - }, - }); - const { logs } = buildState.context; - - useEffect(() => { - buildSend({ type: "RESET", buildNumber, timeCursor: new Date() }); - }, [buildNumber, buildSend]); - - return logs; -}; diff --git a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts b/site/src/xServices/workspaceBuild/workspaceBuildXService.ts deleted file mode 100644 index 65e750d426637..0000000000000 --- a/site/src/xServices/workspaceBuild/workspaceBuildXService.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { assign, createMachine } from "xstate"; -import * as API from "../../api/api"; -import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated"; - -type LogsContext = { - // Build - username: string; - workspaceName: string; - buildNumber: number; - buildId: string; - // Used to reference logs before + after. - timeCursor: Date; - build?: WorkspaceBuild; - getBuildError?: unknown; - // Logs - logs?: ProvisionerJobLog[]; -}; - -type LogsEvent = - | { - type: "ADD_LOG"; - log: ProvisionerJobLog; - } - | { - type: "BUILD_DONE"; - } - | { - type: "RESET"; - buildNumber: number; - timeCursor: Date; - }; - -export const workspaceBuildMachine = createMachine( - { - id: "workspaceBuildState", - predictableActionArguments: true, - tsTypes: {} as import("./workspaceBuildXService.typegen").Typegen0, - schema: { - context: {} as LogsContext, - events: {} as LogsEvent, - services: {} as { - getWorkspaceBuild: { - data: WorkspaceBuild; - }; - getLogs: { - data: ProvisionerJobLog[]; - }; - }, - }, - initial: "gettingBuild", - on: { - RESET: { - target: "gettingBuild", - actions: ["resetContext"], - }, - }, - states: { - gettingBuild: { - entry: "clearGetBuildError", - invoke: { - src: "getWorkspaceBuild", - onDone: { - target: "logs", - actions: ["assignBuild", "assignBuildId"], - }, - onError: { - target: "idle", - actions: "assignGetBuildError", - }, - }, - }, - idle: {}, - logs: { - initial: "gettingExistentLogs", - states: { - gettingExistentLogs: { - invoke: { - id: "getLogs", - src: "getLogs", - onDone: { - actions: ["assignLogs"], - target: "watchingLogs", - }, - }, - }, - watchingLogs: { - id: "watchingLogs", - invoke: { - id: "streamWorkspaceBuildLogs", - src: "streamWorkspaceBuildLogs", - }, - on: { - ADD_LOG: { - actions: "addLog", - }, - BUILD_DONE: { - target: "loaded", - }, - }, - }, - loaded: { - type: "final", - }, - }, - }, - }, - }, - { - actions: { - resetContext: assign({ - buildNumber: (_, event) => event.buildNumber, - timeCursor: (_, event) => event.timeCursor, - logs: undefined, - }), - // Build ID - assignBuildId: assign({ - buildId: (_, event) => event.data.id, - }), - // Build - assignBuild: assign({ - build: (_, event) => event.data, - }), - assignGetBuildError: assign({ - getBuildError: (_, event) => event.data, - }), - clearGetBuildError: assign({ - getBuildError: (_) => undefined, - }), - // Logs - assignLogs: assign({ - logs: (_, event) => event.data, - }), - addLog: assign({ - logs: (context, event) => { - const previousLogs = context.logs ?? []; - return [...previousLogs, event.log]; - }, - }), - }, - services: { - getWorkspaceBuild: (ctx) => - API.getWorkspaceBuildByNumber( - ctx.username, - ctx.workspaceName, - ctx.buildNumber, - ), - getLogs: async (ctx) => - API.getWorkspaceBuildLogs(ctx.buildId, ctx.timeCursor), - streamWorkspaceBuildLogs: (ctx) => async (callback) => { - if (!ctx.logs) { - throw new Error("logs must be set"); - } - const after = - ctx.logs.length > 0 ? ctx.logs[ctx.logs.length - 1].id : undefined; - const socket = API.watchBuildLogsByBuildId(ctx.buildId, { - after, - onMessage: (log) => { - callback({ type: "ADD_LOG", log }); - }, - onDone: () => { - callback({ type: "BUILD_DONE" }); - }, - onError: (err) => { - console.error(err); - }, - }); - return () => { - socket.close(); - }; - }, - }, - }, -);