Skip to content

Commit

Permalink
refactor: Move schedule to the header (#2775)
Browse files Browse the repository at this point in the history
  • Loading branch information
BrunoQuaresma committed Jul 1, 2022
1 parent 22febc7 commit 2d0ea00
Show file tree
Hide file tree
Showing 8 changed files with 444 additions and 157 deletions.
40 changes: 17 additions & 23 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { makeStyles } from "@material-ui/core/styles"
import { FC } from "react"
import React, { FC } from "react"
import { useNavigate } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { BuildsTable } from "../BuildsTable/BuildsTable"
Expand All @@ -9,8 +9,8 @@ import { Resources } from "../Resources/Resources"
import { Stack } from "../Stack/Stack"
import { WorkspaceActions } from "../WorkspaceActions/WorkspaceActions"
import { WorkspaceDeletedBanner } from "../WorkspaceDeletedBanner/WorkspaceDeletedBanner"
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
import { WorkspaceScheduleBanner } from "../WorkspaceScheduleBanner/WorkspaceScheduleBanner"
import { WorkspaceScheduleButton } from "../WorkspaceScheduleButton/WorkspaceScheduleButton"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
import { WorkspaceStats } from "../WorkspaceStats/WorkspaceStats"

Expand Down Expand Up @@ -58,16 +58,22 @@ export const Workspace: FC<WorkspaceProps> = ({
return (
<Margins>
<PageHeader
className={styles.header}
actions={
<WorkspaceActions
workspace={workspace}
handleStart={handleStart}
handleStop={handleStop}
handleDelete={handleDelete}
handleUpdate={handleUpdate}
handleCancel={handleCancel}
/>
<Stack direction="row" spacing={1}>
<WorkspaceScheduleButton
workspace={workspace}
onDeadlineMinus={scheduleProps.onDeadlineMinus}
onDeadlinePlus={scheduleProps.onDeadlinePlus}
/>
<WorkspaceActions
workspace={workspace}
handleStart={handleStart}
handleStop={handleStop}
handleDelete={handleDelete}
handleUpdate={handleUpdate}
handleCancel={handleCancel}
/>
</Stack>
}
>
<PageHeaderTitle>{workspace.name}</PageHeaderTitle>
Expand Down Expand Up @@ -102,14 +108,6 @@ export const Workspace: FC<WorkspaceProps> = ({
<BuildsTable builds={builds} className={styles.timelineTable} />
</WorkspaceSection>
</Stack>

<Stack direction="column" className={styles.secondColumnSpacer} spacing={3}>
<WorkspaceSchedule
workspace={workspace}
onDeadlineMinus={scheduleProps.onDeadlineMinus}
onDeadlinePlus={scheduleProps.onDeadlinePlus}
/>
</Stack>
</Stack>
</Margins>
)
Expand All @@ -125,10 +123,6 @@ export const useStyles = makeStyles((theme) => {
secondColumnSpacer: {
flex: `0 0 ${spacerWidth}px`,
},
header: {
// 100% - (the size of sidebar + the space between both )
maxWidth: `calc(100% - (${spacerWidth}px + ${theme.spacing(3)}px))`,
},
layout: {
alignItems: "flex-start",
},
Expand Down
152 changes: 20 additions & 132 deletions site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import IconButton from "@material-ui/core/IconButton"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import Tooltip from "@material-ui/core/Tooltip"
import Typography from "@material-ui/core/Typography"
import AddBoxIcon from "@material-ui/icons/AddBox"
import IndeterminateCheckBoxIcon from "@material-ui/icons/IndeterminateCheckBox"
import ScheduleIcon from "@material-ui/icons/Schedule"
import cronstrue from "cronstrue"
import dayjs from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat"
import duration from "dayjs/plugin/duration"
Expand All @@ -17,8 +10,12 @@ import { FC } from "react"
import { Link as RouterLink } from "react-router-dom"
import { Workspace } from "../../api/typesGenerated"
import { MONOSPACE_FONT_FAMILY } from "../../theme/constants"
import { extractTimezone, stripTimezone } from "../../util/schedule"
import { isWorkspaceOn } from "../../util/workspace"
import {
autoStartDisplay,
autoStopDisplay,
extractTimezone,
Language as ScheduleLanguage,
} from "../../util/schedule"
import { Stack } from "../Stack/Stack"

// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
Expand All @@ -30,133 +27,39 @@ dayjs.extend(relativeTime)
dayjs.extend(timezone)

export const Language = {
autoStartDisplay: (schedule: string | undefined): string => {
if (schedule) {
return cronstrue.toString(stripTimezone(schedule), { throwExceptionOnParseError: false })
} else {
return "Manual"
}
},
autoStartLabel: "START",
autoStopLabel: "SHUTDOWN",
autoStopDisplay: (workspace: Workspace): string => {
const deadline = dayjs(workspace.latest_build.deadline).utc()
// a manual shutdown has a deadline of '"0001-01-01T00:00:00Z"'
// SEE: #1834
const hasDeadline = deadline.year() > 1
const ttl = workspace.ttl_ms

if (isWorkspaceOn(workspace) && hasDeadline) {
// Workspace is on --> derive from latest_build.deadline. Note that the
// user may modify their workspace object (ttl) while the workspace is
// running and depending on system semantics, the deadline may still
// represent the previously defined ttl. Thus, we always derive from the
// deadline as the source of truth.
const now = dayjs().utc()
if (now.isAfter(deadline)) {
return "Workspace is shutting down"
} else {
return deadline.tz(dayjs.tz.guess()).format("MMM D, YYYY h:mm A")
}
} else if (!ttl || ttl < 1) {
// If the workspace is not on, and the ttl is 0 or undefined, then the
// workspace is set to manually shutdown.
return "Manual"
} else {
// The workspace has a ttl set, but is either in an unknown state or is
// not running. Therefore, we derive from workspace.ttl.
const duration = dayjs.duration(ttl, "milliseconds")
return `${duration.humanize()} after start`
}
},
editScheduleLink: "Edit schedule",
editDeadlineMinus: "Subtract one hour",
editDeadlinePlus: "Add one hour",
scheduleHeader: (workspace: Workspace): string => {
const tz = workspace.autostart_schedule
? extractTimezone(workspace.autostart_schedule)
: dayjs.tz.guess()
return `Schedule (${tz})`
},
timezoneLabel: "Timezone",
}

export interface WorkspaceScheduleProps {
now?: dayjs.Dayjs
workspace: Workspace
onDeadlinePlus: () => void
onDeadlineMinus: () => void
}

export const shouldDisplayPlusMinus = (workspace: Workspace): boolean => {
if (!isWorkspaceOn(workspace)) {
return false
}
const deadline = dayjs(workspace.latest_build.deadline).utc()
return deadline.year() > 1
}

export const deadlineMinusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => {
const delta = dayjs(workspace.latest_build.deadline).diff(now)
return delta <= 30 * 60 * 1000 // 30 minutes
}

export const deadlinePlusDisabled = (workspace: Workspace, now: dayjs.Dayjs): boolean => {
const delta = dayjs(workspace.latest_build.deadline).diff(now)
return delta >= 24 * 60 * 60 * 1000 // 24 hours
}

export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({
now,
workspace,
onDeadlineMinus,
onDeadlinePlus,
}) => {
export const WorkspaceSchedule: FC<WorkspaceScheduleProps> = ({ workspace }) => {
const styles = useStyles()
const editDeadlineButtons = shouldDisplayPlusMinus(workspace) ? (
<Stack direction="row" spacing={0}>
<IconButton
size="small"
disabled={deadlineMinusDisabled(workspace, now ?? dayjs())}
className={styles.editDeadline}
onClick={onDeadlineMinus}
>
<Tooltip title={Language.editDeadlineMinus}>
<IndeterminateCheckBoxIcon />
</Tooltip>
</IconButton>
<IconButton
size="small"
disabled={deadlinePlusDisabled(workspace, now ?? dayjs())}
className={styles.editDeadline}
onClick={onDeadlinePlus}
>
<Tooltip title={Language.editDeadlinePlus}>
<AddBoxIcon />
</Tooltip>
</IconButton>
</Stack>
) : null
const timezone = workspace.autostart_schedule
? extractTimezone(workspace.autostart_schedule)
: dayjs.tz.guess()

return (
<div className={styles.schedule}>
<Stack spacing={2}>
<Typography variant="body1" className={styles.title}>
<ScheduleIcon className={styles.scheduleIcon} />
{Language.scheduleHeader(workspace)}
</Typography>
<div>
<span className={styles.scheduleLabel}>{Language.autoStartLabel}</span>
<span className={styles.scheduleLabel}>{Language.timezoneLabel}</span>
<span className={styles.scheduleValue}>{timezone}</span>
</div>
<div>
<span className={styles.scheduleLabel}>{ScheduleLanguage.autoStartLabel}</span>
<span className={[styles.scheduleValue, "chromatic-ignore"].join(" ")}>
{Language.autoStartDisplay(workspace.autostart_schedule)}
{autoStartDisplay(workspace.autostart_schedule)}
</span>
</div>
<div>
<span className={styles.scheduleLabel}>{Language.autoStopLabel}</span>
<span className={styles.scheduleLabel}>{ScheduleLanguage.autoStopLabel}</span>
<Stack direction="row">
<span className={[styles.scheduleValue, "chromatic-ignore"].join(" ")}>
{Language.autoStopDisplay(workspace)}
{autoStopDisplay(workspace)}
</span>
{editDeadlineButtons}
</Stack>
</div>
<div>
Expand All @@ -177,18 +80,6 @@ const useStyles = makeStyles((theme) => ({
schedule: {
fontFamily: MONOSPACE_FONT_FAMILY,
},
title: {
fontWeight: 600,

fontFamily: "inherit",
display: "flex",
alignItems: "center",
},
scheduleIcon: {
width: 16,
height: 16,
marginRight: theme.spacing(1),
},
scheduleLabel: {
fontSize: 12,
textTransform: "uppercase",
Expand All @@ -198,14 +89,11 @@ const useStyles = makeStyles((theme) => ({
},
scheduleValue: {
fontSize: 14,
marginTop: theme.spacing(0.75),
marginTop: theme.spacing(0.5),
display: "inline-block",
color: theme.palette.text.secondary,
},
scheduleAction: {
cursor: "pointer",
},
editDeadline: {
color: theme.palette.text.secondary,
},
}))

0 comments on commit 2d0ea00

Please sign in to comment.