mirror of
https://github.com/coder/coder.git
synced 2025-07-06 15:41:45 +00:00
feat(site): display build logs on workspace transitioning statuses (#8397)
This commit is contained in:
6
coderd/apidoc/docs.go
generated
6
coderd/apidoc/docs.go
generated
@ -7609,13 +7609,15 @@ const docTemplate = `{
|
|||||||
"moons",
|
"moons",
|
||||||
"workspace_actions",
|
"workspace_actions",
|
||||||
"tailnet_pg_coordinator",
|
"tailnet_pg_coordinator",
|
||||||
"convert-to-oidc"
|
"convert-to-oidc",
|
||||||
|
"workspace_build_logs_ui"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentMoons",
|
"ExperimentMoons",
|
||||||
"ExperimentWorkspaceActions",
|
"ExperimentWorkspaceActions",
|
||||||
"ExperimentTailnetPGCoordinator",
|
"ExperimentTailnetPGCoordinator",
|
||||||
"ExperimentConvertToOIDC"
|
"ExperimentConvertToOIDC",
|
||||||
|
"ExperimentWorkspaceBuildLogsUI"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Feature": {
|
"codersdk.Feature": {
|
||||||
|
6
coderd/apidoc/swagger.json
generated
6
coderd/apidoc/swagger.json
generated
@ -6810,13 +6810,15 @@
|
|||||||
"moons",
|
"moons",
|
||||||
"workspace_actions",
|
"workspace_actions",
|
||||||
"tailnet_pg_coordinator",
|
"tailnet_pg_coordinator",
|
||||||
"convert-to-oidc"
|
"convert-to-oidc",
|
||||||
|
"workspace_build_logs_ui"
|
||||||
],
|
],
|
||||||
"x-enum-varnames": [
|
"x-enum-varnames": [
|
||||||
"ExperimentMoons",
|
"ExperimentMoons",
|
||||||
"ExperimentWorkspaceActions",
|
"ExperimentWorkspaceActions",
|
||||||
"ExperimentTailnetPGCoordinator",
|
"ExperimentTailnetPGCoordinator",
|
||||||
"ExperimentConvertToOIDC"
|
"ExperimentConvertToOIDC",
|
||||||
|
"ExperimentWorkspaceBuildLogsUI"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"codersdk.Feature": {
|
"codersdk.Feature": {
|
||||||
|
@ -1763,6 +1763,7 @@ const (
|
|||||||
// oidc.
|
// oidc.
|
||||||
ExperimentConvertToOIDC Experiment = "convert-to-oidc"
|
ExperimentConvertToOIDC Experiment = "convert-to-oidc"
|
||||||
|
|
||||||
|
ExperimentWorkspaceBuildLogsUI Experiment = "workspace_build_logs_ui"
|
||||||
// Add new experiments here!
|
// Add new experiments here!
|
||||||
// ExperimentExample Experiment = "example"
|
// ExperimentExample Experiment = "example"
|
||||||
)
|
)
|
||||||
@ -1771,7 +1772,9 @@ const (
|
|||||||
// users to opt-in to via --experimental='*'.
|
// users to opt-in to via --experimental='*'.
|
||||||
// Experiments that are not ready for consumption by all users should
|
// Experiments that are not ready for consumption by all users should
|
||||||
// not be included here and will be essentially hidden.
|
// not be included here and will be essentially hidden.
|
||||||
var ExperimentsAll = Experiments{}
|
var ExperimentsAll = Experiments{
|
||||||
|
ExperimentWorkspaceBuildLogsUI,
|
||||||
|
}
|
||||||
|
|
||||||
// Experiments is a list of experiments that are enabled for the deployment.
|
// Experiments is a list of experiments that are enabled for the deployment.
|
||||||
// Multiple experiments may be enabled at the same time.
|
// Multiple experiments may be enabled at the same time.
|
||||||
|
@ -2531,12 +2531,13 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||||||
|
|
||||||
#### Enumerated Values
|
#### Enumerated Values
|
||||||
|
|
||||||
| Value |
|
| Value |
|
||||||
| ------------------------ |
|
| ------------------------- |
|
||||||
| `moons` |
|
| `moons` |
|
||||||
| `workspace_actions` |
|
| `workspace_actions` |
|
||||||
| `tailnet_pg_coordinator` |
|
| `tailnet_pg_coordinator` |
|
||||||
| `convert-to-oidc` |
|
| `convert-to-oidc` |
|
||||||
|
| `workspace_build_logs_ui` |
|
||||||
|
|
||||||
## codersdk.Feature
|
## codersdk.Feature
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import { HelmetProvider } from "react-helmet-async"
|
|||||||
import { dark } from "../src/theme"
|
import { dark } from "../src/theme"
|
||||||
import "../src/theme/globalFonts"
|
import "../src/theme/globalFonts"
|
||||||
import "../src/i18n"
|
import "../src/i18n"
|
||||||
|
import { LocalPreferencesProvider } from "../src/contexts/LocalPreferencesContext"
|
||||||
|
|
||||||
export const decorators = [
|
export const decorators = [
|
||||||
(Story) => (
|
(Story) => (
|
||||||
@ -23,6 +24,13 @@ export const decorators = [
|
|||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
(Story) => {
|
||||||
|
return (
|
||||||
|
<LocalPreferencesProvider>
|
||||||
|
<Story />
|
||||||
|
</LocalPreferencesProvider>
|
||||||
|
)
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export const parameters = {
|
export const parameters = {
|
||||||
|
@ -1431,11 +1431,13 @@ export type Experiment =
|
|||||||
| "moons"
|
| "moons"
|
||||||
| "tailnet_pg_coordinator"
|
| "tailnet_pg_coordinator"
|
||||||
| "workspace_actions"
|
| "workspace_actions"
|
||||||
|
| "workspace_build_logs_ui"
|
||||||
export const Experiments: Experiment[] = [
|
export const Experiments: Experiment[] = [
|
||||||
"convert-to-oidc",
|
"convert-to-oidc",
|
||||||
"moons",
|
"moons",
|
||||||
"tailnet_pg_coordinator",
|
"tailnet_pg_coordinator",
|
||||||
"workspace_actions",
|
"workspace_actions",
|
||||||
|
"workspace_build_logs_ui",
|
||||||
]
|
]
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
|
@ -9,6 +9,7 @@ import { GlobalSnackbar } from "./components/GlobalSnackbar/GlobalSnackbar"
|
|||||||
import { dark } from "./theme"
|
import { dark } from "./theme"
|
||||||
import "./theme/globalFonts"
|
import "./theme/globalFonts"
|
||||||
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"
|
import { StyledEngineProvider, ThemeProvider } from "@mui/material/styles"
|
||||||
|
import { LocalPreferencesProvider } from "contexts/LocalPreferencesContext"
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
defaultOptions: {
|
defaultOptions: {
|
||||||
@ -25,17 +26,19 @@ export const AppProviders: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
return (
|
return (
|
||||||
<HelmetProvider>
|
<HelmetProvider>
|
||||||
<StyledEngineProvider injectFirst>
|
<StyledEngineProvider injectFirst>
|
||||||
<ThemeProvider theme={dark}>
|
<LocalPreferencesProvider>
|
||||||
<CssBaseline enableColorScheme />
|
<ThemeProvider theme={dark}>
|
||||||
<ErrorBoundary>
|
<CssBaseline enableColorScheme />
|
||||||
<QueryClientProvider client={queryClient}>
|
<ErrorBoundary>
|
||||||
<AuthProvider>
|
<QueryClientProvider client={queryClient}>
|
||||||
{children}
|
<AuthProvider>
|
||||||
<GlobalSnackbar />
|
{children}
|
||||||
</AuthProvider>
|
<GlobalSnackbar />
|
||||||
</QueryClientProvider>
|
</AuthProvider>
|
||||||
</ErrorBoundary>
|
</QueryClientProvider>
|
||||||
</ThemeProvider>
|
</ErrorBoundary>
|
||||||
|
</ThemeProvider>
|
||||||
|
</LocalPreferencesProvider>
|
||||||
</StyledEngineProvider>
|
</StyledEngineProvider>
|
||||||
</HelmetProvider>
|
</HelmetProvider>
|
||||||
)
|
)
|
||||||
|
@ -97,10 +97,11 @@ const useStyles = makeStyles<
|
|||||||
>((theme) => ({
|
>((theme) => ({
|
||||||
root: {
|
root: {
|
||||||
minHeight: 156,
|
minHeight: 156,
|
||||||
padding: theme.spacing(2, 0),
|
padding: theme.spacing(1, 0),
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
overflowX: "auto",
|
overflowX: "auto",
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
},
|
},
|
||||||
scrollWrapper: {
|
scrollWrapper: {
|
||||||
minWidth: "fit-content",
|
minWidth: "fit-content",
|
||||||
|
@ -379,7 +379,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
|
|||||||
|
|
||||||
{buildLogs && buildLogs.length > 0 && (
|
{buildLogs && buildLogs.length > 0 && (
|
||||||
<WorkspaceBuildLogs
|
<WorkspaceBuildLogs
|
||||||
templateEditorPane
|
sx={{ borderRadius: 0 }}
|
||||||
hideTimestamps
|
hideTimestamps
|
||||||
logs={buildLogs}
|
logs={buildLogs}
|
||||||
/>
|
/>
|
||||||
|
@ -8,6 +8,7 @@ import { withReactContext } from "storybook-react-context"
|
|||||||
import EventSource from "eventsourcemock"
|
import EventSource from "eventsourcemock"
|
||||||
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
|
import { ProxyContext, getPreferredProxy } from "contexts/ProxyContext"
|
||||||
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"
|
import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"
|
||||||
|
import { WorkspaceBuildLogsSection } from "pages/WorkspacePage/WorkspaceBuildLogsSection"
|
||||||
|
|
||||||
const MockedAppearance = {
|
const MockedAppearance = {
|
||||||
config: Mocks.MockAppearance,
|
config: Mocks.MockAppearance,
|
||||||
@ -152,7 +153,7 @@ export const FailedWithLogs: Story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
failedBuildLogs: makeFailedBuildLogs(),
|
buildLogs: <WorkspaceBuildLogsSection logs={makeFailedBuildLogs()} />,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,8 +171,8 @@ export const FailedWithRetry: Story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
failedBuildLogs: makeFailedBuildLogs(),
|
|
||||||
canRetryDebugMode: true,
|
canRetryDebugMode: true,
|
||||||
|
buildLogs: <WorkspaceBuildLogsSection logs={makeFailedBuildLogs()} />,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,6 +230,7 @@ export const CancellationError: Story = {
|
|||||||
message: "Job could not be canceled.",
|
message: "Job could not be canceled.",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
buildLogs: <WorkspaceBuildLogsSection logs={makeFailedBuildLogs()} />,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,7 +2,6 @@ import Button from "@mui/material/Button"
|
|||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles"
|
||||||
import { Avatar } from "components/Avatar/Avatar"
|
import { Avatar } from "components/Avatar/Avatar"
|
||||||
import { AgentRow } from "components/Resources/AgentRow"
|
import { AgentRow } from "components/Resources/AgentRow"
|
||||||
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
|
||||||
import {
|
import {
|
||||||
ActiveTransition,
|
ActiveTransition,
|
||||||
WorkspaceBuildProgress,
|
WorkspaceBuildProgress,
|
||||||
@ -70,8 +69,10 @@ export interface WorkspaceProps {
|
|||||||
sshPrefix?: string
|
sshPrefix?: string
|
||||||
template?: TypesGen.Template
|
template?: TypesGen.Template
|
||||||
quota_budget?: number
|
quota_budget?: number
|
||||||
failedBuildLogs: TypesGen.ProvisionerJobLog[] | undefined
|
|
||||||
handleBuildRetry: () => void
|
handleBuildRetry: () => void
|
||||||
|
buildLogs?: React.ReactNode
|
||||||
|
canChangeBuildLogsVisibility: boolean
|
||||||
|
isWorkspaceBuildLogsUIActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -102,9 +103,11 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
sshPrefix,
|
sshPrefix,
|
||||||
template,
|
template,
|
||||||
quota_budget,
|
quota_budget,
|
||||||
failedBuildLogs,
|
|
||||||
handleBuildRetry,
|
handleBuildRetry,
|
||||||
templateWarnings,
|
templateWarnings,
|
||||||
|
buildLogs,
|
||||||
|
canChangeBuildLogsVisibility,
|
||||||
|
isWorkspaceBuildLogsUIActive,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
@ -208,6 +211,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
canChangeVersions={canChangeVersions}
|
canChangeVersions={canChangeVersions}
|
||||||
isUpdating={isUpdating}
|
isUpdating={isUpdating}
|
||||||
isRestarting={isRestarting}
|
isRestarting={isRestarting}
|
||||||
|
canChangeBuildLogsVisibility={canChangeBuildLogsVisibility}
|
||||||
|
isWorkspaceBuildLogsUIActive={isWorkspaceBuildLogsUIActive}
|
||||||
/>
|
/>
|
||||||
</PageHeaderActions>
|
</PageHeaderActions>
|
||||||
</FullWidthPageHeader>
|
</FullWidthPageHeader>
|
||||||
@ -259,28 +264,25 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
</Alert>
|
</Alert>
|
||||||
</Maybe>
|
</Maybe>
|
||||||
|
|
||||||
{failedBuildLogs && (
|
{workspace.latest_build.job.error && (
|
||||||
<Stack>
|
<Alert
|
||||||
<Alert
|
severity="error"
|
||||||
severity="error"
|
actions={
|
||||||
actions={
|
canRetryDebugMode && (
|
||||||
canRetryDebugMode && (
|
<Button
|
||||||
<Button
|
key={0}
|
||||||
key={0}
|
onClick={handleBuildRetry}
|
||||||
onClick={handleBuildRetry}
|
variant="text"
|
||||||
variant="text"
|
size="small"
|
||||||
size="small"
|
>
|
||||||
>
|
{t("actionButton.retryDebugMode")}
|
||||||
{t("actionButton.retryDebugMode")}
|
</Button>
|
||||||
</Button>
|
)
|
||||||
)
|
}
|
||||||
}
|
>
|
||||||
>
|
<AlertTitle>Workspace build failed</AlertTitle>
|
||||||
<AlertTitle>Workspace build failed</AlertTitle>
|
<AlertDetail>{workspace.latest_build.job.error}</AlertDetail>
|
||||||
<AlertDetail>{workspace.latest_build.job.error}</AlertDetail>
|
</Alert>
|
||||||
</Alert>
|
|
||||||
<WorkspaceBuildLogs logs={failedBuildLogs} />
|
|
||||||
</Stack>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{transitionStats !== undefined && (
|
{transitionStats !== undefined && (
|
||||||
@ -290,6 +292,8 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{buildLogs}
|
||||||
|
|
||||||
{typeof resources !== "undefined" && resources.length > 0 && (
|
{typeof resources !== "undefined" && resources.length > 0 && (
|
||||||
<Resources
|
<Resources
|
||||||
resources={resources}
|
resources={resources}
|
||||||
|
@ -22,6 +22,10 @@ import SettingsOutlined from "@mui/icons-material/SettingsOutlined"
|
|||||||
import HistoryOutlined from "@mui/icons-material/HistoryOutlined"
|
import HistoryOutlined from "@mui/icons-material/HistoryOutlined"
|
||||||
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
|
import DeleteOutlined from "@mui/icons-material/DeleteOutlined"
|
||||||
import IconButton from "@mui/material/IconButton"
|
import IconButton from "@mui/material/IconButton"
|
||||||
|
import Divider from "@mui/material/Divider"
|
||||||
|
import VisibilityOffOutlined from "@mui/icons-material/VisibilityOffOutlined"
|
||||||
|
import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined"
|
||||||
|
import { useLocalPreferences } from "contexts/LocalPreferencesContext"
|
||||||
|
|
||||||
export interface WorkspaceActionsProps {
|
export interface WorkspaceActionsProps {
|
||||||
workspaceStatus: WorkspaceStatus
|
workspaceStatus: WorkspaceStatus
|
||||||
@ -38,6 +42,8 @@ export interface WorkspaceActionsProps {
|
|||||||
isRestarting: boolean
|
isRestarting: boolean
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
canChangeVersions: boolean
|
canChangeVersions: boolean
|
||||||
|
canChangeBuildLogsVisibility: boolean
|
||||||
|
isWorkspaceBuildLogsUIActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||||
@ -54,6 +60,8 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||||||
isUpdating,
|
isUpdating,
|
||||||
isRestarting,
|
isRestarting,
|
||||||
canChangeVersions,
|
canChangeVersions,
|
||||||
|
canChangeBuildLogsVisibility,
|
||||||
|
isWorkspaceBuildLogsUIActive,
|
||||||
}) => {
|
}) => {
|
||||||
const styles = useStyles()
|
const styles = useStyles()
|
||||||
const {
|
const {
|
||||||
@ -64,6 +72,9 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||||||
const canBeUpdated = isOutdated && canAcceptJobs
|
const canBeUpdated = isOutdated && canAcceptJobs
|
||||||
const menuTriggerRef = useRef<HTMLButtonElement>(null)
|
const menuTriggerRef = useRef<HTMLButtonElement>(null)
|
||||||
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
const [isMenuOpen, setIsMenuOpen] = useState(false)
|
||||||
|
const localPreferences = useLocalPreferences()
|
||||||
|
const isBuildLogsVisible =
|
||||||
|
localPreferences.getPreference("buildLogsVisibility") === "visible"
|
||||||
|
|
||||||
// A mapping of button type to the corresponding React component
|
// A mapping of button type to the corresponding React component
|
||||||
const buttonMapping: ButtonMapping = {
|
const buttonMapping: ButtonMapping = {
|
||||||
@ -140,6 +151,39 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
|||||||
<DeleteOutlined />
|
<DeleteOutlined />
|
||||||
Delete
|
Delete
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
|
||||||
|
{isWorkspaceBuildLogsUIActive && (
|
||||||
|
<>
|
||||||
|
<Divider sx={{ borderColor: (theme) => theme.palette.divider }} />
|
||||||
|
{isBuildLogsVisible ? (
|
||||||
|
<MenuItem
|
||||||
|
disabled={!canChangeBuildLogsVisibility}
|
||||||
|
onClick={onMenuItemClick(() => {
|
||||||
|
localPreferences.setPreference(
|
||||||
|
"buildLogsVisibility",
|
||||||
|
"hide",
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<VisibilityOffOutlined />
|
||||||
|
Hide build logs
|
||||||
|
</MenuItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem
|
||||||
|
disabled={!canChangeBuildLogsVisibility}
|
||||||
|
onClick={onMenuItemClick(() => {
|
||||||
|
localPreferences.setPreference(
|
||||||
|
"buildLogsVisibility",
|
||||||
|
"visible",
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<VisibilityOutlined />
|
||||||
|
Show build logs
|
||||||
|
</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import { makeStyles } from "@mui/styles"
|
import { makeStyles } from "@mui/styles"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { FC, Fragment } from "react"
|
import { ComponentProps, FC, Fragment } from "react"
|
||||||
import { ProvisionerJobLog } from "../../api/typesGenerated"
|
import { ProvisionerJobLog } from "../../api/typesGenerated"
|
||||||
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
|
||||||
import { Logs } from "../Logs/Logs"
|
import { Logs } from "../Logs/Logs"
|
||||||
import { Theme } from "@mui/material/styles"
|
import Box from "@mui/material/Box"
|
||||||
|
|
||||||
const Language = {
|
const Language = {
|
||||||
seconds: "seconds",
|
seconds: "seconds",
|
||||||
@ -38,26 +38,30 @@ const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => {
|
|||||||
return completedAt.diff(startedAt, "seconds")
|
return completedAt.diff(startedAt, "seconds")
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkspaceBuildLogsProps {
|
export type WorkspaceBuildLogsProps = {
|
||||||
logs: ProvisionerJobLog[]
|
logs: ProvisionerJobLog[]
|
||||||
hideTimestamps?: boolean
|
hideTimestamps?: boolean
|
||||||
|
} & ComponentProps<typeof Box>
|
||||||
// If true, render different styles that fit the template editor pane
|
|
||||||
// a bit better.
|
|
||||||
templateEditorPane?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
||||||
hideTimestamps,
|
hideTimestamps,
|
||||||
logs,
|
logs,
|
||||||
templateEditorPane,
|
...boxProps
|
||||||
}) => {
|
}) => {
|
||||||
const groupedLogsByStage = groupLogsByStage(logs)
|
const groupedLogsByStage = groupLogsByStage(logs)
|
||||||
const stages = Object.keys(groupedLogsByStage)
|
const stages = Object.keys(groupedLogsByStage)
|
||||||
const styles = useStyles({ templateEditorPane: Boolean(templateEditorPane) })
|
const styles = useStyles()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.logs}>
|
<Box
|
||||||
|
{...boxProps}
|
||||||
|
sx={{
|
||||||
|
border: (theme) => `1px solid ${theme.palette.divider}`,
|
||||||
|
borderRadius: 1,
|
||||||
|
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||||
|
...boxProps.sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{stages.map((stage) => {
|
{stages.map((stage) => {
|
||||||
const logs = groupedLogsByStage[stage]
|
const logs = groupedLogsByStage[stage]
|
||||||
const isEmpty = logs.every((log) => log.output === "")
|
const isEmpty = logs.every((log) => log.output === "")
|
||||||
@ -83,56 +87,36 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
|
|||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles<
|
const useStyles = makeStyles((theme) => ({
|
||||||
Theme,
|
|
||||||
{
|
|
||||||
templateEditorPane: boolean
|
|
||||||
}
|
|
||||||
>((theme) => ({
|
|
||||||
logs: {
|
|
||||||
border: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderRadius: (props) =>
|
|
||||||
props.templateEditorPane ? "0px" : theme.shape.borderRadius,
|
|
||||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
|
||||||
},
|
|
||||||
|
|
||||||
header: {
|
header: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
padding: theme.spacing(2),
|
fontWeight: 600,
|
||||||
paddingLeft: theme.spacing(3),
|
padding: theme.spacing(0.5, 3),
|
||||||
paddingRight: theme.spacing(3),
|
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
|
||||||
borderBottom: `1px solid ${theme.palette.divider}`,
|
|
||||||
backgroundColor: theme.palette.background.paper,
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
fontFamily: "Inter",
|
fontFamily: "Inter",
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
"&:first-of-type": {
|
position: "sticky",
|
||||||
borderTopLeftRadius: theme.shape.borderRadius,
|
top: 0,
|
||||||
borderTopRightRadius: theme.shape.borderRadius,
|
background: theme.palette.background.default,
|
||||||
borderTop: 0,
|
|
||||||
},
|
|
||||||
|
|
||||||
"&:last-child": {
|
"&:last-child": {
|
||||||
borderBottom: 0,
|
borderBottom: 0,
|
||||||
borderTop: `1px solid ${theme.palette.divider}`,
|
borderRadius: "0 0 8px 8px",
|
||||||
borderBottomLeftRadius: theme.shape.borderRadius,
|
|
||||||
borderBottomRightRadius: theme.shape.borderRadius,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"& + $header": {
|
"&:first-child": {
|
||||||
borderTop: 0,
|
borderRadius: "8px 8px 0 0",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
duration: {
|
duration: {
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
color: theme.palette.text.secondary,
|
color: theme.palette.text.secondary,
|
||||||
fontSize: theme.typography.body2.fontSize,
|
fontSize: 12,
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
|
111
site/src/contexts/LocalPreferencesContext.tsx
Normal file
111
site/src/contexts/LocalPreferencesContext.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import {
|
||||||
|
ReactNode,
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from "react"
|
||||||
|
|
||||||
|
const LOCAL_PREFERENCES_KEY = "local-preferences"
|
||||||
|
|
||||||
|
const defaultValues = {
|
||||||
|
buildLogsVisibility: "visible" as "visible" | "hide",
|
||||||
|
}
|
||||||
|
|
||||||
|
type LocalPreferencesValues = typeof defaultValues
|
||||||
|
type LocalPreference = keyof LocalPreferencesValues
|
||||||
|
type LocalPreferenceContextValues = {
|
||||||
|
values: LocalPreferencesValues
|
||||||
|
getPreference: (
|
||||||
|
name: LocalPreference,
|
||||||
|
) => LocalPreferencesValues[LocalPreference]
|
||||||
|
setPreference: (
|
||||||
|
name: LocalPreference,
|
||||||
|
value: LocalPreferencesValues[LocalPreference],
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const LocalPreferencesContext = createContext<
|
||||||
|
LocalPreferenceContextValues | undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
|
export const LocalPreferencesProvider = ({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
}) => {
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
ready: boolean
|
||||||
|
values: LocalPreferencesValues
|
||||||
|
}>({ ready: false, values: defaultValues })
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const preferencesStr = window.localStorage.getItem(LOCAL_PREFERENCES_KEY)
|
||||||
|
if (preferencesStr) {
|
||||||
|
try {
|
||||||
|
const values = JSON.parse(preferencesStr)
|
||||||
|
setState({ ...values, ready: true })
|
||||||
|
return
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(
|
||||||
|
"Error on parsing local preferences. Default values are used.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState((state) => ({ ...state, ready: true }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const getPreference: LocalPreferenceContextValues["getPreference"] =
|
||||||
|
useCallback(
|
||||||
|
(name) => {
|
||||||
|
return state.values[name]
|
||||||
|
},
|
||||||
|
[state.values],
|
||||||
|
)
|
||||||
|
|
||||||
|
const setPreference: LocalPreferenceContextValues["setPreference"] =
|
||||||
|
useCallback((name, value) => {
|
||||||
|
setState((state) => {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
values: {
|
||||||
|
...state.values,
|
||||||
|
[name]: value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
window.localStorage.setItem(
|
||||||
|
LOCAL_PREFERENCES_KEY,
|
||||||
|
JSON.stringify(newState),
|
||||||
|
)
|
||||||
|
return newState
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<LocalPreferencesContext.Provider
|
||||||
|
value={
|
||||||
|
state.ready
|
||||||
|
? {
|
||||||
|
values: state.values,
|
||||||
|
getPreference,
|
||||||
|
setPreference,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</LocalPreferencesContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useLocalPreferences = () => {
|
||||||
|
const context = useContext(LocalPreferencesContext)
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error(
|
||||||
|
"useLocalPreference must be used within a LocalPreferenceProvider",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
@ -1,10 +1,4 @@
|
|||||||
interface UseLocalStorage {
|
export const useLocalStorage = () => {
|
||||||
saveLocal: (arg0: string, arg1: string) => void
|
|
||||||
getLocal: (arg0: string) => string | undefined
|
|
||||||
clearLocal: (arg0: string) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLocalStorage = (): UseLocalStorage => {
|
|
||||||
return {
|
return {
|
||||||
saveLocal,
|
saveLocal,
|
||||||
getLocal,
|
getLocal,
|
||||||
|
90
site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx
Normal file
90
site/src/pages/WorkspacePage/WorkspaceBuildLogsSection.tsx
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import CloseOutlined from "@mui/icons-material/CloseOutlined"
|
||||||
|
import Box from "@mui/material/Box"
|
||||||
|
import IconButton from "@mui/material/IconButton"
|
||||||
|
import Tooltip from "@mui/material/Tooltip"
|
||||||
|
import { ProvisionerJobLog } from "api/typesGenerated"
|
||||||
|
import { Loader } from "components/Loader/Loader"
|
||||||
|
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
|
||||||
|
import { useRef, useEffect } from "react"
|
||||||
|
|
||||||
|
export const WorkspaceBuildLogsSection = ({
|
||||||
|
logs,
|
||||||
|
onHide,
|
||||||
|
}: {
|
||||||
|
logs: ProvisionerJobLog[] | undefined
|
||||||
|
onHide?: () => void
|
||||||
|
}) => {
|
||||||
|
const scrollRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const scrollEl = scrollRef.current
|
||||||
|
if (scrollEl) {
|
||||||
|
scrollEl.scrollTop = scrollEl.scrollHeight
|
||||||
|
}
|
||||||
|
}, [logs])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
borderRadius: 1,
|
||||||
|
border: `1px solid ${theme.palette.divider}`,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={(theme) => ({
|
||||||
|
background: theme.palette.background.paper,
|
||||||
|
borderBottom: `1px solid ${theme.palette.divider}`,
|
||||||
|
padding: theme.spacing(1, 1, 1, 3),
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: 600,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
borderRadius: "8px 8px 0 0",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
Build logs
|
||||||
|
{onHide && (
|
||||||
|
<Box sx={{ marginLeft: "auto" }}>
|
||||||
|
<Tooltip title="Hide build logs" placement="top">
|
||||||
|
<IconButton
|
||||||
|
onClick={onHide}
|
||||||
|
size="small"
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
"&:hover": {
|
||||||
|
color: theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<CloseOutlined sx={{ height: 16, width: 16 }} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
ref={scrollRef}
|
||||||
|
sx={() => ({
|
||||||
|
height: "400px",
|
||||||
|
overflowY: "auto",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{logs ? (
|
||||||
|
<WorkspaceBuildLogs logs={logs} sx={{ border: 0, borderRadius: 0 }} />
|
||||||
|
) : (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Loader />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
@ -1,10 +1,7 @@
|
|||||||
import { useQuery } from "@tanstack/react-query"
|
|
||||||
import { useMachine } from "@xstate/react"
|
import { useMachine } from "@xstate/react"
|
||||||
import { getWorkspaceBuildLogs } from "api/api"
|
|
||||||
import { Workspace } from "api/typesGenerated"
|
|
||||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne"
|
||||||
import { Loader } from "components/Loader/Loader"
|
import { Loader } from "components/Loader/Loader"
|
||||||
import { FC, useRef } from "react"
|
import { FC } from "react"
|
||||||
import { useParams } from "react-router-dom"
|
import { useParams } from "react-router-dom"
|
||||||
import { quotaMachine } from "xServices/quotas/quotasXService"
|
import { quotaMachine } from "xServices/quotas/quotasXService"
|
||||||
import { workspaceMachine } from "xServices/workspace/workspaceXService"
|
import { workspaceMachine } from "xServices/workspace/workspaceXService"
|
||||||
@ -15,23 +12,6 @@ import { useOrganizationId } from "hooks"
|
|||||||
import { isAxiosError } from "axios"
|
import { isAxiosError } from "axios"
|
||||||
import { Margins } from "components/Margins/Margins"
|
import { Margins } from "components/Margins/Margins"
|
||||||
|
|
||||||
const useFailedBuildLogs = (workspace: Workspace | undefined) => {
|
|
||||||
const now = useRef(new Date())
|
|
||||||
return useQuery({
|
|
||||||
queryKey: ["logs", workspace?.latest_build.id],
|
|
||||||
queryFn: () => {
|
|
||||||
if (!workspace) {
|
|
||||||
throw new Error(
|
|
||||||
`Build log query being called before workspace is defined`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return getWorkspaceBuildLogs(workspace.latest_build.id, now.current)
|
|
||||||
},
|
|
||||||
enabled: workspace?.latest_build.job.error !== undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const WorkspacePage: FC = () => {
|
export const WorkspacePage: FC = () => {
|
||||||
const params = useParams() as {
|
const params = useParams() as {
|
||||||
username: string
|
username: string
|
||||||
@ -50,7 +30,6 @@ export const WorkspacePage: FC = () => {
|
|||||||
const { workspace, error } = workspaceState.context
|
const { workspace, error } = workspaceState.context
|
||||||
const [quotaState] = useMachine(quotaMachine, { context: { username } })
|
const [quotaState] = useMachine(quotaMachine, { context: { username } })
|
||||||
const { getQuotaError } = quotaState.context
|
const { getQuotaError } = quotaState.context
|
||||||
const failedBuildLogs = useFailedBuildLogs(workspace)
|
|
||||||
const pageError = error ?? getQuotaError
|
const pageError = error ?? getQuotaError
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -73,7 +52,6 @@ export const WorkspacePage: FC = () => {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<WorkspaceReadyPage
|
<WorkspaceReadyPage
|
||||||
failedBuildLogs={failedBuildLogs.data}
|
|
||||||
workspaceState={workspaceState}
|
workspaceState={workspaceState}
|
||||||
quotaState={quotaState}
|
quotaState={quotaState}
|
||||||
workspaceSend={workspaceSend}
|
workspaceSend={workspaceSend}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useActor } from "@xstate/react"
|
import { useActor, useMachine } from "@xstate/react"
|
||||||
import { ProvisionerJobLog } from "api/typesGenerated"
|
|
||||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { useFeatureVisibility } from "hooks/useFeatureVisibility"
|
import { useFeatureVisibility } from "hooks/useFeatureVisibility"
|
||||||
@ -21,7 +20,7 @@ import {
|
|||||||
WorkspaceErrors,
|
WorkspaceErrors,
|
||||||
} from "../../components/Workspace/Workspace"
|
} from "../../components/Workspace/Workspace"
|
||||||
import { pageTitle } from "../../utils/page"
|
import { pageTitle } from "../../utils/page"
|
||||||
import { getFaviconByStatus } from "../../utils/workspace"
|
import { getFaviconByStatus, hasJobError } from "../../utils/workspace"
|
||||||
import {
|
import {
|
||||||
WorkspaceEvent,
|
WorkspaceEvent,
|
||||||
workspaceMachine,
|
workspaceMachine,
|
||||||
@ -38,18 +37,20 @@ import {
|
|||||||
import { useMe } from "hooks/useMe"
|
import { useMe } from "hooks/useMe"
|
||||||
import Checkbox from "@mui/material/Checkbox"
|
import Checkbox from "@mui/material/Checkbox"
|
||||||
import FormControlLabel from "@mui/material/FormControlLabel"
|
import FormControlLabel from "@mui/material/FormControlLabel"
|
||||||
|
import { workspaceBuildMachine } from "xServices/workspaceBuild/workspaceBuildXService"
|
||||||
|
import * as TypesGen from "api/typesGenerated"
|
||||||
|
import { useLocalPreferences } from "contexts/LocalPreferencesContext"
|
||||||
|
import { WorkspaceBuildLogsSection } from "./WorkspaceBuildLogsSection"
|
||||||
|
|
||||||
interface WorkspaceReadyPageProps {
|
interface WorkspaceReadyPageProps {
|
||||||
workspaceState: StateFrom<typeof workspaceMachine>
|
workspaceState: StateFrom<typeof workspaceMachine>
|
||||||
quotaState: StateFrom<typeof quotaMachine>
|
quotaState: StateFrom<typeof quotaMachine>
|
||||||
workspaceSend: (event: WorkspaceEvent) => void
|
workspaceSend: (event: WorkspaceEvent) => void
|
||||||
failedBuildLogs: ProvisionerJobLog[] | undefined
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const WorkspaceReadyPage = ({
|
export const WorkspaceReadyPage = ({
|
||||||
workspaceState,
|
workspaceState,
|
||||||
quotaState,
|
quotaState,
|
||||||
failedBuildLogs,
|
|
||||||
workspaceSend,
|
workspaceSend,
|
||||||
}: WorkspaceReadyPageProps): JSX.Element => {
|
}: WorkspaceReadyPageProps): JSX.Element => {
|
||||||
const [_, bannerSend] = useActor(
|
const [_, bannerSend] = useActor(
|
||||||
@ -92,6 +93,17 @@ export const WorkspaceReadyPage = ({
|
|||||||
const [isConfirmingRestart, setIsConfirmingRestart] = useState(false)
|
const [isConfirmingRestart, setIsConfirmingRestart] = useState(false)
|
||||||
const user = useMe()
|
const user = useMe()
|
||||||
const { isWarningIgnored, ignoreWarning } = useIgnoreWarnings(user.id)
|
const { isWarningIgnored, ignoreWarning } = useIgnoreWarnings(user.id)
|
||||||
|
const buildLogs = useBuildLogs(workspace)
|
||||||
|
const localPreferences = useLocalPreferences()
|
||||||
|
const dashboard = useDashboard()
|
||||||
|
const canChangeBuildLogsVisibility = !hasJobError(workspace)
|
||||||
|
const isWorkspaceBuildLogsUIActive = dashboard.experiments.includes(
|
||||||
|
"workspace_build_logs_ui",
|
||||||
|
)
|
||||||
|
const shouldDisplayBuildLogs =
|
||||||
|
hasJobError(workspace) ||
|
||||||
|
(localPreferences.getPreference("buildLogsVisibility") === "visible" &&
|
||||||
|
isWorkspaceBuildLogsUIActive)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
mutate: restartWorkspace,
|
mutate: restartWorkspace,
|
||||||
@ -121,7 +133,6 @@ export const WorkspaceReadyPage = ({
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<Workspace
|
<Workspace
|
||||||
failedBuildLogs={failedBuildLogs}
|
|
||||||
scheduleProps={{
|
scheduleProps={{
|
||||||
onDeadlineMinus: (hours: number) => {
|
onDeadlineMinus: (hours: number) => {
|
||||||
bannerSend({
|
bannerSend({
|
||||||
@ -184,6 +195,20 @@ export const WorkspaceReadyPage = ({
|
|||||||
template={template}
|
template={template}
|
||||||
quota_budget={quotaState.context.quota?.budget}
|
quota_budget={quotaState.context.quota?.budget}
|
||||||
templateWarnings={templateVersion?.warnings}
|
templateWarnings={templateVersion?.warnings}
|
||||||
|
canChangeBuildLogsVisibility={canChangeBuildLogsVisibility}
|
||||||
|
isWorkspaceBuildLogsUIActive={isWorkspaceBuildLogsUIActive}
|
||||||
|
buildLogs={
|
||||||
|
shouldDisplayBuildLogs && (
|
||||||
|
<WorkspaceBuildLogsSection
|
||||||
|
logs={buildLogs}
|
||||||
|
onHide={() => {
|
||||||
|
if (canChangeBuildLogsVisibility) {
|
||||||
|
localPreferences.setPreference("buildLogsVisibility", "hide")
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<DeleteDialog
|
<DeleteDialog
|
||||||
entity="workspace"
|
entity="workspace"
|
||||||
@ -331,3 +356,22 @@ const WarningDialog: FC<
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useBuildLogs = (workspace: TypesGen.Workspace) => {
|
||||||
|
const buildNumber = workspace.latest_build.build_number.toString()
|
||||||
|
const [buildState, buildSend] = useMachine(workspaceBuildMachine, {
|
||||||
|
context: {
|
||||||
|
buildNumber,
|
||||||
|
username: workspace.owner_name,
|
||||||
|
workspaceName: workspace.name,
|
||||||
|
timeCursor: new Date(),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { logs } = buildState.context
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
buildSend({ type: "RESET", buildNumber, timeCursor: new Date() })
|
||||||
|
}, [buildNumber, buildSend])
|
||||||
|
|
||||||
|
return logs
|
||||||
|
}
|
||||||
|
@ -281,3 +281,7 @@ const getPendingWorkspaceStatusText = (
|
|||||||
const LoadingIcon = () => {
|
const LoadingIcon = () => {
|
||||||
return <CircularProgress size={10} style={{ color: "#FFF" }} />
|
return <CircularProgress size={10} style={{ color: "#FFF" }} />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const hasJobError = (workspace: TypesGen.Workspace) => {
|
||||||
|
return workspace.latest_build.job.error !== undefined
|
||||||
|
}
|
||||||
|
@ -24,6 +24,11 @@ type LogsEvent =
|
|||||||
| {
|
| {
|
||||||
type: "BUILD_DONE"
|
type: "BUILD_DONE"
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: "RESET"
|
||||||
|
buildNumber: string
|
||||||
|
timeCursor: Date
|
||||||
|
}
|
||||||
|
|
||||||
export const workspaceBuildMachine = createMachine(
|
export const workspaceBuildMachine = createMachine(
|
||||||
{
|
{
|
||||||
@ -43,6 +48,12 @@ export const workspaceBuildMachine = createMachine(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
initial: "gettingBuild",
|
initial: "gettingBuild",
|
||||||
|
on: {
|
||||||
|
RESET: {
|
||||||
|
target: "gettingBuild",
|
||||||
|
actions: ["resetContext"],
|
||||||
|
},
|
||||||
|
},
|
||||||
states: {
|
states: {
|
||||||
gettingBuild: {
|
gettingBuild: {
|
||||||
entry: "clearGetBuildError",
|
entry: "clearGetBuildError",
|
||||||
@ -96,6 +107,11 @@ export const workspaceBuildMachine = createMachine(
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
actions: {
|
actions: {
|
||||||
|
resetContext: assign({
|
||||||
|
buildNumber: (_, event) => event.buildNumber,
|
||||||
|
timeCursor: (_, event) => event.timeCursor,
|
||||||
|
logs: undefined,
|
||||||
|
}),
|
||||||
// Build ID
|
// Build ID
|
||||||
assignBuildId: assign({
|
assignBuildId: assign({
|
||||||
buildId: (_, event) => event.data.id,
|
buildId: (_, event) => event.data.id,
|
||||||
@ -134,8 +150,8 @@ export const workspaceBuildMachine = createMachine(
|
|||||||
if (!ctx.logs) {
|
if (!ctx.logs) {
|
||||||
throw new Error("logs must be set")
|
throw new Error("logs must be set")
|
||||||
}
|
}
|
||||||
|
const after =
|
||||||
const after = ctx.logs[ctx.logs.length - 1].id
|
ctx.logs.length > 0 ? ctx.logs[ctx.logs.length - 1].id : undefined
|
||||||
const socket = API.watchBuildLogsByBuildId(ctx.buildId, {
|
const socket = API.watchBuildLogsByBuildId(ctx.buildId, {
|
||||||
after,
|
after,
|
||||||
onMessage: (log) => {
|
onMessage: (log) => {
|
||||||
|
Reference in New Issue
Block a user