refactor(site): Update workspace header (#7433)

This commit is contained in:
Bruno Quaresma
2023-05-05 15:02:21 +00:00
committed by GitHub
parent 7f02fa696a
commit 3632ac8c01
7 changed files with 293 additions and 149 deletions
@@ -0,0 +1,67 @@
import { makeStyles } from "@material-ui/core/styles"
import { FC, PropsWithChildren } from "react"
export const FullWidthPageHeader: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles()
return (
<header className={styles.header} data-testid="header">
{children}
</header>
)
}
export const PageHeaderActions: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles()
return <div className={styles.actions}>{children}</div>
}
export const PageHeaderTitle: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles()
return <h1 className={styles.title}>{children}</h1>
}
export const PageHeaderSubtitle: FC<PropsWithChildren> = ({ children }) => {
const styles = useStyles()
return <span className={styles.subtitle}>{children}</span>
}
const useStyles = makeStyles((theme) => ({
header: {
padding: theme.spacing(3),
background: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
display: "flex",
alignItems: "center",
gap: theme.spacing(6),
position: "sticky",
top: 0,
zIndex: 10,
flexWrap: "wrap",
[theme.breakpoints.down("md")]: {
position: "unset",
alignItems: "flex-start",
},
[theme.breakpoints.down("sm")]: {
flexDirection: "column",
},
},
actions: {
marginLeft: "auto",
[theme.breakpoints.down("sm")]: {
marginLeft: "unset",
},
},
title: {
fontSize: 18,
fontWeight: 500,
margin: 0,
},
subtitle: {
fontSize: 14,
color: theme.palette.text.secondary,
marginTop: theme.spacing(0.25),
display: "block",
},
}))
+18 -7
View File
@@ -1,19 +1,30 @@
import { makeStyles } from "@material-ui/core/styles"
import { ComponentProps, FC, PropsWithChildren } from "react"
import { combineClasses } from "utils/combineClasses"
export const Stats: FC<PropsWithChildren<ComponentProps<"div">>> = (props) => {
export const Stats: FC<ComponentProps<"div">> = (props) => {
const styles = useStyles()
return <div className={styles.stats} {...props} />
return (
<div
{...props}
className={combineClasses([styles.stats, props.className])}
/>
)
}
export const StatsItem: FC<{
label: string
value: string | number | JSX.Element
}> = ({ label, value }) => {
export const StatsItem: FC<
{
label: string
value: string | number | JSX.Element
} & ComponentProps<"div">
> = ({ label, value, ...divProps }) => {
const styles = useStyles()
return (
<div className={styles.statItem}>
<div
{...divProps}
className={combineClasses([styles.statItem, divProps.className])}
>
<span className={styles.statsLabel}>{label}:</span>
<span className={styles.statsValue}>{value}</span>
</div>
+113 -117
View File
@@ -8,7 +8,6 @@ import {
ActiveTransition,
WorkspaceBuildProgress,
} from "components/WorkspaceBuildProgress/WorkspaceBuildProgress"
import { WorkspaceStatusBadge } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
@@ -16,16 +15,17 @@ import * as TypesGen from "../../api/typesGenerated"
import { AlertBanner } from "../AlertBanner/AlertBanner"
import { BuildsTable } from "../BuildsTable/BuildsTable"
import { Margins } from "../Margins/Margins"
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "../PageHeader/PageHeader"
import { Resources } from "../Resources/Resources"
import { Stack } from "../Stack/Stack"
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"
import {
FullWidthPageHeader,
PageHeaderActions,
PageHeaderTitle,
PageHeaderSubtitle,
} from "components/PageHeader/FullWidthPageHeader"
export enum WorkspaceErrors {
GET_BUILDS_ERROR = "getBuildsError",
@@ -125,31 +125,11 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
transitionStats = ActiveTransition(template, workspace)
}
return (
<Margins>
<PageHeader
actions={
<Stack direction="row" spacing={1} className={styles.actions}>
<WorkspaceActions
workspaceStatus={workspace.latest_build.status}
isOutdated={workspace.outdated}
handleStart={handleStart}
handleStop={handleStop}
handleRestart={handleRestart}
handleDelete={handleDelete}
handleUpdate={handleUpdate}
handleCancel={handleCancel}
handleSettings={handleSettings}
handleChangeVersion={handleChangeVersion}
canChangeVersions={canChangeVersions}
isUpdating={isUpdating}
isRestarting={isRestarting}
/>
</Stack>
}
>
<>
<FullWidthPageHeader>
<Stack direction="row" spacing={3} alignItems="center">
<Avatar
size="xl"
size="md"
src={workspace.template_icon}
variant={workspace.template_icon ? "square" : undefined}
fitImage={Boolean(workspace.template_icon)}
@@ -157,32 +137,10 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
{workspace.name}
</Avatar>
<div>
<PageHeaderTitle>
{workspace.name}
<WorkspaceStatusBadge
build={workspace.latest_build}
className={styles.statusBadge}
/>
</PageHeaderTitle>
<PageHeaderSubtitle condensed>
{workspace.owner_name}
</PageHeaderSubtitle>
<PageHeaderTitle>{workspace.name}</PageHeaderTitle>
<PageHeaderSubtitle>{workspace.owner_name}</PageHeaderSubtitle>
</div>
</Stack>
</PageHeader>
<Stack
direction="column"
className={styles.firstColumnSpacer}
spacing={4}
>
{buildError}
{cancellationError}
<WorkspaceDeletedBanner
workspace={workspace}
handleClick={() => navigate(`/templates`)}
/>
<WorkspaceStats
workspace={workspace}
@@ -195,76 +153,110 @@ export const Workspace: FC<React.PropsWithChildren<WorkspaceProps>> = ({
onDeadlinePlus={scheduleProps.onDeadlinePlus}
/>
{failedBuildLogs && (
<Stack>
<AlertBanner severity="error">
<Stack
className={styles.fullWidth}
direction="row"
alignItems="center"
justifyContent="space-between"
>
<Stack spacing={0}>
<span>Workspace build failed</span>
<span className={styles.errorDetails}>
{workspace.latest_build.job.error}
</span>
</Stack>
<PageHeaderActions>
<WorkspaceActions
workspaceStatus={workspace.latest_build.status}
isOutdated={workspace.outdated}
handleStart={handleStart}
handleStop={handleStop}
handleRestart={handleRestart}
handleDelete={handleDelete}
handleUpdate={handleUpdate}
handleCancel={handleCancel}
handleSettings={handleSettings}
handleChangeVersion={handleChangeVersion}
canChangeVersions={canChangeVersions}
isUpdating={isUpdating}
isRestarting={isRestarting}
/>
</PageHeaderActions>
</FullWidthPageHeader>
{canUpdateTemplate && (
<div>
<Button
onClick={handleBuildRetry}
startIcon={<RefreshOutlined />}
size="small"
variant="outlined"
>
{t("actionButton.retryDebugMode")}
</Button>
</div>
)}
</Stack>
</AlertBanner>
<WorkspaceBuildLogs logs={failedBuildLogs} />
</Stack>
)}
<Margins className={styles.content}>
<Stack
direction="column"
className={styles.firstColumnSpacer}
spacing={4}
>
{buildError}
{cancellationError}
{transitionStats !== undefined && (
<WorkspaceBuildProgress
<WorkspaceDeletedBanner
workspace={workspace}
transitionStats={transitionStats}
handleClick={() => navigate(`/templates`)}
/>
)}
{typeof resources !== "undefined" && resources.length > 0 && (
<Resources
resources={resources}
agentRow={(agent) => (
<AgentRow
key={agent.id}
agent={agent}
workspace={workspace}
sshPrefix={sshPrefix}
showApps={canUpdateWorkspace}
hideSSHButton={hideSSHButton}
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
serverVersion={serverVersion}
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
/>
)}
/>
)}
{failedBuildLogs && (
<Stack>
<AlertBanner severity="error">
<Stack
className={styles.fullWidth}
direction="row"
alignItems="center"
justifyContent="space-between"
>
<Stack spacing={0}>
<span>Workspace build failed</span>
<span className={styles.errorDetails}>
{workspace.latest_build.job.error}
</span>
</Stack>
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
<AlertBanner
severity="error"
error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]}
/>
) : (
<BuildsTable builds={builds} />
)}
</Stack>
</Margins>
{canUpdateTemplate && (
<div>
<Button
onClick={handleBuildRetry}
startIcon={<RefreshOutlined />}
size="small"
variant="outlined"
>
{t("actionButton.retryDebugMode")}
</Button>
</div>
)}
</Stack>
</AlertBanner>
<WorkspaceBuildLogs logs={failedBuildLogs} />
</Stack>
)}
{transitionStats !== undefined && (
<WorkspaceBuildProgress
workspace={workspace}
transitionStats={transitionStats}
/>
)}
{typeof resources !== "undefined" && resources.length > 0 && (
<Resources
resources={resources}
agentRow={(agent) => (
<AgentRow
key={agent.id}
agent={agent}
workspace={workspace}
sshPrefix={sshPrefix}
showApps={canUpdateWorkspace}
hideSSHButton={hideSSHButton}
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
serverVersion={serverVersion}
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
/>
)}
/>
)}
{workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR] ? (
<AlertBanner
severity="error"
error={workspaceErrors[WorkspaceErrors.GET_BUILDS_ERROR]}
/>
) : (
<BuildsTable builds={builds} />
)}
</Stack>
</Margins>
</>
)
}
@@ -272,6 +264,10 @@ const spacerWidth = 300
export const useStyles = makeStyles((theme) => {
return {
content: {
marginTop: theme.spacing(4),
},
statusBadge: {
marginLeft: theme.spacing(2),
},
@@ -7,7 +7,6 @@ import ReplayIcon from "@material-ui/icons/Replay"
import { LoadingButton } from "components/LoadingButton/LoadingButton"
import { FC, PropsWithChildren } from "react"
import { useTranslation } from "react-i18next"
import { makeStyles } from "@material-ui/core/styles"
interface WorkspaceAction {
handleAction: () => void
@@ -17,15 +16,14 @@ export const UpdateButton: FC<PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const { t } = useTranslation("workspacePage")
const styles = useStyles()
return (
<Button
size="small"
data-testid="workspace-update-button"
variant="outlined"
startIcon={<CloudQueueIcon />}
onClick={handleAction}
className={styles.fixedWidth}
>
{t("actionButton.update")}
</Button>
@@ -36,14 +34,12 @@ export const StartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const { t } = useTranslation("workspacePage")
const styles = useStyles()
return (
<Button
variant="outlined"
startIcon={<PlayCircleOutlineIcon />}
onClick={handleAction}
className={styles.fixedWidth}
>
{t("actionButton.start")}
</Button>
@@ -54,14 +50,13 @@ export const StopButton: FC<PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const { t } = useTranslation("workspacePage")
const styles = useStyles()
return (
<Button
size="small"
variant="outlined"
startIcon={<CropSquareIcon />}
onClick={handleAction}
className={styles.fixedWidth}
>
{t("actionButton.stop")}
</Button>
@@ -72,14 +67,13 @@ export const RestartButton: FC<PropsWithChildren<WorkspaceAction>> = ({
handleAction,
}) => {
const { t } = useTranslation("workspacePage")
const styles = useStyles()
return (
<Button
size="small"
variant="outlined"
startIcon={<ReplayIcon />}
onClick={handleAction}
className={styles.fixedWidth}
>
{t("actionButton.restart")}
</Button>
@@ -104,7 +98,7 @@ export const DisabledButton: FC<PropsWithChildren<DisabledProps>> = ({
label,
}) => {
return (
<Button variant="outlined" disabled>
<Button variant="outlined" size="small" disabled>
{label}
</Button>
)
@@ -117,20 +111,12 @@ interface LoadingProps {
export const ActionLoadingButton: FC<PropsWithChildren<LoadingProps>> = ({
label,
}) => {
const styles = useStyles()
return (
<LoadingButton
loading
size="small"
variant="outlined"
loadingLabel={label}
className={styles.fixedWidth}
/>
)
}
const useStyles = makeStyles((theme) => ({
fixedWidth: {
// Make it fixed so the loading changes will not "flick" the UI
width: theme.spacing(16),
},
}))
@@ -1,5 +1,4 @@
import MenuItem from "@material-ui/core/MenuItem"
import Button from "@material-ui/core/Button"
import Menu from "@material-ui/core/Menu"
import { makeStyles } from "@material-ui/core/styles"
import MoreVertOutlined from "@material-ui/icons/MoreVertOutlined"
@@ -23,6 +22,7 @@ import {
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
import HistoryOutlined from "@material-ui/icons/HistoryOutlined"
import DeleteOutlined from "@material-ui/icons/DeleteOutlined"
import IconButton from "@material-ui/core/IconButton"
export interface WorkspaceActionsProps {
workspaceStatus: WorkspaceStatus
@@ -148,17 +148,18 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
))}
{canCancel && <CancelButton handleAction={handleCancel} />}
<div>
<Button
<IconButton
title="More options"
size="small"
data-testid="workspace-options-button"
aria-controls="workspace-options"
aria-haspopup="true"
variant="outlined"
disabled={!canAcceptJobs}
ref={menuTriggerRef}
onClick={() => setIsMenuOpen(true)}
>
<MoreVertOutlined />
</Button>
</IconButton>
<Menu
id="workspace-options"
anchorEl={menuTriggerRef.current}
@@ -189,6 +190,6 @@ const useStyles = makeStyles((theme) => ({
actions: {
display: "flex",
alignItems: "center",
gap: theme.spacing(2),
gap: theme.spacing(1.5),
},
}))
@@ -19,6 +19,7 @@ import AddIcon from "@material-ui/icons/AddOutlined"
import Popover from "@material-ui/core/Popover"
import TextField from "@material-ui/core/TextField"
import Button from "@material-ui/core/Button"
import { WorkspaceStatusText } from "components/WorkspaceStatusBadge/WorkspaceStatusBadge"
const Language = {
workspaceDetails: "Workspace Details",
@@ -67,8 +68,14 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
return (
<>
<Stats aria-label={Language.workspaceDetails}>
<Stats aria-label={Language.workspaceDetails} className={styles.stats}>
<StatsItem
className={styles.statsItem}
label="Status"
value={<WorkspaceStatusText build={workspace.latest_build} />}
/>
<StatsItem
className={styles.statsItem}
label={Language.templateLabel}
value={
<Link
@@ -80,6 +87,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
}
/>
<StatsItem
className={styles.statsItem}
label={Language.versionLabel}
value={
<>
@@ -100,6 +108,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
}
/>
<StatsItem
className={styles.statsItem}
label={Language.lastBuiltLabel}
value={
<>
@@ -110,6 +119,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
/>
{shouldDisplayScheduleLabel(workspace) && (
<StatsItem
className={styles.statsItem}
label={getScheduleLabel(workspace)}
value={
<span className={styles.scheduleValue}>
@@ -152,6 +162,7 @@ export const WorkspaceStats: FC<WorkspaceStatsProps> = ({
)}
{workspace.latest_build.daily_cost > 0 && (
<StatsItem
className={styles.statsItem}
label={Language.costLabel}
value={`${workspace.latest_build.daily_cost} ${
quota_budget ? `/ ${quota_budget}` : ""
@@ -296,6 +307,32 @@ const getScheduleLabel = (workspace: Workspace) => {
}
const useStyles = makeStyles((theme) => ({
stats: {
padding: 0,
border: 0,
gap: theme.spacing(6),
rowGap: theme.spacing(3),
flex: 1,
[theme.breakpoints.down("sm")]: {
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
gap: theme.spacing(1),
},
},
statsItem: {
flexDirection: "column",
gap: 0,
padding: 0,
"& > span:first-child": {
fontSize: 12,
fontWeight: 500,
},
},
scheduleValue: {
display: "flex",
alignItems: "center",
@@ -8,6 +8,8 @@ import { Pill } from "components/Pill/Pill"
import i18next from "i18next"
import { FC, ReactNode, PropsWithChildren } from "react"
import { PaletteIndex } from "theme/palettes"
import { makeStyles } from "@material-ui/core/styles"
import { combineClasses } from "utils/combineClasses"
const LoadingIcon: FC = () => {
return <CircularProgress size={10} style={{ color: "#FFF" }} />
@@ -102,3 +104,47 @@ export const WorkspaceStatusBadge: FC<
const { text, icon, type } = getStatus(build.status)
return <Pill className={className} icon={icon} text={text} type={type} />
}
export const WorkspaceStatusText: FC<
PropsWithChildren<WorkspaceStatusBadgeProps>
> = ({ build, className }) => {
const styles = useStyles()
const { text, type } = getStatus(build.status)
return (
<span
role="status"
className={combineClasses([
className,
styles.root,
styles[`type-${type}`],
])}
>
{text}
</span>
)
}
const useStyles = makeStyles((theme) => ({
root: { fontWeight: 600 },
"type-error": {
color: theme.palette.error.light,
},
"type-warning": {
color: theme.palette.warning.light,
},
"type-success": {
color: theme.palette.success.light,
},
"type-info": {
color: theme.palette.info.light,
},
"type-undefined": {
color: theme.palette.text.secondary,
},
"type-primary": {
color: theme.palette.text.primary,
},
"type-secondary": {
color: theme.palette.text.secondary,
},
}))