feat: set icons for each type of notification (#17115)

Each notification type will have an icon to represent the context:

<img width="503" alt="Screenshot 2025-03-26 at 13 44 35"
src="https://github.com/user-attachments/assets/1187c1c0-1043-4a32-b105-a7f91b52f8ca"
/>

This depends on https://github.com/coder/coder/pull/17013
This commit is contained in:
Bruno Quaresma
2025-03-31 09:40:24 -03:00
committed by GitHub
parent 9bc727e977
commit 489641d0be
11 changed files with 154 additions and 54 deletions
+21 -21
View File
@@ -31,30 +31,30 @@ const (
var fallbackIcons = map[uuid.UUID]string{ var fallbackIcons = map[uuid.UUID]string{
// workspace related notifications // workspace related notifications
notifications.TemplateWorkspaceCreated: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceCreated: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceManuallyUpdated: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceManuallyUpdated: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceDeleted: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceDeleted: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceAutobuildFailed: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceAutobuildFailed: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceDormant: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceDormant: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceAutoUpdated: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceAutoUpdated: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceMarkedForDeletion: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceMarkedForDeletion: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceManualBuildFailed: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceManualBuildFailed: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceOutOfMemory: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceOutOfMemory: codersdk.InboxNotificationFallbackIconWorkspace,
notifications.TemplateWorkspaceOutOfDisk: codersdk.FallbackIconWorkspace, notifications.TemplateWorkspaceOutOfDisk: codersdk.InboxNotificationFallbackIconWorkspace,
// account related notifications // account related notifications
notifications.TemplateUserAccountCreated: codersdk.FallbackIconAccount, notifications.TemplateUserAccountCreated: codersdk.InboxNotificationFallbackIconAccount,
notifications.TemplateUserAccountDeleted: codersdk.FallbackIconAccount, notifications.TemplateUserAccountDeleted: codersdk.InboxNotificationFallbackIconAccount,
notifications.TemplateUserAccountSuspended: codersdk.FallbackIconAccount, notifications.TemplateUserAccountSuspended: codersdk.InboxNotificationFallbackIconAccount,
notifications.TemplateUserAccountActivated: codersdk.FallbackIconAccount, notifications.TemplateUserAccountActivated: codersdk.InboxNotificationFallbackIconAccount,
notifications.TemplateYourAccountSuspended: codersdk.FallbackIconAccount, notifications.TemplateYourAccountSuspended: codersdk.InboxNotificationFallbackIconAccount,
notifications.TemplateYourAccountActivated: codersdk.FallbackIconAccount, notifications.TemplateYourAccountActivated: codersdk.InboxNotificationFallbackIconAccount,
notifications.TemplateUserRequestedOneTimePasscode: codersdk.FallbackIconAccount, notifications.TemplateUserRequestedOneTimePasscode: codersdk.InboxNotificationFallbackIconAccount,
// template related notifications // template related notifications
notifications.TemplateTemplateDeleted: codersdk.FallbackIconTemplate, notifications.TemplateTemplateDeleted: codersdk.InboxNotificationFallbackIconTemplate,
notifications.TemplateTemplateDeprecated: codersdk.FallbackIconTemplate, notifications.TemplateTemplateDeprecated: codersdk.InboxNotificationFallbackIconTemplate,
notifications.TemplateWorkspaceBuildsFailedReport: codersdk.FallbackIconTemplate, notifications.TemplateWorkspaceBuildsFailedReport: codersdk.InboxNotificationFallbackIconTemplate,
} }
func ensureNotificationIcon(notif codersdk.InboxNotification) codersdk.InboxNotification { func ensureNotificationIcon(notif codersdk.InboxNotification) codersdk.InboxNotification {
@@ -64,7 +64,7 @@ func ensureNotificationIcon(notif codersdk.InboxNotification) codersdk.InboxNoti
fallbackIcon, ok := fallbackIcons[notif.TemplateID] fallbackIcon, ok := fallbackIcons[notif.TemplateID]
if !ok { if !ok {
fallbackIcon = codersdk.FallbackIconOther fallbackIcon = codersdk.InboxNotificationFallbackIconOther
} }
notif.Icon = fallbackIcon notif.Icon = fallbackIcon
+5 -5
View File
@@ -20,12 +20,12 @@ func TestInboxNotifications_ensureNotificationIcon(t *testing.T) {
templateID uuid.UUID templateID uuid.UUID
expectedIcon string expectedIcon string
}{ }{
{"WorkspaceCreated", "", notifications.TemplateWorkspaceCreated, codersdk.FallbackIconWorkspace}, {"WorkspaceCreated", "", notifications.TemplateWorkspaceCreated, codersdk.InboxNotificationFallbackIconWorkspace},
{"UserAccountCreated", "", notifications.TemplateUserAccountCreated, codersdk.FallbackIconAccount}, {"UserAccountCreated", "", notifications.TemplateUserAccountCreated, codersdk.InboxNotificationFallbackIconAccount},
{"TemplateDeleted", "", notifications.TemplateTemplateDeleted, codersdk.FallbackIconTemplate}, {"TemplateDeleted", "", notifications.TemplateTemplateDeleted, codersdk.InboxNotificationFallbackIconTemplate},
{"TestNotification", "", notifications.TemplateTestNotification, codersdk.FallbackIconOther}, {"TestNotification", "", notifications.TemplateTestNotification, codersdk.InboxNotificationFallbackIconOther},
{"TestExistingIcon", "https://cdn.coder.com/icon_notif.png", notifications.TemplateTemplateDeleted, "https://cdn.coder.com/icon_notif.png"}, {"TestExistingIcon", "https://cdn.coder.com/icon_notif.png", notifications.TemplateTemplateDeleted, "https://cdn.coder.com/icon_notif.png"},
{"UnknownTemplate", "", uuid.New(), codersdk.FallbackIconOther}, {"UnknownTemplate", "", uuid.New(), codersdk.InboxNotificationFallbackIconOther},
} }
for _, tt := range tests { for _, tt := range tests {
+7 -7
View File
@@ -137,7 +137,7 @@ func TestInboxNotification_Watch(t *testing.T) {
require.Equal(t, memberClient.ID, notif.Notification.UserID) require.Equal(t, memberClient.ID, notif.Notification.UserID)
// check for the fallback icon logic // check for the fallback icon logic
require.Equal(t, codersdk.FallbackIconWorkspace, notif.Notification.Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconWorkspace, notif.Notification.Icon)
}) })
t.Run("OK - change format", func(t *testing.T) { t.Run("OK - change format", func(t *testing.T) {
@@ -557,11 +557,11 @@ func TestInboxNotifications_List(t *testing.T) {
require.Len(t, notifs.Notifications, 10) require.Len(t, notifs.Notifications, 10)
require.Equal(t, "https://dev.coder.com/icon.png", notifs.Notifications[0].Icon) require.Equal(t, "https://dev.coder.com/icon.png", notifs.Notifications[0].Icon)
require.Equal(t, codersdk.FallbackIconWorkspace, notifs.Notifications[9].Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconWorkspace, notifs.Notifications[9].Icon)
require.Equal(t, codersdk.FallbackIconWorkspace, notifs.Notifications[8].Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconWorkspace, notifs.Notifications[8].Icon)
require.Equal(t, codersdk.FallbackIconAccount, notifs.Notifications[7].Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconAccount, notifs.Notifications[7].Icon)
require.Equal(t, codersdk.FallbackIconTemplate, notifs.Notifications[6].Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconTemplate, notifs.Notifications[6].Icon)
require.Equal(t, codersdk.FallbackIconOther, notifs.Notifications[4].Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconOther, notifs.Notifications[4].Icon)
}) })
t.Run("OK with template filter", func(t *testing.T) { t.Run("OK with template filter", func(t *testing.T) {
@@ -607,7 +607,7 @@ func TestInboxNotifications_List(t *testing.T) {
require.Len(t, notifs.Notifications, 5) require.Len(t, notifs.Notifications, 5)
require.Equal(t, "Notification 8", notifs.Notifications[0].Title) require.Equal(t, "Notification 8", notifs.Notifications[0].Title)
require.Equal(t, codersdk.FallbackIconWorkspace, notifs.Notifications[0].Icon) require.Equal(t, codersdk.InboxNotificationFallbackIconWorkspace, notifs.Notifications[0].Icon)
}) })
t.Run("OK with target filter", func(t *testing.T) { t.Run("OK with target filter", func(t *testing.T) {
+4 -4
View File
@@ -11,10 +11,10 @@ import (
) )
const ( const (
FallbackIconWorkspace = "DEFAULT_ICON_WORKSPACE" InboxNotificationFallbackIconWorkspace = "DEFAULT_ICON_WORKSPACE"
FallbackIconAccount = "DEFAULT_ICON_ACCOUNT" InboxNotificationFallbackIconAccount = "DEFAULT_ICON_ACCOUNT"
FallbackIconTemplate = "DEFAULT_ICON_TEMPLATE" InboxNotificationFallbackIconTemplate = "DEFAULT_ICON_TEMPLATE"
FallbackIconOther = "DEFAULT_ICON_OTHER" InboxNotificationFallbackIconOther = "DEFAULT_ICON_OTHER"
) )
type InboxNotification struct { type InboxNotification struct {
+12 -12
View File
@@ -839,18 +839,6 @@ export interface ExternalAuthUser {
readonly name: string; readonly name: string;
} }
// From codersdk/inboxnotification.go
export const FallbackIconAccount = "DEFAULT_ICON_ACCOUNT";
// From codersdk/inboxnotification.go
export const FallbackIconOther = "DEFAULT_ICON_OTHER";
// From codersdk/inboxnotification.go
export const FallbackIconTemplate = "DEFAULT_ICON_TEMPLATE";
// From codersdk/inboxnotification.go
export const FallbackIconWorkspace = "DEFAULT_ICON_WORKSPACE";
// From codersdk/deployment.go // From codersdk/deployment.go
export interface Feature { export interface Feature {
readonly entitlement: Entitlement; readonly entitlement: Entitlement;
@@ -1124,6 +1112,18 @@ export interface InboxNotificationAction {
readonly url: string; readonly url: string;
} }
// From codersdk/inboxnotification.go
export const InboxNotificationFallbackIconAccount = "DEFAULT_ICON_ACCOUNT";
// From codersdk/inboxnotification.go
export const InboxNotificationFallbackIconOther = "DEFAULT_ICON_OTHER";
// From codersdk/inboxnotification.go
export const InboxNotificationFallbackIconTemplate = "DEFAULT_ICON_TEMPLATE";
// From codersdk/inboxnotification.go
export const InboxNotificationFallbackIconWorkspace = "DEFAULT_ICON_WORKSPACE";
// From codersdk/insights.go // From codersdk/insights.go
export type InsightsReportInterval = "day" | "week"; export type InsightsReportInterval = "day" | "week";
+1 -1
View File
@@ -28,7 +28,7 @@ const avatarVariants = cva(
}, },
variant: { variant: {
default: null, default: null,
icon: null, icon: "[&_svg]:size-full",
}, },
}, },
defaultVariants: { defaultVariants: {
@@ -0,0 +1,46 @@
import type { Meta, StoryObj } from "@storybook/react";
import { InboxAvatar } from "./InboxAvatar";
const meta: Meta<typeof InboxAvatar> = {
title: "modules/notifications/NotificationsInbox/InboxAvatar",
component: InboxAvatar,
};
export default meta;
type Story = StoryObj<typeof InboxAvatar>;
export const Custom: Story = {
args: {
icon: "/icon/git.svg",
},
};
export const EmptyIcon: Story = {
args: {
icon: "",
},
};
export const FallbackWorkspace: Story = {
args: {
icon: "DEFAULT_ICON_WORKSPACE",
},
};
export const FallbackAccount: Story = {
args: {
icon: "DEFAULT_ICON_ACCOUNT",
},
};
export const FallbackTemplate: Story = {
args: {
icon: "DEFAULT_ICON_TEMPLATE",
},
};
export const FallbackOther: Story = {
args: {
icon: "DEFAULT_ICON_OTHER",
},
};
@@ -0,0 +1,54 @@
import {
InboxNotificationFallbackIconAccount,
InboxNotificationFallbackIconOther,
InboxNotificationFallbackIconTemplate,
InboxNotificationFallbackIconWorkspace,
} from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import {
InfoIcon,
LaptopIcon,
LayoutTemplateIcon,
UserIcon,
} from "lucide-react";
import type { FC } from "react";
import type React from "react";
const InboxNotificationFallbackIcons = [
InboxNotificationFallbackIconAccount,
InboxNotificationFallbackIconWorkspace,
InboxNotificationFallbackIconTemplate,
InboxNotificationFallbackIconOther,
] as const;
type InboxNotificationFallbackIcon =
(typeof InboxNotificationFallbackIcons)[number];
const fallbackIcons: Record<InboxNotificationFallbackIcon, React.ReactNode> = {
DEFAULT_ICON_WORKSPACE: <LaptopIcon />,
DEFAULT_ICON_ACCOUNT: <UserIcon />,
DEFAULT_ICON_TEMPLATE: <LayoutTemplateIcon />,
DEFAULT_ICON_OTHER: <InfoIcon />,
};
type InboxAvatarProps = {
icon: string;
};
export const InboxAvatar: FC<InboxAvatarProps> = ({ icon }) => {
if (icon === "") {
return <Avatar variant="icon">{fallbackIcons.DEFAULT_ICON_OTHER}</Avatar>;
}
if (isInboxNotificationFallbackIcon(icon)) {
return <Avatar variant="icon">{fallbackIcons[icon]}</Avatar>;
}
return <Avatar variant="icon" src={icon} />;
};
function isInboxNotificationFallbackIcon(
icon: string,
): icon is InboxNotificationFallbackIcon {
return (InboxNotificationFallbackIcons as readonly string[]).includes(icon);
}
@@ -61,6 +61,7 @@ export const Markdown: Story = {
url: "https://dev.coder.com/workspaces?filter=template%3Acoder-with-ai", url: "https://dev.coder.com/workspaces?filter=template%3Acoder-with-ai",
}, },
], ],
icon: "DEFAULT_ICON_TEMPLATE",
}, },
}, },
}; };
@@ -1,13 +1,12 @@
import type { InboxNotification } from "api/typesGenerated"; import type { InboxNotification } from "api/typesGenerated";
import { Avatar } from "components/Avatar/Avatar";
import { Button } from "components/Button/Button"; import { Button } from "components/Button/Button";
import { Link } from "components/Link/Link"; import { Link } from "components/Link/Link";
import { SquareCheckBig } from "lucide-react"; import { SquareCheckBig } from "lucide-react";
import type { FC } from "react"; import type { FC } from "react";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import { Link as RouterLink } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom";
import { cn } from "utils/cn";
import { relativeTime } from "utils/time"; import { relativeTime } from "utils/time";
import { InboxAvatar } from "./InboxAvatar";
type InboxItemProps = { type InboxItemProps = {
notification: InboxNotification; notification: InboxNotification;
@@ -25,7 +24,7 @@ export const InboxItem: FC<InboxItemProps> = ({
tabIndex={-1} tabIndex={-1}
> >
<div className="flex-shrink-0"> <div className="flex-shrink-0">
<Avatar fallback="AR" /> <InboxAvatar icon={notification.icon} />
</div> </div>
<div className="flex flex-col gap-3 flex-1"> <div className="flex flex-col gap-3 flex-1">
+1 -1
View File
@@ -4261,7 +4261,7 @@ export const MockNotification: TypesGen.InboxNotification = {
template_id: MockTemplate.id, template_id: MockTemplate.id,
targets: [], targets: [],
title: "User account created", title: "User account created",
icon: "user", icon: "DEFAULT_ICON_ACCOUNT",
}; };
export const MockNotifications: TypesGen.InboxNotification[] = [ export const MockNotifications: TypesGen.InboxNotification[] = [