mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: replace other deprecated Popovers which open on hover (#19753)
closes #19397 dependency: #19709 #19635 covered a large chunk of deprecated `Popover`s which open on hover, specifically the ones which instantiated `HelpTooltip`. This PR replaces all of the non-`HelpTooltip` hover-triggered popovers, and removes the deprecated popover component :) --------- Co-authored-by: ケイラ <mckayla@hey.com>
This commit is contained in:
@@ -1,57 +0,0 @@
|
||||
import Button from "@mui/material/Button";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, screen, userEvent, waitFor, within } from "storybook/test";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "./Popover";
|
||||
|
||||
const meta: Meta<typeof Popover> = {
|
||||
title: "components/PopoverDeprecated",
|
||||
component: Popover,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Popover>;
|
||||
|
||||
const content = `
|
||||
According to all known laws of aviation, there is no way a bee should be able to fly.
|
||||
Its wings are too small to get its fat little body off the ground. The bee, of course,
|
||||
flies anyway because bees don't care what humans think is impossible.
|
||||
`;
|
||||
|
||||
export const Example: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<PopoverTrigger>
|
||||
<Button>Click here!</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>{content}</PopoverContent>
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await step("click to open", async () => {
|
||||
await userEvent.click(canvas.getByRole("button"));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(/according to all known laws/i),
|
||||
).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
export const Horizontal: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<>
|
||||
<PopoverTrigger>
|
||||
<Button>Click here!</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent horizontal="right">{content}</PopoverContent>
|
||||
</>
|
||||
),
|
||||
},
|
||||
play: Example.play,
|
||||
};
|
||||
@@ -1,245 +0,0 @@
|
||||
import MuiPopover, {
|
||||
type PopoverProps as MuiPopoverProps,
|
||||
// biome-ignore lint/style/noRestrictedImports: This is the base component that our custom popover is based on
|
||||
} from "@mui/material/Popover";
|
||||
import {
|
||||
cloneElement,
|
||||
createContext,
|
||||
type FC,
|
||||
type HTMLAttributes,
|
||||
type PointerEvent,
|
||||
type PointerEventHandler,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
type RefObject,
|
||||
useContext,
|
||||
useEffect,
|
||||
useId,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
|
||||
type TriggerMode = "hover" | "click";
|
||||
|
||||
type TriggerRef = RefObject<HTMLElement | null>;
|
||||
|
||||
// Have to append ReactNode type to satisfy React's cloneElement function. It
|
||||
// has absolutely no bearing on what happens at runtime
|
||||
type TriggerElement = ReactNode &
|
||||
ReactElement<{
|
||||
ref: TriggerRef;
|
||||
onClick?: () => void;
|
||||
}>;
|
||||
|
||||
type PopoverContextValue = {
|
||||
id: string;
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
triggerRef: TriggerRef;
|
||||
mode: TriggerMode;
|
||||
};
|
||||
|
||||
const PopoverContext = createContext<PopoverContextValue | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
type BasePopoverProps = {
|
||||
children: ReactNode;
|
||||
mode?: TriggerMode;
|
||||
};
|
||||
|
||||
// By separating controlled and uncontrolled props, we achieve more accurate
|
||||
// type inference.
|
||||
type UncontrolledPopoverProps = BasePopoverProps & {
|
||||
open?: undefined;
|
||||
onOpenChange?: undefined;
|
||||
};
|
||||
|
||||
type ControlledPopoverProps = BasePopoverProps & {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
};
|
||||
|
||||
type PopoverProps = UncontrolledPopoverProps | ControlledPopoverProps;
|
||||
|
||||
/** @deprecated prefer `components.Popover` */
|
||||
export const Popover: FC<PopoverProps> = (props) => {
|
||||
const hookId = useId();
|
||||
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
||||
const triggerRef: TriggerRef = useRef(null);
|
||||
|
||||
// Helps makes sure that popovers close properly when the user switches to
|
||||
// a different tab. This won't help with controlled instances of the
|
||||
// component, but this is basically the most we can do from here
|
||||
useEffect(() => {
|
||||
const closeOnTabSwitch = () => setUncontrolledOpen(false);
|
||||
window.addEventListener("blur", closeOnTabSwitch);
|
||||
return () => window.removeEventListener("blur", closeOnTabSwitch);
|
||||
}, []);
|
||||
|
||||
const value: PopoverContextValue = {
|
||||
triggerRef,
|
||||
id: `${hookId}-popover`,
|
||||
mode: props.mode ?? "click",
|
||||
open: props.open ?? uncontrolledOpen,
|
||||
setOpen: props.onOpenChange ?? setUncontrolledOpen,
|
||||
};
|
||||
|
||||
return (
|
||||
<PopoverContext.Provider value={value}>
|
||||
{props.children}
|
||||
</PopoverContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePopover = () => {
|
||||
const context = useContext(PopoverContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"Popover compound components cannot be rendered outside the Popover component",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
type PopoverTriggerRenderProps = Readonly<{
|
||||
isOpen: boolean;
|
||||
}>;
|
||||
|
||||
type PopoverTriggerProps = Readonly<
|
||||
Omit<HTMLAttributes<HTMLElement>, "children"> & {
|
||||
children:
|
||||
| TriggerElement
|
||||
| ((props: PopoverTriggerRenderProps) => TriggerElement);
|
||||
}
|
||||
>;
|
||||
|
||||
/** @deprecated prefer `components.Popover.PopoverTrigger` */
|
||||
export const PopoverTrigger: FC<PopoverTriggerProps> = (props) => {
|
||||
const popover = usePopover();
|
||||
const { children, onClick, onPointerEnter, onPointerLeave, ...elementProps } =
|
||||
props;
|
||||
|
||||
const clickProps = {
|
||||
onClick: (event: PointerEvent<HTMLElement>) => {
|
||||
popover.setOpen(true);
|
||||
onClick?.(event);
|
||||
},
|
||||
};
|
||||
|
||||
const hoverProps = {
|
||||
onPointerEnter: (event: PointerEvent<HTMLElement>) => {
|
||||
popover.setOpen(true);
|
||||
onPointerEnter?.(event);
|
||||
},
|
||||
onPointerLeave: (event: PointerEvent<HTMLElement>) => {
|
||||
popover.setOpen(false);
|
||||
onPointerLeave?.(event);
|
||||
},
|
||||
};
|
||||
|
||||
const evaluatedChildren =
|
||||
typeof children === "function"
|
||||
? children({ isOpen: popover.open })
|
||||
: children;
|
||||
|
||||
return cloneElement(evaluatedChildren, {
|
||||
...elementProps,
|
||||
...(popover.mode === "click" ? clickProps : hoverProps),
|
||||
// @ts-expect-error I would usually not hack around this, but this component
|
||||
// is going to be deleted imminently.
|
||||
"aria-haspopup": true,
|
||||
"aria-owns": popover.id,
|
||||
"aria-expanded": popover.open,
|
||||
ref: popover.triggerRef,
|
||||
});
|
||||
};
|
||||
|
||||
type Horizontal = "left" | "right";
|
||||
|
||||
type PopoverContentProps = Omit<
|
||||
MuiPopoverProps,
|
||||
"open" | "onClose" | "anchorEl"
|
||||
> & {
|
||||
horizontal?: Horizontal;
|
||||
};
|
||||
|
||||
/** @deprecated prefer `components.Popover.PopoverContent` */
|
||||
export const PopoverContent: FC<PopoverContentProps> = ({
|
||||
horizontal = "left",
|
||||
onPointerEnter,
|
||||
onPointerLeave,
|
||||
...popoverProps
|
||||
}) => {
|
||||
const popover = usePopover();
|
||||
const hoverMode = popover.mode === "hover";
|
||||
|
||||
return (
|
||||
<MuiPopover
|
||||
disablePortal
|
||||
css={{
|
||||
// When it is on hover mode, and the mode is moving from the trigger to
|
||||
// the popover, if there is any space, the popover will be closed. I
|
||||
// found this is a limitation on how MUI structured the component. It is
|
||||
// not a big issue for now but we can re-evaluate it in the future.
|
||||
marginTop: hoverMode ? undefined : 8,
|
||||
pointerEvents: hoverMode ? "none" : undefined,
|
||||
"& .MuiPaper-root": {
|
||||
minWidth: 320,
|
||||
fontSize: 14,
|
||||
pointerEvents: hoverMode ? "auto" : undefined,
|
||||
},
|
||||
}}
|
||||
{...horizontalProps(horizontal)}
|
||||
{...modeProps(popover, onPointerEnter, onPointerLeave)}
|
||||
{...popoverProps}
|
||||
id={popover.id}
|
||||
open={popover.open}
|
||||
onClose={() => popover.setOpen(false)}
|
||||
anchorEl={popover.triggerRef.current}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const modeProps = (
|
||||
popover: PopoverContextValue,
|
||||
externalOnPointerEnter: PointerEventHandler<HTMLDivElement> | undefined,
|
||||
externalOnPointerLeave: PointerEventHandler<HTMLDivElement> | undefined,
|
||||
) => {
|
||||
if (popover.mode === "hover") {
|
||||
return {
|
||||
onPointerEnter: (event: PointerEvent<HTMLDivElement>) => {
|
||||
popover.setOpen(true);
|
||||
externalOnPointerEnter?.(event);
|
||||
},
|
||||
onPointerLeave: (event: PointerEvent<HTMLDivElement>) => {
|
||||
popover.setOpen(false);
|
||||
externalOnPointerLeave?.(event);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
};
|
||||
|
||||
const horizontalProps = (horizontal: Horizontal) => {
|
||||
if (horizontal === "right") {
|
||||
return {
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "right",
|
||||
},
|
||||
transformOrigin: {
|
||||
vertical: "top",
|
||||
horizontal: "right",
|
||||
},
|
||||
} as const;
|
||||
}
|
||||
|
||||
return {
|
||||
anchorOrigin: {
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
},
|
||||
} as const;
|
||||
};
|
||||
@@ -30,8 +30,8 @@ const Example: Story = {
|
||||
|
||||
await step("click to open", async () => {
|
||||
await userEvent.click(canvas.getByRole("button"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/v2\.\d+\.\d+/i)).toBeInTheDocument(),
|
||||
await waitFor(async () =>
|
||||
expect(await screen.findByText(/v2\.\d+\.\d+/i)).toBeInTheDocument(),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
+30
-23
@@ -7,17 +7,18 @@ import {
|
||||
PremiumBadge,
|
||||
} from "components/Badges/Badges";
|
||||
import { Button } from "components/Button/Button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "components/SettingsHeader/SettingsHeader";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { useFormik } from "formik";
|
||||
import type { FC } from "react";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
@@ -66,25 +67,31 @@ export const AppearanceSettingsPageView: FC<
|
||||
</SettingsHeader>
|
||||
|
||||
<Badges>
|
||||
<Popover mode="hover">
|
||||
{isEntitled && !isPremium ? (
|
||||
<EnterpriseBadge />
|
||||
) : (
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
{isEntitled && !isPremium ? (
|
||||
<EnterpriseBadge />
|
||||
) : (
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
|
||||
<PopoverContent css={{ transform: "translateY(-28px)" }}>
|
||||
<PopoverPaywall
|
||||
message="Appearance"
|
||||
description="With a Premium license, you can customize the appearance and branding of your deployment."
|
||||
documentationLink="https://coder.com/docs/admin/appearance"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent
|
||||
sideOffset={-28}
|
||||
collisionPadding={16}
|
||||
className="p-0"
|
||||
>
|
||||
<PopoverPaywall
|
||||
message="Appearance"
|
||||
description="With a Premium license, you can customize the appearance and branding of your deployment."
|
||||
documentationLink="https://coder.com/docs/admin/appearance"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badges>
|
||||
|
||||
<Fieldset
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
MockOrganization,
|
||||
MockOrganization2,
|
||||
MockOrganization3,
|
||||
MockOrganizationSyncSettings,
|
||||
MockOrganizationSyncSettings2,
|
||||
MockOrganizationSyncSettingsEmpty,
|
||||
@@ -15,7 +16,7 @@ const meta: Meta<typeof IdpOrgSyncPageView> = {
|
||||
args: {
|
||||
organizationSyncSettings: MockOrganizationSyncSettings2,
|
||||
claimFieldValues: Object.keys(MockOrganizationSyncSettings2.mapping),
|
||||
organizations: [MockOrganization, MockOrganization2],
|
||||
organizations: [MockOrganization, MockOrganization2, MockOrganization3],
|
||||
error: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useTheme } from "@emotion/react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import type { FC } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
import { isUUID } from "utils/uuid";
|
||||
@@ -25,10 +25,10 @@ export const OrganizationPills: FC<OrganizationPillsProps> = ({
|
||||
<div className="flex flex-row gap-2">
|
||||
{orgs.length > 0 ? (
|
||||
<Pill
|
||||
className={cn("border-none w-fit", {
|
||||
"bg-surface-destructive": orgs[0].isUUID,
|
||||
"bg-surface-secondary": !orgs[0].isUUID,
|
||||
})}
|
||||
className={cn(
|
||||
"border-none w-fit",
|
||||
orgs[0].isUUID ? "bg-surface-destructive" : "bg-surface-secondary",
|
||||
)}
|
||||
>
|
||||
{orgs[0].name}
|
||||
</Pill>
|
||||
@@ -46,58 +46,37 @@ interface OverflowPillProps {
|
||||
}
|
||||
|
||||
const OverflowPillList: FC<OverflowPillProps> = ({ organizations }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<Pill
|
||||
className="min-h-4 min-w-6 bg-surface-secondary border-none px-3 py-1"
|
||||
data-testid="overflow-pill"
|
||||
>
|
||||
+{organizations.length}
|
||||
</Pill>
|
||||
</PopoverTrigger>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill
|
||||
className="min-h-4 min-w-6 bg-surface-secondary border-none px-3 py-1"
|
||||
data-testid="overflow-pill"
|
||||
>
|
||||
+{organizations.length}
|
||||
</Pill>
|
||||
</TooltipTrigger>
|
||||
|
||||
<PopoverContent
|
||||
disableRestoreFocus
|
||||
disableScrollLock
|
||||
css={{
|
||||
".MuiPaper-root": {
|
||||
display: "flex",
|
||||
flexFlow: "column wrap",
|
||||
columnGap: 8,
|
||||
rowGap: 12,
|
||||
padding: "12px 16px",
|
||||
alignContent: "space-around",
|
||||
minWidth: "auto",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: -4,
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<ul className="list-none my-0 pl-0">
|
||||
{organizations.map((organization) => (
|
||||
<li key={organization.name} className="mb-2 last:mb-0">
|
||||
<Pill
|
||||
className={cn("border-none w-fit", {
|
||||
"bg-surface-destructive": organization.isUUID,
|
||||
"bg-surface-secondary": !organization.isUUID,
|
||||
})}
|
||||
>
|
||||
{organization.name}
|
||||
</Pill>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent className="px-4 py-3 border-surface-quaternary">
|
||||
<ul className="flex flex-col gap-2 list-none my-0 pl-0">
|
||||
{organizations.map((organization) => (
|
||||
<li key={organization.name}>
|
||||
<Pill
|
||||
className={cn(
|
||||
"border-none w-fit",
|
||||
organization.isUUID
|
||||
? "bg-surface-destructive"
|
||||
: "bg-surface-secondary",
|
||||
)}
|
||||
>
|
||||
{organization.name}
|
||||
</Pill>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+2
@@ -52,3 +52,5 @@ export default meta;
|
||||
type Story = StoryObj<typeof ObservabilitySettingsPageView>;
|
||||
|
||||
export const Page: Story = {};
|
||||
|
||||
export const Premium: Story = { args: { isPremium: true } };
|
||||
|
||||
+30
-23
@@ -4,11 +4,6 @@ import {
|
||||
EnterpriseBadge,
|
||||
PremiumBadge,
|
||||
} from "components/Badges/Badges";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
|
||||
import {
|
||||
SettingsHeader,
|
||||
@@ -17,6 +12,12 @@ import {
|
||||
SettingsHeaderTitle,
|
||||
} from "components/SettingsHeader/SettingsHeader";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import type { FC } from "react";
|
||||
import { deploymentGroupHasParent } from "utils/deployOptions";
|
||||
import { docs } from "utils/docs";
|
||||
@@ -52,25 +53,31 @@ export const ObservabilitySettingsPageView: FC<
|
||||
</SettingsHeader>
|
||||
|
||||
<Badges>
|
||||
<Popover mode="hover">
|
||||
{featureAuditLogEnabled && !isPremium ? (
|
||||
<EnterpriseBadge />
|
||||
) : (
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
{featureAuditLogEnabled && !isPremium ? (
|
||||
<EnterpriseBadge />
|
||||
) : (
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
|
||||
<PopoverContent css={{ transform: "translateY(-28px)" }}>
|
||||
<PopoverPaywall
|
||||
message="Observability"
|
||||
description="With a Premium license, you can monitor your application with logs and metrics."
|
||||
documentationLink="https://coder.com/docs/admin/appearance"
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent
|
||||
sideOffset={-28}
|
||||
collisionPadding={16}
|
||||
className="p-0"
|
||||
>
|
||||
<PopoverPaywall
|
||||
message="Observability"
|
||||
description="With a Premium license, you can monitor your application with logs and metrics."
|
||||
documentationLink="https://coder.com/docs/admin/appearance"
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badges>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,15 +5,16 @@ import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Badges, PremiumBadge } from "components/Badges/Badges";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { ChooseOne, Cond } from "components/Conditionals/ChooseOne";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { IconField } from "components/IconField/IconField";
|
||||
import { Paywall } from "components/Paywall/Paywall";
|
||||
import { PopoverPaywall } from "components/Paywall/PopoverPaywall";
|
||||
import { Spinner } from "components/Spinner/Spinner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { useFormik } from "formik";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
@@ -81,23 +82,29 @@ export const CreateOrganizationPageView: FC<
|
||||
)}
|
||||
|
||||
<Badges>
|
||||
<Popover mode="hover">
|
||||
{isEntitled && (
|
||||
<PopoverTrigger>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</PopoverTrigger>
|
||||
)}
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
{isEntitled && (
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<PremiumBadge />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
)}
|
||||
|
||||
<PopoverContent css={{ transform: "translateY(-28px)" }}>
|
||||
<PopoverPaywall
|
||||
message="Organizations"
|
||||
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
|
||||
documentationLink={docs("/admin/users/organizations")}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent
|
||||
sideOffset={-28}
|
||||
collisionPadding={16}
|
||||
className="p-0"
|
||||
>
|
||||
<PopoverPaywall
|
||||
message="Organizations"
|
||||
description="Create multiple organizations within a single Coder deployment, allowing several platform teams to operate with isolated users, templates, and distinct underlying infrastructure."
|
||||
documentationLink={docs("/admin/users/organizations")}
|
||||
/>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</Badges>
|
||||
|
||||
<header className="flex flex-col items-center">
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import type { Permission } from "api/typesGenerated";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import type { FC } from "react";
|
||||
|
||||
function getUniqueResourceTypes(jsonObject: readonly Permission[]) {
|
||||
@@ -76,52 +77,34 @@ const OverflowPermissionPill: FC<OverflowPermissionPillProps> = ({
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<Pill
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
data-testid="overflow-permissions-pill"
|
||||
>
|
||||
+{resources.length} more
|
||||
</Pill>
|
||||
</PopoverTrigger>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
data-testid="overflow-permissions-pill"
|
||||
>
|
||||
+{resources.length} more
|
||||
</Pill>
|
||||
</TooltipTrigger>
|
||||
|
||||
<PopoverContent
|
||||
disableRestoreFocus
|
||||
disableScrollLock
|
||||
css={{
|
||||
".MuiPaper-root": {
|
||||
display: "flex",
|
||||
flexFlow: "column wrap",
|
||||
columnGap: 8,
|
||||
rowGap: 12,
|
||||
padding: "12px 16px",
|
||||
alignContent: "space-around",
|
||||
minWidth: "auto",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: -4,
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
{resources.map((resource) => (
|
||||
<PermissionsPill
|
||||
key={resource}
|
||||
resource={resource}
|
||||
permissions={permissions}
|
||||
/>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent className="px-4 py-3 border-surface-quaternary">
|
||||
<ul className="flex flex-col gap-2 list-none my-0 pl-0">
|
||||
{resources.map((resource) => (
|
||||
<li key={resource}>
|
||||
<PermissionsPill
|
||||
resource={resource}
|
||||
permissions={permissions}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import type { Interpolation, Theme } from "@emotion/react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import type { FC } from "react";
|
||||
import { isUUID } from "utils/uuid";
|
||||
|
||||
@@ -34,53 +35,26 @@ interface OverflowPillProps {
|
||||
}
|
||||
|
||||
const OverflowPill: FC<OverflowPillProps> = ({ roles }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<Pill
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
data-testid="overflow-pill"
|
||||
>
|
||||
+{roles.length} more
|
||||
</Pill>
|
||||
</PopoverTrigger>
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill data-testid="overflow-pill">+{roles.length} more</Pill>
|
||||
</TooltipTrigger>
|
||||
|
||||
<PopoverContent
|
||||
disableRestoreFocus
|
||||
disableScrollLock
|
||||
css={{
|
||||
".MuiPaper-root": {
|
||||
display: "flex",
|
||||
flexFlow: "column wrap",
|
||||
columnGap: 8,
|
||||
rowGap: 12,
|
||||
padding: "12px 16px",
|
||||
alignContent: "space-around",
|
||||
minWidth: "auto",
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: -4,
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<Pill key={role} css={isUUID(role) ? styles.errorPill : styles.pill}>
|
||||
{role}
|
||||
</Pill>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<TooltipContent className="px-4 py-3 border-surface-quaternary">
|
||||
<ul className="flex flex-col gap-2 list-none my-0 pl-0">
|
||||
{roles.map((role) => (
|
||||
<li key={role}>
|
||||
<Pill css={isUUID(role) ? styles.errorPill : styles.pill}>
|
||||
{role}
|
||||
</Pill>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
MockGroup,
|
||||
MockGroup2,
|
||||
MockGroup3,
|
||||
MockGroupSyncSettings,
|
||||
MockGroupSyncSettings2,
|
||||
MockLegacyMappingGroupSyncSettings,
|
||||
MockMultipleOverflowGroupSyncSettings,
|
||||
MockOrganization,
|
||||
MockRoleSyncSettings,
|
||||
} from "testHelpers/entities";
|
||||
@@ -12,7 +14,7 @@ import { expect, userEvent } from "storybook/test";
|
||||
import IdpSyncPageView from "./IdpSyncPageView";
|
||||
|
||||
const groupsMap = new Map<string, string>();
|
||||
for (const group of [MockGroup, MockGroup2]) {
|
||||
for (const group of [MockGroup, MockGroup2, MockGroup3]) {
|
||||
groupsMap.set(group.id, group.display_name || group.name);
|
||||
}
|
||||
|
||||
@@ -70,6 +72,12 @@ export const MissingGroups: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleOverflowGroups: Story = {
|
||||
args: {
|
||||
groupSyncSettings: MockMultipleOverflowGroupSyncSettings,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLegacyMapping: Story = {
|
||||
args: {
|
||||
groupSyncSettings: MockLegacyMappingGroupSyncSettings,
|
||||
|
||||
@@ -14,15 +14,16 @@
|
||||
* users like that, though, know that it will be painful
|
||||
*/
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import Tooltip from "@mui/material/Tooltip";
|
||||
import MUITooltip from "@mui/material/Tooltip";
|
||||
import type { LoginType, SlimRole } from "api/typesGenerated";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import { TableCell } from "components/Table/Table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import type { FC } from "react";
|
||||
import { EditRolesButton } from "./EditRolesButton";
|
||||
|
||||
@@ -87,9 +88,9 @@ export const UserRoleCell: FC<UserRoleCellProps> = ({
|
||||
}
|
||||
>
|
||||
{mainDisplayRole.global ? (
|
||||
<Tooltip title="This user has this role for all organizations.">
|
||||
<MUITooltip title="This user has this role for all organizations.">
|
||||
<span>{displayName}*</span>
|
||||
</Tooltip>
|
||||
</MUITooltip>
|
||||
) : (
|
||||
displayName
|
||||
)}
|
||||
@@ -109,51 +110,37 @@ const OverflowRolePill: FC<OverflowRolePillProps> = ({ roles }) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<Pill
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
>
|
||||
+{roles.length} more
|
||||
</Pill>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
disableRestoreFocus
|
||||
disableScrollLock
|
||||
css={{
|
||||
".MuiPaper-root": {
|
||||
display: "flex",
|
||||
flexFlow: "row wrap",
|
||||
columnGap: 8,
|
||||
rowGap: 12,
|
||||
padding: "12px 16px",
|
||||
alignContent: "space-around",
|
||||
minWidth: "auto",
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{ vertical: -4, horizontal: "center" }}
|
||||
transformOrigin={{ vertical: "bottom", horizontal: "center" }}
|
||||
>
|
||||
{roles.map((role) => (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<Pill
|
||||
key={role.name}
|
||||
css={role.global ? styles.globalRoleBadge : styles.roleBadge}
|
||||
css={{
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderColor: theme.palette.divider,
|
||||
}}
|
||||
>
|
||||
{role.global ? (
|
||||
<Tooltip title="This user has this role for all organizations.">
|
||||
<span>{role.display_name || role.name}*</span>
|
||||
</Tooltip>
|
||||
) : (
|
||||
role.display_name || role.name
|
||||
)}
|
||||
+{roles.length} more
|
||||
</Pill>
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="flex flex-row flex-wrap content-around gap-x-2 gap-y-3 px-4 py-3 border-surface-quaternary">
|
||||
{roles.map((role) => (
|
||||
<Pill
|
||||
key={role.name}
|
||||
css={role.global ? styles.globalRoleBadge : styles.roleBadge}
|
||||
>
|
||||
{role.global ? (
|
||||
<MUITooltip title="This user has this role for all organizations.">
|
||||
<span>{role.display_name || role.name}*</span>
|
||||
</MUITooltip>
|
||||
) : (
|
||||
role.display_name || role.name
|
||||
)}
|
||||
</Pill>
|
||||
))}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -3,13 +3,14 @@ import List from "@mui/material/List";
|
||||
import ListItem from "@mui/material/ListItem";
|
||||
import type { Group } from "api/typesGenerated";
|
||||
import { Avatar } from "components/Avatar/Avatar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { OverflowY } from "components/OverflowY/OverflowY";
|
||||
import { TableCell } from "components/Table/Table";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { UsersIcon } from "lucide-react";
|
||||
import type { FC } from "react";
|
||||
import { cn } from "utils/cn";
|
||||
@@ -29,99 +30,83 @@ export const UserGroupsCell: FC<GroupsCellProps> = ({ userGroups }) => {
|
||||
// the table UI
|
||||
<em css={{ fontStyle: "normal" }}>N/A</em>
|
||||
) : (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<button
|
||||
css={{
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
color: "inherit",
|
||||
lineHeight: "1",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<UsersIcon
|
||||
className={cn([
|
||||
"size-4 opacity-50",
|
||||
{
|
||||
"opacity-80": userGroups.length > 0,
|
||||
},
|
||||
])}
|
||||
/>
|
||||
|
||||
<span>
|
||||
{userGroups.length} Group{userGroups.length !== 1 && "s"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
disableScrollLock
|
||||
disableRestoreFocus
|
||||
css={{
|
||||
".MuiPaper-root": {
|
||||
minWidth: "auto",
|
||||
},
|
||||
}}
|
||||
anchorOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "center",
|
||||
}}
|
||||
>
|
||||
<OverflowY maxHeight={400}>
|
||||
<List
|
||||
component="ul"
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
css={{
|
||||
display: "flex",
|
||||
flexFlow: "column nowrap",
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
padding: "4px 2px",
|
||||
gap: 0,
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent",
|
||||
border: "none",
|
||||
padding: 0,
|
||||
color: "inherit",
|
||||
lineHeight: "1",
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
{userGroups.map((group) => {
|
||||
const groupName = group.display_name || group.name;
|
||||
return (
|
||||
<ListItem
|
||||
key={group.id}
|
||||
css={{
|
||||
columnGap: 10,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Avatar
|
||||
size="sm"
|
||||
variant="icon"
|
||||
src={group.avatar_url}
|
||||
fallback={groupName}
|
||||
/>
|
||||
<div className="flex flex-row gap-2 items-center">
|
||||
<UsersIcon
|
||||
className={cn([
|
||||
"size-4 opacity-50",
|
||||
userGroups.length > 0 && "opacity-80",
|
||||
])}
|
||||
/>
|
||||
|
||||
<span
|
||||
<span>
|
||||
{userGroups.length} Group{userGroups.length !== 1 && "s"}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
|
||||
<TooltipContent className="p-0 bg-surface-secondary border-surface-quaternary text-white">
|
||||
<OverflowY maxHeight={400}>
|
||||
<List
|
||||
component="ul"
|
||||
css={{
|
||||
display: "flex",
|
||||
flexFlow: "column nowrap",
|
||||
fontSize: theme.typography.body2.fontSize,
|
||||
padding: "4px 2px",
|
||||
gap: 0,
|
||||
}}
|
||||
>
|
||||
{userGroups.map((group) => {
|
||||
const groupName = group.display_name || group.name;
|
||||
return (
|
||||
<ListItem
|
||||
key={group.id}
|
||||
css={{
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
lineHeight: 1,
|
||||
margin: 0,
|
||||
columnGap: 10,
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
{groupName || <em>N/A</em>}
|
||||
</span>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</OverflowY>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<Avatar
|
||||
size="sm"
|
||||
variant="icon"
|
||||
src={group.avatar_url}
|
||||
fallback={groupName}
|
||||
/>
|
||||
|
||||
<span
|
||||
css={{
|
||||
whiteSpace: "nowrap",
|
||||
textOverflow: "ellipsis",
|
||||
overflow: "hidden",
|
||||
lineHeight: 1,
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{groupName || <em>N/A</em>}
|
||||
</span>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</OverflowY>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</TableCell>
|
||||
);
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
|
||||
import type { AlertProps } from "components/Alert/Alert";
|
||||
import { Button, type ButtonProps } from "components/Button/Button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
usePopover,
|
||||
} from "components/deprecated/Popover/Popover";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import type { FC, ReactNode } from "react";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "components/Tooltip/Tooltip";
|
||||
import { type FC, type ReactNode, useState } from "react";
|
||||
import type { ThemeRole } from "theme/roles";
|
||||
|
||||
export type NotificationItem = {
|
||||
@@ -29,48 +29,58 @@ export const Notifications: FC<NotificationsProps> = ({
|
||||
severity,
|
||||
icon,
|
||||
}) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Popover mode="hover">
|
||||
<PopoverTrigger>
|
||||
<div
|
||||
css={styles.pillContainer}
|
||||
data-testid={`${severity}-notifications`}
|
||||
>
|
||||
<NotificationPill items={items} severity={severity} icon={icon} />
|
||||
</div>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
horizontal="right"
|
||||
css={{
|
||||
"& .MuiPaper-root": {
|
||||
<TooltipProvider>
|
||||
<Tooltip open={isOpen} onOpenChange={setIsOpen} delayDuration={0}>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
css={styles.pillContainer}
|
||||
data-testid={`${severity}-notifications`}
|
||||
>
|
||||
<NotificationPill
|
||||
items={items}
|
||||
severity={severity}
|
||||
icon={icon}
|
||||
isTooltipOpen={isOpen}
|
||||
/>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
align="end"
|
||||
collisionPadding={16}
|
||||
className="max-w-[400px] p-0 bg-surface-secondary border-surface-quaternary text-sm text-white"
|
||||
style={{
|
||||
borderColor: theme.roles[severity].outline,
|
||||
maxWidth: 400,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{items.map((n) => (
|
||||
<NotificationItem notification={n} key={n.title} />
|
||||
))}
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
}}
|
||||
>
|
||||
{items.map((n) => (
|
||||
<NotificationItem notification={n} key={n.title} />
|
||||
))}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const NotificationPill: FC<NotificationsProps> = ({
|
||||
type NotificationPillProps = NotificationsProps & {
|
||||
isTooltipOpen: boolean;
|
||||
};
|
||||
|
||||
const NotificationPill: FC<NotificationPillProps> = ({
|
||||
items,
|
||||
severity,
|
||||
icon,
|
||||
isTooltipOpen,
|
||||
}) => {
|
||||
const popover = usePopover();
|
||||
|
||||
return (
|
||||
<Pill
|
||||
icon={icon}
|
||||
css={(theme) => ({
|
||||
"& svg": { color: theme.roles[severity].outline },
|
||||
borderColor: popover.open ? theme.roles[severity].outline : undefined,
|
||||
borderColor: isTooltipOpen ? theme.roles[severity].outline : undefined,
|
||||
})}
|
||||
>
|
||||
{items.length}
|
||||
@@ -99,7 +109,7 @@ export const NotificationActionButton: FC<ButtonProps> = (props) => {
|
||||
};
|
||||
|
||||
const styles = {
|
||||
// Adds some spacing from the popover content
|
||||
// Adds some spacing from the Tooltip content
|
||||
pillContainer: {
|
||||
padding: "8px 0",
|
||||
},
|
||||
|
||||
+29
-33
@@ -9,7 +9,7 @@ import { withDashboardProvider } from "testHelpers/storybook";
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { getWorkspaceResolveAutostartQueryKey } from "api/queries/workspaceQuota";
|
||||
import type { WorkspacePermissions } from "modules/workspaces/permissions";
|
||||
import { expect, userEvent, waitFor, within } from "storybook/test";
|
||||
import { expect, screen, userEvent, waitFor } from "storybook/test";
|
||||
import { WorkspaceNotifications } from "./WorkspaceNotifications";
|
||||
|
||||
export const defaultPermissions: WorkspacePermissions = {
|
||||
@@ -51,15 +51,13 @@ export const Outdated: Story = {
|
||||
workspace: MockOutdatedWorkspace,
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(screen.getByTestId("info-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(MockTemplateVersion.message),
|
||||
).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
MockTemplateVersion.message,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -71,13 +69,13 @@ export const OutdatedWithMarkdownMessage: Story = {
|
||||
latestVersion: MockTemplateVersionWithMarkdownMessage,
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(screen.getByTestId("info-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/an update is available/i)).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
/an update is available/i,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -102,15 +100,13 @@ export const RequiresManualUpdate: Story = {
|
||||
],
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(screen.getByTestId("warning-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByText(/unable to automatically update/i),
|
||||
).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
/unable to automatically update/i,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -131,13 +127,13 @@ export const Unhealthy: Story = {
|
||||
},
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(screen.getByTestId("warning-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/workspace is unhealthy/i)).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
/workspace is unhealthy/i,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -165,13 +161,13 @@ export const Dormant: Story = {
|
||||
workspace: DormantWorkspace,
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(screen.getByTestId("warning-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/workspace is dormant/i)).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
/workspace is dormant/i,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -205,13 +201,13 @@ export const PendingInQueue: Story = {
|
||||
},
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(await screen.findByTestId("info-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/build is pending/i)).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
/build is pending/i,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
@@ -227,13 +223,13 @@ export const TemplateDeprecated: Story = {
|
||||
},
|
||||
},
|
||||
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const screen = within(canvasElement);
|
||||
|
||||
play: async ({ step }) => {
|
||||
await step("activate hover trigger", async () => {
|
||||
await userEvent.hover(screen.getByTestId("warning-notifications"));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByText(/deprecated template/i)).toBeInTheDocument(),
|
||||
expect(screen.getByRole("tooltip")).toHaveTextContent(
|
||||
/deprecated template/i,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -39,6 +39,18 @@ export const MockOrganization2: TypesGen.Organization = {
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
export const MockOrganization3: TypesGen.Organization = {
|
||||
id: "my-organization-3-id",
|
||||
name: "my-organization-3",
|
||||
display_name: "My Organization 3",
|
||||
description:
|
||||
"Yet another organization that will show up in OrganizationPills.",
|
||||
icon: "/emojis/1f957.png",
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
is_default: false,
|
||||
};
|
||||
|
||||
export const MockTemplateDAUResponse: TypesGen.DAUsResponse = {
|
||||
tz_hour_offset: 0,
|
||||
entries: [
|
||||
@@ -2905,6 +2917,20 @@ export const MockGroupSyncSettings2: TypesGen.GroupSyncSettings = {
|
||||
auto_create_missing_groups: false,
|
||||
};
|
||||
|
||||
export const MockMultipleOverflowGroupSyncSettings: TypesGen.GroupSyncSettings =
|
||||
{
|
||||
field: "group-multiple-overflow-test",
|
||||
mapping: {
|
||||
"idp-group-1": [
|
||||
"fbd2116a-8961-4954-87ae-e4575bd29ce0",
|
||||
"13de3eb4-9b4f-49e7-b0f8-0c3728a0d2e2",
|
||||
"d3562dc1-c120-43a9-ba02-88e43bbca192",
|
||||
],
|
||||
},
|
||||
regex_filter: "@[a-zA-Z0-9_]+",
|
||||
auto_create_missing_groups: false,
|
||||
};
|
||||
|
||||
export const MockRoleSyncSettings: TypesGen.RoleSyncSettings = {
|
||||
field: "role-test",
|
||||
mapping: {
|
||||
@@ -2929,7 +2955,11 @@ export const MockOrganizationSyncSettings2: TypesGen.OrganizationSyncSettings =
|
||||
{
|
||||
field: "organization-test",
|
||||
mapping: {
|
||||
"idp-org-1": ["my-organization-id", "my-organization-2-id"],
|
||||
"idp-org-1": [
|
||||
"my-organization-id",
|
||||
"my-organization-2-id",
|
||||
"my-organization-3-id",
|
||||
],
|
||||
"idp-org-2": ["my-organization-id"],
|
||||
},
|
||||
organization_assign_default: true,
|
||||
@@ -2970,6 +3000,20 @@ export const MockGroup2: TypesGen.Group = {
|
||||
total_member_count: 2,
|
||||
};
|
||||
|
||||
export const MockGroup3: TypesGen.Group = {
|
||||
id: "d3562dc1-c120-43a9-ba02-88e43bbca192",
|
||||
name: "Back-End",
|
||||
display_name: "",
|
||||
avatar_url: "https://example.com",
|
||||
organization_id: MockOrganization.id,
|
||||
organization_name: MockOrganization.name,
|
||||
organization_display_name: MockOrganization.display_name,
|
||||
members: [MockUserOwner, MockUserMember],
|
||||
quota_allowance: 5,
|
||||
source: "user",
|
||||
total_member_count: 2,
|
||||
};
|
||||
|
||||
const MockEveryoneGroup: TypesGen.Group = {
|
||||
// The "Everyone" group must have the same ID as a the organization it belongs
|
||||
// to.
|
||||
|
||||
Reference in New Issue
Block a user