Skip to content

Commit a273a52

Browse files
author
Mohammod Al Amin Ashik
committed
feat: add toast notifications to all save/modify operations app-wide
Added toast feedback to every mutation operation across the desktop app: - SpacesPage: create, delete, set active space - RegistryPage: install, uninstall server - ClientsPage: save config, delete client, toggle feature set grants, toggle individual features (also replaced custom toast with useToast) - SpaceSwitcher: set active space, create space - ServerLogViewer: clear logs, copy log path (replaced alert() calls)
1 parent 76336c0 commit a273a52

5 files changed

Lines changed: 88 additions & 42 deletions

File tree

apps/desktop/src/components/ServerLogViewer.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState, useRef } from 'react';
22
import { X, Download, Trash2, RefreshCw } from 'lucide-react';
3+
import { useToast, ToastContainer } from '@mcpmux/ui';
34
import { getServerLogs, clearServerLogs, getServerLogFile, type ServerLogEntry } from '@/lib/api/logs';
45

56
interface ServerLogViewerProps {
@@ -38,6 +39,7 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
3839
const [autoRefresh, setAutoRefresh] = useState(false);
3940
const scrollContainerRef = useRef<HTMLDivElement>(null);
4041
const shouldScrollRef = useRef(true);
42+
const { toasts, success, error: showError, dismiss } = useToast();
4143

4244
const loadLogs = async () => {
4345
try {
@@ -97,19 +99,19 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
9799
try {
98100
await clearServerLogs(serverId);
99101
setLogs([]);
102+
success('Logs cleared', `All logs for "${serverName}" have been cleared`);
100103
} catch (e) {
101-
alert(`Failed to clear logs: ${e}`);
104+
showError('Failed to clear logs', e instanceof Error ? e.message : String(e));
102105
}
103106
};
104107

105108
const handleOpenInEditor = async () => {
106109
try {
107110
const filePath = await getServerLogFile(serverId);
108-
// Copy path to clipboard for user to paste into their editor
109111
await navigator.clipboard.writeText(filePath);
110-
alert(`Log file path copied to clipboard:\n${filePath}\n\nPaste this into your file explorer or text editor.`);
112+
success('Path copied', `Log file path copied to clipboard`);
111113
} catch (e) {
112-
alert(`Failed to get log file path: ${e}`);
114+
showError('Failed to get log file path', e instanceof Error ? e.message : String(e));
113115
}
114116
};
115117

@@ -131,6 +133,7 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
131133

132134
return (
133135
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
136+
<ToastContainer toasts={toasts} onClose={dismiss} />
134137
<div className="bg-[rgb(var(--card))] border border-[rgb(var(--border-subtle))] rounded-xl shadow-xl w-[90vw] h-[85vh] flex flex-col">
135138
{/* Header */}
136139
<div className="flex items-center justify-between p-4 border-b border-[rgb(var(--border-subtle))]">

apps/desktop/src/components/SpaceSwitcher.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Plus,
66
Loader2,
77
} from 'lucide-react';
8-
import { Button } from '@mcpmux/ui';
8+
import { Button, useToast, ToastContainer } from '@mcpmux/ui';
99
import {
1010
useAppStore,
1111
useActiveSpace,
@@ -25,6 +25,7 @@ export function SpaceSwitcher({ className = '' }: SpaceSwitcherProps) {
2525
const [newName, setNewName] = useState('');
2626
const [showCreateInput, setShowCreateInput] = useState(false);
2727
const dropdownRef = useRef<HTMLDivElement>(null);
28+
const { toasts, success, error: showError, dismiss } = useToast();
2829

2930
const spaces = useSpaces();
3031
const activeSpace = useActiveSpace();
@@ -56,8 +57,10 @@ export function SpaceSwitcher({ className = '' }: SpaceSwitcherProps) {
5657
await setActiveSpaceAPI(spaceId);
5758
setActiveSpaceInStore(spaceId);
5859
setIsOpen(false);
60+
const activatedSpace = spaces.find(s => s.id === spaceId);
61+
success('Space activated', `Switched to "${activatedSpace?.name || 'Space'}"`);
5962
} catch (e) {
60-
console.error('Failed to switch space:', e);
63+
showError('Failed to switch space', e instanceof Error ? e.message : String(e));
6164
}
6265
};
6366

@@ -73,15 +76,17 @@ export function SpaceSwitcher({ className = '' }: SpaceSwitcherProps) {
7376
setNewName('');
7477
setShowCreateInput(false);
7578
setIsOpen(false);
79+
success('Space created', `"${space.name}" has been created and activated`);
7680
} catch (e) {
77-
console.error('Failed to create space:', e);
81+
showError('Failed to create space', e instanceof Error ? e.message : String(e));
7882
} finally {
7983
setIsCreating(false);
8084
}
8185
};
8286

8387
return (
8488
<div ref={dropdownRef} className={`relative ${className}`}>
89+
<ToastContainer toasts={toasts} onClose={dismiss} />
8590
{/* Trigger Button */}
8691
<button
8792
onClick={() => setIsOpen(!isOpen)}

apps/desktop/src/features/clients/ClientsPage.tsx

Lines changed: 32 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
Card,
2626
CardContent,
2727
Button,
28+
useToast,
29+
ToastContainer,
2830
} from '@mcpmux/ui';
2931
import type { OAuthClient, UpdateClientRequest } from '@/lib/api/gateway';
3032
import { listOAuthClients, updateOAuthClient, deleteOAuthClient } from '@/lib/api/gateway';
@@ -97,8 +99,7 @@ export default function ClientsPage() {
9799
// Panel state
98100
const [selectedClient, setSelectedClient] = useState<OAuthClient | null>(null);
99101

100-
// Toast notification for reconnects
101-
const [toast, setToast] = useState<{ message: string; type: 'info' | 'success' } | null>(null);
102+
const { toasts, success, error: showError, info, dismiss } = useToast();
102103

103104
// Edit state
104105
const [editAlias, setEditAlias] = useState('');
@@ -264,8 +265,7 @@ export default function ClientsPage() {
264265
// Show toast for reconnections (silent approval)
265266
if (event.payload.action === 'reconnected') {
266267
const name = event.payload.client_name || event.payload.client_id;
267-
setToast({ message: `${name} connected`, type: 'info' });
268-
setTimeout(() => setToast(null), 3000);
268+
info('Client connected', `${name} connected`);
269269
}
270270
});
271271

@@ -309,17 +309,24 @@ export default function ClientsPage() {
309309
const toggleFeatureSetGrant = async (featureSetId: string) => {
310310
if (!selectedClient || !activeSpace) return;
311311

312+
const featureSet = availableFeatureSets.find(fs => fs.id === featureSetId);
313+
const fsName = featureSet?.name || 'Feature set';
314+
312315
try {
313316
if (grantedFeatureSetIds.includes(featureSetId)) {
314317
await revokeOAuthClientFeatureSet(selectedClient.client_id, activeSpace.id, featureSetId);
315318
setGrantedFeatureSetIds(prev => prev.filter(id => id !== featureSetId));
319+
success('Permission revoked', `"${fsName}" removed from client`);
316320
} else {
317321
await grantOAuthClientFeatureSet(selectedClient.client_id, activeSpace.id, featureSetId);
318322
setGrantedFeatureSetIds(prev => [...prev, featureSetId]);
323+
success('Permission granted', `"${fsName}" added to client`);
319324
}
320325
loadResolvedFeatures(selectedClient.client_id);
321326
} catch (e) {
322-
setError(e instanceof Error ? e.message : String(e));
327+
const msg = e instanceof Error ? e.message : String(e);
328+
setError(msg);
329+
showError('Failed to update permission', msg);
323330
}
324331
};
325332

@@ -345,8 +352,11 @@ export default function ClientsPage() {
345352
));
346353

347354
setSelectedClient(updated);
355+
success('Client settings saved', `"${updated.client_alias || updated.client_name}" has been updated`);
348356
} catch (e) {
349-
setError(e instanceof Error ? e.message : String(e));
357+
const msg = e instanceof Error ? e.message : String(e);
358+
setError(msg);
359+
showError('Failed to save settings', msg);
350360
} finally {
351361
setIsSaving(false);
352362
}
@@ -355,12 +365,18 @@ export default function ClientsPage() {
355365
const handleDelete = async (clientId: string) => {
356366
if (!confirm('Remove this client? All tokens will be revoked.')) return;
357367

368+
const deletedClient = oauthClients.find(c => c.client_id === clientId);
369+
const clientName = deletedClient?.client_alias || deletedClient?.client_name || 'Client';
370+
358371
try {
359372
await deleteOAuthClient(clientId);
360373
setOAuthClients(prev => prev.filter(c => c.client_id !== clientId));
361374
setSelectedClient(null);
375+
success('Client removed', `"${clientName}" and its tokens have been revoked`);
362376
} catch (e) {
363-
setError(e instanceof Error ? e.message : String(e));
377+
const msg = e instanceof Error ? e.message : String(e);
378+
setError(msg);
379+
showError('Failed to remove client', msg);
364380
}
365381
};
366382

@@ -440,13 +456,15 @@ export default function ClientsPage() {
440456
const isAdded = individualFeatureMembers.some(m => m.member_id === featureId);
441457
console.log('Feature is currently added:', isAdded);
442458

459+
const feature = availableFeatures.find(f => f.id === featureId);
460+
const featureName = feature?.feature_name || 'Feature';
461+
443462
try {
444463
if (isAdded) {
445-
console.log('Removing feature from set...');
446464
await removeFeatureFromSet(clientCustomFeatureSet.id, featureId);
447465
setIndividualFeatureMembers(prev => prev.filter(m => m.member_id !== featureId));
466+
success('Feature removed', `"${featureName}" removed from client`);
448467
} else {
449-
console.log('Adding feature to set...');
450468
await addFeatureToSet(clientCustomFeatureSet.id, featureId, 'include');
451469
setIndividualFeatureMembers(prev => [...prev, {
452470
id: '',
@@ -455,14 +473,14 @@ export default function ClientsPage() {
455473
member_id: featureId,
456474
mode: 'include',
457475
}]);
476+
success('Feature added', `"${featureName}" added to client`);
458477
}
459478

460-
console.log('Reloading resolved features...');
461479
await loadResolvedFeatures(selectedClient.client_id);
462-
console.log('Toggle complete');
463480
} catch (e) {
464-
console.error('Error toggling feature:', e);
465-
setError(e instanceof Error ? e.message : String(e));
481+
const msg = e instanceof Error ? e.message : String(e);
482+
setError(msg);
483+
showError('Failed to toggle feature', msg);
466484
}
467485
};
468486

@@ -1282,21 +1300,7 @@ export default function ClientsPage() {
12821300
</div>
12831301
)}
12841302

1285-
{/* Toast notification */}
1286-
{toast && (
1287-
<div className="fixed bottom-4 right-4 z-50 animate-in slide-in-from-bottom-4">
1288-
<div className={`px-4 py-2 rounded-lg shadow-lg ${
1289-
toast.type === 'success'
1290-
? 'bg-green-600 text-white'
1291-
: 'bg-[rgb(var(--surface-elevated))] border border-[rgb(var(--border))] text-[rgb(var(--foreground))]'
1292-
}`}>
1293-
<div className="flex items-center gap-2">
1294-
<Wifi className="h-4 w-4" />
1295-
<span className="text-sm">{toast.message}</span>
1296-
</div>
1297-
</div>
1298-
</div>
1299-
)}
1303+
<ToastContainer toasts={toasts} onClose={dismiss} />
13001304
</div>
13011305
);
13021306
}

apps/desktop/src/features/registry/RegistryPage.tsx

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { useEffect, useState } from 'react';
88
import { ChevronDown } from 'lucide-react';
9+
import { useToast, ToastContainer } from '@mcpmux/ui';
910
import { useRegistryStore } from '../../stores/registryStore';
1011
import { ServerCard } from './ServerCard';
1112
import { ServerDetailModal } from './ServerDetailModal';
@@ -37,6 +38,7 @@ export function RegistryPage() {
3738

3839
const [localSearch, setLocalSearch] = useState('');
3940
const viewSpace = useViewSpace();
41+
const { toasts, success, error: showToastError, dismiss } = useToast();
4042

4143
const itemsPerPage = uiConfig?.items_per_page ?? 24;
4244

@@ -89,13 +91,27 @@ export function RegistryPage() {
8991
}, [localSearch, searchQuery, search]);
9092

9193
const handleInstall = async (id: string) => {
92-
await installServer(id, viewSpace?.id);
94+
const server = servers.find(s => s.id === id);
95+
const serverName = server?.name || 'Server';
96+
try {
97+
await installServer(id, viewSpace?.id);
98+
success('Server installed', `"${serverName}" has been installed`);
99+
} catch {
100+
showToastError('Install failed', `Failed to install "${serverName}"`);
101+
}
93102
};
94103

95104
const handleUninstall = async (id: string) => {
96-
await uninstallServer(id);
97-
if (selectedServer?.id === id) {
98-
selectServer(null);
105+
const server = servers.find(s => s.id === id);
106+
const serverName = server?.name || 'Server';
107+
try {
108+
await uninstallServer(id);
109+
if (selectedServer?.id === id) {
110+
selectServer(null);
111+
}
112+
success('Server uninstalled', `"${serverName}" has been uninstalled`);
113+
} catch {
114+
showToastError('Uninstall failed', `Failed to uninstall "${serverName}"`);
99115
}
100116
};
101117

@@ -104,6 +120,7 @@ export function RegistryPage() {
104120

105121
return (
106122
<div className="h-full flex flex-col" data-testid="registry-page">
123+
<ToastContainer toasts={toasts} onClose={dismiss} />
107124
{/* Header */}
108125
<div className="p-6 border-b border-[rgb(var(--border-subtle))]">
109126
<div className="flex items-center gap-3 mb-1">

apps/desktop/src/features/spaces/SpacesPage.tsx

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import {
1414
CardTitle,
1515
CardContent,
1616
Button,
17+
useToast,
18+
ToastContainer,
1719
} from '@mcpmux/ui';
1820
import {
1921
useAppStore,
@@ -37,6 +39,7 @@ export function SpacesPage() {
3739
const [searchQuery, setSearchQuery] = useState('');
3840
const [error, setError] = useState<string | null>(null);
3941
const [isActionLoading, setIsActionLoading] = useState<string | null>(null); // ID of space being acted on
42+
const { toasts, success, error: showError, dismiss } = useToast();
4043

4144
// Create Modal State
4245
const [showCreateModal, setShowCreateModal] = useState(false);
@@ -55,8 +58,11 @@ export function SpacesPage() {
5558
setNewSpaceName('');
5659
setNewSpaceIcon('🌐');
5760
setShowCreateModal(false);
61+
success('Space created', `"${space.name}" has been created`);
5862
} catch (e) {
59-
setError(e instanceof Error ? e.message : String(e));
63+
const msg = e instanceof Error ? e.message : String(e);
64+
setError(msg);
65+
showError('Failed to create space', msg);
6066
} finally {
6167
setIsCreating(false);
6268
}
@@ -68,10 +74,14 @@ export function SpacesPage() {
6874
setIsActionLoading(id);
6975
setError(null);
7076
try {
77+
const deletedSpace = spaces.find(s => s.id === id);
7178
await deleteSpace(id);
7279
removeSpace(id);
80+
success('Space deleted', `"${deletedSpace?.name || 'Space'}" has been deleted`);
7381
} catch (e) {
74-
setError(e instanceof Error ? e.message : String(e));
82+
const msg = e instanceof Error ? e.message : String(e);
83+
setError(msg);
84+
showError('Failed to delete space', msg);
7585
} finally {
7686
setIsActionLoading(null);
7787
}
@@ -83,8 +93,12 @@ export function SpacesPage() {
8393
try {
8494
await setActiveSpaceAPI(id);
8595
setActiveSpaceInStore(id);
96+
const activatedSpace = spaces.find(s => s.id === id);
97+
success('Active space changed', `"${activatedSpace?.name || 'Space'}" is now active`);
8698
} catch (e) {
87-
setError(e instanceof Error ? e.message : String(e));
99+
const msg = e instanceof Error ? e.message : String(e);
100+
setError(msg);
101+
showError('Failed to set active space', msg);
88102
} finally {
89103
setIsActionLoading(null);
90104
}
@@ -101,6 +115,8 @@ export function SpacesPage() {
101115
});
102116

103117
return (
118+
<>
119+
<ToastContainer toasts={toasts} onClose={dismiss} />
104120
<div className="h-full flex flex-col relative" data-testid="spaces-page">
105121
{/* Header */}
106122
<div className="flex-shrink-0 p-8 border-b border-[rgb(var(--border-subtle))]">
@@ -323,6 +339,7 @@ export function SpacesPage() {
323339
</div>
324340
)}
325341
</div>
342+
</>
326343
);
327344
}
328345

0 commit comments

Comments
 (0)