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:
@@ -6,6 +6,42 @@ export function setTokenProvider(provider: (() => Promise<string | undefined>) |
|
|||||||
tokenProvider = provider;
|
tokenProvider = provider;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom error class that extracts a human-readable message from API responses.
|
||||||
|
* Spring Boot error bodies typically contain: { message, error, status, detail }
|
||||||
|
* Our own controllers may return: { error, message, detail }
|
||||||
|
*/
|
||||||
|
export class ApiError extends Error {
|
||||||
|
public readonly status: number;
|
||||||
|
public readonly detail: string | null;
|
||||||
|
|
||||||
|
constructor(status: number, body: string) {
|
||||||
|
const parsed = ApiError.parseBody(body);
|
||||||
|
super(parsed.message);
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.status = status;
|
||||||
|
this.detail = parsed.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static parseBody(body: string): { message: string; detail: string | null } {
|
||||||
|
try {
|
||||||
|
const json = JSON.parse(body);
|
||||||
|
// Try common Spring Boot / custom error fields in priority order
|
||||||
|
const message = json.message || json.error || json.detail || json.reason || `Request failed`;
|
||||||
|
const detail = json.detail || json.path || null;
|
||||||
|
return { message, detail };
|
||||||
|
} catch {
|
||||||
|
// Not JSON — use raw text, truncated
|
||||||
|
return { message: body.slice(0, 200) || 'Request failed', detail: null };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns just the human message (no "Error:" prefix, no status code, no JSON blob). */
|
||||||
|
toString(): string {
|
||||||
|
return this.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||||
const token = tokenProvider ? await tokenProvider() : null;
|
const token = tokenProvider ? await tokenProvider() : null;
|
||||||
@@ -24,20 +60,20 @@ async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T>
|
|||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
// Don't hard-redirect — let React Query retry (token may not be ready yet).
|
// Don't hard-redirect — let React Query retry (token may not be ready yet).
|
||||||
// The ProtectedRoute handles unauthenticated state.
|
// The ProtectedRoute handles unauthenticated state.
|
||||||
throw new Error('Unauthorized');
|
throw new ApiError(401, '{"message":"Unauthorized"}');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
const errorHeader = response.headers.get('X-Cameleer-Error');
|
const errorHeader = response.headers.get('X-Cameleer-Error');
|
||||||
if (errorHeader === 'APP_MFA_REQUIRED') {
|
if (errorHeader === 'APP_MFA_REQUIRED') {
|
||||||
window.location.href = '/platform/tenant/settings?mfa=required';
|
window.location.href = '/platform/tenant/settings?mfa=required';
|
||||||
throw new Error('MFA enrollment required');
|
throw new ApiError(403, '{"message":"MFA enrollment required"}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
throw new Error(`API error ${response.status}: ${text}`);
|
throw new ApiError(response.status, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (response.status === 204) return undefined as T;
|
if (response.status === 204) return undefined as T;
|
||||||
@@ -57,3 +93,13 @@ export const api = {
|
|||||||
apiFetch<T>(path, { method: 'PUT', body }),
|
apiFetch<T>(path, { method: 'PUT', body }),
|
||||||
delete: <T>(path: string) => apiFetch<T>(path, { method: 'DELETE' }),
|
delete: <T>(path: string) => apiFetch<T>(path, { method: 'DELETE' }),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract a human-readable error message for toast display.
|
||||||
|
* Works with ApiError (preferred) and plain Error/unknown.
|
||||||
|
*/
|
||||||
|
export function errorMessage(err: unknown): string {
|
||||||
|
if (err instanceof ApiError) return err.message;
|
||||||
|
if (err instanceof Error) return err.message;
|
||||||
|
return String(err);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState, useEffect, useRef } from 'react';
|
|||||||
import { useLogto } from '@logto/react';
|
import { useLogto } from '@logto/react';
|
||||||
import { Card, Input, Button, FormField, Alert } from '@cameleer/design-system';
|
import { Card, Input, Button, FormField, Alert } from '@cameleer/design-system';
|
||||||
import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg';
|
import cameleerLogo from '@cameleer/design-system/assets/cameleer-logo.svg';
|
||||||
import { api } from '../api/client';
|
import { api, errorMessage } from '../api/client';
|
||||||
import { toSlug } from '../utils/slug';
|
import { toSlug } from '../utils/slug';
|
||||||
import styles from './OnboardingPage.module.css';
|
import styles from './OnboardingPage.module.css';
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ export function OnboardingPage() {
|
|||||||
// auto-approves and redirects back with fresh tokens.
|
// auto-approves and redirects back with fresh tokens.
|
||||||
await signIn(`${window.location.origin}/platform/callback`);
|
await signIn(`${window.location.origin}/platform/callback`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const msg = err instanceof Error ? err.message : String(err);
|
const msg = err instanceof Error ? err.message : errorMessage(err);
|
||||||
if (msg.includes('409')) {
|
if (msg.includes('409')) {
|
||||||
setError('This organization name is already taken. Try a different organization name.');
|
setError('This organization name is already taken. Try a different organization name.');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { QRCodeSVG } from 'qrcode.react';
|
import { QRCodeSVG } from 'qrcode.react';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -49,7 +50,7 @@ function MfaSection() {
|
|||||||
setSetupData(data);
|
setSetupData(data);
|
||||||
setVerifyCode('');
|
setVerifyCode('');
|
||||||
} catch (err) {
|
} 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' });
|
toast({ title: 'Invalid code. Please try again.', variant: 'error' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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);
|
setCodesSaved(false);
|
||||||
toast({ title: 'Backup codes regenerated', variant: 'success' });
|
toast({ title: 'Backup codes regenerated', variant: 'success' });
|
||||||
} catch (err) {
|
} 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);
|
setSetupData(null);
|
||||||
toast({ title: 'MFA removed', variant: 'success' });
|
toast({ title: 'MFA removed', variant: 'success' });
|
||||||
} catch (err) {
|
} 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 });
|
await updateSettings.mutateAsync({ mfaRequired: false });
|
||||||
toast({ title: 'MFA requirement disabled for all members', variant: 'success' });
|
toast({ title: 'MFA requirement disabled for all members', variant: 'success' });
|
||||||
} catch (err) {
|
} 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);
|
setConfirmEnable(false);
|
||||||
toast({ title: 'MFA is now required for all members', variant: 'success' });
|
toast({ title: 'MFA is now required for all members', variant: 'success' });
|
||||||
} catch (err) {
|
} 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('');
|
setNewPassword('');
|
||||||
setConfirmPassword('');
|
setConfirmPassword('');
|
||||||
} catch (err) {
|
} 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' });
|
toast({ title: 'Server admin password reset successfully', variant: 'success' });
|
||||||
setServerAdminPw('');
|
setServerAdminPw('');
|
||||||
} catch (err) {
|
} 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 }}
|
style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 12 }}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useRef } from 'react';
|
import { useState, useRef } from 'react';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import {
|
import {
|
||||||
Alert, AlertDialog, Badge, Button, Card, DataTable,
|
Alert, AlertDialog, Badge, Button, Card, DataTable,
|
||||||
EmptyState, FileInput, FormField, Input, Spinner, useToast,
|
EmptyState, FileInput, FormField, Input, Spinner, useToast,
|
||||||
@@ -87,7 +88,7 @@ export function SsoPage() {
|
|||||||
toast({ title: `SSO connector "${connectorName}" created`, variant: 'success' });
|
toast({ title: `SSO connector "${connectorName}" created`, variant: 'success' });
|
||||||
resetForm();
|
resetForm();
|
||||||
} catch (err) {
|
} 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' });
|
toast({ title: `Deleted "${deleteTarget.connectorName}"`, variant: 'success' });
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -112,7 +113,7 @@ export function SsoPage() {
|
|||||||
toast({ title: 'Connection test failed', description: 'IdP metadata could not be resolved', variant: 'warning' });
|
toast({ title: 'Connection test failed', description: 'IdP metadata could not be resolved', variant: 'warning' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} 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();
|
certRef.current?.clear();
|
||||||
setLabel('');
|
setLabel('');
|
||||||
} catch (err) {
|
} 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);
|
await activateMutation.mutateAsync(id);
|
||||||
toast({ title: 'CA certificate activated', variant: 'success' });
|
toast({ title: 'CA certificate activated', variant: 'success' });
|
||||||
} catch (err) {
|
} 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' });
|
toast({ title: 'CA certificate removed', variant: 'success' });
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||||
setDeleteTarget(null);
|
setDeleteTarget(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
@@ -131,7 +132,7 @@ export function TeamPage() {
|
|||||||
setInviteRole('viewer');
|
setInviteRole('viewer');
|
||||||
setShowInvite(false);
|
setShowInvite(false);
|
||||||
} catch (err) {
|
} 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' });
|
toast({ title: `Removed ${removeTarget.name}`, variant: 'success' });
|
||||||
setRemoveTarget(null);
|
setRemoveTarget(null);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Remove failed', description: String(err), variant: 'error' });
|
toast({ title: 'Remove failed', description: errorMessage(err), variant: 'error' });
|
||||||
setRemoveTarget(null);
|
setRemoveTarget(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -269,7 +270,7 @@ export function TeamPage() {
|
|||||||
toast({ title: `MFA reset for ${mfaResetTarget.name || mfaResetTarget.email}`, variant: 'success' });
|
toast({ title: `MFA reset for ${mfaResetTarget.name || mfaResetTarget.email}`, variant: 'success' });
|
||||||
setMfaResetTarget(null);
|
setMfaResetTarget(null);
|
||||||
} catch (err) {
|
} 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}
|
loading={resetMfa.isPending}
|
||||||
@@ -297,7 +298,7 @@ export function TeamPage() {
|
|||||||
setPwTarget(null);
|
setPwTarget(null);
|
||||||
setPwValue('');
|
setPwValue('');
|
||||||
} catch (err) {
|
} 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 }}
|
style={{ display: 'flex', flexDirection: 'column', gap: 16 }}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import { ArrowUpCircle, ExternalLink, Key, RefreshCw, Settings } from 'lucide-react';
|
import { ArrowUpCircle, ExternalLink, Key, RefreshCw, Settings } from 'lucide-react';
|
||||||
import { useTenantDashboard, useRestartServer, useUpgradeServer } from '../../api/tenant-hooks';
|
import { useTenantDashboard, useRestartServer, useUpgradeServer } from '../../api/tenant-hooks';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
||||||
import { UsageIndicator } from '../../components/UsageIndicator';
|
import { UsageIndicator } from '../../components/UsageIndicator';
|
||||||
import { tierColor } from '../../utils/tier';
|
import { tierColor } from '../../utils/tier';
|
||||||
@@ -107,7 +108,7 @@ export function TenantDashboardPage() {
|
|||||||
await restartServer.mutateAsync();
|
await restartServer.mutateAsync();
|
||||||
toast({ title: 'Server restarted', variant: 'success' });
|
toast({ title: 'Server restarted', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
|
toast({ title: 'Restart failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={restartServer.isPending}
|
loading={restartServer.isPending}
|
||||||
@@ -122,7 +123,7 @@ export function TenantDashboardPage() {
|
|||||||
await upgradeServer.mutateAsync();
|
await upgradeServer.mutateAsync();
|
||||||
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
|
toast({ title: 'Upgrade failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading={upgradeServer.isPending}
|
loading={upgradeServer.isPending}
|
||||||
|
|||||||
9
ui/src/pages/vendor/CertificatesPage.tsx
vendored
9
ui/src/pages/vendor/CertificatesPage.tsx
vendored
@@ -1,4 +1,5 @@
|
|||||||
import { useRef, useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
Badge,
|
Badge,
|
||||||
@@ -163,7 +164,7 @@ export function CertificatesPage() {
|
|||||||
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
|
toast({ title: 'Validation failed', description: result.errors.join(', '), variant: 'error' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Upload failed', description: String(err), variant: 'error' });
|
toast({ title: 'Upload failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,7 +173,7 @@ export function CertificatesPage() {
|
|||||||
await activateMutation.mutateAsync();
|
await activateMutation.mutateAsync();
|
||||||
toast({ title: 'Certificate activated', variant: 'success' });
|
toast({ title: 'Certificate activated', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Activation failed', description: String(err), variant: 'error' });
|
toast({ title: 'Activation failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,7 +182,7 @@ export function CertificatesPage() {
|
|||||||
await restoreMutation.mutateAsync();
|
await restoreMutation.mutateAsync();
|
||||||
toast({ title: 'Certificate restored from archive', variant: 'success' });
|
toast({ title: 'Certificate restored from archive', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Restore failed', description: String(err), variant: 'error' });
|
toast({ title: 'Restore failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,7 +191,7 @@ export function CertificatesPage() {
|
|||||||
await discardMutation.mutateAsync();
|
await discardMutation.mutateAsync();
|
||||||
toast({ title: 'Staged certificate discarded', variant: 'success' });
|
toast({ title: 'Staged certificate discarded', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Discard failed', description: String(err), variant: 'error' });
|
toast({ title: 'Discard failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
ui/src/pages/vendor/CreateTenantPage.tsx
vendored
3
ui/src/pages/vendor/CreateTenantPage.tsx
vendored
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react';
|
|||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
import { Button, Card, FormField, Input, useToast } from '@cameleer/design-system';
|
import { Button, Card, FormField, Input, useToast } from '@cameleer/design-system';
|
||||||
import { useCreateTenant } from '../../api/vendor-hooks';
|
import { useCreateTenant } from '../../api/vendor-hooks';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import { toSlug } from '../../utils/slug';
|
import { toSlug } from '../../utils/slug';
|
||||||
|
|
||||||
const TIERS = ['STARTER', 'TEAM', 'BUSINESS', 'ENTERPRISE'];
|
const TIERS = ['STARTER', 'TEAM', 'BUSINESS', 'ENTERPRISE'];
|
||||||
@@ -35,7 +36,7 @@ export function CreateTenantPage() {
|
|||||||
toast({ title: 'Tenant created — provisioning in progress', variant: 'success' });
|
toast({ title: 'Tenant created — provisioning in progress', variant: 'success' });
|
||||||
navigate(`/vendor/tenants/${result.id}`);
|
navigate(`/vendor/tenants/${result.id}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Failed to create tenant', description: String(err), variant: 'error' });
|
toast({ title: 'Failed to create tenant', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
9
ui/src/pages/vendor/EmailConfigPage.tsx
vendored
9
ui/src/pages/vendor/EmailConfigPage.tsx
vendored
@@ -9,6 +9,7 @@ import {
|
|||||||
useToast,
|
useToast,
|
||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import { Send, Trash2, Save, Power } from 'lucide-react';
|
import { Send, Trash2, Save, Power } from 'lucide-react';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import {
|
import {
|
||||||
useEmailConnector,
|
useEmailConnector,
|
||||||
useSaveEmailConnector,
|
useSaveEmailConnector,
|
||||||
@@ -70,7 +71,7 @@ export function EmailConfigPage() {
|
|||||||
setEditing(false);
|
setEditing(false);
|
||||||
setPassword('');
|
setPassword('');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Failed to save', description: String(err), variant: 'error' });
|
toast({ title: 'Failed to save', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +82,7 @@ export function EmailConfigPage() {
|
|||||||
setConfirmDelete(false);
|
setConfirmDelete(false);
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Failed to delete', description: String(err), variant: 'error' });
|
toast({ title: 'Failed to delete', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ export function EmailConfigPage() {
|
|||||||
toast({ title: 'Test failed', description: result.message, variant: 'error' });
|
toast({ title: 'Test failed', description: result.message, variant: 'error' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Test failed', description: String(err), variant: 'error' });
|
toast({ title: 'Test failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,7 +113,7 @@ export function EmailConfigPage() {
|
|||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Failed to toggle registration', description: String(err), variant: 'error' });
|
toast({ title: 'Failed to toggle registration', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
ui/src/pages/vendor/LicenseVerifyPage.tsx
vendored
3
ui/src/pages/vendor/LicenseVerifyPage.tsx
vendored
@@ -7,6 +7,7 @@ import {
|
|||||||
} from '@cameleer/design-system';
|
} from '@cameleer/design-system';
|
||||||
import { Clipboard, Search, ShieldCheck } from 'lucide-react';
|
import { Clipboard, Search, ShieldCheck } from 'lucide-react';
|
||||||
import { useVerifyLicense, usePublicKey } from '../../api/vendor-hooks';
|
import { useVerifyLicense, usePublicKey } from '../../api/vendor-hooks';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import styles from '../../styles/platform.module.css';
|
import styles from '../../styles/platform.module.css';
|
||||||
import type { VerifyLicenseResponse } from '../../types/api';
|
import type { VerifyLicenseResponse } from '../../types/api';
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ export function LicenseVerifyPage() {
|
|||||||
const res = await verifyLicense.mutateAsync(token.trim());
|
const res = await verifyLicense.mutateAsync(token.trim());
|
||||||
setResult(res);
|
setResult(res);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Verification failed', description: String(err), variant: 'error' });
|
toast({ title: 'Verification failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
ui/src/pages/vendor/TenantDetailPage.tsx
vendored
11
ui/src/pages/vendor/TenantDetailPage.tsx
vendored
@@ -20,6 +20,7 @@ import {
|
|||||||
useRestartServer,
|
useRestartServer,
|
||||||
useUpgradeServer,
|
useUpgradeServer,
|
||||||
} from '../../api/vendor-hooks';
|
} from '../../api/vendor-hooks';
|
||||||
|
import { errorMessage } from '../../api/client';
|
||||||
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
import { ServerStatusBadge } from '../../components/ServerStatusBadge';
|
||||||
import { tierColor } from '../../utils/tier';
|
import { tierColor } from '../../utils/tier';
|
||||||
import styles from '../../styles/platform.module.css';
|
import styles from '../../styles/platform.module.css';
|
||||||
@@ -155,7 +156,7 @@ export function TenantDetailPage() {
|
|||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Minting failed', description: String(err), variant: 'error' });
|
toast({ title: 'Minting failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,7 +201,7 @@ export function TenantDetailPage() {
|
|||||||
toast({ title: 'Tenant suspended', variant: 'warning' });
|
toast({ title: 'Tenant suspended', variant: 'warning' });
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Action failed', description: String(err), variant: 'error' });
|
toast({ title: 'Action failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +211,7 @@ export function TenantDetailPage() {
|
|||||||
await restartServer.mutateAsync(id);
|
await restartServer.mutateAsync(id);
|
||||||
toast({ title: 'Server restarted', variant: 'success' });
|
toast({ title: 'Server restarted', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Restart failed', description: String(err), variant: 'error' });
|
toast({ title: 'Restart failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +221,7 @@ export function TenantDetailPage() {
|
|||||||
await upgradeServer.mutateAsync(id);
|
await upgradeServer.mutateAsync(id);
|
||||||
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
toast({ title: 'Server upgrade started — pulling latest images', variant: 'success' });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Upgrade failed', description: String(err), variant: 'error' });
|
toast({ title: 'Upgrade failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,7 +232,7 @@ export function TenantDetailPage() {
|
|||||||
toast({ title: 'Tenant deleted', variant: 'success' });
|
toast({ title: 'Tenant deleted', variant: 'success' });
|
||||||
navigate('/vendor/tenants');
|
navigate('/vendor/tenants');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({ title: 'Delete failed', description: String(err), variant: 'error' });
|
toast({ title: 'Delete failed', description: errorMessage(err), variant: 'error' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user