mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
261 lines
6.7 KiB
TypeScript
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",
|
|
},
|
|
},
|
|
}
|
|
})
|