Files
coder/site/src/components/TemplateLayout/TemplateLayout.tsx
T

261 lines
6.7 KiB
TypeScript

import Avatar from "@material-ui/core/Avatar"
import Button from "@material-ui/core/Button"
import Link from "@material-ui/core/Link"
import { makeStyles } from "@material-ui/core/styles"
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
import { useMachine } from "@xstate/react"
import {
PageHeader,
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader"
import { useOrganizationId } from "hooks/useOrganizationId"
import { createContext, FC, Suspense, useContext } from "react"
import {
Link as RouterLink,
NavLink,
Outlet,
useParams,
} from "react-router-dom"
import { combineClasses } from "util/combineClasses"
import { firstLetter } from "util/firstLetter"
import {
TemplateContext,
templateMachine,
} from "xServices/template/templateXService"
import { Margins } from "components/Margins/Margins"
import { Stack } from "components/Stack/Stack"
import { Permissions } from "xServices/auth/authXService"
import { Loader } from "components/Loader/Loader"
import { usePermissions } from "hooks/usePermissions"
const Language = {
settingsButton: "Settings",
createButton: "Create workspace",
noDescription: "",
}
const useTemplateName = () => {
const { template } = useParams()
if (!template) {
throw new Error("No template found in the URL")
}
return template
}
type TemplateLayoutContextValue = {
context: TemplateContext
permissions?: Permissions
}
const TemplateLayoutContext = createContext<
TemplateLayoutContextValue | undefined
>(undefined)
export const useTemplateLayoutContext = (): TemplateLayoutContextValue => {
const context = useContext(TemplateLayoutContext)
if (!context) {
throw new Error(
"useTemplateLayoutContext only can be used inside of TemplateLayout",
)
}
return context
}
const TemplateSettingsButton: FC<{ templateName: string }> = ({
templateName,
}) => (
<Link
underline="none"
component={RouterLink}
to={`/templates/${templateName}/settings`}
>
<Button variant="outlined" startIcon={<SettingsOutlined />}>
{Language.settingsButton}
</Button>
</Link>
)
const CreateWorkspaceButton: FC<{
templateName: string
className?: string
}> = ({ templateName, className }) => (
<Link
underline="none"
component={RouterLink}
to={`/templates/${templateName}/workspace`}
>
<Button className={className ?? ""} startIcon={<AddCircleOutline />}>
{Language.createButton}
</Button>
</Link>
)
export const TemplateLayout: FC<{ children?: JSX.Element }> = ({
children = <Outlet />,
}) => {
const styles = useStyles()
const organizationId = useOrganizationId()
const templateName = useTemplateName()
const [templateState, _] = useMachine(templateMachine, {
context: {
templateName,
organizationId,
},
})
const { template, permissions: templatePermissions } = templateState.context
const permissions = usePermissions()
const hasIcon = template && template.icon && template.icon !== ""
if (!template) {
return <Loader />
}
const generatePageHeaderActions = (): JSX.Element[] => {
const pageActions: JSX.Element[] = []
if (templatePermissions?.canUpdateTemplate) {
pageActions.push(<TemplateSettingsButton templateName={template.name} />)
}
pageActions.push(<CreateWorkspaceButton templateName={template.name} />)
return pageActions
}
return (
<>
<Margins>
<PageHeader
actions={
<>
{generatePageHeaderActions().map((action, i) => (
<div key={i}>{action}</div>
))}
</>
}
>
<Stack direction="row" spacing={3} className={styles.pageTitle}>
<div>
{hasIcon ? (
<div className={styles.iconWrapper}>
<img src={template.icon} alt="" />
</div>
) : (
<Avatar className={styles.avatar}>
{firstLetter(template.name)}
</Avatar>
)}
</div>
<div>
<PageHeaderTitle>
{template.display_name.length > 0
? template.display_name
: template.name}
</PageHeaderTitle>
<PageHeaderSubtitle condensed>
{template.description === ""
? Language.noDescription
: template.description}
</PageHeaderSubtitle>
</div>
</Stack>
</PageHeader>
</Margins>
<div className={styles.tabs}>
<Margins>
<Stack direction="row" spacing={0.25}>
<NavLink
end
to={`/templates/${template.name}`}
className={({ isActive }) =>
combineClasses([
styles.tabItem,
isActive ? styles.tabItemActive : undefined,
])
}
>
Summary
</NavLink>
<NavLink
to={`/templates/${template.name}/permissions`}
className={({ isActive }) =>
combineClasses([
styles.tabItem,
isActive ? styles.tabItemActive : undefined,
])
}
>
Permissions
</NavLink>
</Stack>
</Margins>
</div>
<Margins>
<TemplateLayoutContext.Provider
value={{ permissions, context: templateState.context }}
>
<Suspense fallback={<Loader />}>{children}</Suspense>
</TemplateLayoutContext.Provider>
</Margins>
</>
)
}
export const useStyles = makeStyles((theme) => {
return {
pageTitle: {
alignItems: "center",
},
avatar: {
width: theme.spacing(6),
height: theme.spacing(6),
fontSize: theme.spacing(3),
},
iconWrapper: {
width: theme.spacing(6),
height: theme.spacing(6),
"& img": {
width: "100%",
},
},
tabs: {
borderBottom: `1px solid ${theme.palette.divider}`,
marginBottom: theme.spacing(5),
},
tabItem: {
textDecoration: "none",
color: theme.palette.text.secondary,
fontSize: 14,
display: "block",
padding: theme.spacing(0, 2, 2),
"&:hover": {
color: theme.palette.text.primary,
},
},
tabItemActive: {
color: theme.palette.text.primary,
position: "relative",
"&:before": {
content: `""`,
left: 0,
bottom: 0,
height: 2,
width: "100%",
background: theme.palette.secondary.dark,
position: "absolute",
},
},
}
})