mirror of
https://github.com/coder/coder.git
synced 2026-11-09 07:02:08 +08:00
139 lines
3.7 KiB
TypeScript
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,
|
|
},
|
|
}))
|