Skip to content

Commit dfd8efc

Browse files
committed
feat: add component stories
1 parent 74729eb commit dfd8efc

File tree

5 files changed

+709
-0
lines changed

5 files changed

+709
-0
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromatic } from "testHelpers/chromatic";
3+
import {
4+
MockTemplate,
5+
} from "testHelpers/entities";
6+
import { TemplateCard } from "./TemplateCard";
7+
8+
const meta: Meta<typeof TemplateCard> = {
9+
title: "modules/templates/TemplateCard",
10+
parameters: { chromatic },
11+
component: TemplateCard,
12+
args: {
13+
template: MockTemplate,
14+
},
15+
};
16+
17+
export default meta;
18+
type Story = StoryObj<typeof TemplateCard>;
19+
20+
export const Template: Story = {};
21+
22+
export const DeprecatedTemplate: Story = { args: {
23+
template: {
24+
...MockTemplate,
25+
deprecated: true
26+
}
27+
},};
28+
29+
export const LongContentTemplate: Story = {
30+
args: {
31+
template: {
32+
...MockTemplate,
33+
display_name: 'Very Long Template Name',
34+
organization_name: 'Very Long Organization Name',
35+
description: 'This is a very long test description. This is a very long test description. This is a very long test description. This is a very long test description',
36+
active_user_count: 999
37+
}
38+
},
39+
};
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { chromaticWithTablet } from "testHelpers/chromatic";
3+
import {
4+
mockApiError,
5+
MockTemplate,
6+
MockTemplateExample,
7+
MockTemplateExample2,
8+
} from "testHelpers/entities";
9+
import { TemplatesPageView } from "./TemplatesPageView";
10+
11+
const meta: Meta<typeof TemplatesPageView> = {
12+
title: "pages/MultiOrgTemplatesPage",
13+
parameters: { chromatic: chromaticWithTablet },
14+
component: TemplatesPageView,
15+
};
16+
17+
export default meta;
18+
type Story = StoryObj<typeof TemplatesPageView>;
19+
20+
export const WithTemplates: Story = {
21+
args: {
22+
canCreateTemplates: true,
23+
error: undefined,
24+
templates: [
25+
MockTemplate,
26+
{
27+
...MockTemplate,
28+
active_user_count: -1,
29+
description: "🚀 Some new template that has no activity data",
30+
icon: "/icon/goland.svg",
31+
},
32+
{
33+
...MockTemplate,
34+
active_user_count: 150,
35+
description: "😮 Wow, this one has a bunch of usage!",
36+
icon: "",
37+
},
38+
{
39+
...MockTemplate,
40+
description:
41+
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ",
42+
},
43+
{
44+
...MockTemplate,
45+
name: "template-without-icon",
46+
display_name: "No Icon",
47+
description: "This one has no icon",
48+
icon: "",
49+
},
50+
{
51+
...MockTemplate,
52+
name: "template-without-icon-deprecated",
53+
display_name: "Deprecated No Icon",
54+
description: "This one has no icon and is deprecated",
55+
deprecated: true,
56+
deprecation_message: "This template is so old, it's deprecated",
57+
icon: "",
58+
},
59+
{
60+
...MockTemplate,
61+
name: "deprecated-template",
62+
display_name: "Deprecated",
63+
description: "Template is incompatible",
64+
},
65+
],
66+
examples: [],
67+
},
68+
};
69+
70+
export const EmptyCanCreate: Story = {
71+
args: {
72+
canCreateTemplates: true,
73+
error: undefined,
74+
templates: [],
75+
examples: [MockTemplateExample, MockTemplateExample2],
76+
},
77+
};
78+
79+
export const EmptyCannotCreate: Story = {
80+
args: {
81+
error: undefined,
82+
templates: [],
83+
examples: [MockTemplateExample, MockTemplateExample2],
84+
canCreateTemplates: false,
85+
},
86+
};
87+
88+
export const Error: Story = {
89+
args: {
90+
error: mockApiError({
91+
message: "Something went wrong fetching templates.",
92+
}),
93+
templates: undefined,
94+
examples: undefined,
95+
canCreateTemplates: false,
96+
},
97+
};
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 { 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 { Margins } from "components/Margins/Margins";
16+
import {
17+
PageHeader,
18+
PageHeaderSubtitle,
19+
PageHeaderTitle,
20+
} from "components/PageHeader/PageHeader";
21+
import { Stack } from "components/Stack/Stack";
22+
import { TemplateCard } from "modules/templates/TemplateCard/TemplateCard";
23+
import { docs } from "utils/docs";
24+
import { CreateTemplateButton } from "../CreateTemplateButton";
25+
import { EmptyTemplates } from "../EmptyTemplates";
26+
27+
export const Language = {
28+
templateTooltipTitle: "What is template?",
29+
templateTooltipText:
30+
"With templates you can create a common configuration for your workspaces using Terraform.",
31+
templateTooltipLink: "Manage templates",
32+
};
33+
34+
const TemplateHelpTooltip: FC = () => {
35+
return (
36+
<HelpTooltip>
37+
<HelpTooltipTrigger />
38+
<HelpTooltipContent>
39+
<HelpTooltipTitle>{Language.templateTooltipTitle}</HelpTooltipTitle>
40+
<HelpTooltipText>{Language.templateTooltipText}</HelpTooltipText>
41+
<HelpTooltipLinksGroup>
42+
<HelpTooltipLink href={docs("/templates")}>
43+
{Language.templateTooltipLink}
44+
</HelpTooltipLink>
45+
</HelpTooltipLinksGroup>
46+
</HelpTooltipContent>
47+
</HelpTooltip>
48+
);
49+
};
50+
51+
export interface TemplatesPageViewProps {
52+
templates: Template[] | undefined;
53+
examples: TemplateExample[] | undefined;
54+
canCreateTemplates: boolean;
55+
error?: unknown;
56+
}
57+
58+
export type TemplatesByOrg = Record<string, number>;
59+
60+
const getTemplatesByOrg = (templates: Template[]): TemplatesByOrg => {
61+
const orgs: TemplatesByOrg = {
62+
all: 0
63+
}
64+
65+
templates.forEach((template) => {
66+
if (orgs[template.organization_name]) {
67+
orgs[template.organization_name] += 1
68+
} else {
69+
orgs[template.organization_name] = 1;
70+
}
71+
72+
orgs.all += 1;
73+
})
74+
75+
return orgs;
76+
}
77+
78+
export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
79+
templates,
80+
examples,
81+
canCreateTemplates,
82+
error,
83+
}) => {
84+
const [urlParams] = useSearchParams();
85+
const isEmpty = templates && templates.length === 0;
86+
const navigate = useNavigate();
87+
88+
const activeOrg = urlParams.get("org") ?? "all";
89+
90+
const templatesByOrg = getTemplatesByOrg(templates ?? []);
91+
92+
return (
93+
<Margins>
94+
<PageHeader
95+
actions={
96+
canCreateTemplates && <CreateTemplateButton onNavigate={navigate} />
97+
}
98+
>
99+
<PageHeaderTitle>
100+
<Stack spacing={1} direction="row" alignItems="center">
101+
Templates
102+
<TemplateHelpTooltip />
103+
</Stack>
104+
</PageHeaderTitle>
105+
{templates && templates.length > 0 && (
106+
<PageHeaderSubtitle>
107+
Select a template to create a workspace.
108+
</PageHeaderSubtitle>
109+
)}
110+
</PageHeader>
111+
112+
{Boolean(error) && <ErrorAlert error={error} />}
113+
114+
<Stack direction="row" spacing={4} alignItems="flex-start">
115+
<Stack
116+
css={{ width: 208, flexShrink: 0, position: "sticky", top: 48 }}
117+
>
118+
<span css={styles.filterCaption}>ORGANIZATION</span>
119+
{Object.keys(templatesByOrg).map((org) => (
120+
<Link
121+
key={org}
122+
to={`?org=${org}`}
123+
css={[
124+
styles.tagLink,
125+
org === activeOrg && styles.tagLinkActive,
126+
]}
127+
>
128+
{org === 'all' ? 'All Organizations' : org} ({templatesByOrg[org] ?? 0})
129+
</Link>
130+
))}
131+
</Stack>
132+
133+
134+
<div
135+
css={{
136+
display: "flex",
137+
flexWrap: "wrap",
138+
gap: 32,
139+
height: "max-content",
140+
}}
141+
>
142+
{isEmpty ? (
143+
<EmptyTemplates
144+
canCreateTemplates={canCreateTemplates}
145+
examples={examples ?? []}
146+
/>
147+
) : (templates &&
148+
templates.map((template) => (
149+
<TemplateCard
150+
css={(theme) => ({
151+
backgroundColor: theme.palette.background.paper,
152+
})}
153+
template={template}
154+
key={template.id}
155+
/>
156+
)))}
157+
</div>
158+
</Stack>
159+
</Margins>
160+
);
161+
};
162+
163+
const styles = {
164+
filterCaption: (theme) => ({
165+
textTransform: "uppercase",
166+
fontWeight: 600,
167+
fontSize: 12,
168+
color: theme.palette.text.secondary,
169+
letterSpacing: "0.1em",
170+
}),
171+
tagLink: (theme) => ({
172+
color: theme.palette.text.secondary,
173+
textDecoration: "none",
174+
fontSize: 14,
175+
textTransform: "capitalize",
176+
177+
"&:hover": {
178+
color: theme.palette.text.primary,
179+
},
180+
}),
181+
tagLinkActive: (theme) => ({
182+
color: theme.palette.text.primary,
183+
fontWeight: 600,
184+
}),
185+
secondary: (theme) => ({
186+
color: theme.palette.text.secondary,
187+
}),
188+
actionButton: (theme) => ({
189+
transition: "none",
190+
color: theme.palette.text.secondary,
191+
"&:hover": {
192+
borderColor: theme.palette.text.primary,
193+
},
194+
}),
195+
} satisfies Record<string, Interpolation<Theme>>;

0 commit comments

Comments
 (0)