diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index c6c238e5df..dfb6e05be3 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,31 +1,231 @@ -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; +type TemplatesQuery = UseQueryResult; + +type WorkspacesButtonProps = PropsWithChildren<{ + templatesFetchStatus: TemplatesQuery["status"]; + templates: TemplatesQuery["data"]; +}>; + +export function WorkspacesButton({ + children, + templatesFetchStatus, + templates, +}: WorkspacesButtonProps) { + // 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 = ( + + Create one now. + + } + /> + ); + } else if (processed.length === 0) { + emptyState = ; + } + + return ( + <> + + 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 }} + /> + + + {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, @@ -45,182 +245,3 @@ function sortTemplatesByUsersDesc( .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<{ - templatesFetchStatus: TemplatesQuery["status"]; - templates: TemplatesQuery["data"]; -}>; - -export function WorkspacesButton({ - children, - 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); - - let emptyState: ReactNode = undefined; - if (templates?.length === 0) { - emptyState = ( - - Create one now. - - } - /> - ); - } else if (processed.length === 0) { - emptyState = ; - } - - return ( - } variant="contained"> - {children} - - } - > - setSearchTerm(newValue)} - placeholder="Type/select a workspace template" - label="Template select for workspace" - sx={{ flexShrink: 0, columnGap: COLUMN_GAP }} - /> - - - {templatesFetchStatus === "loading" ? ( - - ) : ( - <> - {processed.map((template) => ( - - ))} - - {emptyState} - - )} - - - - - - - - {Language.seeAllTemplates} - - - - ); -}