refactor(client): centralize error feedback with toast events
- emit API/network failures through a shared toast event bridge - remove per-page inline error notifications to avoid duplicate error UI - dedupe repeated toast messages to keep feedback readable
This commit is contained in:
@@ -11,19 +11,29 @@ import type {
|
|||||||
ScenarioStep,
|
ScenarioStep,
|
||||||
Snippet,
|
Snippet,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
import { emitApiErrorToast } from '../lib/toast-events';
|
||||||
|
|
||||||
// In dev, Vite proxies /environments /sessions /scenarios to localhost:3000.
|
// In dev, Vite proxies /environments /sessions /scenarios to localhost:3000.
|
||||||
// In production (or when VITE_API_URL is set) we hit the configured origin directly.
|
// In production (or when VITE_API_URL is set) we hit the configured origin directly.
|
||||||
const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? '';
|
const BASE_URL: string = (import.meta.env.VITE_API_URL as string | undefined) ?? '';
|
||||||
|
|
||||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
const res = await fetch(`${BASE_URL}${path}`, {
|
let res: Response;
|
||||||
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
try {
|
||||||
...init,
|
res = await fetch(`${BASE_URL}${path}`, {
|
||||||
});
|
headers: { 'Content-Type': 'application/json', ...init?.headers },
|
||||||
|
...init,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
const message = (err as Error).message || 'Network request failed';
|
||||||
|
emitApiErrorToast(message);
|
||||||
|
throw new Error(message);
|
||||||
|
}
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const text = await res.text().catch(() => res.statusText);
|
const text = await res.text().catch(() => res.statusText);
|
||||||
throw new Error(`${res.status} ${text}`);
|
const message = `${res.status} ${text}`;
|
||||||
|
emitApiErrorToast(message);
|
||||||
|
throw new Error(message);
|
||||||
}
|
}
|
||||||
const contentLength = res.headers.get('content-length');
|
const contentLength = res.headers.get('content-length');
|
||||||
if (res.status === 204 || contentLength === '0') return undefined as T;
|
if (res.status === 204 || contentLength === '0') return undefined as T;
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export const API_ERROR_TOAST_EVENT = 'liqa:api-error-toast';
|
||||||
|
|
||||||
|
export interface ApiErrorToastDetail {
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitApiErrorToast(message: string): void {
|
||||||
|
if (typeof window === 'undefined') return;
|
||||||
|
window.dispatchEvent(
|
||||||
|
new CustomEvent<ApiErrorToastDetail>(API_ERROR_TOAST_EVENT, {
|
||||||
|
detail: { message },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function subscribeApiErrorToasts(
|
||||||
|
handler: (message: string) => void,
|
||||||
|
): () => void {
|
||||||
|
if (typeof window === 'undefined') return () => {};
|
||||||
|
const listener = (event: Event) => {
|
||||||
|
const custom = event as CustomEvent<ApiErrorToastDetail>;
|
||||||
|
const message = custom.detail?.message;
|
||||||
|
if (message) handler(message);
|
||||||
|
};
|
||||||
|
window.addEventListener(API_ERROR_TOAST_EVENT, listener as EventListener);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener(API_ERROR_TOAST_EVENT, listener as EventListener);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -60,8 +60,6 @@ export function CreateCredentialPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCardFull}>
|
<Card className={styles.formCardFull}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
Notification,
|
|
||||||
Timestamp,
|
Timestamp,
|
||||||
UuidBadge,
|
UuidBadge,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
@@ -83,15 +82,6 @@ export function CredentialDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Notification
|
|
||||||
variant="error"
|
|
||||||
title={error.startsWith('404') ? t('errors.not_found') : undefined}
|
|
||||||
>
|
|
||||||
{error.startsWith('404') ? t('errors.not_found_credential', { id }) : error}
|
|
||||||
</Notification>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading && <p className={styles.muted}>{t('credentials.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('credentials.loading')}</p>}
|
||||||
|
|
||||||
{credential && (
|
{credential && (
|
||||||
|
|||||||
@@ -154,7 +154,6 @@ export function CredentialsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className={styles.muted}>{t('credentials.loading')}</p>
|
<p className={styles.muted}>{t('credentials.loading')}</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ export function EditCredentialPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading && <p className={styles.muted}>{t('credentials.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('credentials.loading')}</p>}
|
||||||
|
|
||||||
{!loading && credential && (
|
{!loading && credential && (
|
||||||
|
|||||||
@@ -67,8 +67,6 @@ export function CreateEnvironmentPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCardFull}>
|
<Card className={styles.formCardFull}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
|
|||||||
@@ -91,7 +91,6 @@ export function EditEnvironmentPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading && <p className={styles.muted}>{t('environments.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('environments.loading')}</p>}
|
||||||
|
|
||||||
{!loading && env && (
|
{!loading && env && (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Upload, Pencil, Trash2, Globe } from 'lucide-react';
|
|||||||
import { stringify as yamlStringify } from 'yaml';
|
import { stringify as yamlStringify } from 'yaml';
|
||||||
import { environments } from '../../api';
|
import { environments } from '../../api';
|
||||||
import type { Environment } from '../../api';
|
import type { Environment } from '../../api';
|
||||||
import { Breadcrumbs, Button, Card, DescriptionList, Notification, Timestamp, UuidBadge } from '../../ui';
|
import { Breadcrumbs, Button, Card, DescriptionList, Timestamp, UuidBadge } from '../../ui';
|
||||||
import styles from '../Page.module.css';
|
import styles from '../Page.module.css';
|
||||||
|
|
||||||
export function EnvironmentDetailPage() {
|
export function EnvironmentDetailPage() {
|
||||||
@@ -75,15 +75,6 @@ export function EnvironmentDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Notification
|
|
||||||
variant="error"
|
|
||||||
title={error.startsWith('404') ? t('errors.not_found') : undefined}
|
|
||||||
>
|
|
||||||
{error.startsWith('404') ? t('errors.not_found_environment', { id }) : error}
|
|
||||||
</Notification>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading && <p className={styles.muted}>{t('environments.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('environments.loading')}</p>}
|
||||||
|
|
||||||
{env && (
|
{env && (
|
||||||
|
|||||||
@@ -171,7 +171,6 @@ export function EnvironmentsPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className={styles.muted}>{t('environments.loading')}</p>
|
<p className={styles.muted}>{t('environments.loading')}</p>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -213,11 +213,6 @@ export function RunDetailPage() {
|
|||||||
<AutoRefreshIndicator active={polling} pulseKey={pulseKey} error={!!error} onClick={manualRefresh} />
|
<AutoRefreshIndicator active={polling} pulseKey={pulseKey} error={!!error} onClick={manualRefresh} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Notification variant="error">
|
|
||||||
{error.startsWith('404') ? t('errors.not_found') : error}
|
|
||||||
</Notification>
|
|
||||||
)}
|
|
||||||
{loading && <p className={styles.muted}>{t('runs.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('runs.loading')}</p>}
|
||||||
|
|
||||||
{run && (
|
{run && (
|
||||||
|
|||||||
@@ -48,8 +48,6 @@ export function CreateScenarioPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCard}>
|
<Card className={styles.formCard}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
|
|||||||
@@ -64,8 +64,6 @@ export function CreateStepPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCardFull}>
|
<Card className={styles.formCardFull}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ export function EditScenarioPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading && <p className={styles.muted}>{t('scenarios.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('scenarios.loading')}</p>}
|
||||||
|
|
||||||
{!loading && scenario && (
|
{!loading && scenario && (
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ export function EditStepPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading && <p className={styles.muted}>{t('steps.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('steps.loading')}</p>}
|
||||||
|
|
||||||
{!loading && step && (
|
{!loading && step && (
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
Card,
|
Card,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
Input,
|
Input,
|
||||||
Notification,
|
|
||||||
Select,
|
Select,
|
||||||
Table,
|
Table,
|
||||||
Timestamp,
|
Timestamp,
|
||||||
@@ -306,14 +305,6 @@ export function ScenarioDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Notification
|
|
||||||
variant="error"
|
|
||||||
title={error.startsWith('404') ? t('errors.not_found') : undefined}
|
|
||||||
>
|
|
||||||
{error.startsWith('404') ? t('errors.not_found_scenario', { id }) : error}
|
|
||||||
</Notification>
|
|
||||||
)}
|
|
||||||
{loading && <p className={styles.muted}>{t('scenarios.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('scenarios.loading')}</p>}
|
||||||
|
|
||||||
{scenario && (
|
{scenario && (
|
||||||
|
|||||||
@@ -114,7 +114,6 @@ export function ScenariosPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
Notification,
|
|
||||||
Timestamp,
|
Timestamp,
|
||||||
UuidBadge,
|
UuidBadge,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
@@ -56,14 +55,6 @@ export function SessionDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Notification
|
|
||||||
variant="error"
|
|
||||||
title={error.startsWith('404') ? t('errors.not_found') : undefined}
|
|
||||||
>
|
|
||||||
{error.startsWith('404') ? t('errors.not_found_session', { id }) : error}
|
|
||||||
</Notification>
|
|
||||||
)}
|
|
||||||
{loading && <p className={styles.muted}>{t('sessions.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('sessions.loading')}</p>}
|
||||||
|
|
||||||
{session && (
|
{session && (
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ export function SessionsPage() {
|
|||||||
<div className={styles.pageToolbar}>
|
<div className={styles.pageToolbar}>
|
||||||
<Breadcrumbs items={[{ label: t('sessions.title'), icon: <Monitor size={14} /> }]} />
|
<Breadcrumbs items={[{ label: t('sessions.title'), icon: <Monitor size={14} /> }]} />
|
||||||
</div>
|
</div>
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={items}
|
data={items}
|
||||||
|
|||||||
@@ -61,8 +61,6 @@ export function CreateSnippetPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} noValidate>
|
<form onSubmit={handleSubmit} noValidate>
|
||||||
<Card className={styles.formCardFull}>
|
<Card className={styles.formCardFull}>
|
||||||
<div className={styles.formFields}>
|
<div className={styles.formFields}>
|
||||||
|
|||||||
@@ -83,7 +83,6 @@ export function EditSnippetPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading && <p className={styles.muted}>{t('snippets.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('snippets.loading')}</p>}
|
||||||
|
|
||||||
{!loading && snippet && (
|
{!loading && snippet && (
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
DescriptionList,
|
DescriptionList,
|
||||||
Notification,
|
|
||||||
Timestamp,
|
Timestamp,
|
||||||
UuidBadge,
|
UuidBadge,
|
||||||
} from '../../ui';
|
} from '../../ui';
|
||||||
@@ -83,15 +82,6 @@ export function SnippetDetailPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
|
||||||
<Notification
|
|
||||||
variant="error"
|
|
||||||
title={error.startsWith('404') ? t('errors.not_found') : undefined}
|
|
||||||
>
|
|
||||||
{error.startsWith('404') ? t('errors.not_found_snippet', { id }) : error}
|
|
||||||
</Notification>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{loading && <p className={styles.muted}>{t('snippets.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('snippets.loading')}</p>}
|
||||||
|
|
||||||
{snippet && (
|
{snippet && (
|
||||||
|
|||||||
@@ -132,7 +132,6 @@ export function SnippetsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && <p className={styles.error}>{error}</p>}
|
|
||||||
{loading && <p className={styles.muted}>{t('snippets.loading')}</p>}
|
{loading && <p className={styles.muted}>{t('snippets.loading')}</p>}
|
||||||
|
|
||||||
{!loading && (
|
{!loading && (
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createContext, useCallback, useContext, useMemo, useState } from 'react';
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { AlertCircle, CheckCircle2, Info, X } from 'lucide-react';
|
import { AlertCircle, CheckCircle2, Info, X } from 'lucide-react';
|
||||||
import styles from './ToastProvider.module.css';
|
import styles from './ToastProvider.module.css';
|
||||||
|
import { subscribeApiErrorToasts } from '../../lib/toast-events';
|
||||||
|
|
||||||
type ToastVariant = 'success' | 'error' | 'info';
|
type ToastVariant = 'success' | 'error' | 'info';
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ export interface ToastProviderProps {
|
|||||||
export function ToastProvider({ children, durationMs = 3500 }: ToastProviderProps) {
|
export function ToastProvider({ children, durationMs = 3500 }: ToastProviderProps) {
|
||||||
const EXIT_ANIMATION_MS = 100;
|
const EXIT_ANIMATION_MS = 100;
|
||||||
const [items, setItems] = useState<ToastItem[]>([]);
|
const [items, setItems] = useState<ToastItem[]>([]);
|
||||||
|
const lastShownAtRef = useRef<Map<string, number>>(new Map());
|
||||||
|
|
||||||
const remove = useCallback((id: number) => {
|
const remove = useCallback((id: number) => {
|
||||||
setItems((prev) => prev.filter((item) => item.id !== id));
|
setItems((prev) => prev.filter((item) => item.id !== id));
|
||||||
@@ -49,6 +51,12 @@ export function ToastProvider({ children, durationMs = 3500 }: ToastProviderProp
|
|||||||
}, [remove]);
|
}, [remove]);
|
||||||
|
|
||||||
const show = useCallback((message: string, variant: ToastVariant = 'info') => {
|
const show = useCallback((message: string, variant: ToastVariant = 'info') => {
|
||||||
|
const now = Date.now();
|
||||||
|
const dedupeKey = `${variant}:${message}`;
|
||||||
|
const previousTs = lastShownAtRef.current.get(dedupeKey) ?? 0;
|
||||||
|
if (now - previousTs < 600) return;
|
||||||
|
lastShownAtRef.current.set(dedupeKey, now);
|
||||||
|
|
||||||
const id = Date.now() + Math.floor(Math.random() * 1000);
|
const id = Date.now() + Math.floor(Math.random() * 1000);
|
||||||
setItems((prev) => [...prev, { id, message, variant, closing: false }]);
|
setItems((prev) => [...prev, { id, message, variant, closing: false }]);
|
||||||
window.setTimeout(() => dismiss(id), durationMs);
|
window.setTimeout(() => dismiss(id), durationMs);
|
||||||
@@ -61,6 +69,10 @@ export function ToastProvider({ children, durationMs = 3500 }: ToastProviderProp
|
|||||||
info: (message: string) => show(message, 'info'),
|
info: (message: string) => show(message, 'info'),
|
||||||
}), [show]);
|
}), [show]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return subscribeApiErrorToasts((message) => api.error(message));
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastContext.Provider value={api}>
|
<ToastContext.Provider value={api}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/App.tsx","./src/declarations.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/api/types.ts","./src/hooks/useTheme.ts","./src/i18n/index.ts","./src/lib/hljs.ts","./src/pages/credential/CreateCredentialPage.tsx","./src/pages/credential/CredentialDetailPage.tsx","./src/pages/credential/CredentialsPage.tsx","./src/pages/credential/EditCredentialPage.tsx","./src/pages/environment/CreateEnvironmentPage.tsx","./src/pages/environment/EditEnvironmentPage.tsx","./src/pages/environment/EnvironmentDetailPage.tsx","./src/pages/environment/EnvironmentsPage.tsx","./src/pages/run/AllRunsPage.tsx","./src/pages/run/RunDetailPage.tsx","./src/pages/run/RunsPage.tsx","./src/pages/scenario/CreateScenarioPage.tsx","./src/pages/scenario/CreateStepPage.tsx","./src/pages/scenario/EditScenarioPage.tsx","./src/pages/scenario/EditStepPage.tsx","./src/pages/scenario/ScenarioDetailPage.tsx","./src/pages/scenario/ScenariosPage.tsx","./src/pages/session/SessionDetailPage.tsx","./src/pages/session/SessionsPage.tsx","./src/pages/snippet/CreateSnippetPage.tsx","./src/pages/snippet/EditSnippetPage.tsx","./src/pages/snippet/SnippetDetailPage.tsx","./src/pages/snippet/SnippetsPage.tsx","./src/ui/index.ts","./src/ui/AutoRefreshIndicator/AutoRefreshIndicator.tsx","./src/ui/Badge/Badge.tsx","./src/ui/Breadcrumbs/Breadcrumbs.tsx","./src/ui/Button/Button.tsx","./src/ui/Card/Card.tsx","./src/ui/CodeBlock/CodeBlock.tsx","./src/ui/CodeEditor/CodeEditor.tsx","./src/ui/ContextMenu/ContextMenu.tsx","./src/ui/DescriptionList/DescriptionList.tsx","./src/ui/Input/Input.tsx","./src/ui/Notification/Notification.tsx","./src/ui/Pagination/Pagination.tsx","./src/ui/Search/Search.tsx","./src/ui/Select/Select.tsx","./src/ui/SidePanel/SidePanel.tsx","./src/ui/Table/Table.tsx","./src/ui/Textarea/Textarea.tsx","./src/ui/ThemeSwitcher/ThemeSwitcher.tsx","./src/ui/Timestamp/Timestamp.tsx","./src/ui/Toast/ToastProvider.tsx","./src/ui/UuidBadge/UuidBadge.tsx"],"version":"5.8.3"}
|
{"root":["./src/App.tsx","./src/declarations.d.ts","./src/main.tsx","./src/api/client.ts","./src/api/index.ts","./src/api/types.ts","./src/hooks/useTheme.ts","./src/i18n/index.ts","./src/lib/hljs.ts","./src/lib/toast-events.ts","./src/pages/credential/CreateCredentialPage.tsx","./src/pages/credential/CredentialDetailPage.tsx","./src/pages/credential/CredentialsPage.tsx","./src/pages/credential/EditCredentialPage.tsx","./src/pages/environment/CreateEnvironmentPage.tsx","./src/pages/environment/EditEnvironmentPage.tsx","./src/pages/environment/EnvironmentDetailPage.tsx","./src/pages/environment/EnvironmentsPage.tsx","./src/pages/run/AllRunsPage.tsx","./src/pages/run/RunDetailPage.tsx","./src/pages/run/RunsPage.tsx","./src/pages/scenario/CreateScenarioPage.tsx","./src/pages/scenario/CreateStepPage.tsx","./src/pages/scenario/EditScenarioPage.tsx","./src/pages/scenario/EditStepPage.tsx","./src/pages/scenario/ScenarioDetailPage.tsx","./src/pages/scenario/ScenariosPage.tsx","./src/pages/session/SessionDetailPage.tsx","./src/pages/session/SessionsPage.tsx","./src/pages/snippet/CreateSnippetPage.tsx","./src/pages/snippet/EditSnippetPage.tsx","./src/pages/snippet/SnippetDetailPage.tsx","./src/pages/snippet/SnippetsPage.tsx","./src/ui/index.ts","./src/ui/AutoRefreshIndicator/AutoRefreshIndicator.tsx","./src/ui/Badge/Badge.tsx","./src/ui/Breadcrumbs/Breadcrumbs.tsx","./src/ui/Button/Button.tsx","./src/ui/Card/Card.tsx","./src/ui/CodeBlock/CodeBlock.tsx","./src/ui/CodeEditor/CodeEditor.tsx","./src/ui/ContextMenu/ContextMenu.tsx","./src/ui/DescriptionList/DescriptionList.tsx","./src/ui/Input/Input.tsx","./src/ui/Notification/Notification.tsx","./src/ui/Pagination/Pagination.tsx","./src/ui/Search/Search.tsx","./src/ui/Select/Select.tsx","./src/ui/SidePanel/SidePanel.tsx","./src/ui/Table/Table.tsx","./src/ui/Textarea/Textarea.tsx","./src/ui/ThemeSwitcher/ThemeSwitcher.tsx","./src/ui/Timestamp/Timestamp.tsx","./src/ui/Toast/ToastProvider.tsx","./src/ui/UuidBadge/UuidBadge.tsx"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user