diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx
index c6c238e5dff9a..dfb6e05be35b0 100644
--- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx
+++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx
@@ -1,121 +1,30 @@
-import { type PropsWithChildren, type ReactNode, useState } from "react";
-import { useTheme } from "@emotion/react";
-import { Language } from "./WorkspacesPageView";
-
+import {
+ type PropsWithChildren,
+ type ReactNode,
+ useState,
+ useRef,
+} from "react";
import { type Template } from "api/typesGenerated";
import { type UseQueryResult } from "react-query";
-
-import { Link as RouterLink } from "react-router-dom";
+import {
+ Link as RouterLink,
+ LinkProps as RouterLinkProps,
+} from "react-router-dom";
import Box from "@mui/system/Box";
import Button from "@mui/material/Button";
import Link from "@mui/material/Link";
import AddIcon from "@mui/icons-material/AddOutlined";
import OpenIcon from "@mui/icons-material/OpenInNewOutlined";
-import Typography from "@mui/material/Typography";
-
import { Loader } from "components/Loader/Loader";
import { OverflowY } from "components/OverflowY/OverflowY";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Avatar } from "components/Avatar/Avatar";
import { SearchBox } from "./WorkspacesSearchBox";
-import {
- PopoverContainer,
- PopoverLink,
-} from "components/PopoverContainer/PopoverContainer";
+import Popover from "@mui/material/Popover";
const ICON_SIZE = 18;
const COLUMN_GAP = 1.5;
-function sortTemplatesByUsersDesc(
- templates: readonly Template[],
- searchTerm: string,
-) {
- const allWhitespace = /^\s+$/.test(searchTerm);
- if (allWhitespace) {
- return templates;
- }
-
- const termMatcher = new RegExp(searchTerm.replaceAll(/[^\w]/g, "."), "i");
- return templates
- .filter(
- (template) =>
- termMatcher.test(template.display_name) ||
- termMatcher.test(template.name),
- )
- .sort((t1, t2) => t2.active_user_count - t1.active_user_count)
- .slice(0, 10);
-}
-
-function WorkspaceResultsRow({ template }: { template: Template }) {
- const theme = useTheme();
-
- return (
-
-
-
- {template.display_name || "-"}
-
-
-
-
- {template.display_name || template.name || "[Unnamed]"}
-
-
-
- {/*
- * There are some templates that have -1 as their user count –
- * basically functioning like a null value in JS. Can safely just
- * treat them as if they were 0.
- */}
- {template.active_user_count <= 0
- ? "No"
- : template.active_user_count}{" "}
- developer
- {template.active_user_count === 1 ? "" : "s"}
-
-
-
-
- );
-}
-
type TemplatesQuery = UseQueryResult;
type WorkspacesButtonProps = PropsWithChildren<{
@@ -128,13 +37,14 @@ export function WorkspacesButton({
templatesFetchStatus,
templates,
}: WorkspacesButtonProps) {
- const theme = useTheme();
-
// Dataset should always be small enough that client-side filtering should be
// good enough. Can swap out down the line if it becomes an issue
const [searchTerm, setSearchTerm] = useState("");
const processed = sortTemplatesByUsersDesc(templates ?? [], searchTerm);
+ const anchorRef = useRef(null);
+ const [isOpen, setIsOpen] = useState(false);
+
let emptyState: ReactNode = undefined;
if (templates?.length === 0) {
emptyState = (
@@ -152,75 +62,186 @@ export function WorkspacesButton({
}
return (
- } variant="contained">
- {children}
-
- }
- >
- setSearchTerm(newValue)}
- placeholder="Type/select a workspace template"
- label="Template select for workspace"
- sx={{ flexShrink: 0, columnGap: COLUMN_GAP }}
- />
-
-
+ }
+ variant="contained"
+ ref={anchorRef}
+ onClick={() => {
+ setIsOpen(true);
}}
>
- {templatesFetchStatus === "loading" ? (
-
- ) : (
- <>
- {processed.map((template) => (
-
- ))}
-
- {emptyState}
- >
- )}
-
-
-
+ setIsOpen(false)}
+ anchorEl={anchorRef.current}
+ anchorOrigin={{
+ vertical: "bottom",
+ horizontal: "right",
+ }}
+ transformOrigin={{
+ vertical: "top",
+ horizontal: "right",
}}
+ css={(theme) => ({
+ marginTop: theme.spacing(1),
+ "& .MuiPaper-root": {
+ width: theme.spacing(40),
+ },
+ })}
>
- setSearchTerm(newValue)}
+ placeholder="Type/select a workspace template"
+ label="Template select for workspace"
+ sx={{ flexShrink: 0, columnGap: COLUMN_GAP }}
+ />
+
+
-
-
-
- {Language.seeAllTemplates}
+ {templatesFetchStatus === "loading" ? (
+
+ ) : (
+ <>
+ {processed.map((template) => (
+
+ ))}
+
+ {emptyState}
+ >
+ )}
+
+
+ ({
+ padding: theme.spacing(1, 0),
+ borderTop: `1px solid ${theme.palette.divider}`,
+ })}
+ >
+ ({
+ display: "flex",
+ alignItems: "center",
+ columnGap: theme.spacing(COLUMN_GAP),
+
+ color: theme.palette.primary.main,
+ })}
+ >
+
+ See all templates
+
-
-
+
+ >
+ );
+}
+
+function WorkspaceResultsRow({ template }: { template: Template }) {
+ return (
+ ({
+ display: "flex",
+ gap: theme.spacing(COLUMN_GAP),
+ alignItems: "center",
+ })}
+ >
+
+ {template.display_name || "-"}
+
+
+ ({
+ color: theme.palette.text.primary,
+ display: "flex",
+ flexDirection: "column",
+ lineHeight: "140%",
+ fontSize: 14,
+ overflow: "hidden",
+ })}
+ >
+
+ {template.display_name || template.name || "[Unnamed]"}
+
+ ({
+ fontSize: 13,
+ color: theme.palette.text.secondary,
+ })}
+ >
+ {/*
+ * There are some templates that have -1 as their user count –
+ * basically functioning like a null value in JS. Can safely just
+ * treat them as if they were 0.
+ */}
+ {template.active_user_count <= 0 ? "No" : template.active_user_count}{" "}
+ developer
+ {template.active_user_count === 1 ? "" : "s"}
+
+
+
+ );
+}
+
+function PopoverLink(props: RouterLinkProps) {
+ return (
+ ({
+ color: theme.palette.text.primary,
+ padding: theme.spacing(1, 2),
+ fontSize: 14,
+ outline: "none",
+ textDecoration: "none",
+ "&:focus": {
+ backgroundColor: theme.palette.action.focus,
+ },
+ "&:hover": {
+ textDecoration: "none",
+ backgroundColor: theme.palette.action.hover,
+ },
+ })}
+ />
);
}
+
+function sortTemplatesByUsersDesc(
+ templates: readonly Template[],
+ searchTerm: string,
+) {
+ const allWhitespace = /^\s+$/.test(searchTerm);
+ if (allWhitespace) {
+ return templates;
+ }
+
+ const termMatcher = new RegExp(searchTerm.replaceAll(/[^\w]/g, "."), "i");
+ return templates
+ .filter(
+ (template) =>
+ termMatcher.test(template.display_name) ||
+ termMatcher.test(template.name),
+ )
+ .sort((t1, t2) => t2.active_user_count - t1.active_user_count)
+ .slice(0, 10);
+}