mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
update tables
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { db, olms } from "@server/db";
|
||||
import { db, olms, users } from "@server/db";
|
||||
import {
|
||||
clients,
|
||||
orgs,
|
||||
@@ -19,7 +19,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
||||
import NodeCache from "node-cache";
|
||||
import semver from "semver";
|
||||
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
const olmVersionCache = new NodeCache({ stdTTL: 3600 });
|
||||
|
||||
async function getLatestOlmVersion(): Promise<string | null> {
|
||||
try {
|
||||
@@ -29,7 +29,7 @@ async function getLatestOlmVersion(): Promise<string | null> {
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
const timeoutId = setTimeout(() => controller.abort(), 1500);
|
||||
|
||||
const response = await fetch(
|
||||
"https://api.github.com/repos/fosrl/olm/tags",
|
||||
@@ -112,11 +112,15 @@ function queryClients(orgId: string, accessibleClientIds: number[]) {
|
||||
orgName: orgs.name,
|
||||
type: clients.type,
|
||||
online: clients.online,
|
||||
olmVersion: olms.version
|
||||
olmVersion: olms.version,
|
||||
userId: clients.userId,
|
||||
username: users.username,
|
||||
userEmail: users.email
|
||||
})
|
||||
.from(clients)
|
||||
.leftJoin(orgs, eq(clients.orgId, orgs.orgId))
|
||||
.leftJoin(olms, eq(clients.clientId, olms.clientId))
|
||||
.leftJoin(users, eq(clients.userId, users.userId))
|
||||
.where(
|
||||
and(
|
||||
inArray(clients.clientId, accessibleClientIds),
|
||||
|
||||
@@ -230,10 +230,11 @@ export default function ExitNodesTable({
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const nodeRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
|
||||
@@ -47,6 +47,9 @@ export default async function ClientsPage(props: ClientsPageProps) {
|
||||
online: client.online,
|
||||
olmVersion: client.olmVersion || undefined,
|
||||
olmUpdateAvailable: client.olmUpdateAvailable || false,
|
||||
userId: client.userId,
|
||||
username: client.username,
|
||||
userEmail: client.userEmail
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -899,7 +899,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "healthCheck",
|
||||
header: t("healthCheck"),
|
||||
header: () => (<span className="p-3">{t("healthCheck")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
const isEnabled = row.original.hcEnabled;
|
||||
@@ -971,7 +971,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "path",
|
||||
header: t("matchPath"),
|
||||
header: () => (<span className="p-3">{t("matchPath")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const hasPathMatch = !!(
|
||||
row.original.path || row.original.pathMatchType
|
||||
@@ -1033,7 +1033,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: t("address"),
|
||||
header: () => (<span className="p-3">{t("address")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const selectedSite = sites.find(
|
||||
(site) => site.siteId === row.original.siteId
|
||||
@@ -1247,7 +1247,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "rewritePath",
|
||||
header: t("rewritePath"),
|
||||
header: () => (<span className="p-3">{t("rewritePath")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const hasRewritePath = !!(
|
||||
row.original.rewritePath || row.original.rewritePathType
|
||||
@@ -1317,7 +1317,7 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "enabled",
|
||||
header: t("enabled"),
|
||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<Switch
|
||||
@@ -1338,8 +1338,9 @@ export default function ReverseProxyTargets(props: {
|
||||
|
||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<div className="flex items-center w-full">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => removeTarget(row.original.targetId)}
|
||||
|
||||
@@ -465,7 +465,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "action",
|
||||
header: t('rulesAction'),
|
||||
header: () => (<span className="p-3">{t('rulesAction')}</span>),
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.action}
|
||||
@@ -488,7 +488,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "match",
|
||||
header: t('rulesMatchType'),
|
||||
header: () => (<span className="p-3">{t('rulesMatchType')}</span>),
|
||||
cell: ({ row }) => (
|
||||
<Select
|
||||
defaultValue={row.original.match}
|
||||
@@ -512,7 +512,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "value",
|
||||
header: t('value'),
|
||||
header: () => (<span className="p-3">{t('value')}</span>),
|
||||
cell: ({ row }) => (
|
||||
row.original.match === "COUNTRY" ? (
|
||||
<Popover>
|
||||
@@ -573,7 +573,7 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: t('enabled'),
|
||||
header: () => (<span className="p-3">{t('enabled')}</span>),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={row.original.enabled}
|
||||
@@ -585,8 +585,9 @@ export default function ResourceRules(props: {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t('actions')}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => removeRule(row.original.ruleId)}
|
||||
|
||||
@@ -793,7 +793,7 @@ export default function Page() {
|
||||
|
||||
const healthCheckColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "healthCheck",
|
||||
header: t("healthCheck"),
|
||||
header: () => (<span className="p-3">{t("healthCheck")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const status = row.original.hcHealth || "unknown";
|
||||
const isEnabled = row.original.hcEnabled;
|
||||
@@ -865,7 +865,7 @@ export default function Page() {
|
||||
|
||||
const matchPathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "path",
|
||||
header: t("matchPath"),
|
||||
header: () => (<span className="p-3">{t("matchPath")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const hasPathMatch = !!(
|
||||
row.original.path || row.original.pathMatchType
|
||||
@@ -927,7 +927,7 @@ export default function Page() {
|
||||
|
||||
const addressColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "address",
|
||||
header: t("address"),
|
||||
header: () => (<span className="p-3">{t("address")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const selectedSite = sites.find(
|
||||
(site) => site.siteId === row.original.siteId
|
||||
@@ -1141,7 +1141,7 @@ export default function Page() {
|
||||
|
||||
const rewritePathColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "rewritePath",
|
||||
header: t("rewritePath"),
|
||||
header: () => (<span className="p-3">{t("rewritePath")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const hasRewritePath = !!(
|
||||
row.original.rewritePath || row.original.rewritePathType
|
||||
@@ -1211,7 +1211,7 @@ export default function Page() {
|
||||
|
||||
const enabledColumn: ColumnDef<LocalTarget> = {
|
||||
accessorKey: "enabled",
|
||||
header: t("enabled"),
|
||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-center w-full">
|
||||
<Switch
|
||||
@@ -1232,6 +1232,7 @@ export default function Page() {
|
||||
|
||||
const actionsColumn: ColumnDef<LocalTarget> = {
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end w-full">
|
||||
<Button
|
||||
|
||||
@@ -182,11 +182,21 @@ export default function UsersTable({ users }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
router.push(`/admin/users/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -210,16 +220,6 @@ export default function UsersTable({ users }: Props) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(`/admin/users/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -238,13 +238,9 @@ export default function UsersTable({ users }: Props) {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("userQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("userQuestionRemove")}</p>
|
||||
|
||||
<p>
|
||||
{t("userMessageRemove")}
|
||||
</p>
|
||||
<p>{t("userMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("userDeleteConfirm")}
|
||||
|
||||
@@ -133,10 +133,19 @@ export default function IdpTable({ idps }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const siteRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center">
|
||||
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -167,16 +176,6 @@ export default function IdpTable({ idps }: Props) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link href={`/admin/idp/${siteRow.idpId}/general`}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -201,11 +201,21 @@ export default function UsersTable({ users }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
router.push(`/admin/users/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
@@ -229,16 +239,6 @@ export default function UsersTable({ users }: Props) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
router.push(`/admin/users/${r.id}`);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -104,7 +104,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: t("key"),
|
||||
header: () => (<span className="p-3">{t("key")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
@@ -112,7 +112,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: t("createdAt"),
|
||||
header: () => (<span className="p-3">{t("createdAt")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")} </span>;
|
||||
@@ -120,10 +120,19 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link href={`/admin/api-keys/${r.id}`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -153,14 +162,6 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href={`/admin/api-keys/${r.id}`}>
|
||||
<Button variant={"secondary"} className="ml-2" size="sm">
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -178,13 +179,9 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("apiKeysQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("apiKeysQuestionRemove")}</p>
|
||||
|
||||
<p>
|
||||
{t("apiKeysMessageRemove")}
|
||||
</p>
|
||||
<p>{t("apiKeysMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("apiKeysDeleteConfirm")}
|
||||
|
||||
@@ -157,12 +157,10 @@ export default function BlueprintsTable({ blueprints, orgId }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => {
|
||||
return null;
|
||||
},
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex justify-end">
|
||||
<div className="flex">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="items-center"
|
||||
|
||||
@@ -19,9 +19,7 @@ export default function SiteInfoCard({}: ClientInfoCardProps) {
|
||||
|
||||
return (
|
||||
<Alert>
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle className="font-semibold">{t("clientInformation")}</AlertTitle>
|
||||
<AlertDescription className="mt-4">
|
||||
<AlertDescription>
|
||||
<InfoSections cols={2}>
|
||||
<>
|
||||
<InfoSection>
|
||||
|
||||
@@ -11,6 +11,8 @@ interface DataTableProps<TData, TValue> {
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
addClient?: () => void;
|
||||
columnVisibility?: Record<string, boolean>;
|
||||
enableColumnVisibility?: boolean;
|
||||
}
|
||||
|
||||
export function ClientsDataTable<TData, TValue>({
|
||||
@@ -18,7 +20,9 @@ export function ClientsDataTable<TData, TValue>({
|
||||
data,
|
||||
addClient,
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
isRefreshing,
|
||||
columnVisibility,
|
||||
enableColumnVisibility
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
return (
|
||||
<DataTable
|
||||
@@ -32,6 +36,8 @@ export function ClientsDataTable<TData, TValue>({
|
||||
onRefresh={onRefresh}
|
||||
isRefreshing={isRefreshing}
|
||||
addButtonText="Add Client"
|
||||
columnVisibility={columnVisibility}
|
||||
enableColumnVisibility={enableColumnVisibility}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,9 @@ export type ClientRow = {
|
||||
online: boolean;
|
||||
olmVersion?: string;
|
||||
olmUpdateAvailable: boolean;
|
||||
userId: string | null;
|
||||
username: string | null;
|
||||
userEmail: string | null;
|
||||
};
|
||||
|
||||
type ClientTableProps = {
|
||||
@@ -115,6 +118,37 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "userId",
|
||||
header: ({ column }) => {
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={() =>
|
||||
column.toggleSorting(column.getIsSorted() === "asc")
|
||||
}
|
||||
>
|
||||
User
|
||||
<ArrowUpDown className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return r.userId ? (
|
||||
<Link
|
||||
href={`/${r.orgId}/settings/access/users/${r.userId}`}
|
||||
>
|
||||
<Button variant="outline">
|
||||
{r.userEmail || r.username || r.userId}
|
||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
"-"
|
||||
);
|
||||
}
|
||||
},
|
||||
// {
|
||||
// accessorKey: "siteName",
|
||||
// header: ({ column }) => {
|
||||
@@ -239,9 +273,7 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
</div>
|
||||
</Badge>
|
||||
{originalRow.olmUpdateAvailable && (
|
||||
<InfoPopup
|
||||
info={t("olmUpdateAvailableInfo")}
|
||||
/>
|
||||
<InfoPopup info={t("olmUpdateAvailableInfo")} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -265,11 +297,19 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const clientRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
|
||||
<div className="flex items-center">
|
||||
<Link
|
||||
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -296,14 +336,6 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${clientRow.orgId}/settings/clients/${clientRow.id}`}
|
||||
>
|
||||
<Button variant={"secondary"} className="ml-2">
|
||||
Edit
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -321,12 +353,8 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("deleteClientQuestion")}
|
||||
</p>
|
||||
<p>
|
||||
{t("clientMessageRemove")}
|
||||
</p>
|
||||
<p>{t("deleteClientQuestion")}</p>
|
||||
<p>{t("clientMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText="Confirm Delete Client"
|
||||
@@ -344,6 +372,11 @@ export default function ClientsTable({ clients, orgId }: ClientTableProps) {
|
||||
}}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
columnVisibility={{
|
||||
client: false,
|
||||
subnet: false
|
||||
}}
|
||||
enableColumnVisibility={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -184,14 +184,14 @@ const DockerContainersTable: FC<{
|
||||
const columns: ColumnDef<Container>[] = [
|
||||
{
|
||||
accessorKey: "name",
|
||||
header: t("containerName"),
|
||||
header: () => (<span className="p-3">{t("containerName")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="font-medium">{row.original.name}</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
accessorKey: "image",
|
||||
header: t("containerImage"),
|
||||
header: () => (<span className="p-3">{t("containerImage")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{row.original.image}
|
||||
@@ -200,7 +200,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
accessorKey: "state",
|
||||
header: t("containerState"),
|
||||
header: () => (<span className="p-3">{t("containerState")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<Badge
|
||||
variant={
|
||||
@@ -215,7 +215,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
accessorKey: "networks",
|
||||
header: t("containerNetworks"),
|
||||
header: () => (<span className="p-3">{t("containerNetworks")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const networks = Object.keys(row.original.networks);
|
||||
return (
|
||||
@@ -233,7 +233,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
accessorKey: "hostname",
|
||||
header: t("containerHostnameIp"),
|
||||
header: () => (<span className="p-3">{t("containerHostnameIp")}</span>),
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => (
|
||||
<div className="text-sm font-mono">
|
||||
@@ -243,7 +243,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
accessorKey: "labels",
|
||||
header: t("containerLabels"),
|
||||
header: () => (<span className="p-3">{t("containerLabels")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const labels = row.original.labels || {};
|
||||
const labelEntries = Object.entries(labels);
|
||||
@@ -295,7 +295,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
accessorKey: "ports",
|
||||
header: t("containerPorts"),
|
||||
header: () => (<span className="p-3">{t("containerPorts")}</span>),
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const ports = getExposedPorts(row.original);
|
||||
@@ -353,7 +353,7 @@ const DockerContainersTable: FC<{
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: t("containerActions"),
|
||||
header: () => (<span className="p-3">{t("containerActions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const ports = getExposedPorts(row.original);
|
||||
return (
|
||||
|
||||
@@ -124,7 +124,6 @@ export function DNSRecordsDataTable<TData, TValue>({
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow
|
||||
key={headerGroup.id}
|
||||
className="bg-secondary dark:bg-transparent"
|
||||
>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
|
||||
@@ -209,16 +209,16 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => {
|
||||
const domain = row.original;
|
||||
const isRestarting = restartingDomains.has(domain.domainId);
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{domain.failed && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => restartDomain(domain.domainId)}
|
||||
disabled={isRestarting}
|
||||
>
|
||||
@@ -232,6 +232,14 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
: t("restart", { fallback: "Restart" })}
|
||||
</Button>
|
||||
)}
|
||||
<Link
|
||||
href={`/${orgId}/settings/domains/${domain.domainId}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
@@ -266,15 +274,6 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Link
|
||||
href={`/${orgId}/settings/domains/${domain.domainId}`}
|
||||
>
|
||||
<Button variant={"secondary"} size="sm">
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
{/* <Button
|
||||
variant="secondary"
|
||||
|
||||
@@ -69,11 +69,11 @@ export default function InvitationsTable({
|
||||
const columns: ColumnDef<InvitationRow>[] = [
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: t("email")
|
||||
header: () => (<span className="p-3">{t("email")}</span>)
|
||||
},
|
||||
{
|
||||
accessorKey: "expiresAt",
|
||||
header: t("expiresAt"),
|
||||
header: () => (<span className="p-3">{t("expiresAt")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const expiresAt = new Date(row.original.expiresAt);
|
||||
const isExpired = expiresAt < new Date();
|
||||
@@ -87,7 +87,7 @@ export default function InvitationsTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: t("role")
|
||||
header: () => (<span className="p-3">{t("role")}</span>)
|
||||
},
|
||||
{
|
||||
id: "dots",
|
||||
|
||||
@@ -123,10 +123,10 @@ export function LicenseKeysDataTable({
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
header: () => <span className="p-3">{t("actions")}</span>,
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button variant={"outline"}
|
||||
onClick={() => onDelete(row.original)}
|
||||
>
|
||||
{t("delete")}
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function OrgApiKeysTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "key",
|
||||
header: t("key"),
|
||||
header: () => (<span className="p-3">{t("key")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span className="font-mono">{r.key}</span>;
|
||||
@@ -115,7 +115,7 @@ export default function OrgApiKeysTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "createdAt",
|
||||
header: t("createdAt"),
|
||||
header: () => (<span className="p-3">{t("createdAt")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return <span>{moment(r.createdAt).format("lll")}</span>;
|
||||
@@ -123,10 +123,19 @@ export default function OrgApiKeysTable({
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const r = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center">
|
||||
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -158,17 +167,6 @@ export default function OrgApiKeysTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<Link href={`/${orgId}/settings/api-keys/${r.id}`}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
|
||||
info={mapping}
|
||||
/>
|
||||
) : (
|
||||
"--"
|
||||
"-"
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -129,18 +129,19 @@ export default function PolicyTable({ policies, onDelete, onAdd, onEdit }: Props
|
||||
info={mapping}
|
||||
/>
|
||||
) : (
|
||||
"--"
|
||||
"-"
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t('actions')}</span>),
|
||||
cell: ({ row }) => {
|
||||
const policy = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
onClick={() => onEdit(policy)}
|
||||
>
|
||||
|
||||
@@ -9,13 +9,17 @@ import {
|
||||
SortingState,
|
||||
getSortedRowModel,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel
|
||||
getFilteredRowModel,
|
||||
VisibilityState
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { Button } from "@app/components/ui/button";
|
||||
import {
|
||||
@@ -25,7 +29,8 @@ import {
|
||||
ArrowUpRight,
|
||||
ShieldOff,
|
||||
ShieldCheck,
|
||||
RefreshCw
|
||||
RefreshCw,
|
||||
Columns
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -106,15 +111,14 @@ type ResourcesTableProps = {
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
PAGE_SIZE: 'datatable-page-size',
|
||||
PAGE_SIZE: "datatable-page-size",
|
||||
getTablePageSize: (tableId?: string) =>
|
||||
tableId ? `datatable-${tableId}-page-size` : STORAGE_KEYS.PAGE_SIZE
|
||||
};
|
||||
|
||||
const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
if (typeof window === 'undefined') return defaultSize;
|
||||
if (typeof window === "undefined") return defaultSize;
|
||||
|
||||
try {
|
||||
const key = STORAGE_KEYS.getTablePageSize(tableId);
|
||||
@@ -126,23 +130,22 @@ const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to read page size from localStorage:', error);
|
||||
console.warn("Failed to read page size from localStorage:", error);
|
||||
}
|
||||
return defaultSize;
|
||||
};
|
||||
|
||||
const setStoredPageSize = (pageSize: number, tableId?: string): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const key = STORAGE_KEYS.getTablePageSize(tableId);
|
||||
localStorage.setItem(key, pageSize.toString());
|
||||
} catch (error) {
|
||||
console.warn('Failed to save page size to localStorage:', error);
|
||||
console.warn("Failed to save page size to localStorage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export default function ResourcesTable({
|
||||
resources,
|
||||
internalResources,
|
||||
@@ -159,10 +162,10 @@ export default function ResourcesTable({
|
||||
const api = createApiClient({ env });
|
||||
|
||||
const [proxyPageSize, setProxyPageSize] = useState<number>(() =>
|
||||
getStoredPageSize('proxy-resources', 20)
|
||||
getStoredPageSize("proxy-resources", 20)
|
||||
);
|
||||
const [internalPageSize, setInternalPageSize] = useState<number>(() =>
|
||||
getStoredPageSize('internal-resources', 20)
|
||||
getStoredPageSize("internal-resources", 20)
|
||||
);
|
||||
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@@ -190,6 +193,8 @@ export default function ResourcesTable({
|
||||
useState<ColumnFiltersState>([]);
|
||||
const [internalGlobalFilter, setInternalGlobalFilter] = useState<any>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [proxyColumnVisibility, setProxyColumnVisibility] = useState<VisibilityState>({});
|
||||
const [internalColumnVisibility, setInternalColumnVisibility] = useState<VisibilityState>({});
|
||||
|
||||
const currentView = searchParams.get("view") || defaultView;
|
||||
|
||||
@@ -384,15 +389,23 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "protocol",
|
||||
header: t("protocol"),
|
||||
header: () => (<span className="p-3">{t("protocol")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return <span>{resourceRow.http ? (resourceRow.ssl ? "HTTPS" : "HTTP") : resourceRow.protocol.toUpperCase()}</span>;
|
||||
return (
|
||||
<span>
|
||||
{resourceRow.http
|
||||
? resourceRow.ssl
|
||||
? "HTTPS"
|
||||
: "HTTP"
|
||||
: resourceRow.protocol.toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: "domain",
|
||||
header: t("access"),
|
||||
header: () => (<span className="p-3">{t("access")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
@@ -455,7 +468,7 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
header: t("enabled"),
|
||||
header: () => (<span className="p-3">{t("enabled")}</span>),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={
|
||||
@@ -474,10 +487,19 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.nice}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -508,18 +530,6 @@ export default function ResourcesTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/resources/${resourceRow.nice}`}
|
||||
>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -545,14 +555,14 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "siteName",
|
||||
header: t("siteName"),
|
||||
header: () => (<span className="p-3">{t("siteName")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<Link
|
||||
href={`/${resourceRow.orgId}/settings/sites/${resourceRow.siteNiceId}`}
|
||||
>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline">
|
||||
{resourceRow.siteName}
|
||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
@@ -562,7 +572,7 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "protocol",
|
||||
header: t("protocol"),
|
||||
header: () => (<span className="p-3">{t("protocol")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
||||
@@ -570,7 +580,7 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "proxyPort",
|
||||
header: t("proxyPort"),
|
||||
header: () => (<span className="p-3">{t("proxyPort")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
@@ -583,7 +593,7 @@ export default function ResourcesTable({
|
||||
},
|
||||
{
|
||||
accessorKey: "destination",
|
||||
header: t("resourcesTableDestination"),
|
||||
header: () => (<span className="p-3">{t("resourcesTableDestination")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
const destination = `${resourceRow.destinationIp}:${resourceRow.destinationPort}`;
|
||||
@@ -593,10 +603,20 @@ export default function ResourcesTable({
|
||||
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant={"outline"}
|
||||
onClick={() => {
|
||||
setEditingResource(resourceRow);
|
||||
setIsEditDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -621,16 +641,6 @@ export default function ResourcesTable({
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setEditingResource(resourceRow);
|
||||
setIsEditDialogOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -647,6 +657,7 @@ export default function ResourcesTable({
|
||||
onColumnFiltersChange: setProxyColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onGlobalFilterChange: setProxyGlobalFilter,
|
||||
onColumnVisibilityChange: setProxyColumnVisibility,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: proxyPageSize,
|
||||
@@ -656,7 +667,8 @@ export default function ResourcesTable({
|
||||
state: {
|
||||
sorting: proxySorting,
|
||||
columnFilters: proxyColumnFilters,
|
||||
globalFilter: proxyGlobalFilter
|
||||
globalFilter: proxyGlobalFilter,
|
||||
columnVisibility: proxyColumnVisibility
|
||||
}
|
||||
});
|
||||
|
||||
@@ -670,6 +682,7 @@ export default function ResourcesTable({
|
||||
onColumnFiltersChange: setInternalColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onGlobalFilterChange: setInternalGlobalFilter,
|
||||
onColumnVisibilityChange: setInternalColumnVisibility,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: internalPageSize,
|
||||
@@ -679,18 +692,19 @@ export default function ResourcesTable({
|
||||
state: {
|
||||
sorting: internalSorting,
|
||||
columnFilters: internalColumnFilters,
|
||||
globalFilter: internalGlobalFilter
|
||||
globalFilter: internalGlobalFilter,
|
||||
columnVisibility: internalColumnVisibility
|
||||
}
|
||||
});
|
||||
|
||||
const handleProxyPageSizeChange = (newPageSize: number) => {
|
||||
setProxyPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, 'proxy-resources');
|
||||
setStoredPageSize(newPageSize, "proxy-resources");
|
||||
};
|
||||
|
||||
const handleInternalPageSizeChange = (newPageSize: number) => {
|
||||
setInternalPageSize(newPageSize);
|
||||
setStoredPageSize(newPageSize, 'internal-resources');
|
||||
setStoredPageSize(newPageSize, "internal-resources");
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -704,12 +718,8 @@ export default function ResourcesTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("resourceQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{t("resourceMessageRemove")}
|
||||
</p>
|
||||
<p>{t("resourceQuestionRemove")}</p>
|
||||
<p>{t("resourceMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
@@ -728,12 +738,8 @@ export default function ResourcesTable({
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("resourceQuestionRemove")}
|
||||
</p>
|
||||
<p>
|
||||
{t("resourceMessageRemove")}
|
||||
</p>
|
||||
<p>{t("resourceQuestionRemove")}</p>
|
||||
<p>{t("resourceMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
buttonText={t("resourceDeleteConfirm")}
|
||||
@@ -771,6 +777,80 @@ export default function ResourcesTable({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:justify-end">
|
||||
{currentView === "proxy" && proxyTable.getAllColumns().some((column) => column.getCanHide()) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Columns className="mr-0 sm:mr-2 h-4 w-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{t("columns") || "Columns"}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>
|
||||
{t("toggleColumns") || "Toggle columns"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{proxyTable
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{typeof column.columnDef.header === "string"
|
||||
? column.columnDef.header
|
||||
: column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{currentView === "internal" && internalTable.getAllColumns().some((column) => column.getCanHide()) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Columns className="mr-0 sm:mr-2 h-4 w-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{t("columns") || "Columns"}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuLabel>
|
||||
{t("toggleColumns") || "Toggle columns"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{internalTable
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
>
|
||||
{typeof column.columnDef.header === "string"
|
||||
? column.columnDef.header
|
||||
: column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
<div>
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -778,14 +858,14 @@ export default function ResourcesTable({
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("refresh")}
|
||||
<span className="hidden sm:inline">
|
||||
{t("refresh")}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
{getActionButton()}
|
||||
</div>
|
||||
<div>{getActionButton()}</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -796,23 +876,25 @@ export default function ResourcesTable({
|
||||
.getHeaderGroups()
|
||||
.map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map(
|
||||
(header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
{headerGroup.headers
|
||||
.filter((header) => header.column.getIsVisible())
|
||||
.map(
|
||||
(header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
@@ -867,7 +949,9 @@ export default function ResourcesTable({
|
||||
<div className="mt-4">
|
||||
<DataTablePagination
|
||||
table={proxyTable}
|
||||
onPageSizeChange={handleProxyPageSizeChange}
|
||||
onPageSizeChange={
|
||||
handleProxyPageSizeChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -897,23 +981,25 @@ export default function ResourcesTable({
|
||||
.getHeaderGroups()
|
||||
.map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map(
|
||||
(header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
{headerGroup.headers
|
||||
.filter((header) => header.column.getIsVisible())
|
||||
.map(
|
||||
(header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header
|
||||
.column
|
||||
.columnDef
|
||||
.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</TableHead>
|
||||
)
|
||||
)}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
@@ -968,7 +1054,9 @@ export default function ResourcesTable({
|
||||
<div className="mt-4">
|
||||
<DataTablePagination
|
||||
table={internalTable}
|
||||
onPageSizeChange={handleInternalPageSizeChange}
|
||||
onPageSizeChange={
|
||||
handleInternalPageSizeChange
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
@@ -80,18 +80,18 @@ export default function UsersTable({ roles: r }: RolesTableProps) {
|
||||
},
|
||||
{
|
||||
accessorKey: "description",
|
||||
header: t("description")
|
||||
header: () => (<span className="p-3">{t("description")}</span>)
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const roleRow = row.original;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center">
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
size="sm"
|
||||
variant={"outline"}
|
||||
disabled={roleRow.isAdmin || false}
|
||||
onClick={() => {
|
||||
setIsDeleteModalOpen(true);
|
||||
|
||||
@@ -122,7 +122,7 @@ export default function ShareLinksTable({
|
||||
const r = row.original;
|
||||
return (
|
||||
<Link href={`/${orgId}/settings/resources/${r.resourceNiceId}`}>
|
||||
<Button variant="outline" size="sm">
|
||||
<Button variant="outline">
|
||||
{r.resourceName}
|
||||
<ArrowUpRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
@@ -254,10 +254,11 @@ export default function ShareLinksTable({
|
||||
},
|
||||
{
|
||||
id: "delete",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const resourceRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end space-x-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* <DropdownMenu> */}
|
||||
{/* <DropdownMenuTrigger asChild> */}
|
||||
{/* <Button variant="ghost" className="h-8 w-8 p-0"> */}
|
||||
@@ -281,9 +282,7 @@ export default function ShareLinksTable({
|
||||
{/* </DropdownMenuItem> */}
|
||||
{/* </DropdownMenuContent> */}
|
||||
{/* </DropdownMenu> */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
<Button variant={"outline"}
|
||||
onClick={() =>
|
||||
deleteSharelink(row.original.accessTokenId)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ interface DataTableProps<TData, TValue> {
|
||||
createSite?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
columnVisibility?: Record<string, boolean>;
|
||||
enableColumnVisibility?: boolean;
|
||||
}
|
||||
|
||||
export function SitesDataTable<TData, TValue>({
|
||||
@@ -17,7 +19,9 @@ export function SitesDataTable<TData, TValue>({
|
||||
data,
|
||||
createSite,
|
||||
onRefresh,
|
||||
isRefreshing
|
||||
isRefreshing,
|
||||
columnVisibility,
|
||||
enableColumnVisibility
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
|
||||
const t = useTranslations();
|
||||
@@ -38,6 +42,8 @@ export function SitesDataTable<TData, TValue>({
|
||||
id: "name",
|
||||
desc: false
|
||||
}}
|
||||
columnVisibility={columnVisibility}
|
||||
enableColumnVisibility={enableColumnVisibility}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -361,10 +361,19 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
: []),
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const siteRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Link
|
||||
href={`/${siteRow.orgId}/settings/sites/${siteRow.nice}`}
|
||||
>
|
||||
<Button variant={"outline"}>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -393,15 +402,6 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -440,6 +440,12 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
columnVisibility={{
|
||||
nice: false,
|
||||
exitNode: false,
|
||||
address: false
|
||||
}}
|
||||
enableColumnVisibility={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -143,10 +143,35 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const userRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="flex items-center">
|
||||
{userRow.isOwner && (
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
disabled={true}
|
||||
>
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
disabled={userRow.isOwner}
|
||||
>
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
<>
|
||||
<div>
|
||||
{userRow.isOwner && (
|
||||
@@ -200,32 +225,6 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
{userRow.isOwner && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
disabled={true}
|
||||
>
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{!userRow.isOwner && (
|
||||
<Link
|
||||
href={`/${org?.org.orgId}/settings/access/users/${userRow.id}`}
|
||||
>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
disabled={userRow.isOwner}
|
||||
>
|
||||
{t("manage")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -117,10 +117,20 @@ export default function IdpTable({ idps, orgId }: Props) {
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => (<span className="p-3">{t("actions")}</span>),
|
||||
cell: ({ row }) => {
|
||||
const siteRow = row.original;
|
||||
return (
|
||||
<div className="flex items-center justify-end">
|
||||
<Link href={`/${orgId}/settings/idp/${siteRow.idpId}/general`}>
|
||||
<Button
|
||||
variant={"outline"}
|
||||
className="ml-2"
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" className="h-8 w-8 p-0">
|
||||
@@ -151,16 +161,6 @@ export default function IdpTable({ idps, orgId }: Props) {
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Link href={`/${orgId}/settings/idp/${siteRow.idpId}/general`}>
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
className="ml-2"
|
||||
size="sm"
|
||||
>
|
||||
{t("edit")}
|
||||
<ArrowRight className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
SortingState,
|
||||
getSortedRowModel,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel
|
||||
getFilteredRowModel,
|
||||
VisibilityState
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Table,
|
||||
@@ -23,7 +24,7 @@ import { Button } from "@app/components/ui/button";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Input } from "@app/components/ui/input";
|
||||
import { DataTablePagination } from "@app/components/DataTablePagination";
|
||||
import { Plus, Search, RefreshCw } from "lucide-react";
|
||||
import { Plus, Search, RefreshCw, Columns } from "lucide-react";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -32,16 +33,24 @@ import {
|
||||
} from "@app/components/ui/card";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@app/components/ui/tabs";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
PAGE_SIZE: 'datatable-page-size',
|
||||
getTablePageSize: (tableId?: string) =>
|
||||
PAGE_SIZE: "datatable-page-size",
|
||||
getTablePageSize: (tableId?: string) =>
|
||||
tableId ? `${tableId}-size` : STORAGE_KEYS.PAGE_SIZE
|
||||
};
|
||||
|
||||
const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
if (typeof window === 'undefined') return defaultSize;
|
||||
|
||||
if (typeof window === "undefined") return defaultSize;
|
||||
|
||||
try {
|
||||
const key = STORAGE_KEYS.getTablePageSize(tableId);
|
||||
const stored = localStorage.getItem(key);
|
||||
@@ -53,19 +62,19 @@ const getStoredPageSize = (tableId?: string, defaultSize = 20): number => {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Failed to read page size from localStorage:', error);
|
||||
console.warn("Failed to read page size from localStorage:", error);
|
||||
}
|
||||
return defaultSize;
|
||||
};
|
||||
|
||||
const setStoredPageSize = (pageSize: number, tableId?: string): void => {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
try {
|
||||
const key = STORAGE_KEYS.getTablePageSize(tableId);
|
||||
localStorage.setItem(key, pageSize.toString());
|
||||
} catch (error) {
|
||||
console.warn('Failed to save page size to localStorage:', error);
|
||||
console.warn("Failed to save page size to localStorage:", error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,6 +102,8 @@ type DataTableProps<TData, TValue> = {
|
||||
defaultTab?: string;
|
||||
persistPageSize?: boolean | string;
|
||||
defaultPageSize?: number;
|
||||
columnVisibility?: Record<string, boolean>;
|
||||
enableColumnVisibility?: boolean;
|
||||
};
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
@@ -109,13 +120,16 @@ export function DataTable<TData, TValue>({
|
||||
tabs,
|
||||
defaultTab,
|
||||
persistPageSize = false,
|
||||
defaultPageSize = 20
|
||||
defaultPageSize = 20,
|
||||
columnVisibility: defaultColumnVisibility,
|
||||
enableColumnVisibility = false
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
|
||||
// Determine table identifier for storage
|
||||
const tableId = typeof persistPageSize === 'string' ? persistPageSize : undefined;
|
||||
|
||||
const tableId =
|
||||
typeof persistPageSize === "string" ? persistPageSize : undefined;
|
||||
|
||||
// Initialize page size from storage or default
|
||||
const [pageSize, setPageSize] = useState<number>(() => {
|
||||
if (persistPageSize) {
|
||||
@@ -123,12 +137,15 @@ export function DataTable<TData, TValue>({
|
||||
}
|
||||
return defaultPageSize;
|
||||
});
|
||||
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>(
|
||||
defaultSort ? [defaultSort] : []
|
||||
);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [globalFilter, setGlobalFilter] = useState<any>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
defaultColumnVisibility || {}
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
defaultTab || tabs?.[0]?.id || ""
|
||||
);
|
||||
@@ -157,16 +174,19 @@ export function DataTable<TData, TValue>({
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: pageSize,
|
||||
pageIndex: 0
|
||||
}
|
||||
},
|
||||
columnVisibility: defaultColumnVisibility || {}
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnFilters,
|
||||
globalFilter
|
||||
globalFilter,
|
||||
columnVisibility
|
||||
}
|
||||
});
|
||||
|
||||
@@ -174,7 +194,7 @@ export function DataTable<TData, TValue>({
|
||||
const currentPageSize = table.getState().pagination.pageSize;
|
||||
if (currentPageSize !== pageSize) {
|
||||
table.setPageSize(pageSize);
|
||||
|
||||
|
||||
// Persist to localStorage if enabled
|
||||
if (persistPageSize) {
|
||||
setStoredPageSize(pageSize, tableId);
|
||||
@@ -192,7 +212,7 @@ export function DataTable<TData, TValue>({
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPageSize(newPageSize);
|
||||
table.setPageSize(newPageSize);
|
||||
|
||||
|
||||
// Persist immediately when changed
|
||||
if (persistPageSize) {
|
||||
setStoredPageSize(newPageSize, tableId);
|
||||
@@ -238,6 +258,53 @@ export function DataTable<TData, TValue>({
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 sm:justify-end">
|
||||
{enableColumnVisibility &&
|
||||
table
|
||||
.getAllColumns()
|
||||
.some((column) => column.getCanHide()) && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Columns className="mr-0 sm:mr-2 h-4 w-4" />
|
||||
<span className="hidden sm:inline">
|
||||
{t("columns") || "Columns"}
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="w-48"
|
||||
>
|
||||
<DropdownMenuLabel>
|
||||
{t("toggleColumns") || "Toggle columns"}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(
|
||||
!!value
|
||||
)
|
||||
}
|
||||
>
|
||||
{typeof column.columnDef
|
||||
.header === "string"
|
||||
? column.columnDef
|
||||
.header
|
||||
: column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{onRefresh && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -245,9 +312,11 @@ export function DataTable<TData, TValue>({
|
||||
disabled={isRefreshing}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
className={`mr-0 sm:mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
/>
|
||||
{t("refresh")}
|
||||
<span className="hidden sm:inline">
|
||||
{t("refresh")}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
{onAdd && addButtonText && (
|
||||
@@ -264,7 +333,10 @@ export function DataTable<TData, TValue>({
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead key={header.id}>
|
||||
<TableHead
|
||||
key={header.id}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
@@ -287,7 +359,10 @@ export function DataTable<TData, TValue>({
|
||||
}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id}>
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className="whitespace-nowrap"
|
||||
>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
@@ -309,8 +384,8 @@ export function DataTable<TData, TValue>({
|
||||
</TableBody>
|
||||
</Table>
|
||||
<div className="mt-4">
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user