Files
coder/site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx
T

139 lines
3.7 KiB
TypeScript

import { makeStyles } from "@mui/styles"
import dayjs from "dayjs"
import { FC, Fragment } from "react"
import { ProvisionerJobLog } from "../../api/typesGenerated"
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
import { Logs } from "../Logs/Logs"
import { Theme } from "@mui/material/styles"
const Language = {
seconds: "seconds",
}
type Stage = ProvisionerJobLog["stage"]
type LogsGroupedByStage = Record<Stage, ProvisionerJobLog[]>
type GroupLogsByStageFn = (logs: ProvisionerJobLog[]) => LogsGroupedByStage
export const groupLogsByStage: GroupLogsByStageFn = (logs) => {
const logsByStage: LogsGroupedByStage = {}
for (const log of logs) {
if (log.stage in logsByStage) {
logsByStage[log.stage].push(log)
} else {
logsByStage[log.stage] = [log]
}
}
return logsByStage
}
const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => {
if (logs.length < 2) {
return
}
const startedAt = dayjs(logs[0].created_at)
const completedAt = dayjs(logs[logs.length - 1].created_at)
return completedAt.diff(startedAt, "seconds")
}
export interface WorkspaceBuildLogsProps {
logs: ProvisionerJobLog[]
hideTimestamps?: boolean
// If true, render different styles that fit the template editor pane
// a bit better.
templateEditorPane?: boolean
}
export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
hideTimestamps,
logs,
templateEditorPane,
}) => {
const groupedLogsByStage = groupLogsByStage(logs)
const stages = Object.keys(groupedLogsByStage)
const styles = useStyles({ templateEditorPane: Boolean(templateEditorPane) })
return (
<div className={styles.logs}>
{stages.map((stage) => {
const logs = groupedLogsByStage[stage]
const isEmpty = logs.every((log) => log.output === "")
const lines = logs.map((log) => ({
time: log.created_at,
output: log.output,
level: log.log_level,
}))
const duration = getStageDurationInSeconds(logs)
const shouldDisplayDuration = duration !== undefined
return (
<Fragment key={stage}>
<div className={styles.header}>
<div>{stage}</div>
{shouldDisplayDuration && (
<div className={styles.duration}>
{duration} {Language.seconds}
</div>
)}
</div>
{!isEmpty && <Logs hideTimestamps={hideTimestamps} lines={lines} />}
</Fragment>
)
})}
</div>
)
}
const useStyles = makeStyles<
Theme,
{
templateEditorPane: boolean
}
>((theme) => ({
logs: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: (props) =>
props.templateEditorPane ? "0px" : theme.shape.borderRadius,
fontFamily: MONOSPACE_FONT_FAMILY,
},
header: {
fontSize: 14,
padding: theme.spacing(2),
paddingLeft: theme.spacing(3),
paddingRight: theme.spacing(3),
borderTop: `1px solid ${theme.palette.divider}`,
borderBottom: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
display: "flex",
alignItems: "center",
fontFamily: "Inter",
"&:first-of-type": {
borderTopLeftRadius: theme.shape.borderRadius,
borderTopRightRadius: theme.shape.borderRadius,
borderTop: 0,
},
"&:last-child": {
borderBottom: 0,
borderTop: `1px solid ${theme.palette.divider}`,
borderBottomLeftRadius: theme.shape.borderRadius,
borderBottomRightRadius: theme.shape.borderRadius,
},
"& + $header": {
borderTop: 0,
},
},
duration: {
marginLeft: "auto",
color: theme.palette.text.secondary,
fontSize: theme.typography.body2.fontSize,
},
}))