Skip to content

Commit 35f9e2e

Browse files
refactor(site): refactor create workspace button (#10303)
1 parent 0f2d4fd commit 35f9e2e

File tree

1 file changed

+186
-165
lines changed

1 file changed

+186
-165
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,30 @@
1-
import { type PropsWithChildren, type ReactNode, useState } from "react";
2-
import { useTheme } from "@emotion/react";
3-
import { Language } from "./WorkspacesPageView";
4-
1+
import {
2+
type PropsWithChildren,
3+
type ReactNode,
4+
useState,
5+
useRef,
6+
} from "react";
57
import { type Template } from "api/typesGenerated";
68
import { type UseQueryResult } from "react-query";
7-
8-
import { Link as RouterLink } from "react-router-dom";
9+
import {
10+
Link as RouterLink,
11+
LinkProps as RouterLinkProps,
12+
} from "react-router-dom";
913
import Box from "@mui/system/Box";
1014
import Button from "@mui/material/Button";
1115
import Link from "@mui/material/Link";
1216
import AddIcon from "@mui/icons-material/AddOutlined";
1317
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
14-
import Typography from "@mui/material/Typography";
15-
1618
import { Loader } from "components/Loader/Loader";
1719
import { OverflowY } from "components/OverflowY/OverflowY";
1820
import { EmptyState } from "components/EmptyState/EmptyState";
1921
import { Avatar } from "components/Avatar/Avatar";
2022
import { SearchBox } from "./WorkspacesSearchBox";
21-
import {
22-
PopoverContainer,
23-
PopoverLink,
24-
} from "components/PopoverContainer/PopoverContainer";
23+
import Popover from "@mui/material/Popover";
2524

2625
const ICON_SIZE = 18;
2726
const COLUMN_GAP = 1.5;
2827

29-
function sortTemplatesByUsersDesc(
30-
templates: readonly Template[],
31-
searchTerm: string,
32-
) {
33-
const allWhitespace = /^\s+$/.test(searchTerm);
34-
if (allWhitespace) {
35-
return templates;
36-
}
37-
38-
const termMatcher = new RegExp(searchTerm.replaceAll(/[^\w]/g, "."), "i");
39-
return templates
40-
.filter(
41-
(template) =>
42-
termMatcher.test(template.display_name) ||
43-
termMatcher.test(template.name),
44-
)
45-
.sort((t1, t2) => t2.active_user_count - t1.active_user_count)
46-
.slice(0, 10);
47-
}
48-
49-
function WorkspaceResultsRow({ template }: { template: Template }) {
50-
const theme = useTheme();
51-
52-
return (
53-
<PopoverLink to={`/templates/${template.name}/workspace`}>
54-
<Box
55-
sx={{
56-
display: "flex",
57-
columnGap: COLUMN_GAP,
58-
alignItems: "center",
59-
paddingX: 2,
60-
paddingY: 1,
61-
overflowY: "hidden",
62-
}}
63-
>
64-
<Avatar
65-
src={template.icon}
66-
fitImage
67-
alt={template.display_name || "Coder template"}
68-
sx={{
69-
width: `${ICON_SIZE}px`,
70-
height: `${ICON_SIZE}px`,
71-
fontSize: `${ICON_SIZE * 0.5}px`,
72-
fontWeight: 700,
73-
}}
74-
>
75-
{template.display_name || "-"}
76-
</Avatar>
77-
78-
<Box
79-
sx={{
80-
lineHeight: 1,
81-
width: "100%",
82-
overflow: "hidden",
83-
color: "white",
84-
}}
85-
>
86-
<Typography
87-
component="p"
88-
sx={{ marginY: 0, paddingBottom: 0.5, lineHeight: 1 }}
89-
noWrap
90-
>
91-
{template.display_name || template.name || "[Unnamed]"}
92-
</Typography>
93-
94-
<Box
95-
component="p"
96-
sx={{
97-
marginY: 0,
98-
fontSize: 14,
99-
color: theme.palette.text.secondary,
100-
}}
101-
>
102-
{/*
103-
* There are some templates that have -1 as their user count –
104-
* basically functioning like a null value in JS. Can safely just
105-
* treat them as if they were 0.
106-
*/}
107-
{template.active_user_count <= 0
108-
? "No"
109-
: template.active_user_count}{" "}
110-
developer
111-
{template.active_user_count === 1 ? "" : "s"}
112-
</Box>
113-
</Box>
114-
</Box>
115-
</PopoverLink>
116-
);
117-
}
118-
11928
type TemplatesQuery = UseQueryResult<Template[]>;
12029

12130
type WorkspacesButtonProps = PropsWithChildren<{
@@ -128,13 +37,14 @@ export function WorkspacesButton({
12837
templatesFetchStatus,
12938
templates,
13039
}: WorkspacesButtonProps) {
131-
const theme = useTheme();
132-
13340
// Dataset should always be small enough that client-side filtering should be
13441
// good enough. Can swap out down the line if it becomes an issue
13542
const [searchTerm, setSearchTerm] = useState("");
13643
const processed = sortTemplatesByUsersDesc(templates ?? [], searchTerm);
13744

45+
const anchorRef = useRef<HTMLButtonElement>(null);
46+
const [isOpen, setIsOpen] = useState(false);
47+
13848
let emptyState: ReactNode = undefined;
13949
if (templates?.length === 0) {
14050
emptyState = (
@@ -152,75 +62,186 @@ export function WorkspacesButton({
15262
}
15363

15464
return (
155-
<PopoverContainer
156-
// Stopgap value until bug where string-based horizontal origin isn't
157-
// being applied consistently can get figured out
158-
originX={-115}
159-
originY="bottom"
160-
sx={{ display: "flex", flexFlow: "column nowrap" }}
161-
anchorButton={
162-
<Button startIcon={<AddIcon />} variant="contained">
163-
{children}
164-
</Button>
165-
}
166-
>
167-
<SearchBox
168-
value={searchTerm}
169-
onValueChange={(newValue) => setSearchTerm(newValue)}
170-
placeholder="Type/select a workspace template"
171-
label="Template select for workspace"
172-
sx={{ flexShrink: 0, columnGap: COLUMN_GAP }}
173-
/>
174-
175-
<OverflowY
176-
maxHeight={380}
177-
sx={{
178-
display: "flex",
179-
flexFlow: "column nowrap",
180-
paddingY: 1,
65+
<>
66+
<Button
67+
startIcon={<AddIcon />}
68+
variant="contained"
69+
ref={anchorRef}
70+
onClick={() => {
71+
setIsOpen(true);
18172
}}
18273
>
183-
{templatesFetchStatus === "loading" ? (
184-
<Loader size={14} />
185-
) : (
186-
<>
187-
{processed.map((template) => (
188-
<WorkspaceResultsRow key={template.id} template={template} />
189-
))}
190-
191-
{emptyState}
192-
</>
193-
)}
194-
</OverflowY>
195-
196-
<Link
197-
component={RouterLink}
198-
to="/templates"
199-
sx={{
200-
outline: "none",
201-
"&:focus": {
202-
backgroundColor: theme.palette.action.focus,
203-
},
74+
{children}
75+
</Button>
76+
<Popover
77+
disablePortal
78+
open={isOpen}
79+
onClose={() => setIsOpen(false)}
80+
anchorEl={anchorRef.current}
81+
anchorOrigin={{
82+
vertical: "bottom",
83+
horizontal: "right",
84+
}}
85+
transformOrigin={{
86+
vertical: "top",
87+
horizontal: "right",
20488
}}
89+
css={(theme) => ({
90+
marginTop: theme.spacing(1),
91+
"& .MuiPaper-root": {
92+
width: theme.spacing(40),
93+
},
94+
})}
20595
>
206-
<Box
96+
<SearchBox
97+
value={searchTerm}
98+
onValueChange={(newValue) => setSearchTerm(newValue)}
99+
placeholder="Type/select a workspace template"
100+
label="Template select for workspace"
101+
sx={{ flexShrink: 0, columnGap: COLUMN_GAP }}
102+
/>
103+
104+
<OverflowY
105+
maxHeight={380}
207106
sx={{
208-
padding: 2,
209107
display: "flex",
210-
flexFlow: "row nowrap",
211-
alignItems: "center",
212-
columnGap: COLUMN_GAP,
213-
borderTop: `1px solid ${theme.palette.divider}`,
108+
flexDirection: "column",
109+
paddingY: 1,
214110
}}
215111
>
216-
<Box component="span" sx={{ width: `${ICON_SIZE}px` }}>
217-
<OpenIcon
218-
sx={{ fontSize: "16px", marginX: "auto", display: "block" }}
219-
/>
220-
</Box>
221-
<span>{Language.seeAllTemplates}</span>
112+
{templatesFetchStatus === "loading" ? (
113+
<Loader size={14} />
114+
) : (
115+
<>
116+
{processed.map((template) => (
117+
<WorkspaceResultsRow key={template.id} template={template} />
118+
))}
119+
120+
{emptyState}
121+
</>
122+
)}
123+
</OverflowY>
124+
125+
<Box
126+
css={(theme) => ({
127+
padding: theme.spacing(1, 0),
128+
borderTop: `1px solid ${theme.palette.divider}`,
129+
})}
130+
>
131+
<PopoverLink
132+
to="/templates"
133+
css={(theme) => ({
134+
display: "flex",
135+
alignItems: "center",
136+
columnGap: theme.spacing(COLUMN_GAP),
137+
138+
color: theme.palette.primary.main,
139+
})}
140+
>
141+
<OpenIcon css={{ width: 14, height: 14 }} />
142+
<span>See all templates</span>
143+
</PopoverLink>
222144
</Box>
223-
</Link>
224-
</PopoverContainer>
145+
</Popover>
146+
</>
147+
);
148+
}
149+
150+
function WorkspaceResultsRow({ template }: { template: Template }) {
151+
return (
152+
<PopoverLink
153+
to={`/templates/${template.name}/workspace`}
154+
css={(theme) => ({
155+
display: "flex",
156+
gap: theme.spacing(COLUMN_GAP),
157+
alignItems: "center",
158+
})}
159+
>
160+
<Avatar
161+
src={template.icon}
162+
fitImage
163+
alt={template.display_name || "Coder template"}
164+
sx={{
165+
width: `${ICON_SIZE}px`,
166+
height: `${ICON_SIZE}px`,
167+
fontSize: `${ICON_SIZE * 0.5}px`,
168+
fontWeight: 700,
169+
}}
170+
>
171+
{template.display_name || "-"}
172+
</Avatar>
173+
174+
<Box
175+
css={(theme) => ({
176+
color: theme.palette.text.primary,
177+
display: "flex",
178+
flexDirection: "column",
179+
lineHeight: "140%",
180+
fontSize: 14,
181+
overflow: "hidden",
182+
})}
183+
>
184+
<span css={{ whiteSpace: "nowrap", textOverflow: "ellipsis" }}>
185+
{template.display_name || template.name || "[Unnamed]"}
186+
</span>
187+
<span
188+
css={(theme) => ({
189+
fontSize: 13,
190+
color: theme.palette.text.secondary,
191+
})}
192+
>
193+
{/*
194+
* There are some templates that have -1 as their user count –
195+
* basically functioning like a null value in JS. Can safely just
196+
* treat them as if they were 0.
197+
*/}
198+
{template.active_user_count <= 0 ? "No" : template.active_user_count}{" "}
199+
developer
200+
{template.active_user_count === 1 ? "" : "s"}
201+
</span>
202+
</Box>
203+
</PopoverLink>
204+
);
205+
}
206+
207+
function PopoverLink(props: RouterLinkProps) {
208+
return (
209+
<RouterLink
210+
{...props}
211+
css={(theme) => ({
212+
color: theme.palette.text.primary,
213+
padding: theme.spacing(1, 2),
214+
fontSize: 14,
215+
outline: "none",
216+
textDecoration: "none",
217+
"&:focus": {
218+
backgroundColor: theme.palette.action.focus,
219+
},
220+
"&:hover": {
221+
textDecoration: "none",
222+
backgroundColor: theme.palette.action.hover,
223+
},
224+
})}
225+
/>
225226
);
226227
}
228+
229+
function sortTemplatesByUsersDesc(
230+
templates: readonly Template[],
231+
searchTerm: string,
232+
) {
233+
const allWhitespace = /^\s+$/.test(searchTerm);
234+
if (allWhitespace) {
235+
return templates;
236+
}
237+
238+
const termMatcher = new RegExp(searchTerm.replaceAll(/[^\w]/g, "."), "i");
239+
return templates
240+
.filter(
241+
(template) =>
242+
termMatcher.test(template.display_name) ||
243+
termMatcher.test(template.name),
244+
)
245+
.sort((t1, t2) => t2.active_user_count - t1.active_user_count)
246+
.slice(0, 10);
247+
}

0 commit comments

Comments
 (0)