Skip to content

Commit 44bcbde

Browse files
committed
Add base layout
1 parent 0af367a commit 44bcbde

File tree

14 files changed

+443
-310
lines changed

14 files changed

+443
-310
lines changed

site/src/AppRouter.tsx

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import { useSelector } from "@xstate/react"
22
import { FeatureNames } from "api/types"
33
import { RequirePermission } from "components/RequirePermission/RequirePermission"
4+
import { TemplateLayout } from "components/TemplateLayout/TemplateLayout"
45
import { SetupPage } from "pages/SetupPage/SetupPage"
6+
import TemplateCollaboratorsPage from "pages/TemplatePage/TemplateCollaboratorsPage/TemplateCollaboratorsPage"
7+
import TemplateSummaryPage from "pages/TemplatePage/TemplateSummaryPage/TemplateSummaryPage"
58
import { TemplateSettingsPage } from "pages/TemplateSettingsPage/TemplateSettingsPage"
69
import { FC, lazy, Suspense, useContext } from "react"
710
import { Route, Routes } from "react-router-dom"
@@ -33,7 +36,6 @@ const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
3336
const WorkspacesPage = lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
3437
const CreateWorkspacePage = lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
3538
const AuditPage = lazy(() => import("./pages/AuditPage/AuditPage"))
36-
const TemplatePage = lazy(() => import("./pages/TemplatePage/TemplatePage"))
3739

3840
export const AppRouter: FC = () => {
3941
const xServices = useContext(XServiceContext)
@@ -87,12 +89,22 @@ export const AppRouter: FC = () => {
8789

8890
<Route path=":template">
8991
<Route
90-
index
9192
element={
9293
<AuthAndFrame>
93-
<TemplatePage />
94+
<TemplateLayout />
9495
</AuthAndFrame>
9596
}
97+
>
98+
<Route index element={<TemplateSummaryPage />} />
99+
<Route path="collaborators" element={<TemplateCollaboratorsPage />} />
100+
</Route>
101+
<Route
102+
path="workspace"
103+
element={
104+
<RequireAuth>
105+
<CreateWorkspacePage />
106+
</RequireAuth>
107+
}
96108
/>
97109
<Route
98110
path="workspace"
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
import Avatar from "@material-ui/core/Avatar"
2+
import Button from "@material-ui/core/Button"
3+
import Link from "@material-ui/core/Link"
4+
import { makeStyles } from "@material-ui/core/styles"
5+
import AddCircleOutline from "@material-ui/icons/AddCircleOutline"
6+
import SettingsOutlined from "@material-ui/icons/SettingsOutlined"
7+
import { useMachine, useSelector } from "@xstate/react"
8+
import { DeleteDialog } from "components/Dialogs/DeleteDialog/DeleteDialog"
9+
import { DeleteButton } from "components/DropdownButton/ActionCtas"
10+
import { DropdownButton } from "components/DropdownButton/DropdownButton"
11+
import { Loader } from "components/Loader/Loader"
12+
import { PageHeader, PageHeaderSubtitle, PageHeaderTitle } from "components/PageHeader/PageHeader"
13+
import { useOrganizationId } from "hooks/useOrganizationId"
14+
import { FC, useContext } from "react"
15+
import { useTranslation } from "react-i18next"
16+
import { Link as RouterLink, Navigate, NavLink, Outlet, useParams } from "react-router-dom"
17+
import { combineClasses } from "util/combineClasses"
18+
import { firstLetter } from "util/firstLetter"
19+
import { selectPermissions } from "xServices/auth/authSelectors"
20+
import { XServiceContext } from "xServices/StateContext"
21+
import { templateMachine } from "xServices/template/templateXService"
22+
import { Margins } from "../../components/Margins/Margins"
23+
import { Stack } from "../../components/Stack/Stack"
24+
25+
const useTemplateName = () => {
26+
const { template } = useParams()
27+
28+
if (!template) {
29+
throw new Error("No template found in the URL")
30+
}
31+
32+
return template
33+
}
34+
35+
const Language = {
36+
settingsButton: "Settings",
37+
createButton: "Create workspace",
38+
noDescription: "",
39+
}
40+
41+
export const TemplateLayout: FC = () => {
42+
const styles = useStyles()
43+
const organizationId = useOrganizationId()
44+
const templateName = useTemplateName()
45+
const { t } = useTranslation("templatePage")
46+
const [templateState, templateSend] = useMachine(templateMachine, {
47+
context: {
48+
templateName,
49+
organizationId,
50+
},
51+
})
52+
const { template, activeTemplateVersion, templateResources, templateDAUs } = templateState.context
53+
const xServices = useContext(XServiceContext)
54+
const permissions = useSelector(xServices.authXService, selectPermissions)
55+
const isLoading =
56+
!template || !activeTemplateVersion || !templateResources || !permissions || !templateDAUs
57+
58+
if (isLoading) {
59+
return <Loader />
60+
}
61+
62+
if (templateState.matches("deleted")) {
63+
return <Navigate to="/templates" />
64+
}
65+
66+
const hasIcon = template.icon && template.icon !== ""
67+
68+
const createWorkspaceButton = (className?: string) => (
69+
<Link underline="none" component={RouterLink} to={`/templates/${template.name}/workspace`}>
70+
<Button className={className ?? ""} startIcon={<AddCircleOutline />}>
71+
{Language.createButton}
72+
</Button>
73+
</Link>
74+
)
75+
76+
const handleDeleteTemplate = () => {
77+
templateSend("DELETE")
78+
}
79+
80+
return (
81+
<>
82+
<Margins>
83+
<PageHeader
84+
actions={
85+
<>
86+
<Link
87+
underline="none"
88+
component={RouterLink}
89+
to={`/templates/${template.name}/settings`}
90+
>
91+
<Button variant="outlined" startIcon={<SettingsOutlined />}>
92+
{Language.settingsButton}
93+
</Button>
94+
</Link>
95+
96+
{permissions.deleteTemplates ? (
97+
<DropdownButton
98+
primaryAction={createWorkspaceButton(styles.actionButton)}
99+
secondaryActions={[
100+
{
101+
action: "delete",
102+
button: <DeleteButton handleAction={handleDeleteTemplate} />,
103+
},
104+
]}
105+
canCancel={false}
106+
/>
107+
) : (
108+
createWorkspaceButton()
109+
)}
110+
</>
111+
}
112+
>
113+
<Stack direction="row" spacing={3} className={styles.pageTitle}>
114+
<div>
115+
{hasIcon ? (
116+
<div className={styles.iconWrapper}>
117+
<img src={template.icon} alt="" />
118+
</div>
119+
) : (
120+
<Avatar className={styles.avatar}>{firstLetter(template.name)}</Avatar>
121+
)}
122+
</div>
123+
<div>
124+
<PageHeaderTitle>{template.name}</PageHeaderTitle>
125+
<PageHeaderSubtitle condensed>
126+
{template.description === "" ? Language.noDescription : template.description}
127+
</PageHeaderSubtitle>
128+
</div>
129+
</Stack>
130+
</PageHeader>
131+
</Margins>
132+
133+
<div className={styles.tabs}>
134+
<Margins>
135+
<Stack direction="row" spacing={0.25}>
136+
<NavLink
137+
end
138+
to={`/templates/${template.name}`}
139+
className={({ isActive }) =>
140+
combineClasses([styles.tabItem, isActive ? styles.tabItemActive : undefined])
141+
}
142+
>
143+
Summary
144+
</NavLink>
145+
<NavLink
146+
to={`/templates/${template.name}/collaborators`}
147+
className={({ isActive }) =>
148+
combineClasses([styles.tabItem, isActive ? styles.tabItemActive : undefined])
149+
}
150+
>
151+
Collaborators
152+
</NavLink>
153+
</Stack>
154+
</Margins>
155+
</div>
156+
157+
<Margins>
158+
<Outlet context={templateState.context} />
159+
</Margins>
160+
161+
<DeleteDialog
162+
isOpen={templateState.matches("confirmingDelete")}
163+
confirmLoading={templateState.matches("deleting")}
164+
title={t("deleteDialog.title")}
165+
description={t("deleteDialog.description")}
166+
onConfirm={() => {
167+
templateSend("CONFIRM_DELETE")
168+
}}
169+
onCancel={() => {
170+
templateSend("CANCEL_DELETE")
171+
}}
172+
/>
173+
</>
174+
)
175+
}
176+
177+
export const useStyles = makeStyles((theme) => {
178+
return {
179+
actionButton: {
180+
border: "none",
181+
borderRadius: `${theme.shape.borderRadius}px 0px 0px ${theme.shape.borderRadius}px`,
182+
},
183+
pageTitle: {
184+
alignItems: "center",
185+
},
186+
avatar: {
187+
width: theme.spacing(6),
188+
height: theme.spacing(6),
189+
fontSize: theme.spacing(3),
190+
},
191+
iconWrapper: {
192+
width: theme.spacing(6),
193+
height: theme.spacing(6),
194+
"& img": {
195+
width: "100%",
196+
},
197+
},
198+
199+
tabs: {
200+
borderBottom: `1px solid ${theme.palette.divider}`,
201+
marginBottom: theme.spacing(5),
202+
},
203+
204+
tabItem: {
205+
textDecoration: "none",
206+
color: theme.palette.text.secondary,
207+
fontSize: 14,
208+
display: "block",
209+
padding: theme.spacing(0, 2, 2),
210+
211+
"&:hover": {
212+
color: theme.palette.text.primary,
213+
},
214+
},
215+
216+
tabItemActive: {
217+
color: theme.palette.text.primary,
218+
position: "relative",
219+
220+
"&:before": {
221+
content: `""`,
222+
left: 0,
223+
bottom: 0,
224+
height: 2,
225+
width: "100%",
226+
background: theme.palette.secondary.dark,
227+
position: "absolute",
228+
},
229+
},
230+
}
231+
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { FC } from "react"
2+
import { Helmet } from "react-helmet-async"
3+
import { useOutletContext } from "react-router-dom"
4+
import { pageTitle } from "util/page"
5+
import { TemplateContext } from "xServices/template/templateXService"
6+
import { TemplateCollaboratorsPageView } from "./TemplateCollaboratorsPageView"
7+
8+
export const TemplateCollaboratorsPage: FC<React.PropsWithChildren<unknown>> = () => {
9+
const { template, activeTemplateVersion, templateResources, deleteTemplateError } =
10+
useOutletContext<TemplateContext>()
11+
12+
if (!template || !activeTemplateVersion || !templateResources) {
13+
throw new Error(
14+
"This page should not be displayed until template, activeTemplateVersion or templateResources being loaded.",
15+
)
16+
}
17+
18+
return (
19+
<>
20+
<Helmet>
21+
<title>{pageTitle(`${template.name} · Collaborators`)}</title>
22+
</Helmet>
23+
<TemplateCollaboratorsPageView deleteTemplateError={deleteTemplateError} />
24+
</>
25+
)
26+
}
27+
28+
export default TemplateCollaboratorsPage
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import { ErrorSummary } from "components/ErrorSummary/ErrorSummary"
3+
import { Stack } from "components/Stack/Stack"
4+
import { FC } from "react"
5+
6+
export interface TemplateCollaboratorsPageViewProps {
7+
deleteTemplateError: Error | unknown
8+
}
9+
10+
export const TemplateCollaboratorsPageView: FC<
11+
React.PropsWithChildren<TemplateCollaboratorsPageViewProps>
12+
> = ({ deleteTemplateError }) => {
13+
const deleteError = deleteTemplateError ? (
14+
<ErrorSummary error={deleteTemplateError} dismissible />
15+
) : null
16+
17+
return (
18+
<Stack spacing={2.5}>
19+
{deleteError}
20+
<h2>Collaborators</h2>
21+
</Stack>
22+
)
23+
}
24+
25+
export const useStyles = makeStyles(() => {
26+
return {}
27+
})

0 commit comments

Comments
 (0)