Files
LokiStack/frontend/src/pages/deployments/components/DeploymentCard.tsx
T
opsx 004375e0ce fix(ns): 命名空间固定 log —— 平台/Loki 实例/Promtail 全部收敛到 log
按需求:所有 lokistack 相关资源(管理平台副本、Loki 实例、Promtail 采集器)
固定使用 log 命名空间,不提供覆盖开关。

改动:
1. 后端 services/deployment.go Create():k8s-yaml 模式下 namespace 为空时
   兜底为 "log",前端漏传也不会跑偏。
2. 前端 DeploymentCard.tsx:命名空间展示 fallback 'loki' → 'log'
3. 前端 DeploymentFormDialog.tsx:输入框 placeholder 'loki' → 'log'
   (form-schema.ts 的 DEFAULT_VALUES.namespace 原本已是 'log')

未动:
  kubernetes/install.sh 的 NS="log" 早就是固定值
  collector_deployer.go 已自动跟随 loki.Namespace(collector.Namespace 为空时)

资源名不冲突:
  平台        lokistack / lokistack-data / lokistack-secrets / lokistack-nodeport
  Loki 实例    loki / loki-config / loki-data / loki-headless / loki-storage-credentials
  Promtail    promtail-* (DaemonSet)

镜像:lghost/lokistack:latest + v0.1 = sha256:9d15b7ffbb3d
2026-10-26 18:55:06 +00:00

150 lines
6.2 KiB
TypeScript

import { memo, useEffect, useState } from 'react'
import { Link } from 'react-router'
import { Play, Square, RotateCw, Trash2, Terminal, ExternalLink, Pencil, Rocket } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { StatusBadge } from '@/components/StatusBadge'
import { deploymentApi } from '@/api'
import { ResourceCard } from '@/components/ResourceCard'
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
onEditRetention: (d: LokiDeployment) => void
}
/** 部署卡片 — 列表项展示与操作入口(部署/停止/重启/日志/访问/删除) */
export const DeploymentCard = memo(function DeploymentCard({
deployment: d,
operating,
onDeploy,
onStop,
onRestart,
onDelete,
onViewLogs,
onEditRetention,
}: 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 (
<ResourceCard
icon={Rocket}
iconClassName={d.status === 'running' ? 'text-success' : d.status === 'failed' ? 'text-destructive' : 'text-muted-foreground'}
title={d.name}
titleExtra={
d.status === 'running' && ready !== null ? (
<span
className={`w-2 h-2 rounded-full shrink-0 ${ready ? 'bg-success' : 'bg-destructive'}`}
title={ready ? 'Loki 就绪' : 'Loki 未就绪'}
/>
) : undefined
}
badges={<StatusBadge status={d.status} />}
description={d.description || d.uuid}
fields={[
{ label: '方式', value: d.mode === 'host-docker' ? '主机 Docker' : d.mode === 'k8s-yaml' ? 'Kubernetes' : d.mode },
{ label: '版本', value: d.version },
{ label: '目标', value: getTargetLabel(d) },
...(d.mode !== 'host-docker' ? [
{ label: '命名空间', value: d.namespace || 'log' },
{ label: '副本', value: `${d.replicas} · 保留 ${d.retentionDays || 30} 天` },
] : [{ label: '保留', value: `${d.retentionDays || 30} 天` }]),
{ label: '存储', value: d.storageConfig?.name || '本地' },
...(d.endpoint ? [{ label: '访问', value: (
<span className="text-primary">{d.endpoint.replace(/^https?:\/\//, '')}</span>
), title: d.endpoint }] : []),
...(d.grafanaEnabled && d.grafanaEndpoint ? [{ label: 'Grafana', value: (
<a href={d.grafanaEndpoint} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline" title={`${d.grafanaEndpoint}(admin/admin)`}>
{d.grafanaEndpoint.replace(/^https?:\/\//, '')}
</a>
) }] : []),
]}
message={
d.statusMessage ||
(d.status === 'running'
? '提示:到「日志查询 → 数据源」关联 Loki 后即可查询;跨集群/公网访问需先为 Loki 配置 Ingress 域名。'
: undefined)
}
actions={
<>
<Button variant="outline" size="sm" className="text-xs" onClick={() => onEditRetention(d)} disabled={isDeploying || isBusy}>
<Pencil className="size-3 mr-1" />配置
</Button>
{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="size-3 mr-1" />{isBusy ? '部署中...' : '部署'}
</Button>
) : isDeploying ? (
<Button variant="outline" size="sm" className="flex-1 text-xs" disabled>
<RotateCw className="size-3 mr-1 animate-spin" />部署中...
</Button>
) : (
<>
<Button variant="outline" size="sm" className="flex-1 text-xs" onClick={() => onStop(d.id)}>
<Square className="size-3 mr-1" />停止
</Button>
<Button variant="outline" size="sm" className="flex-1 text-xs" onClick={() => onRestart(d.id)} disabled={isBusy}>
<RotateCw className={`size-3 mr-1 ${isBusy ? 'animate-spin' : ''}`} />重启
</Button>
</>
)}
{d.status === 'running' && (
<>
<Button variant="outline" size="sm" className="text-xs" asChild>
<Link to="/logs">日志查询</Link>
</Button>
<Button variant="outline" size="sm" className="text-xs" asChild>
<Link to="/collectors">采集器</Link>
</Button>
</>
)}
<Button variant="outline" size="sm" className="text-xs" onClick={() => onViewLogs(d)}>
<Terminal className="size-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="size-3" />
</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="size-3.5" />
</Button>
</>
}
/>
)
})