This commit is contained in:
2026-07-01 09:22:26 +00:00
parent 4aa1dab283
commit aa47a19e98
53 changed files with 4721 additions and 77 deletions
@@ -0,0 +1,157 @@
import { useCallback, useEffect, useState } from 'react';
import { BarChart3, Plus, Loader2, ListChecks } from 'lucide-react';
import { usePluginContext } from './client';
import { CredentialPanel } from './CredentialPanel';
import { NewJobDialog } from './NewJobDialog';
import { JobDetail } from './JobDetail';
import type { S3Credential, AnalysisJob } from './types';
const API_BASE = '/api/v1/plugin/data_analytics';
interface Props {
pluginName: string;
}
export function Dashboard({ pluginName }: Props) {
const { client } = usePluginContext();
const [credentials, setCredentials] = useState<S3Credential[]>([]);
const [credLoading, setCredLoading] = useState(true);
const [jobs, setJobs] = useState<AnalysisJob[]>([]);
const [jobLoading, setJobLoading] = useState(true);
const [showNewJob, setShowNewJob] = useState(false);
const [openJobId, setOpenJobId] = useState<string | null>(null);
const [error, setError] = useState('');
const loadCredentials = useCallback(async () => {
setCredLoading(true);
try {
const resp = await client.get<{ credentials: S3Credential[] }>(`${API_BASE}/credentials`);
setCredentials(resp.data.credentials || []);
} catch (e: unknown) {
const msg = (e as { response?: { data?: { detail?: string } } }).response?.data?.detail;
setError(msg || '加载凭证失败');
} finally {
setCredLoading(false);
}
}, [client]);
const loadJobs = useCallback(async () => {
setJobLoading(true);
try {
const resp = await client.get<{ jobs: AnalysisJob[] }>(`${API_BASE}/jobs`);
setJobs(resp.data.jobs || []);
} catch (e: unknown) {
const msg = (e as { response?: { data?: { detail?: string } } }).response?.data?.detail;
setError(msg || '加载任务失败');
} finally {
setJobLoading(false);
}
}, [client]);
useEffect(() => {
loadCredentials();
loadJobs();
}, [loadCredentials, loadJobs]);
// 轮询任务列表,方便看状态变化
useEffect(() => {
const t = setInterval(loadJobs, 5000);
return () => clearInterval(t);
}, [loadJobs]);
return (
<div className="h-full overflow-y-auto p-6 bg-bg-base">
<div className="max-w-5xl mx-auto space-y-6">
<div className="flex items-center gap-3 pb-2">
<div className="w-10 h-10 rounded-xl bg-accent-light text-accent flex items-center justify-center">
<BarChart3 size={20} />
</div>
<div>
<h2 className="text-lg font-bold text-text-primary"></h2>
<p className="text-xs text-text-muted mt-0.5">
S3 agent python_executor / ray_submit{' '}
<span className="font-mono text-[10px] opacity-70">{pluginName}</span>
</p>
</div>
</div>
{error && (
<div className="p-3 bg-danger-bg text-danger text-sm rounded-xl border border-danger/20">{error}</div>
)}
<CredentialPanel credentials={credentials} loading={credLoading} onChanged={loadCredentials} />
<div className="bg-bg-card rounded-2xl border border-border-primary p-5">
<div className="flex items-center justify-between mb-4">
<div>
<h3 className="font-semibold text-text-primary flex items-center gap-2">
<ListChecks size={16} className="text-accent" />
</h3>
<p className="text-xs text-text-muted mt-0.5"> 5 </p>
</div>
<button
className="px-3 py-1.5 text-xs rounded-lg bg-accent text-white hover:opacity-90 transition flex items-center gap-1.5 disabled:opacity-50"
onClick={() => setShowNewJob(true)}
disabled={credentials.length === 0}
title={credentials.length === 0 ? '请先添加凭证' : ''}
>
<Plus size={14} />
</button>
</div>
{jobLoading && jobs.length === 0 ? (
<div className="flex items-center justify-center py-8 text-text-muted">
<Loader2 size={20} className="animate-spin" />
</div>
) : jobs.length === 0 ? (
<div className="text-sm text-text-muted text-center py-8 border border-dashed border-border-primary rounded-xl">
</div>
) : (
<div className="space-y-2">
{jobs.map((j) => (
<button
key={j.job_id}
className="w-full text-left p-3 bg-bg-secondary rounded-xl border border-border-secondary hover:border-accent transition flex items-center justify-between gap-3"
onClick={() => setOpenJobId(j.job_id)}
>
<div className="min-w-0 flex-1">
<div className="text-sm text-text-primary truncate">{j.description}</div>
<div className="text-[11px] text-text-muted mt-0.5 font-mono">
{j.job_id.slice(0, 8)} · {j.created_at?.slice(0, 19).replace('T', ' ')}
</div>
</div>
<StatusBadge status={j.status} />
</button>
))}
</div>
)}
</div>
</div>
{showNewJob && (
<NewJobDialog
credentials={credentials}
onClose={() => setShowNewJob(false)}
onCreated={loadJobs}
/>
)}
{openJobId && <JobDetail jobId={openJobId} onClose={() => setOpenJobId(null)} />}
</div>
);
}
function StatusBadge({ status }: { status: string }) {
const map: Record<string, string> = {
pending: 'bg-bg-base text-text-muted border-border-primary',
running: 'bg-warning-bg text-warning border-warning/20',
completed: 'bg-success-bg text-success border-success/20',
failed: 'bg-danger-bg text-danger border-danger/20',
};
const cls = map[status] || map.pending;
return (
<span className={`text-[10px] font-medium px-2 py-1 rounded-lg border ${cls} shrink-0`}>
{status}
</span>
);
}