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