Cleaned up more unused imports, parameters, and variables: - FileManagerGrid.tsx: removed 4 unused variables (params, function) - FileManagerContextMenu.tsx: removed Share import - FileManagerSidebar.tsx: removed onLoadDirectory parameter - DraggableWindow.tsx: removed Square import - FileWindow.tsx: removed updateWindow variable - ServerConfig.tsx: removed 2 unused error parameters Reduced lint errors from 236 to 222 (14 errors fixed total) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
227 lines
8.0 KiB
TypeScript
227 lines
8.0 KiB
TypeScript
import React, { useState, useEffect, useRef } from "react";
|
|
import { Button } from "@/components/ui/button.tsx";
|
|
import { Input } from "@/components/ui/input.tsx";
|
|
import { FormControl, FormItem, FormLabel } from "@/components/ui/form.tsx";
|
|
import { getCredentials } from "@/ui/main-axios.ts";
|
|
import { useTranslation } from "react-i18next";
|
|
import type { Credential } from "../../../../types";
|
|
|
|
interface CredentialSelectorProps {
|
|
value?: number | null;
|
|
onValueChange: (credentialId: number | null) => void;
|
|
onCredentialSelect?: (credential: Credential | null) => void;
|
|
}
|
|
|
|
export function CredentialSelector({
|
|
value,
|
|
onValueChange,
|
|
onCredentialSelect,
|
|
}: CredentialSelectorProps) {
|
|
const { t } = useTranslation();
|
|
const [credentials, setCredentials] = useState<Credential[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
|
|
const buttonRef = useRef<HTMLButtonElement>(null);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
useEffect(() => {
|
|
const fetchCredentials = async () => {
|
|
try {
|
|
setLoading(true);
|
|
const data = await getCredentials();
|
|
const credentialsArray = Array.isArray(data)
|
|
? data
|
|
: data.credentials || data.data || [];
|
|
setCredentials(credentialsArray);
|
|
} catch {
|
|
const { toast } = await import("sonner");
|
|
toast.error(t("credentials.failedToFetchCredentials"));
|
|
setCredentials([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
fetchCredentials();
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
function handleClickOutside(event: MouseEvent) {
|
|
if (
|
|
dropdownRef.current &&
|
|
!dropdownRef.current.contains(event.target as Node) &&
|
|
buttonRef.current &&
|
|
!buttonRef.current.contains(event.target as Node)
|
|
) {
|
|
setDropdownOpen(false);
|
|
}
|
|
}
|
|
|
|
if (dropdownOpen) {
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
} else {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
}
|
|
|
|
return () => {
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
|
};
|
|
}, [dropdownOpen]);
|
|
|
|
const selectedCredential = credentials.find((c) => c.id === value);
|
|
|
|
const filteredCredentials = credentials.filter((credential) => {
|
|
if (!searchQuery) return true;
|
|
const searchLower = searchQuery.toLowerCase();
|
|
return (
|
|
credential.name.toLowerCase().includes(searchLower) ||
|
|
credential.username.toLowerCase().includes(searchLower) ||
|
|
(credential.folder &&
|
|
credential.folder.toLowerCase().includes(searchLower))
|
|
);
|
|
});
|
|
|
|
const handleCredentialSelect = (credential: Credential) => {
|
|
onValueChange(credential.id);
|
|
if (onCredentialSelect) {
|
|
onCredentialSelect(credential);
|
|
}
|
|
setDropdownOpen(false);
|
|
setSearchQuery("");
|
|
};
|
|
|
|
const handleClear = () => {
|
|
onValueChange(null);
|
|
if (onCredentialSelect) {
|
|
onCredentialSelect(null);
|
|
}
|
|
setDropdownOpen(false);
|
|
setSearchQuery("");
|
|
};
|
|
|
|
return (
|
|
<FormItem>
|
|
<FormLabel>{t("hosts.selectCredential")}</FormLabel>
|
|
<FormControl>
|
|
<div className="relative">
|
|
<Button
|
|
ref={buttonRef}
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full justify-between text-left rounded-lg px-3 py-2 bg-muted/50 focus:bg-background focus:ring-1 focus:ring-ring border border-border text-foreground transition-all duration-200"
|
|
onClick={() => setDropdownOpen(!dropdownOpen)}
|
|
>
|
|
{loading ? (
|
|
t("common.loading")
|
|
) : value === "existing_credential" ? (
|
|
<div className="flex items-center justify-between w-full">
|
|
<div>
|
|
<span className="font-medium">
|
|
{t("hosts.existingCredential")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
) : selectedCredential ? (
|
|
<div className="flex items-center justify-between w-full">
|
|
<div>
|
|
<span className="font-medium">{selectedCredential.name}</span>
|
|
<span className="text-sm text-muted-foreground ml-2">
|
|
({selectedCredential.username} •{" "}
|
|
{selectedCredential.authType})
|
|
</span>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
t("hosts.selectCredentialPlaceholder")
|
|
)}
|
|
<svg
|
|
className="h-4 w-4"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
viewBox="0 0 24 24"
|
|
>
|
|
<path
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
strokeWidth={2}
|
|
d="M19 9l-7 7-7-7"
|
|
/>
|
|
</svg>
|
|
</Button>
|
|
|
|
{dropdownOpen && (
|
|
<div
|
|
ref={dropdownRef}
|
|
className="absolute top-full left-0 z-50 mt-1 w-full bg-card border border-border rounded-lg shadow-lg max-h-80 overflow-hidden backdrop-blur-sm"
|
|
>
|
|
<div className="p-2 border-b border-border">
|
|
<Input
|
|
placeholder={t("credentials.searchCredentials")}
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
className="h-8"
|
|
/>
|
|
</div>
|
|
|
|
<div className="max-h-60 overflow-y-auto p-2">
|
|
{loading ? (
|
|
<div className="p-3 text-center text-sm text-muted-foreground">
|
|
{t("common.loading")}
|
|
</div>
|
|
) : filteredCredentials.length === 0 ? (
|
|
<div className="p-3 text-center text-sm text-muted-foreground">
|
|
{searchQuery
|
|
? t("credentials.noCredentialsMatchFilters")
|
|
: t("credentials.noCredentialsYet")}
|
|
</div>
|
|
) : (
|
|
<div className="grid grid-cols-1 gap-2.5">
|
|
{value && (
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className="w-full justify-start text-left rounded-lg px-3 py-2 text-destructive hover:bg-destructive/10 transition-colors duration-200"
|
|
onClick={handleClear}
|
|
>
|
|
{t("common.clear")}
|
|
</Button>
|
|
)}
|
|
{filteredCredentials.map((credential) => (
|
|
<Button
|
|
key={credential.id}
|
|
type="button"
|
|
variant="ghost"
|
|
size="sm"
|
|
className={`w-full justify-start text-left rounded-lg px-3 py-7 hover:bg-muted focus:bg-muted focus:outline-none transition-colors duration-200 ${
|
|
credential.id === value ? "bg-muted" : ""
|
|
}`}
|
|
onClick={() => handleCredentialSelect(credential)}
|
|
>
|
|
<div className="w-full">
|
|
<div className="flex items-center justify-between">
|
|
<span className="font-medium">
|
|
{credential.name}
|
|
</span>
|
|
</div>
|
|
<div className="text-xs text-muted-foreground mt-0.5">
|
|
{credential.username} • {credential.authType}
|
|
{credential.description &&
|
|
` • ${credential.description}`}
|
|
</div>
|
|
</div>
|
|
</Button>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</FormControl>
|
|
</FormItem>
|
|
);
|
|
}
|