Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit88e8c96

Browse files
feature: Load workspace build logs from streaming (#1997)
1 parentd6e9eab commit88e8c96

File tree

11 files changed

+133
-24
lines changed

11 files changed

+133
-24
lines changed

‎.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"VMID",
7373
"weblinks",
7474
"webrtc",
75+
"workspacebuilds",
7576
"xerrors",
7677
"xstate",
7778
"yamux"

‎site/can-ndjson-stream.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
declare module"can-ndjson-stream"{
2+
functionndjsonStream<TValueType>(body:ReadableStream<Uint8Array>|null):Promise<ReadableStream<TValueType>>
3+
exportdefaultndjsonStream
4+
}

‎site/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@xstate/inspect":"0.6.5",
3636
"@xstate/react":"3.0.0",
3737
"axios":"0.26.1",
38+
"can-ndjson-stream":"1.0.2",
3839
"cronstrue":"2.5.0",
3940
"dayjs":"1.11.2",
4041
"formik":"2.2.9",

‎site/src/api/api.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
importaxios,{AxiosRequestHeaders}from"axios"
2+
importndjsonStreamfrom"can-ndjson-stream"
23
import*asTypesfrom"./types"
34
import{WorkspaceBuildTransition}from"./types"
45
import*asTypesGenfrom"./typesGenerated"
@@ -271,6 +272,20 @@ export const getWorkspaceBuildLogs = async (buildname: string): Promise<TypesGen
271272
returnresponse.data
272273
}
273274

275+
exportconststreamWorkspaceBuildLogs=async(
276+
buildname:string,
277+
):Promise<ReadableStreamDefaultReader<TypesGen.ProvisionerJobLog>>=>{
278+
// Axios does not support HTTP stream in the browser
279+
// https://github.com/axios/axios/issues/1474
280+
// So we are going to use window.fetch and return a "stream" reader
281+
constreader=awaitwindow
282+
.fetch(`/api/v2/workspacebuilds/${buildname}/logs?follow=true`)
283+
.then((res)=>ndjsonStream<TypesGen.ProvisionerJobLog>(res.body))
284+
.then((stream)=>stream.getReader())
285+
286+
returnreader
287+
}
288+
274289
exportconstputWorkspaceExtension=async(
275290
workspaceId:string,
276291
extendWorkspaceRequest:TypesGen.PutExtendWorkspaceRequest,

‎site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.stories.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,9 @@ export const Example = Template.bind({})
1313
Example.args={
1414
logs:MockWorkspaceBuildLogs,
1515
}
16+
17+
exportconstLoading=Template.bind({})
18+
Loading.args={
19+
logs:MockWorkspaceBuildLogs,
20+
isWaitingForLogs:true,
21+
}

‎site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
importCircularProgressfrom"@material-ui/core/CircularProgress"
12
import{makeStyles}from"@material-ui/core/styles"
23
importdayjsfrom"dayjs"
34
import{FC}from"react"
45
import{ProvisionerJobLog}from"../../api/typesGenerated"
56
import{MONOSPACE_FONT_FAMILY}from"../../theme/constants"
67
import{Logs}from"../Logs/Logs"
78

9+
constLanguage={
10+
seconds:"seconds",
11+
}
12+
813
typeStage=ProvisionerJobLog["stage"]
914

1015
constgroupLogsByStage=(logs:ProvisionerJobLog[])=>{
@@ -35,29 +40,38 @@ const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => {
3540

3641
exportinterfaceWorkspaceBuildLogsProps{
3742
logs:ProvisionerJobLog[]
43+
isWaitingForLogs:boolean
3844
}
3945

40-
exportconstWorkspaceBuildLogs:FC<WorkspaceBuildLogsProps>=({ logs})=>{
46+
exportconstWorkspaceBuildLogs:FC<WorkspaceBuildLogsProps>=({ logs, isWaitingForLogs})=>{
4147
constgroupedLogsByStage=groupLogsByStage(logs)
4248
conststages=Object.keys(groupedLogsByStage)
4349
conststyles=useStyles()
4450

4551
return(
4652
<divclassName={styles.logs}>
47-
{stages.map((stage)=>{
53+
{stages.map((stage,stageIndex)=>{
4854
constlogs=groupedLogsByStage[stage]
4955
constisEmpty=logs.every((log)=>log.output==="")
5056
constlines=logs.map((log)=>({
5157
time:log.created_at,
5258
output:log.output,
5359
}))
5460
constduration=getStageDurationInSeconds(logs)
61+
constisLastStage=stageIndex===stages.length-1
62+
constshouldDisplaySpinner=isWaitingForLogs&&isLastStage
63+
constshouldDisplayDuration=!isWaitingForLogs&&duration
5564

5665
return(
5766
<divkey={stage}>
5867
<divclassName={styles.header}>
5968
<div>{stage}</div>
60-
{duration&&<divclassName={styles.duration}>{duration} seconds</div>}
69+
{shouldDisplaySpinner&&<CircularProgresssize={14}className={styles.spinner}/>}
70+
{shouldDisplayDuration&&(
71+
<divclassName={styles.duration}>
72+
{duration}{Language.seconds}
73+
</div>
74+
)}
6175
</div>
6276
{!isEmpty&&<Logslines={lines}className={styles.codeBlock}/>}
6377
</div>
@@ -78,6 +92,7 @@ const useStyles = makeStyles((theme) => ({
7892
fontSize:theme.typography.body1.fontSize,
7993
padding:theme.spacing(2),
8094
paddingLeft:theme.spacing(4),
95+
paddingRight:theme.spacing(4),
8196
borderBottom:`1px solid${theme.palette.divider}`,
8297
backgroundColor:theme.palette.background.paper,
8398
display:"flex",
@@ -94,4 +109,8 @@ const useStyles = makeStyles((theme) => ({
94109
padding:theme.spacing(2),
95110
paddingLeft:theme.spacing(4),
96111
},
112+
113+
spinner:{
114+
marginLeft:"auto",
115+
},
97116
}))

‎site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
11
import{screen}from"@testing-library/react"
2+
import*asAPIfrom"../../api/api"
23
import{MockWorkspaceBuild,MockWorkspaceBuildLogs,renderWithAuth}from"../../testHelpers/renderHelpers"
34
import{WorkspaceBuildPage}from"./WorkspaceBuildPage"
45

56
describe("WorkspaceBuildPage",()=>{
67
it("renders the stats and logs",async()=>{
8+
jest.spyOn(API,"streamWorkspaceBuildLogs").mockResolvedValueOnce({
9+
read(){
10+
returnPromise.resolve({
11+
value:undefined,
12+
done:true,
13+
})
14+
},
15+
releaseLock:jest.fn(),
16+
closed:Promise.resolve(undefined),
17+
cancel:jest.fn(),
18+
})
719
renderWithAuth(<WorkspaceBuildPage/>,{route:`/builds/${MockWorkspaceBuild.id}`,path:"/builds/:buildId"})
820

921
awaitscreen.findByText(MockWorkspaceBuild.workspace_name)

‎site/src/pages/WorkspaceBuildPage/WorkspaceBuildPage.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export const WorkspaceBuildPage: FC = () => {
2929
constbuildId=useBuildId()
3030
const[buildState]=useMachine(workspaceBuildMachine,{context:{ buildId}})
3131
const{ logs, build}=buildState.context
32+
constisWaitingForLogs=!buildState.matches("logs.loaded")
3233
conststyles=useStyles()
3334

3435
return(
@@ -40,7 +41,7 @@ export const WorkspaceBuildPage: FC = () => {
4041

4142
{build&&<WorkspaceBuildStatsbuild={build}/>}
4243
{!logs&&<Loader/>}
43-
{logs&&<WorkspaceBuildLogslogs={sortLogsByCreatedAt(logs)}/>}
44+
{logs&&<WorkspaceBuildLogslogs={sortLogsByCreatedAt(logs)}isWaitingForLogs={isWaitingForLogs}/>}
4445
</Stack>
4546
</Margins>
4647
)

‎site/src/xServices/workspaceBuild/workspaceBuildXService.ts

Lines changed: 57 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,28 @@ type LogsContext = {
99
getBuildError?:Error|unknown
1010
// Logs
1111
logs?:ProvisionerJobLog[]
12-
getBuildLogsError?:Error|unknown
1312
}
1413

14+
typeLogsEvent=
15+
|{
16+
type:"ADD_LOG"
17+
log:ProvisionerJobLog
18+
}
19+
|{
20+
type:"NO_MORE_LOGS"
21+
}
22+
1523
exportconstworkspaceBuildMachine=createMachine(
1624
{
1725
id:"workspaceBuildState",
1826
schema:{
1927
context:{}asLogsContext,
28+
events:{}asLogsEvent,
2029
services:{}as{
2130
getWorkspaceBuild:{
2231
data:WorkspaceBuild
2332
}
24-
getWorkspaceBuildLogs:{
33+
getLogs:{
2534
data:ProvisionerJobLog[]
2635
}
2736
},
@@ -50,23 +59,36 @@ export const workspaceBuildMachine = createMachine(
5059
},
5160
},
5261
logs:{
53-
initial:"gettingLogs",
62+
initial:"gettingExistentLogs",
5463
states:{
55-
gettingLogs:{
56-
entry:"clearGetBuildLogsError",
64+
gettingExistentLogs:{
5765
invoke:{
58-
src:"getWorkspaceBuildLogs",
66+
id:"getLogs",
67+
src:"getLogs",
5968
onDone:{
60-
target:"idle",
61-
actions:"assignLogs",
62-
},
63-
onError:{
64-
target:"idle",
65-
actions:"assignGetBuildLogsError",
69+
actions:["assignLogs"],
70+
target:"watchingLogs",
6671
},
6772
},
6873
},
69-
idle:{},
74+
watchingLogs:{
75+
id:"watchingLogs",
76+
invoke:{
77+
id:"streamWorkspaceBuildLogs",
78+
src:"streamWorkspaceBuildLogs",
79+
},
80+
},
81+
loaded:{
82+
type:"final",
83+
},
84+
},
85+
on:{
86+
ADD_LOG:{
87+
actions:"addLog",
88+
},
89+
NO_MORE_LOGS:{
90+
target:"logs.loaded",
91+
},
7092
},
7193
},
7294
},
@@ -87,16 +109,32 @@ export const workspaceBuildMachine = createMachine(
87109
assignLogs:assign({
88110
logs:(_,event)=>event.data,
89111
}),
90-
assignGetBuildLogsError:assign({
91-
getBuildLogsError:(_,event)=>event.data,
92-
}),
93-
clearGetBuildLogsError:assign({
94-
getBuildLogsError:(_)=>undefined,
112+
addLog:assign({
113+
logs:(context,event)=>{
114+
constpreviousLogs=context.logs??[]
115+
return[...previousLogs,event.log]
116+
},
95117
}),
96118
},
97119
services:{
98120
getWorkspaceBuild:(ctx)=>API.getWorkspaceBuild(ctx.buildId),
99-
getWorkspaceBuildLogs:(ctx)=>API.getWorkspaceBuildLogs(ctx.buildId),
121+
getLogs:async(ctx)=>API.getWorkspaceBuildLogs(ctx.buildId),
122+
streamWorkspaceBuildLogs:(ctx)=>async(callback)=>{
123+
constreader=awaitAPI.streamWorkspaceBuildLogs(ctx.buildId)
124+
125+
// Watching for the stream
126+
// eslint-disable-next-line no-constant-condition, @typescript-eslint/no-unnecessary-condition
127+
while(true){
128+
const{ value, done}=awaitreader.read()
129+
130+
if(done){
131+
callback("NO_MORE_LOGS")
132+
break
133+
}
134+
135+
callback({type:"ADD_LOG",log:value})
136+
}
137+
},
100138
},
101139
},
102140
)

‎site/tsconfig.test.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
22
"extends":"./tsconfig.json",
33
"exclude": ["node_modules","_jest"],
4-
"include": ["**/*.stories.tsx","**/*.test.tsx"]
4+
"include": ["**/*.stories.tsx","**/*.test.tsx","**/*.d.ts"]
55
}

‎site/yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4731,6 +4731,18 @@ camelcase@^6.2.0:
47314731
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a"
47324732
integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==
47334733

4734+
can-namespace@^1.0.0:
4735+
version "1.0.0"
4736+
resolved "https://registry.yarnpkg.com/can-namespace/-/can-namespace-1.0.0.tgz#0b8fafafbb11352b9ead4222ffe3822405b43e99"
4737+
integrity sha512-1sBY/SLwwcmxz3NhyVhLjt2uD/dZ7V1mII82/MIXSDn5QXnslnosJnjlP8+yTx2uTCRvw1jlFDElRs4pX7AG5w==
4738+
4739+
can-ndjson-stream@1.0.2:
4740+
version "1.0.2"
4741+
resolved "https://registry.yarnpkg.com/can-ndjson-stream/-/can-ndjson-stream-1.0.2.tgz#6a8131f9c8c697215163b3fe49a0c02e4439cb47"
4742+
integrity sha512-//tM8wcTV42SyD1JGua7WMVftZEeTwapcHJTTe3vJwuVywXD01CJbdEkgwRYjy2evIByVJV21ZKBdSv5ygIw1w==
4743+
dependencies:
4744+
can-namespace "^1.0.0"
4745+
47344746
caniuse-api@^3.0.0:
47354747
version "3.0.0"
47364748
resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0"

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp