mirror of
https://github.com/coder/coder.git
synced 2025-07-12 00:14:10 +00:00
chore(site): refactor popover to make it easier to extend (#13611)
This commit is contained in:
@ -160,7 +160,7 @@ export const HelpTooltipAction: FC<HelpTooltipActionProps> = ({
|
|||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
onClick();
|
onClick();
|
||||||
popover.setIsOpen(false);
|
popover.setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Icon css={styles.actionIcon} />
|
<Icon css={styles.actionIcon} />
|
||||||
|
@ -3,7 +3,7 @@ import Button from "@mui/material/Button";
|
|||||||
import InputAdornment from "@mui/material/InputAdornment";
|
import InputAdornment from "@mui/material/InputAdornment";
|
||||||
import TextField, { type TextFieldProps } from "@mui/material/TextField";
|
import TextField, { type TextFieldProps } from "@mui/material/TextField";
|
||||||
import { visuallyHidden } from "@mui/utils";
|
import { visuallyHidden } from "@mui/utils";
|
||||||
import { type FC, lazy, Suspense } from "react";
|
import { type FC, lazy, Suspense, useState } from "react";
|
||||||
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
||||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||||
import { Loader } from "components/Loader/Loader";
|
import { Loader } from "components/Loader/Loader";
|
||||||
@ -37,6 +37,7 @@ export const IconField: FC<IconFieldProps> = ({
|
|||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const hasIcon = textFieldProps.value && textFieldProps.value !== "";
|
const hasIcon = textFieldProps.value && textFieldProps.value !== "";
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
@ -86,31 +87,26 @@ export const IconField: FC<IconFieldProps> = ({
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
{(popover) => (
|
<PopoverTrigger>
|
||||||
<>
|
<Button fullWidth endIcon={<DropdownArrow />}>
|
||||||
<PopoverTrigger>
|
Select emoji
|
||||||
<Button fullWidth endIcon={<DropdownArrow />}>
|
</Button>
|
||||||
Select emoji
|
</PopoverTrigger>
|
||||||
</Button>
|
<PopoverContent
|
||||||
</PopoverTrigger>
|
id="emoji"
|
||||||
<PopoverContent
|
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
|
||||||
id="emoji"
|
>
|
||||||
css={{ marginTop: 0, ".MuiPaper-root": { width: "auto" } }}
|
<Suspense fallback={<Loader />}>
|
||||||
>
|
<EmojiPicker
|
||||||
<Suspense fallback={<Loader />}>
|
onEmojiSelect={(emoji) => {
|
||||||
<EmojiPicker
|
const value = emoji.src ?? urlFromUnifiedCode(emoji.unified);
|
||||||
onEmojiSelect={(emoji) => {
|
onPickEmoji(value);
|
||||||
const value =
|
setOpen(false);
|
||||||
emoji.src ?? urlFromUnifiedCode(emoji.unified);
|
}}
|
||||||
onPickEmoji(value);
|
/>
|
||||||
popover.setIsOpen(false);
|
</Suspense>
|
||||||
}}
|
</PopoverContent>
|
||||||
/>
|
|
||||||
</Suspense>
|
|
||||||
</PopoverContent>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
</Popover>
|
||||||
|
|
||||||
{/*
|
{/*
|
||||||
|
@ -12,7 +12,6 @@ import {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
type RefObject,
|
type RefObject,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
|
||||||
useId,
|
useId,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
@ -25,14 +24,12 @@ type TriggerRef = RefObject<HTMLElement>;
|
|||||||
type TriggerElement = ReactElement<{
|
type TriggerElement = ReactElement<{
|
||||||
ref: TriggerRef;
|
ref: TriggerRef;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
"aria-haspopup"?: boolean;
|
|
||||||
"aria-owns"?: string | undefined;
|
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type PopoverContextValue = {
|
type PopoverContextValue = {
|
||||||
id: string;
|
id: string;
|
||||||
isOpen: boolean;
|
open: boolean;
|
||||||
setIsOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
setOpen: (open: boolean) => void;
|
||||||
triggerRef: TriggerRef;
|
triggerRef: TriggerRef;
|
||||||
mode: TriggerMode;
|
mode: TriggerMode;
|
||||||
};
|
};
|
||||||
@ -41,32 +38,41 @@ const PopoverContext = createContext<PopoverContextValue | undefined>(
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
export interface PopoverProps {
|
type BasePopoverProps = {
|
||||||
children: ReactNode | ((popover: PopoverContextValue) => ReactNode); // Allows inline usage
|
children: ReactNode;
|
||||||
mode?: TriggerMode;
|
mode?: TriggerMode;
|
||||||
isDefaultOpen?: boolean;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const Popover: FC<PopoverProps> = ({
|
// By separating controlled and uncontrolled props, we achieve more accurate
|
||||||
children,
|
// type inference.
|
||||||
mode,
|
type UncontrolledPopoverProps = BasePopoverProps & {
|
||||||
isDefaultOpen,
|
open?: undefined;
|
||||||
}) => {
|
onOpenChange?: undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ControlledPopoverProps = BasePopoverProps & {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PopoverProps = UncontrolledPopoverProps | ControlledPopoverProps;
|
||||||
|
|
||||||
|
export const Popover: FC<PopoverProps> = (props) => {
|
||||||
const hookId = useId();
|
const hookId = useId();
|
||||||
const [isOpen, setIsOpen] = useState(isDefaultOpen ?? false);
|
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||||
const triggerRef = useRef<HTMLElement>(null);
|
const triggerRef: TriggerRef = useRef(null);
|
||||||
|
|
||||||
const value: PopoverContextValue = {
|
const value: PopoverContextValue = {
|
||||||
isOpen,
|
|
||||||
setIsOpen,
|
|
||||||
triggerRef,
|
triggerRef,
|
||||||
id: `${hookId}-popover`,
|
id: `${hookId}-popover`,
|
||||||
mode: mode ?? "click",
|
mode: props.mode ?? "click",
|
||||||
|
open: props.open ?? uncontrolledOpen,
|
||||||
|
setOpen: props.onOpenChange ?? setUncontrolledOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PopoverContext.Provider value={value}>
|
<PopoverContext.Provider value={value}>
|
||||||
{typeof children === "function" ? children(value) : children}
|
{props.children}
|
||||||
</PopoverContext.Provider>
|
</PopoverContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -82,23 +88,25 @@ export const usePopover = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const PopoverTrigger = (
|
export const PopoverTrigger = (
|
||||||
props: HTMLAttributes<HTMLElement> & { children: TriggerElement },
|
props: HTMLAttributes<HTMLElement> & {
|
||||||
|
children: TriggerElement;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const popover = usePopover();
|
const popover = usePopover();
|
||||||
const { children, ...elementProps } = props;
|
const { children, ...elementProps } = props;
|
||||||
|
|
||||||
const clickProps = {
|
const clickProps = {
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
popover.setIsOpen((isOpen) => !isOpen);
|
popover.setOpen(true);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const hoverProps = {
|
const hoverProps = {
|
||||||
onPointerEnter: () => {
|
onPointerEnter: () => {
|
||||||
popover.setIsOpen(true);
|
popover.setOpen(true);
|
||||||
},
|
},
|
||||||
onPointerLeave: () => {
|
onPointerLeave: () => {
|
||||||
popover.setIsOpen(false);
|
popover.setOpen(false);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -106,7 +114,8 @@ export const PopoverTrigger = (
|
|||||||
...elementProps,
|
...elementProps,
|
||||||
...(popover.mode === "click" ? clickProps : hoverProps),
|
...(popover.mode === "click" ? clickProps : hoverProps),
|
||||||
"aria-haspopup": true,
|
"aria-haspopup": true,
|
||||||
"aria-owns": popover.isOpen ? popover.id : undefined,
|
"aria-owns": popover.id,
|
||||||
|
"aria-expanded": popover.open,
|
||||||
ref: popover.triggerRef,
|
ref: popover.triggerRef,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@ -125,22 +134,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
|
|||||||
...popoverProps
|
...popoverProps
|
||||||
}) => {
|
}) => {
|
||||||
const popover = usePopover();
|
const popover = usePopover();
|
||||||
const [isReady, setIsReady] = useState(false);
|
|
||||||
const hoverMode = popover.mode === "hover";
|
const hoverMode = popover.mode === "hover";
|
||||||
|
|
||||||
// This is a hack to make sure the popover is not rendered until the trigger
|
|
||||||
// is ready. This is a limitation on MUI that does not support defaultIsOpen
|
|
||||||
// on Popover but we need it to storybook the component.
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isReady && popover.triggerRef.current !== null) {
|
|
||||||
setIsReady(true);
|
|
||||||
}
|
|
||||||
}, [isReady, popover.triggerRef]);
|
|
||||||
|
|
||||||
if (!popover.triggerRef.current) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MuiPopover
|
<MuiPopover
|
||||||
disablePortal
|
disablePortal
|
||||||
@ -161,8 +156,8 @@ export const PopoverContent: FC<PopoverContentProps> = ({
|
|||||||
{...modeProps(popover)}
|
{...modeProps(popover)}
|
||||||
{...popoverProps}
|
{...popoverProps}
|
||||||
id={popover.id}
|
id={popover.id}
|
||||||
open={popover.isOpen}
|
open={popover.open}
|
||||||
onClose={() => popover.setIsOpen(false)}
|
onClose={() => popover.setOpen(false)}
|
||||||
anchorEl={popover.triggerRef.current}
|
anchorEl={popover.triggerRef.current}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@ -172,10 +167,10 @@ const modeProps = (popover: PopoverContextValue) => {
|
|||||||
if (popover.mode === "hover") {
|
if (popover.mode === "hover") {
|
||||||
return {
|
return {
|
||||||
onPointerEnter: () => {
|
onPointerEnter: () => {
|
||||||
popover.setIsOpen(true);
|
popover.setOpen(true);
|
||||||
},
|
},
|
||||||
onPointerLeave: () => {
|
onPointerLeave: () => {
|
||||||
popover.setIsOpen(false);
|
popover.setOpen(false);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ const DeploymentDropdownContent: FC<DeploymentDropdownProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const popover = usePopover();
|
const popover = usePopover();
|
||||||
|
|
||||||
const onPopoverClose = () => popover.setIsOpen(false);
|
const onPopoverClose = () => popover.setOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav>
|
<nav>
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
MockUser,
|
MockUser,
|
||||||
MockWorkspaceProxies,
|
MockWorkspaceProxies,
|
||||||
} from "testHelpers/entities";
|
} from "testHelpers/entities";
|
||||||
|
import { withDesktopViewport } from "testHelpers/storybook";
|
||||||
import { ProxyMenu } from "./ProxyMenu";
|
import { ProxyMenu } from "./ProxyMenu";
|
||||||
|
|
||||||
const defaultProxyContextValue = {
|
const defaultProxyContextValue = {
|
||||||
@ -36,11 +37,7 @@ const meta: Meta<typeof ProxyMenu> = {
|
|||||||
<Story />
|
<Story />
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
),
|
),
|
||||||
(Story) => (
|
withDesktopViewport,
|
||||||
<div css={{ width: 1200, height: 800 }}>
|
|
||||||
<Story />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
parameters: {
|
parameters: {
|
||||||
queries: [
|
queries: [
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
|
import { css, type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||||
import Badge from "@mui/material/Badge";
|
import Badge from "@mui/material/Badge";
|
||||||
import type { FC } from "react";
|
import { useState, type FC } from "react";
|
||||||
import type * as TypesGen from "api/typesGenerated";
|
import type * as TypesGen from "api/typesGenerated";
|
||||||
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
|
||||||
import {
|
import {
|
||||||
@ -26,48 +26,45 @@ export const UserDropdown: FC<UserDropdownProps> = ({
|
|||||||
onSignOut,
|
onSignOut,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
{(popover) => (
|
<PopoverTrigger>
|
||||||
<>
|
<button css={styles.button} data-testid="user-dropdown-trigger">
|
||||||
<PopoverTrigger>
|
<div css={styles.badgeContainer}>
|
||||||
<button css={styles.button} data-testid="user-dropdown-trigger">
|
<Badge overlap="circular">
|
||||||
<div css={styles.badgeContainer}>
|
<UserAvatar
|
||||||
<Badge overlap="circular">
|
css={styles.avatar}
|
||||||
<UserAvatar
|
username={user.username}
|
||||||
css={styles.avatar}
|
avatarURL={user.avatar_url}
|
||||||
username={user.username}
|
/>
|
||||||
avatarURL={user.avatar_url}
|
</Badge>
|
||||||
/>
|
<DropdownArrow
|
||||||
</Badge>
|
color={theme.experimental.l2.fill.solid}
|
||||||
<DropdownArrow
|
close={open}
|
||||||
color={theme.experimental.l2.fill.solid}
|
|
||||||
close={popover.isOpen}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
|
|
||||||
<PopoverContent
|
|
||||||
horizontal="right"
|
|
||||||
css={{
|
|
||||||
".MuiPaper-root": {
|
|
||||||
minWidth: "auto",
|
|
||||||
width: 260,
|
|
||||||
boxShadow: theme.shadows[6],
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<UserDropdownContent
|
|
||||||
user={user}
|
|
||||||
buildInfo={buildInfo}
|
|
||||||
supportLinks={supportLinks}
|
|
||||||
onSignOut={onSignOut}
|
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</div>
|
||||||
</>
|
</button>
|
||||||
)}
|
</PopoverTrigger>
|
||||||
|
|
||||||
|
<PopoverContent
|
||||||
|
horizontal="right"
|
||||||
|
css={{
|
||||||
|
".MuiPaper-root": {
|
||||||
|
minWidth: "auto",
|
||||||
|
width: 260,
|
||||||
|
boxShadow: theme.shadows[6],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<UserDropdownContent
|
||||||
|
user={user}
|
||||||
|
buildInfo={buildInfo}
|
||||||
|
supportLinks={supportLinks}
|
||||||
|
onSignOut={onSignOut}
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -43,7 +43,7 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
|||||||
const popover = usePopover();
|
const popover = usePopover();
|
||||||
|
|
||||||
const onPopoverClose = () => {
|
const onPopoverClose = () => {
|
||||||
popover.setIsOpen(false);
|
popover.setOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderMenuIcon = (icon: string): JSX.Element => {
|
const renderMenuIcon = (icon: string): JSX.Element => {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { userEvent, within } from "@storybook/test";
|
||||||
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
|
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
|
||||||
|
import { withDesktopViewport } from "testHelpers/storybook";
|
||||||
import { SSHButton } from "./SSHButton";
|
import { SSHButton } from "./SSHButton";
|
||||||
|
|
||||||
const meta: Meta<typeof SSHButton> = {
|
const meta: Meta<typeof SSHButton> = {
|
||||||
@ -22,7 +24,12 @@ export const Opened: Story = {
|
|||||||
args: {
|
args: {
|
||||||
workspaceName: MockWorkspace.name,
|
workspaceName: MockWorkspace.name,
|
||||||
agentName: MockWorkspaceAgent.name,
|
agentName: MockWorkspaceAgent.name,
|
||||||
isDefaultOpen: true,
|
|
||||||
sshPrefix: "coder.",
|
sshPrefix: "coder.",
|
||||||
},
|
},
|
||||||
|
play: async ({ canvasElement }) => {
|
||||||
|
const canvas = within(canvasElement);
|
||||||
|
const button = canvas.getByRole("button");
|
||||||
|
await userEvent.click(button);
|
||||||
|
},
|
||||||
|
decorators: [withDesktopViewport],
|
||||||
};
|
};
|
||||||
|
@ -20,20 +20,18 @@ import { docs } from "utils/docs";
|
|||||||
export interface SSHButtonProps {
|
export interface SSHButtonProps {
|
||||||
workspaceName: string;
|
workspaceName: string;
|
||||||
agentName: string;
|
agentName: string;
|
||||||
isDefaultOpen?: boolean;
|
|
||||||
sshPrefix?: string;
|
sshPrefix?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SSHButton: FC<SSHButtonProps> = ({
|
export const SSHButton: FC<SSHButtonProps> = ({
|
||||||
workspaceName,
|
workspaceName,
|
||||||
agentName,
|
agentName,
|
||||||
isDefaultOpen = false,
|
|
||||||
sshPrefix,
|
sshPrefix,
|
||||||
}) => {
|
}) => {
|
||||||
const paper = useClassName(classNames.paper, []);
|
const paper = useClassName(classNames.paper, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isDefaultOpen={isDefaultOpen}>
|
<Popover>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<Button
|
<Button
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -56,7 +56,7 @@ export const WorkspaceOutdatedTooltipContent: FC<TooltipProps> = ({
|
|||||||
const popover = usePopover();
|
const popover = usePopover();
|
||||||
const { data: activeVersion } = useQuery({
|
const { data: activeVersion } = useQuery({
|
||||||
...templateVersion(latestVersionId),
|
...templateVersion(latestVersionId),
|
||||||
enabled: popover.isOpen,
|
enabled: popover.open,
|
||||||
});
|
});
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
@ -49,92 +49,89 @@ export const DateRange: FC<DateRangeProps> = ({ value, onChange }) => {
|
|||||||
key: "selection",
|
key: "selection",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
{(popover) => (
|
<PopoverTrigger>
|
||||||
<>
|
<Button>
|
||||||
<PopoverTrigger>
|
<span>{format(value.startDate, "MMM d, Y")}</span>
|
||||||
<Button>
|
<ArrowRightAltOutlined
|
||||||
<span>{format(value.startDate, "MMM d, Y")}</span>
|
css={{ width: 16, height: 16, marginLeft: 8, marginRight: 8 }}
|
||||||
<ArrowRightAltOutlined
|
/>
|
||||||
css={{ width: 16, height: 16, marginLeft: 8, marginRight: 8 }}
|
<span>{format(value.endDate, "MMM d, Y")}</span>
|
||||||
/>
|
</Button>
|
||||||
<span>{format(value.endDate, "MMM d, Y")}</span>
|
</PopoverTrigger>
|
||||||
</Button>
|
<PopoverContent>
|
||||||
</PopoverTrigger>
|
<DateRangePicker
|
||||||
<PopoverContent>
|
css={styles.wrapper}
|
||||||
<DateRangePicker
|
onChange={(item) => {
|
||||||
css={styles.wrapper}
|
const range = item.selection;
|
||||||
onChange={(item) => {
|
setRanges([range]);
|
||||||
const range = item.selection;
|
|
||||||
setRanges([range]);
|
|
||||||
|
|
||||||
// When it is the first selection, we don't want to close the popover
|
// When it is the first selection, we don't want to close the popover
|
||||||
// We have to do that ourselves because the library doesn't provide a way to do it
|
// We have to do that ourselves because the library doesn't provide a way to do it
|
||||||
if (selectionStatusRef.current === "idle") {
|
if (selectionStatusRef.current === "idle") {
|
||||||
selectionStatusRef.current = "selecting";
|
selectionStatusRef.current = "selecting";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
selectionStatusRef.current = "idle";
|
selectionStatusRef.current = "idle";
|
||||||
const startDate = range.startDate as Date;
|
const startDate = range.startDate as Date;
|
||||||
const endDate = range.endDate as Date;
|
const endDate = range.endDate as Date;
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
onChange({
|
onChange({
|
||||||
startDate: startOfDay(startDate),
|
startDate: startOfDay(startDate),
|
||||||
endDate: isToday(endDate)
|
endDate: isToday(endDate)
|
||||||
? startOfHour(addHours(now, 1))
|
? startOfHour(addHours(now, 1))
|
||||||
: startOfDay(addDays(endDate, 1)),
|
: startOfDay(addDays(endDate, 1)),
|
||||||
});
|
});
|
||||||
popover.setIsOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
moveRangeOnFirstSelection={false}
|
moveRangeOnFirstSelection={false}
|
||||||
months={2}
|
months={2}
|
||||||
ranges={ranges}
|
ranges={ranges}
|
||||||
maxDate={new Date()}
|
maxDate={new Date()}
|
||||||
direction="horizontal"
|
direction="horizontal"
|
||||||
staticRanges={createStaticRanges([
|
staticRanges={createStaticRanges([
|
||||||
{
|
{
|
||||||
label: "Today",
|
label: "Today",
|
||||||
range: () => ({
|
range: () => ({
|
||||||
startDate: new Date(),
|
startDate: new Date(),
|
||||||
endDate: new Date(),
|
endDate: new Date(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Yesterday",
|
label: "Yesterday",
|
||||||
range: () => ({
|
range: () => ({
|
||||||
startDate: subDays(new Date(), 1),
|
startDate: subDays(new Date(), 1),
|
||||||
endDate: subDays(new Date(), 1),
|
endDate: subDays(new Date(), 1),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Last 7 days",
|
label: "Last 7 days",
|
||||||
range: () => ({
|
range: () => ({
|
||||||
startDate: subDays(new Date(), 6),
|
startDate: subDays(new Date(), 6),
|
||||||
endDate: new Date(),
|
endDate: new Date(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Last 14 days",
|
label: "Last 14 days",
|
||||||
range: () => ({
|
range: () => ({
|
||||||
startDate: subDays(new Date(), 13),
|
startDate: subDays(new Date(), 13),
|
||||||
endDate: new Date(),
|
endDate: new Date(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Last 30 days",
|
label: "Last 30 days",
|
||||||
range: () => ({
|
range: () => ({
|
||||||
startDate: subDays(new Date(), 29),
|
startDate: subDays(new Date(), 29),
|
||||||
endDate: new Date(),
|
endDate: new Date(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
</PopoverContent>
|
</PopoverContent>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Popover>
|
</Popover>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,43 +1,43 @@
|
|||||||
import type { Meta, StoryObj } from "@storybook/react";
|
import type { Meta, StoryObj } from "@storybook/react";
|
||||||
|
import { userEvent, within } from "@storybook/test";
|
||||||
import {
|
import {
|
||||||
MockOwnerRole,
|
MockOwnerRole,
|
||||||
MockSiteRoles,
|
MockSiteRoles,
|
||||||
MockUserAdminRole,
|
MockUserAdminRole,
|
||||||
} from "testHelpers/entities";
|
} from "testHelpers/entities";
|
||||||
|
import { withDesktopViewport } from "testHelpers/storybook";
|
||||||
import { EditRolesButton } from "./EditRolesButton";
|
import { EditRolesButton } from "./EditRolesButton";
|
||||||
|
|
||||||
const meta: Meta<typeof EditRolesButton> = {
|
const meta: Meta<typeof EditRolesButton> = {
|
||||||
title: "pages/UsersPage/EditRolesButton",
|
title: "pages/UsersPage/EditRolesButton",
|
||||||
component: EditRolesButton,
|
component: EditRolesButton,
|
||||||
args: {
|
args: {
|
||||||
isDefaultOpen: true,
|
selectedRoleNames: new Set([MockUserAdminRole.name, MockOwnerRole.name]),
|
||||||
|
roles: MockSiteRoles,
|
||||||
},
|
},
|
||||||
|
decorators: [withDesktopViewport],
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof EditRolesButton>;
|
type Story = StoryObj<typeof EditRolesButton>;
|
||||||
|
|
||||||
const selectedRoleNames = new Set([MockUserAdminRole.name, MockOwnerRole.name]);
|
export const Closed: Story = {};
|
||||||
|
|
||||||
export const Open: Story = {
|
export const Open: Story = {
|
||||||
args: {
|
play: async ({ canvasElement }) => {
|
||||||
selectedRoleNames,
|
const canvas = within(canvasElement);
|
||||||
roles: MockSiteRoles,
|
await userEvent.click(canvas.getByRole("button"));
|
||||||
},
|
|
||||||
parameters: {
|
|
||||||
chromatic: { delay: 300 },
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Loading: Story = {
|
export const Loading: Story = {
|
||||||
args: {
|
args: {
|
||||||
isLoading: true,
|
isLoading: true,
|
||||||
selectedRoleNames,
|
|
||||||
roles: MockSiteRoles,
|
|
||||||
userLoginType: "password",
|
userLoginType: "password",
|
||||||
oidcRoleSync: false,
|
oidcRoleSync: false,
|
||||||
},
|
},
|
||||||
parameters: {
|
play: async ({ canvasElement }) => {
|
||||||
chromatic: { delay: 300 },
|
const canvas = within(canvasElement);
|
||||||
|
await userEvent.click(canvas.getByRole("button"));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -72,7 +72,6 @@ export interface EditRolesButtonProps {
|
|||||||
roles: readonly SlimRole[];
|
roles: readonly SlimRole[];
|
||||||
selectedRoleNames: Set<string>;
|
selectedRoleNames: Set<string>;
|
||||||
onChange: (roles: SlimRole["name"][]) => void;
|
onChange: (roles: SlimRole["name"][]) => void;
|
||||||
isDefaultOpen?: boolean;
|
|
||||||
oidcRoleSync: boolean;
|
oidcRoleSync: boolean;
|
||||||
userLoginType: string;
|
userLoginType: string;
|
||||||
}
|
}
|
||||||
@ -82,7 +81,6 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
|||||||
selectedRoleNames,
|
selectedRoleNames,
|
||||||
onChange,
|
onChange,
|
||||||
isLoading,
|
isLoading,
|
||||||
isDefaultOpen = false,
|
|
||||||
userLoginType,
|
userLoginType,
|
||||||
oidcRoleSync,
|
oidcRoleSync,
|
||||||
}) => {
|
}) => {
|
||||||
@ -116,7 +114,7 @@ export const EditRolesButton: FC<EditRolesButtonProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover isDefaultOpen={isDefaultOpen}>
|
<Popover>
|
||||||
<PopoverTrigger>
|
<PopoverTrigger>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
|
@ -117,7 +117,7 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
|
|||||||
<Form
|
<Form
|
||||||
onSubmit={(buildParameters) => {
|
onSubmit={(buildParameters) => {
|
||||||
onSubmit(buildParameters);
|
onSubmit(buildParameters);
|
||||||
popover.setIsOpen(false);
|
popover.setOpen(false);
|
||||||
}}
|
}}
|
||||||
ephemeralParameters={ephemeralParameters}
|
ephemeralParameters={ephemeralParameters}
|
||||||
buildParameters={buildParameters.map(
|
buildParameters={buildParameters.map(
|
||||||
|
@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|||||||
import { expect, userEvent, fn, waitFor, within } from "@storybook/test";
|
import { expect, userEvent, fn, waitFor, within } from "@storybook/test";
|
||||||
import { agentLogsKey, buildLogsKey } from "api/queries/workspaces";
|
import { agentLogsKey, buildLogsKey } from "api/queries/workspaces";
|
||||||
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
|
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
|
||||||
|
import { withDesktopViewport } from "testHelpers/storybook";
|
||||||
import { DownloadLogsDialog } from "./DownloadLogsDialog";
|
import { DownloadLogsDialog } from "./DownloadLogsDialog";
|
||||||
|
|
||||||
const meta: Meta<typeof DownloadLogsDialog> = {
|
const meta: Meta<typeof DownloadLogsDialog> = {
|
||||||
@ -24,13 +25,7 @@ const meta: Meta<typeof DownloadLogsDialog> = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [withDesktopViewport],
|
||||||
(Story) => (
|
|
||||||
<div css={{ width: 1200, height: 800 }}>
|
|
||||||
<Story />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
|||||||
import { userEvent, within, expect } from "@storybook/test";
|
import { userEvent, within, expect } from "@storybook/test";
|
||||||
import { buildLogsKey, agentLogsKey } from "api/queries/workspaces";
|
import { buildLogsKey, agentLogsKey } from "api/queries/workspaces";
|
||||||
import * as Mocks from "testHelpers/entities";
|
import * as Mocks from "testHelpers/entities";
|
||||||
|
import { withDesktopViewport } from "testHelpers/storybook";
|
||||||
import { WorkspaceActions } from "./WorkspaceActions";
|
import { WorkspaceActions } from "./WorkspaceActions";
|
||||||
|
|
||||||
const meta: Meta<typeof WorkspaceActions> = {
|
const meta: Meta<typeof WorkspaceActions> = {
|
||||||
@ -10,13 +11,7 @@ const meta: Meta<typeof WorkspaceActions> = {
|
|||||||
args: {
|
args: {
|
||||||
isUpdating: false,
|
isUpdating: false,
|
||||||
},
|
},
|
||||||
decorators: [
|
decorators: [withDesktopViewport],
|
||||||
(Story) => (
|
|
||||||
<div css={{ width: 1200, height: 800 }}>
|
|
||||||
<Story />
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
|
@ -70,7 +70,7 @@ const NotificationPill: FC<NotificationsProps> = ({
|
|||||||
icon={icon}
|
icon={icon}
|
||||||
css={(theme) => ({
|
css={(theme) => ({
|
||||||
"& svg": { color: theme.roles[severity].outline },
|
"& svg": { color: theme.roles[severity].outline },
|
||||||
borderColor: popover.isOpen ? theme.roles[severity].outline : undefined,
|
borderColor: popover.open ? theme.roles[severity].outline : undefined,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{items.length}
|
{items.length}
|
||||||
|
@ -78,3 +78,9 @@ export const withWebSocket = (Story: FC, { parameters }: StoryContext) => {
|
|||||||
|
|
||||||
return <Story />;
|
return <Story />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const withDesktopViewport = (Story: FC) => (
|
||||||
|
<div style={{ width: 1200, height: 800 }}>
|
||||||
|
<Story />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
Reference in New Issue
Block a user