mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-28 06:05:24 +00:00
331 lines
13 KiB
TypeScript
331 lines
13 KiB
TypeScript
import { useState } from "react";
|
|
import { useSshAuthProviders } from "@/hooks/useSshAuthProviders";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
ArrowUpDown,
|
|
Check,
|
|
ExternalLink,
|
|
Filter,
|
|
GripVertical,
|
|
Plus,
|
|
Search,
|
|
SlidersHorizontal,
|
|
X,
|
|
} from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { CredentialList } from "@/sidebar/CredentialList";
|
|
import { requestManage } from "@/manage/manage-requests";
|
|
import { useCredentialSidebarPreferences } from "@/sidebar/credential-tree/hooks/useCredentialSidebarPreferences";
|
|
import { useArrangeLock } from "@/sidebar/use-arrange-lock";
|
|
import { CustomizeCredentialsSidebarPanel } from "@/sidebar/CustomizeCredentialsSidebarPanel";
|
|
import { Button } from "@/components/button";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuCheckboxItem,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuSeparator,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/dropdown-menu";
|
|
import type { CredentialSortKey } from "@/types/credential-sidebar-preferences";
|
|
|
|
export function CredentialsPanel({
|
|
active: _active = true,
|
|
}: {
|
|
onEditingChange?: (editing: boolean) => void;
|
|
active?: boolean;
|
|
}) {
|
|
const { t } = useTranslation();
|
|
const sshAuthProviders = useSshAuthProviders();
|
|
const { preferences: sidebarPrefs, update: updateSidebarPrefs } =
|
|
useCredentialSidebarPreferences();
|
|
const [search, setSearch] = useState("");
|
|
const [allTags, setAllTags] = useState<string[]>([]);
|
|
const [customizePanelOpen, setCustomizePanelOpen] = useState(false);
|
|
|
|
const sortKey = sidebarPrefs.sort.key;
|
|
const { arrangeLocked, toggleArrangeLock } = useArrangeLock(
|
|
"credentialSidebarArrangeLocked",
|
|
);
|
|
const filterState = sidebarPrefs.filters;
|
|
const filterActive =
|
|
filterState.type.length > 0 || filterState.tags.length > 0;
|
|
|
|
function handleSortChange(key: CredentialSortKey) {
|
|
updateSidebarPrefs((prev) => ({
|
|
...prev,
|
|
sort: { ...prev.sort, key },
|
|
}));
|
|
}
|
|
|
|
function handleArrangeLockToggle() {
|
|
const unlocking = arrangeLocked;
|
|
toggleArrangeLock();
|
|
// Same reasoning as the hosts panel: reordering only sticks visually
|
|
// under manual sort, so unlocking switches to it.
|
|
if (unlocking && sortKey !== "manual") {
|
|
handleSortChange("manual");
|
|
toast.info(t("credentials.arrangeUnlockedSwitchedToManual"));
|
|
}
|
|
}
|
|
|
|
function handleFilterToggle<K extends keyof typeof filterState>(
|
|
group: K,
|
|
value: (typeof filterState)[K][number],
|
|
) {
|
|
updateSidebarPrefs((prev) => {
|
|
const arr = prev.filters[group] as string[];
|
|
const next = arr.includes(value as string)
|
|
? arr.filter((v) => v !== value)
|
|
: [...arr, value as string];
|
|
return {
|
|
...prev,
|
|
filters: { ...prev.filters, [group]: next },
|
|
};
|
|
});
|
|
}
|
|
|
|
function handleFilterClear() {
|
|
updateSidebarPrefs((prev) => ({
|
|
...prev,
|
|
filters: { type: [], tags: [] },
|
|
}));
|
|
}
|
|
|
|
return (
|
|
<div className="flex flex-col flex-1 min-h-0 overflow-hidden">
|
|
{
|
|
<div className="flex flex-col px-2 py-1.5 shrink-0 border-b border-border/60 gap-1.5">
|
|
<div className="flex items-center gap-2 px-2.5 h-7 bg-muted/60 border border-border/60 rounded-sm">
|
|
<Search className="size-3 text-muted-foreground/60 shrink-0" />
|
|
<input
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder={t("credentials.searchCredentials")}
|
|
className="flex-1 text-xs bg-transparent outline-none placeholder:text-muted-foreground/50 text-foreground min-w-0"
|
|
/>
|
|
{search && (
|
|
<button
|
|
onClick={() => setSearch("")}
|
|
className="text-muted-foreground/60 hover:text-muted-foreground transition-colors"
|
|
>
|
|
<X className="size-3" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex items-center gap-1.5 overflow-x-auto overflow-y-hidden toolbar-scrollbar">
|
|
<div className="flex items-center border border-border shrink-0">
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={`size-7 ${sortKey !== "default" || !arrangeLocked ? "text-accent-brand" : "text-muted-foreground hover:text-foreground"}`}
|
|
title={t("credentials.sortCredentials")}
|
|
>
|
|
<ArrowUpDown className="size-3.5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="start"
|
|
className="text-xs min-w-[160px]"
|
|
>
|
|
<DropdownMenuItem
|
|
onClick={() => handleSortChange("default")}
|
|
className="flex items-center gap-1.5"
|
|
>
|
|
{sortKey === "default" ? (
|
|
<Check className="size-3 shrink-0 text-accent-brand" />
|
|
) : (
|
|
<span className="size-3 shrink-0 inline-block" />
|
|
)}
|
|
{t("credentials.sortDefault")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
{(["name-asc", "name-desc"] as const).map((key) => (
|
|
<DropdownMenuItem
|
|
key={key}
|
|
onClick={() => handleSortChange(key)}
|
|
className="flex items-center gap-1.5"
|
|
>
|
|
{sortKey === key ? (
|
|
<Check className="size-3 shrink-0 text-accent-brand" />
|
|
) : (
|
|
<span className="size-3 shrink-0 inline-block" />
|
|
)}
|
|
{t(
|
|
`credentials.sort${key === "name-asc" ? "NameAsc" : "NameDesc"}`,
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
{(["username-asc", "username-desc"] as const).map((key) => (
|
|
<DropdownMenuItem
|
|
key={key}
|
|
onClick={() => handleSortChange(key)}
|
|
className="flex items-center gap-1.5"
|
|
>
|
|
{sortKey === key ? (
|
|
<Check className="size-3 shrink-0 text-accent-brand" />
|
|
) : (
|
|
<span className="size-3 shrink-0 inline-block" />
|
|
)}
|
|
{t(
|
|
`credentials.sort${key === "username-asc" ? "UsernameAsc" : "UsernameDesc"}`,
|
|
)}
|
|
</DropdownMenuItem>
|
|
))}
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuItem
|
|
onClick={() => handleSortChange("manual")}
|
|
className="flex items-center gap-1.5"
|
|
>
|
|
{sortKey === "manual" ? (
|
|
<Check className="size-3 shrink-0 text-accent-brand" />
|
|
) : (
|
|
<GripVertical className="size-3 shrink-0 text-muted-foreground/40" />
|
|
)}
|
|
{t("credentials.sortManual")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuCheckboxItem
|
|
checked={!arrangeLocked}
|
|
onCheckedChange={handleArrangeLockToggle}
|
|
onSelect={(event) => event.preventDefault()}
|
|
>
|
|
{t("credentials.arrangeUnlocked")}
|
|
</DropdownMenuCheckboxItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<div className="w-px self-stretch bg-border" />
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
className={`size-7 ${filterActive ? "text-accent-brand" : "text-muted-foreground hover:text-foreground"}`}
|
|
title={t("credentials.filterCredentials")}
|
|
>
|
|
<Filter className="size-3.5" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent
|
|
align="start"
|
|
className="text-xs min-w-[180px]"
|
|
>
|
|
{filterActive && (
|
|
<>
|
|
<DropdownMenuItem
|
|
onClick={handleFilterClear}
|
|
className="flex items-center gap-1.5 text-accent-brand"
|
|
>
|
|
<X className="size-3 shrink-0" />
|
|
{t("credentials.filterClearAll")}
|
|
</DropdownMenuItem>
|
|
<DropdownMenuSeparator />
|
|
</>
|
|
)}
|
|
<DropdownMenuLabel>
|
|
{t("credentials.filterTypeGroup")}
|
|
</DropdownMenuLabel>
|
|
{sshAuthProviders.providers
|
|
.filter((option) => option.credentialType)
|
|
.map((option) => (
|
|
<DropdownMenuCheckboxItem
|
|
key={option.type}
|
|
checked={filterState.type.includes(option.type)}
|
|
onCheckedChange={() =>
|
|
handleFilterToggle("type", option.type)
|
|
}
|
|
onSelect={(e) => e.preventDefault()}
|
|
>
|
|
{t(
|
|
`credentials.filterType${option.type.charAt(0).toUpperCase() + option.type.slice(1)}`,
|
|
{ defaultValue: t(option.labelKey) },
|
|
)}
|
|
</DropdownMenuCheckboxItem>
|
|
))}
|
|
{allTags.length > 0 && (
|
|
<>
|
|
<DropdownMenuSeparator />
|
|
<DropdownMenuLabel>
|
|
{t("credentials.filterTagsGroup")}
|
|
</DropdownMenuLabel>
|
|
{allTags.map((tag) => (
|
|
<DropdownMenuCheckboxItem
|
|
key={tag}
|
|
checked={filterState.tags.includes(tag)}
|
|
onCheckedChange={() =>
|
|
handleFilterToggle("tags", tag)
|
|
}
|
|
onSelect={(e) => e.preventDefault()}
|
|
>
|
|
{tag}
|
|
</DropdownMenuCheckboxItem>
|
|
))}
|
|
</>
|
|
)}
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
<div className="w-px self-stretch bg-border" />
|
|
<a
|
|
href="https://docs.termix.site/features/files-and-hosts/credentials"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
title={t("hosts.docsLink")}
|
|
className="flex items-center justify-center size-7 text-muted-foreground hover:text-foreground shrink-0 transition-colors"
|
|
>
|
|
<ExternalLink className="size-3.5" />
|
|
</a>
|
|
</div>
|
|
<div className="flex items-center border border-border shrink-0">
|
|
<button
|
|
onClick={() => setCustomizePanelOpen(true)}
|
|
title={t("credentials.customizeSidebar")}
|
|
className="flex items-center justify-center size-7 text-muted-foreground hover:text-foreground hover:bg-muted transition-colors shrink-0"
|
|
>
|
|
<SlidersHorizontal className="size-3.5" />
|
|
</button>
|
|
</div>
|
|
<div className="flex items-center border border-accent-brand/30 ml-auto shrink-0">
|
|
<button
|
|
onClick={() =>
|
|
requestManage({ kind: "credential", credentialId: null })
|
|
}
|
|
title={t("credentials.addCredential")}
|
|
className="flex items-center justify-center gap-1 h-7 px-2 text-[10px] font-medium text-accent-brand hover:bg-accent-brand/10 transition-colors"
|
|
>
|
|
<Plus className="size-3 shrink-0" />
|
|
<span className="hidden min-[280px]:inline">
|
|
{t("credentials.addCredential")}
|
|
</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<div className="flex flex-col flex-1 min-h-0">
|
|
<CredentialList
|
|
search={search}
|
|
sort={sortKey}
|
|
arrangeLocked={arrangeLocked}
|
|
filter={filterState}
|
|
density={sidebarPrefs.display.density}
|
|
trayTrigger={sidebarPrefs.display.trayTrigger}
|
|
showTags={sidebarPrefs.display.showTags}
|
|
onTagsChange={setAllTags}
|
|
/>
|
|
</div>
|
|
|
|
<CustomizeCredentialsSidebarPanel
|
|
open={customizePanelOpen}
|
|
onOpenChange={setCustomizePanelOpen}
|
|
preferences={sidebarPrefs}
|
|
update={updateSidebarPrefs}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|