Skip to content

Commit ef9ed21

Browse files
authored
remove the notifications (#197)
* remove the notifications * removed notification and the WS connection * restore notification and notificationSocket
1 parent e4b624c commit ef9ed21

21 files changed

Lines changed: 71 additions & 416 deletions

frontend/src/api/notifications.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// This module is currently unused but intentionally retained for future notification work.
2+
// Do not remove it.
3+
14
import { apiFetch } from "./client";
25

36
export type Notification = {

frontend/src/api/notificationsSocket.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// This module is currently unused but intentionally retained for future notification work.
2+
// Do not remove it.
3+
14
import type { Notification } from "./notifications";
25
import { createWebSocketClient } from "./ws";
36

frontend/src/app/index.css

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
--accent-foreground: oklch(32.95% 0.0209 254.12);
3636

3737
--destructive: oklch(95.06% 0.0247 29.93);
38-
--notification-badge: #d00000;
3938
--sign-out-foreground: #d2161c;
4039

4140
--border: oklch(78.06% 0.0056 286.27);
@@ -119,7 +118,6 @@
119118
--accent-foreground: oklch(95.04% 0.0042 236.5);
120119

121120
--destructive: oklch(33.1% 0.1332 27.42);
122-
--notification-badge: #e00000;
123121
--sign-out-foreground: #ff6b6b;
124122

125123
--border: oklch(47.01% 0.0112 285.96);
@@ -192,7 +190,6 @@
192190
--color-input: var(--input);
193191
--color-border: var(--border);
194192
--color-destructive: var(--destructive);
195-
--color-notification-badge: var(--notification-badge);
196193
--color-sign-out-foreground: var(--sign-out-foreground);
197194

198195
--color-accent-foreground: var(--accent-foreground);

frontend/src/app/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { getAppConfig } from "./config";
1111
export default function App() {
1212
const { themeMode, isDarkMode, setThemeMode } = useTheme();
1313
const { user } = useUser();
14-
const { services, notifications, onNotificationsViewed, onTogglePin } = useLaunchpadData(user);
14+
const { services, onTogglePin } = useLaunchpadData(user);
1515

1616
const config = getAppConfig();
1717

@@ -24,8 +24,6 @@ export default function App() {
2424
onThemeChange={setThemeMode}
2525
user={user}
2626
onSignOut={() => signOut()}
27-
notifications={notifications}
28-
onNotificationsViewed={onNotificationsViewed}
2927
logoSrc={config?.logoUrl || undefined}
3028
logoSrcDark={config?.logoUrlDark || undefined}
3129
/>

frontend/src/components/Header.tsx

Lines changed: 1 addition & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Menu as MenuPrimitive } from "@base-ui/react/menu";
2-
import { Bell, ChevronDown, LogOut, Monitor, Moon, Sun } from "lucide-react";
2+
import { ChevronDown, LogOut, Monitor, Moon, Sun } from "lucide-react";
33
import type { ReactNode } from "react";
44
import builtInLogoDark from "../assets/nebari-logo_dark.svg";
55
import builtInLogoLight from "../assets/nebari-logo_light.svg";
@@ -17,15 +17,6 @@ import {
1717
} from "./ui/dropdown-menu";
1818
import { MenuBarActions, MenuBarBrand, NavigationMenu } from "./ui/navigation-menu";
1919

20-
type Notification = {
21-
id: string;
22-
title: string;
23-
message: string;
24-
createdAt: string;
25-
image?: string;
26-
read?: boolean;
27-
};
28-
2920
type User = {
3021
name?: string;
3122
email?: string;
@@ -40,8 +31,6 @@ export type HeaderProps = {
4031
user?: User | null;
4132
onSignIn?: () => void;
4233
onSignOut?: () => void;
43-
notifications?: Notification[];
44-
onNotificationsViewed?: (ids: string[]) => void | Promise<void>;
4534
logoSrc?: string;
4635
logoSrcDark?: string;
4736
};
@@ -55,14 +44,10 @@ export function Header(props: HeaderProps): ReactNode {
5544
user,
5645
onSignIn,
5746
onSignOut,
58-
notifications = [],
59-
onNotificationsViewed,
6047
logoSrc: logoSrcProp,
6148
logoSrcDark: logoSrcDarkProp,
6249
} = props;
6350

64-
const unreadNotifications = notifications.filter((item) => !item.read);
65-
const unreadCount = unreadNotifications.length;
6651
// Dark mode prefers the dark logo, then the light/general custom logo, then
6752
// the built-in dark logo. Light mode uses the custom logo or the built-in.
6853
const logoSrc = isDarkMode
@@ -71,82 +56,13 @@ export function Header(props: HeaderProps): ReactNode {
7156

7257
const initials = getUserInitials(user?.name, user?.email);
7358

74-
const handleNotificationsOpen = () => {
75-
if (!onNotificationsViewed) return;
76-
77-
const unreadIds = unreadNotifications.map((item) => item.id);
78-
if (unreadIds.length > 0) {
79-
void onNotificationsViewed(unreadIds);
80-
}
81-
};
82-
8359
return (
8460
<NavigationMenu className="h-14 justify-between border-header-border bg-header-background pl-4 text-header-foreground">
8561
<MenuBarBrand href={homeHref} aria-label="Go to homepage">
8662
<img src={logoSrc} alt="Nebari" className="h-8 w-auto" />
8763
</MenuBarBrand>
8864

8965
<MenuBarActions className="gap-2">
90-
<DropdownMenu modal={false} onOpenChange={(open) => open && handleNotificationsOpen()}>
91-
<DropdownMenuTrigger
92-
variant="ghost"
93-
className="relative w-8 px-0 hover:bg-header-action-hover hover:no-underline focus-visible:ring-offset-0 active:bg-header-action-hover"
94-
aria-label="Notifications"
95-
>
96-
<Bell />
97-
{unreadCount > 0 ? (
98-
<span className="absolute -right-0.5 -top-0.5 z-10 flex h-4 min-w-4 items-center justify-center rounded-full bg-notification-badge px-1 pt-px text-[9px] font-semibold leading-none text-white tabular-nums">
99-
{unreadCount}
100-
</span>
101-
) : null}
102-
</DropdownMenuTrigger>
103-
104-
<DropdownMenuPortal>
105-
<DropdownMenuContent
106-
align="end"
107-
className="max-h-(--available-height) w-[552px] overflow-y-auto p-0"
108-
>
109-
{notifications.length > 0 ? (
110-
notifications.map((notification) => (
111-
<DropdownMenuItem
112-
key={notification.id}
113-
className="items-start gap-4 rounded-none border-b px-4 py-4 whitespace-normal last:border-b-0"
114-
>
115-
<div className="flex h-12 w-12 shrink-0 items-center justify-center overflow-hidden rounded-xl bg-muted">
116-
{notification.image ? (
117-
<img
118-
src={notification.image}
119-
alt=""
120-
aria-hidden="true"
121-
className="h-9 w-9 object-contain"
122-
/>
123-
) : null}
124-
</div>
125-
126-
<div className="min-w-0 flex-1">
127-
<div className="flex items-start justify-between gap-3">
128-
<span className="text-[15px] font-semibold leading-6 text-foreground">
129-
{notification.title}
130-
</span>
131-
132-
{!notification.read ? (
133-
<span className="mt-2 h-2 w-2 shrink-0 rounded-full bg-primary" />
134-
) : null}
135-
</div>
136-
137-
<p className="text-(--text-secondary) text-sm leading-7">
138-
{notification.message}
139-
</p>
140-
</div>
141-
</DropdownMenuItem>
142-
))
143-
) : (
144-
<div className="px-4 py-4 text-sm text-muted-foreground">No notifications</div>
145-
)}
146-
</DropdownMenuContent>
147-
</DropdownMenuPortal>
148-
</DropdownMenu>
149-
15066
{user ? (
15167
<DropdownMenu modal={false}>
15268
<DropdownMenuTrigger

frontend/src/components/ui/navigation-menu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ function MenuBarNav({ className, ...props }: MenuBarNavProps) {
197197
);
198198
}
199199

200-
/** Right-side action slot for notifications, settings, and account controls. */
200+
/** Right-side action slot for settings and account controls. */
201201
function MenuBarActions({ className, ...props }: MenuBarActionsProps) {
202202
return (
203203
<div

frontend/src/hooks/useLaunchpadData.ts

Lines changed: 4 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,17 @@ import { useCallback, useEffect, useMemo, useState } from "react";
22
import { apiFetch } from "../api/client";
33
import { listServices, type Service } from "../api/listServices";
44
import { mapService } from "../api/mapServices";
5-
import { listNotifications, markNotificationRead, type Notification } from "../api/notifications";
6-
import type { NotificationSocketMessage } from "../api/notificationsSocket";
75
import { deletePin, putPin } from "../api/pin";
86
import type { ServiceSocketMessage } from "../api/servicesSocket";
97
import { createWebSocketClient } from "../api/ws";
108

11-
type AppSocketMessage = ServiceSocketMessage | NotificationSocketMessage;
12-
139
export function useLaunchpadData(user: unknown) {
1410
const [services, setServices] = useState<Service[]>([]);
15-
const [notifications, setNotifications] = useState<Notification[]>([]);
1611

1712
useEffect(() => {
1813
listServices().then(setServices).catch(console.error);
19-
listNotifications().then(setNotifications).catch(console.error);
2014
}, [user]);
2115

22-
const onNotificationsViewed = useCallback(async (ids: string[]) => {
23-
const uniqueIds = [...new Set(ids)];
24-
if (uniqueIds.length === 0) return;
25-
26-
setNotifications((prev) =>
27-
prev.map((notification) =>
28-
uniqueIds.includes(notification.id) ? { ...notification, read: true } : notification,
29-
),
30-
);
31-
32-
try {
33-
await Promise.all(uniqueIds.map((id) => markNotificationRead(id)));
34-
} catch (err) {
35-
console.error("markNotificationRead failed", err);
36-
setNotifications((prev) =>
37-
prev.map((notification) =>
38-
uniqueIds.includes(notification.id) ? { ...notification, read: false } : notification,
39-
),
40-
);
41-
}
42-
}, []);
43-
4416
const onTogglePin = useCallback(async (serviceId: string, nextPinned: boolean) => {
4517
let previousPinned: boolean | undefined;
4618

@@ -84,7 +56,7 @@ export function useLaunchpadData(user: unknown) {
8456
const appSocket = useMemo(() => {
8557
const isAuthenticated = Boolean(user);
8658

87-
return createWebSocketClient<AppSocketMessage>({
59+
return createWebSocketClient<ServiceSocketMessage>({
8860
path: "/ws",
8961
// Fetch a fresh single-use ticket before each connect (and each reconnect).
9062
// Browsers cannot send Authorization headers on WebSocket upgrade requests,
@@ -106,23 +78,9 @@ export function useLaunchpadData(user: unknown) {
10678
onClose: () => console.log("app websocket disconnected"),
10779
onError: (event) => console.error("app websocket error", event),
10880
onMessage: (message) => {
109-
if (message.type === "notification.created") {
110-
const nextNotification: Notification = {
111-
id: message.notification.id,
112-
title: message.notification.title,
113-
message: message.notification.message,
114-
createdAt: message.notification.createdAt,
115-
image: message.notification.image ?? "",
116-
read: message.notification.read ?? false,
117-
};
118-
119-
setNotifications((prev) => {
120-
const exists = prev.some((n) => n.id === nextNotification.id);
121-
return exists ? prev : [nextNotification, ...prev];
122-
});
123-
124-
return;
125-
}
81+
// The backend WebSocket also carries other event types. Ignore any
82+
// frame that is not a service event without changing React state.
83+
if (!message.service) return;
12684

12785
const nextService = mapService(message.service);
12886

@@ -174,8 +132,6 @@ export function useLaunchpadData(user: unknown) {
174132

175133
return {
176134
services,
177-
notifications,
178-
onNotificationsViewed,
179135
onTogglePin,
180136
};
181137
}

frontend/src/mocks/fixtures.ts

Lines changed: 0 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
// place that defines what the SPA renders under VITE_USE_MOCKS=1.
33

44
import type { Service } from "../api/listServices";
5-
import type { Notification } from "../api/notifications";
65

76
export type AccessRequest = {
87
id: string;
@@ -60,25 +59,6 @@ export const seedServices: Service[] = [
6059
},
6160
];
6261

63-
export const seedNotifications: Notification[] = [
64-
{
65-
id: "ntf-1",
66-
image: "",
67-
title: "Welcome to Nebari (mocked)",
68-
message: "You are running against MSW mocks. Set VITE_USE_MOCKS=0 to hit a real webapi.",
69-
read: false,
70-
createdAt: new Date(Date.now() - 1000 * 60 * 60).toISOString(),
71-
},
72-
{
73-
id: "ntf-2",
74-
image: "",
75-
title: "Maintenance window scheduled",
76-
message: "Brief restart of JupyterHub planned for tonight at 22:00 UTC.",
77-
read: false,
78-
createdAt: new Date(Date.now() - 1000 * 60 * 60 * 24).toISOString(),
79-
},
80-
];
81-
8262
export const seedAccessRequests: AccessRequest[] = [
8363
{
8464
id: "req-1",

frontend/src/mocks/handlers.ts

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// Stateful MSW handlers. These override a subset of the generated handlers
2-
// so the SPA sees realistic mutation across calls (pin/unpin, mark-read,
3-
// request access, approve/deny). Anything not listed here falls through to
2+
// so the SPA sees realistic mutation across calls (pin/unpin, request access,
3+
// approve/deny). Anything not listed here falls through to
44
// the generated layer in ./generated/handlers.ts.
55

66
import { HttpResponse, http } from "msw";
@@ -56,15 +56,6 @@ const overrides = [
5656
return HttpResponse.json({ success: true, message: "Request submitted" });
5757
}),
5858

59-
http.get("/api/v1/notifications", () => HttpResponse.json(store.notifications)),
60-
61-
http.put("/api/v1/notifications/:id/read", ({ params }) => {
62-
const n = store.notifications.find((x) => x.id === params.id);
63-
if (!n) return new HttpResponse(null, { status: 404 });
64-
n.read = true;
65-
return new HttpResponse(null, { status: 204 });
66-
}),
67-
6859
http.get("/api/v1/pins", () => {
6960
const uids = store.services.filter((s) => s.pinned).map((s) => s.id);
7061
return HttpResponse.json({ pins: uids.map((id) => ({ id })), uids });

frontend/src/mocks/store.ts

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,17 @@
44
// seed fixtures.
55

66
import type { Service } from "../api/listServices";
7-
import type { Notification } from "../api/notifications";
8-
import {
9-
type AccessRequest,
10-
seedAccessRequests,
11-
seedCategories,
12-
seedNotifications,
13-
seedServices,
14-
} from "./fixtures";
7+
import { type AccessRequest, seedAccessRequests, seedCategories, seedServices } from "./fixtures";
158

169
type Store = {
1710
services: Service[];
18-
notifications: Notification[];
1911
accessRequests: AccessRequest[];
2012
categories: Record<string, string>;
2113
};
2214

2315
function snapshot(): Store {
2416
return {
2517
services: seedServices.map((s) => ({ ...s, category: [...s.category] })),
26-
notifications: seedNotifications.map((n) => ({ ...n })),
2718
accessRequests: seedAccessRequests.map((r) => ({ ...r })),
2819
categories: { ...seedCategories },
2920
};
@@ -34,7 +25,6 @@ export const store: Store = snapshot();
3425
export function resetStore(): void {
3526
const fresh = snapshot();
3627
store.services = fresh.services;
37-
store.notifications = fresh.notifications;
3828
store.accessRequests = fresh.accessRequests;
3929
store.categories = fresh.categories;
4030
}

0 commit comments

Comments
 (0)