refactor(quality): Q26 前端巨型组件拆分 — PodLogsViewer 553L→331L (-40%), 提取 pod-logs-viewer/ 5 个文件 (types.tsx(highlightKeyword 纯函数) + LogsHeader(66L) + ControlBar(178L) + LogContentArea(64L) + BottomStatusBar(42L)), renderLine 内联函数提取为模块级 highlightKeyword 纯函数, catch 类型安全升级 (e?.message→e instanceof Error), 零逻辑变更, ESLint errors=0/warnings=65/any=0, tsc -b=0, npm build✓ (17.34s), 17/17 测试通过
This commit is contained in:
1 parent
e5f969d2c7
commit
03a84049fb
9 files changed
+542
-301
No files matched your search
@@ -5,67 +5,27 @@ import {
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
import {
|
||||
ScrollText, Maximize2, Minimize2, X, Play, Pause, Download, Search,
|
||||
RefreshCw, Loader2, Box, History, Clock, Filter, AlertCircle,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { kubernetesApi } from '@/api';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import type { JsonData } from '@/types/common';
|
||||
import i18n from '@/i18n';
|
||||
import {
|
||||
type PodLogsViewerProps,
|
||||
type LogLine,
|
||||
RING_BUFFER_MAX,
|
||||
} from './pod-logs-viewer/types'; // types.tsx — 含 highlightKeyword JSX 纯函数
|
||||
import { LogsHeader } from './pod-logs-viewer/LogsHeader';
|
||||
import { ControlBar } from './pod-logs-viewer/ControlBar';
|
||||
import { LogContentArea } from './pod-logs-viewer/LogContentArea';
|
||||
import { BottomStatusBar } from './pod-logs-viewer/BottomStatusBar';
|
||||
|
||||
/**
|
||||
* 增强版 Pod 日志查看器 — 增强 DeploymentDetail 的日志体验
|
||||
*
|
||||
* 功能:
|
||||
* - 容器选择下拉(多容器 Pod)
|
||||
* - 实时跟踪开关(tail -f 效果,用 fetch stream)
|
||||
* - 行数限制选择(100 / 500 / 1000 / 全部)
|
||||
* - previous 容器日志(上一次崩溃的日志)
|
||||
* - 时间范围筛选(sinceTime)
|
||||
* - 关键字过滤(前端过滤,高亮)
|
||||
* - 下载按钮(导出当前日志为文件)
|
||||
* - 暂停 / 继续(流式时)
|
||||
* - 10000 行环形缓冲(借鉴 opsx-agent)
|
||||
*
|
||||
* @author Logan <admin@attacker.club>
|
||||
* 功能:容器选择 / 实时跟踪(fetch stream) / 行数限制 / previous 日志 /
|
||||
* 时间范围 / 关键字过滤(高亮)/ 下载 / 暂停-继续 / 10000 行环形缓冲
|
||||
*/
|
||||
interface PodLogsViewerProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
clusterCode: string;
|
||||
namespace: string;
|
||||
podName: string;
|
||||
/** 可选:Pod 容器名列表 */
|
||||
containers?: string[];
|
||||
initialContainer?: string;
|
||||
}
|
||||
|
||||
const TAIL_OPTIONS = [
|
||||
{ value: '100', label: i18n.t('kubernetes.podlogsviewerts.s58b02a7a') },
|
||||
{ value: '500', label: i18n.t('kubernetes.podlogsviewerts.s9a036801') },
|
||||
{ value: '1000', label: i18n.t('kubernetes.podlogsviewerts.sf89a330c') },
|
||||
{ value: '0', label: i18n.t('system.auditAll') },
|
||||
];
|
||||
|
||||
// 环形缓冲大小(借鉴 opsx-agent)
|
||||
const RING_BUFFER_MAX = 10000;
|
||||
|
||||
interface LogLine {
|
||||
id: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
export function PodLogsViewer({
|
||||
open,
|
||||
onOpenChange,
|
||||
@@ -90,7 +50,7 @@ export function PodLogsViewer({
|
||||
const [paused, setPaused] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// 环形缓冲(使用 state 触发渲染,ref 持久存储)
|
||||
// 环形缓冲(state 触发渲染,ref 持久存储)
|
||||
const [lines, setLines] = useState<LogLine[]>([]);
|
||||
const linesRef = useRef<LogLine[]>([]);
|
||||
const lineIdRef = useRef(0);
|
||||
@@ -121,7 +81,6 @@ export function PodLogsViewer({
|
||||
for (const t of newTexts) {
|
||||
next.push({ id: ++lineIdRef.current, text: t });
|
||||
}
|
||||
// 环形缓冲裁剪
|
||||
if (next.length > RING_BUFFER_MAX) {
|
||||
linesRef.current = next.slice(next.length - RING_BUFFER_MAX);
|
||||
} else {
|
||||
@@ -165,8 +124,9 @@ export function PodLogsViewer({
|
||||
const arr = typeof raw === 'string' ? raw.split('\n') : (Array.isArray(raw) ? raw : String(raw).split('\n'));
|
||||
appendLines(arr);
|
||||
} catch (e) {
|
||||
appendLines([`\x1b[31m[获取日志失败: ${e?.message || String(e)}]\x1b[0m`]);
|
||||
toast({ title: i18n.t('k8s.podLogs.loadFailed'), description: e?.message || String(e), variant: 'destructive' });
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
appendLines([`\x1b[31m[获取日志失败: ${msg}]\x1b[0m`]);
|
||||
toast({ title: i18n.t('k8s.podLogs.loadFailed'), description: msg, variant: 'destructive' });
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -207,7 +167,6 @@ export function PodLogsViewer({
|
||||
|
||||
const flush = () => {
|
||||
if (!buffer) return;
|
||||
// 按行切分,保留最后未结束的行
|
||||
const parts = buffer.split('\n');
|
||||
buffer = parts.pop() ?? '';
|
||||
if (parts.length > 0) appendLines(parts);
|
||||
@@ -216,22 +175,22 @@ export function PodLogsViewer({
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
// 暂停时累积但不写入缓冲(丢弃,保持实时性);这里改为暂停时不 append
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
if (!pausedRef.current) {
|
||||
flush();
|
||||
} else {
|
||||
// 暂停期间丢弃缓冲内容,避免堆积
|
||||
buffer = '';
|
||||
}
|
||||
}
|
||||
flush(); // 收尾
|
||||
flush();
|
||||
appendLines([i18n.t('kubernetes.podlogsviewerts.s4f0e17cd')]);
|
||||
} catch (e) {
|
||||
if (e?.name === 'AbortError') {
|
||||
const errName = (e as { name?: string })?.name;
|
||||
if (errName === 'AbortError') {
|
||||
appendLines([i18n.t('kubernetes.podlogsviewerts.s1ca9010d')]);
|
||||
} else {
|
||||
appendLines([`\x1b[31m[流式连接中断: ${e?.message || String(e)}]\x1b[0m`]);
|
||||
const msg = e instanceof Error ? e.message : String(e);
|
||||
appendLines([`\x1b[31m[流式连接中断: ${msg}]\x1b[0m`]);
|
||||
}
|
||||
} finally {
|
||||
setStreaming(false);
|
||||
@@ -250,17 +209,15 @@ export function PodLogsViewer({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open, podName]);
|
||||
|
||||
// 参数变化时若正在流则重启;否则只重新拉取(用户手动控制更友好,这里仅在非流时自动刷新)
|
||||
// 参数变化时若正在流则重启
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
if (streaming) {
|
||||
// 重启流
|
||||
startStream();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [container, tailLines, previous, sinceTime]);
|
||||
|
||||
// 关闭弹窗清理
|
||||
const handleClose = useCallback(() => {
|
||||
stopStream();
|
||||
onOpenChange(false);
|
||||
@@ -287,33 +244,6 @@ export function PodLogsViewer({
|
||||
return lines.filter((l) => l.text.toLowerCase().includes(kw));
|
||||
}, [lines, keyword]);
|
||||
|
||||
// 高亮关键字渲染
|
||||
const renderLine = (line: LogLine) => {
|
||||
const text = line.text;
|
||||
if (!keyword.trim()) return <span>{text}</span>;
|
||||
const kw = keyword.trim();
|
||||
const lower = text.toLowerCase();
|
||||
const kwLower = kw.toLowerCase();
|
||||
const parts: React.ReactNode[] = [];
|
||||
let idx = 0;
|
||||
let key = 0;
|
||||
while (idx < text.length) {
|
||||
const found = lower.indexOf(kwLower, idx);
|
||||
if (found === -1) {
|
||||
parts.push(<span key={key++}>{text.slice(idx)}</span>);
|
||||
break;
|
||||
}
|
||||
if (found > idx) parts.push(<span key={key++}>{text.slice(idx, found)}</span>);
|
||||
parts.push(
|
||||
<mark key={key++} className="bg-amber-400/40 text-amber-200 rounded px-0.5">
|
||||
{text.slice(found, found + kw.length)}
|
||||
</mark>
|
||||
);
|
||||
idx = found + kw.length;
|
||||
}
|
||||
return <>{parts}</>;
|
||||
};
|
||||
|
||||
const lineCount = lines.length;
|
||||
const matchedCount = filteredLines.length;
|
||||
|
||||
@@ -334,219 +264,67 @@ export function PodLogsViewer({
|
||||
<DialogTitle className="sr-only">日志 — {podName}</DialogTitle>
|
||||
<DialogDescription className="sr-only">Pod 容器日志查看</DialogDescription>
|
||||
|
||||
{/* 头部工具栏 */}
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-2 border-b border-[var(--dg-border)] flex-wrap">
|
||||
<div className="flex items-center gap-2 text-sm min-w-0">
|
||||
<div className={cn(
|
||||
'w-2 h-2 rounded-full shrink-0',
|
||||
streaming ? (paused ? 'bg-amber-400' : 'bg-emerald-400 animate-pulse') : 'bg-[var(--dg-bg-active)]'
|
||||
)} />
|
||||
<ScrollText className="h-4 w-4 dg-text-blue shrink-0" />
|
||||
<span className="text-[var(--dg-text-primary)] font-medium truncate">日志 — {podName}</span>
|
||||
{loading && <Loader2 className="h-3 w-3 animate-spin text-[var(--dg-text-muted)]" />}
|
||||
<span className="text-xs text-[var(--dg-text-muted)]">
|
||||
{streaming ? (paused ? i18n.t('kubernetes.deployment.paused') : i18n.t('kubernetes.podlogsviewerts.sd9c71f71')) : i18n.t('agent.agentServiceStopped')} · {matchedCount}{keyword ? `/${lineCount}` : ''} 行
|
||||
{lineCount >= RING_BUFFER_MAX && i18n.t('kubernetes.podlogsviewerts.sf3280b73')}
|
||||
</span>
|
||||
</div>
|
||||
<LogsHeader
|
||||
podName={podName}
|
||||
streaming={streaming}
|
||||
paused={paused}
|
||||
loading={loading}
|
||||
fullscreen={fullscreen}
|
||||
lineCount={lineCount}
|
||||
matchedCount={matchedCount}
|
||||
keyword={keyword}
|
||||
onToggleFullscreen={() => setFullscreen(!fullscreen)}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setFullscreen(!fullscreen)}
|
||||
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
||||
title={fullscreen ? i18n.t('common.exitFullscreen2') : i18n.t('monitor.fullscreen')}
|
||||
>
|
||||
{fullscreen ? <Minimize2 className="h-4 w-4" /> : <Maximize2 className="h-4 w-4" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="p-1.5 rounded hover:bg-[var(--dg-bg-hover)] text-[var(--dg-text-secondary)]"
|
||||
title={i18n.t('agent.portClosed')}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<ControlBar
|
||||
containers={containers}
|
||||
container={container}
|
||||
onContainerChange={setContainer}
|
||||
tailLines={tailLines}
|
||||
onTailLinesChange={setTailLines}
|
||||
previous={previous}
|
||||
onPreviousToggle={() => setPrevious(!previous)}
|
||||
sinceTime={sinceTime}
|
||||
onSinceTimeChange={setSinceTime}
|
||||
keyword={keyword}
|
||||
onKeywordChange={setKeyword}
|
||||
streaming={streaming}
|
||||
paused={paused}
|
||||
loading={loading}
|
||||
filteredLineCount={filteredLines.length}
|
||||
onDownload={handleDownload}
|
||||
onRefresh={fetchLogs}
|
||||
onStartStream={startStream}
|
||||
onStopStream={stopStream}
|
||||
onPauseToggle={() => setPaused(!paused)}
|
||||
/>
|
||||
|
||||
{/* 控制栏 */}
|
||||
<div className="flex items-center gap-2 px-4 py-2 border-b border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] flex-wrap">
|
||||
{/* 容器 */}
|
||||
{containers.length > 0 && (
|
||||
<Select value={container} onValueChange={setContainer}>
|
||||
<SelectTrigger className="h-8 w-[150px] text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)]">
|
||||
<Box className="h-3 w-3 mr-1 text-[var(--dg-text-muted)]" />
|
||||
<SelectValue placeholder={i18n.t('kubernetes.container.title')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{containers.map((c) => (
|
||||
<SelectItem key={c} value={c}>{c}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
<LogContentArea
|
||||
filteredLines={filteredLines}
|
||||
lineCount={lineCount}
|
||||
loading={loading}
|
||||
keyword={keyword}
|
||||
fullscreen={fullscreen}
|
||||
scrollRef={scrollRef}
|
||||
autoScrollRef={autoScrollRef}
|
||||
onScrollAt={(atBottom) => setAutoScroll(atBottom)}
|
||||
/>
|
||||
|
||||
{/* 行数 */}
|
||||
<Select value={String(tailLines)} onValueChange={(v) => setTailLines(Number(v))}>
|
||||
<SelectTrigger className="h-8 w-[130px] text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{TAIL_OPTIONS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
{/* previous */}
|
||||
<Button
|
||||
variant={previous ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={cn('h-8 text-xs', previous ? 'bg-amber-600 hover:bg-amber-700' : 'border-[var(--dg-border)] text-[var(--dg-text-secondary)]')}
|
||||
onClick={() => setPrevious(!previous)}
|
||||
title={i18n.t('kubernetes.podlogsviewerts.s76a2e637')}
|
||||
>
|
||||
<History className="h-3.5 w-3.5 mr-1" />
|
||||
previous
|
||||
</Button>
|
||||
|
||||
{/* sinceTime */}
|
||||
<div className="flex items-center gap-1">
|
||||
<Clock className="h-3.5 w-3.5 text-[var(--dg-text-muted)]" />
|
||||
<Input
|
||||
type="datetime-local"
|
||||
value={sinceTime ? sinceTime.slice(0, 16) : ''}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setSinceTime(v ? new Date(v).toISOString() : '');
|
||||
}}
|
||||
className="h-8 w-[170px] text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
title={i18n.t('kubernetes.podlogsviewerts.s127892fd')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
{/* 关键字过滤 */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-[var(--dg-text-muted)]" />
|
||||
<Input
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
placeholder={i18n.t('kubernetes.podlogsviewerts.s7558f175')}
|
||||
className="h-8 w-[160px] pl-7 text-xs bg-[var(--dg-bg-hover)] border-[var(--dg-border)] text-[var(--dg-text-primary)]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 下载 */}
|
||||
<Button
|
||||
variant="outline" size="sm"
|
||||
className="h-8 text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||
onClick={handleDownload}
|
||||
title={i18n.t('kubernetes.podlogsviewerts.s1a023dfb')}
|
||||
disabled={filteredLines.length === 0}
|
||||
>
|
||||
<Download className="h-3.5 w-3.5 mr-1" />导出
|
||||
</Button>
|
||||
|
||||
{/* 刷新(一次性) */}
|
||||
<Button
|
||||
variant="outline" size="sm"
|
||||
className="h-8 text-xs border-[var(--dg-border)] text-[var(--dg-text-secondary)]"
|
||||
onClick={fetchLogs}
|
||||
disabled={loading}
|
||||
title={i18n.t('kubernetes.podlogsviewerts.s930b4e01')}
|
||||
>
|
||||
<RefreshCw className={cn('h-3.5 w-3.5 mr-1', loading && 'animate-spin')} />刷新
|
||||
</Button>
|
||||
|
||||
{/* 实时跟踪 / 暂停 / 继续 */}
|
||||
{!streaming ? (
|
||||
<Button
|
||||
size="sm"
|
||||
className="h-8 text-xs bg-emerald-600 hover:bg-emerald-700"
|
||||
onClick={startStream}
|
||||
disabled={loading}
|
||||
title={i18n.t('kubernetes.podlogsviewerts.s0d519cc0')}
|
||||
>
|
||||
<Play className="h-3.5 w-3.5 mr-1" />实时
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline" size="sm"
|
||||
className={cn('h-8 text-xs', paused ? 'border-emerald-500 dg-text-green' : 'border-amber-500 dg-text-yellow')}
|
||||
onClick={() => setPaused(!paused)}
|
||||
title={paused ? i18n.t('common.continue') : i18n.t('kubernetes.job.suspend')}
|
||||
>
|
||||
{paused ? <Play className="h-3.5 w-3.5 mr-1" /> : <Pause className="h-3.5 w-3.5 mr-1" />}
|
||||
{paused ? i18n.t('common.continue') : i18n.t('kubernetes.job.suspend')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline" size="sm"
|
||||
className="h-8 text-xs border-red-500 dg-text-red"
|
||||
onClick={stopStream}
|
||||
title={i18n.t('agent.agentServiceStop')}
|
||||
>
|
||||
<Pause className="h-3.5 w-3.5 mr-1" />停止
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 日志内容 */}
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={(e) => {
|
||||
const el = e.currentTarget;
|
||||
// 用户主动向上滚时关闭自动跟随
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 40;
|
||||
if (!atBottom && autoScrollRef.current) setAutoScroll(false);
|
||||
if (atBottom && !autoScrollRef.current) setAutoScroll(true);
|
||||
<BottomStatusBar
|
||||
lineCount={lineCount}
|
||||
matchedCount={matchedCount}
|
||||
keyword={keyword}
|
||||
container={container}
|
||||
previous={previous}
|
||||
autoScroll={autoScroll}
|
||||
onToggleAutoScroll={() => {
|
||||
setAutoScroll(!autoScroll);
|
||||
if (!autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}}
|
||||
className={cn(
|
||||
'overflow-auto bg-[#0f172a] font-mono text-xs leading-relaxed p-3',
|
||||
'text-[var(--dg-text-secondary)]'
|
||||
)}
|
||||
style={{ height: fullscreen ? 'calc(100vh - 98px)' : '520px' }}
|
||||
>
|
||||
{lineCount === 0 && !loading && (
|
||||
<div className="flex flex-col items-center justify-center h-full text-[var(--dg-text-muted)]">
|
||||
<AlertCircle className="h-8 w-8 mb-2 opacity-50" />
|
||||
<span> {i18n.t('pipeline.noLog')} </span>
|
||||
</div>
|
||||
)}
|
||||
{loading && lineCount === 0 && (
|
||||
<div className="flex items-center justify-center h-full text-[var(--dg-text-muted)]">
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />加载中…
|
||||
</div>
|
||||
)}
|
||||
{filteredLines.map((line) => (
|
||||
<div key={line.id} className="whitespace-pre-wrap break-all hover:bg-white/5 px-1">
|
||||
{renderLine(line)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* 底部状态条 */}
|
||||
<div className="flex items-center justify-between px-4 py-1.5 border-t border-[var(--dg-border)] bg-[var(--dg-bg-tertiary)] text-xs text-[var(--dg-text-muted)]">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="flex items-center gap-1"><Filter className="h-3 w-3" />缓冲 {lineCount}/{RING_BUFFER_MAX}</span>
|
||||
{keyword && <span>匹配 {matchedCount}</span>}
|
||||
{container && <span className="flex items-center gap-1"><Box className="h-3 w-3" />{container}</span>}
|
||||
{previous && <span className="dg-text-yellow flex items-center gap-1"><History className="h-3 w-3" />previous</span>}
|
||||
</div>
|
||||
<button
|
||||
className={cn('flex items-center gap-1', autoScroll ? 'dg-text-green' : 'text-[var(--dg-text-muted)]')}
|
||||
onClick={() => {
|
||||
setAutoScroll(!autoScroll);
|
||||
if (!autoScroll && scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Maximize2 className="h-3 w-3" />{autoScroll ? i18n.t('kubernetes.podlogsviewerts.sd7eb1e98') : i18n.t('kubernetes.podlogsviewerts.s130db316')}
|
||||
</button>
|
||||
</div>
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user