fix: add fallback icons for notifications (#17013)

Related: https://github.com/coder/internal/issues/522
This commit is contained in:
Vincent Vielle
2025-03-28 12:21:48 +01:00
committed by GitHub
parent ca414b031a
commit 148dae1e9f
6 changed files with 187 additions and 4 deletions
+47 -2
View File
@@ -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")
}
@@ -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)
})
}
}
+69 -2
View File
@@ -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) {
+1
View File
@@ -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 (
+7
View File
@@ -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"`
+12
View File
@@ -839,6 +839,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;