fix(ui): extract meaningful error messages from API responses
Introduces ApiError class in client.ts that parses Spring Boot error bodies to extract human-readable messages (message, error, detail fields). Adds errorMessage() helper used by all toast descriptions instead of raw String(err) which dumped JSON blobs to the user. Affected: all 10 page components that display error toasts. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
@@ -49,7 +50,7 @@ function MfaSection() {
|
||||
setSetupData(data);
|
||||
setVerifyCode('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to start MFA setup', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to start MFA setup', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +70,7 @@ function MfaSection() {
|
||||
toast({ title: 'Invalid code. Please try again.', variant: 'error' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Verification failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Verification failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +81,7 @@ function MfaSection() {
|
||||
setCodesSaved(false);
|
||||
toast({ title: 'Backup codes regenerated', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to regenerate backup codes', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to regenerate backup codes', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +93,7 @@ function MfaSection() {
|
||||
setSetupData(null);
|
||||
toast({ title: 'MFA removed', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to remove MFA', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to remove MFA', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,7 +288,7 @@ function MfaEnforcementToggle() {
|
||||
await updateSettings.mutateAsync({ mfaRequired: false });
|
||||
toast({ title: 'MFA requirement disabled for all members', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to update MFA setting', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to update MFA setting', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -297,7 +298,7 @@ function MfaEnforcementToggle() {
|
||||
setConfirmEnable(false);
|
||||
toast({ title: 'MFA is now required for all members', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to update MFA setting', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to update MFA setting', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,7 +366,7 @@ export function SettingsPage() {
|
||||
setNewPassword('');
|
||||
setConfirmPassword('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to change password', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to change password', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +476,7 @@ export function SettingsPage() {
|
||||
toast({ title: 'Server admin password reset successfully', variant: 'success' });
|
||||
setServerAdminPw('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to reset server admin password', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to reset server admin password', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert, AlertDialog, Badge, Button, Card, DataTable,
|
||||
EmptyState, FileInput, FormField, Input, Spinner, useToast,
|
||||
@@ -87,7 +88,7 @@ export function SsoPage() {
|
||||
toast({ title: `SSO connector "${connectorName}" created`, variant: 'success' });
|
||||
resetForm();
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to create connector', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to create connector', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ export function SsoPage() {
|
||||
toast({ title: `Deleted "${deleteTarget.connectorName}"`, variant: 'success' });
|
||||
setDeleteTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}
|
||||
@@ -112,7 +113,7 @@ export function SsoPage() {
|
||||
toast({ title: 'Connection test failed', description: 'IdP metadata could not be resolved', variant: 'warning' });
|
||||
}
|
||||
} catch (err) {
|
||||
toast({ title: 'Connection test failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Connection test failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -351,7 +352,7 @@ function CaCertificatesSection() {
|
||||
certRef.current?.clear();
|
||||
setLabel('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Upload failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Upload failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,7 +361,7 @@ function CaCertificatesSection() {
|
||||
await activateMutation.mutateAsync(id);
|
||||
toast({ title: 'CA certificate activated', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Activation failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Activation failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +372,7 @@ function CaCertificatesSection() {
|
||||
toast({ title: 'CA certificate removed', variant: 'success' });
|
||||
setDeleteTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||
setDeleteTarget(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useState } from 'react';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import {
|
||||
Alert,
|
||||
AlertDialog,
|
||||
@@ -131,7 +132,7 @@ export function TeamPage() {
|
||||
setInviteRole('viewer');
|
||||
setShowInvite(false);
|
||||
} catch (err) {
|
||||
toast({ title: 'Invite failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Invite failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +143,7 @@ export function TeamPage() {
|
||||
toast({ title: `Removed ${removeTarget.name}`, variant: 'success' });
|
||||
setRemoveTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Remove failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Remove failed', description: errorMessage(err), variant: 'error' });
|
||||
setRemoveTarget(null);
|
||||
}
|
||||
}
|
||||
@@ -269,7 +270,7 @@ export function TeamPage() {
|
||||
toast({ title: `MFA reset for ${mfaResetTarget.name || mfaResetTarget.email}`, variant: 'success' });
|
||||
setMfaResetTarget(null);
|
||||
} catch (err) {
|
||||
toast({ title: 'Failed to reset MFA', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Failed to reset MFA', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
loading={resetMfa.isPending}
|
||||
@@ -297,7 +298,7 @@ export function TeamPage() {
|
||||
setPwTarget(null);
|
||||
setPwValue('');
|
||||
} catch (err) {
|
||||
toast({ title: 'Reset failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Reset failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@cameleer/design-system';
|
||||
import { ArrowUpCircle, ExternalLink, Key, RefreshCw, Settings } from 'lucide-react';
|
||||
import { useTenantDashboard, useRestartServer, useUpgradeServer } from '../../api/tenant-hooks';
|
||||
import { errorMessage } from '../../api/client';
|
||||
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
||||
import { UsageIndicator } from '../../components/UsageIndicator';
|
||||
import { tierColor } from '../../utils/tier';
|
||||
@@ -107,7 +108,7 @@ export function TenantDashboardPage() {
|
||||
await restartServer.mutateAsync();
|
||||
toast({ title: 'Server restarted', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Restart failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
loading={restartServer.isPending}
|
||||
@@ -122,7 +123,7 @@ export function TenantDashboardPage() {
|
||||
await upgradeServer.mutateAsync();
|
||||
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
||||
} catch (err) {
|
||||
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
|
||||
toast({ title: 'Upgrade failed', description: errorMessage(err), variant: 'error' });
|
||||
}
|
||||
}}
|
||||
loading={upgradeServer.isPending}
|
||||
|
||||
Reference in New Issue
Block a user