Skip to content

Commit ea7c2e7

Browse files
BrunoQuaresmakylecarbs
authored andcommitted
feat: Add workspace build logs page (#1598)
1 parent f7c19fd commit ea7c2e7

File tree

18 files changed

+839
-35
lines changed

18 files changed

+839
-35
lines changed

.vscode/settings.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"buildname",
34
"circbuf",
45
"cliflag",
56
"cliui",
@@ -55,6 +56,7 @@
5556
"TCGETS",
5657
"tcpip",
5758
"TCSETS",
59+
"testid",
5860
"tfexec",
5961
"tfjson",
6062
"tfstate",
@@ -76,7 +78,7 @@
7678
},
7779
{
7880
"match": "provisionerd/proto/provisionerd.proto",
79-
"cmd": "make provisionerd/proto/provisionerd.pb.go",
81+
"cmd": "make provisionerd/proto/provisionerd.pb.go"
8082
}
8183
]
8284
},
@@ -104,5 +106,5 @@
104106
},
105107
// We often use a version of TypeScript that's ahead of the version shipped
106108
// with VS Code.
107-
"typescript.tsdk": "./site/node_modules/typescript/lib",
109+
"typescript.tsdk": "./site/node_modules/typescript/lib"
108110
}

site/src/AppRouter.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
1515
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
1616
import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage"
1717
import { UsersPage } from "./pages/UsersPage/UsersPage"
18+
import { WorkspaceBuildPage } from "./pages/WorkspaceBuildPage/WorkspaceBuildPage"
1819
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
1920
import { WorkspaceSettingsPage } from "./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"
2021

@@ -138,6 +139,15 @@ export const AppRouter: React.FC = () => (
138139
</Route>
139140
</Route>
140141

142+
<Route
143+
path="builds/:buildId"
144+
element={
145+
<AuthAndFrame>
146+
<WorkspaceBuildPage />
147+
</AuthAndFrame>
148+
}
149+
/>
150+
141151
{/* Using path="*"" means "match anything", so this route
142152
acts like a catch-all for URLs that we don't have explicit
143153
routes for. */}

site/src/api/api.ts

+10
Original file line numberDiff line numberDiff line change
@@ -248,3 +248,13 @@ export const getWorkspaceBuilds = async (workspaceId: string): Promise<TypesGen.
248248
const response = await axios.get<TypesGen.WorkspaceBuild[]>(`/api/v2/workspaces/${workspaceId}/builds`)
249249
return response.data
250250
}
251+
252+
export const getWorkspaceBuild = async (workspaceId: string): Promise<TypesGen.WorkspaceBuild> => {
253+
const response = await axios.get<TypesGen.WorkspaceBuild>(`/api/v2/workspacebuilds/${workspaceId}`)
254+
return response.data
255+
}
256+
257+
export const getWorkspaceBuildLogs = async (buildname: string): Promise<TypesGen.ProvisionerJobLog[]> => {
258+
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(`/api/v2/workspacebuilds/${buildname}/logs`)
259+
return response.data
260+
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
import Box from "@material-ui/core/Box"
2-
import { Theme } from "@material-ui/core/styles"
2+
import { makeStyles, Theme } from "@material-ui/core/styles"
33
import Table from "@material-ui/core/Table"
44
import TableBody from "@material-ui/core/TableBody"
55
import TableCell from "@material-ui/core/TableCell"
66
import TableHead from "@material-ui/core/TableHead"
77
import TableRow from "@material-ui/core/TableRow"
88
import useTheme from "@material-ui/styles/useTheme"
9-
import dayjs from "dayjs"
10-
import duration from "dayjs/plugin/duration"
11-
import relativeTime from "dayjs/plugin/relativeTime"
129
import React from "react"
10+
import { useNavigate } from "react-router-dom"
1311
import * as TypesGen from "../../api/typesGenerated"
14-
import { getDisplayStatus } from "../../util/workspace"
12+
import { displayWorkspaceBuildDuration, getDisplayStatus } from "../../util/workspace"
1513
import { EmptyState } from "../EmptyState/EmptyState"
1614
import { TableLoader } from "../TableLoader/TableLoader"
1715

18-
dayjs.extend(relativeTime)
19-
dayjs.extend(duration)
20-
2116
export const Language = {
2217
emptyMessage: "No builds found",
2318
inProgressLabel: "In progress",
@@ -27,19 +22,6 @@ export const Language = {
2722
statusLabel: "Status",
2823
}
2924

30-
const getDurationInSeconds = (build: TypesGen.WorkspaceBuild) => {
31-
let display = Language.inProgressLabel
32-
33-
if (build.job.started_at && build.job.completed_at) {
34-
const startedAt = dayjs(build.job.started_at)
35-
const completedAt = dayjs(build.job.completed_at)
36-
const diff = completedAt.diff(startedAt, "seconds")
37-
display = `${diff} seconds`
38-
}
39-
40-
return display
41-
}
42-
4325
export interface BuildsTableProps {
4426
builds?: TypesGen.WorkspaceBuild[]
4527
className?: string
@@ -48,6 +30,8 @@ export interface BuildsTableProps {
4830
export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) => {
4931
const isLoading = !builds
5032
const theme: Theme = useTheme()
33+
const navigate = useNavigate()
34+
const styles = useStyles()
5135

5236
return (
5337
<Table className={className}>
@@ -62,18 +46,35 @@ export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) =
6246
<TableBody>
6347
{isLoading && <TableLoader />}
6448
{builds &&
65-
builds.map((b) => {
66-
const status = getDisplayStatus(theme, b)
67-
const duration = getDurationInSeconds(b)
49+
builds.map((build) => {
50+
const status = getDisplayStatus(theme, build)
51+
52+
const navigateToBuildPage = () => {
53+
navigate(`/builds/${build.id}`)
54+
}
6855

6956
return (
70-
<TableRow key={b.id} data-testid={`build-${b.id}`}>
71-
<TableCell>{b.transition}</TableCell>
57+
<TableRow
58+
hover
59+
key={build.id}
60+
data-testid={`build-${build.id}`}
61+
tabIndex={0}
62+
onClick={navigateToBuildPage}
63+
onKeyDown={(event) => {
64+
if (event.key === "Enter") {
65+
navigateToBuildPage()
66+
}
67+
}}
68+
className={styles.clickableTableRow}
69+
>
70+
<TableCell>{build.transition}</TableCell>
7271
<TableCell>
73-
<span style={{ color: theme.palette.text.secondary }}>{duration}</span>
72+
<span style={{ color: theme.palette.text.secondary }}>{displayWorkspaceBuildDuration(build)}</span>
7473
</TableCell>
7574
<TableCell>
76-
<span style={{ color: theme.palette.text.secondary }}>{new Date(b.created_at).toLocaleString()}</span>
75+
<span style={{ color: theme.palette.text.secondary }}>
76+
{new Date(build.created_at).toLocaleString()}
77+
</span>
7778
</TableCell>
7879
<TableCell>
7980
<span style={{ color: status.color }}>{status.status}</span>
@@ -95,3 +96,17 @@ export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) =
9596
</Table>
9697
)
9798
}
99+
100+
const useStyles = makeStyles((theme) => ({
101+
clickableTableRow: {
102+
cursor: "pointer",
103+
104+
"&:hover td": {
105+
backgroundColor: theme.palette.background.default,
106+
},
107+
108+
"&:focus": {
109+
outline: `1px solid ${theme.palette.primary.dark}`,
110+
},
111+
},
112+
}))

site/src/components/Loader/Loader.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Box from "@material-ui/core/Box"
2+
import CircularProgress from "@material-ui/core/CircularProgress"
3+
import React from "react"
4+
5+
export const Loader: React.FC<{ size?: number }> = ({ size = 26 }) => {
6+
return (
7+
<Box p={4} width="100%" display="flex" alignItems="center" justifyContent="center">
8+
<CircularProgress size={size} />
9+
</Box>
10+
)
11+
}
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import React from "react"
3+
import { MockWorkspaceBuildLogs } from "../../testHelpers/entities"
4+
import { Logs, LogsProps } from "./Logs"
5+
6+
export default {
7+
title: "components/Logs",
8+
component: Logs,
9+
} as ComponentMeta<typeof Logs>
10+
11+
const Template: Story<LogsProps> = (args) => <Logs {...args} />
12+
13+
const lines = MockWorkspaceBuildLogs.map((log) => ({
14+
time: log.created_at,
15+
output: log.output,
16+
}))
17+
export const Example = Template.bind({})
18+
Example.args = {
19+
lines,
20+
}

site/src/components/Logs/Logs.tsx

+53
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import dayjs from "dayjs"
3+
import React from "react"
4+
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
5+
import { combineClasses } from "../../util/combineClasses"
6+
7+
interface Line {
8+
time: string
9+
output: string
10+
}
11+
12+
export interface LogsProps {
13+
lines: Line[]
14+
className?: string
15+
}
16+
17+
export const Logs: React.FC<LogsProps> = ({ lines, className = "" }) => {
18+
const styles = useStyles()
19+
20+
return (
21+
<div className={combineClasses([className, styles.root])}>
22+
{lines.map((line, idx) => (
23+
<div className={styles.line} key={idx}>
24+
<div className={styles.time}>{dayjs(line.time).format(`HH:mm:ss.SSS`)}</div>
25+
<div>{line.output}</div>
26+
</div>
27+
))}
28+
</div>
29+
)
30+
}
31+
32+
const useStyles = makeStyles((theme) => ({
33+
root: {
34+
minHeight: 156,
35+
background: theme.palette.background.default,
36+
color: theme.palette.text.primary,
37+
fontFamily: MONOSPACE_FONT_FAMILY,
38+
fontSize: 13,
39+
wordBreak: "break-all",
40+
padding: theme.spacing(2),
41+
borderRadius: theme.shape.borderRadius,
42+
overflowX: "auto",
43+
},
44+
line: {
45+
display: "flex",
46+
alignItems: "baseline",
47+
},
48+
time: {
49+
width: theme.spacing(12.5),
50+
marginRight: theme.spacing(3),
51+
flexShrink: 0,
52+
},
53+
}))

site/src/components/TableLoader/TableLoader.tsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
1-
import Box from "@material-ui/core/Box"
2-
import CircularProgress from "@material-ui/core/CircularProgress"
31
import { makeStyles } from "@material-ui/core/styles"
42
import TableCell from "@material-ui/core/TableCell"
53
import TableRow from "@material-ui/core/TableRow"
64
import React from "react"
5+
import { Loader } from "../Loader/Loader"
76

87
export const TableLoader: React.FC = () => {
98
const styles = useStyles()
109

1110
return (
1211
<TableRow>
1312
<TableCell colSpan={999} className={styles.cell}>
14-
<Box p={4}>
15-
<CircularProgress size={26} />
16-
</Box>
13+
<Loader />
1714
</TableCell>
1815
</TableRow>
1916
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import React from "react"
3+
import { MockWorkspaceBuildLogs } from "../../testHelpers/entities"
4+
import { WorkspaceBuildLogs, WorkspaceBuildLogsProps } from "./WorkspaceBuildLogs"
5+
6+
export default {
7+
title: "components/WorkspaceBuildLogs",
8+
component: WorkspaceBuildLogs,
9+
} as ComponentMeta<typeof WorkspaceBuildLogs>
10+
11+
const Template: Story<WorkspaceBuildLogsProps> = (args) => <WorkspaceBuildLogs {...args} />
12+
13+
export const Example = Template.bind({})
14+
Example.args = {
15+
logs: MockWorkspaceBuildLogs,
16+
}

0 commit comments

Comments
 (0)