refactor: Deployment详情页优化 — 查看/编辑/YAML模式精简, 副本数移至Tab栏, 伸缩改HPA+CronHPA
This commit is contained in:
1 parent
d52fe8749d
commit
be9db65a22
3 files changed
+167
-37
No files matched your search
@@ -35,10 +35,11 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
hostNetwork: true
|
hostNetwork: true
|
||||||
dnsPolicy: ClusterFirstWithHostNet
|
dnsPolicy: ClusterFirstWithHostNet
|
||||||
|
enableServiceLinks: false
|
||||||
containers:
|
containers:
|
||||||
- name: aiops
|
- name: aiops
|
||||||
image: aiops-aiops:latest
|
image: 10.10.10.72/library/aiops:embed
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: Always
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
containerPort: 8080
|
containerPort: 8080
|
||||||
|
|||||||
@@ -78,6 +78,10 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
const [events, setEvents] = useState<EventItem[]>([]);
|
const [events, setEvents] = useState<EventItem[]>([]);
|
||||||
const [tabLoading, setTabLoading] = useState('');
|
const [tabLoading, setTabLoading] = useState('');
|
||||||
|
|
||||||
|
// HPA / CronHPA state
|
||||||
|
const [hpas, setHpas] = useState<any[]>([]);
|
||||||
|
const [cronHpas, setCronHpas] = useState<any[]>([]);
|
||||||
|
|
||||||
const ready = deployment?.ready_replicas ?? deployment?.readyReplicas ?? 0;
|
const ready = deployment?.ready_replicas ?? deployment?.readyReplicas ?? 0;
|
||||||
const total = deployment?.replicas ?? 0;
|
const total = deployment?.replicas ?? 0;
|
||||||
const isHealthy = ready === total && total > 0;
|
const isHealthy = ready === total && total > 0;
|
||||||
@@ -155,6 +159,22 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
}).catch(() => {}).finally(() => setTabLoading(''));
|
}).catch(() => {}).finally(() => setTabLoading(''));
|
||||||
}, [tab, clusterCode, namespace, deployment?.name]);
|
}, [tab, clusterCode, namespace, deployment?.name]);
|
||||||
|
|
||||||
|
// Load HPA and CronHPA for "伸缩" tab
|
||||||
|
useEffect(() => {
|
||||||
|
if (tab !== 'scale') return;
|
||||||
|
setTabLoading('scale');
|
||||||
|
Promise.all([
|
||||||
|
kubernetesApi.getHPAs(clusterCode, namespace).catch(() => []),
|
||||||
|
Promise.resolve([]), // CronHPA 待后端支持
|
||||||
|
]).then(([hpaRes]) => {
|
||||||
|
const toArr = (v: any) => (Array.isArray(v) ? v : (v?.list || v?.items || v?.data || []));
|
||||||
|
const allHpas = toArr(hpaRes);
|
||||||
|
const depName = deployment?.name || '';
|
||||||
|
setHpas(allHpas.filter((h: any) => (h.scaleTargetRef?.name || h.targetRef?.name || '') === depName));
|
||||||
|
setCronHpas([]);
|
||||||
|
}).catch(() => {}).finally(() => setTabLoading(''));
|
||||||
|
}, [tab, clusterCode, namespace, deployment?.name]);
|
||||||
|
|
||||||
// Rollback
|
// Rollback
|
||||||
const handleRollback = async (rev: any) => {
|
const handleRollback = async (rev: any) => {
|
||||||
const revision = rev.revision ?? rev.revision_number ?? rev.number;
|
const revision = rev.revision ?? rev.revision_number ?? rev.number;
|
||||||
@@ -223,7 +243,7 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* ============ Tab Bar ============ */}
|
{/* ============ Tab Bar + 副本数 ============ */}
|
||||||
<div className="flex items-center gap-0.5 border-b border-[var(--dg-border)] overflow-x-auto">
|
<div className="flex items-center gap-0.5 border-b border-[var(--dg-border)] overflow-x-auto">
|
||||||
{TABS.map(t => (
|
{TABS.map(t => (
|
||||||
<button
|
<button
|
||||||
@@ -240,6 +260,30 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
{t.key === 'pods' && <Badge className="ml-1 text-[10px] px-1.5 py-0 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]">{t.count}</Badge>}
|
{t.key === 'pods' && <Badge className="ml-1 text-[10px] px-1.5 py-0 bg-[var(--dg-bg-tertiary)] text-[var(--dg-text-muted)]">{t.count}</Badge>}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
{/* 副本数控制 — Tab 行右侧 */}
|
||||||
|
<div className="ml-auto flex items-center gap-1 pr-2 shrink-0">
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||||
|
disabled={scaleLoading || scaleValue <= 0}
|
||||||
|
onClick={() => setScaleValue((v: number) => Math.max(0, v - 1))}>
|
||||||
|
<Minus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<Input
|
||||||
|
type="number" min={0} value={scaleValue}
|
||||||
|
onChange={e => setScaleValue(Math.max(0, Number(e.target.value)))}
|
||||||
|
className="w-16 h-7 text-center text-sm font-bold bg-slate-800 border-slate-700 text-slate-200"
|
||||||
|
/>
|
||||||
|
<Button variant="ghost" size="icon" className="h-7 w-7 text-[var(--dg-text-muted)] hover:text-[var(--dg-text-primary)]"
|
||||||
|
disabled={scaleLoading}
|
||||||
|
onClick={() => setScaleValue((v: number) => v + 1)}>
|
||||||
|
<Plus className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
{scaleValue !== total && (
|
||||||
|
<Button size="sm" className="h-7 text-xs bg-[var(--dg-blue)] hover:bg-[var(--dg-blue-hover)]"
|
||||||
|
disabled={scaleLoading} onClick={handleScale}>
|
||||||
|
{scaleLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : '应用'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ============ Tab Content ============ */}
|
{/* ============ Tab Content ============ */}
|
||||||
@@ -247,12 +291,11 @@ export function DeploymentView(props: DeploymentViewProps) {
|
|||||||
{tab === 'services' && <ServicesTab services={services} ingresses={ingresses} loading={tabLoading} namespace={namespace} />}
|
{tab === 'services' && <ServicesTab services={services} ingresses={ingresses} loading={tabLoading} namespace={namespace} />}
|
||||||
{tab === 'events' && <EventsTab events={events} loading={tabLoading} />}
|
{tab === 'events' && <EventsTab events={events} loading={tabLoading} />}
|
||||||
{tab === 'scale' && (
|
{tab === 'scale' && (
|
||||||
<ScaleTab
|
<HPACronHPATab
|
||||||
current={total}
|
hpas={hpas}
|
||||||
value={scaleValue}
|
cronHpas={cronHpas}
|
||||||
onChange={setScaleValue}
|
loading={tabLoading}
|
||||||
onApply={handleScale}
|
replicas={total}
|
||||||
loading={scaleLoading}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tab === 'history' && <HistoryTab revisions={revisions} rollbackLoading={rollbackLoading} onRollback={handleRollback} />}
|
{tab === 'history' && <HistoryTab revisions={revisions} rollbackLoading={rollbackLoading} onRollback={handleRollback} />}
|
||||||
@@ -506,33 +549,122 @@ function EventsTab({ events, loading }: { events: EventItem[]; loading: string }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════ TAB: 容器伸缩 ═══════════════
|
// ═══════════════ TAB: 伸缩 (HPA + CronHPA) ═══════════════
|
||||||
function ScaleTab({ current, value, onChange, onApply, loading }: { current: number; value: number; onChange: (v: number) => void; onApply: () => void; loading: boolean }) {
|
function HPACronHPATab({ hpas, cronHpas, loading, replicas }: { hpas: any[]; cronHpas: any[]; loading: string; replicas: number }) {
|
||||||
|
if (loading === 'scale') {
|
||||||
|
return <div className="flex items-center justify-center py-12"><Loader2 className="h-6 w-6 animate-spin text-[var(--dg-text-muted)]" /></div>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* 当前副本数提示 */}
|
||||||
|
<div className="flex items-center gap-2 px-2">
|
||||||
|
<span className="text-sm text-[var(--dg-text-muted)]">当前副本:</span>
|
||||||
|
<span className="text-sm font-bold text-[var(--dg-text-primary)]">{replicas}</span>
|
||||||
|
<span className="text-xs text-[var(--dg-text-muted)]">(可通过 Tab 栏右侧 +/- 手动调整)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* HPA 策略 */}
|
||||||
<Card className="dg-card">
|
<Card className="dg-card">
|
||||||
<CardContent className="p-6">
|
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Activity className="h-4 w-4 text-[var(--dg-orange)]" />HPA — 水平自动伸缩</CardTitle></CardHeader>
|
||||||
<div className="max-w-sm mx-auto text-center space-y-4">
|
<CardContent>
|
||||||
<p className="text-sm text-[var(--dg-text-muted)]">当前副本数: <span className="font-bold text-[var(--dg-text-primary)]">{current}</span></p>
|
{hpas.length === 0 ? (
|
||||||
<div className="flex items-center justify-center gap-4">
|
<div className="text-sm text-[var(--dg-text-muted)] py-4">
|
||||||
<Button variant="outline" size="icon" className="h-10 w-10" disabled={loading || value <= 0} onClick={() => onChange(Math.max(0, value - 1))}>
|
未关联 HPA — <Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">创建 HPA</Button>
|
||||||
<Minus className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
<Input
|
|
||||||
type="number" min={0} value={value}
|
|
||||||
onChange={e => onChange(Math.max(0, Number(e.target.value)))}
|
|
||||||
className="w-28 text-center text-2xl font-bold"
|
|
||||||
/>
|
|
||||||
<Button variant="outline" size="icon" className="h-10 w-10" disabled={loading} onClick={() => onChange(value + 1)}>
|
|
||||||
<Plus className="h-5 w-5" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<Button className="w-full" onClick={onApply} disabled={loading}>
|
) : (
|
||||||
{loading ? <Loader2 className="h-4 w-4 animate-spin mr-2" /> : null}
|
<div className="overflow-x-auto">
|
||||||
确认伸缩
|
<Table>
|
||||||
</Button>
|
<TableHeader>
|
||||||
|
<TableRow className="border-[var(--dg-border)]">
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">最小/最大</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">指标</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">当前负载</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">状态</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{hpas.map((hpa: any, i: number) => {
|
||||||
|
const minR = hpa.spec?.minReplicas ?? hpa.minReplicas ?? 1;
|
||||||
|
const maxR = hpa.spec?.maxReplicas ?? hpa.maxReplicas ?? 10;
|
||||||
|
const metrics = hpa.spec?.metrics || hpa.metrics || [];
|
||||||
|
const curR = hpa.status?.currentReplicas ?? hpa.currentReplicas ?? 0;
|
||||||
|
const conditions = hpa.status?.conditions || hpa.conditions || [];
|
||||||
|
const ableToScale = conditions.find((c: any) => c.type === 'AbleToScale');
|
||||||
|
const ok = ableToScale?.status === 'True';
|
||||||
|
return (
|
||||||
|
<TableRow key={hpa.name || i} className="border-[var(--dg-border)]">
|
||||||
|
<TableCell className="font-medium text-[var(--dg-text-primary)]">{hpa.name || hpa.metadata?.name}</TableCell>
|
||||||
|
<TableCell className="text-sm text-[var(--dg-text-secondary)] font-mono">{minR} → {maxR}</TableCell>
|
||||||
|
<TableCell className="text-xs text-[var(--dg-text-muted)]">
|
||||||
|
{metrics.map((m: any, j: number) => {
|
||||||
|
const resource = m.resource || m.object?.metric?.name || '';
|
||||||
|
const target = m.resource?.target?.averageUtilization || m.resource?.target?.averageValue || m.resource?.target?.type || '-';
|
||||||
|
return <div key={j}>{resource?.name || resource}: {target}{m.resource?.target?.averageUtilization ? '%' : ''}</div>;
|
||||||
|
})}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-sm font-mono text-[var(--dg-text-secondary)]">{curR}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className={ok ? 'bg-[var(--dg-green)]/20 text-[var(--dg-green)]' : 'bg-[var(--dg-red)]/20 text-[var(--dg-red)]'}>
|
||||||
|
{ok ? '正常' : '异常'}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
{/* CronHPA 策略 */}
|
||||||
|
<Card className="dg-card">
|
||||||
|
<CardHeader className="pb-2"><CardTitle className="text-sm flex items-center gap-2"><Clock className="h-4 w-4 text-[var(--dg-blue)]" />CronHPA — 定时伸缩</CardTitle></CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{cronHpas.length === 0 ? (
|
||||||
|
<div className="text-sm text-[var(--dg-text-muted)] py-4">
|
||||||
|
未关联 CronHPA — <Button variant="link" className="h-auto p-0 text-[var(--dg-blue)] text-sm">创建 CronHPA</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="border-[var(--dg-border)]">
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">名称</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">目标副本</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">调度规则</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">时区</TableHead>
|
||||||
|
<TableHead className="text-[var(--dg-text-muted)]">状态</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{cronHpas.map((cron: any, i: number) => {
|
||||||
|
const jobs = cron.spec?.jobs || cron.jobs || [];
|
||||||
|
const tz = cron.spec?.timezone || 'UTC';
|
||||||
|
return (
|
||||||
|
<TableRow key={cron.name || i} className="border-[var(--dg-border)]">
|
||||||
|
<TableCell className="font-medium text-[var(--dg-text-primary)]">{cron.name || cron.metadata?.name}</TableCell>
|
||||||
|
<TableCell className="text-sm text-[var(--dg-text-secondary)]">{
|
||||||
|
jobs.map((j: any) => j.targetSize || j.targetReplicas || '?').join(', ')
|
||||||
|
}</TableCell>
|
||||||
|
<TableCell className="text-xs font-mono text-[var(--dg-text-muted)]">{jobs.map((j: any) => j.schedule || '?').join(', ')}</TableCell>
|
||||||
|
<TableCell className="text-xs text-[var(--dg-text-muted)]">{tz}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Badge className="bg-[var(--dg-green)]/20 text-[var(--dg-green)]">已启用</Badge>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,25 +302,25 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* 模式切换 Tab — 三模式:查看 / 编辑 / YAML */}
|
{/* 模式切换 — 查看 / 编辑 / YAML */}
|
||||||
<div className="inline-flex rounded-md border border-slate-700 overflow-hidden">
|
<div className="inline-flex rounded-md border border-slate-700 overflow-hidden">
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'view' ? 'bg-slate-700 text-slate-100' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'view' ? 'bg-slate-700 text-slate-100' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
||||||
onClick={() => onModeChange('view')}
|
onClick={() => onModeChange('view')}
|
||||||
>
|
>
|
||||||
查看模式
|
查看
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'edit' ? 'bg-blue-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'edit' ? 'bg-blue-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
||||||
onClick={() => onModeChange('edit')}
|
onClick={() => onModeChange('edit')}
|
||||||
>
|
>
|
||||||
<Pencil className="h-3 w-3 inline mr-1" />编辑模式
|
编辑
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-emerald-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
className={`px-3 py-1.5 text-xs font-medium transition-colors ${mode === 'yaml' ? 'bg-emerald-600 text-white' : 'bg-transparent text-slate-400 hover:text-slate-200'}`}
|
||||||
onClick={() => onModeChange('yaml')}
|
onClick={() => onModeChange('yaml')}
|
||||||
>
|
>
|
||||||
<FileCode className="h-3 w-3 inline mr-1" />YAML编辑
|
YAML
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,9 +330,6 @@ export function DeploymentWorkload({ clusterCode, namespace, deploymentName, mod
|
|||||||
|
|
||||||
{/* 操作按钮行 */}
|
{/* 操作按钮行 */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => onModeChange('edit')}>
|
|
||||||
<Pencil className="h-3.5 w-3.5 mr-1" />编辑
|
|
||||||
</Button>
|
|
||||||
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => setModal({ kind: 'image' })}>
|
<Button variant="outline" size="sm" className="border-slate-700 text-slate-300" onClick={() => setModal({ kind: 'image' })}>
|
||||||
<ImageIcon className="h-3.5 w-3.5 mr-1" />调整镜像
|
<ImageIcon className="h-3.5 w-3.5 mr-1" />调整镜像
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in new issue
Block a user