feat: opsx-loki 全面升级 — 前端重构/用户与LDAP/部署向导/日志查询
基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。 前端(Gin+React+shadcn/ui): - 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、 反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放 - 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info) - 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环) - 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史 - 按用户+标签策略的查询授权(服务端强制合并策略) 后端(Go+Gin+JSON DB): - 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流 - LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录 - 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建 - Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、 凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入 - DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签 - 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页 - 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录 修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、 健康检查 CreatedAt 零值等 10+ 存量 bug。 数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移; HKDF 加密前缀保留(已加密凭证不受影响)。
This commit is contained in:
1 parent
327e282356
commit
f9c6c1d826
204 files changed
+14107
-9103
No files matched your search
@@ -0,0 +1,179 @@
|
||||
import { memo, useEffect, useState } from 'react'
|
||||
import { Play, Square, RotateCw, Trash2, Terminal, ExternalLink } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { StatusBadge, StatusIcon } from '@/components/StatusBadge'
|
||||
import { InfoRow } from '@/components/InfoRow'
|
||||
import { deploymentApi } from '@/api'
|
||||
import type { LokiDeployment } from '@/types'
|
||||
|
||||
/** 部署目标展示:优先集群,其次主机 */
|
||||
const getTargetLabel = (d: LokiDeployment) => {
|
||||
if (d.cluster) return `集群:${d.cluster.name}`
|
||||
if (d.host) return `主机:${d.host.name}`
|
||||
return '未知'
|
||||
}
|
||||
|
||||
interface DeploymentCardProps {
|
||||
deployment: LokiDeployment
|
||||
/** 部署/重启进行中的部署 ID(按钮 loading 态) */
|
||||
operating: number | null
|
||||
onDeploy: (id: number) => void
|
||||
onStop: (id: number) => void
|
||||
onRestart: (id: number) => void
|
||||
onDelete: (d: LokiDeployment) => void
|
||||
onViewLogs: (d: LokiDeployment) => void
|
||||
}
|
||||
|
||||
/** 部署卡片 — 列表项展示与操作入口(部署/停止/重启/日志/访问/删除) */
|
||||
export const DeploymentCard = memo(function DeploymentCard({
|
||||
deployment: d,
|
||||
operating,
|
||||
onDeploy,
|
||||
onStop,
|
||||
onRestart,
|
||||
onDelete,
|
||||
onViewLogs,
|
||||
}: DeploymentCardProps) {
|
||||
// Loki 就绪探测(运行中时经数据源代理 /ready)
|
||||
const [ready, setReady] = useState<boolean | null>(null)
|
||||
useEffect(() => {
|
||||
if (d.status !== 'running') return
|
||||
let cancelled = false
|
||||
deploymentApi
|
||||
.ready(d.id)
|
||||
.then((r) => {
|
||||
if (!cancelled) setReady(r.ready)
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setReady(false)
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [d.id, d.status])
|
||||
|
||||
const isDeploying = d.status === 'deploying'
|
||||
const isBusy = operating === d.id
|
||||
const canDeploy = d.status === 'pending' || d.status === 'failed' || d.status === 'stopped'
|
||||
|
||||
return (
|
||||
<Card className="border-border hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<StatusIcon status={d.status} size="md" />
|
||||
<div className="min-w-0">
|
||||
<h3 className="font-semibold text-foreground truncate">
|
||||
{d.name}
|
||||
</h3>
|
||||
<p className="text-xs text-muted-foreground">{d.uuid}</p>
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={d.status} />
|
||||
{d.status === 'running' && ready !== null && (
|
||||
<span
|
||||
className={`w-2 h-2 rounded-full ${ready ? 'bg-success' : 'bg-destructive'}`}
|
||||
title={ready ? 'Loki 就绪' : 'Loki 未就绪'}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-muted-foreground mb-3">{d.description || '无描述'}</p>
|
||||
|
||||
<div className="space-y-1.5 text-xs text-muted-foreground mb-4">
|
||||
<InfoRow label="方式" value={d.mode} />
|
||||
<InfoRow label="版本" value={d.version} />
|
||||
<InfoRow label="目标" value={getTargetLabel(d)} />
|
||||
<InfoRow label="命名空间" value={d.namespace || 'loki'} />
|
||||
<InfoRow label="副本数" value={String(d.replicas)} />
|
||||
<InfoRow label="存储" value={d.storageConfig?.name || '本地'} />
|
||||
{d.endpoint && (
|
||||
<InfoRow label="访问地址" value={d.endpoint} valueClass="text-primary truncate max-w-[200px]" />
|
||||
)}
|
||||
{d.grafanaEnabled && d.grafanaEndpoint && (
|
||||
<div className="flex justify-between items-center gap-2">
|
||||
<span>Grafana</span>
|
||||
<a
|
||||
href={d.grafanaEndpoint}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-primary hover:underline truncate max-w-[200px]"
|
||||
title={`${d.grafanaEndpoint}(admin/admin)`}
|
||||
>
|
||||
{d.grafanaEndpoint.replace(/^https?:\/\//, '')}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
{d.statusMessage && (
|
||||
<div className="p-2 bg-muted rounded text-xs">
|
||||
{d.statusMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 pt-3 border-t border-border">
|
||||
{canDeploy ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="flex-1 text-xs bg-success/10 text-primary hover:bg-success/20 border-success/30"
|
||||
onClick={() => onDeploy(d.id)}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<Play className="w-3 h-3 mr-1" />
|
||||
{isBusy ? '部署中...' : '部署'}
|
||||
</Button>
|
||||
) : isDeploying ? (
|
||||
<Button variant="outline" size="sm" className="flex-1 text-xs" disabled>
|
||||
<RotateCw className="w-3 h-3 mr-1 animate-spin" />
|
||||
部署中...
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="text-xs" onClick={() => onStop(d.id)}>
|
||||
<Square className="w-3 h-3 mr-1" />
|
||||
停止
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs"
|
||||
onClick={() => onRestart(d.id)}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<RotateCw className={`w-3 h-3 mr-1 ${isBusy ? 'animate-spin' : ''}`} />
|
||||
重启
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<Button variant="outline" size="sm" className="text-xs" onClick={() => onViewLogs(d)}>
|
||||
<Terminal className="w-3 h-3 mr-1" />
|
||||
日志
|
||||
</Button>
|
||||
{d.endpoint && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="text-xs text-primary hover:text-primary/80"
|
||||
onClick={() => window.open(d.endpoint, '_blank', 'noopener,noreferrer')}
|
||||
title="浏览器访问"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 mr-1" />
|
||||
访问
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
aria-label={`删除部署 ${d.name}`}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
onClick={() => onDelete(d)}
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
})
|
||||
Reference in new issue
Block a user