Skip to content

Commit a68bb60

Browse files
committed
feat: initial changes for multi-org templates page
1 parent a6d66cc commit a68bb60

File tree

2 files changed

+209
-8
lines changed

2 files changed

+209
-8
lines changed

site/src/pages/TemplatesPage/TemplatesPage.tsx

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,48 @@ import type { FC } from "react";
22
import { Helmet } from "react-helmet-async";
33
import { useQuery } from "react-query";
44
import { templateExamples, templates } from "api/queries/templates";
5+
import { myOrganizations } from "api/queries/users";
56
import { useAuthenticated } from "contexts/auth/RequireAuth";
67
import { useDashboard } from "modules/dashboard/useDashboard";
78
import { pageTitle } from "utils/page";
89
import { TemplatesPageView } from "./TemplatesPageView";
10+
import { TemplatesPageViewV2 } from "./TemplatesPageViewV2";
911

1012
export const TemplatesPage: FC = () => {
1113
const { permissions } = useAuthenticated();
12-
const { organizationId } = useDashboard();
14+
const { organizationId, experiments } = useDashboard();
1315

16+
const organizationsQuery = useQuery(myOrganizations());
1417
const templatesQuery = useQuery(templates(organizationId));
1518
const examplesQuery = useQuery({
1619
...templateExamples(organizationId),
1720
enabled: permissions.createTemplates,
1821
});
19-
const error = templatesQuery.error || examplesQuery.error;
22+
const error = templatesQuery.error || examplesQuery.error || organizationsQuery.error;
23+
const multiOrgExperimentEnabled = experiments.includes("multi-organization");
2024

25+
console.log({ multiOrgExperimentEnabled })
2126
return (
2227
<>
2328
<Helmet>
2429
<title>{pageTitle("Templates")}</title>
2530
</Helmet>
26-
<TemplatesPageView
27-
error={error}
28-
canCreateTemplates={permissions.createTemplates}
29-
examples={examplesQuery.data}
30-
templates={templatesQuery.data}
31-
/>
31+
{multiOrgExperimentEnabled ? (
32+
<TemplatesPageViewV2
33+
error={error}
34+
canCreateTemplates={permissions.createTemplates}
35+
examples={examplesQuery.data}
36+
templates={templatesQuery.data}
37+
organizations={organizationsQuery.data}
38+
/>
39+
) : (
40+
<TemplatesPageView
41+
error={error}
42+
canCreateTemplates={permissions.createTemplates}
43+
examples={examplesQuery.data}
44+
templates={templatesQuery.data}
45+
/>
46+
)}
3247
</>
3348
);
3449
};
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
import type { Interpolation, Theme } from "@emotion/react";
2+
import type { FC } from "react";
3+
import { Link, useNavigate, useSearchParams } from "react-router-dom";
4+
import type { Organization, Template, TemplateExample } from "api/typesGenerated";
5+
import { ErrorAlert } from "components/Alert/ErrorAlert";
6+
import {
7+
HelpTooltip,
8+
HelpTooltipContent,
9+
HelpTooltipLink,
10+
HelpTooltipLinksGroup,
11+
HelpTooltipText,
12+
HelpTooltipTitle,
13+
HelpTooltipTrigger,
14+
} from "components/HelpTooltip/HelpTooltip";
15+
// import { Loader } from "components/Loader/Loader";
16+
import { Margins } from "components/Margins/Margins";
17+
import {
18+
PageHeader,
19+
PageHeaderSubtitle,
20+
PageHeaderTitle,
21+
} from "components/PageHeader/PageHeader";
22+
import { Stack } from "components/Stack/Stack";
23+
// import { createDayString } from "utils/createDayString";
24+
import { docs } from "utils/docs";
25+
import {
26+
formatTemplateBuildTime,
27+
formatTemplateActiveDevelopers,
28+
} from "utils/templates";
29+
import { CreateTemplateButton } from "./CreateTemplateButton";
30+
import { EmptyTemplates } from "./EmptyTemplates";
31+
32+
export const Language = {
33+
developerCount: (activeCount: number): string => {
34+
return `${formatTemplateActiveDevelopers(activeCount)} developer${
35+
activeCount !== 1 ? "s" : ""
36+
}`;
37+
},
38+
nameLabel: "Name",
39+
buildTimeLabel: "Build time",
40+
usedByLabel: "Used by",
41+
lastUpdatedLabel: "Last updated",
42+
templateTooltipTitle: "What is template?",
43+
templateTooltipText:
44+
"With templates you can create a common configuration for your workspaces using Terraform.",
45+
templateTooltipLink: "Manage templates",
46+
};
47+
48+
const TemplateHelpTooltip: FC = () => {
49+
return (
50+
<HelpTooltip>
51+
<HelpTooltipTrigger />
52+
<HelpTooltipContent>
53+
<HelpTooltipTitle>{Language.templateTooltipTitle}</HelpTooltipTitle>
54+
<HelpTooltipText>{Language.templateTooltipText}</HelpTooltipText>
55+
<HelpTooltipLinksGroup>
56+
<HelpTooltipLink href={docs("/templates")}>
57+
{Language.templateTooltipLink}
58+
</HelpTooltipLink>
59+
</HelpTooltipLinksGroup>
60+
</HelpTooltipContent>
61+
</HelpTooltip>
62+
);
63+
};
64+
65+
export interface TemplatesPageViewProps {
66+
templates: Template[] | undefined;
67+
organizations: Organization[] | undefined;
68+
examples: TemplateExample[] | undefined;
69+
canCreateTemplates: boolean;
70+
error?: unknown;
71+
}
72+
73+
export const TemplatesPageViewV2: FC<TemplatesPageViewProps> = ({
74+
templates,
75+
organizations,
76+
examples,
77+
canCreateTemplates,
78+
error,
79+
}) => {
80+
const [urlParams] = useSearchParams();
81+
const isEmpty = templates && templates.length === 0;
82+
const navigate = useNavigate();
83+
84+
const activeOrg = urlParams.get("org") ?? "all";
85+
86+
return (
87+
<Margins>
88+
<PageHeader
89+
actions={
90+
canCreateTemplates && <CreateTemplateButton onNavigate={navigate} />
91+
}
92+
>
93+
<PageHeaderTitle>
94+
<Stack spacing={1} direction="row" alignItems="center">
95+
Templates
96+
<TemplateHelpTooltip />
97+
</Stack>
98+
</PageHeaderTitle>
99+
{templates && templates.length > 0 && (
100+
<PageHeaderSubtitle>
101+
Select a template to create a workspace.
102+
</PageHeaderSubtitle>
103+
)}
104+
</PageHeader>
105+
106+
{Boolean(error) && <ErrorAlert error={error} />}
107+
108+
{/* {Boolean(isLoading) && <Loader />} */}
109+
110+
<Stack direction="row" spacing={4} alignItems="flex-start">
111+
<Stack
112+
css={{ width: 208, flexShrink: 0, position: "sticky", top: 48 }}
113+
>
114+
<span css={styles.filterCaption}>ORGANIZATION</span>
115+
{organizations?.map((org) => (
116+
<Link
117+
key={org.id}
118+
to={`?org=${org.name}`}
119+
css={[
120+
styles.tagLink,
121+
org.name === activeOrg && styles.tagLinkActive,
122+
]}
123+
>
124+
{/* {org.name} ({starterTemplatesByTag[tag].length}) */}
125+
{org.name} (1)
126+
</Link>
127+
))}
128+
</Stack>
129+
130+
131+
<div
132+
css={{
133+
display: "flex",
134+
flexWrap: "wrap",
135+
gap: 32,
136+
height: "max-content",
137+
}}
138+
>
139+
{isEmpty ? (
140+
<EmptyTemplates
141+
canCreateTemplates={canCreateTemplates}
142+
examples={examples ?? []}
143+
/>
144+
) : (templates &&
145+
templates.map((template) => (
146+
<p key={template.id}>{template.name}</p>
147+
)))}
148+
</div>
149+
</Stack>
150+
</Margins>
151+
);
152+
};
153+
154+
const styles = {
155+
filterCaption: (theme) => ({
156+
textTransform: "uppercase",
157+
fontWeight: 600,
158+
fontSize: 12,
159+
color: theme.palette.text.secondary,
160+
letterSpacing: "0.1em",
161+
}),
162+
tagLink: (theme) => ({
163+
color: theme.palette.text.secondary,
164+
textDecoration: "none",
165+
fontSize: 14,
166+
textTransform: "capitalize",
167+
168+
"&:hover": {
169+
color: theme.palette.text.primary,
170+
},
171+
}),
172+
tagLinkActive: (theme) => ({
173+
color: theme.palette.text.primary,
174+
fontWeight: 600,
175+
}),
176+
secondary: (theme) => ({
177+
color: theme.palette.text.secondary,
178+
}),
179+
actionButton: (theme) => ({
180+
transition: "none",
181+
color: theme.palette.text.secondary,
182+
"&:hover": {
183+
borderColor: theme.palette.text.primary,
184+
},
185+
}),
186+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)