Skip to content

Commit 680e28b

Browse files
authored
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
1 parent 0aa84b1 commit 680e28b

File tree

5 files changed

+87
-46
lines changed

5 files changed

+87
-46
lines changed

site/src/components/AvatarData/AvatarData.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,31 @@ export interface AvatarDataProps {
88
subtitle?: ReactNode;
99
src?: string;
1010
avatar?: React.ReactNode;
11+
12+
/**
13+
* Lets you specify the character(s) displayed in an avatar when an image is
14+
* unavailable (like when the network request fails).
15+
*
16+
* If not specified, the component will try to parse the first character
17+
* from the title prop if it is a string.
18+
*/
19+
imgFallbackText?: string;
1120
}
1221

1322
export const AvatarData: FC<AvatarDataProps> = ({
1423
title,
1524
subtitle,
1625
src,
26+
imgFallbackText,
1727
avatar,
1828
}) => {
1929
const theme = useTheme();
20-
2130
if (!avatar) {
22-
avatar = <Avatar src={src}>{title}</Avatar>;
31+
avatar = (
32+
<Avatar background src={src}>
33+
{(typeof title === "string" ? title : imgFallbackText) || "-"}
34+
</Avatar>
35+
);
2336
}
2437

2538
return (

site/src/pages/ManagementSettingsPage/SidebarView.tsx

Lines changed: 39 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -387,49 +387,49 @@ const styles = {
387387

388388
const classNames = {
389389
link: (css, theme) => css`
390-
color: inherit;
391-
display: block;
392-
font-size: 14px;
393-
text-decoration: none;
394-
padding: 10px 12px 10px 16px;
395-
border-radius: 4px;
396-
transition: background-color 0.15s ease-in-out;
397-
position: relative;
398-
399-
&:hover {
400-
background-color: ${theme.palette.action.hover};
401-
}
402-
403-
border-left: 3px solid transparent;
404-
`,
390+
color: inherit;
391+
display: block;
392+
font-size: 14px;
393+
text-decoration: none;
394+
padding: 10px 12px 10px 16px;
395+
border-radius: 4px;
396+
transition: background-color 0.15s ease-in-out;
397+
position: relative;
398+
399+
&:hover {
400+
background-color: ${theme.palette.action.hover};
401+
}
402+
403+
border-left: 3px solid transparent;
404+
`,
405405

406406
activeLink: (css, theme) => css`
407-
border-left-color: ${theme.palette.primary.main};
408-
border-top-left-radius: 0;
409-
border-bottom-left-radius: 0;
410-
`,
407+
border-left-color: ${theme.palette.primary.main};
408+
border-top-left-radius: 0;
409+
border-bottom-left-radius: 0;
410+
`,
411411

412412
subLink: (css, theme) => css`
413-
color: ${theme.palette.text.secondary};
414-
text-decoration: none;
415-
416-
display: block;
417-
font-size: 13px;
418-
margin-left: 44px;
419-
padding: 4px 12px;
420-
border-radius: 4px;
421-
transition: background-color 0.15s ease-in-out;
422-
margin-bottom: 1px;
423-
position: relative;
424-
425-
&:hover {
426-
color: ${theme.palette.text.primary};
427-
background-color: ${theme.palette.action.hover};
428-
}
429-
`,
413+
color: ${theme.palette.text.secondary};
414+
text-decoration: none;
415+
416+
display: block;
417+
font-size: 13px;
418+
margin-left: 44px;
419+
padding: 4px 12px;
420+
border-radius: 4px;
421+
transition: background-color 0.15s ease-in-out;
422+
margin-bottom: 1px;
423+
position: relative;
424+
425+
&:hover {
426+
color: ${theme.palette.text.primary};
427+
background-color: ${theme.palette.action.hover};
428+
}
429+
`,
430430

431431
activeSubLink: (css, theme) => css`
432-
color: ${theme.palette.text.primary};
433-
font-weight: 600;
434-
`,
432+
color: ${theme.palette.text.primary};
433+
font-weight: 600;
434+
`,
435435
} satisfies Record<string, ClassName>;

site/src/pages/WorkspacePage/WorkspaceTopbar.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { WorkspaceStatusBadge } from "modules/workspaces/WorkspaceStatusBadge/Wo
2525
import type { FC } from "react";
2626
import { useQuery } from "react-query";
2727
import { Link as RouterLink } from "react-router-dom";
28+
import { isEmojiUrl } from "utils/appearance";
2829
import { displayDormantDeletion } from "utils/dormant";
2930
import { WorkspaceActions } from "./WorkspaceActions/WorkspaceActions";
3031
import { WorkspaceNotifications } from "./WorkspaceNotifications/WorkspaceNotifications";
@@ -344,9 +345,15 @@ const OrganizationBreadcrumb: FC<OrganizationBreadcrumbProps> = ({
344345
subtitle="Organization"
345346
avatar={
346347
orgIconUrl && (
347-
<ExternalAvatar src={orgIconUrl} variant="square" fitImage />
348+
<ExternalAvatar
349+
src={orgIconUrl}
350+
title={orgName}
351+
variant={isEmojiUrl(orgIconUrl) ? "square" : "circular"}
352+
fitImage
353+
/>
348354
)
349355
}
356+
imgFallbackText={orgName}
350357
/>
351358
</HelpTooltipContent>
352359
</Popover>
@@ -405,8 +412,14 @@ const WorkspaceBreadcrumb: FC<WorkspaceBreadcrumbProps> = ({
405412
</Link>
406413
}
407414
avatar={
408-
<ExternalAvatar src={templateIconUrl} variant="square" fitImage />
415+
<ExternalAvatar
416+
src={templateIconUrl}
417+
title={workspaceName}
418+
variant={isEmojiUrl(templateIconUrl) ? "square" : "circular"}
419+
fitImage
420+
/>
409421
}
422+
imgFallbackText={templateVersionDisplayName}
410423
/>
411424
</HelpTooltipContent>
412425
</Popover>

site/src/theme/externalImages.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ const parseInvertFilterParameters = (
7575

7676
let extraStyles: CSSObject | undefined;
7777

78-
const brightness = params.get("brightness");
79-
if (multiplier.test(brightness!)) {
78+
const brightness = params.get("brightness") ?? "";
79+
if (multiplier.test(brightness)) {
8080
let filter = baseStyles.filter ?? "";
8181
filter += ` brightness(${brightness})`;
8282
extraStyles = { ...extraStyles, filter };
@@ -131,7 +131,7 @@ export function getExternalImageStylesFromUrl(
131131
) {
132132
return parseImageParameters(
133133
modes,
134-
defaultParametersForBuiltinIcons.get(url.pathname)!,
134+
defaultParametersForBuiltinIcons.get(url.pathname) as string,
135135
);
136136
}
137137

site/src/utils/appearance.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,18 @@ export const getLogoURL = (): string => {
1414
?.getAttribute("content");
1515
return c && !c.startsWith("{{ .") ? c : "";
1616
};
17+
18+
/**
19+
* Exposes an easy way to determine if a given URL is for an emoji hosted on
20+
* the Coder deployment.
21+
*
22+
* Helps when you need to style emojis differently (i.e., not adding rounding to
23+
* the container so that the emoji doesn't get cut off).
24+
*/
25+
export function isEmojiUrl(url: string | undefined): boolean {
26+
if (!url) {
27+
return false;
28+
}
29+
30+
return url.startsWith("/emojis/");
31+
}

0 commit comments

Comments
 (0)