From a87a44412fe208c77476bcbc503c75b6ed6f5341 Mon Sep 17 00:00:00 2001 From: Andrew Aquino Date: Thu, 11 Dec 2025 13:30:16 -0800 Subject: [PATCH] feat: show sharing info at bottom of AppLink's tooltip (#21197) for #19974 `ShareIcon`, which returns a MUI tooltip, had these problems: 1. There was no Storybook story which rendered the "Open external URL" tooltip 2. `AppLink` renders a button. If it has tooltip text, this button is also a Radix `TooltipTrigger`. Since `ShareIcon` is rendered as a child of the `AppLink` button, `ShareIcon`'s tooltip (a nested tooltip) was never appearing. I did try turning `ShareIcon` into a Radix tooltip as well, but I still couldn't get the nested tooltip's text to appear, and nested tooltips are not very accessible. AFAIK there's no way to focus a child element of a button. I've deleted the separate `ShareIcon` component and moved `ShareIcon`'s tooltip text to the bottom of `AppLink`'s tooltip: ## before image ^The `UsersIcon` on the right is a MUI tooltip trigger, but it can't receive focus. It's supposed to show the text "Shared with all authenticated users" when focused ## after image --- .../resources/AppLink/AppLink.stories.tsx | 13 ++++++ .../src/modules/resources/AppLink/AppLink.tsx | 46 +++++++++++++++++-- .../modules/resources/AppLink/ShareIcon.tsx | 45 ------------------ 3 files changed, 55 insertions(+), 49 deletions(-) delete mode 100644 site/src/modules/resources/AppLink/ShareIcon.tsx diff --git a/site/src/modules/resources/AppLink/AppLink.stories.tsx b/site/src/modules/resources/AppLink/AppLink.stories.tsx index d7d00a1c02..e094c6a32b 100644 --- a/site/src/modules/resources/AppLink/AppLink.stories.tsx +++ b/site/src/modules/resources/AppLink/AppLink.stories.tsx @@ -77,6 +77,19 @@ export const ExternalAppNotInstalled: Story = { }, }; +export const ExternalAppShareable: Story = { + args: { + workspace: MockWorkspace, + app: { + ...MockWorkspaceApp, + url: "vscode://open", + external: true, + sharing_level: "authenticated", + }, + agent: MockWorkspaceAgent, + }, +}; + export const SharingLevelOwner: Story = { args: { workspace: MockWorkspace, diff --git a/site/src/modules/resources/AppLink/AppLink.tsx b/site/src/modules/resources/AppLink/AppLink.tsx index 419148ed09..9294f94912 100644 --- a/site/src/modules/resources/AppLink/AppLink.tsx +++ b/site/src/modules/resources/AppLink/AppLink.tsx @@ -9,13 +9,19 @@ import { TooltipTrigger, } from "components/Tooltip/Tooltip"; import { useProxy } from "contexts/ProxyContext"; -import { CircleAlertIcon } from "lucide-react"; +import { + Building2Icon, + CircleAlertIcon, + GlobeIcon, + type LucideIcon, + SquareArrowOutUpRightIcon, + UsersIcon, +} from "lucide-react"; import { isExternalApp, needsSessionToken } from "modules/apps/apps"; import { useAppLink } from "modules/apps/useAppLink"; import { type FC, type ReactNode, useState } from "react"; import { AgentButton } from "../AgentButton"; import { BaseIcon } from "./BaseIcon"; -import { ShareIcon } from "./ShareIcon"; export const DisplayAppNameMap: Record = { port_forwarding_helper: "Ports", @@ -115,13 +121,24 @@ export const AppLink: FC = ({ } const canShare = app.sharing_level !== "owner"; + const { shareTooltip, shareIcon: ShareIcon } = canShare + ? app.external + ? { + shareTooltip: "Open external URL", + shareIcon: SquareArrowOutUpRightIcon, + } + : shareDetails[app.sharing_level] + : { + shareTooltip: null, + shareIcon: null, + }; const button = grouped ? ( {icon} {link.label} - {canShare && } + {ShareIcon && } ) : ( @@ -129,7 +146,7 @@ export const AppLink: FC = ({ {icon} {link.label} - {canShare && } + {ShareIcon && } ); @@ -146,6 +163,7 @@ export const AppLink: FC = ({ {app.tooltip} ) : null} + {shareTooltip} ); @@ -153,3 +171,23 @@ export const AppLink: FC = ({ return button; }; + +const shareDetails: { + [SharingLevel in TypesGen.WorkspaceAppSharingLevel as Exclude< + SharingLevel, + "owner" + >]: { shareTooltip: string; shareIcon: LucideIcon }; +} = { + authenticated: { + shareTooltip: "Shared with all authenticated users", + shareIcon: UsersIcon, + }, + organization: { + shareTooltip: "Shared with organization members", + shareIcon: Building2Icon, + }, + public: { + shareTooltip: "Shared publicly", + shareIcon: GlobeIcon, + }, +}; diff --git a/site/src/modules/resources/AppLink/ShareIcon.tsx b/site/src/modules/resources/AppLink/ShareIcon.tsx deleted file mode 100644 index d9d536f999..0000000000 --- a/site/src/modules/resources/AppLink/ShareIcon.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import Tooltip from "@mui/material/Tooltip"; -import type * as TypesGen from "api/typesGenerated"; -import { - Building2Icon, - GlobeIcon, - SquareArrowOutUpRightIcon, - UsersIcon, -} from "lucide-react"; - -interface ShareIconProps { - app: TypesGen.WorkspaceApp; -} - -export const ShareIcon = ({ app }: ShareIconProps) => { - if (app.external) { - return ( - - - - ); - } - if (app.sharing_level === "authenticated") { - return ( - - - - ); - } - if (app.sharing_level === "organization") { - return ( - - - - ); - } - if (app.sharing_level === "public") { - return ( - - - - ); - } - - return null; -};