From 680e28bdce0db5dcba4b64459b0d6c4aa3d4ba77 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Wed, 2 Oct 2024 09:46:25 -0500 Subject: [PATCH] fix: display workspace avatars correctly when URLs fail to load (#14814) ## Changes made - Updated custom avatar components to favor background color by default - Updated `AvatarData` component to let you manually specify the source of the text used when images fail to load, and updated the orgs breadcrumb segment to use it - Added some logic for handling emoji images better --- site/src/components/AvatarData/AvatarData.tsx | 17 ++++- .../ManagementSettingsPage/SidebarView.tsx | 70 +++++++++---------- .../pages/WorkspacePage/WorkspaceTopbar.tsx | 17 ++++- site/src/theme/externalImages.ts | 6 +- site/src/utils/appearance.ts | 15 ++++ 5 files changed, 83 insertions(+), 42 deletions(-) diff --git a/site/src/components/AvatarData/AvatarData.tsx b/site/src/components/AvatarData/AvatarData.tsx index e1598feb29..eb9fa81d49 100644 --- a/site/src/components/AvatarData/AvatarData.tsx +++ b/site/src/components/AvatarData/AvatarData.tsx @@ -8,18 +8,31 @@ export interface AvatarDataProps { subtitle?: ReactNode; src?: string; avatar?: React.ReactNode; + + /** + * Lets you specify the character(s) displayed in an avatar when an image is + * unavailable (like when the network request fails). + * + * If not specified, the component will try to parse the first character + * from the title prop if it is a string. + */ + imgFallbackText?: string; } export const AvatarData: FC = ({ title, subtitle, src, + imgFallbackText, avatar, }) => { const theme = useTheme(); - if (!avatar) { - avatar = {title}; + avatar = ( + + {(typeof title === "string" ? title : imgFallbackText) || "-"} + + ); } return ( diff --git a/site/src/pages/ManagementSettingsPage/SidebarView.tsx b/site/src/pages/ManagementSettingsPage/SidebarView.tsx index 37db583f23..76874490e9 100644 --- a/site/src/pages/ManagementSettingsPage/SidebarView.tsx +++ b/site/src/pages/ManagementSettingsPage/SidebarView.tsx @@ -387,49 +387,49 @@ const styles = { const classNames = { link: (css, theme) => css` - color: inherit; - display: block; - font-size: 14px; - text-decoration: none; - padding: 10px 12px 10px 16px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - position: relative; + color: inherit; + display: block; + font-size: 14px; + text-decoration: none; + padding: 10px 12px 10px 16px; + border-radius: 4px; + transition: background-color 0.15s ease-in-out; + position: relative; - &:hover { - background-color: ${theme.palette.action.hover}; - } + &:hover { + background-color: ${theme.palette.action.hover}; + } - border-left: 3px solid transparent; - `, + border-left: 3px solid transparent; + `, activeLink: (css, theme) => css` - border-left-color: ${theme.palette.primary.main}; - border-top-left-radius: 0; - border-bottom-left-radius: 0; - `, + border-left-color: ${theme.palette.primary.main}; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + `, subLink: (css, theme) => css` - color: ${theme.palette.text.secondary}; - text-decoration: none; + color: ${theme.palette.text.secondary}; + text-decoration: none; - display: block; - font-size: 13px; - margin-left: 44px; - padding: 4px 12px; - border-radius: 4px; - transition: background-color 0.15s ease-in-out; - margin-bottom: 1px; - position: relative; + display: block; + font-size: 13px; + margin-left: 44px; + padding: 4px 12px; + border-radius: 4px; + transition: background-color 0.15s ease-in-out; + margin-bottom: 1px; + position: relative; - &:hover { - color: ${theme.palette.text.primary}; - background-color: ${theme.palette.action.hover}; - } - `, + &:hover { + color: ${theme.palette.text.primary}; + background-color: ${theme.palette.action.hover}; + } + `, activeSubLink: (css, theme) => css` - color: ${theme.palette.text.primary}; - font-weight: 600; - `, + color: ${theme.palette.text.primary}; + font-weight: 600; + `, } satisfies Record; diff --git a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx index 176b68d372..e3be26462c 100644 --- a/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceTopbar.tsx @@ -25,6 +25,7 @@ import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/Wo import type { FC } from "react"; import { useQuery } from "react-query"; import { Link as RouterLink } from "react-router-dom"; +import { isEmojiUrl } from "utils/appearance"; import { displayDormantDeletion } from "utils/dormant"; import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions"; import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications"; @@ -344,9 +345,15 @@ const OrganizationBreadcrumb: FC = ({ subtitle="Organization" avatar={ orgIconUrl && ( - + ) } + imgFallbackText={orgName} /> @@ -405,8 +412,14 @@ const WorkspaceBreadcrumb: FC = ({ } avatar={ - + } + imgFallbackText={templateVersionDisplayName} /> diff --git a/site/src/theme/externalImages.ts b/site/src/theme/externalImages.ts index 834e8fb7d8..612d7ab2c7 100644 --- a/site/src/theme/externalImages.ts +++ b/site/src/theme/externalImages.ts @@ -75,8 +75,8 @@ const parseInvertFilterParameters = ( let extraStyles: CSSObject | undefined; - const brightness = params.get("brightness"); - if (multiplier.test(brightness!)) { + const brightness = params.get("brightness") ?? ""; + if (multiplier.test(brightness)) { let filter = baseStyles.filter ?? ""; filter += ` brightness(${brightness})`; extraStyles = { ...extraStyles, filter }; @@ -131,7 +131,7 @@ export function getExternalImageStylesFromUrl( ) { return parseImageParameters( modes, - defaultParametersForBuiltinIcons.get(url.pathname)!, + defaultParametersForBuiltinIcons.get(url.pathname) as string, ); } diff --git a/site/src/utils/appearance.ts b/site/src/utils/appearance.ts index d025ec15df..a7c2fb142b 100644 --- a/site/src/utils/appearance.ts +++ b/site/src/utils/appearance.ts @@ -14,3 +14,18 @@ export const getLogoURL = (): string => { ?.getAttribute("content"); return c && !c.startsWith("{{ .") ? c : ""; }; + +/** + * Exposes an easy way to determine if a given URL is for an emoji hosted on + * the Coder deployment. + * + * Helps when you need to style emojis differently (i.e., not adding rounding to + * the container so that the emoji doesn't get cut off). + */ +export function isEmojiUrl(url: string | undefined): boolean { + if (!url) { + return false; + } + + return url.startsWith("/emojis/"); +}