完善客户端模块与工作区能力
This commit is contained in:
@@ -2,21 +2,28 @@
|
||||
* Settings Page
|
||||
* Makelore application configuration.
|
||||
*/
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useState, type FormEvent } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import {
|
||||
ChevronDown,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
FolderPlus,
|
||||
Play,
|
||||
KeyRound,
|
||||
RefreshCw,
|
||||
RotateCw,
|
||||
Square,
|
||||
} from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DisclosureContent } from '@/components/ui/disclosure';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
@@ -38,11 +45,6 @@ import { useSettingsStore } from '@/stores/settings';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
function getHealthCheckedLabel(value: number | null): string {
|
||||
if (!value) return 'Health not checked yet';
|
||||
return `Health checked ${new Date(value).toLocaleTimeString()}`;
|
||||
}
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation('settings');
|
||||
const navigate = useNavigate();
|
||||
@@ -64,24 +66,20 @@ export function Settings() {
|
||||
setProxyAllServer,
|
||||
setProxyBypassRules,
|
||||
devModeUnlocked,
|
||||
setDevModeUnlocked,
|
||||
unlockDevMode,
|
||||
lockDevMode,
|
||||
telemetryEnabled,
|
||||
setTelemetryEnabled,
|
||||
} = useSettingsStore();
|
||||
|
||||
const opencodeStatus = useOpencodeStore((state) => state.status);
|
||||
const opencodeHealth = useOpencodeStore((state) => state.health);
|
||||
const opencodeHealthCheckedAt = useOpencodeStore((state) => state.healthCheckedAt);
|
||||
const opencodeRuntimeConfigSummary = useOpencodeStore((state) => state.runtimeConfigSummary);
|
||||
const opencodeProjects = useOpencodeStore((state) => state.projects);
|
||||
const activeOpencodeProject = useOpencodeStore((state) => state.activeProject);
|
||||
const opencodeLoading = useOpencodeStore((state) => state.loading);
|
||||
const opencodeError = useOpencodeStore((state) => state.error);
|
||||
const refreshOpencodeStatus = useOpencodeStore((state) => state.refreshStatus);
|
||||
const startOpencodeRuntime = useOpencodeStore((state) => state.start);
|
||||
const stopOpencodeRuntime = useOpencodeStore((state) => state.stop);
|
||||
const restartOpencodeRuntime = useOpencodeStore((state) => state.restart);
|
||||
const checkOpencodeHealth = useOpencodeStore((state) => state.checkHealth);
|
||||
const loadOpencodeRuntimeConfigSummary = useOpencodeStore(
|
||||
(state) => state.loadRuntimeConfigSummary
|
||||
);
|
||||
@@ -102,6 +100,10 @@ export function Settings() {
|
||||
const [showTelemetryViewer, setShowTelemetryViewer] = useState(false);
|
||||
const [telemetryEntries, setTelemetryEntries] = useState<UiTelemetryEntry[]>([]);
|
||||
const [showAdditionalSettings, setShowAdditionalSettings] = useState(false);
|
||||
const [adminDialogOpen, setAdminDialogOpen] = useState(false);
|
||||
const [adminPassword, setAdminPassword] = useState('');
|
||||
const [adminPasswordError, setAdminPasswordError] = useState<string | null>(null);
|
||||
const [adminUnlocking, setAdminUnlocking] = useState(false);
|
||||
const [workspaceProjectError, setWorkspaceProjectError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -146,10 +148,16 @@ export function Settings() {
|
||||
useEffect(() => {
|
||||
void Promise.allSettled([
|
||||
refreshOpencodeStatus(),
|
||||
]);
|
||||
}, [refreshOpencodeStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!devModeUnlocked) return;
|
||||
void Promise.allSettled([
|
||||
loadOpencodeRuntimeConfigSummary(),
|
||||
loadOpencodeProjects(),
|
||||
]);
|
||||
}, [loadOpencodeProjects, loadOpencodeRuntimeConfigSummary, refreshOpencodeStatus]);
|
||||
}, [devModeUnlocked, loadOpencodeProjects, loadOpencodeRuntimeConfigSummary]);
|
||||
|
||||
const proxySettingsDirty = useMemo(() => {
|
||||
return (
|
||||
@@ -213,6 +221,43 @@ export function Settings() {
|
||||
]);
|
||||
};
|
||||
|
||||
const handleRevealAdditionalSettings = () => {
|
||||
if (showAdditionalSettings) {
|
||||
setShowAdditionalSettings(false);
|
||||
return;
|
||||
}
|
||||
if (devModeUnlocked) {
|
||||
setShowAdditionalSettings(true);
|
||||
return;
|
||||
}
|
||||
setAdminPassword('');
|
||||
setAdminPasswordError(null);
|
||||
setAdminDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleUnlockDevMode = async (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault();
|
||||
setAdminUnlocking(true);
|
||||
setAdminPasswordError(null);
|
||||
try {
|
||||
const unlocked = await unlockDevMode(adminPassword);
|
||||
if (!unlocked) {
|
||||
setAdminPasswordError(t('admin.invalidPassword', 'The administrator password is incorrect.'));
|
||||
return;
|
||||
}
|
||||
setAdminDialogOpen(false);
|
||||
setAdminPassword('');
|
||||
setShowAdditionalSettings(true);
|
||||
} finally {
|
||||
setAdminUnlocking(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleLockDevMode = async () => {
|
||||
await lockDevMode();
|
||||
setShowAdditionalSettings(false);
|
||||
};
|
||||
|
||||
const handleAddOpencodeProject = async () => {
|
||||
const project = await pickAndOpenOpencodeProject();
|
||||
if (!project) return;
|
||||
@@ -267,7 +312,7 @@ export function Settings() {
|
||||
<p className="mt-1 text-sm text-muted-foreground">{t('subtitle')}</p>
|
||||
</div>
|
||||
|
||||
<section className="space-y-5">
|
||||
<section className="space-y-5" data-testid="settings-general-section">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-lg font-medium">{t('appearance.title')}</h2>
|
||||
@@ -279,7 +324,7 @@ export function Settings() {
|
||||
aria-label={t('appearance.showMoreSettings')}
|
||||
aria-expanded={showAdditionalSettings}
|
||||
data-testid="settings-reveal-additional"
|
||||
onClick={() => setShowAdditionalSettings((value) => !value)}
|
||||
onClick={handleRevealAdditionalSettings}
|
||||
>
|
||||
<ChevronDown
|
||||
className={cn(
|
||||
@@ -317,7 +362,119 @@ export function Settings() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<DisclosureContent open={showAdditionalSettings} innerClassName="space-y-8">
|
||||
<section className="space-y-5" data-testid="settings-runtime-summary">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">Runtime</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Check the local runtime status or restart it when needed.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={opencodeStatus.state === 'running' ? 'default' : 'secondary'}>
|
||||
{opencodeStatus.state}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center justify-between gap-4 rounded-lg border bg-card p-4">
|
||||
<div className="flex min-w-0 flex-wrap items-center gap-x-6 gap-y-2 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Status</div>
|
||||
<div className="font-medium">{opencodeStatus.state}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Port</div>
|
||||
<div className="font-medium">{opencodeStatus.port}</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void restartOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Restart runtime"
|
||||
data-testid="settings-runtime-restart"
|
||||
>
|
||||
<RotateCw className={cn('mr-2 h-4 w-4', opencodeLoading && 'animate-spin')} />
|
||||
Restart runtime
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{(opencodeStatus.error || opencodeError) && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{opencodeStatus.error ?? opencodeError}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5" data-testid="settings-updates">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{t('updates.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('updates.description')}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<UpdateSettings />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Dialog
|
||||
open={adminDialogOpen}
|
||||
onOpenChange={(open) => {
|
||||
setAdminDialogOpen(open);
|
||||
if (!open) {
|
||||
setAdminPassword('');
|
||||
setAdminPasswordError(null);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent data-testid="settings-admin-password-dialog" className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<KeyRound className="h-5 w-5 text-brand" />
|
||||
{t('admin.title', '管理员验证')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{t('admin.description', '请输入管理员密码以管理开发者设置。')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<form className="space-y-4" onSubmit={handleUnlockDevMode}>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="settings-admin-password">{t('admin.password', '管理员密码')}</Label>
|
||||
<Input
|
||||
id="settings-admin-password"
|
||||
type="password"
|
||||
value={adminPassword}
|
||||
onChange={(event) => setAdminPassword(event.target.value)}
|
||||
autoFocus
|
||||
autoComplete="current-password"
|
||||
data-testid="settings-admin-password-input"
|
||||
aria-invalid={Boolean(adminPasswordError)}
|
||||
/>
|
||||
{adminPasswordError && (
|
||||
<p className="text-sm text-destructive" role="alert">
|
||||
{adminPasswordError}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button type="button" variant="outline" onClick={() => setAdminDialogOpen(false)}>
|
||||
{t('admin.cancel', '取消')}
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={!adminPassword.trim() || adminUnlocking}
|
||||
data-testid="settings-admin-unlock-button"
|
||||
>
|
||||
{adminUnlocking && <RefreshCw className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{t('admin.unlock', '解锁管理设置')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{showAdditionalSettings && devModeUnlocked && (
|
||||
<DisclosureContent open innerClassName="space-y-8">
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5" data-testid="settings-opencode-workspace">
|
||||
@@ -340,123 +497,40 @@ export function Settings() {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
||||
<div
|
||||
className="space-y-4 rounded-lg border bg-card p-4"
|
||||
data-testid="settings-opencode-runtime"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Runtime</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local runtime process used by Makelore sessions.
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={opencodeStatus.state === 'running' ? 'default' : 'secondary'}>
|
||||
{opencodeStatus.state}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 sm:grid-cols-2">
|
||||
{opencodeStatus.state === 'running' ? (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void restartOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Restart runtime"
|
||||
>
|
||||
<RotateCw className="mr-2 h-4 w-4" />
|
||||
Restart runtime
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void stopOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Stop runtime"
|
||||
>
|
||||
<Square className="mr-2 h-4 w-4" />
|
||||
Stop runtime
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<Button
|
||||
onClick={() => void startOpencodeRuntime()}
|
||||
disabled={opencodeLoading}
|
||||
aria-label="Start runtime"
|
||||
>
|
||||
<Play className="mr-2 h-4 w-4" />
|
||||
Start runtime
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
className={opencodeStatus.state === 'running' ? 'sm:col-span-2' : undefined}
|
||||
onClick={() => void checkOpencodeHealth()}
|
||||
disabled={opencodeLoading || opencodeStatus.state !== 'running'}
|
||||
aria-label="Check health"
|
||||
>
|
||||
Check health
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Port</span>
|
||||
<span className="font-medium">{opencodeStatus.port}</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Health</span>
|
||||
<span className="font-medium">
|
||||
{opencodeHealth ? (opencodeHealth.ok ? 'Healthy' : 'Unhealthy') : 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{getHealthCheckedLabel(opencodeHealthCheckedAt)}
|
||||
</div>
|
||||
{(opencodeStatus.error || opencodeError) && (
|
||||
<div className="rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive">
|
||||
{opencodeStatus.error ?? opencodeError}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="space-y-4 rounded-lg border bg-card p-4"
|
||||
data-testid="settings-opencode-models"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Models</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Resolved from the runtime configuration.
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate('/models')}>
|
||||
Configure models
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="space-y-4 rounded-lg border bg-card p-4"
|
||||
data-testid="settings-opencode-models"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Models</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Resolved from the runtime configuration.
|
||||
</p>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Default model</div>
|
||||
<div className="break-all font-medium">
|
||||
{opencodeRuntimeConfigSummary?.model ?? 'No configured model'}
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={() => navigate('/models')}>
|
||||
Configure models
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Default model</div>
|
||||
<div className="break-all font-medium">
|
||||
{opencodeRuntimeConfigSummary?.model ?? 'No configured model'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Small model</div>
|
||||
<div className="break-all font-medium">
|
||||
{opencodeRuntimeConfigSummary?.smallModel ?? 'No configured small model'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Providers</span>
|
||||
<span className="font-medium">
|
||||
{`${opencodeRuntimeConfigSummary?.providerCount ?? 0} providers`}
|
||||
</span>
|
||||
<div>
|
||||
<div className="text-xs text-muted-foreground">Small model</div>
|
||||
<div className="break-all font-medium">
|
||||
{opencodeRuntimeConfigSummary?.smallModel ?? 'No configured small model'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-muted-foreground">Providers</span>
|
||||
<span className="font-medium">
|
||||
{`${opencodeRuntimeConfigSummary?.providerCount ?? 0} providers`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -621,34 +695,25 @@ export function Settings() {
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{t('updates.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('updates.description')}</p>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<UpdateSettings />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
<section className="space-y-5">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{t('advanced.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('advanced.description')}</p>
|
||||
<section className="space-y-5" data-testid="settings-advanced-section">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-medium">{t('advanced.title')}</h2>
|
||||
<p className="text-sm text-muted-foreground">{t('advanced.description')}</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => void handleLockDevMode()}>
|
||||
{t('admin.lock', '锁定')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-card p-4">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">{t('advanced.devMode')}</Label>
|
||||
<p className="text-sm text-muted-foreground">{t('advanced.devModeDesc')}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('advanced.sessionUnlocked', '管理员权限已在本次应用运行期间解锁。')}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
data-testid="settings-dev-mode-switch"
|
||||
checked={devModeUnlocked}
|
||||
onCheckedChange={setDevModeUnlocked}
|
||||
/>
|
||||
<Badge variant="secondary">{t('admin.unlocked', '已解锁')}</Badge>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border bg-card p-4">
|
||||
@@ -679,48 +744,47 @@ export function Settings() {
|
||||
</div>
|
||||
</div>
|
||||
<DisclosureContent open={showLogs} className="mt-4" innerClassName="max-h-80 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-3 text-xs">
|
||||
{logContent || '(No logs available yet)'}
|
||||
{logContent || '(No logs available yet)'}
|
||||
</DisclosureContent>
|
||||
</div>
|
||||
|
||||
{devModeUnlocked && (
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Telemetry Viewer</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local-only UI and performance events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowTelemetryViewer((value) => !value)}
|
||||
>
|
||||
{showTelemetryViewer ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
clearUiTelemetry();
|
||||
setTelemetryEntries([]);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
<div className="rounded-lg border bg-card p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<Label className="text-sm font-medium">Telemetry Viewer</Label>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Local-only UI and performance events.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setShowTelemetryViewer((value) => !value)}
|
||||
>
|
||||
{showTelemetryViewer ? 'Hide' : 'Show'}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
clearUiTelemetry();
|
||||
setTelemetryEntries([]);
|
||||
}}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
<DisclosureContent open={showTelemetryViewer} className="mt-4" innerClassName="max-h-80 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-3 text-xs">
|
||||
{telemetryEntries.length > 0
|
||||
? JSON.stringify(telemetryEntries, null, 2)
|
||||
: '(No telemetry entries)'}
|
||||
</DisclosureContent>
|
||||
</div>
|
||||
)}
|
||||
<DisclosureContent open={showTelemetryViewer} className="mt-4" innerClassName="max-h-80 overflow-auto whitespace-pre-wrap rounded-md bg-muted p-3 text-xs">
|
||||
{telemetryEntries.length > 0
|
||||
? JSON.stringify(telemetryEntries, null, 2)
|
||||
: '(No telemetry entries)'}
|
||||
</DisclosureContent>
|
||||
</div>
|
||||
</section>
|
||||
</DisclosureContent>
|
||||
</DisclosureContent>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user