Files
Termix/src/ui/desktop/apps/host-manager/credentials/CredentialViewer.tsx
T

529 lines
18 KiB
TypeScript
Raw Normal View History

2025-09-12 14:42:00 -05:00
import React, { useState, useEffect } from "react";
+1
2025-12-31 22:20:12 -06:00
import { Button } from "@/components/ui/button.tsx";
2025-09-12 14:42:00 -05:00
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
+1
2025-12-31 22:20:12 -06:00
} from "@/components/ui/card.tsx";
import { Badge } from "@/components/ui/badge.tsx";
import { Separator } from "@/components/ui/separator.tsx";
import { ScrollArea } from "@/components/ui/scroll-area.tsx";
2025-09-12 14:42:00 -05:00
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
+1
2025-12-31 22:20:12 -06:00
} from "@/components/ui/sheet.tsx";
2025-09-12 14:42:00 -05:00
import {
Key,
User,
Calendar,
Hash,
Folder,
Edit3,
Copy,
Shield,
Clock,
Server,
Eye,
EyeOff,
AlertTriangle,
CheckCircle,
FileText,
} from "lucide-react";
+1
2025-12-31 22:20:12 -06:00
import { getCredentialDetails, getCredentialHosts } from "@/ui/main-axios.ts";
2025-09-12 14:42:00 -05:00
import { toast } from "sonner";
import { useTranslation } from "react-i18next";
import type {
Credential,
HostInfo,
CredentialViewerProps,
} from "../../../types/index.js";
const CredentialViewer: React.FC<CredentialViewerProps> = ({
credential,
onClose,
onEdit,
}) => {
const { t } = useTranslation();
const [credentialDetails, setCredentialDetails] = useState<Credential | null>(
null,
);
const [hostsUsing, setHostsUsing] = useState<HostInfo[]>([]);
const [loading, setLoading] = useState(true);
const [showSensitive, setShowSensitive] = useState<Record<string, boolean>>(
{},
);
const [activeTab, setActiveTab] = useState<"overview" | "security" | "usage">(
"overview",
);
useEffect(() => {
fetchCredentialDetails();
fetchHostsUsing();
}, [credential.id]);
const fetchCredentialDetails = async () => {
try {
const response = await getCredentialDetails(credential.id);
setCredentialDetails(response);
+7
2025-11-05 10:36:16 -06:00
} catch {
2025-09-12 14:42:00 -05:00
toast.error(t("credentials.failedToFetchCredentialDetails"));
}
};
const fetchHostsUsing = async () => {
try {
const response = await getCredentialHosts(credential.id);
setHostsUsing(response);
+7
2025-11-05 10:36:16 -06:00
} catch {
2025-09-12 14:42:00 -05:00
toast.error(t("credentials.failedToFetchHostsUsing"));
} finally {
setLoading(false);
}
};
const toggleSensitiveVisibility = (field: string) => {
setShowSensitive((prev) => ({
...prev,
[field]: !prev[field],
}));
};
const copyToClipboard = async (text: string, fieldName: string) => {
try {
await navigator.clipboard.writeText(text);
toast.success(t("copiedToClipboard", { field: fieldName }));
+7
2025-11-05 10:36:16 -06:00
} catch {
2025-09-12 14:42:00 -05:00
toast.error(t("credentials.failedToCopy"));
}
};
const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleString();
};
const getAuthIcon = (authType: string) => {
return authType === "password" ? (
+1
2025-12-31 22:20:12 -06:00
<Key className="h-5 w-5 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
) : (
+1
2025-12-31 22:20:12 -06:00
<Shield className="h-5 w-5 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
);
};
const renderSensitiveField = (
value: string | undefined,
fieldName: string,
label: string,
isMultiline = false,
) => {
if (!value) return null;
const isVisible = showSensitive[fieldName];
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
+1
2025-12-31 22:20:12 -06:00
<label className="text-sm font-medium text-foreground-secondary">
2025-09-12 14:42:00 -05:00
{label}
</label>
<div className="flex items-center space-x-2">
<Button
variant="ghost"
size="sm"
onClick={() => toggleSensitiveVisibility(fieldName)}
>
{isVisible ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => copyToClipboard(value, label)}
>
<Copy className="h-4 w-4" />
</Button>
</div>
</div>
<div
+1
2025-12-31 22:20:12 -06:00
className={`p-3 rounded-md bg-surface ${isMultiline ? "" : "min-h-[2.5rem]"}`}
2025-09-12 14:42:00 -05:00
>
{isVisible ? (
<pre
className={`text-sm ${isMultiline ? "whitespace-pre-wrap" : "whitespace-nowrap"} font-mono`}
>
{value}
</pre>
) : (
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{"•".repeat(isMultiline ? 50 : 20)}
</div>
)}
</div>
</div>
);
};
if (loading || !credentialDetails) {
return (
<Sheet open={true} onOpenChange={onClose}>
<SheetContent className="w-[600px] max-w-[50vw]">
<div className="flex items-center justify-center h-64">
+1
2025-12-31 22:20:12 -06:00
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-foreground-subtle"></div>
2025-09-12 14:42:00 -05:00
</div>
</SheetContent>
</Sheet>
);
}
return (
<Sheet open={true} onOpenChange={onClose}>
+1
2025-12-31 22:20:12 -06:00
<SheetContent className="w-[600px] max-w-[50vw] overflow-y-auto thin-scrollbar">
2025-09-12 14:42:00 -05:00
<SheetHeader className="space-y-6 pb-8">
<SheetTitle className="flex items-center space-x-4">
+1
2025-12-31 22:20:12 -06:00
<div className="p-2 rounded-lg bg-surface">
2025-09-12 14:42:00 -05:00
{getAuthIcon(credentialDetails.authType)}
</div>
<div className="flex-1">
<div className="text-xl font-semibold">
{credentialDetails.name}
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm font-normal text-foreground-subtle mt-1">
2025-09-12 14:42:00 -05:00
{credentialDetails.description}
</div>
</div>
<div className="flex items-center space-x-2">
+1
2025-12-31 22:20:12 -06:00
<Badge variant="outline" className="text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{credentialDetails.authType}
</Badge>
{credentialDetails.keyType && (
<Badge
variant="secondary"
+1
2025-12-31 22:20:12 -06:00
className="bg-surface text-foreground-secondary"
2025-09-12 14:42:00 -05:00
>
{credentialDetails.keyType}
</Badge>
)}
</div>
</SheetTitle>
</SheetHeader>
<div className="space-y-10">
+1
2025-12-31 22:20:12 -06:00
<div className="flex space-x-2 p-2 bg-surface border border-border rounded-lg">
2025-09-12 14:42:00 -05:00
<Button
variant={activeTab === "overview" ? "default" : "ghost"}
size="sm"
onClick={() => setActiveTab("overview")}
className="flex-1 h-10"
>
<FileText className="h-4 w-4 mr-2" />
{t("credentials.overview")}
</Button>
<Button
variant={activeTab === "security" ? "default" : "ghost"}
size="sm"
onClick={() => setActiveTab("security")}
className="flex-1 h-10"
>
<Shield className="h-4 w-4 mr-2" />
{t("credentials.security")}
</Button>
<Button
variant={activeTab === "usage" ? "default" : "ghost"}
size="sm"
onClick={() => setActiveTab("usage")}
className="flex-1 h-10"
>
<Server className="h-4 w-4 mr-2" />
{t("credentials.usage")}
</Button>
</div>
{activeTab === "overview" && (
<div className="grid gap-10 lg:grid-cols-2">
+1
2025-12-31 22:20:12 -06:00
<Card className="border-border">
2025-09-12 14:42:00 -05:00
<CardHeader className="pb-8">
<CardTitle className="text-lg font-semibold">
{t("credentials.basicInformation")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-8">
<div className="flex items-center space-x-5">
+1
2025-12-31 22:20:12 -06:00
<div className="p-2 rounded-lg bg-surface">
<User className="h-4 w-4 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
</div>
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("common.username")}
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="font-medium text-foreground">
2025-09-12 14:42:00 -05:00
{credentialDetails.username}
</div>
</div>
</div>
{credentialDetails.folder && (
<div className="flex items-center space-x-4">
+1
2025-12-31 22:20:12 -06:00
<Folder className="h-4 w-4 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("common.folder")}
</div>
<div className="font-medium">
{credentialDetails.folder}
</div>
</div>
</div>
)}
{credentialDetails.tags.length > 0 && (
<div className="flex items-start space-x-4">
+1
2025-12-31 22:20:12 -06:00
<Hash className="h-4 w-4 text-foreground-subtle mt-1" />
2025-09-12 14:42:00 -05:00
<div className="flex-1">
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle mb-3">
2025-09-12 14:42:00 -05:00
{t("hosts.tags")}
</div>
<div className="flex flex-wrap gap-2">
{credentialDetails.tags.map((tag, index) => (
<Badge
key={index}
variant="outline"
className="text-xs"
>
{tag}
</Badge>
))}
</div>
</div>
</div>
)}
<Separator />
<div className="flex items-center space-x-4">
+1
2025-12-31 22:20:12 -06:00
<Calendar className="h-4 w-4 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("credentials.created")}
</div>
<div className="font-medium">
{formatDate(credentialDetails.createdAt)}
</div>
</div>
</div>
<div className="flex items-center space-x-4">
+1
2025-12-31 22:20:12 -06:00
<Calendar className="h-4 w-4 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("credentials.lastModified")}
</div>
<div className="font-medium">
{formatDate(credentialDetails.updatedAt)}
</div>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-lg">
{t("credentials.usageStatistics")}
</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
+1
2025-12-31 22:20:12 -06:00
<div className="text-center p-6 bg-surface rounded-lg">
<div className="text-3xl font-bold text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{credentialDetails.usageCount}
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("credentials.timesUsed")}
</div>
</div>
{credentialDetails.lastUsed && (
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-center space-x-4 p-4 bg-surface rounded-lg">
<Clock className="h-5 w-5 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("credentials.lastUsed")}
</div>
<div className="font-medium">
{formatDate(credentialDetails.lastUsed)}
</div>
</div>
</div>
)}
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-center space-x-4 p-4 bg-surface rounded-lg">
<Server className="h-5 w-5 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{t("credentials.connectedHosts")}
</div>
<div className="font-medium">{hostsUsing.length}</div>
</div>
</div>
</CardContent>
</Card>
</div>
)}
{activeTab === "security" && (
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center space-x-2">
+1
2025-12-31 22:20:12 -06:00
<Shield className="h-5 w-5 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<span>{t("credentials.securityDetails")}</span>
</CardTitle>
<CardDescription>
{t("credentials.securityDetailsDescription")}
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-center space-x-4 p-6 bg-surface rounded-lg">
<CheckCircle className="h-6 w-6 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="font-medium text-foreground">
2025-09-12 14:42:00 -05:00
{t("credentials.credentialSecured")}
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-secondary">
2025-09-12 14:42:00 -05:00
{t("credentials.credentialSecuredDescription")}
</div>
</div>
</div>
{credentialDetails.authType === "password" && (
<div>
<h3 className="font-semibold mb-4">
{t("credentials.passwordAuthentication")}
</h3>
{renderSensitiveField(
credentialDetails.password,
"password",
t("common.password"),
)}
</div>
)}
{credentialDetails.authType === "key" && (
<div className="space-y-6">
<h3 className="font-semibold mb-2">
{t("credentials.keyAuthentication")}
</h3>
<div className="grid gap-6 md:grid-cols-2">
<div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm font-medium text-foreground-secondary mb-3">
2025-09-12 14:42:00 -05:00
{t("credentials.keyType")}
</div>
<Badge variant="outline" className="text-sm">
{credentialDetails.keyType?.toUpperCase() ||
t("unknown").toUpperCase()}
</Badge>
</div>
</div>
{renderSensitiveField(
credentialDetails.key,
"key",
t("credentials.privateKey"),
true,
)}
{credentialDetails.keyPassword &&
renderSensitiveField(
credentialDetails.keyPassword,
"keyPassword",
t("credentials.keyPassphrase"),
)}
</div>
)}
+1
2025-12-31 22:20:12 -06:00
<div className="flex items-start space-x-4 p-6 bg-surface rounded-lg">
<AlertTriangle className="h-5 w-5 text-foreground-subtle mt-0.5" />
2025-09-12 14:42:00 -05:00
<div className="text-sm">
+1
2025-12-31 22:20:12 -06:00
<div className="font-medium text-foreground mb-2">
2025-09-12 14:42:00 -05:00
{t("credentials.securityReminder")}
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-foreground-secondary">
2025-09-12 14:42:00 -05:00
{t("credentials.securityReminderText")}
</div>
</div>
</div>
</CardContent>
</Card>
)}
{activeTab === "usage" && (
<Card>
<CardHeader>
<CardTitle className="text-lg flex items-center space-x-2">
+1
2025-12-31 22:20:12 -06:00
<Server className="h-5 w-5 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<span>{t("credentials.hostsUsingCredential")}</span>
<Badge variant="secondary">{hostsUsing.length}</Badge>
</CardTitle>
</CardHeader>
<CardContent>
{hostsUsing.length === 0 ? (
+1
2025-12-31 22:20:12 -06:00
<div className="text-center py-10 text-foreground-subtle">
<Server className="h-12 w-12 mx-auto mb-6 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
<p>{t("credentials.noHostsUsingCredential")}</p>
</div>
) : (
<ScrollArea className="h-64">
<div className="space-y-3">
{hostsUsing.map((host) => (
<div
key={host.id}
+1
2025-12-31 22:20:12 -06:00
className="flex items-center justify-between p-4 border rounded-lg hover:bg-surface"
2025-09-12 14:42:00 -05:00
>
<div className="flex items-center space-x-3">
+1
2025-12-31 22:20:12 -06:00
<div className="p-2 bg-surface rounded">
<Server className="h-4 w-4 text-foreground-subtle" />
2025-09-12 14:42:00 -05:00
</div>
<div>
<div className="font-medium">
{host.name || `${host.ip}:${host.port}`}
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{host.ip}:{host.port}
</div>
</div>
</div>
+1
2025-12-31 22:20:12 -06:00
<div className="text-right text-sm text-foreground-subtle">
2025-09-12 14:42:00 -05:00
{formatDate(host.createdAt)}
</div>
</div>
))}
</div>
</ScrollArea>
)}
</CardContent>
</Card>
)}
</div>
<SheetFooter>
<Button variant="outline" onClick={onClose}>
{t("common.close")}
</Button>
<Button onClick={onEdit}>
<Edit3 className="h-4 w-4 mr-2" />
{t("credentials.editCredential")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
};
export default CredentialViewer;