mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-05 02:06:41 +00:00
move all components to components dir
This commit is contained in:
@@ -1,43 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DataTable } from "@app/components/ui/data-table";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
createSite?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
}
|
||||
|
||||
export function SitesDataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
createSite,
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={data}
|
||||
persistPageSize="sites-table"
|
||||
title={t('sites')}
|
||||
searchPlaceholder={t('searchSitesProgress')}
|
||||
searchColumn="name"
|
||||
onAdd={createSite}
|
||||
addButtonText={t('siteAdd')}
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={{
|
||||
id: "name",
|
||||
desc: false
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrowRight, DockIcon as Docker, Globe, Server, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export const SitesSplashCard = () => {
|
||||
const [isDismissed, setIsDismissed] = useState(true);
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const key = "sites-splash-card-dismissed";
|
||||
const t = useTranslations();
|
||||
|
||||
useEffect(() => {
|
||||
const dismissed = localStorage.getItem(key);
|
||||
if (dismissed === "true") {
|
||||
setIsDismissed(true);
|
||||
} else {
|
||||
setIsDismissed(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleDismiss = () => {
|
||||
setIsDismissed(true);
|
||||
localStorage.setItem(key, "true");
|
||||
};
|
||||
|
||||
if (isDismissed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="w-full mx-auto overflow-hidden mb-8 hidden md:block relative">
|
||||
<button
|
||||
onClick={handleDismiss}
|
||||
className="absolute top-2 right-2 p-2"
|
||||
aria-label={t('dismiss')}
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
<CardContent className="grid gap-6 p-6 sm:grid-cols-2">
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
<Globe className="text-blue-500" />
|
||||
Newt ({t('recommended')})
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
{t('siteNewtDescription')}
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Server className="text-green-500 w-4 h-4" />
|
||||
{t('siteRunsInDocker')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Server className="text-green-500 w-4 h-4" />
|
||||
{t('siteRunsInShell')}
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div className="mt-4">
|
||||
<Link
|
||||
href="https://docs.digpangolin.com/manage/sites/install-site"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Button
|
||||
className="w-full flex items-center"
|
||||
variant="secondary"
|
||||
>
|
||||
{t('siteInstallNewt')}{" "}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-xl font-semibold flex items-center gap-2">
|
||||
{t('siteWg')}
|
||||
</h3>
|
||||
<p className="text-sm">
|
||||
{t('siteWgAnyClients')}
|
||||
</p>
|
||||
<ul className="text-sm text-muted-foreground space-y-2">
|
||||
<li className="flex items-center gap-2">
|
||||
<Docker className="text-purple-500 w-4 h-4" />
|
||||
{t('siteWgCompatibleAllClients')}
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<Server className="text-purple-500 w-4 h-4" />
|
||||
{t('siteWgManualConfigurationRequired')}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default SitesSplashCard;
|
||||
@@ -1,389 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Column, ColumnDef } from "@tanstack/react-table";
|
||||
import { SitesDataTable } from "./SitesDataTable";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
ArrowRight,
|
||||
ArrowUpDown,
|
||||
Check,
|
||||
MoreHorizontal,
|
||||
X
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { AxiosResponse } from "axios";
|
||||
import { useState, useEffect } from "react";
|
||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||
import { toast } from "@app/hooks/useToast";
|
||||
import { formatAxiosError } from "@app/lib/api";
|
||||
import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { parseDataSize } from "@app/lib/dataSize";
|
||||
import { Badge } from "@app/components/ui/badge";
|
||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||
|
||||
export type SiteRow = {
|
||||
id: number;
|
||||
nice: string;
|
||||
name: string;
|
||||
mbIn: string;
|
||||
mbOut: string;
|
||||
orgId: string;
|
||||
type: "newt" | "wireguard";
|
||||
newtVersion?: string;
|
||||
newtUpdateAvailable?: boolean;
|
||||
online: boolean;
|
||||
address?: string;
|
||||
};
|
||||
|
||||
type SitesTableProps = {
|
||||
sites: SiteRow[];
|
||||
orgId: string;
|
||||
};
|
||||
|
||||
export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
|
||||
const [rows, setRows] = useState<SiteRow[]>(sites);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
|
||||
const api = createApiClient(useEnvContext());
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
// Update local state when props change (e.g., after refresh)
|
||||
useEffect(() => {
|
||||
setRows(sites);
|
||||
}, [sites]);
|
||||
|
||||
const refreshData = async () => {
|
||||
console.log("Data refreshed");
|
||||
setIsRefreshing(true);
|
||||
try {
|
||||
await new Promise((resolve) => setTimeout(resolve, 200));
|
||||
router.refresh();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: t("error"),
|
||||
description: t("refreshError"),
|
||||
variant: "destructive"
|
||||
});
|
||||
} finally {
|
||||
setIsRefreshing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSite = (siteId: number) => {
|
||||
api.delete(`/site/${siteId}`)
|
||||
.catch((e) => {
|
||||
console.error(t("siteErrorDelete"), e);
|
||||
toast({
|
||||
variant: "destructive",
|
||||
title: t("siteErrorDelete"),
|
||||
description: formatAxiosError(e, t("siteErrorDelete"))
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
router.refresh();
|
||||
setIsDeleteModalOpen(false);
|
||||
|
||||
const newRows = rows.filter((row) => row.id !== siteId);
|
||||
|
||||
setRows(newRows);
|
||||
});
|
||||
};
|
||||
|
||||
const columns: ColumnDef<SiteRow>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("name")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "online",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("online")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
if (
|
||||
originalRow.type == "newt" ||
|
||||
originalRow.type == "wireguard"
|
||||
) {
|
||||
if (originalRow.online) {
|
||||
return (
|
||||
<span className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>{t("online")}</span>
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<span className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>{t("offline")}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return <span>-</span>;
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "nice",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
className="hidden md:flex whitespace-nowrap"
|
||||
>
|
||||
{t("site")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="hidden md:block whitespace-nowrap">
|
||||
{row.original.nice}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "mbIn",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("dataIn")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
sortingFn: (rowA, rowB) =>
|
||||
parseDataSize(rowA.original.mbIn) -
|
||||
parseDataSize(rowB.original.mbIn)
|
||||
},
|
||||
{
|
||||
accessorKey: "mbOut",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("dataOut")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
sortingFn: (rowA, rowB) =>
|
||||
parseDataSize(rowA.original.mbOut) -
|
||||
parseDataSize(rowB.original.mbOut)
|
||||
},
|
||||
{
|
||||
accessorKey: "type",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
{t("connectionType")}
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const originalRow = row.original;
|
||||
|
||||
if (originalRow.type === "newt") {
|
||||
return (
|
||||
<div className="flex items-center space-x-1">
|
||||
<Badge variant="secondary">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>Newt</span>
|
||||
{originalRow.newtVersion && (
|
||||
<span className="text-xs text-gray-500">
|
||||
v{originalRow.newtVersion}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Badge>
|
||||
{originalRow.newtUpdateAvailable && (
|
||||
<InfoPopup
|
||||
info={t("newtUpdateAvailableInfo")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (originalRow.type === "wireguard") {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>WireGuard</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (originalRow.type === "local") {
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span>{t("local")}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
...(env.flags.enableClients ? [{
|
||||
accessorKey: "address",
|
||||
header: ({ column }: { column: Column<SiteRow, unknown> }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
Address
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}] : []),
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }) => {
|
||||
const siteRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
<span className="sr-only">Open menu</span>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<Link
|
||||
className="block w-full"
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
<DropdownMenuItem>
|
||||
{t("viewSettings")}
|
||||
</DropdownMenuItem>
|
||||
</Link>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setSelectedSite(siteRow);
|
||||
setIsDeleteModalOpen(true);
|
||||
}}
|
||||
>
|
||||
<span className="text-red-500">
|
||||
{t("delete")}
|
||||
</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Link
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
<Button variant={"secondary"} size="sm">
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{selectedSite && (
|
||||
<ConfirmDeleteDialog
|
||||
open={isDeleteModalOpen}
|
||||
setOpen={(val) => {
|
||||
setIsDeleteModalOpen(val);
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="space-y-4">
|
||||
<p>
|
||||
{t("siteQuestionRemove", {
|
||||
selectedSite:
|
||||
selectedSite?.name || selectedSite?.id
|
||||
})}
|
||||
</p>
|
||||
|
||||
<p>{t("siteMessageRemove")}</p>
|
||||
|
||||
<p>{t("siteMessageConfirm")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("siteConfirmDelete")}
|
||||
onConfirm={async () => deleteSite(selectedSite!.id)}
|
||||
string={selectedSite.name}
|
||||
title={t("siteDelete")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<SitesDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
createSite={() =>
|
||||
router.push(`/${orgId}/settings/sites/create`)
|
||||
}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import { useSiteContext } from "@app/hooks/useSiteContext";
|
||||
import {
|
||||
InfoSection,
|
||||
InfoSectionContent,
|
||||
InfoSections,
|
||||
InfoSectionTitle
|
||||
} from "@app/components/InfoSection";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
|
||||
type SiteInfoCardProps = {};
|
||||
|
||||
export default function SiteInfoCard({}: SiteInfoCardProps) {
|
||||
const { site, updateSite } = useSiteContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
|
||||
const getConnectionTypeString = (type: string) => {
|
||||
if (type === "newt") {
|
||||
return "Newt";
|
||||
} else if (type === "wireguard") {
|
||||
return "WireGuard";
|
||||
} else if (type === "local") {
|
||||
return t("local");
|
||||
} else {
|
||||
return t("unknown");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<AlertDescription>
|
||||
<InfoSections cols={env.flags.enableClients ? 3 : 2}>
|
||||
{(site.type == "newt" || site.type == "wireguard") && (
|
||||
<>
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("status")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{site.online ? (
|
||||
<div className="text-green-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-green-500 rounded-full"></div>
|
||||
<span>{t("online")}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-neutral-500 flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-gray-500 rounded-full"></div>
|
||||
<span>{t("offline")}</span>
|
||||
</div>
|
||||
)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
</>
|
||||
)}
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>
|
||||
{t("connectionType")}
|
||||
</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{getConnectionTypeString(site.type)}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
|
||||
{env.flags.enableClients && site.type == "newt" && (
|
||||
<InfoSection>
|
||||
<InfoSectionTitle>Address</InfoSectionTitle>
|
||||
<InfoSectionContent>
|
||||
{site.address?.split("/")[0]}
|
||||
</InfoSectionContent>
|
||||
</InfoSection>
|
||||
)}
|
||||
</InfoSections>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { redirect } from "next/navigation";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { HorizontalTabs } from "@app/components/HorizontalTabs";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import SiteInfoCard from "./SiteInfoCard";
|
||||
import SiteInfoCard from "../../../../../components/SiteInfoCard";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface SettingsLayoutProps {
|
||||
|
||||
@@ -2,9 +2,9 @@ import { internal } from "@app/lib/api";
|
||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||
import { ListSitesResponse } from "@server/routers/site";
|
||||
import { AxiosResponse } from "axios";
|
||||
import SitesTable, { SiteRow } from "./SitesTable";
|
||||
import SitesTable, { SiteRow } from "../../../../components/SitesTable";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import SitesSplashCard from "./SitesSplashCard";
|
||||
import SitesSplashCard from "../../../../components/SitesSplashCard";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
type SitesPageProps = {
|
||||
|
||||
Reference in New Issue
Block a user