Skip to content

feat: add template editor to the ui #5963

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 23 commits into from
Feb 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Improve UX
  • Loading branch information
kylecarbs committed Feb 5, 2023
commit a315d7874fc642e991d2dc3cb4566d69f85cf668
1 change: 1 addition & 0 deletions site/src/components/TemplateLayout/TemplateLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { Avatar } from "components/Avatar/Avatar"

const Language = {
settingsButton: "Settings",
editButton: "Edit",
createButton: "Create workspace",
noDescription: "",
}
Expand Down
109 changes: 90 additions & 19 deletions site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import Tab from "@material-ui/core/Tab"
import Tabs from "@material-ui/core/Tabs"
import Tooltip from "@material-ui/core/Tooltip"
import CreateIcon from "@material-ui/icons/AddBox"
import BuildIcon from "@material-ui/icons/BuildOutlined"
import PreviewIcon from "@material-ui/icons/Visibility"
import {
ProvisionerJobLog,
Template,
Expand All @@ -15,7 +17,7 @@ import { Avatar } from "components/Avatar/Avatar"
import { AvatarData } from "components/AvatarData/AvatarData"
import { TemplateResourcesTable } from "components/TemplateResourcesTable/TemplateResourcesTable"
import { WorkspaceBuildLogs } from "components/WorkspaceBuildLogs/WorkspaceBuildLogs"
import { FC, useCallback, useEffect, useState } from "react"
import { FC, useCallback, useEffect, useRef, useState } from "react"
import { navHeight } from "theme/constants"
import { TemplateVersionFiles } from "util/templateVersion"
import {
Expand All @@ -25,6 +27,10 @@ import {
} from "./FileDialog"
import { FileTree } from "./FileTree"
import { MonacoEditor } from "./MonacoEditor"
import {
getStatus,
TemplateVersionStatusBadge,
} from "./TemplateVersionStatusBadge"

interface File {
path: string
Expand Down Expand Up @@ -103,6 +109,23 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
document.removeEventListener("keydown", keyListener)
}
}, [files, triggerPreview])

// Automatically switch to the template preview tab when the build succeeds.
const previousVersion = useRef<TemplateVersion>()
useEffect(() => {
if (!previousVersion.current) {
previousVersion.current = templateVersion
return
}
if (
previousVersion.current.job.status === "running" &&
templateVersion.job.status === "succeeded"
) {
setSelectedTab(1)
}
previousVersion.current = templateVersion
}, [templateVersion])

const hasIcon = template.icon && template.icon !== ""
const templateVersionSucceeded = templateVersion.job.status === "succeeded"
const styles = useStyles({
Expand All @@ -126,6 +149,11 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
</div>

<div className={styles.topbarSides}>
<div>
Build Status:
<TemplateVersionStatusBadge version={templateVersion} />
</div>

<Button
size="small"
variant="outlined"
Expand Down Expand Up @@ -263,29 +291,47 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
</div>

<div className={styles.panelWrapper}>
<Tabs
value={selectedTab}
onChange={(_, value) => {
setSelectedTab(value)
}}
className={styles.tabs}
>
<Tab
style={{ minWidth: 120 }}
label={<div>Build Log {templateVersion.job.status}</div>}
/>
<div className={styles.tabs}>
<button
className={`${styles.tab} ${selectedTab === 0 ? "active" : ""}`}
onClick={() => {
setSelectedTab(0)
}}
>
{templateVersion.job.status !== "succeeded" ? (
getStatus(templateVersion).icon
) : (
<BuildIcon />
)}
Build Log
</button>

{!disableUpdate && (
<Tab style={{ minWidth: 120 }} label="Preview" />
<button
className={`${styles.tab} ${
selectedTab === 1 ? "active" : ""
}`}
onClick={() => {
setSelectedTab(1)
}}
>
<PreviewIcon />
Workspace Preview
</button>
)}
</Tabs>
</div>

<div
className={`${styles.panel} ${styles.buildLogs} ${
selectedTab === 0 ? "" : "hidden"
}`}
>
{buildLogs && (
<WorkspaceBuildLogs hideTimestamps logs={buildLogs} />
<WorkspaceBuildLogs
templateEditorPane
hideTimestamps
logs={buildLogs}
/>
)}
{templateVersion.job.error && (
<div className={styles.buildLogError}>
Expand Down Expand Up @@ -360,9 +406,9 @@ const useStyles = makeStyles<
color: theme.palette.text.hint,
},
editorPane: {
display: "flex",
flexDirection: "row",
flex: 1,
display: "grid",
width: "100%",
gridTemplateColumns: "0.6fr 0.4fr",
height: `calc(100vh - ${navHeight + topbarHeight}px)`,
overflow: "hidden",
},
Expand All @@ -383,6 +429,8 @@ const useStyles = makeStyles<
},
tabs: {
borderBottom: `1px solid ${theme.palette.divider}`,
display: "flex",
boxShadow: "#000000 0 6px 6px -6px inset",

"& .MuiTab-root": {
padding: 0,
Expand All @@ -391,6 +439,30 @@ const useStyles = makeStyles<
letterSpacing: "unset",
},
},
tab: {
cursor: "pointer",
padding: "12px 24px",
fontSize: 14,
background: "transparent",
fontFamily: "inherit",
border: 0,
color: theme.palette.text.hint,
transition: "150ms ease all",
display: "flex",
gap: 8,
alignItems: "center",
justifyContent: "center",

"& svg": {
maxWidth: 16,
maxHeight: 16,
},

"&.active": {
color: "white",
background: theme.palette.background.paperLight,
},
},
tabBar: {
padding: "8px 16px",
position: "sticky",
Expand All @@ -408,7 +480,6 @@ const useStyles = makeStyles<
buildLogs: {
display: "flex",
flexDirection: "column-reverse",
padding: 16,
overflowY: "auto",
},
buildLogError: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { TemplateVersion } from "api/typesGenerated"
import { FC, ReactNode } from "react"
import { PaletteIndex } from "theme/palettes"

import CircularProgress from "@material-ui/core/CircularProgress"
import ErrorIcon from "@material-ui/icons/ErrorOutline"
import CheckIcon from "@material-ui/icons/CheckOutlined"
import { Pill } from "components/Pill/Pill"

export const TemplateVersionStatusBadge: FC<{
version: TemplateVersion
}> = ({ version }) => {
const { text, icon, type } = getStatus(version)
return <Pill icon={icon} text={text} type={type} />
}

const LoadingIcon: FC = () => {
return <CircularProgress size={10} style={{ color: "#FFF" }} />
}

export const getStatus = (
version: TemplateVersion,
): {
type?: PaletteIndex
text: string
icon: ReactNode
} => {
console.log("Getting status", version.job.status)
switch (version.job.status) {
case "running":
return {
type: "info",
text: "Running",
icon: <LoadingIcon />,
}
case "pending":
return {
text: "Pending",
icon: <LoadingIcon />,
type: "info",
}
case "canceling":
return {
type: "warning",
text: "Canceling",
icon: <LoadingIcon />,
}
case "canceled":
return {
type: "warning",
text: "Canceled",
icon: <ErrorIcon />,
}
case "failed":
return {
type: "error",
text: "Failed",
icon: <ErrorIcon />,
}
case "succeeded":
return {
type: "success",
text: "Success",
icon: <CheckIcon />,
}
}
}
15 changes: 11 additions & 4 deletions site/src/components/WorkspaceBuildLogs/WorkspaceBuildLogs.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { makeStyles } from "@material-ui/core/styles"
import { makeStyles, Theme } from "@material-ui/core/styles"
import dayjs from "dayjs"
import { FC, Fragment } from "react"
import { ProvisionerJobLog } from "../../api/typesGenerated"
Expand Down Expand Up @@ -40,15 +40,20 @@ const getStageDurationInSeconds = (logs: ProvisionerJobLog[]) => {
export interface WorkspaceBuildLogsProps {
logs: ProvisionerJobLog[]
hideTimestamps?: boolean

// If true, render different styles that fit the template editor pane
// a bit better.
templateEditorPane?: boolean
}

export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
hideTimestamps,
logs,
templateEditorPane,
}) => {
const groupedLogsByStage = groupLogsByStage(logs)
const stages = Object.keys(groupedLogsByStage)
const styles = useStyles()
const styles = useStyles({ templateEditorPane })

return (
<div className={styles.logs}>
Expand Down Expand Up @@ -81,10 +86,12 @@ export const WorkspaceBuildLogs: FC<WorkspaceBuildLogsProps> = ({
)
}

const useStyles = makeStyles((theme) => ({
const useStyles = makeStyles<Theme, {
templateEditorPane: boolean
}>((theme) => ({
logs: {
border: `1px solid ${theme.palette.divider}`,
borderRadius: theme.shape.borderRadius,
borderRadius: (props) => props.templateEditorPane ? "0px" : theme.shape.borderRadius,
fontFamily: MONOSPACE_FONT_FAMILY,
},

Expand Down
19 changes: 16 additions & 3 deletions site/src/pages/TemplateVersionPage/TemplateVersionPageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { MarkdownIcon } from "components/Icons/MarkdownIcon"
import { TerraformIcon } from "components/Icons/TerraformIcon"
import { Loader } from "components/Loader/Loader"
import { Margins } from "components/Margins/Margins"
import EditIcon from "@material-ui/icons/Edit"
import {
PageHeader,
PageHeaderCaption,
Expand All @@ -13,14 +14,16 @@ import { Stack } from "components/Stack/Stack"
import { Stats, StatsItem } from "components/Stats/Stats"
import { UseTabResult } from "hooks/useTab"
import { FC } from "react"
import { Link as RouterLink } from "react-router-dom"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { combineClasses } from "util/combineClasses"
import { createDayString } from "util/createDayString"
import { TemplateVersionMachineContext } from "xServices/templateVersion/templateVersionXService"
import { TemplateVersionFiles } from "util/templateVersion"
import { SyntaxHighlighter } from "components/SyntaxHighlighter/SyntaxHighlighter"
import { DockerIcon } from "components/Icons/DockerIcon"
import Button from "@material-ui/core/Button"
import Link from "@material-ui/core/Link"

const iconByExtension: Record<string, JSX.Element> = {
tf: <TerraformIcon />,
Expand Down Expand Up @@ -116,7 +119,17 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({

return (
<Margins>
<PageHeader>
<PageHeader
actions={
<Link
underline="none"
component={RouterLink}
to={`/templates/${templateName}/versions/${versionName}/edit`}
>
<Button startIcon={<EditIcon />}>Edit</Button>
</Link>
}
>
<PageHeaderCaption>{t("header.caption")}</PageHeaderCaption>
<PageHeaderTitle>{versionName}</PageHeaderTitle>
</PageHeader>
Expand All @@ -131,7 +144,7 @@ export const TemplateVersionPageView: FC<TemplateVersionPageViewProps> = ({
<StatsItem
label={t("stats.template")}
value={
<Link to={`/templates/${templateName}`}>{templateName}</Link>
<RouterLink to={`/templates/${templateName}`}>{templateName}</RouterLink>
}
/>
<StatsItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,22 @@ export const templateVersionEditorMachine = createMachine(
const previousLogs = context.buildLogs ?? []
return [...previousLogs, event.log]
},
// Instead of periodically fetching the version,
// we just assume the state is running after the first log.
//
// The machine fetches the version after the log stream ends anyways!
version: (context) => {
if (!context.version || context.buildLogs?.length !== 0) {
return context.version
}
return {
...context.version,
job: {
...context.version.job,
status: "running",
},
}
}
}),
},
services: {
Expand Down