update tables

This commit is contained in:
miloschwartz
2025-11-05 10:29:29 -08:00
parent f628a76223
commit 2274404324
29 changed files with 540 additions and 332 deletions

View File

@@ -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),

View File

@@ -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">

View File

@@ -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
};
});

View File

@@ -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)}

View File

@@ -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)}

View File

@@ -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

View File

@@ -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")}

View File

@@ -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>
);
}

View File

@@ -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>
</>
);

View File

@@ -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")}

View File

@@ -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"

View File

@@ -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>

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
</>
);

View File

@@ -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 (

View File

@@ -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}>

View File

@@ -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"

View File

@@ -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",

View File

@@ -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")}

View File

@@ -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>
);
}

View File

@@ -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)}
>

View File

@@ -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>

View File

@@ -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);

View File

@@ -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)
}

View File

@@ -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}
/>
);
}

View File

@@ -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}
/>
</>
);

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>