From f0a2aaefd776803e66c2819a64de6a995d10f4a6 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:20:08 +0000 Subject: [PATCH 01/25] chore: Add OverflowY component --- .../components/WorkspacesButton/OverflowY.tsx | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 site/src/components/WorkspacesButton/OverflowY.tsx diff --git a/site/src/components/WorkspacesButton/OverflowY.tsx b/site/src/components/WorkspacesButton/OverflowY.tsx new file mode 100644 index 0000000000000..f8b77ff67a049 --- /dev/null +++ b/site/src/components/WorkspacesButton/OverflowY.tsx @@ -0,0 +1,35 @@ +import { type ReactNode } from "react"; +import { type SystemStyleObject } from "@mui/system"; +import Box from "@mui/system/Box"; + +type Props = { + children: ReactNode; + height?: number; + maxHeight?: number; + sx?: SystemStyleObject; +}; + +export function OverflowY({ children, height, maxHeight, sx }: Props) { + const computedHeight = height === undefined ? "100%" : `${height}px`; + + // Doing Math.max check to catch cases where height is accidentally larger + // than maxHeight + const computedMaxHeight = + maxHeight === undefined + ? computedHeight + : `${Math.max(height ?? 0, maxHeight)}px`; + + return ( + + {children} + + ); +} From 24201675bd9837fb5b79cfc1933544407466a8cb Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:20:19 +0000 Subject: [PATCH 02/25] chore: Add PopoverContainer component --- .../WorkspacesButton/PopoverContainer.tsx | 142 ++++++++++++++++++ 1 file changed, 142 insertions(+) create mode 100644 site/src/components/WorkspacesButton/PopoverContainer.tsx diff --git a/site/src/components/WorkspacesButton/PopoverContainer.tsx b/site/src/components/WorkspacesButton/PopoverContainer.tsx new file mode 100644 index 0000000000000..c8fd233728c62 --- /dev/null +++ b/site/src/components/WorkspacesButton/PopoverContainer.tsx @@ -0,0 +1,142 @@ +import { + type KeyboardEvent, + type ReactElement, + useEffect, + useRef, + useState, + PropsWithChildren, +} from "react"; + +import { type Theme, type SystemStyleObject } from "@mui/system"; +import Popover, { type PopoverOrigin } from "@mui/material/Popover"; + +type Props = PropsWithChildren<{ + /** + * Does not require any hooks or refs to work. Also does not override any refs + * or event handlers attached to the button. + */ + anchorButton: ReactElement; + width?: number; + originX?: PopoverOrigin["horizontal"]; + originY?: PopoverOrigin["vertical"]; + sx?: SystemStyleObject; +}>; + +function getButton(container: HTMLElement) { + return ( + container.querySelector("button") ?? + container.querySelector('[aria-role="button"]') + ); +} + +export function PopoverContainer({ + children, + anchorButton, + originX = 0, + originY = 0, + width = 320, + sx = {}, +}: Props) { + const buttonContainerRef = useRef(null); + + // Ref value is for effects and event listeners; state value is for React + // renders. Have to duplicate state because after the initial render, it's + // never safe to reference ref contents inside a render path, especially with + // React 18 concurrency. Duplication is a necessary evil because of MUI's + // weird, clunky APIs + const anchorButtonRef = useRef(null); + const [loadedButton, setLoadedButton] = useState(); + + // Makes container listen to changes in button. If this approach becomes + // untenable in the future, it can be replaced with React.cloneElement, but + // the trade-off there is that every single anchorButton will need to be + // wrapped inside React.forwardRef, making the abstraction leak a little more + useEffect(() => { + const buttonContainer = buttonContainerRef.current; + if (buttonContainer === null) { + throw new Error("Please attach container ref to button container"); + } + + const initialButton = getButton(buttonContainer); + if (initialButton === null) { + throw new Error("Initial ref query failed"); + } + anchorButtonRef.current = initialButton; + + const onContainerMutation: MutationCallback = () => { + const newButton = getButton(buttonContainer); + if (newButton === null) { + throw new Error("Semantic button removed after DOM update"); + } + + anchorButtonRef.current = newButton; + setLoadedButton((current) => { + return current === undefined ? undefined : newButton; + }); + }; + + const observer = new MutationObserver(onContainerMutation); + observer.observe(buttonContainer, { + childList: true, + subtree: true, + }); + + return () => observer.disconnect(); + }, []); + + // Not using useInteractive because the container element is just meant to + // catch events from the inner button, not act as a button itself + const onInnerButtonInteraction = () => { + if (anchorButtonRef.current === null) { + throw new Error("Usable ref value is unavailable"); + } + + setLoadedButton(anchorButtonRef.current); + }; + + const onInnerButtonKeydown = (event: KeyboardEvent) => { + if (event.key === "Enter" || event.key === " ") { + onInnerButtonInteraction(); + } + }; + + return ( + <> + {/* Cannot switch with Box component; breaks implementation */} +
+ {anchorButton} +
+ + setLoadedButton(undefined)} + anchorOrigin={{ horizontal: originX, vertical: originY }} + sx={{ + "& .MuiPaper-root": { + overflowY: "hidden", + width, + paddingY: 0, + ...sx, + }, + }} + transitionDuration={{ + enter: 300, + exit: 0, + }} + > + {children} + + + ); +} From 88d73af46c9a16cc721ce41af4461a29a299c5c9 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:21:31 +0000 Subject: [PATCH 03/25] chore: Add SearchBox --- .../components/WorkspacesButton/SearchBox.tsx | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 site/src/components/WorkspacesButton/SearchBox.tsx diff --git a/site/src/components/WorkspacesButton/SearchBox.tsx b/site/src/components/WorkspacesButton/SearchBox.tsx new file mode 100644 index 0000000000000..f90fb9b0d4eb3 --- /dev/null +++ b/site/src/components/WorkspacesButton/SearchBox.tsx @@ -0,0 +1,88 @@ +import { + type ForwardedRef, + type KeyboardEvent, + forwardRef, + useId, +} from "react"; + +import Box from "@mui/system/Box"; +import SearchIcon from "@mui/icons-material/SearchOutlined"; +import { visuallyHidden } from "@mui/utils"; +import { type SystemStyleObject } from "@mui/system"; + +type Props = { + value: string; + onValueChange: (newValue: string) => void; + + placeholder?: string; + label?: string; + onKeyDown?: (event: KeyboardEvent) => void; + sx?: SystemStyleObject; +}; + +export const SearchBox = forwardRef(function SearchBox( + { + value, + onValueChange, + onKeyDown, + label = "Search", + placeholder = "Search...", + sx = {}, + }: Props, + ref?: ForwardedRef, +) { + const hookId = useId(); + const inputId = `${hookId}-${SearchBox.name}-input`; + + return ( + `1px solid ${theme.palette.divider}`, + ...sx, + }} + onKeyDown={onKeyDown} + > + + theme.palette.text.secondary, + }} + /> + + + + {label} + + + onValueChange(e.target.value)} + sx={{ + height: "100%", + border: 0, + background: "none", + width: "100%", + outline: 0, + "&::placeholder": { + color: (theme) => theme.palette.text.secondary, + }, + }} + /> + + ); +}); From 209eed45f7c8a946bcbe60c9454212ad9a6acdff Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:23:51 +0000 Subject: [PATCH 04/25] feat: add WorkspacesButton --- .../WorkspacesButton/WorkspacesButton.tsx | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 site/src/components/WorkspacesButton/WorkspacesButton.tsx diff --git a/site/src/components/WorkspacesButton/WorkspacesButton.tsx b/site/src/components/WorkspacesButton/WorkspacesButton.tsx new file mode 100644 index 0000000000000..b9e3a8bc5e074 --- /dev/null +++ b/site/src/components/WorkspacesButton/WorkspacesButton.tsx @@ -0,0 +1,244 @@ +import { ReactNode, useState } from "react"; +import { useOrganizationId, usePermissions } from "hooks"; + +import { useQuery } from "@tanstack/react-query"; +import { type Template } from "api/typesGenerated"; +import { templates } from "api/queries/templates"; +import { Link as RouterLink } 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 { Loader } from "components/Loader/Loader"; +import { PopoverContainer } from "./PopoverContainer"; +import { OverflowY } from "./OverflowY"; +import { SearchBox } from "./SearchBox"; +import { EmptyState } from "components/EmptyState/EmptyState"; +import { Avatar } from "components/Avatar/Avatar"; + +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)) + .sort((t1, t2) => t2.active_user_count - t1.active_user_count) + .slice(0, 10); +} + +function WorkspaceResultsRow({ template }: { template: Template }) { + return ( + theme.palette.action.focus, + }, + "&:hover": { + backgroundColor: (theme) => theme.palette.action.hover, + }, + }} + > + + + {template.display_name || "-"} + + + + + {template.display_name || "[Unnamed]"} + + + 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"} + + + + + ); +} + +export function WorkspacesButton() { + const organizationId = useOrganizationId(); + const permissions = usePermissions(); + + const templatesQuery = useQuery({ + ...templates(organizationId), + + // Creating icons via the selector to guarantee icons array stays as stable + // as possible, and only changes when the query produces new data + select: (templates) => { + return { + list: templates, + icons: templates.map((t) => t.icon), + }; + }, + }); + + // 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( + templatesQuery.data?.list ?? [], + searchTerm, + ); + + let emptyState: ReactNode = undefined; + if (templatesQuery.data?.list.length === 0) { + emptyState = ( + + Create one now. + + } + /> + ); + } else if (processed.length === 0) { + emptyState = ; + } + + return ( + } variant="contained"> + Create Workspace… + + } + > + setSearchTerm(newValue)} + placeholder="Type/select a workspace template" + label="Template select for workspace" + sx={{ flexShrink: 0, columnGap: COLUMN_GAP }} + /> + + + {templatesQuery.isLoading ? ( + + ) : ( + <> + {processed.map((template) => ( + + ))} + + {emptyState} + + )} + + + {permissions.createTemplates && ( + theme.palette.action.focus, + }, + }} + > + `1px solid ${theme.palette.divider}`, + }} + > + + + + See all templates + + + )} + + ); +} From 9300b117e203fd6bb7237faa7cf03ed2ceab2d7f Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:31:10 +0000 Subject: [PATCH 05/25] chore: Install MUI utils package --- site/package.json | 1 + site/pnpm-lock.yaml | 161 ++++++++++++++++++++------------------------ 2 files changed, 73 insertions(+), 89 deletions(-) diff --git a/site/package.json b/site/package.json index 8476c50b94423..f95d9b0a9acf8 100644 --- a/site/package.json +++ b/site/package.json @@ -45,6 +45,7 @@ "@mui/material": "5.14.0", "@mui/styles": "5.14.0", "@mui/system": "5.14.0", + "@mui/utils": "5.14.11", "@tanstack/react-query": "4.35.3", "@vitejs/plugin-react": "4.1.0", "@xstate/inspect": "0.8.0", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index 13a45910805d9..7944c25e8e8cd 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -51,6 +51,9 @@ dependencies: '@mui/system': specifier: 5.14.0 version: 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) + '@mui/utils': + specifier: 5.14.11 + version: 5.14.11(@types/react@18.2.6)(react@18.2.0) '@tanstack/react-query': specifier: 4.35.3 version: 4.35.3(react-dom@18.2.0)(react@18.2.0) @@ -2112,6 +2115,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 + dev: true /@babel/runtime@7.22.6: resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==} @@ -2124,7 +2128,6 @@ packages: engines: {node: '>=6.9.0'} dependencies: regenerator-runtime: 0.14.0 - dev: true /@babel/template@7.22.15: resolution: {integrity: sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==} @@ -2239,7 +2242,7 @@ packages: resolution: {integrity: sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==} dependencies: '@babel/helper-module-imports': 7.22.5 - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@emotion/hash': 0.9.1 '@emotion/memoize': 0.8.1 '@emotion/serialize': 1.1.2 @@ -3260,10 +3263,10 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@emotion/is-prop-valid': 1.2.1 '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.14.3(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.6 clsx: 1.2.1 @@ -3284,10 +3287,10 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@emotion/is-prop-valid': 1.2.1 '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.14.3(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@popperjs/core': 2.11.8 '@types/react': 18.2.6 clsx: 1.2.1 @@ -3343,7 +3346,7 @@ packages: '@mui/material': 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@mui/system': 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.14.1(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 clsx: 1.2.1 prop-types: 15.8.1 @@ -3376,7 +3379,7 @@ packages: '@mui/core-downloads-tracker': 5.14.2 '@mui/system': 5.14.0(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(@types/react@18.2.6)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.14.1(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 '@types/react-transition-group': 4.4.6 clsx: 1.2.1 @@ -3398,8 +3401,8 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 - '@mui/utils': 5.14.3(react@18.2.0) + '@babel/runtime': 7.23.1 + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 prop-types: 15.8.1 react: 18.2.0 @@ -3418,7 +3421,7 @@ packages: '@emotion/styled': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@emotion/cache': 11.11.0 '@emotion/react': 11.11.1(@types/react@18.2.6)(react@18.2.0) '@emotion/styled': 11.11.0(@emotion/react@11.11.1)(@types/react@18.2.6)(react@18.2.0) @@ -3441,7 +3444,7 @@ packages: '@emotion/hash': 0.9.1 '@mui/private-theming': 5.13.7(@types/react@18.2.6)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.14.1(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 clsx: 1.2.1 csstype: 3.1.2 @@ -3480,7 +3483,7 @@ packages: '@mui/private-theming': 5.13.7(@types/react@18.2.6)(react@18.2.0) '@mui/styled-engine': 5.13.2(@emotion/react@11.11.1)(@emotion/styled@11.11.0)(react@18.2.0) '@mui/types': 7.2.4(@types/react@18.2.6) - '@mui/utils': 5.14.3(react@18.2.0) + '@mui/utils': 5.14.11(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 clsx: 1.2.1 csstype: 3.1.2 @@ -3499,29 +3502,19 @@ packages: '@types/react': 18.2.6 dev: false - /@mui/utils@5.14.1(react@18.2.0): - resolution: {integrity: sha512-39KHKK2JeqRmuUcLDLwM+c2XfVC136C5/yUyQXmO2PVbOb2Bol4KxtkssEqCbTwg87PSCG3f1Tb0keRsK7cVGw==} - engines: {node: '>=12.0.0'} - peerDependencies: - react: ^17.0.0 || ^18.0.0 - dependencies: - '@babel/runtime': 7.22.15 - '@types/prop-types': 15.7.5 - '@types/react-is': 18.2.1 - prop-types: 15.8.1 - react: 18.2.0 - react-is: 18.2.0 - dev: false - - /@mui/utils@5.14.3(react@18.2.0): - resolution: {integrity: sha512-gZ6Etw+ppO43GYc1HFZSLjwd4DoZoa+RrYTD25wQLfzcSoPjVoC/zZqA2Lkq0zjgwGBQOSxKZI6jfp9uXR+kgw==} + /@mui/utils@5.14.11(@types/react@18.2.6)(react@18.2.0): + resolution: {integrity: sha512-fmkIiCPKyDssYrJ5qk+dime1nlO3dmWfCtaPY/uVBqCRMBZ11JhddB9m8sjI2mgqQQwRJG5bq3biaosNdU/s4Q==} engines: {node: '>=12.0.0'} peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 react: ^17.0.0 || ^18.0.0 + peerDependenciesMeta: + '@types/react': + optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/prop-types': 15.7.5 - '@types/react-is': 18.2.1 + '@types/react': 18.2.6 prop-types: 15.8.1 react: 18.2.0 react-is: 18.2.0 @@ -3622,13 +3615,13 @@ packages: /@radix-ui/number@1.0.1: resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 dev: true /@radix-ui/primitive@1.0.1: resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 dev: true /@radix-ui/react-arrow@1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0): @@ -3644,7 +3637,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.6 '@types/react-dom': 18.2.4 @@ -3665,7 +3658,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) @@ -3685,7 +3678,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -3699,7 +3692,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -3713,7 +3706,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -3731,7 +3724,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) @@ -3752,7 +3745,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -3770,7 +3763,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0) @@ -3789,7 +3782,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 react: 18.2.0 @@ -3808,7 +3801,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-arrow': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) @@ -3838,7 +3831,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.6 '@types/react-dom': 18.2.4 @@ -3859,7 +3852,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-slot': 1.0.2(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 '@types/react-dom': 18.2.4 @@ -3880,7 +3873,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) @@ -3909,7 +3902,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/number': 1.0.1 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) @@ -3950,7 +3943,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.6 '@types/react-dom': 18.2.4 @@ -3967,7 +3960,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 react: 18.2.0 @@ -3986,7 +3979,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0) @@ -4013,7 +4006,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.6)(react@18.2.0) @@ -4036,7 +4029,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/primitive': 1.0.1 '@radix-ui/react-context': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@radix-ui/react-direction': 1.0.1(@types/react@18.2.6)(react@18.2.0) @@ -4059,7 +4052,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -4073,7 +4066,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 react: 18.2.0 @@ -4088,7 +4081,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 react: 18.2.0 @@ -4103,7 +4096,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -4117,7 +4110,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/react': 18.2.6 react: 18.2.0 dev: true @@ -4131,7 +4124,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/rect': 1.0.1 '@types/react': 18.2.6 react: 18.2.0 @@ -4146,7 +4139,7 @@ packages: '@types/react': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-use-layout-effect': 1.0.1(@types/react@18.2.6)(react@18.2.0) '@types/react': 18.2.6 react: 18.2.0 @@ -4165,7 +4158,7 @@ packages: '@types/react-dom': optional: true dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.4)(@types/react@18.2.6)(react-dom@18.2.0)(react@18.2.0) '@types/react': 18.2.6 '@types/react-dom': 18.2.4 @@ -4176,7 +4169,7 @@ packages: /@radix-ui/rect@1.0.1: resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 dev: true /@remix-run/router@1.9.0: @@ -5547,7 +5540,7 @@ packages: engines: {node: '>=14'} dependencies: '@babel/code-frame': 7.22.13 - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 '@types/aria-query': 5.0.1 aria-query: 5.1.3 chalk: 4.1.2 @@ -5917,7 +5910,7 @@ packages: /@types/node-fetch@2.6.6: resolution: {integrity: sha512-95X8guJYhfqiuVVhRFxVQcf4hW/2bCuoPwDasMf/531STFoNoWTT7YDnWdXHEZKqAGUigmpG31r2FE70LwnzJw==} dependencies: - '@types/node': 18.17.0 + '@types/node': 18.18.1 form-data: 4.0.0 dev: true @@ -5925,10 +5918,6 @@ packages: resolution: {integrity: sha512-Y1zz/LIuJek01+hlPNzzXQhmq/Z2BCP96j18MSXC0S0jSu/IG4FFxmBs7W4/lI2vPJ7foVfEB0hUVtnOjnCiTg==} dev: true - /@types/node@18.17.0: - resolution: {integrity: sha512-GXZxEtOxYGFchyUzxvKI14iff9KZ2DI+A6a37o6EQevtg6uO9t+aUZKcaC1Te5Ng1OnLM7K9NVVj+FbecD9cJg==} - dev: true - /@types/node@18.18.1: resolution: {integrity: sha512-3G42sxmm0fF2+Vtb9TJQpnjmP+uKlWvFa8KoEGquh4gqRmoUG/N0ufuhikw6HEsdG2G2oIKhog1GCTfz9v5NdQ==} @@ -5984,12 +5973,6 @@ packages: '@types/react': 18.2.6 dev: true - /@types/react-is@18.2.1: - resolution: {integrity: sha512-wyUkmaaSZEzFZivD8F2ftSyAfk6L+DfFliVj/mYdOXbVjRcS87fQJLTnhk6dRZPuJjI+9g6RZJO4PNCngUrmyw==} - dependencies: - '@types/react': 18.2.6 - dev: false - /@types/react-syntax-highlighter@15.5.5: resolution: {integrity: sha512-QH3JZQXa2usAvJvSsdSUJ4Yu4j8ReuZpgRrEW+XP+Rmosbn425YshW9iGEb/pAARm8496axHhHUPRH3UmTiB6A==} dependencies: @@ -6847,7 +6830,7 @@ packages: resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} engines: {node: '>=10', npm: '>=6'} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 cosmiconfig: 7.1.0 resolve: 1.22.4 dev: false @@ -7566,7 +7549,7 @@ packages: /css-vendor@2.0.8: resolution: {integrity: sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 is-in-browser: 1.1.3 dev: false @@ -7866,7 +7849,7 @@ packages: /dom-helpers@5.2.1: resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 csstype: 3.1.2 dev: false @@ -10553,7 +10536,7 @@ packages: /jss-plugin-camel-case@10.10.0: resolution: {integrity: sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 hyphenate-style-name: 1.0.4 jss: 10.10.0 dev: false @@ -10561,21 +10544,21 @@ packages: /jss-plugin-default-unit@10.10.0: resolution: {integrity: sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 jss: 10.10.0 dev: false /jss-plugin-global@10.10.0: resolution: {integrity: sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 jss: 10.10.0 dev: false /jss-plugin-nested@10.10.0: resolution: {integrity: sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 jss: 10.10.0 tiny-warning: 1.0.3 dev: false @@ -10583,14 +10566,14 @@ packages: /jss-plugin-props-sort@10.10.0: resolution: {integrity: sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 jss: 10.10.0 dev: false /jss-plugin-rule-value-function@10.10.0: resolution: {integrity: sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 jss: 10.10.0 tiny-warning: 1.0.3 dev: false @@ -10598,7 +10581,7 @@ packages: /jss-plugin-vendor-prefixer@10.10.0: resolution: {integrity: sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 css-vendor: 2.0.8 jss: 10.10.0 dev: false @@ -10606,7 +10589,7 @@ packages: /jss@10.10.0: resolution: {integrity: sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 csstype: 3.1.2 is-in-browser: 1.1.3 tiny-warning: 1.0.3 @@ -12234,7 +12217,7 @@ packages: peerDependencies: react: '>=16.13.1' dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 react: 18.2.0 dev: true @@ -12415,7 +12398,7 @@ packages: react: '>=16.6.0' react-dom: '>=16.6.0' dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 dom-helpers: 5.2.1 loose-envify: 1.4.0 prop-types: 15.8.1 @@ -12606,7 +12589,7 @@ packages: /regenerator-transform@0.15.2: resolution: {integrity: sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 dev: true /regexp-tree@0.1.27: @@ -12830,7 +12813,7 @@ packages: /rtl-css-js@1.16.1: resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} dependencies: - '@babel/runtime': 7.22.15 + '@babel/runtime': 7.23.1 dev: false /run-async@2.4.1: @@ -14137,7 +14120,7 @@ packages: peerDependencies: eslint: '>=7' meow: ^9.0.0 - optionator: 0.9.3 + optionator: ^0.9.1 stylelint: '>=13' typescript: '*' vite: '>=2.0.0' From 2b13f1ed4fa419ae9af8195c094030b77b9e79e0 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:32:08 +0000 Subject: [PATCH 06/25] chore: integrate WorkspacesButton --- .../WorkspacesPage/WorkspacesPageView.tsx | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index b19ed581dba73..2805b87fc3836 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,14 +1,8 @@ -import Link from "@mui/material/Link"; import { Workspace } from "api/typesGenerated"; import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"; import { ComponentProps, FC } from "react"; -import { Link as RouterLink } from "react-router-dom"; import { Margins } from "components/Margins/Margins"; -import { - PageHeader, - PageHeaderSubtitle, - PageHeaderTitle, -} from "components/PageHeader/PageHeader"; +import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"; import { Stack } from "components/Stack/Stack"; import { WorkspaceHelpTooltip } from "./WorkspaceHelpTooltip"; import { WorkspacesTable } from "pages/WorkspacesPage/WorkspacesTable"; @@ -24,6 +18,7 @@ import { import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; +import { WorkspacesButton } from "components/WorkspacesButton/WorkspacesButton"; export const Language = { pageTitle: "Workspaces", @@ -78,21 +73,13 @@ export const WorkspacesPageView: FC< return ( - + }> {Language.pageTitle} - - - {Language.createANewWorkspace} - - {Language.template} - - . - From e86207ed8a4daf08b7c310cef44fc79a30171154 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 12:57:18 +0000 Subject: [PATCH 07/25] chore: reorganize files --- .../{WorkspacesButton => OverflowY}/OverflowY.tsx | 3 +++ .../PopoverContainer.tsx | 7 +++++++ .../WorkspacesPage}/WorkspacesButton.tsx | 8 +++++--- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 2 +- .../WorkspacesPage/WorkspacesSearchBox.tsx} | 6 ++++++ 5 files changed, 22 insertions(+), 4 deletions(-) rename site/src/components/{WorkspacesButton => OverflowY}/OverflowY.tsx (92%) rename site/src/components/{WorkspacesButton => PopoverContainer}/PopoverContainer.tsx (94%) rename site/src/{components/WorkspacesButton => pages/WorkspacesPage}/WorkspacesButton.tsx (97%) rename site/src/{components/WorkspacesButton/SearchBox.tsx => pages/WorkspacesPage/WorkspacesSearchBox.tsx} (90%) diff --git a/site/src/components/WorkspacesButton/OverflowY.tsx b/site/src/components/OverflowY/OverflowY.tsx similarity index 92% rename from site/src/components/WorkspacesButton/OverflowY.tsx rename to site/src/components/OverflowY/OverflowY.tsx index f8b77ff67a049..f7d1357f5e905 100644 --- a/site/src/components/WorkspacesButton/OverflowY.tsx +++ b/site/src/components/OverflowY/OverflowY.tsx @@ -1,3 +1,6 @@ +/** + * @file Provides reusable vertical overflow behavior. + */ import { type ReactNode } from "react"; import { type SystemStyleObject } from "@mui/system"; import Box from "@mui/system/Box"; diff --git a/site/src/components/WorkspacesButton/PopoverContainer.tsx b/site/src/components/PopoverContainer/PopoverContainer.tsx similarity index 94% rename from site/src/components/WorkspacesButton/PopoverContainer.tsx rename to site/src/components/PopoverContainer/PopoverContainer.tsx index c8fd233728c62..8ff27a63b5cf1 100644 --- a/site/src/components/WorkspacesButton/PopoverContainer.tsx +++ b/site/src/components/PopoverContainer/PopoverContainer.tsx @@ -1,3 +1,10 @@ +/** + * @file Abstracts over MUI's Popover component to simplify using it (and hide) + * some of the wonkier parts of the API. + * + * Just place a button and some content in the component, and things just work. + * No setup needed with hooks or refs. + */ import { type KeyboardEvent, type ReactElement, diff --git a/site/src/components/WorkspacesButton/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx similarity index 97% rename from site/src/components/WorkspacesButton/WorkspacesButton.tsx rename to site/src/pages/WorkspacesPage/WorkspacesButton.tsx index b9e3a8bc5e074..a95804e58d218 100644 --- a/site/src/components/WorkspacesButton/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -4,18 +4,20 @@ import { useOrganizationId, usePermissions } from "hooks"; import { useQuery } from "@tanstack/react-query"; import { type Template } from "api/typesGenerated"; import { templates } from "api/queries/templates"; + import { Link as RouterLink } 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 { Loader } from "components/Loader/Loader"; -import { PopoverContainer } from "./PopoverContainer"; -import { OverflowY } from "./OverflowY"; -import { SearchBox } from "./SearchBox"; +import { PopoverContainer } from "components/PopoverContainer/PopoverContainer"; +import { OverflowY } from "components/OverflowY/OverflowY"; import { EmptyState } from "components/EmptyState/EmptyState"; import { Avatar } from "components/Avatar/Avatar"; +import { SearchBox } from "./WorkspacesSearchBox"; const ICON_SIZE = 18; const COLUMN_GAP = 1.5; diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 2805b87fc3836..0b934a67c9e11 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -18,7 +18,7 @@ import { import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; -import { WorkspacesButton } from "components/WorkspacesButton/WorkspacesButton"; +import { WorkspacesButton } from "./WorkspacesButton"; export const Language = { pageTitle: "Workspaces", diff --git a/site/src/components/WorkspacesButton/SearchBox.tsx b/site/src/pages/WorkspacesPage/WorkspacesSearchBox.tsx similarity index 90% rename from site/src/components/WorkspacesButton/SearchBox.tsx rename to site/src/pages/WorkspacesPage/WorkspacesSearchBox.tsx index f90fb9b0d4eb3..297ae5699fbdc 100644 --- a/site/src/components/WorkspacesButton/SearchBox.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesSearchBox.tsx @@ -1,3 +1,9 @@ +/** + * @file Defines a controlled searchbox component for processing form state. + * + * Not defined as a top-level component just yet, because it's not clear how + * reusable this is outside of workspace dropdowns. + */ import { type ForwardedRef, type KeyboardEvent, From 5b249d580736ef745340c84db64f6de84a8451a5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 13:00:51 +0000 Subject: [PATCH 08/25] fix: resolve hover state visual glitch --- site/src/pages/WorkspacesPage/WorkspacesButton.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index a95804e58d218..f6b79b03c7aa2 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -47,12 +47,12 @@ function WorkspaceResultsRow({ template }: { template: Template }) { // reasons; avoids extra clicks on the user's part to={`/templates/${template.name}/workspace`} sx={{ - textDecoration: "none", outline: "none", "&:focus": { backgroundColor: (theme) => theme.palette.action.focus, }, "&:hover": { + textDecoration: "none", backgroundColor: (theme) => theme.palette.action.hover, }, }} From b1e1271710d794f389419b859d72585289474662 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 19:27:29 +0000 Subject: [PATCH 09/25] chore: Add story for OverflowY --- .../OverflowY/OverflowY.stories.tsx | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 site/src/components/OverflowY/OverflowY.stories.tsx diff --git a/site/src/components/OverflowY/OverflowY.stories.tsx b/site/src/components/OverflowY/OverflowY.stories.tsx new file mode 100644 index 0000000000000..2a44e82159a99 --- /dev/null +++ b/site/src/components/OverflowY/OverflowY.stories.tsx @@ -0,0 +1,33 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { OverflowY } from "./OverflowY"; + +const numbers: number[] = []; +for (let i = 0; i < 20; i++) { + numbers.push(i + 1); +} + +const meta: Meta = { + title: `components/${OverflowY.name}`, + component: OverflowY, + args: { + maxHeight: 400, + children: numbers.map((num, i) => ( +

+ Element {num} +

+ )), + }, +}; + +export default meta; + +type Story = StoryObj; +export const Example: Story = {}; From b367495fa21c66637af28a2462a53d368c312f56 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 19:30:12 +0000 Subject: [PATCH 10/25] fix: remove dynamic name from OverflowY story --- site/src/components/OverflowY/OverflowY.stories.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/OverflowY/OverflowY.stories.tsx b/site/src/components/OverflowY/OverflowY.stories.tsx index 2a44e82159a99..e472a38397447 100644 --- a/site/src/components/OverflowY/OverflowY.stories.tsx +++ b/site/src/components/OverflowY/OverflowY.stories.tsx @@ -7,7 +7,7 @@ for (let i = 0; i < 20; i++) { } const meta: Meta = { - title: `components/${OverflowY.name}`, + title: "components/OverflowY", component: OverflowY, args: { maxHeight: 400, From 5a347693c084c4c4bf7e87c6ce0bce60a1f76308 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 19:46:03 +0000 Subject: [PATCH 11/25] chore: update stories again --- .../OverflowY/OverflowY.stories.tsx | 3 ++- .../PopoverContainer.stories.tsx | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 site/src/components/PopoverContainer/PopoverContainer.stories.tsx diff --git a/site/src/components/OverflowY/OverflowY.stories.tsx b/site/src/components/OverflowY/OverflowY.stories.tsx index e472a38397447..128fdb6652b92 100644 --- a/site/src/components/OverflowY/OverflowY.stories.tsx +++ b/site/src/components/OverflowY/OverflowY.stories.tsx @@ -14,10 +14,11 @@ const meta: Meta = { children: numbers.map((num, i) => (

diff --git a/site/src/components/PopoverContainer/PopoverContainer.stories.tsx b/site/src/components/PopoverContainer/PopoverContainer.stories.tsx new file mode 100644 index 0000000000000..c466be5bdb674 --- /dev/null +++ b/site/src/components/PopoverContainer/PopoverContainer.stories.tsx @@ -0,0 +1,23 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { PopoverContainer } from "./PopoverContainer"; +import Button from "@mui/material/Button"; + +const numbers: number[] = []; +for (let i = 0; i < 20; i++) { + numbers.push(i + 1); +} + +const meta: Meta = { + title: "components/PopoverContainer", + component: PopoverContainer, + args: { + anchorButton: , + children:

Hiya!

, + originY: "bottom", + }, +}; + +export default meta; + +type Story = StoryObj; +export const Example: Story = {}; From b9f6cb8132abbb0150ed6a6a430b3bf119ce6d55 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Tue, 3 Oct 2023 20:04:15 +0000 Subject: [PATCH 12/25] fix: remove all references to icons (for now) --- .../pages/WorkspacesPage/WorkspacesButton.tsx | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index f6b79b03c7aa2..95bba3945736b 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -130,30 +130,18 @@ function WorkspaceResultsRow({ template }: { template: Template }) { export function WorkspacesButton() { const organizationId = useOrganizationId(); const permissions = usePermissions(); - - const templatesQuery = useQuery({ - ...templates(organizationId), - - // Creating icons via the selector to guarantee icons array stays as stable - // as possible, and only changes when the query produces new data - select: (templates) => { - return { - list: templates, - icons: templates.map((t) => t.icon), - }; - }, - }); + const templatesQuery = useQuery(templates(organizationId)); // 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( - templatesQuery.data?.list ?? [], + templatesQuery.data ?? [], searchTerm, ); let emptyState: ReactNode = undefined; - if (templatesQuery.data?.list.length === 0) { + if (templatesQuery.data?.length === 0) { emptyState = ( Date: Wed, 4 Oct 2023 19:07:18 +0000 Subject: [PATCH 13/25] refactor: move flex shrink to be OverflowY concern --- site/src/components/OverflowY/OverflowY.tsx | 1 + site/src/pages/WorkspacesPage/WorkspacesButton.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/OverflowY/OverflowY.tsx b/site/src/components/OverflowY/OverflowY.tsx index f7d1357f5e905..1252287a94f97 100644 --- a/site/src/components/OverflowY/OverflowY.tsx +++ b/site/src/components/OverflowY/OverflowY.tsx @@ -29,6 +29,7 @@ export function OverflowY({ children, height, maxHeight, sx }: Props) { height: computedHeight, maxHeight: computedMaxHeight, overflowY: "auto", + flexShrink: 1, ...sx, }} > diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index 95bba3945736b..1653eb0dc0684 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -180,7 +180,6 @@ export function WorkspacesButton() { Date: Wed, 4 Oct 2023 21:51:51 +0000 Subject: [PATCH 14/25] fix: remove needless render key --- site/src/pages/WorkspacesPage/WorkspacesButton.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index 1653eb0dc0684..232fc693a1bc5 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -41,7 +41,6 @@ function sortTemplatesByUsersDesc( function WorkspaceResultsRow({ template }: { template: Template }) { return ( Date: Thu, 5 Oct 2023 13:20:37 +0000 Subject: [PATCH 15/25] fix: make sure popover closes before navigation --- .../PopoverContainer/PopoverContainer.tsx | 161 ++++++++++++++---- .../pages/WorkspacesPage/WorkspacesButton.tsx | 26 +-- 2 files changed, 135 insertions(+), 52 deletions(-) diff --git a/site/src/components/PopoverContainer/PopoverContainer.tsx b/site/src/components/PopoverContainer/PopoverContainer.tsx index 8ff27a63b5cf1..8a72c76156a7d 100644 --- a/site/src/components/PopoverContainer/PopoverContainer.tsx +++ b/site/src/components/PopoverContainer/PopoverContainer.tsx @@ -1,41 +1,123 @@ /** - * @file Abstracts over MUI's Popover component to simplify using it (and hide) - * some of the wonkier parts of the API. + * @file Abstracts over MUI's Popover component to simplify using it (and hide + * some of the wonkier parts of the API). * * Just place a button and some content in the component, and things just work. * No setup needed with hooks or refs. */ import { type KeyboardEvent, + type MouseEvent, + type PropsWithChildren, type ReactElement, + createContext, + useCallback, + useContext, useEffect, useRef, useState, - PropsWithChildren, } from "react"; -import { type Theme, type SystemStyleObject } from "@mui/system"; +import { type Theme, type SystemStyleObject, Box } from "@mui/system"; import Popover, { type PopoverOrigin } from "@mui/material/Popover"; +import { useNavigate, type LinkProps } from "react-router-dom"; +import { useTheme } from "@emotion/react"; -type Props = PropsWithChildren<{ +function getButton(container: HTMLElement) { + return ( + container.querySelector("button") ?? + container.querySelector('[aria-role="button"]') + ); +} + +const ClosePopoverContext = createContext<(() => void) | null>(null); + +type PopoverLinkProps = LinkProps & { + to: string; + sx?: SystemStyleObject; +}; + +/** + * A custom version of a React Router Link that makes sure to close the popover + * before starting a navigation. + * + * This is necessary because React Router's navigation logic doesn't work well + * with modals (including MUI's base Popover component). + * + * --- + * If the page being navigated to has lazy loading and isn't available yet, the + * previous components are supposed to be hidden during the transition, but + * because most React modals use React.createPortal to put content outside of + * the main DOM tree, React Router has no way of knowing about them. So open + * modals have a high risk of not disappearing until the page transition + * finishes and the previous components fully unmount. + */ +export function PopoverLink({ + children, + to, + sx, + ...linkProps +}: PopoverLinkProps) { + const closePopover = useContext(ClosePopoverContext); + if (closePopover === null) { + throw new Error("PopoverLink is not located inside of a PopoverContainer"); + } + + // Luckily, useNavigate and Link are designed to be imperative/declarative + // mirrors of each other, so their inputs should never get out of sync + const navigate = useNavigate(); + const theme = useTheme(); + + const onClick = (event: MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + closePopover(); + + // Hacky, but by using a promise to push the navigation to resolve via the + // micro-task queue, there's guaranteed to be a period for the popover to + // close. Tried React DOM's flushSync function, but it was unreliable. + void Promise.resolve().then(() => { + navigate(to, linkProps); + }); + }; + + return ( + + {children} + + ); +} + +type PopoverContainerProps = PropsWithChildren<{ /** * Does not require any hooks or refs to work. Also does not override any refs * or event handlers attached to the button. */ anchorButton: ReactElement; + width?: number; originX?: PopoverOrigin["horizontal"]; originY?: PopoverOrigin["vertical"]; sx?: SystemStyleObject; }>; -function getButton(container: HTMLElement) { - return ( - container.querySelector("button") ?? - container.querySelector('[aria-role="button"]') - ); -} - export function PopoverContainer({ children, anchorButton, @@ -43,7 +125,14 @@ export function PopoverContainer({ originY = 0, width = 320, sx = {}, -}: Props) { +}: PopoverContainerProps) { + const parentClosePopover = useContext(ClosePopoverContext); + if (parentClosePopover !== null) { + throw new Error( + "Popover detected inside of Popover - this will always be a bad user experience", + ); + } + const buttonContainerRef = useRef(null); // Ref value is for effects and event listeners; state value is for React @@ -107,6 +196,10 @@ export function PopoverContainer({ } }; + const closePopover = useCallback(() => { + setLoadedButton(undefined); + }, []); + return ( <> {/* Cannot switch with Box component; breaks implementation */} @@ -124,26 +217,28 @@ export function PopoverContainer({ {anchorButton} - setLoadedButton(undefined)} - anchorOrigin={{ horizontal: originX, vertical: originY }} - sx={{ - "& .MuiPaper-root": { - overflowY: "hidden", - width, - paddingY: 0, - ...sx, - }, - }} - transitionDuration={{ - enter: 300, - exit: 0, - }} - > - {children} - + + + {children} + + ); } diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index 232fc693a1bc5..ed3ae936f34d0 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useState } from "react"; +import { type ReactNode, useState } from "react"; import { useOrganizationId, usePermissions } from "hooks"; import { useQuery } from "@tanstack/react-query"; @@ -13,11 +13,14 @@ import AddIcon from "@mui/icons-material/AddOutlined"; import OpenIcon from "@mui/icons-material/OpenInNewOutlined"; import { Loader } from "components/Loader/Loader"; -import { PopoverContainer } from "components/PopoverContainer/PopoverContainer"; 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"; const ICON_SIZE = 18; const COLUMN_GAP = 1.5; @@ -40,22 +43,7 @@ function sortTemplatesByUsersDesc( function WorkspaceResultsRow({ template }: { template: Template }) { return ( - theme.palette.action.focus, - }, - "&:hover": { - textDecoration: "none", - backgroundColor: (theme) => theme.palette.action.hover, - }, - }} - > +
- + ); } From 1a0112746cdfd292fcb591b594e87a7b15bd69e2 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 13:41:23 +0000 Subject: [PATCH 16/25] refactor: clean up WorkspacesButton to use more native MUI --- coderd/database/queries.sql.go | 226 +++++++++--------- .../pages/WorkspacesPage/WorkspacesButton.tsx | 24 +- 2 files changed, 125 insertions(+), 125 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 40b7ee7e72715..f1ea7719dfdaf 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9554,6 +9554,119 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10502,116 +10615,3 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } - -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index ed3ae936f34d0..d086309b788d4 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -11,6 +11,7 @@ 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"; @@ -21,6 +22,7 @@ import { PopoverContainer, PopoverLink, } from "components/PopoverContainer/PopoverContainer"; +import { useTheme } from "@emotion/react"; const ICON_SIZE = 18; const COLUMN_GAP = 1.5; @@ -42,6 +44,8 @@ function sortTemplatesByUsersDesc( } function WorkspaceResultsRow({ template }: { template: Template }) { + const theme = useTheme(); + return ( - {template.display_name || "[Unnamed]"} - + theme.palette.text.secondary, + color: theme.palette.text.secondary, }} > {/* @@ -118,6 +117,7 @@ export function WorkspacesButton() { const organizationId = useOrganizationId(); const permissions = usePermissions(); const templatesQuery = useQuery(templates(organizationId)); + 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 @@ -192,7 +192,7 @@ export function WorkspacesButton() { sx={{ outline: "none", "&:focus": { - backgroundColor: (theme) => theme.palette.action.focus, + backgroundColor: theme.palette.action.focus, }, }} > @@ -203,7 +203,7 @@ export function WorkspacesButton() { flexFlow: "row nowrap", alignItems: "center", columnGap: COLUMN_GAP, - borderTop: (theme) => `1px solid ${theme.palette.divider}`, + borderTop: `1px solid ${theme.palette.divider}`, }} > From 55c606134b8e83506811bb0d7735104aa2d3d829 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 13:55:53 +0000 Subject: [PATCH 17/25] fix: update integration into rest of view --- site/src/components/PageHeader/PageHeader.tsx | 1 - site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/site/src/components/PageHeader/PageHeader.tsx b/site/src/components/PageHeader/PageHeader.tsx index 1202c7adbd1d0..f963560958ea0 100644 --- a/site/src/components/PageHeader/PageHeader.tsx +++ b/site/src/components/PageHeader/PageHeader.tsx @@ -36,7 +36,6 @@ export const PageHeader: FC> = ({ marginLeft: "auto", [theme.breakpoints.down("md")]: { - marginTop: theme.spacing(3), marginLeft: "initial", width: "100%", }, diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 0b934a67c9e11..e93ec9157e849 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -73,7 +73,7 @@ export const WorkspacesPageView: FC< return ( - }> + }> {Language.pageTitle} From 5aea06f29676ab6c6f6db33a09dd8464fd4fcc57 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 14:07:23 +0000 Subject: [PATCH 18/25] fix: remove JS security concern --- site/src/components/PopoverContainer/PopoverContainer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/components/PopoverContainer/PopoverContainer.tsx b/site/src/components/PopoverContainer/PopoverContainer.tsx index 8a72c76156a7d..833d8b267d5fe 100644 --- a/site/src/components/PopoverContainer/PopoverContainer.tsx +++ b/site/src/components/PopoverContainer/PopoverContainer.tsx @@ -85,7 +85,7 @@ export function PopoverLink({ Date: Thu, 5 Oct 2023 14:07:44 +0000 Subject: [PATCH 19/25] refactor: parameterize button language --- .../pages/WorkspacesPage/WorkspacesButton.tsx | 16 ++++++++++++---- .../pages/WorkspacesPage/WorkspacesPageView.tsx | 11 +++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index d086309b788d4..8a686fc4262e1 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useState } from "react"; +import { type ReactNode, useState, ReactElement } from "react"; import { useOrganizationId, usePermissions } from "hooks"; import { useQuery } from "@tanstack/react-query"; @@ -113,7 +113,15 @@ function WorkspaceResultsRow({ template }: { template: Template }) { ); } -export function WorkspacesButton() { +type WorkspacesButtonProps = { + children: string | ReactElement; + seeMoreTemplatesText: string | ReactElement; +}; + +export function WorkspacesButton({ + children, + seeMoreTemplatesText, +}: WorkspacesButtonProps) { const organizationId = useOrganizationId(); const permissions = usePermissions(); const templatesQuery = useQuery(templates(organizationId)); @@ -152,7 +160,7 @@ export function WorkspacesButton() { sx={{ display: "flex", flexFlow: "column nowrap" }} anchorButton={ } > @@ -211,7 +219,7 @@ export function WorkspacesButton() { sx={{ fontSize: "16px", marginX: "auto", display: "block" }} /> - See all templates + {seeMoreTemplatesText} )} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index e93ec9157e849..6564c69baf1af 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -25,7 +25,8 @@ export const Language = { yourWorkspacesButton: "Your workspaces", allWorkspacesButton: "All workspaces", runningWorkspacesButton: "Running workspaces", - createANewWorkspace: `Create a new workspace from a `, + createWorkspace: <>Create Workspace…, + moreTemplates: "See all templates", template: "Template", }; @@ -73,7 +74,13 @@ export const WorkspacesPageView: FC< return ( - }> + + {Language.createWorkspace} + + } + > {Language.pageTitle} From 12ac56f6060b4c89224f12f91a3d99754a77cc7a Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 14:18:22 +0000 Subject: [PATCH 20/25] revert: undo sql/go file change --- coderd/database/queries.sql.go | 226 ++++++++++++++++----------------- 1 file changed, 113 insertions(+), 113 deletions(-) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 687a411c9ec84..a2949bb542f3d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -9561,119 +9561,6 @@ func (q *sqlQuerier) InsertWorkspaceResourceMetadata(ctx context.Context, arg In return items, nil } -const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many -SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) -` - -func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many -INSERT INTO - workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) -SELECT - $1 :: uuid AS workspace_agent_id, - $2 :: timestamptz AS created_at, - unnest($3 :: uuid [ ]) AS log_source_id, - unnest($4 :: text [ ]) AS log_path, - unnest($5 :: text [ ]) AS script, - unnest($6 :: text [ ]) AS cron, - unnest($7 :: boolean [ ]) AS start_blocks_login, - unnest($8 :: boolean [ ]) AS run_on_start, - unnest($9 :: boolean [ ]) AS run_on_stop, - unnest($10 :: integer [ ]) AS timeout_seconds -RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds -` - -type InsertWorkspaceAgentScriptsParams struct { - WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` - CreatedAt time.Time `db:"created_at" json:"created_at"` - LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` - LogPath []string `db:"log_path" json:"log_path"` - Script []string `db:"script" json:"script"` - Cron []string `db:"cron" json:"cron"` - StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` - RunOnStart []bool `db:"run_on_start" json:"run_on_start"` - RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` - TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` -} - -func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { - rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, - arg.WorkspaceAgentID, - arg.CreatedAt, - pq.Array(arg.LogSourceID), - pq.Array(arg.LogPath), - pq.Array(arg.Script), - pq.Array(arg.Cron), - pq.Array(arg.StartBlocksLogin), - pq.Array(arg.RunOnStart), - pq.Array(arg.RunOnStop), - pq.Array(arg.TimeoutSeconds), - ) - if err != nil { - return nil, err - } - defer rows.Close() - var items []WorkspaceAgentScript - for rows.Next() { - var i WorkspaceAgentScript - if err := rows.Scan( - &i.WorkspaceAgentID, - &i.LogSourceID, - &i.LogPath, - &i.CreatedAt, - &i.Script, - &i.Cron, - &i.StartBlocksLogin, - &i.RunOnStart, - &i.RunOnStop, - &i.TimeoutSeconds, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - const getDeploymentWorkspaceStats = `-- name: GetDeploymentWorkspaceStats :one WITH workspaces_with_jobs AS ( SELECT @@ -10602,3 +10489,116 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C _, err := q.db.ExecContext(ctx, updateWorkspacesDormantDeletingAtByTemplateID, arg.TimeTilDormantAutodeleteMs, arg.DormantAt, arg.TemplateID) return err } + +const getWorkspaceAgentScriptsByAgentIDs = `-- name: GetWorkspaceAgentScriptsByAgentIDs :many +SELECT workspace_agent_id, log_source_id, log_path, created_at, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds FROM workspace_agent_scripts WHERE workspace_agent_id = ANY($1 :: uuid [ ]) +` + +func (q *sqlQuerier) GetWorkspaceAgentScriptsByAgentIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentScriptsByAgentIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const insertWorkspaceAgentScripts = `-- name: InsertWorkspaceAgentScripts :many +INSERT INTO + workspace_agent_scripts (workspace_agent_id, created_at, log_source_id, log_path, script, cron, start_blocks_login, run_on_start, run_on_stop, timeout_seconds) +SELECT + $1 :: uuid AS workspace_agent_id, + $2 :: timestamptz AS created_at, + unnest($3 :: uuid [ ]) AS log_source_id, + unnest($4 :: text [ ]) AS log_path, + unnest($5 :: text [ ]) AS script, + unnest($6 :: text [ ]) AS cron, + unnest($7 :: boolean [ ]) AS start_blocks_login, + unnest($8 :: boolean [ ]) AS run_on_start, + unnest($9 :: boolean [ ]) AS run_on_stop, + unnest($10 :: integer [ ]) AS timeout_seconds +RETURNING workspace_agent_scripts.workspace_agent_id, workspace_agent_scripts.log_source_id, workspace_agent_scripts.log_path, workspace_agent_scripts.created_at, workspace_agent_scripts.script, workspace_agent_scripts.cron, workspace_agent_scripts.start_blocks_login, workspace_agent_scripts.run_on_start, workspace_agent_scripts.run_on_stop, workspace_agent_scripts.timeout_seconds +` + +type InsertWorkspaceAgentScriptsParams struct { + WorkspaceAgentID uuid.UUID `db:"workspace_agent_id" json:"workspace_agent_id"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + LogSourceID []uuid.UUID `db:"log_source_id" json:"log_source_id"` + LogPath []string `db:"log_path" json:"log_path"` + Script []string `db:"script" json:"script"` + Cron []string `db:"cron" json:"cron"` + StartBlocksLogin []bool `db:"start_blocks_login" json:"start_blocks_login"` + RunOnStart []bool `db:"run_on_start" json:"run_on_start"` + RunOnStop []bool `db:"run_on_stop" json:"run_on_stop"` + TimeoutSeconds []int32 `db:"timeout_seconds" json:"timeout_seconds"` +} + +func (q *sqlQuerier) InsertWorkspaceAgentScripts(ctx context.Context, arg InsertWorkspaceAgentScriptsParams) ([]WorkspaceAgentScript, error) { + rows, err := q.db.QueryContext(ctx, insertWorkspaceAgentScripts, + arg.WorkspaceAgentID, + arg.CreatedAt, + pq.Array(arg.LogSourceID), + pq.Array(arg.LogPath), + pq.Array(arg.Script), + pq.Array(arg.Cron), + pq.Array(arg.StartBlocksLogin), + pq.Array(arg.RunOnStart), + pq.Array(arg.RunOnStop), + pq.Array(arg.TimeoutSeconds), + ) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgentScript + for rows.Next() { + var i WorkspaceAgentScript + if err := rows.Scan( + &i.WorkspaceAgentID, + &i.LogSourceID, + &i.LogPath, + &i.CreatedAt, + &i.Script, + &i.Cron, + &i.StartBlocksLogin, + &i.RunOnStart, + &i.RunOnStop, + &i.TimeoutSeconds, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} From 99f2656adf7a8a08a4ee3605a4cefbb0c8003a7e Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 18:37:47 +0000 Subject: [PATCH 21/25] fix: remove permissions dependency --- .../pages/WorkspacesPage/WorkspacesButton.tsx | 60 +++++++++---------- .../WorkspacesPage/WorkspacesPageView.tsx | 6 +- 2 files changed, 29 insertions(+), 37 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index 8a686fc4262e1..a6a7471dc0bbe 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,5 +1,6 @@ import { type ReactNode, useState, ReactElement } from "react"; -import { useOrganizationId, usePermissions } from "hooks"; +import { useOrganizationId } from "hooks"; +import { Language } from "./WorkspacesPageView"; import { useQuery } from "@tanstack/react-query"; import { type Template } from "api/typesGenerated"; @@ -115,15 +116,10 @@ function WorkspaceResultsRow({ template }: { template: Template }) { type WorkspacesButtonProps = { children: string | ReactElement; - seeMoreTemplatesText: string | ReactElement; }; -export function WorkspacesButton({ - children, - seeMoreTemplatesText, -}: WorkspacesButtonProps) { +export function WorkspacesButton({ children }: WorkspacesButtonProps) { const organizationId = useOrganizationId(); - const permissions = usePermissions(); const templatesQuery = useQuery(templates(organizationId)); const theme = useTheme(); @@ -193,36 +189,34 @@ export function WorkspacesButton({ )} - {permissions.createTemplates && ( - + - - - - - {seeMoreTemplatesText} + + - - )} + {Language.seeAllTemplates} + + ); } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 6564c69baf1af..41b107255b822 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -26,7 +26,7 @@ export const Language = { allWorkspacesButton: "All workspaces", runningWorkspacesButton: "Running workspaces", createWorkspace: <>Create Workspace…, - moreTemplates: "See all templates", + seeAllTemplates: "See all templates", template: "Template", }; @@ -76,9 +76,7 @@ export const WorkspacesPageView: FC< - {Language.createWorkspace} - + {Language.createWorkspace} } > From 430e30cff732c1ec4072719d64fd9e6ecdc51fa2 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 18:38:47 +0000 Subject: [PATCH 22/25] fix: simplify button prop types --- site/src/pages/WorkspacesPage/WorkspacesButton.tsx | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index a6a7471dc0bbe..e429a84aa8157 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,4 +1,4 @@ -import { type ReactNode, useState, ReactElement } from "react"; +import { type PropsWithChildren, type ReactNode, useState } from "react"; import { useOrganizationId } from "hooks"; import { Language } from "./WorkspacesPageView"; @@ -114,11 +114,7 @@ function WorkspaceResultsRow({ template }: { template: Template }) { ); } -type WorkspacesButtonProps = { - children: string | ReactElement; -}; - -export function WorkspacesButton({ children }: WorkspacesButtonProps) { +export function WorkspacesButton({ children }: PropsWithChildren) { const organizationId = useOrganizationId(); const templatesQuery = useQuery(templates(organizationId)); const theme = useTheme(); From ff37ab512aed51fe10be459030c47429db8844e5 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 19:02:07 +0000 Subject: [PATCH 23/25] fix: lift data dependencies to page component --- .../pages/WorkspacesPage/WorkspacesButton.tsx | 17 ++++++++++------- .../src/pages/WorkspacesPage/WorkspacesPage.tsx | 13 +++++++++++-- .../pages/WorkspacesPage/WorkspacesPageView.tsx | 9 +++++++-- 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index e429a84aa8157..b137c1523c5ed 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,10 +1,8 @@ import { type PropsWithChildren, type ReactNode, useState } from "react"; -import { useOrganizationId } from "hooks"; import { Language } from "./WorkspacesPageView"; -import { useQuery } from "@tanstack/react-query"; +import { UseQueryResult } from "@tanstack/react-query"; import { type Template } from "api/typesGenerated"; -import { templates } from "api/queries/templates"; import { Link as RouterLink } from "react-router-dom"; import Box from "@mui/system/Box"; @@ -114,9 +112,14 @@ function WorkspaceResultsRow({ template }: { template: Template }) { ); } -export function WorkspacesButton({ children }: PropsWithChildren) { - const organizationId = useOrganizationId(); - const templatesQuery = useQuery(templates(organizationId)); +type WorkspacesButtonProps = PropsWithChildren<{ + templatesQuery: UseQueryResult; +}>; + +export function WorkspacesButton({ + children, + templatesQuery, +}: WorkspacesButtonProps) { const theme = useTheme(); // Dataset should always be small enough that client-side filtering should be @@ -172,7 +175,7 @@ export function WorkspacesButton({ children }: PropsWithChildren) { paddingY: 1, }} > - {templatesQuery.isLoading ? ( + {status === "loading" ? ( ) : ( <> diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 5273cd2f4486f..bf361dceb94a2 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -22,6 +22,8 @@ import TextField from "@mui/material/TextField"; import { displayError } from "components/GlobalSnackbar/utils"; import { getErrorMessage } from "api/errors"; import { useEffectEvent } from "hooks/hookPolyfills"; +import { useQuery } from "@tanstack/react-query"; +import { templates } from "api/queries/templates"; function useSafeSearchParams() { // Have to wrap setSearchParams because React Router doesn't make sure that @@ -44,8 +46,13 @@ const WorkspacesPage: FC = () => { // each hook. const searchParamsResult = useSafeSearchParams(); const pagination = usePagination({ searchParamsResult }); + + const organizationId = useOrganizationId(); + const templatesQuery = useQuery(templates(organizationId)); + const filterProps = useWorkspacesFilter({ searchParamsResult, + organizationId, onFilterChange: () => pagination.goToPage(1), }); @@ -107,6 +114,7 @@ const WorkspacesPage: FC = () => { checkedWorkspaces={checkedWorkspaces} onCheckChange={setCheckedWorkspaces} canCheckWorkspaces={canCheckWorkspaces} + templatesQuery={templatesQuery} workspaces={data?.workspaces} dormantWorkspaces={dormantWorkspaces} error={error} @@ -143,11 +151,13 @@ export default WorkspacesPage; type UseWorkspacesFilterOptions = { searchParamsResult: ReturnType; onFilterChange: () => void; + organizationId: string; }; const useWorkspacesFilter = ({ searchParamsResult, onFilterChange, + organizationId, }: UseWorkspacesFilterOptions) => { const filter = useFilter({ fallbackFilter: "owner:me", @@ -164,9 +174,8 @@ const useWorkspacesFilter = ({ enabled: canFilterByUser, }); - const orgId = useOrganizationId(); const templateMenu = useTemplateFilterMenu({ - orgId, + orgId: organizationId, value: filter.values.template, onChange: (option) => filter.update({ ...filter.values, template: option?.value }), diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 41b107255b822..bf10fa730a54f 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -1,4 +1,4 @@ -import { Workspace } from "api/typesGenerated"; +import { Template, Workspace } from "api/typesGenerated"; import { PaginationWidgetBase } from "components/PaginationWidget/PaginationWidgetBase"; import { ComponentProps, FC } from "react"; import { Margins } from "components/Margins/Margins"; @@ -19,6 +19,7 @@ import Box from "@mui/material/Box"; import Button from "@mui/material/Button"; import DeleteOutlined from "@mui/icons-material/DeleteOutlined"; import { WorkspacesButton } from "./WorkspacesButton"; +import { UseQueryResult } from "@tanstack/react-query"; export const Language = { pageTitle: "Workspaces", @@ -44,6 +45,7 @@ export interface WorkspacesPageViewProps { onCheckChange: (checkedWorkspaces: Workspace[]) => void; onDeleteAll: () => void; canCheckWorkspaces: boolean; + templatesQuery: UseQueryResult; } export const WorkspacesPageView: FC< @@ -62,6 +64,7 @@ export const WorkspacesPageView: FC< onCheckChange, onDeleteAll, canCheckWorkspaces, + templatesQuery, }) => { const { saveLocal } = useLocalStorage(); @@ -76,7 +79,9 @@ export const WorkspacesPageView: FC< {Language.createWorkspace} + + {Language.createWorkspace} + } > From 1b934ab0f07140e80f8a3004625633e752fb1984 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 19:05:40 +0000 Subject: [PATCH 24/25] refactor: clean up props --- .../pages/WorkspacesPage/WorkspacesButton.tsx | 19 ++++++++----------- .../WorkspacesPage/WorkspacesPageView.tsx | 5 ++++- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index b137c1523c5ed..1181a68ab78fe 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,7 +1,6 @@ import { type PropsWithChildren, type ReactNode, useState } from "react"; +import { useTheme } from "@emotion/react"; import { Language } from "./WorkspacesPageView"; - -import { UseQueryResult } from "@tanstack/react-query"; import { type Template } from "api/typesGenerated"; import { Link as RouterLink } from "react-router-dom"; @@ -21,7 +20,6 @@ import { PopoverContainer, PopoverLink, } from "components/PopoverContainer/PopoverContainer"; -import { useTheme } from "@emotion/react"; const ICON_SIZE = 18; const COLUMN_GAP = 1.5; @@ -113,25 +111,24 @@ function WorkspaceResultsRow({ template }: { template: Template }) { } type WorkspacesButtonProps = PropsWithChildren<{ - templatesQuery: UseQueryResult; + isLoadingTemplates: boolean; + templates: readonly Template[]; }>; export function WorkspacesButton({ children, - templatesQuery, + isLoadingTemplates, + 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( - templatesQuery.data ?? [], - searchTerm, - ); + const processed = sortTemplatesByUsersDesc(templates, searchTerm); let emptyState: ReactNode = undefined; - if (templatesQuery.data?.length === 0) { + if (templates.length === 0) { emptyState = ( - {status === "loading" ? ( + {isLoadingTemplates ? ( ) : ( <> diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index bf10fa730a54f..def386a6808c9 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -79,7 +79,10 @@ export const WorkspacesPageView: FC< + {Language.createWorkspace} } From 84c66423e3a6b5b003ccc7864c186535a45c6c92 Mon Sep 17 00:00:00 2001 From: Parkreiner Date: Thu, 5 Oct 2023 20:01:07 +0000 Subject: [PATCH 25/25] fix: update dependencies again for Storybook --- .../pages/WorkspacesPage/WorkspacesButton.tsx | 16 ++++++++++------ site/src/pages/WorkspacesPage/WorkspacesPage.tsx | 3 ++- .../WorkspacesPageView.stories.tsx | 16 ++++++++++++++++ .../pages/WorkspacesPage/WorkspacesPageView.tsx | 13 +++++++++---- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx index 1181a68ab78fe..ae388a45b1ad1 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesButton.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesButton.tsx @@ -1,7 +1,9 @@ import { type PropsWithChildren, type ReactNode, useState } from "react"; import { useTheme } from "@emotion/react"; import { Language } from "./WorkspacesPageView"; + import { type Template } from "api/typesGenerated"; +import { type UseQueryResult } from "@tanstack/react-query"; import { Link as RouterLink } from "react-router-dom"; import Box from "@mui/system/Box"; @@ -110,14 +112,16 @@ function WorkspaceResultsRow({ template }: { template: Template }) { ); } +type TemplatesQuery = UseQueryResult; + type WorkspacesButtonProps = PropsWithChildren<{ - isLoadingTemplates: boolean; - templates: readonly Template[]; + templatesFetchStatus: TemplatesQuery["status"]; + templates: TemplatesQuery["data"]; }>; export function WorkspacesButton({ children, - isLoadingTemplates, + templatesFetchStatus, templates, }: WorkspacesButtonProps) { const theme = useTheme(); @@ -125,10 +129,10 @@ export function WorkspacesButton({ // 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 processed = sortTemplatesByUsersDesc(templates ?? [], searchTerm); let emptyState: ReactNode = undefined; - if (templates.length === 0) { + if (templates?.length === 0) { emptyState = ( - {isLoadingTemplates ? ( + {templatesFetchStatus === "loading" ? ( ) : ( <> diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index bf361dceb94a2..1fc6174dec2b2 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -114,7 +114,8 @@ const WorkspacesPage: FC = () => { checkedWorkspaces={checkedWorkspaces} onCheckChange={setCheckedWorkspaces} canCheckWorkspaces={canCheckWorkspaces} - templatesQuery={templatesQuery} + templates={templatesQuery.data} + templatesFetchStatus={templatesQuery.status} workspaces={data?.workspaces} dormantWorkspaces={dormantWorkspaces} error={error} diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index b7d8bcd24c6c7..81b00ac113c23 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -16,6 +16,7 @@ import { mockApiError, MockUser, MockPendingProvisionerJob, + MockTemplate, } from "testHelpers/entities"; import { WorkspacesPageView } from "./WorkspacesPageView"; import { DashboardProviderContext } from "components/Dashboard/DashboardProvider"; @@ -92,6 +93,19 @@ const defaultFilterProps = getDefaultFilterProps({ }, }); +const mockTemplates = [ + MockTemplate, + ...[1, 2, 3, 4].map((num) => { + return { + ...MockTemplate, + active_user_count: Math.floor(Math.random() * 10) * num, + display_name: `Extra Template ${num}`, + description: "Auto-Generated template", + icon: num % 2 === 0 ? "" : "/icon/goland.svg", + }; + }), +]; + const meta: Meta = { title: "pages/WorkspacesPageView", component: WorkspacesPageView, @@ -100,6 +114,8 @@ const meta: Meta = { filterProps: defaultFilterProps, checkedWorkspaces: [], canCheckWorkspaces: true, + templates: mockTemplates, + templatesFetchStatus: "success", }, decorators: [ (Story) => ( diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index def386a6808c9..81bdaddc5450c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -31,6 +31,8 @@ export const Language = { template: "Template", }; +type TemplateQuery = UseQueryResult; + export interface WorkspacesPageViewProps { error: unknown; workspaces?: Workspace[]; @@ -45,7 +47,9 @@ export interface WorkspacesPageViewProps { onCheckChange: (checkedWorkspaces: Workspace[]) => void; onDeleteAll: () => void; canCheckWorkspaces: boolean; - templatesQuery: UseQueryResult; + + templatesFetchStatus: TemplateQuery["status"]; + templates: TemplateQuery["data"]; } export const WorkspacesPageView: FC< @@ -64,7 +68,8 @@ export const WorkspacesPageView: FC< onCheckChange, onDeleteAll, canCheckWorkspaces, - templatesQuery, + templates, + templatesFetchStatus, }) => { const { saveLocal } = useLocalStorage(); @@ -80,8 +85,8 @@ export const WorkspacesPageView: FC< {Language.createWorkspace}