Skip to content

feat: Add timeline in the workspace page #1533

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 18, 2022
5 changes: 5 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,3 +221,8 @@ export const regenerateUserSSHKey = async (userId = "me"): Promise<TypesGen.GitS
const response = await axios.put<TypesGen.GitSSHKey>(`/api/v2/users/${userId}/gitsshkey`)
return response.data
}

export const getWorkspaceBuilds = async (workspaceId: string): Promise<TypesGen.WorkspaceBuild[]> => {
const response = await axios.get<TypesGen.WorkspaceBuild[]>(`/api/v2/workspaces/${workspaceId}/builds`)
return response.data
}
103 changes: 103 additions & 0 deletions site/src/components/BuildsTable/BuildsTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import Box from "@material-ui/core/Box"
import { Theme } from "@material-ui/core/styles"
import Table from "@material-ui/core/Table"
import TableBody from "@material-ui/core/TableBody"
import TableCell from "@material-ui/core/TableCell"
import TableHead from "@material-ui/core/TableHead"
import TableRow from "@material-ui/core/TableRow"
import useTheme from "@material-ui/styles/useTheme"
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
import relativeTime from "dayjs/plugin/relativeTime"
import React from "react"
import * as TypesGen from "../../api/typesGenerated"
import { getDisplayStatus } from "../../util/workspace"
import { EmptyState } from "../EmptyState/EmptyState"
import { TableLoader } from "../TableLoader/TableLoader"

dayjs.extend(relativeTime)
dayjs.extend(duration)

export const Language = {
pageTitle: "Builds",
usersTitle: "All users",
emptyMessage: "No users found",
usernameLabel: "User",
suspendMenuItem: "Suspend",
resetPasswordMenuItem: "Reset password",
rolesLabel: "Roles",
inProgressLabel: "In progress",
actionLabel: "Action",
durationLabel: "Duration",
startedAtLabel: "Started at",
statusLabel: "Status",
}

const getDurationInSeconds = (build: TypesGen.WorkspaceBuild) => {
let display = Language.inProgressLabel

if (build.job.started_at && build.job.completed_at) {
const startedAt = dayjs(build.job.started_at)
const completedAt = dayjs(build.job.completed_at)
const diff = completedAt.diff(startedAt, "seconds")
display = `${diff} seconds`
}

return display
}

export interface BuildsTableProps {
builds?: TypesGen.WorkspaceBuild[]
className?: string
}

export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) => {
const isLoading = !builds
const theme: Theme = useTheme()

return (
<Table className={className}>
<TableHead>
<TableRow>
<TableCell width="20%">{Language.actionLabel}</TableCell>
<TableCell width="20%">{Language.durationLabel}</TableCell>
<TableCell width="40%">{Language.startedAtLabel}</TableCell>
<TableCell width="20%">{Language.statusLabel}</TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading && <TableLoader />}
{builds &&
builds.map((b) => {
const status = getDisplayStatus(theme, b)
const duration = getDurationInSeconds(b)

return (
<TableRow key={b.id}>
<TableCell>{b.transition}</TableCell>
<TableCell>
<span style={{ color: theme.palette.text.secondary }}>{duration}</span>
</TableCell>
<TableCell>
<span style={{ color: theme.palette.text.secondary }}>{new Date(b.created_at).toLocaleString()}</span>
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used toLocaleString() because it is better to have, for logs at least, the format 5/17/2022, 2:06:25 PM instead of 50 seconds ago.

</TableCell>
<TableCell>
<span style={{ color: status.color }}>{status.status}</span>
</TableCell>
</TableRow>
)
})}

{builds && builds.length === 0 && (
<TableRow>
<TableCell colSpan={999}>
<Box p={4}>
<EmptyState message="No builds for this workspace" />
</Box>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
18 changes: 11 additions & 7 deletions site/src/components/Workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Typography from "@material-ui/core/Typography"
import React from "react"
import * as TypesGen from "../../api/typesGenerated"
import { WorkspaceStatus } from "../../util/workspace"
import { BuildsTable } from "../BuildsTable/BuildsTable"
import { WorkspaceSchedule } from "../WorkspaceSchedule/WorkspaceSchedule"
import { WorkspaceSection } from "../WorkspaceSection/WorkspaceSection"
import { WorkspaceStatusBar } from "../WorkspaceStatusBar/WorkspaceStatusBar"
Expand All @@ -16,6 +17,7 @@ export interface WorkspaceProps {
handleRetry: () => void
handleUpdate: () => void
workspaceStatus: WorkspaceStatus
builds?: TypesGen.WorkspaceBuild[]
}

/**
Expand All @@ -28,6 +30,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({
handleRetry,
handleUpdate,
workspaceStatus,
builds,
}) => {
const styles = useStyles()

Expand Down Expand Up @@ -56,13 +59,8 @@ export const Workspace: React.FC<WorkspaceProps> = ({
</WorkspaceSection>
</div>
<div className={styles.timelineContainer}>
<WorkspaceSection title="Timeline">
<div
className={styles.vertical}
style={{ justifyContent: "center", alignItems: "center", height: "300px" }}
>
<Placeholder />
</div>
<WorkspaceSection title="Timeline" contentsProps={{ className: styles.timelineContents }}>
<BuildsTable builds={builds} className={styles.timelineTable} />
</WorkspaceSection>
</div>
</div>
Expand Down Expand Up @@ -105,5 +103,11 @@ export const useStyles = makeStyles(() => {
timelineContainer: {
flex: 1,
},
timelineContents: {
margin: 0,
},
timelineTable: {
border: 0,
},
}
})
10 changes: 7 additions & 3 deletions site/src/components/WorkspaceSection/WorkspaceSection.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import Paper from "@material-ui/core/Paper"
import { makeStyles } from "@material-ui/core/styles"
import Typography from "@material-ui/core/Typography"
import React from "react"
import React, { HTMLProps } from "react"
import { CardPadding, CardRadius } from "../../theme/constants"
import { combineClasses } from "../../util/combineClasses"

export interface WorkspaceSectionProps {
title?: string
contentsProps?: HTMLProps<HTMLDivElement>
}

export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({ title, children }) => {
export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({ title, children, contentsProps }) => {
const styles = useStyles()

return (
Expand All @@ -21,7 +23,9 @@ export const WorkspaceSection: React.FC<WorkspaceSectionProps> = ({ title, child
</div>
)}

<div className={styles.contents}>{children}</div>
<div {...contentsProps} className={combineClasses([styles.contents, contentsProps?.className])}>
{children}
</div>
</Paper>
)
}
Expand Down
3 changes: 2 additions & 1 deletion site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export const WorkspacePage: React.FC = () => {

const xServices = useContext(XServiceContext)
const [workspaceState, workspaceSend] = useActor(xServices.workspaceXService)
const { workspace, getWorkspaceError, getTemplateError, getOrganizationError } = workspaceState.context
const { workspace, getWorkspaceError, getTemplateError, getOrganizationError, builds } = workspaceState.context
const workspaceStatus = useSelector(xServices.workspaceXService, (state) => {
return getWorkspaceStatus(state.context.workspace?.latest_build)
})
Expand Down Expand Up @@ -44,6 +44,7 @@ export const WorkspacePage: React.FC = () => {
handleRetry={() => workspaceSend("RETRY")}
handleUpdate={() => workspaceSend("UPDATE")}
workspaceStatus={workspaceStatus}
builds={builds}
/>
</Stack>
</Margins>
Expand Down
73 changes: 2 additions & 71 deletions site/src/pages/WorkspacesPage/WorkspacesPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ import relativeTime from "dayjs/plugin/relativeTime"
import React from "react"
import { Link as RouterLink } from "react-router-dom"
import * as TypesGen from "../../api/typesGenerated"
import { WorkspaceBuild } from "../../api/typesGenerated"
import { Margins } from "../../components/Margins/Margins"
import { Stack } from "../../components/Stack/Stack"
import { firstLetter } from "../../util/firstLetter"
import { getWorkspaceStatus } from "../../util/workspace"
import { getDisplayStatus } from "../../util/workspace"

dayjs.extend(relativeTime)

Expand Down Expand Up @@ -68,7 +67,7 @@ export const WorkspacesPageView: React.FC<WorkspacesPageViewProps> = (props) =>
</TableRow>
)}
{props.workspaces?.map((workspace) => {
const status = getStatus(theme, workspace.latest_build)
const status = getDisplayStatus(theme, workspace.latest_build)
return (
<TableRow key={workspace.id} className={styles.workspaceRow}>
<TableCell>
Expand Down Expand Up @@ -108,74 +107,6 @@ export const WorkspacesPageView: React.FC<WorkspacesPageViewProps> = (props) =>
)
}

const getStatus = (
theme: Theme,
build: WorkspaceBuild,
): {
color: string
status: string
} => {
const status = getWorkspaceStatus(build)
switch (status) {
case undefined:
return {
color: theme.palette.text.secondary,
status: "Loading...",
}
case "started":
return {
color: theme.palette.success.main,
status: "⦿ Running",
}
case "starting":
return {
color: theme.palette.success.main,
status: "⦿ Starting",
}
case "stopping":
return {
color: theme.palette.text.secondary,
status: "◍ Stopping",
}
case "stopped":
return {
color: theme.palette.text.secondary,
status: "◍ Stopped",
}
case "deleting":
return {
color: theme.palette.text.secondary,
status: "⦸ Deleting",
}
case "deleted":
return {
color: theme.palette.text.secondary,
status: "⦸ Deleted",
}
case "canceling":
return {
color: theme.palette.warning.light,
status: "◍ Canceling",
}
case "canceled":
return {
color: theme.palette.text.secondary,
status: "◍ Canceled",
}
case "error":
return {
color: theme.palette.error.main,
status: "ⓧ Failed",
}
case "queued":
return {
color: theme.palette.text.secondary,
status: "◍ Queued",
}
}
throw new Error("unknown status " + status)
}

const useStyles = makeStyles((theme) => ({
actions: {
marginTop: theme.spacing(3),
Expand Down
69 changes: 69 additions & 0 deletions site/src/util/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Theme } from "@material-ui/core/styles"
import { WorkspaceBuildTransition } from "../api/types"
import { WorkspaceBuild } from "../api/typesGenerated"

Expand Down Expand Up @@ -47,3 +48,71 @@ export const getWorkspaceStatus = (workspaceBuild?: WorkspaceBuild): WorkspaceSt
return "error"
}
}

export const getDisplayStatus = (
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Moved this to utils/workspaces so I could use it in the Timeline component.
  • I see we also have another getStatus function so I moved this with a new name. Probably we want to use only one, but I think it is better to make this on the design refactoring.

theme: Theme,
build: WorkspaceBuild,
): {
color: string
status: string
} => {
const status = getWorkspaceStatus(build)
switch (status) {
case undefined:
return {
color: theme.palette.text.secondary,
status: "Loading...",
}
case "started":
return {
color: theme.palette.success.main,
status: "⦿ Running",
}
case "starting":
return {
color: theme.palette.success.main,
status: "⦿ Starting",
}
case "stopping":
return {
color: theme.palette.text.secondary,
status: "◍ Stopping",
}
case "stopped":
return {
color: theme.palette.text.secondary,
status: "◍ Stopped",
}
case "deleting":
return {
color: theme.palette.text.secondary,
status: "⦸ Deleting",
}
case "deleted":
return {
color: theme.palette.text.secondary,
status: "⦸ Deleted",
}
case "canceling":
return {
color: theme.palette.warning.light,
status: "◍ Canceling",
}
case "canceled":
return {
color: theme.palette.text.secondary,
status: "◍ Canceled",
}
case "error":
return {
color: theme.palette.error.main,
status: "ⓧ Failed",
}
case "queued":
return {
color: theme.palette.text.secondary,
status: "◍ Queued",
}
}
throw new Error("unknown status " + status)
}
Loading