From 4cfcc75d98e1b733bc43df9e86611e9620748a7c Mon Sep 17 00:00:00 2001 From: Vincent Vielle Date: Fri, 28 Mar 2025 18:29:12 +0000 Subject: [PATCH] fix: add fallback icons for notifications (#17013) Related: https://github.com/coder/internal/issues/522 --- coderd/inboxnotifications.go | 49 ++++++++++++++- coderd/inboxnotifications_internal_test.go | 51 ++++++++++++++++ coderd/inboxnotifications_test.go | 71 +++++++++++++++++++++- coderd/notifications/events.go | 1 + codersdk/inboxnotification.go | 7 +++ site/src/api/typesGenerated.ts | 12 ++++ 6 files changed, 187 insertions(+), 4 deletions(-) create mode 100644 coderd/inboxnotifications_internal_test.go diff --git a/coderd/inboxnotifications.go b/coderd/inboxnotifications.go index 37ae8905c7d24..df6ebe9d25aaf 100644 --- a/coderd/inboxnotifications.go +++ b/coderd/inboxnotifications.go @@ -16,6 +16,7 @@ import ( "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/httpapi" "github.com/coder/coder/v2/coderd/httpmw" + "github.com/coder/coder/v2/coderd/notifications" "github.com/coder/coder/v2/coderd/pubsub" markdown "github.com/coder/coder/v2/coderd/render" "github.com/coder/coder/v2/codersdk" @@ -28,9 +29,51 @@ const ( notificationFormatPlaintext = "plaintext" ) +var fallbackIcons = map[uuid.UUID]string{ + // workspace related notifications + notifications.TemplateWorkspaceCreated: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceManuallyUpdated: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceDeleted: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceAutobuildFailed: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceDormant: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceAutoUpdated: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceMarkedForDeletion: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceManualBuildFailed: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceOutOfMemory: codersdk.FallbackIconWorkspace, + notifications.TemplateWorkspaceOutOfDisk: codersdk.FallbackIconWorkspace, + + // account related notifications + notifications.TemplateUserAccountCreated: codersdk.FallbackIconAccount, + notifications.TemplateUserAccountDeleted: codersdk.FallbackIconAccount, + notifications.TemplateUserAccountSuspended: codersdk.FallbackIconAccount, + notifications.TemplateUserAccountActivated: codersdk.FallbackIconAccount, + notifications.TemplateYourAccountSuspended: codersdk.FallbackIconAccount, + notifications.TemplateYourAccountActivated: codersdk.FallbackIconAccount, + notifications.TemplateUserRequestedOneTimePasscode: codersdk.FallbackIconAccount, + + // template related notifications + notifications.TemplateTemplateDeleted: codersdk.FallbackIconTemplate, + notifications.TemplateTemplateDeprecated: codersdk.FallbackIconTemplate, + notifications.TemplateWorkspaceBuildsFailedReport: codersdk.FallbackIconTemplate, +} + +func ensureNotificationIcon(notif codersdk.InboxNotification) codersdk.InboxNotification { + if notif.Icon != "" { + return notif + } + + fallbackIcon, ok := fallbackIcons[notif.TemplateID] + if !ok { + fallbackIcon = codersdk.FallbackIconOther + } + + notif.Icon = fallbackIcon + return notif +} + // convertInboxNotificationResponse works as a util function to transform a database.InboxNotification to codersdk.InboxNotification func convertInboxNotificationResponse(ctx context.Context, logger slog.Logger, notif database.InboxNotification) codersdk.InboxNotification { - return codersdk.InboxNotification{ + convertedNotif := codersdk.InboxNotification{ ID: notif.ID, UserID: notif.UserID, TemplateID: notif.TemplateID, @@ -54,6 +97,8 @@ func convertInboxNotificationResponse(ctx context.Context, logger slog.Logger, n }(), CreatedAt: notif.CreatedAt, } + + return ensureNotificationIcon(convertedNotif) } // watchInboxNotifications watches for new inbox notifications and sends them to the client. @@ -147,7 +192,7 @@ func (api *API) watchInboxNotifications(rw http.ResponseWriter, r *http.Request) // keep a safe guard in case of latency to push notifications through websocket select { - case notificationCh <- payload.InboxNotification: + case notificationCh <- ensureNotificationIcon(payload.InboxNotification): default: api.Logger.Error(ctx, "failed to push consumed notification into websocket handler, check latency") } diff --git a/coderd/inboxnotifications_internal_test.go b/coderd/inboxnotifications_internal_test.go new file mode 100644 index 0000000000000..6dd36fcffe145 --- /dev/null +++ b/coderd/inboxnotifications_internal_test.go @@ -0,0 +1,51 @@ +package coderd + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/coderd/notifications" + "github.com/coder/coder/v2/codersdk" +) + +func TestInboxNotifications_ensureNotificationIcon(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + icon string + templateID uuid.UUID + expectedIcon string + }{ + {"WorkspaceCreated", "", notifications.TemplateWorkspaceCreated, codersdk.FallbackIconWorkspace}, + {"UserAccountCreated", "", notifications.TemplateUserAccountCreated, codersdk.FallbackIconAccount}, + {"TemplateDeleted", "", notifications.TemplateTemplateDeleted, codersdk.FallbackIconTemplate}, + {"TestNotification", "", notifications.TemplateTestNotification, codersdk.FallbackIconOther}, + {"TestExistingIcon", "https://cdn.coder.com/icon_notif.png", notifications.TemplateTemplateDeleted, "https://cdn.coder.com/icon_notif.png"}, + {"UnknownTemplate", "", uuid.New(), codersdk.FallbackIconOther}, + } + + for _, tt := range tests { + tt := tt + + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + notif := codersdk.InboxNotification{ + ID: uuid.New(), + UserID: uuid.New(), + TemplateID: tt.templateID, + Title: "notification title", + Content: "notification content", + Icon: tt.icon, + CreatedAt: time.Now(), + } + + notif = ensureNotificationIcon(notif) + require.Equal(t, tt.expectedIcon, notif.Icon) + }) + } +} diff --git a/coderd/inboxnotifications_test.go b/coderd/inboxnotifications_test.go index ed0696195cb60..d9ee0ee936a94 100644 --- a/coderd/inboxnotifications_test.go +++ b/coderd/inboxnotifications_test.go @@ -135,6 +135,9 @@ func TestInboxNotification_Watch(t *testing.T) { require.Equal(t, 1, notif.UnreadCount) require.Equal(t, memberClient.ID, notif.Notification.UserID) + + // check for the fallback icon logic + require.Equal(t, codersdk.FallbackIconWorkspace, notif.Notification.Icon) }) t.Run("OK - change format", func(t *testing.T) { @@ -474,8 +477,9 @@ func TestInboxNotifications_List(t *testing.T) { TemplateID: notifications.TemplateWorkspaceOutOfMemory, Title: fmt.Sprintf("Notification %d", i), Actions: json.RawMessage("[]"), - Content: fmt.Sprintf("Content of the notif %d", i), - CreatedAt: dbtime.Now(), + + Content: fmt.Sprintf("Content of the notif %d", i), + CreatedAt: dbtime.Now(), }) } @@ -498,6 +502,68 @@ func TestInboxNotifications_List(t *testing.T) { require.Equal(t, "Notification 14", notifs.Notifications[0].Title) }) + t.Run("OK check icons", func(t *testing.T) { + t.Parallel() + + client, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{}) + firstUser := coderdtest.CreateFirstUser(t, client) + client, member := coderdtest.CreateAnotherUser(t, client, firstUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + notifs, err := client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 0, notifs.UnreadCount) + require.Empty(t, notifs.Notifications) + + for i := range 10 { + dbgen.NotificationInbox(t, api.Database, database.InsertInboxNotificationParams{ + ID: uuid.New(), + UserID: member.ID, + TemplateID: func() uuid.UUID { + switch i { + case 0: + return notifications.TemplateWorkspaceCreated + case 1: + return notifications.TemplateWorkspaceMarkedForDeletion + case 2: + return notifications.TemplateUserAccountActivated + case 3: + return notifications.TemplateTemplateDeprecated + default: + return notifications.TemplateTestNotification + } + }(), + Title: fmt.Sprintf("Notification %d", i), + Actions: json.RawMessage("[]"), + Icon: func() string { + if i == 9 { + return "https://dev.coder.com/icon.png" + } + + return "" + }(), + Content: fmt.Sprintf("Content of the notif %d", i), + CreatedAt: dbtime.Now(), + }) + } + + notifs, err = client.ListInboxNotifications(ctx, codersdk.ListInboxNotificationsRequest{}) + require.NoError(t, err) + require.NotNil(t, notifs) + require.Equal(t, 10, notifs.UnreadCount) + require.Len(t, notifs.Notifications, 10) + + 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.FallbackIconWorkspace, notifs.Notifications[8].Icon) + require.Equal(t, codersdk.FallbackIconAccount, notifs.Notifications[7].Icon) + require.Equal(t, codersdk.FallbackIconTemplate, notifs.Notifications[6].Icon) + require.Equal(t, codersdk.FallbackIconOther, notifs.Notifications[4].Icon) + }) + t.Run("OK with template filter", func(t *testing.T) { t.Parallel() @@ -541,6 +607,7 @@ func TestInboxNotifications_List(t *testing.T) { require.Len(t, notifs.Notifications, 5) require.Equal(t, "Notification 8", notifs.Notifications[0].Title) + require.Equal(t, codersdk.FallbackIconWorkspace, notifs.Notifications[0].Icon) }) t.Run("OK with target filter", func(t *testing.T) { diff --git a/coderd/notifications/events.go b/coderd/notifications/events.go index 3399da96cf28a..2f45205bf33ec 100644 --- a/coderd/notifications/events.go +++ b/coderd/notifications/events.go @@ -4,6 +4,7 @@ import "github.com/google/uuid" // These vars are mapped to UUIDs in the notification_templates table. // TODO: autogenerate these: https://github.com/coder/team-coconut/issues/36 +// TODO(defelmnq): add fallback icon to coderd/inboxnofication.go when adding a new template // Workspace-related events. var ( diff --git a/codersdk/inboxnotification.go b/codersdk/inboxnotification.go index 056584d6cf359..ba68351c39bfe 100644 --- a/codersdk/inboxnotification.go +++ b/codersdk/inboxnotification.go @@ -10,6 +10,13 @@ import ( "github.com/google/uuid" ) +const ( + FallbackIconWorkspace = "DEFAULT_ICON_WORKSPACE" + FallbackIconAccount = "DEFAULT_ICON_ACCOUNT" + FallbackIconTemplate = "DEFAULT_ICON_TEMPLATE" + FallbackIconOther = "DEFAULT_ICON_OTHER" +) + type InboxNotification struct { ID uuid.UUID `json:"id" format:"uuid"` UserID uuid.UUID `json:"user_id" format:"uuid"` diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index fe966d7b5ddd2..87a6836a7d26f 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -832,6 +832,18 @@ export interface ExternalAuthUser { 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 export interface Feature { readonly entitlement: Entitlement;