refactor: replace deprecated Popovers which open on click (#19709)

for #19397 
dependency: #19635 

This PR specifically covers components which open a popover that's
triggered by a click, rather than a hover.

---------

Co-authored-by: ケイラ <mckayla@hey.com>
This commit is contained in:
Andrew Aquino
2025-09-24 10:00:54 -07:00
committed by GitHub
co-authored by ケイラ
parent a6fc28cc6f
commit 669984c943
21 changed files with 391 additions and 270 deletions
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
import { Avatar } from "components/Avatar/Avatar";
import { useState } from "react";
import { action } from "storybook/actions";
import { expect, userEvent, within } from "storybook/test";
import { expect, screen, userEvent, within } from "storybook/test";
import {
SelectFilter,
type SelectFilterOption,
@@ -88,7 +88,7 @@ export const SelectingOption: Story = {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
const option = canvas.getByText("Option 25");
const option = screen.getByText("Option 25");
await userEvent.click(option);
await expect(button).toHaveTextContent("Option 25");
},
@@ -102,7 +102,7 @@ export const UnselectingOption: Story = {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
const menu = canvasElement.querySelector<HTMLElement>("[role=menu]")!;
const menu = screen.getByRole("menu");
const option = within(menu).getByText("Option 26");
await userEvent.click(option);
await expect(button).toHaveTextContent("All options");
@@ -140,7 +140,7 @@ export const SearchingOption: Story = {
const canvas = within(canvasElement);
const button = canvas.getByRole("button");
await userEvent.click(button);
const search = canvas.getByLabelText("Search options");
const search = screen.getByLabelText("Search options");
await userEvent.type(search, "option-2");
},
};
+10 -10
View File
@@ -10,9 +10,9 @@ import {
SelectMenuTrigger,
} from "components/SelectMenu/SelectMenu";
import { type FC, type ReactNode, useState } from "react";
import { cn } from "utils/cn";
const BASE_WIDTH = 200;
const POPOVER_WIDTH = 320;
export type SelectFilterOption = {
startIcon?: ReactNode;
@@ -60,15 +60,15 @@ export const SelectFilter: FC<SelectFilterProps> = ({
</SelectMenuButton>
</SelectMenuTrigger>
<SelectMenuContent
horizontal="right"
css={{
"& .MuiPaper-root": {
// When including selectFilterSearch, we aim for the width to be as
// wide as possible.
width: selectFilterSearch ? "100%" : undefined,
maxWidth: POPOVER_WIDTH,
minWidth: width,
},
align="end"
className={cn([
// When including selectFilterSearch, we aim for the width to be as
// wide as possible.
selectFilterSearch && "w-full",
"max-w-[320px]",
])}
style={{
minWidth: width,
}}
>
{selectFilterSearch}
+11 -14
View File
@@ -3,13 +3,13 @@ import InputAdornment from "@mui/material/InputAdornment";
import TextField, { type TextFieldProps } from "@mui/material/TextField";
import { visuallyHidden } from "@mui/utils";
import { Button } from "components/Button/Button";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Loader } from "components/Loader/Loader";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Loader } from "components/Loader/Loader";
} from "components/Popover/Popover";
import { ChevronDownIcon } from "lucide-react";
import { type FC, lazy, Suspense, useState } from "react";
@@ -80,24 +80,21 @@ export const IconField: FC<IconFieldProps> = ({
<Global
styles={css`
em-emoji-picker {
--rgb-background: ${theme.palette.background.paper};
--rgb-input: ${theme.palette.primary.main};
--rgb-color: ${theme.palette.text.primary};
// Hack to prevent the right side from being cut off
width: 350px;
}
`}
em-emoji-picker {
--rgb-background: ${theme.palette.background.paper};
--rgb-input: ${theme.palette.primary.main};
--rgb-color: ${theme.palette.text.primary};
}
`}
/>
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<PopoverTrigger asChild>
<Button variant="outline" size="lg" className="flex-shrink-0">
Emoji
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent id="emoji" horizontal="right">
<PopoverContent id="emoji" side="bottom" align="end" className="w-min">
<Suspense fallback={<Loader />}>
<EmojiPicker
onEmojiSelect={(emoji) => {
+7
View File
@@ -10,10 +10,16 @@ import {
} from "react";
import { cn } from "utils/cn";
export type PopoverContentProps = PopoverPrimitive.PopoverContentProps;
export type PopoverTriggerProps = PopoverPrimitive.PopoverTriggerProps;
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverClose = PopoverPrimitive.PopoverClose;
export const PopoverContent = forwardRef<
ElementRef<typeof PopoverPrimitive.Content>,
ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
@@ -23,6 +29,7 @@ export const PopoverContent = forwardRef<
ref={ref}
align={align}
sideOffset={sideOffset}
collisionPadding={16}
className={cn(
`z-50 w-72 rounded-md border border-solid bg-surface-primary
text-content-primary shadow-md outline-none
+17 -3
View File
@@ -4,8 +4,10 @@ import { Button, type ButtonProps } from "components/Button/Button";
import {
Popover,
PopoverContent,
type PopoverContentProps,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
type PopoverTriggerProps,
} from "components/Popover/Popover";
import {
SearchField,
type SearchFieldProps,
@@ -24,9 +26,21 @@ import { cn } from "utils/cn";
export const SelectMenu = Popover;
export const SelectMenuTrigger = PopoverTrigger;
export const SelectMenuTrigger: FC<PopoverTriggerProps> = (props) => {
return <PopoverTrigger asChild {...props} />;
};
export const SelectMenuContent = PopoverContent;
export const SelectMenuContent: FC<PopoverContentProps> = (props) => {
return (
<PopoverContent
{...props}
className={cn(
"w-auto bg-surface-secondary border-surface-quaternary overflow-y-auto text-sm",
props.className,
)}
/>
);
};
type SelectMenuButtonProps = ButtonProps & {
startIcon?: React.ReactNode;
+1 -1
View File
@@ -2,7 +2,7 @@ import { css } from "@emotion/css";
import { type Theme, useTheme } from "@emotion/react";
import { type DependencyList, useMemo } from "react";
export type ClassName = (cssFn: typeof css, theme: Theme) => string;
type ClassName = (cssFn: typeof css, theme: Theme) => string;
/**
* @deprecated This hook was used as an escape hatch to generate class names
@@ -1,12 +1,12 @@
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
import { css, type Interpolation, type Theme } from "@emotion/react";
import MenuItem from "@mui/material/MenuItem";
import { Button } from "components/Button/Button";
import {
Popover,
PopoverClose,
PopoverContent,
PopoverTrigger,
usePopover,
} from "components/deprecated/Popover/Popover";
} from "components/Popover/Popover";
import { ChevronDownIcon } from "lucide-react";
import { linkToAuditing } from "modules/navigation";
import type { FC } from "react";
@@ -27,8 +27,6 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
canViewConnectionLog,
canViewHealth,
}) => {
const theme = useTheme();
if (
!canViewAuditLog &&
!canViewConnectionLog &&
@@ -41,7 +39,7 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
return (
<Popover>
<PopoverTrigger>
<PopoverTrigger asChild>
<Button variant="outline" size="lg">
Admin settings
<ChevronDownIcon className="text-content-primary !size-icon-xs" />
@@ -49,14 +47,8 @@ export const DeploymentDropdown: FC<DeploymentDropdownProps> = ({
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{
".MuiPaper-root": {
minWidth: "auto",
width: 180,
boxShadow: theme.shadows[6],
},
}}
align="end"
className="bg-surface-secondary border-surface-quaternary w-[180px] min-w-auto"
>
<DeploymentDropdownContent
canViewDeployment={canViewDeployment}
@@ -77,61 +69,54 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
canViewHealth,
canViewConnectionLog,
}) => {
const popover = usePopover();
const onPopoverClose = () => popover.setOpen(false);
return (
<nav>
{canViewDeployment && (
<MenuItem
component={NavLink}
to="/deployment"
css={styles.menuItem}
onClick={onPopoverClose}
>
Deployment
</MenuItem>
<PopoverClose asChild>
<MenuItem component={NavLink} to="/deployment" css={styles.menuItem}>
Deployment
</MenuItem>
</PopoverClose>
)}
{canViewOrganizations && (
<MenuItem
component={NavLink}
to="/organizations"
css={styles.menuItem}
onClick={onPopoverClose}
>
Organizations
</MenuItem>
<PopoverClose asChild>
<MenuItem
component={NavLink}
to="/organizations"
css={styles.menuItem}
>
Organizations
</MenuItem>
</PopoverClose>
)}
{canViewAuditLog && (
<MenuItem
component={NavLink}
to={linkToAuditing}
css={styles.menuItem}
onClick={onPopoverClose}
>
Audit Logs
</MenuItem>
<PopoverClose asChild>
<MenuItem
component={NavLink}
to={linkToAuditing}
css={styles.menuItem}
>
Audit Logs
</MenuItem>
</PopoverClose>
)}
{canViewConnectionLog && (
<MenuItem
component={NavLink}
to="/connectionlog"
css={styles.menuItem}
onClick={onPopoverClose}
>
Connection Logs
</MenuItem>
<PopoverClose asChild>
<MenuItem
component={NavLink}
to="/connectionlog"
css={styles.menuItem}
>
Connection Logs
</MenuItem>
</PopoverClose>
)}
{canViewHealth && (
<MenuItem
component={NavLink}
to="/health"
css={styles.menuItem}
onClick={onPopoverClose}
>
Healthcheck
</MenuItem>
<PopoverClose asChild>
<MenuItem component={NavLink} to="/health" css={styles.menuItem}>
Healthcheck
</MenuItem>
</PopoverClose>
)}
</nav>
);
@@ -139,17 +124,17 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
const styles = {
menuItem: (theme) => css`
text-decoration: none;
color: inherit;
gap: 8px;
padding: 8px 20px;
font-size: 14px;
text-decoration: none;
color: inherit;
gap: 8px;
padding: 8px 20px;
font-size: 14px;
&:hover {
background-color: ${theme.palette.action.hover};
transition: background-color 0.3s ease;
}
`,
&:hover {
background-color: ${theme.palette.action.hover};
transition: background-color 0.3s ease;
}
`,
menuItemIcon: (theme) => ({
color: theme.palette.text.secondary,
width: 20,
@@ -1,12 +1,11 @@
import { useTheme } from "@emotion/react";
import type * as TypesGen from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import { type FC, useState } from "react";
} from "components/Popover/Popover";
import type { FC } from "react";
import { UserDropdownContent } from "./UserDropdownContent";
interface UserDropdownProps {
@@ -22,12 +21,9 @@ export const UserDropdown: FC<UserDropdownProps> = ({
supportLinks,
onSignOut,
}) => {
const theme = useTheme();
const [open, setOpen] = useState(false);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<Popover>
<PopoverTrigger asChild>
<button
type="button"
className="bg-transparent border-0 cursor-pointer p-0"
@@ -37,14 +33,8 @@ export const UserDropdown: FC<UserDropdownProps> = ({
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{
".MuiPaper-root": {
minWidth: "auto",
width: 260,
boxShadow: theme.shadows[6],
},
}}
align="end"
className="min-w-auto w-[260px] bg-surface-secondary border-surface-quaternary"
>
<UserDropdownContent
user={user}
@@ -1,7 +1,7 @@
import { MockUserOwner } from "testHelpers/entities";
import { render, waitForLoaderToBeRemoved } from "testHelpers/renderHelpers";
import { screen } from "@testing-library/react";
import { Popover } from "components/deprecated/Popover/Popover";
import { Popover } from "components/Popover/Popover";
import { Language, UserDropdownContent } from "./UserDropdownContent";
describe("UserDropdownContent", () => {
@@ -8,9 +8,9 @@ import Divider from "@mui/material/Divider";
import MenuItem from "@mui/material/MenuItem";
import type { SvgIconProps } from "@mui/material/SvgIcon";
import Tooltip from "@mui/material/Tooltip";
import { PopoverClose } from "@radix-ui/react-popover";
import type * as TypesGen from "api/typesGenerated";
import { CopyButton } from "components/CopyButton/CopyButton";
import { usePopover } from "components/deprecated/Popover/Popover";
import { ExternalImage } from "components/ExternalImage/ExternalImage";
import { Stack } from "components/Stack/Stack";
import {
@@ -44,12 +44,6 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
supportLinks,
onSignOut,
}) => {
const popover = usePopover();
const onPopoverClose = () => {
popover.setOpen(false);
};
const renderMenuIcon = (icon: string): JSX.Element => {
switch (icon) {
case "bug":
@@ -80,17 +74,21 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
<Divider css={{ marginBottom: 8 }} />
<Link to="/install" css={styles.link}>
<MenuItem css={styles.menuItem} onClick={onPopoverClose}>
<MonitorDownIcon css={styles.menuItemIcon} />
<span css={styles.menuItemText}>Install CLI</span>
</MenuItem>
<PopoverClose asChild>
<MenuItem css={styles.menuItem}>
<MonitorDownIcon css={styles.menuItemIcon} />
<span css={styles.menuItemText}>Install CLI</span>
</MenuItem>
</PopoverClose>
</Link>
<Link to="/settings/account" css={styles.link}>
<MenuItem css={styles.menuItem} onClick={onPopoverClose}>
<CircleUserIcon css={styles.menuItemIcon} />
<span css={styles.menuItemText}>{Language.accountLabel}</span>
</MenuItem>
<PopoverClose asChild>
<MenuItem css={styles.menuItem}>
<CircleUserIcon css={styles.menuItemIcon} />
<span css={styles.menuItemText}>{Language.accountLabel}</span>
</MenuItem>
</PopoverClose>
</Link>
<MenuItem css={styles.menuItem} onClick={onSignOut}>
@@ -109,10 +107,12 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
rel="noreferrer"
css={styles.link}
>
<MenuItem css={styles.menuItem} onClick={onPopoverClose}>
{renderMenuIcon(link.icon)}
<span css={styles.menuItemText}>{link.name}</span>
</MenuItem>
<PopoverClose asChild>
<MenuItem css={styles.menuItem}>
{renderMenuIcon(link.icon)}
<span css={styles.menuItemText}>{link.name}</span>
</MenuItem>
</PopoverClose>
</a>
))}
</>
@@ -133,23 +133,11 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
</Tooltip>
{buildInfo?.deployment_id && (
<div
css={css`
font-size: 12px;
display: flex;
align-items: center;
`}
>
<div className="flex items-center text-xs">
<Tooltip title="Deployment Identifier">
<div
css={css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`}
>
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
{buildInfo.deployment_id}
</div>
</span>
</Tooltip>
<CopyButton
text={buildInfo.deployment_id}
@@ -200,14 +188,14 @@ const styles = {
color: "inherit",
},
menuItem: (theme) => css`
gap: 20px;
padding: 8px 20px;
gap: 20px;
padding: 8px 20px;
&:hover {
background-color: ${theme.palette.action.hover};
transition: background-color 0.3s ease;
}
`,
&:hover {
background-color: ${theme.palette.action.hover};
transition: background-color 0.3s ease;
}
`,
menuItemIcon: (theme) => ({
color: theme.palette.text.secondary,
width: 20,
@@ -217,18 +205,18 @@ const styles = {
fontSize: 14,
},
footerText: (theme) => css`
font-size: 12px;
text-decoration: none;
color: ${theme.palette.text.secondary};
display: flex;
align-items: center;
gap: 4px;
font-size: 12px;
text-decoration: none;
color: ${theme.palette.text.secondary};
display: flex;
align-items: center;
gap: 4px;
& svg {
width: 12px;
height: 12px;
}
`,
& svg {
width: 12px;
height: 12px;
}
`,
buildInfo: (theme) => ({
color: theme.palette.text.primary,
}),
@@ -26,16 +26,16 @@ import {
WorkspaceAppSharingLevels,
} from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import {
HelpTooltipLink,
HelpTooltipText,
HelpTooltipTitle,
} from "components/HelpTooltip/HelpTooltip";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { Spinner } from "components/Spinner/Spinner";
import {
Tooltip,
@@ -44,7 +44,6 @@ import {
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useFormik } from "formik";
import { type ClassName, useClassName } from "hooks/useClassName";
import {
ChevronDownIcon,
ExternalLinkIcon,
@@ -77,7 +76,6 @@ export const PortForwardButton: FC<PortForwardButtonProps> = ({
agent,
}) => {
const { entitlements } = useDashboard();
const paper = useClassName(classNames.paper, []);
const { data: listeningPorts } = useQuery({
queryKey: ["portForward", agent.id],
@@ -95,7 +93,7 @@ export const PortForwardButton: FC<PortForwardButtonProps> = ({
return (
<Popover>
<PopoverTrigger>
<PopoverTrigger asChild>
<Button disabled={!listeningPorts} size="sm" variant="subtle">
<Spinner loading={!listeningPorts}>
<span css={styles.portCount}>{listeningPorts?.length}</span>
@@ -104,7 +102,10 @@ export const PortForwardButton: FC<PortForwardButtonProps> = ({
<ChevronDownIcon className="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent horizontal="right" classes={{ paper }}>
<PopoverContent
align="end"
className="p-0 w-[404px] mt-1 text-content-secondary bg-surface-secondary border-surface-quaternary"
>
<PortForwardPopoverView
host={host}
agent={agent}
@@ -618,15 +619,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
);
};
const classNames = {
paper: (css, theme) => css`
padding: 0;
width: 404px;
color: ${theme.palette.text.secondary};
margin-top: 4px;
`,
} satisfies Record<string, ClassName>;
const styles = {
portCount: (theme) => ({
fontSize: 12,
@@ -3,11 +3,6 @@ import Tooltip from "@mui/material/Tooltip";
import type { SlimRole } from "api/typesGenerated";
import { Button } from "components/Button/Button";
import { CollapsibleSummary } from "components/CollapsibleSummary/CollapsibleSummary";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import {
HelpTooltip,
HelpTooltipContent,
@@ -16,6 +11,11 @@ import {
HelpTooltipTitle,
} from "components/HelpTooltip/HelpTooltip";
import { EditSquare } from "components/Icons/EditSquare";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { UserIcon } from "lucide-react";
import { type FC, useEffect, useState } from "react";
@@ -130,7 +130,7 @@ const EnabledEditRolesButton: FC<EditRolesButtonProps> = ({
return (
<Popover>
<PopoverTrigger>
<PopoverTrigger asChild>
<Tooltip title="Edit user roles">
<Button
variant="subtle"
@@ -143,7 +143,10 @@ const EnabledEditRolesButton: FC<EditRolesButtonProps> = ({
</Tooltip>
</PopoverTrigger>
<PopoverContent className="w-96" disablePortal={false}>
<PopoverContent
align="start"
className="w-96 bg-surface-secondary border-surface-quaternary"
>
<fieldset
className="border-0 m-0 p-0 disabled:opacity-50"
disabled={isLoading}
@@ -6,7 +6,7 @@ import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
} from "components/Popover/Popover";
import dayjs from "dayjs";
import { MoveRightIcon } from "lucide-react";
import { type ComponentProps, type FC, useRef, useState } from "react";
@@ -45,7 +45,7 @@ export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger>
<PopoverTrigger asChild>
<Button variant="outline">
<span>{dayjs(value.startDate).format("MMM D, YYYY")}</span>
<MoveRightIcon />
@@ -139,7 +139,7 @@ const styles = {
fontSize: 14,
color: theme.palette.text.secondary,
"&:hover .rdrStaticRangeLabel": {
"&:is(:hover, :focus) .rdrStaticRangeLabel": {
background: theme.palette.background.paper,
color: theme.palette.text.primary,
},
@@ -0,0 +1,47 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { within } from "@testing-library/react";
import type { ComponentProps } from "react";
import { userEvent } from "storybook/test";
import { TemplateInsightsControls } from "./TemplateInsightsPage";
const meta: Meta<typeof TemplateInsightsControls> = {
title: "pages/TemplatePage/TemplateInsightsControls",
component: TemplateInsightsControls,
};
export default meta;
type Story = StoryObj<typeof TemplateInsightsControls>;
const defaultArgs: Partial<ComponentProps<typeof TemplateInsightsControls>> = {
dateRange: {
startDate: new Date("2025-08-05"),
endDate: new Date("2025-08-07"),
},
setDateRange: () => {},
searchParams: new URLSearchParams(),
setSearchParams: () => {},
};
export const Day: Story = {
args: {
...defaultArgs,
interval: "day",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const datePicker = canvas.getAllByRole("button")[1];
await userEvent.click(datePicker);
},
};
export const Week: Story = {
args: {
...defaultArgs,
interval: "week",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const dropdown = canvas.getAllByRole("button")[1];
await userEvent.click(dropdown);
},
};
@@ -47,7 +47,7 @@ import {
} from "react";
import { Helmet } from "react-helmet-async";
import { useQuery } from "react-query";
import { useSearchParams } from "react-router";
import { type SetURLSearchParams, useSearchParams } from "react-router";
import { getLatencyColor } from "utils/latency";
import {
addTime,
@@ -105,24 +105,13 @@ export default function TemplateInsightsPage() {
</Helmet>
<TemplateInsightsPageView
controls={
<>
<IntervalMenu
value={interval}
onChange={(interval) => {
// When going from daily to week we need to set a safe week range
if (interval === "week") {
setDateRange(lastWeeks(DEFAULT_NUMBER_OF_WEEKS));
}
searchParams.set("interval", interval);
setSearchParams(searchParams);
}}
/>
{interval === "day" ? (
<DailyPicker value={dateRange} onChange={setDateRange} />
) : (
<WeekPicker value={dateRange} onChange={setDateRange} />
)}
</>
<TemplateInsightsControls
interval={interval}
dateRange={dateRange}
setDateRange={setDateRange}
searchParams={searchParams}
setSearchParams={setSearchParams}
/>
}
templateInsights={templateInsights}
userLatency={userLatency}
@@ -134,6 +123,43 @@ export default function TemplateInsightsPage() {
);
}
interface TemplateInsightsControlsProps {
interval: "day" | "week";
dateRange: DateRangeValue;
setDateRange: (value: DateRangeValue) => void;
searchParams: URLSearchParams;
setSearchParams: SetURLSearchParams;
}
export const TemplateInsightsControls: FC<TemplateInsightsControlsProps> = ({
interval,
dateRange,
setDateRange,
searchParams,
setSearchParams,
}) => {
return (
<>
<IntervalMenu
value={interval}
onChange={(interval) => {
// When going from daily to week we need to set a safe week range
if (interval === "week") {
setDateRange(lastWeeks(DEFAULT_NUMBER_OF_WEEKS));
}
searchParams.set("interval", interval);
setSearchParams(searchParams);
}}
/>
{interval === "day" ? (
<DailyPicker value={dateRange} onChange={setDateRange} />
) : (
<WeekPicker value={dateRange} onChange={setDateRange} />
)}
</>
);
};
const getDefaultInterval = (template: Template) => {
const now = new Date();
const templateCreateDate = new Date(template.created_at);
@@ -1,13 +1,13 @@
import Link from "@mui/material/Link";
import useTheme from "@mui/system/useTheme";
import type { ProvisionerDaemon } from "api/typesGenerated";
import { FormSection } from "components/Form/Form";
import { TopbarButton } from "components/FullPageLayout/Topbar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import { FormSection } from "components/Form/Form";
import { TopbarButton } from "components/FullPageLayout/Topbar";
} from "components/Popover/Popover";
import { ChevronDownIcon } from "lucide-react";
import { ProvisionerTagsField } from "modules/provisioners/ProvisionerTagsField";
import type { FC } from "react";
@@ -26,7 +26,7 @@ export const ProvisionerTagsPopover: FC<ProvisionerTagsPopoverProps> = ({
return (
<Popover>
<PopoverTrigger>
<PopoverTrigger asChild>
<TopbarButton
color="neutral"
css={{ paddingLeft: 0, paddingRight: 0, minWidth: "28px !important" }}
@@ -36,8 +36,8 @@ export const ProvisionerTagsPopover: FC<ProvisionerTagsPopoverProps> = ({
</TopbarButton>
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{ ".MuiPaper-root": { width: 300 } }}
align="end"
className="w-[300px] bg-surface-secondary border-surface-quaternary"
>
<div
css={{
@@ -7,12 +7,6 @@ import type {
WorkspaceBuildParameter,
} from "api/typesGenerated";
import { Button } from "components/Button/Button";
import {
Popover,
PopoverContent,
PopoverTrigger,
usePopover,
} from "components/deprecated/Popover/Popover";
import { FormFields } from "components/Form/Form";
import { TopbarButton } from "components/FullPageLayout/Topbar";
import {
@@ -21,13 +15,18 @@ import {
HelpTooltipText,
HelpTooltipTitle,
} from "components/HelpTooltip/HelpTooltip";
import { Link } from "components/Link/Link";
import { Loader } from "components/Loader/Loader";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/Popover/Popover";
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
import { useFormik } from "formik";
import { ChevronDownIcon } from "lucide-react";
import type { FC } from "react";
import { type FC, useState } from "react";
import { useQuery } from "react-query";
import { useNavigate } from "react-router";
import { docs } from "utils/docs";
import { getFormHelpers } from "utils/formUtils";
import {
@@ -48,6 +47,7 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({
label,
onSubmit,
}) => {
const [isOpen, setIsOpen] = useState(false);
const { data: parameters } = useQuery({
queryKey: ["workspace", workspace.id, "parameters"],
queryFn: () => API.getWorkspaceParameters(workspace),
@@ -57,8 +57,8 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({
: undefined;
return (
<Popover>
<PopoverTrigger>
<Popover open={isOpen} onOpenChange={setIsOpen}>
<PopoverTrigger asChild>
<TopbarButton
data-testid="build-parameters-button"
disabled={disabled}
@@ -69,14 +69,15 @@ export const BuildParametersPopover: FC<BuildParametersPopoverProps> = ({
</TopbarButton>
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{ ".MuiPaper-root": { width: 304 } }}
align="end"
className="bg-surface-secondary border-surface-quaternary w-[304px]"
>
<BuildParametersPopoverContent
workspace={workspace}
ephemeralParameters={ephemeralParameters}
buildParameters={parameters?.buildParameters}
onSubmit={onSubmit}
setIsOpen={setIsOpen}
/>
</PopoverContent>
</Popover>
@@ -88,6 +89,7 @@ interface BuildParametersPopoverContentProps {
ephemeralParameters?: TemplateVersionParameter[];
buildParameters?: WorkspaceBuildParameter[];
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
setIsOpen: (newOpen: boolean) => void;
}
const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
@@ -95,23 +97,15 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
ephemeralParameters,
buildParameters,
onSubmit,
setIsOpen,
}) => {
const theme = useTheme();
const popover = usePopover();
const navigate = useNavigate();
if (
!workspace.template_use_classic_parameter_flow &&
ephemeralParameters &&
ephemeralParameters.length > 0
) {
const handleGoToParameters = () => {
popover.setOpen(false);
navigate(
`/@${workspace.owner_name}/${workspace.name}/settings/parameters`,
);
};
return (
<div className="flex flex-col gap-4 p-5">
<p className="m-0 text-sm text-content-secondary">
@@ -137,9 +131,12 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
</ul>
</div>
<Button className="w-full" onClick={handleGoToParameters}>
<Link
href={`/@${workspace.owner_name}/${workspace.name}/settings/parameters`}
className="self-start"
>
Go to workspace parameters
</Button>
</Link>
</div>
);
}
@@ -165,7 +162,7 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
<Form
onSubmit={(buildParameters) => {
onSubmit(buildParameters);
popover.setOpen(false);
setIsOpen(false);
}}
ephemeralParameters={ephemeralParameters}
buildParameters={buildParameters.map(
@@ -1,6 +1,6 @@
import { MockWorkspace } from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, waitFor, within } from "storybook/test";
import { expect, screen, userEvent, waitFor } from "storybook/test";
import { DebugButton } from "./DebugButton";
const meta: Meta<typeof DebugButton> = {
@@ -41,11 +41,9 @@ export const WithOpenBuildParameters: Story = {
},
],
},
play: async ({ canvasElement, step }) => {
const screen = within(canvasElement);
play: async ({ step }) => {
await step("open popover", async () => {
await userEvent.click(screen.getByTestId("build-parameters-button"));
await userEvent.click(screen.getByText("Debug with build parameters"));
await waitFor(() =>
expect(screen.getByText("Build Options")).toBeInTheDocument(),
);
@@ -1,6 +1,10 @@
import { MockWorkspace } from "testHelpers/entities";
import {
MockNonClassicParameterFlowWorkspace,
MockTemplateVersionParameter6,
MockWorkspace,
} from "testHelpers/entities";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, userEvent, waitFor, within } from "storybook/test";
import { expect, screen, userEvent, waitFor } from "storybook/test";
import { RetryButton } from "./RetryButton";
const meta: Meta<typeof RetryButton> = {
@@ -41,14 +45,72 @@ export const WithOpenBuildParameters: Story = {
},
],
},
play: async ({ canvasElement, step }) => {
const screen = within(canvasElement);
play: async ({ step }) => {
await step("open popover", async () => {
await userEvent.click(screen.getByTestId("build-parameters-button"));
await userEvent.click(screen.getByText("Retry with build parameters"));
await waitFor(() =>
expect(screen.getByText("Build Options")).toBeInTheDocument(),
);
});
},
};
export const WithOpenEphemeralBuildParameters: Story = {
args: {
enableBuildParameters: true,
workspace: MockWorkspace,
},
parameters: {
queries: [
{
key: ["workspace", MockWorkspace.id, "parameters"],
data: {
templateVersionRichParameters: [MockTemplateVersionParameter6],
buildParameters: [],
},
},
],
},
play: async ({ step }) => {
await step("open popover", async () => {
await userEvent.click(screen.getByText("Retry with build parameters"));
expect(
await screen.findByText(
"These parameters only apply for a single workspace start.",
),
).toBeInTheDocument();
});
},
};
export const WithOpenEphemeralBuildParametersNotClassic: Story = {
args: {
enableBuildParameters: true,
workspace: MockNonClassicParameterFlowWorkspace,
},
parameters: {
queries: [
{
key: [
"workspace",
MockNonClassicParameterFlowWorkspace.id,
"parameters",
],
data: {
templateVersionRichParameters: [MockTemplateVersionParameter6],
buildParameters: [],
},
},
],
},
play: async ({ step }) => {
await step("open popover", async () => {
await userEvent.click(screen.getByText("Retry with build parameters"));
expect(
await screen.findByText(
"This workspace has ephemeral parameters which may use a temporary value on workspace start. Configure the following parameters in workspace settings.",
),
).toBeInTheDocument();
});
},
};
@@ -2,14 +2,14 @@ import Link from "@mui/material/Link";
import type { Template } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { Button } from "components/Button/Button";
import { Loader } from "components/Loader/Loader";
import { MenuSearch } from "components/Menu/MenuSearch";
import { OverflowY } from "components/OverflowY/OverflowY";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import { Loader } from "components/Loader/Loader";
import { MenuSearch } from "components/Menu/MenuSearch";
import { OverflowY } from "components/OverflowY/OverflowY";
} from "components/Popover/Popover";
import { SearchEmpty } from "components/Search/Search";
import { ChevronDownIcon, ExternalLinkIcon } from "lucide-react";
import { linkToTemplate, useLinks } from "modules/navigation";
@@ -54,21 +54,15 @@ export const WorkspacesButton: FC<WorkspacesButtonProps> = ({
return (
<Popover>
<PopoverTrigger>
<PopoverTrigger asChild>
<Button size="lg">
{children}
<ChevronDownIcon />
</Button>
</PopoverTrigger>
<PopoverContent
horizontal="right"
css={{
".MuiPaper-root": {
width: 320,
padding: 0,
borderRadius: 4,
},
}}
align="end"
className="bg-surface-secondary border-surface-quaternary w-[320px]"
>
<MenuSearch
value={searchTerm}
+21
View File
@@ -1599,6 +1599,12 @@ export const MockPendingWorkspace: TypesGen.Workspace = {
},
};
export const MockNonClassicParameterFlowWorkspace: TypesGen.Workspace = {
...MockWorkspace,
id: "test-non-classic-parameter-flow-workspace",
template_use_classic_parameter_flow: false,
};
// just over one page of workspaces
export const MockWorkspacesResponse: TypesGen.WorkspacesResponse = {
workspaces: range(1, 27).map((id: number) => ({
@@ -1696,6 +1702,21 @@ const MockTemplateVersionParameter5: TypesGen.TemplateVersionParameter = {
ephemeral: false,
};
export const MockTemplateVersionParameter6: TypesGen.TemplateVersionParameter =
{
name: "ephemeral_parameter",
type: "string",
form_type: "input",
description: "This is ephemeral parameter",
description_plaintext: "Markdown: This is ephemeral parameter",
default_value: "abc",
mutable: true,
icon: "/icon/folder.svg",
options: [],
required: true,
ephemeral: true,
};
export const MockTemplateVersionVariable1: TypesGen.TemplateVersionVariable = {
name: "first_variable",
description: "This is first variable.",