refactor: replace MUI Tooltip component with Tooltip (simple usage) (#20849)

for #19974 

Redo of #20027, this time splitting it into multiple PRs + using our
existing `Tooltip` component instead of creating a new component (see
below). This PR covers the most basic usage of the MUI Tooltip, i.e.,
the tooltip content is a string literal.

~~Adds a global `TooltipProvider` to `AppProviders` and our Storybook
decorators, so that we don't have to render a `TooltipProvider` for
every tooltip instance. Removing redundant `TooltipProvider`s will be
another separate PR~~ <- this was done by #20869
This commit is contained in:
Andrew Aquino
2025-11-25 13:40:26 -08:00
committed by GitHub
parent e340560164
commit 4863812d8c
29 changed files with 563 additions and 331 deletions
+19 -5
View File
@@ -1,6 +1,10 @@
import type { Interpolation, Theme } from "@emotion/react";
import Tooltip from "@mui/material/Tooltip";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import {
type FC,
forwardRef,
@@ -69,16 +73,26 @@ export const NotHealthyBadge: FC = () => {
export const NotRegisteredBadge: FC = () => {
return (
<Tooltip title="Workspace Proxy has never come online and needs to be started.">
<span css={[styles.badge, styles.warnBadge]}>Never seen</span>
<Tooltip>
<TooltipTrigger asChild>
<span css={[styles.badge, styles.warnBadge]}>Never seen</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
Workspace Proxy has never come online and needs to be started.
</TooltipContent>
</Tooltip>
);
};
export const NotReachableBadge: FC = () => {
return (
<Tooltip title="Workspace Proxy not responding to http(s) requests.">
<span css={[styles.badge, styles.warnBadge]}>Not reachable</span>
<Tooltip>
<TooltipTrigger asChild>
<span css={[styles.badge, styles.warnBadge]}>Not reachable</span>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
Workspace Proxy not responding to http(s) requests.
</TooltipContent>
</Tooltip>
);
};
+30 -13
View File
@@ -1,7 +1,11 @@
import { useTheme } from "@emotion/react";
import CircularProgress from "@mui/material/CircularProgress";
import Tooltip from "@mui/material/Tooltip";
import { Abbr } from "components/Abbr/Abbr";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { CircleHelpIcon } from "lucide-react";
import type { FC } from "react";
import { cn } from "utils/cn";
@@ -11,14 +15,12 @@ interface LatencyProps {
latency?: number;
isLoading?: boolean;
className?: string;
iconClassName?: string;
}
export const Latency: FC<LatencyProps> = ({
latency,
isLoading,
className,
iconClassName,
}) => {
const theme = useTheme();
// Always use the no latency color for loading.
@@ -26,22 +28,37 @@ export const Latency: FC<LatencyProps> = ({
if (isLoading) {
return (
<Tooltip title="Loading latency..." className={className}>
<CircularProgress
className={cn("!size-icon-xs", iconClassName)}
style={{ color }}
/>
<Tooltip>
<TooltipTrigger asChild>
{/**
* Spinning progress icon must be placed inside a fixed-size container,
* to ensure tooltip remains stationary when opened
*/}
<div
className={cn(
"size-4 flex flex-wrap place-content-center",
className,
)}
>
<CircularProgress className="!size-icon-xs" style={{ color }} />
</div>
</TooltipTrigger>
<TooltipContent side="bottom">Loading latency...</TooltipContent>
</Tooltip>
);
}
if (!latency) {
return (
<Tooltip title="Latency not available" className={className}>
<CircleHelpIcon
className={cn("!size-icon-sm", iconClassName)}
style={{ color }}
/>
<Tooltip>
<TooltipTrigger asChild>
<CircleHelpIcon
aria-label="Latency not available"
className={cn("!size-icon-sm", className)}
style={{ color }}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Latency not available</TooltipContent>
</Tooltip>
);
}
+19 -11
View File
@@ -1,10 +1,15 @@
import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import TextField, { type TextFieldProps } from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useEffectEvent } from "hooks/hookPolyfills";
import { SearchIcon, XIcon } from "lucide-react";
import { type FC, useLayoutEffect, useRef } from "react";
export type SearchFieldProps = Omit<TextFieldProps, "onChange"> & {
onChange: (query: string) => void;
autoFocus?: boolean;
@@ -47,16 +52,19 @@ export const SearchField: FC<SearchFieldProps> = ({
),
endAdornment: value !== "" && (
<InputAdornment position="end">
<Tooltip title="Clear search">
<IconButton
size="small"
onClick={() => {
onChange("");
}}
>
<XIcon className="size-icon-xs" />
<span className="sr-only">Clear search</span>
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<IconButton
size="small"
onClick={() => {
onChange("");
}}
>
<XIcon className="size-icon-xs" />
<span className="sr-only">Clear search</span>
</IconButton>
</TooltipTrigger>
<TooltipContent side="bottom">Clear search</TooltipContent>
</Tooltip>
</InputAdornment>
),
+2
View File
@@ -14,6 +14,8 @@ export const Tooltip = TooltipPrimitive.Root;
export const TooltipTrigger = TooltipPrimitive.Trigger;
export const TooltipArrow = TooltipPrimitive.Arrow;
export type TooltipContentProps = React.ComponentPropsWithoutRef<
typeof TooltipPrimitive.Content
> & {
@@ -112,6 +112,7 @@ export const ProxyMenu: FC<ProxyMenuProps> = ({ proxyContextValue }) => {
// to turn this off because otherwise, screen readers will skip over all
// the descriptive text and will only have access to the latency options
autoFocus={false}
className="z-0"
>
{proxyContextValue.proxies &&
proxyContextValue.proxies.length > 1 && [
@@ -6,11 +6,15 @@ import {
} from "@emotion/react";
import Divider from "@mui/material/Divider";
import MenuItem from "@mui/material/MenuItem";
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 { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import {
CircleUserIcon,
LogOutIcon,
@@ -102,23 +106,31 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
<Divider css={{ marginBottom: "0 !important" }} />
<Stack css={styles.info} spacing={0}>
<Tooltip title="Browse the source code">
<a
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
>
{buildInfo?.version} <SquareArrowOutUpRightIcon />
</a>
<Tooltip>
<TooltipTrigger asChild>
<a
css={[styles.footerText, styles.buildInfo]}
href={buildInfo?.external_url}
target="_blank"
rel="noreferrer"
>
{buildInfo?.version} <SquareArrowOutUpRightIcon />
</a>
</TooltipTrigger>
<TooltipContent side="bottom">Browse the source code</TooltipContent>
</Tooltip>
{buildInfo?.deployment_id && (
<div className="flex items-center text-xs">
<Tooltip title="Deployment Identifier">
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
{buildInfo.deployment_id}
</span>
<Tooltip>
<TooltipTrigger asChild>
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
{buildInfo.deployment_id}
</span>
</TooltipTrigger>
<TooltipContent side="bottom">
Deployment Identifier
</TooltipContent>
</Tooltip>
<CopyButton
text={buildInfo.deployment_id}
+18 -11
View File
@@ -1,7 +1,11 @@
import { useTheme } from "@emotion/react";
import Tooltip from "@mui/material/Tooltip";
import type { HealthMessage, ProvisionerDaemon } from "api/typesGenerated";
import { Pill } from "components/Pill/Pill";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { Building2Icon, UserIcon } from "lucide-react";
import type { FC } from "react";
import { createDayString } from "utils/createDayString";
@@ -74,16 +78,19 @@ export const Provisioner: FC<ProvisionerProps> = ({
justifyContent: "right",
}}
>
<Tooltip title="Scope">
<Pill size="lg" icon={iconScope}>
<span
css={{
":first-letter": { textTransform: "uppercase" },
}}
>
{daemonScope}
</span>
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill size="lg" icon={iconScope}>
<span
css={{
":first-letter": { textTransform: "uppercase" },
}}
>
{daemonScope}
</span>
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Scope</TooltipContent>
</Tooltip>
{extraTags.map(([key, value]) => (
<ProvisionerTag key={key} tagName={key} tagValue={value} />
+14 -5
View File
@@ -1,5 +1,4 @@
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import { watchAgentMetadata } from "api/api";
import type {
ServerSentEvent,
@@ -8,6 +7,11 @@ import type {
} from "api/typesGenerated";
import { displayError } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import dayjs from "dayjs";
import {
type FC,
@@ -183,10 +187,15 @@ const MetadataItem: FC<MetadataItemProps> = ({ item }) => {
status === "loading" ? (
<Skeleton width={65} height={12} variant="text" className="mt-[6px]" />
) : status === "stale" ? (
<Tooltip title="This data is stale and no longer up to date">
<StaticWidth className="text-ellipsis overflow-hidden whitespace-nowrap max-w-64 text-sm text-content-disabled cursor-pointer">
{item.result.value}
</StaticWidth>
<Tooltip>
<TooltipTrigger asChild>
<StaticWidth className="text-ellipsis overflow-hidden whitespace-nowrap max-w-64 text-sm text-content-disabled cursor-pointer">
{item.result.value}
</StaticWidth>
</TooltipTrigger>
<TooltipContent side="bottom">
This data is stale and no longer up to date
</TooltipContent>
</Tooltip>
) : (
<StaticWidth
@@ -141,6 +141,13 @@ export const BunchOfApps: Story = {
},
};
export const Disconnected: Story = {
args: {
agent: M.MockWorkspaceAgentDisconnected,
initialMetadata: [],
},
};
export const Connecting: Story = {
args: {
agent: M.MockWorkspaceAgentConnecting,
+50 -31
View File
@@ -1,6 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import type {
WorkspaceAgent,
WorkspaceAgentDevcontainer,
@@ -13,6 +12,11 @@ import {
HelpTooltipTitle,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { TriangleAlertIcon } from "lucide-react";
import type { FC } from "react";
@@ -35,12 +39,15 @@ const ReadyLifecycle: FC = () => {
const StartingLifecycle: FC = () => {
return (
<Tooltip title="Starting...">
<div
role="status"
aria-label="Starting..."
css={[styles.status, styles.connecting]}
/>
<Tooltip>
<TooltipTrigger asChild>
<div
role="status"
aria-label="Starting..."
css={[styles.status, styles.connecting]}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Starting...</TooltipContent>
</Tooltip>
);
};
@@ -110,12 +117,15 @@ const StartErrorLifecycle: FC<AgentStatusProps> = ({ agent }) => {
const ShuttingDownLifecycle: FC = () => {
return (
<Tooltip title="Stopping...">
<div
role="status"
aria-label="Stopping..."
css={[styles.status, styles.connecting]}
/>
<Tooltip>
<TooltipTrigger asChild>
<div
role="status"
aria-label="Stopping..."
css={[styles.status, styles.connecting]}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Stopping...</TooltipContent>
</Tooltip>
);
};
@@ -170,12 +180,15 @@ const ShutdownErrorLifecycle: FC<AgentStatusProps> = ({ agent }) => {
const OffLifecycle: FC = () => {
return (
<Tooltip title="Stopped">
<div
role="status"
aria-label="Stopped"
css={[styles.status, styles.disconnected]}
/>
<Tooltip>
<TooltipTrigger asChild>
<div
role="status"
aria-label="Stopped"
css={[styles.status, styles.disconnected]}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Stopped</TooltipContent>
</Tooltip>
);
};
@@ -218,24 +231,30 @@ const ConnectedStatus: FC<AgentStatusProps> = ({ agent }) => {
const DisconnectedStatus: FC = () => {
return (
<Tooltip title="Disconnected">
<div
role="status"
aria-label="Disconnected"
css={[styles.status, styles.disconnected]}
/>
<Tooltip>
<TooltipTrigger asChild>
<div
role="status"
aria-label="Disconnected"
css={[styles.status, styles.disconnected]}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Disconnected</TooltipContent>
</Tooltip>
);
};
const ConnectingStatus: FC = () => {
return (
<Tooltip title="Connecting...">
<div
role="status"
aria-label="Connecting..."
css={[styles.status, styles.connecting]}
/>
<Tooltip>
<TooltipTrigger asChild>
<div
role="status"
aria-label="Connecting..."
css={[styles.status, styles.connecting]}
/>
</TooltipTrigger>
<TooltipContent side="bottom">Connecting...</TooltipContent>
</Tooltip>
);
};
@@ -1,12 +1,12 @@
import { MockWorkspaceResource } from "testHelpers/entities";
import { renderComponent } from "testHelpers/renderHelpers";
import { render } from "testHelpers/renderHelpers";
import { screen } from "@testing-library/react";
import type { WorkspaceResourceMetadata } from "api/typesGenerated";
import { ResourceCard } from "./ResourceCard";
describe("Resource Card", () => {
it("renders daily cost and metadata tiles", async () => {
renderComponent(
render(
<ResourceCard resource={MockWorkspaceResource} agentRow={() => <></>} />,
);
expect(
@@ -45,9 +45,7 @@ describe("Resource Card", () => {
],
};
renderComponent(
<ResourceCard resource={mockResource} agentRow={() => <></>} />,
);
render(<ResourceCard resource={mockResource} agentRow={() => <></>} />);
expect(screen.getByText(mockResource.daily_cost)).toBeInTheDocument();
expect(
screen.getByText(mockResource.metadata?.[0].value),
@@ -92,9 +90,7 @@ describe("Resource Card", () => {
],
};
renderComponent(
<ResourceCard resource={mockResource} agentRow={() => <></>} />,
);
render(<ResourceCard resource={mockResource} agentRow={() => <></>} />);
expect(screen.queryByText(mockResource.daily_cost)).not.toBeInTheDocument();
expect(
screen.getByText(mockResource.metadata?.[0].value),
+22 -14
View File
@@ -1,11 +1,15 @@
import type { Interpolation, Theme } from "@emotion/react";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import type { WorkspaceAgent, WorkspaceResource } from "api/typesGenerated";
import { CopyableValue } from "components/CopyableValue/CopyableValue";
import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import { MemoizedInlineMarkdown } from "components/Markdown/Markdown";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { Children, type FC, type JSX, useState } from "react";
import { ResourceAvatar } from "./ResourceAvatar";
import { SensitiveValue } from "./SensitiveValue";
@@ -162,19 +166,23 @@ export const ResourceCard: FC<ResourceCardProps> = ({ resource, agentRow }) => {
})}
</div>
{mLength > 4 && (
<Tooltip
title={
shouldDisplayAllMetadata ? "Hide metadata" : "Show all metadata"
}
>
<IconButton
onClick={() => {
setShouldDisplayAllMetadata((value) => !value);
}}
size="large"
>
<DropdownArrow margin={false} close={shouldDisplayAllMetadata} />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<IconButton
onClick={() => {
setShouldDisplayAllMetadata((value) => !value);
}}
size="large"
>
<DropdownArrow
margin={false}
close={shouldDisplayAllMetadata}
/>
</IconButton>
</TooltipTrigger>
<TooltipContent side="bottom">
{shouldDisplayAllMetadata ? "Hide metadata" : "Show all metadata"}
</TooltipContent>
</Tooltip>
)}
</Stack>
+20 -26
View File
@@ -1,7 +1,10 @@
import { css, type Interpolation, type Theme } from "@emotion/react";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import { CopyableValue } from "components/CopyableValue/CopyableValue";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type FC, useState } from "react";
@@ -25,37 +28,28 @@ export const SensitiveValue: FC<SensitiveValueProps> = ({ value }) => {
);
return (
<div
css={{
display: "flex",
alignItems: "center",
gap: 4,
}}
>
<div className="flex items-center gap-1">
<CopyableValue
value={value}
className="w-[calc(100%-22px)] overflow-hidden whitespace-nowrap text-ellipsis"
>
{displayValue}
</CopyableValue>
<Tooltip title={buttonLabel}>
<IconButton
css={styles.button}
onClick={() => {
setShouldDisplay((value) => !value);
}}
size="small"
aria-label={buttonLabel}
>
{icon}
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<IconButton
className="text-inherit"
onClick={() => {
setShouldDisplay((value) => !value);
}}
size="small"
aria-label={buttonLabel}
>
{icon}
</IconButton>
</TooltipTrigger>
<TooltipContent side="bottom">{buttonLabel}</TooltipContent>
</Tooltip>
</div>
);
};
const styles = {
button: css`
color: inherit;
`,
} satisfies Record<string, Interpolation<Theme>>;
@@ -1,8 +1,12 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import type { WorkspaceBuild } from "api/typesGenerated";
import { BuildIcon } from "components/BuildIcon/BuildIcon";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { InfoIcon } from "lucide-react";
import { createDayString } from "utils/createDayString";
import {
@@ -45,13 +49,18 @@ export const WorkspaceBuildData = ({ build }: { build: WorkspaceBuild }) => {
</span>
{!systemBuildReasons.includes(build.reason) &&
build.transition === "start" && (
<Tooltip title={buildReasonLabels[build.reason]}>
<InfoIcon
css={(theme) => ({
color: theme.palette.info.light,
})}
className="size-icon-xs -mt-px"
/>
<Tooltip>
<TooltipTrigger asChild>
<InfoIcon
css={(theme) => ({
color: theme.palette.info.light,
})}
className="size-icon-xs -mt-px"
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{buildReasonLabels[build.reason]}
</TooltipContent>
</Tooltip>
)}
</div>
@@ -1,7 +1,6 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import MuiLink from "@mui/material/Link";
import Skeleton from "@mui/material/Skeleton";
import Tooltip from "@mui/material/Tooltip";
import type { GetLicensesResponse } from "api/api";
import type { Feature, UserStatusChangeCount } from "api/typesGenerated";
import { Button } from "components/Button/Button";
@@ -12,6 +11,11 @@ import {
} from "components/SettingsHeader/SettingsHeader";
import { Spinner } from "components/Spinner/Spinner";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useWindowSize } from "hooks/useWindowSize";
import { PlusIcon, RotateCwIcon } from "lucide-react";
import type { FC } from "react";
@@ -80,17 +84,23 @@ const LicensesSettingsPageView: FC<Props> = ({
Add a license
</Link>
</Button>
<Tooltip title="Refresh license entitlements. This is done automatically every 10 minutes.">
<Button
disabled={isRefreshing}
onClick={refreshEntitlements}
variant="outline"
>
<Spinner loading={isRefreshing}>
<RotateCwIcon className="size-icon-xs" />
</Spinner>
Refresh
</Button>
<Tooltip>
<TooltipTrigger asChild>
<Button
disabled={isRefreshing}
onClick={refreshEntitlements}
variant="outline"
>
<Spinner loading={isRefreshing}>
<RotateCwIcon className="size-icon-xs" />
</Spinner>
Refresh
</Button>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
Refresh license entitlements. This is done automatically every 10
minutes.
</TooltipContent>
</Tooltip>
</Stack>
</Stack>
@@ -6,7 +6,6 @@ import ListItem from "@mui/material/ListItem";
import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText";
import ToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";
import Tooltip from "@mui/material/Tooltip";
import { getErrorMessage } from "api/errors";
import {
type selectTemplatesByGroup,
@@ -17,6 +16,11 @@ import { Alert } from "components/Alert/Alert";
import { Button } from "components/Button/Button";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import {
castNotificationMethod,
methodIcons,
@@ -186,22 +190,25 @@ const MethodToggleGroup: FC<MethodToggleGroupProps> = ({
const Icon = methodIcons[method];
const label = methodLabels[method];
return (
<Tooltip key={method} title={label}>
<ToggleButton
value={method}
css={styles.toggleButton}
onClick={(e) => {
// Retain the value if the user clicks the same button, ensuring
// at least one value remains selected.
if (method === value) {
e.preventDefault();
e.stopPropagation();
return;
}
}}
>
<Icon aria-label={label} />
</ToggleButton>
<Tooltip key={method}>
<TooltipTrigger asChild>
<ToggleButton
value={method}
css={styles.toggleButton}
onClick={(e) => {
// Retain the value if the user clicks the same button, ensuring
// at least one value remains selected.
if (method === value) {
e.preventDefault();
e.stopPropagation();
return;
}
}}
>
<Icon aria-label={label} />
</ToggleButton>
</TooltipTrigger>
<TooltipContent side="bottom">{label}</TooltipContent>
</Tooltip>
);
})}
@@ -1,12 +1,16 @@
import type { Interpolation, Theme } from "@emotion/react";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import type { ApiErrorResponse } from "api/errors";
import type { ExternalAuth, ExternalAuthDevice } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import { Avatar } from "components/Avatar/Avatar";
import { GitDeviceAuth } from "components/GitDeviceAuth/GitDeviceAuth";
import { SignInLayout } from "components/SignInLayout/SignInLayout";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { Welcome } from "components/Welcome/Welcome";
import { ExternalLinkIcon, RotateCwIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
@@ -82,17 +86,22 @@ const ExternalAuthPageView: FC<ExternalAuthPageViewProps> = ({
return;
}
return (
<Tooltip key={install.id} title={install.account.login}>
<Link
href={install.account.profile_url}
target="_blank"
rel="noreferrer"
>
<Avatar
src={install.account.avatar_url}
fallback={install.account.login}
/>
</Link>
<Tooltip key={install.id}>
<TooltipTrigger asChild>
<Link
href={install.account.profile_url}
target="_blank"
rel="noreferrer"
>
<Avatar
src={install.account.avatar_url}
fallback={install.account.login}
/>
</Link>
</TooltipTrigger>
<TooltipContent side="bottom">
{install.account.login}
</TooltipContent>
</Tooltip>
);
})}
+31 -16
View File
@@ -1,5 +1,4 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Tooltip from "@mui/material/Tooltip";
import type {
DERPNodeReport,
DERPRegionReport,
@@ -7,6 +6,11 @@ import type {
HealthSeverity,
} from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { ChevronLeftIcon, CodeIcon, HashIcon } from "lucide-react";
import type { FC } from "react";
import { Link, useOutletContext, useParams } from "react-router";
@@ -85,15 +89,21 @@ const DERPRegionPage: FC = () => {
<section>
<div css={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
<Tooltip title="Region ID">
<Pill icon={<HashIcon className="size-icon-sm" />}>
{region!.RegionID}
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<HashIcon className="size-icon-sm" />}>
{region!.RegionID}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Region ID</TooltipContent>
</Tooltip>
<Tooltip title="Region Code">
<Pill icon={<CodeIcon className="size-icon-sm" />}>
{region!.RegionCode}
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<CodeIcon className="size-icon-sm" />}>
{region!.RegionCode}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Region Code</TooltipContent>
</Tooltip>
<BooleanPill value={region!.EmbeddedRelay}>
Embedded Relay
@@ -127,13 +137,18 @@ const DERPRegionPage: FC = () => {
</div>
<div css={reportStyles.pills}>
<Tooltip title="Round trip ping">
<Pill
css={{ color: latencyColor }}
icon={<StatusCircle color={latencyColor} />}
>
{report.round_trip_ping_ms}ms
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill
css={{ color: latencyColor }}
icon={<StatusCircle color={latencyColor} />}
>
{report.round_trip_ping_ms}ms
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">
Round trip ping
</TooltipContent>
</Tooltip>
<BooleanPill value={report.can_exchange_messages}>
Exchange Messages
+25 -16
View File
@@ -1,10 +1,14 @@
import CircularProgress from "@mui/material/CircularProgress";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import { health, refreshHealth } from "api/queries/debug";
import type { HealthSeverity } from "api/typesGenerated";
import { ErrorAlert } from "components/Alert/ErrorAlert";
import { Loader } from "components/Loader/Loader";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import kebabCase from "lodash/fp/kebabCase";
import { BellOffIcon, RotateCcwIcon } from "lucide-react";
import { DashboardFullPage } from "modules/dashboard/DashboardLayout";
@@ -76,21 +80,26 @@ export const HealthLayout: FC = () => {
<div className="flex items-center justify-between">
<HealthIcon size={32} severity={healthStatus.severity} />
<Tooltip title="Refresh health checks">
<IconButton
size="small"
disabled={isRefreshing}
data-testid="healthcheck-refresh-button"
onClick={() => {
forceRefresh();
}}
>
{isRefreshing ? (
<CircularProgress size={16} />
) : (
<RotateCcwIcon className="size-5" />
)}
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<IconButton
size="small"
disabled={isRefreshing}
data-testid="healthcheck-refresh-button"
onClick={() => {
forceRefresh();
}}
>
{isRefreshing ? (
<CircularProgress size={16} />
) : (
<RotateCcwIcon className="size-5" />
)}
</IconButton>
</TooltipTrigger>
<TooltipContent side="bottom">
Refresh health checks
</TooltipContent>
</Tooltip>
</div>
<div className="font-medium mt-4">
+12 -5
View File
@@ -1,7 +1,11 @@
import { useTheme } from "@emotion/react";
import Tooltip from "@mui/material/Tooltip";
import type { HealthcheckReport } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { CodeIcon } from "lucide-react";
import { useOutletContext } from "react-router";
import { MONOSPACE_FONT_FAMILY } from "theme/constants";
@@ -45,10 +49,13 @@ const WebsocketPage = () => {
})}
<section>
<Tooltip title="Code">
<Pill icon={<CodeIcon className="size-icon-sm" />}>
{websocket.code}
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<CodeIcon className="size-icon-sm" />}>
{websocket.code}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Code</TooltipContent>
</Tooltip>
</section>
@@ -1,7 +1,11 @@
import { useTheme } from "@emotion/react";
import Tooltip from "@mui/material/Tooltip";
import type { HealthcheckReport } from "api/typesGenerated";
import { Alert } from "components/Alert/Alert";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { GlobeIcon, HashIcon } from "lucide-react";
import type { FC } from "react";
import { useOutletContext } from "react-router";
@@ -106,17 +110,25 @@ const WorkspaceProxyPage: FC = () => {
<div css={{ display: "flex", flexWrap: "wrap", gap: 12 }}>
{region.wildcard_hostname && (
<Tooltip title="Wildcard Hostname">
<Pill icon={<GlobeIcon />}>
{region.wildcard_hostname}
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<GlobeIcon />}>
{region.wildcard_hostname}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">
Wildcard Hostname
</TooltipContent>
</Tooltip>
)}
{region.version && (
<Tooltip title="Version">
<Pill icon={<HashIcon className="size-icon-sm" />}>
{region.version}
</Pill>
<Tooltip>
<TooltipTrigger asChild>
<Pill icon={<HashIcon className="size-icon-sm" />}>
{region.version}
</Pill>
</TooltipTrigger>
<TooltipContent side="bottom">Version</TooltipContent>
</Tooltip>
)}
{region.derp_enabled && (
+27 -22
View File
@@ -3,7 +3,6 @@ import IconButton from "@mui/material/IconButton";
import InputAdornment from "@mui/material/InputAdornment";
import Link from "@mui/material/Link";
import TextField from "@mui/material/TextField";
import Tooltip from "@mui/material/Tooltip";
import { CopyableValue } from "components/CopyableValue/CopyableValue";
import { EmptyState } from "components/EmptyState/EmptyState";
import { Margins } from "components/Margins/Margins";
@@ -12,6 +11,11 @@ import {
PageHeaderSubtitle,
PageHeaderTitle,
} from "components/PageHeader/PageHeader";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { SearchIcon, XIcon } from "lucide-react";
import { type FC, type ReactNode, useMemo, useState } from "react";
import {
@@ -75,20 +79,18 @@ const IconsPage: FC = () => {
<Margins>
<PageHeader
actions={
<Tooltip
placement="bottom-end"
title={
<p className="p-2 leading-6 text-sm">
You can suggest a new icon by submitting a Pull Request to our
public GitHub repository. Just keep in mind that it should be
relevant to many Coder users, and redistributable under a
permissive license.
</p>
}
>
<Link href="https://github.com/coder/coder/tree/main/site/static/icon">
Suggest an icon
</Link>
<Tooltip>
<TooltipTrigger asChild>
<Link href="https://github.com/coder/coder/tree/main/site/static/icon">
Suggest an icon
</Link>
</TooltipTrigger>
<TooltipContent side="bottom" align="end" className="max-w-xs">
You can suggest a new icon by submitting a Pull Request to our
public GitHub repository. Just keep in mind that it should be
relevant to many Coder users, and redistributable under a
permissive license.
</TooltipContent>
</Tooltip>
}
>
@@ -122,13 +124,16 @@ const IconsPage: FC = () => {
),
endAdornment: searchInputText && (
<InputAdornment position="end">
<Tooltip title="Clear filter">
<IconButton
size="small"
onClick={() => setSearchInputText("")}
>
<XIcon className="size-icon-xs" />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<IconButton
size="small"
onClick={() => setSearchInputText("")}
>
<XIcon className="size-icon-xs" />
</IconButton>
</TooltipTrigger>
<TooltipContent side="bottom">Clear filter</TooltipContent>
</Tooltip>
</InputAdornment>
),
@@ -1,7 +1,6 @@
import { useTheme } from "@emotion/react";
import LinearProgress from "@mui/material/LinearProgress";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import { entitlements } from "api/queries/entitlements";
import {
insightsTemplate,
@@ -33,6 +32,12 @@ import {
} from "components/HelpTooltip/HelpTooltip";
import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipArrow,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import {
CircleCheck as CircleCheckIcon,
@@ -495,24 +500,26 @@ const TemplateUsagePanel: FC<TemplateUsagePanelProps> = ({
{usage.display_name}
</div>
</div>
<Tooltip
title={`${Math.floor(percentage)}%`}
placement="top"
arrow
>
<LinearProgress
value={percentage}
variant="determinate"
css={{
width: "100%",
height: 8,
backgroundColor: theme.palette.divider,
"& .MuiLinearProgress-bar": {
backgroundColor: usageColors[i],
borderRadius: 999,
},
}}
/>
<Tooltip>
<TooltipTrigger asChild>
<LinearProgress
value={percentage}
variant="determinate"
css={{
width: "100%",
height: 8,
backgroundColor: theme.palette.divider,
"& .MuiLinearProgress-bar": {
backgroundColor: usageColors[i],
borderRadius: 999,
},
}}
/>
</TooltipTrigger>
<TooltipContent>
{Math.floor(percentage)}%
<TooltipArrow className="fill-border" />
</TooltipContent>
</Tooltip>
<Stack
spacing={0}
@@ -612,11 +619,13 @@ const TemplateParametersUsagePanel: FC<TemplateParametersUsagePanelProps> = ({
}}
>
<div>Value</div>
<Tooltip
title="The number of workspaces using this value"
placement="top"
>
<div>Count</div>
<Tooltip>
<TooltipTrigger asChild>
<div>Count</div>
</TooltipTrigger>
<TooltipContent>
The number of workspaces using this value
</TooltipContent>
</Tooltip>
</ParameterUsageRow>
{[...parameter.values]
@@ -1,6 +1,5 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import IconButton from "@mui/material/IconButton";
import Tooltip from "@mui/material/Tooltip";
import { getErrorDetail, getErrorMessage } from "api/errors";
import type {
ProvisionerJobLog,
@@ -23,6 +22,11 @@ import {
} from "components/FullPageLayout/Topbar";
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import {
ChevronLeftIcon,
ExternalLinkIcon,
@@ -217,10 +221,15 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
data-testid="topbar"
>
<div>
<Tooltip title="Back to the template">
<TopbarIconButton component={RouterLink} to={templateLink}>
<ChevronLeftIcon className="size-icon-sm" />
</TopbarIconButton>
<Tooltip>
<TooltipTrigger asChild>
<TopbarIconButton component={RouterLink} to={templateLink}>
<ChevronLeftIcon className="size-icon-sm" />
</TopbarIconButton>
</TooltipTrigger>
<TooltipContent side="bottom">
Back to the template
</TooltipContent>
</Tooltip>
</div>
@@ -367,16 +376,19 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
},
}}
>
<Tooltip title="Create File" placement="top">
<IconButton
aria-label="Create File"
onClick={(event) => {
setCreateFileOpen(true);
event.currentTarget.blur();
}}
>
<PlusIcon className="size-icon-xs" />
</IconButton>
<Tooltip>
<TooltipTrigger asChild>
<IconButton
aria-label="Create File"
onClick={(event) => {
setCreateFileOpen(true);
event.currentTarget.blur();
}}
>
<PlusIcon className="size-icon-xs" />
</IconButton>
</TooltipTrigger>
<TooltipContent>Create File</TooltipContent>
</Tooltip>
</div>
<CreateFileDialog
@@ -1,5 +1,4 @@
import { useTheme } from "@emotion/react";
import Tooltip from "@mui/material/Tooltip";
import { externalAuthProvider } from "api/queries/externalAuth";
import type {
ExternalAuthLink,
@@ -27,6 +26,11 @@ import {
TableRow,
} from "components/Table/Table";
import { TableEmpty } from "components/TableEmpty/TableEmpty";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import type { ExternalAuthPollingState } from "hooks/useExternalAuth";
import { EllipsisVertical, RefreshCcwIcon } from "lucide-react";
import { type FC, useCallback, useEffect, useState } from "react";
@@ -136,11 +140,13 @@ const ExternalAuthRow: FC<ExternalAuthRowProps> = ({
* attempt to authenticate when the token expires.
*/}
{link?.has_refresh_token && authenticated && (
<Tooltip
title="Authentication token will automatically refresh when expired."
placement="right"
>
<RefreshCcwIcon className="size-3" />
<Tooltip>
<TooltipTrigger asChild>
<RefreshCcwIcon className="size-3" />
</TooltipTrigger>
<TooltipContent side="right" className="max-w-xs">
Authentication token will automatically refresh when expired.
</TooltipContent>
</Tooltip>
)}
@@ -6,7 +6,6 @@ import ListItem from "@mui/material/ListItem";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemText, { listItemTextClasses } from "@mui/material/ListItemText";
import Switch from "@mui/material/Switch";
import Tooltip from "@mui/material/Tooltip";
import {
customNotificationTemplates,
disableNotification,
@@ -23,6 +22,11 @@ import type {
import { displayError, displaySuccess } from "components/GlobalSnackbar/utils";
import { Loader } from "components/Loader/Loader";
import { Stack } from "components/Stack/Stack";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { useAuthenticated } from "hooks";
import {
castNotificationMethod,
@@ -199,8 +203,13 @@ const NotificationsPage: FC = () => {
css={styles.listItemEndIcon}
aria-label="Delivery method"
>
<Tooltip title={`Delivery via ${label}`}>
<Icon aria-label={label} />
<Tooltip>
<TooltipTrigger asChild>
<Icon aria-label={label} />
</TooltipTrigger>
<TooltipContent side="bottom">
Delivery via {label}
</TooltipContent>
</Tooltip>
</ListItemIcon>
</ListItem>
@@ -1,6 +1,10 @@
import Tooltip from "@mui/material/Tooltip";
import type { Workspace, WorkspaceBuildParameter } from "api/typesGenerated";
import { TopbarButton } from "components/FullPageLayout/Topbar";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import {
BanIcon,
CircleStopIcon,
@@ -30,29 +34,30 @@ export const UpdateButton: FC<ActionButtonProps> = ({
requireActiveVersion,
}) => {
return (
<Tooltip
title={
requireActiveVersion
<Tooltip>
<TooltipTrigger asChild>
<TopbarButton
data-testid="workspace-update-button"
disabled={loading}
onClick={() => handleAction()}
>
{requireActiveVersion ? <PlayIcon /> : <CloudIcon />}
{loading ? (
<>Updating&hellip;</>
) : isRunning ? (
<>Update and restart&hellip;</>
) : (
<>Update and start&hellip;</>
)}
</TopbarButton>
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{requireActiveVersion
? "This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version."
: isRunning
? "Stop workspace and restart it with the latest template version."
: "Start workspace with the latest template version."
}
>
<TopbarButton
data-testid="workspace-update-button"
disabled={loading}
onClick={() => handleAction()}
>
{requireActiveVersion ? <PlayIcon /> : <CloudIcon />}
{loading ? (
<>Updating&hellip;</>
) : isRunning ? (
<>Update and restart&hellip;</>
) : (
<>Update and start&hellip;</>
)}
</TopbarButton>
: "Start workspace with the latest template version."}
</TooltipContent>
</Tooltip>
);
};
@@ -88,7 +93,14 @@ export const StartButton: FC<ActionButtonPropsWithWorkspace> = ({
);
if (tooltipText) {
mainButton = <Tooltip title={tooltipText}>{mainButton}</Tooltip>;
mainButton = (
<Tooltip>
<TooltipTrigger asChild>{mainButton}</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{tooltipText}
</TooltipContent>
</Tooltip>
);
}
return (
@@ -1,6 +1,5 @@
import { type Interpolation, type Theme, useTheme } from "@emotion/react";
import Link from "@mui/material/Link";
import Tooltip from "@mui/material/Tooltip";
import { workspaceQuota } from "api/queries/workspaceQuota";
import type * as TypesGen from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
@@ -19,6 +18,11 @@ import {
HelpTooltipContent,
HelpTooltipTrigger,
} from "components/HelpTooltip/HelpTooltip";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "components/Tooltip/Tooltip";
import { ChevronLeftIcon, CircleDollarSign, TrashIcon } from "lucide-react";
import { useDashboard } from "modules/dashboard/useDashboard";
import { linkToTemplate, useLinks } from "modules/navigation";
@@ -108,10 +112,13 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
return (
<Topbar css={{ gridArea: "topbar" }}>
<Tooltip title="Back to workspaces">
<TopbarIconButton component={RouterLink} to="/workspaces">
<ChevronLeftIcon className="size-icon-sm" />
</TopbarIconButton>
<Tooltip>
<TooltipTrigger asChild>
<TopbarIconButton component={RouterLink} to="/workspaces">
<ChevronLeftIcon className="size-icon-sm" />
</TopbarIconButton>
</TooltipTrigger>
<TooltipContent side="bottom">Back to workspaces</TooltipContent>
</Tooltip>
<div css={styles.topbarLeft}>
+1 -1
View File
@@ -1043,7 +1043,7 @@ export const MockWorkspaceAppStatus: TypesGen.WorkspaceAppStatus = {
icon: "",
};
const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
export const MockWorkspaceAgentDisconnected: TypesGen.WorkspaceAgent = {
...MockWorkspaceAgent,
id: "test-workspace-agent-2",
name: "another-workspace-agent",