mirror of
https://github.com/coder/coder.git
synced 2026-09-22 13:10:21 +08:00
feat(site): display build logs history in the build log page (#9150)
This commit is contained in:
+2
-2
@@ -488,7 +488,7 @@ export function waitForBuild(build: TypesGen.WorkspaceBuild) {
|
||||
const { job } = await getWorkspaceBuildByNumber(
|
||||
build.workspace_owner_name,
|
||||
build.workspace_name,
|
||||
String(build.build_number),
|
||||
build.build_number,
|
||||
)
|
||||
latestJobInfo = job
|
||||
|
||||
@@ -772,7 +772,7 @@ export const getWorkspaceBuilds = async (
|
||||
export const getWorkspaceBuildByNumber = async (
|
||||
username = "me",
|
||||
workspaceName: string,
|
||||
buildNumber: string,
|
||||
buildNumber: number,
|
||||
): Promise<TypesGen.WorkspaceBuild> => {
|
||||
const response = await axios.get<TypesGen.WorkspaceBuild>(
|
||||
`/api/v2/users/${username}/workspace/${workspaceName}/builds/${buildNumber}`,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined"
|
||||
import StopOutlined from "@mui/icons-material/StopOutlined"
|
||||
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
|
||||
import { WorkspaceTransition } from "api/typesGenerated"
|
||||
import { ComponentProps } from "react"
|
||||
|
||||
type SVGIcon = typeof PlayArrowOutlined
|
||||
|
||||
type SVGIconProps = ComponentProps<SVGIcon>
|
||||
|
||||
const iconByTransition: Record<WorkspaceTransition, SVGIcon> = {
|
||||
start: PlayArrowOutlined,
|
||||
stop: StopOutlined,
|
||||
delete: DeleteOutlined,
|
||||
}
|
||||
|
||||
export const BuildIcon = (
|
||||
props: SVGIconProps & { transition: WorkspaceTransition },
|
||||
) => {
|
||||
const Icon = iconByTransition[props.transition]
|
||||
return <Icon {...props} />
|
||||
}
|
||||
@@ -1,14 +1,12 @@
|
||||
import Badge from "@mui/material/Badge"
|
||||
import { useTheme, withStyles } from "@mui/styles"
|
||||
import { FC } from "react"
|
||||
import PlayArrowOutlined from "@mui/icons-material/PlayArrowOutlined"
|
||||
import PauseOutlined from "@mui/icons-material/PauseOutlined"
|
||||
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
|
||||
import { WorkspaceBuild, WorkspaceTransition } from "api/typesGenerated"
|
||||
import { WorkspaceBuild } from "api/typesGenerated"
|
||||
import { getDisplayWorkspaceBuildStatus } from "utils/workspace"
|
||||
import { Avatar, AvatarProps } from "components/Avatar/Avatar"
|
||||
import { PaletteIndex } from "theme/theme"
|
||||
import { Theme } from "@mui/material/styles"
|
||||
import { BuildIcon } from "components/BuildIcon/BuildIcon"
|
||||
|
||||
interface StylesBadgeProps {
|
||||
type: PaletteIndex
|
||||
@@ -31,12 +29,6 @@ export interface BuildAvatarProps {
|
||||
size?: AvatarProps["size"]
|
||||
}
|
||||
|
||||
const iconByTransition: Record<WorkspaceTransition, JSX.Element> = {
|
||||
start: <PlayArrowOutlined />,
|
||||
stop: <PauseOutlined />,
|
||||
delete: <DeleteOutlined />,
|
||||
}
|
||||
|
||||
export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
|
||||
const theme = useTheme<Theme>()
|
||||
const displayBuildStatus = getDisplayWorkspaceBuildStatus(theme, build)
|
||||
@@ -55,7 +47,7 @@ export const BuildAvatar: FC<BuildAvatarProps> = ({ build, size }) => {
|
||||
badgeContent={<div></div>}
|
||||
>
|
||||
<Avatar size={size} colorScheme="darken">
|
||||
{iconByTransition[build.transition]}
|
||||
<BuildIcon transition={build.transition} />
|
||||
</Avatar>
|
||||
</StyledBadge>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import Box, { BoxProps } from "@mui/material/Box"
|
||||
import { styled } from "@mui/styles"
|
||||
import { colors } from "theme/colors"
|
||||
|
||||
export const Sidebar = styled((props: BoxProps) => (
|
||||
<Box {...props} component="nav" />
|
||||
))(({ theme }) => ({
|
||||
width: theme.spacing(32),
|
||||
flexShrink: 0,
|
||||
borderRight: `1px solid ${theme.palette.divider}`,
|
||||
height: "100%",
|
||||
overflowY: "auto",
|
||||
}))
|
||||
|
||||
export const SidebarItem = styled(
|
||||
({ active, ...props }: BoxProps & { active?: boolean }) => (
|
||||
<Box component="button" {...props} />
|
||||
),
|
||||
)(({ theme, active }) => ({
|
||||
background: active ? colors.gray[13] : "none",
|
||||
border: "none",
|
||||
fontSize: 14,
|
||||
width: "100%",
|
||||
textAlign: "left",
|
||||
padding: theme.spacing(0, 3),
|
||||
cursor: "pointer",
|
||||
pointerEvents: active ? "none" : "auto",
|
||||
color: active ? theme.palette.text.primary : theme.palette.text.secondary,
|
||||
"&:hover": {
|
||||
background: theme.palette.action.hover,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
paddingTop: theme.spacing(1.25),
|
||||
paddingBottom: theme.spacing(1.25),
|
||||
}))
|
||||
|
||||
export const SidebarCaption = styled(Box)(({ theme }) => ({
|
||||
fontSize: 10,
|
||||
textTransform: "uppercase",
|
||||
fontWeight: 500,
|
||||
color: theme.palette.text.secondary,
|
||||
padding: theme.spacing(1.5, 3),
|
||||
letterSpacing: "0.5px",
|
||||
}))
|
||||
@@ -1,37 +0,0 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { MockWorkspaceBuild } from "../../testHelpers/entities"
|
||||
import {
|
||||
WorkspaceBuildStats,
|
||||
WorkspaceBuildStatsProps,
|
||||
} from "./WorkspaceBuildStats"
|
||||
|
||||
export default {
|
||||
title: "components/WorkspaceBuildStats",
|
||||
component: WorkspaceBuildStats,
|
||||
} as ComponentMeta<typeof WorkspaceBuildStats>
|
||||
|
||||
const Template: Story<WorkspaceBuildStatsProps> = (args) => (
|
||||
<WorkspaceBuildStats {...args} />
|
||||
)
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
build: MockWorkspaceBuild,
|
||||
}
|
||||
|
||||
export const Autostart = Template.bind({})
|
||||
Autostart.args = {
|
||||
build: {
|
||||
...MockWorkspaceBuild,
|
||||
reason: "autostart",
|
||||
},
|
||||
}
|
||||
|
||||
export const Autostop = Template.bind({})
|
||||
Autostop.args = {
|
||||
build: {
|
||||
...MockWorkspaceBuild,
|
||||
transition: "stop",
|
||||
reason: "autostop",
|
||||
},
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Stats, StatsItem } from "components/Stats/Stats"
|
||||
import { FC } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
import { WorkspaceBuild } from "../../api/typesGenerated"
|
||||
import { displayWorkspaceBuildDuration } from "../../utils/workspace"
|
||||
|
||||
export interface WorkspaceBuildStatsProps {
|
||||
build: WorkspaceBuild
|
||||
}
|
||||
|
||||
export const WorkspaceBuildStats: FC<WorkspaceBuildStatsProps> = ({
|
||||
build,
|
||||
}) => {
|
||||
const { t } = useTranslation("buildPage")
|
||||
|
||||
return (
|
||||
<Stats>
|
||||
<StatsItem
|
||||
label={t("stats.workspace")}
|
||||
value={
|
||||
<Link to={`/@${build.workspace_owner_name}/${build.workspace_name}`}>
|
||||
{build.workspace_name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
label={t("stats.duration")}
|
||||
value={displayWorkspaceBuildDuration(build)}
|
||||
/>
|
||||
<StatsItem
|
||||
label={t("stats.startedAt")}
|
||||
value={new Date(build.created_at).toLocaleString()}
|
||||
/>
|
||||
<StatsItem label={t("stats.action")} value={build.transition} />
|
||||
</Stats>
|
||||
)
|
||||
}
|
||||
@@ -22,7 +22,7 @@ describe("WorkspaceBuildPage", () => {
|
||||
expect(getWorkspaceBuildSpy).toBeCalledWith(
|
||||
MockWorkspace.owner_name,
|
||||
MockWorkspace.name,
|
||||
`${MockWorkspaceBuild.build_number}`,
|
||||
MockWorkspaceBuild.build_number,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { useMachine } from "@xstate/react"
|
||||
import { FC } from "react"
|
||||
import { FC, useEffect } 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 { WorkspaceBuildPageView } from "./WorkspaceBuildPageView"
|
||||
import { useQuery } from "@tanstack/react-query"
|
||||
import { getWorkspaceBuilds } from "api/api"
|
||||
import dayjs from "dayjs"
|
||||
import { usePermissions } from "hooks"
|
||||
|
||||
export const WorkspaceBuildPage: FC = () => {
|
||||
const params = useParams() as {
|
||||
@@ -13,12 +17,27 @@ export const WorkspaceBuildPage: FC = () => {
|
||||
buildNumber: string
|
||||
}
|
||||
const workspaceName = params.workspace
|
||||
const buildNumber = params.buildNumber
|
||||
const buildNumber = Number(params.buildNumber)
|
||||
const username = params.username.replace("@", "")
|
||||
const [buildState] = useMachine(workspaceBuildMachine, {
|
||||
const [buildState, send] = useMachine(workspaceBuildMachine, {
|
||||
context: { username, workspaceName, buildNumber, timeCursor: new Date() },
|
||||
})
|
||||
const { logs, build } = buildState.context
|
||||
const { data: builds } = useQuery({
|
||||
queryKey: ["builds", username, build?.workspace_id],
|
||||
queryFn: () => {
|
||||
return getWorkspaceBuilds(
|
||||
build?.workspace_id ?? "",
|
||||
dayjs().add(-30, "day").toDate(),
|
||||
)
|
||||
},
|
||||
enabled: Boolean(build),
|
||||
})
|
||||
const permissions = usePermissions()
|
||||
|
||||
useEffect(() => {
|
||||
send("RESET", { buildNumber, timeCursor: new Date() })
|
||||
}, [buildNumber, send])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -32,7 +51,13 @@ export const WorkspaceBuildPage: FC = () => {
|
||||
</title>
|
||||
</Helmet>
|
||||
|
||||
<WorkspaceBuildPageView logs={logs} build={build} />
|
||||
<WorkspaceBuildPageView
|
||||
logs={logs}
|
||||
build={build}
|
||||
builds={builds}
|
||||
activeBuildNumber={buildNumber}
|
||||
hasDeploymentBanner={permissions.viewDeploymentStats}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,49 @@
|
||||
import { ComponentMeta, Story } from "@storybook/react"
|
||||
import { Meta, StoryObj } from "@storybook/react"
|
||||
import {
|
||||
MockFailedWorkspaceBuild,
|
||||
MockWorkspaceBuild,
|
||||
MockWorkspaceBuildLogs,
|
||||
} from "../../testHelpers/entities"
|
||||
import {
|
||||
WorkspaceBuildPageView,
|
||||
WorkspaceBuildPageViewProps,
|
||||
} from "./WorkspaceBuildPageView"
|
||||
import { WorkspaceBuildPageView } from "./WorkspaceBuildPageView"
|
||||
|
||||
export default {
|
||||
const defaultBuilds = Array.from({ length: 15 }, (_, i) => ({
|
||||
...MockWorkspaceBuild,
|
||||
id: `${i}`,
|
||||
build_number: i,
|
||||
}))
|
||||
|
||||
const meta: Meta<typeof WorkspaceBuildPageView> = {
|
||||
title: "pages/WorkspaceBuildPageView",
|
||||
component: WorkspaceBuildPageView,
|
||||
} as ComponentMeta<typeof WorkspaceBuildPageView>
|
||||
|
||||
const Template: Story<WorkspaceBuildPageViewProps> = (args) => (
|
||||
<WorkspaceBuildPageView {...args} />
|
||||
)
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
build: MockWorkspaceBuild,
|
||||
logs: MockWorkspaceBuildLogs,
|
||||
args: {
|
||||
build: MockWorkspaceBuild,
|
||||
logs: MockWorkspaceBuildLogs,
|
||||
builds: defaultBuilds,
|
||||
activeBuildNumber: defaultBuilds[0].build_number,
|
||||
hasDeploymentBanner: false,
|
||||
},
|
||||
}
|
||||
|
||||
export const FailedDelete = Template.bind({})
|
||||
FailedDelete.args = {
|
||||
build: MockFailedWorkspaceBuild("delete"),
|
||||
logs: MockWorkspaceBuildLogs,
|
||||
export default meta
|
||||
type Story = StoryObj<typeof WorkspaceBuildPageView>
|
||||
|
||||
export const Loaded: Story = {}
|
||||
|
||||
export const LoadingBuildLogs: Story = {
|
||||
args: {
|
||||
builds: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
const failedBuild = {
|
||||
...MockFailedWorkspaceBuild("delete"),
|
||||
build_number: new Date().getDate(),
|
||||
}
|
||||
|
||||
export const FailedDelete: Story = {
|
||||
args: {
|
||||
build: failedBuild,
|
||||
builds: [failedBuild, ...defaultBuilds],
|
||||
activeBuildNumber: failedBuild.build_number,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -2,16 +2,30 @@ import { BuildAvatar } from "components/BuildsTable/BuildAvatar"
|
||||
import { FC } from "react"
|
||||
import { ProvisionerJobLog, WorkspaceBuild } from "../../api/typesGenerated"
|
||||
import { Loader } from "../../components/Loader/Loader"
|
||||
import { Margins } from "../../components/Margins/Margins"
|
||||
import {
|
||||
PageHeader,
|
||||
PageHeaderSubtitle,
|
||||
PageHeaderTitle,
|
||||
} from "../../components/PageHeader/PageHeader"
|
||||
import { Stack } from "../../components/Stack/Stack"
|
||||
import { WorkspaceBuildLogs } from "../../components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
||||
import { WorkspaceBuildStats } from "../../components/WorkspaceBuildStats/WorkspaceBuildStats"
|
||||
import { WorkspaceBuildStateError } from "./WorkspaceBuildStateError"
|
||||
import { makeStyles } from "@mui/styles"
|
||||
import {
|
||||
FullWidthPageHeader,
|
||||
PageHeaderTitle,
|
||||
PageHeaderSubtitle,
|
||||
} from "components/PageHeader/FullWidthPageHeader"
|
||||
import { Link } from "react-router-dom"
|
||||
import { Stats, StatsItem } from "components/Stats/Stats"
|
||||
import {
|
||||
displayWorkspaceBuildDuration,
|
||||
getDisplayWorkspaceBuildInitiatedBy,
|
||||
getDisplayWorkspaceBuildStatus,
|
||||
} from "utils/workspace"
|
||||
import Box from "@mui/material/Box"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarCaption,
|
||||
SidebarItem,
|
||||
} from "components/Sidebar/Sidebar"
|
||||
import { BuildIcon } from "components/BuildIcon/BuildIcon"
|
||||
import Skeleton from "@mui/material/Skeleton"
|
||||
import { Alert } from "components/Alert/Alert"
|
||||
|
||||
const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => {
|
||||
return [...logs].sort(
|
||||
@@ -23,38 +37,248 @@ const sortLogsByCreatedAt = (logs: ProvisionerJobLog[]) => {
|
||||
export interface WorkspaceBuildPageViewProps {
|
||||
logs: ProvisionerJobLog[] | undefined
|
||||
build: WorkspaceBuild | undefined
|
||||
builds: WorkspaceBuild[] | undefined
|
||||
activeBuildNumber: number
|
||||
hasDeploymentBanner: boolean
|
||||
}
|
||||
|
||||
export const WorkspaceBuildPageView: FC<WorkspaceBuildPageViewProps> = ({
|
||||
logs,
|
||||
build,
|
||||
builds,
|
||||
activeBuildNumber,
|
||||
hasDeploymentBanner,
|
||||
}) => {
|
||||
return (
|
||||
<Margins>
|
||||
{build && (
|
||||
<PageHeader>
|
||||
<Stack direction="row" alignItems="center" spacing={3}>
|
||||
<BuildAvatar build={build} size="xl" />
|
||||
<div>
|
||||
<PageHeaderTitle>Build #{build.build_number}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle condensed>
|
||||
{build.initiator_name}
|
||||
</PageHeaderSubtitle>
|
||||
</div>
|
||||
</Stack>
|
||||
</PageHeader>
|
||||
)}
|
||||
const styles = useStyles()
|
||||
const navbarHeight = 62
|
||||
const deploymentBannerHeight = 48
|
||||
const heightOffset = hasDeploymentBanner
|
||||
? navbarHeight + deploymentBannerHeight
|
||||
: navbarHeight
|
||||
|
||||
<Stack spacing={4}>
|
||||
{build &&
|
||||
build.transition === "delete" &&
|
||||
build.job.status === "failed" && (
|
||||
<WorkspaceBuildStateError build={build} />
|
||||
if (!build) {
|
||||
return <Loader />
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
// 62px is the navbar height
|
||||
height: `calc(100vh - ${heightOffset}px)`,
|
||||
overflow: "hidden",
|
||||
// Remove padding added from dashboard layout (.siteContent)
|
||||
marginBottom: "-48px",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<FullWidthPageHeader sticky={false}>
|
||||
<Stack direction="row" alignItems="center" spacing={3}>
|
||||
<BuildAvatar build={build} />
|
||||
<div>
|
||||
<PageHeaderTitle>Build #{build.build_number}</PageHeaderTitle>
|
||||
<PageHeaderSubtitle>{build.initiator_name}</PageHeaderSubtitle>
|
||||
</div>
|
||||
</Stack>
|
||||
|
||||
<Stats aria-label="Build details" className={styles.stats}>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Workspace"
|
||||
value={
|
||||
<Link
|
||||
to={`/@${build.workspace_owner_name}/${build.workspace_name}`}
|
||||
>
|
||||
{build.workspace_name}
|
||||
</Link>
|
||||
}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Template version"
|
||||
value={build.template_version_name}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Duration"
|
||||
value={displayWorkspaceBuildDuration(build)}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Started at"
|
||||
value={new Date(build.created_at).toLocaleString()}
|
||||
/>
|
||||
<StatsItem
|
||||
className={styles.statsItem}
|
||||
label="Action"
|
||||
value={
|
||||
<Box component="span" sx={{ textTransform: "capitalize" }}>
|
||||
{build.transition}
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
</Stats>
|
||||
</FullWidthPageHeader>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "start",
|
||||
overflow: "hidden",
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarCaption>Builds</SidebarCaption>
|
||||
{!builds &&
|
||||
Array.from({ length: 15 }, (_, i) => (
|
||||
<BuildSidebarItemSkeleton key={i} />
|
||||
))}
|
||||
|
||||
{builds?.map((build) => (
|
||||
<BuildSidebarItem
|
||||
key={build.id}
|
||||
build={build}
|
||||
active={build.build_number === activeBuildNumber}
|
||||
/>
|
||||
))}
|
||||
</Sidebar>
|
||||
|
||||
<Box sx={{ height: "100%", overflowY: "auto", width: "100%" }}>
|
||||
{build.transition === "delete" && build.job.status === "failed" && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
borderRadius: 0,
|
||||
border: 0,
|
||||
background: (theme) => theme.palette.error.dark,
|
||||
borderBottom: (theme) => `1px solid ${theme.palette.divider}`,
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
The workspace may have failed to delete due to a Terraform state
|
||||
mismatch. A template admin may run{" "}
|
||||
<Box
|
||||
component="code"
|
||||
display="inline-block"
|
||||
width="fit-content"
|
||||
fontWeight={600}
|
||||
>
|
||||
`
|
||||
{`coder rm ${
|
||||
build.workspace_owner_name + "/" + build.workspace_name
|
||||
} --orphan`}
|
||||
`
|
||||
</Box>{" "}
|
||||
to delete the workspace skipping resource destruction.
|
||||
</Box>
|
||||
</Alert>
|
||||
)}
|
||||
{build && <WorkspaceBuildStats build={build} />}
|
||||
{!logs && <Loader />}
|
||||
{logs && <WorkspaceBuildLogs logs={sortLogsByCreatedAt(logs)} />}
|
||||
</Stack>
|
||||
</Margins>
|
||||
{logs ? (
|
||||
<WorkspaceBuildLogs
|
||||
sx={{ border: 0 }}
|
||||
logs={sortLogsByCreatedAt(logs)}
|
||||
/>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const BuildSidebarItem = ({
|
||||
build,
|
||||
active,
|
||||
}: {
|
||||
build: WorkspaceBuild
|
||||
active: boolean
|
||||
}) => {
|
||||
return (
|
||||
<Link
|
||||
key={build.id}
|
||||
to={`/@${build.workspace_owner_name}/${build.workspace_name}/builds/${build.build_number}`}
|
||||
>
|
||||
<SidebarItem active={active}>
|
||||
<Box sx={{ display: "flex", alignItems: "start", gap: 1 }}>
|
||||
<BuildIcon
|
||||
transition={build.transition}
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: (theme) =>
|
||||
theme.palette[getDisplayWorkspaceBuildStatus(theme, build).type]
|
||||
.light,
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ overflow: "hidden" }}>
|
||||
<Box
|
||||
sx={{
|
||||
textTransform: "capitalize",
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{build.transition} by{" "}
|
||||
<strong>{getDisplayWorkspaceBuildInitiatedBy(build)}</strong>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
color: (theme) => theme.palette.text.secondary,
|
||||
mt: 0.25,
|
||||
}}
|
||||
>
|
||||
{displayWorkspaceBuildDuration(build)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</SidebarItem>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const BuildSidebarItemSkeleton = () => {
|
||||
return (
|
||||
<SidebarItem>
|
||||
<Box sx={{ display: "flex", alignItems: "start", gap: 1 }}>
|
||||
<Skeleton variant="circular" width={16} height={16} />
|
||||
<Box>
|
||||
<Skeleton variant="text" width={94} height={16} />
|
||||
<Skeleton variant="text" width={60} height={14} sx={{ mt: 0.25 }} />
|
||||
</Box>
|
||||
</Box>
|
||||
</SidebarItem>
|
||||
)
|
||||
}
|
||||
|
||||
const useStyles = makeStyles((theme) => ({
|
||||
stats: {
|
||||
padding: 0,
|
||||
border: 0,
|
||||
gap: theme.spacing(6),
|
||||
rowGap: theme.spacing(3),
|
||||
flex: 1,
|
||||
|
||||
[theme.breakpoints.down("md")]: {
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "flex-start",
|
||||
gap: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
|
||||
statsItem: {
|
||||
flexDirection: "column",
|
||||
gap: 0,
|
||||
padding: 0,
|
||||
|
||||
"& > span:first-of-type": {
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import Box from "@mui/material/Box"
|
||||
import { WorkspaceBuild } from "api/typesGenerated"
|
||||
import { Alert } from "components/Alert/Alert"
|
||||
|
||||
const Language = {
|
||||
stateMessage:
|
||||
"The workspace may have failed to delete due to a Terraform state mismatch.",
|
||||
}
|
||||
|
||||
export interface WorkspaceBuildStateErrorProps {
|
||||
build: WorkspaceBuild
|
||||
}
|
||||
|
||||
export const WorkspaceBuildStateError: React.FC<
|
||||
WorkspaceBuildStateErrorProps
|
||||
> = ({ build }) => {
|
||||
const orphanCommand = `coder rm ${
|
||||
build.workspace_owner_name + "/" + build.workspace_name
|
||||
} --orphan`
|
||||
return (
|
||||
<Alert severity="error">
|
||||
<Box>
|
||||
{Language.stateMessage} A template admin may run{" "}
|
||||
<Box
|
||||
component="code"
|
||||
display="inline-block"
|
||||
width="fit-content"
|
||||
fontWeight={600}
|
||||
>
|
||||
`{orphanCommand}`
|
||||
</Box>{" "}
|
||||
to delete the workspace skipping resource destruction.
|
||||
</Box>
|
||||
</Alert>
|
||||
)
|
||||
}
|
||||
@@ -351,7 +351,7 @@ const WarningDialog: FC<
|
||||
}
|
||||
|
||||
const useBuildLogs = (workspace: TypesGen.Workspace) => {
|
||||
const buildNumber = workspace.latest_build.build_number.toString()
|
||||
const buildNumber = workspace.latest_build.build_number
|
||||
const [buildState, buildSend] = useMachine(workspaceBuildMachine, {
|
||||
context: {
|
||||
buildNumber,
|
||||
|
||||
@@ -815,7 +815,7 @@ export const MockFailedWorkspaceBuild = (
|
||||
deadline: "2022-05-17T23:39:00.00Z",
|
||||
reason: "initiator",
|
||||
resources: [],
|
||||
status: "running",
|
||||
status: "failed",
|
||||
daily_cost: 20,
|
||||
})
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ export const getDisplayWorkspaceBuildStatus = (
|
||||
case "succeeded":
|
||||
return {
|
||||
type: "success",
|
||||
color: theme.palette.success.main,
|
||||
color: theme.palette.success.light,
|
||||
status: DisplayWorkspaceBuildStatusLanguage.succeeded,
|
||||
} as const
|
||||
case "pending":
|
||||
|
||||
@@ -6,7 +6,7 @@ type LogsContext = {
|
||||
// Build
|
||||
username: string
|
||||
workspaceName: string
|
||||
buildNumber: string
|
||||
buildNumber: number
|
||||
buildId: string
|
||||
// Used to reference logs before + after.
|
||||
timeCursor: Date
|
||||
@@ -26,7 +26,7 @@ type LogsEvent =
|
||||
}
|
||||
| {
|
||||
type: "RESET"
|
||||
buildNumber: string
|
||||
buildNumber: number
|
||||
timeCursor: Date
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user