Replace LogsModal with LogStreamPanel in PodList for streaming logs Add smart positioning to ResourceActionMenu to flip when near bottom Fix dark mode text visibility by applying class to html element Fix YAML editor loading race condition Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
177 lines
5.7 KiB
TypeScript
177 lines
5.7 KiB
TypeScript
import React, { useState, useMemo } from "react";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@/components/ui";
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui";
|
|
import { Button } from "@/components/ui";
|
|
import { Eye, EyeOff, Copy, Check } from "lucide-react";
|
|
|
|
interface SecretDataModalProps {
|
|
open: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
secretName: string;
|
|
secretYaml: string;
|
|
}
|
|
|
|
interface SecretData {
|
|
[key: string]: string;
|
|
}
|
|
|
|
export function SecretDataModal({ open, onOpenChange, secretName, secretYaml }: SecretDataModalProps) {
|
|
const [revealedKeys, setRevealedKeys] = useState<Set<string>>(new Set());
|
|
const [copiedKey, setCopiedKey] = useState<string | null>(null);
|
|
|
|
const secretData = useMemo<SecretData>(() => {
|
|
try {
|
|
// Simple YAML parsing for the data section
|
|
// Find the data: section and extract key-value pairs
|
|
const lines = secretYaml.split("\n");
|
|
const dataIndex = lines.findIndex(line => line.trim() === "data:");
|
|
|
|
if (dataIndex === -1) {
|
|
return {};
|
|
}
|
|
|
|
const result: SecretData = {};
|
|
const dataIndent = lines[dataIndex].search(/\S/);
|
|
|
|
for (let i = dataIndex + 1; i < lines.length; i++) {
|
|
const line = lines[i];
|
|
const trimmed = line.trim();
|
|
|
|
// Stop if we hit another top-level key
|
|
if (line.search(/\S/) <= dataIndent && trimmed && !trimmed.startsWith("#")) {
|
|
break;
|
|
}
|
|
|
|
// Parse key: value pairs
|
|
const match = trimmed.match(/^([^:]+):\s*(.*)$/);
|
|
if (match && match[1] && match[2]) {
|
|
const key = match[1].trim();
|
|
const value = match[2].trim();
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
} catch (err) {
|
|
console.error("Failed to parse secret YAML:", err);
|
|
return {};
|
|
}
|
|
}, [secretYaml]);
|
|
|
|
const decodedData = useMemo(() => {
|
|
const decoded: Record<string, string> = {};
|
|
Object.entries(secretData).forEach(([key, value]) => {
|
|
try {
|
|
// Decode base64 using native atob
|
|
decoded[key] = atob(value);
|
|
} catch (err) {
|
|
decoded[key] = `[Failed to decode: ${err instanceof Error ? err.message : String(err)}]`;
|
|
}
|
|
});
|
|
return decoded;
|
|
}, [secretData]);
|
|
|
|
const toggleReveal = (key: string) => {
|
|
setRevealedKeys((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(key)) {
|
|
next.delete(key);
|
|
} else {
|
|
next.add(key);
|
|
}
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const copyToClipboard = async (key: string, value: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(value);
|
|
setCopiedKey(key);
|
|
setTimeout(() => setCopiedKey(null), 2000);
|
|
} catch (err) {
|
|
console.error("Failed to copy to clipboard:", err);
|
|
}
|
|
};
|
|
|
|
const dataKeys = Object.keys(secretData);
|
|
|
|
return (
|
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
<DialogContent className="max-w-4xl max-h-[80vh] overflow-y-auto">
|
|
<DialogHeader>
|
|
<DialogTitle>Secret Data: {secretName}</DialogTitle>
|
|
<DialogDescription>
|
|
Decoded secret data. Click the eye icon to reveal values.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
|
|
{dataKeys.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground py-4">No data keys in this secret.</p>
|
|
) : (
|
|
<div className="overflow-x-auto">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Key</TableHead>
|
|
<TableHead>Value</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{dataKeys.map((key) => {
|
|
const isRevealed = revealedKeys.has(key);
|
|
const value = decodedData[key] ?? "";
|
|
const isCopied = copiedKey === key;
|
|
|
|
return (
|
|
<TableRow key={key}>
|
|
<TableCell className="font-medium font-mono text-sm">{key}</TableCell>
|
|
<TableCell className="font-mono text-sm max-w-md truncate">
|
|
{isRevealed ? value : "••••••••"}
|
|
</TableCell>
|
|
<TableCell className="text-right">
|
|
<div className="flex items-center justify-end gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => toggleReveal(key)}
|
|
title={isRevealed ? "Hide value" : "Reveal value"}
|
|
>
|
|
{isRevealed ? (
|
|
<EyeOff className="w-4 h-4" />
|
|
) : (
|
|
<Eye className="w-4 h-4" />
|
|
)}
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => copyToClipboard(key, value)}
|
|
title="Copy to clipboard"
|
|
>
|
|
{isCopied ? (
|
|
<Check className="w-4 h-4 text-green-500" />
|
|
) : (
|
|
<Copy className="w-4 h-4" />
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|