Build client site resource associations and send messages

This commit is contained in:
Owen
2025-11-19 18:05:42 -05:00
parent 806949879a
commit 937b36e756
36 changed files with 904 additions and 583 deletions

View File

@@ -90,7 +90,7 @@ export default function CreateInternalResourceDialog({
mode: z.enum(["host", "cidr"]),
destination: z.string().min(1),
siteId: z.int().positive(t("createInternalResourceDialogPleaseSelectSite")),
protocol: z.enum(["tcp", "udp"]),
// protocol: z.enum(["tcp", "udp"]),
// proxyPort: z.int()
// .positive()
// .min(1, t("createInternalResourceDialogProxyPortMin"))
@@ -177,7 +177,7 @@ export default function CreateInternalResourceDialog({
name: "",
siteId: availableSites[0]?.siteId || 0,
mode: "host",
protocol: "tcp",
// protocol: "tcp",
// proxyPort: undefined,
destination: "",
// destinationPort: undefined,
@@ -196,7 +196,7 @@ export default function CreateInternalResourceDialog({
name: "",
siteId: availableSites[0].siteId,
mode: "host",
protocol: "tcp",
// protocol: "tcp",
// proxyPort: undefined,
destination: "",
// destinationPort: undefined,
@@ -260,35 +260,38 @@ export default function CreateInternalResourceDialog({
{
name: data.name,
mode: data.mode,
protocol: data.mode === "port" ? data.protocol : undefined,
// protocol: data.protocol,
// proxyPort: data.mode === "port" ? data.proxyPort : undefined,
// destinationPort: data.mode === "port" ? data.destinationPort : undefined,
destination: data.destination,
enabled: true,
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : undefined,
roleIds: data.roles ? data.roles.map((r) => parseInt(r.id)) : [],
userIds: data.users ? data.users.map((u) => u.id) : [],
clientIds: data.clients ? data.clients.map((c) => parseInt(c.id)) : []
}
);
const siteResourceId = response.data.data.siteResourceId;
// Set roles and users if provided
if (data.roles && data.roles.length > 0) {
await api.post(`/site-resource/${siteResourceId}/roles`, {
roleIds: data.roles.map((r) => parseInt(r.id))
});
}
// // Set roles and users if provided
// if (data.roles && data.roles.length > 0) {
// await api.post(`/site-resource/${siteResourceId}/roles`, {
// roleIds: data.roles.map((r) => parseInt(r.id))
// });
// }
if (data.users && data.users.length > 0) {
await api.post(`/site-resource/${siteResourceId}/users`, {
userIds: data.users.map((u) => u.id)
});
}
// if (data.users && data.users.length > 0) {
// await api.post(`/site-resource/${siteResourceId}/users`, {
// userIds: data.users.map((u) => u.id)
// });
// }
if (data.clients && data.clients.length > 0) {
await api.post(`/site-resource/${siteResourceId}/clients`, {
clientIds: data.clients.map((c) => parseInt(c.id))
});
}
// if (data.clients && data.clients.length > 0) {
// await api.post(`/site-resource/${siteResourceId}/clients`, {
// clientIds: data.clients.map((c) => parseInt(c.id))
// });
// }
toast({
title: t("createInternalResourceDialogSuccess"),
@@ -444,7 +447,7 @@ export default function CreateInternalResourceDialog({
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem>
{/* <SelectItem value="port">{t("createInternalResourceDialogModePort")}</SelectItem> */}
<SelectItem value="host">{t("createInternalResourceDialogModeHost")}</SelectItem>
<SelectItem value="cidr">{t("createInternalResourceDialogModeCidr")}</SelectItem>
</SelectContent>
@@ -535,7 +538,7 @@ export default function CreateInternalResourceDialog({
<FormDescription>
{mode === "host" && t("createInternalResourceDialogDestinationHostDescription")}
{mode === "cidr" && t("createInternalResourceDialogDestinationCidrDescription")}
{mode === "port" && t("createInternalResourceDialogDestinationIPDescription")}
{/* {mode === "port" && t("createInternalResourceDialogDestinationIPDescription")} */}
</FormDescription>
<FormMessage />
</FormItem>

View File

@@ -36,7 +36,6 @@ import { toast } from "@app/hooks/useToast";
import { useTranslations } from "next-intl";
import { createApiClient, formatAxiosError } from "@app/lib/api";
import { useEnvContext } from "@app/hooks/useEnvContext";
import { Separator } from "@app/components/ui/separator";
import { ListRolesResponse } from "@server/routers/role";
import { ListUsersResponse } from "@server/routers/user";
import { ListSiteResourceRolesResponse } from "@server/routers/siteResource/listSiteResourceRoles";
@@ -52,12 +51,13 @@ type InternalResourceData = {
name: string;
orgId: string;
siteName: string;
mode: "host" | "cidr" | "port";
protocol: string | null;
proxyPort: number | null;
// mode: "host" | "cidr" | "port";
mode: "host" | "cidr";
// protocol: string | null;
// proxyPort: number | null;
siteId: number;
destination: string;
destinationPort?: number | null;
// destinationPort?: number | null;
alias?: string | null;
};
@@ -83,10 +83,10 @@ export default function EditInternalResourceDialog({
const formSchema = z.object({
name: z.string().min(1, t("editInternalResourceDialogNameRequired")).max(255, t("editInternalResourceDialogNameMaxLength")),
mode: z.enum(["host", "cidr", "port"]),
protocol: z.enum(["tcp", "udp"]).nullish(),
proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
// protocol: z.enum(["tcp", "udp"]).nullish(),
// proxyPort: z.int().positive().min(1, t("editInternalResourceDialogProxyPortMin")).max(65535, t("editInternalResourceDialogProxyPortMax")).nullish(),
destination: z.string().min(1),
destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(),
// destinationPort: z.int().positive().min(1, t("editInternalResourceDialogDestinationPortMin")).max(65535, t("editInternalResourceDialogDestinationPortMax")).nullish(),
alias: z.string().nullish(),
roles: z.array(
z.object({
@@ -107,42 +107,42 @@ export default function EditInternalResourceDialog({
})
).optional()
})
.refine(
(data) => {
if (data.mode === "port") {
return data.protocol !== undefined && data.protocol !== null;
}
return true;
},
{
message: t("editInternalResourceDialogProtocol") + " is required for port mode",
path: ["protocol"]
}
)
.refine(
(data) => {
if (data.mode === "port") {
return data.proxyPort !== undefined && data.proxyPort !== null;
}
return true;
},
{
message: t("editInternalResourceDialogSitePort") + " is required for port mode",
path: ["proxyPort"]
}
)
.refine(
(data) => {
if (data.mode === "port") {
return data.destinationPort !== undefined && data.destinationPort !== null;
}
return true;
},
{
message: t("targetPort") + " is required for port mode",
path: ["destinationPort"]
}
);
// .refine(
// (data) => {
// if (data.mode === "port") {
// return data.protocol !== undefined && data.protocol !== null;
// }
// return true;
// },
// {
// message: t("editInternalResourceDialogProtocol") + " is required for port mode",
// path: ["protocol"]
// }
// )
// .refine(
// (data) => {
// if (data.mode === "port") {
// return data.proxyPort !== undefined && data.proxyPort !== null;
// }
// return true;
// },
// {
// message: t("editInternalResourceDialogSitePort") + " is required for port mode",
// path: ["proxyPort"]
// }
// )
// .refine(
// (data) => {
// if (data.mode === "port") {
// return data.destinationPort !== undefined && data.destinationPort !== null;
// }
// return true;
// },
// {
// message: t("targetPort") + " is required for port mode",
// path: ["destinationPort"]
// }
// );
type FormData = z.infer<typeof formSchema>;
@@ -160,10 +160,10 @@ export default function EditInternalResourceDialog({
defaultValues: {
name: resource.name,
mode: resource.mode || "host",
protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
proxyPort: resource.proxyPort ?? undefined,
// protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
// proxyPort: resource.proxyPort ?? undefined,
destination: resource.destination || "",
destinationPort: resource.destinationPort ?? undefined,
// destinationPort: resource.destinationPort ?? undefined,
alias: resource.alias ?? null,
roles: [],
users: [],
@@ -277,10 +277,10 @@ export default function EditInternalResourceDialog({
form.reset({
name: resource.name,
mode: resource.mode || "host",
protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
proxyPort: resource.proxyPort ?? undefined,
// protocol: (resource.protocol as "tcp" | "udp" | null | undefined) ?? undefined,
// proxyPort: resource.proxyPort ?? undefined,
destination: resource.destination || "",
destinationPort: resource.destinationPort ?? undefined,
// destinationPort: resource.destinationPort ?? undefined,
alias: resource.alias ?? null,
roles: [],
users: [],
@@ -298,25 +298,28 @@ export default function EditInternalResourceDialog({
await api.post(`/org/${orgId}/site/${resource.siteId}/resource/${resource.id}`, {
name: data.name,
mode: data.mode,
protocol: data.mode === "port" ? data.protocol : null,
proxyPort: data.mode === "port" ? data.proxyPort : null,
destinationPort: data.mode === "port" ? data.destinationPort : null,
// protocol: data.mode === "port" ? data.protocol : null,
// proxyPort: data.mode === "port" ? data.proxyPort : null,
// destinationPort: data.mode === "port" ? data.destinationPort : null,
destination: data.destination,
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : null
alias: data.alias && typeof data.alias === "string" && data.alias.trim() ? data.alias : null,
roleIds: (data.roles || []).map((r) => parseInt(r.id)),
userIds: (data.users || []).map((u) => u.id),
clientIds: (data.clients || []).map((c) => parseInt(c.id))
});
// Update roles, users, and clients
await Promise.all([
api.post(`/site-resource/${resource.id}/roles`, {
roleIds: (data.roles || []).map((r) => parseInt(r.id))
}),
api.post(`/site-resource/${resource.id}/users`, {
userIds: (data.users || []).map((u) => u.id)
}),
api.post(`/site-resource/${resource.id}/clients`, {
clientIds: (data.clients || []).map((c) => parseInt(c.id))
})
]);
// await Promise.all([
// api.post(`/site-resource/${resource.id}/roles`, {
// roleIds: (data.roles || []).map((r) => parseInt(r.id))
// }),
// api.post(`/site-resource/${resource.id}/users`, {
// userIds: (data.users || []).map((u) => u.id)
// }),
// api.post(`/site-resource/${resource.id}/clients`, {
// clientIds: (data.clients || []).map((c) => parseInt(c.id))
// })
// ]);
toast({
title: t("editInternalResourceDialogSuccess"),
@@ -384,7 +387,7 @@ export default function EditInternalResourceDialog({
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem>
{/* <SelectItem value="port">{t("editInternalResourceDialogModePort")}</SelectItem> */}
<SelectItem value="host">{t("editInternalResourceDialogModeHost")}</SelectItem>
<SelectItem value="cidr">{t("editInternalResourceDialogModeCidr")}</SelectItem>
</SelectContent>
@@ -394,7 +397,7 @@ export default function EditInternalResourceDialog({
)}
/>
{mode === "port" && (
{/* {mode === "port" && (
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
@@ -439,7 +442,7 @@ export default function EditInternalResourceDialog({
)}
/>
</div>
)}
)} */}
</div>
</div>
@@ -459,14 +462,14 @@ export default function EditInternalResourceDialog({
<FormDescription>
{mode === "host" && t("editInternalResourceDialogDestinationHostDescription")}
{mode === "cidr" && t("editInternalResourceDialogDestinationCidrDescription")}
{mode === "port" && t("editInternalResourceDialogDestinationIPDescription")}
{/* {mode === "port" && t("editInternalResourceDialogDestinationIPDescription")} */}
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{mode === "port" && (
{/* {mode === "port" && (
<FormField
control={form.control}
name="destinationPort"
@@ -484,7 +487,7 @@ export default function EditInternalResourceDialog({
</FormItem>
)}
/>
)}
)} */}
</div>
</div>

View File

@@ -19,7 +19,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSeparator,
DropdownMenuCheckboxItem
} from "@app/components/ui/dropdown-menu";
import { Button } from "@app/components/ui/button";
@@ -164,13 +164,14 @@ export type InternalResourceRow = {
orgId: string;
siteName: string;
siteAddress: string | null;
mode: "host" | "cidr" | "port";
protocol: string | null;
proxyPort: number | null;
// mode: "host" | "cidr" | "port";
mode: "host" | "cidr";
// protocol: string | null;
// proxyPort: number | null;
siteId: number;
siteNiceId: string;
destination: string;
destinationPort: number | null;
// destinationPort: number | null;
alias: string | null;
};
@@ -515,10 +516,14 @@ export default function ResourcesTable({
>
<StatusIcon status={overallStatus} />
<span className="text-sm">
{overallStatus === "online" && t("resourcesTableHealthy")}
{overallStatus === "degraded" && t("resourcesTableDegraded")}
{overallStatus === "offline" && t("resourcesTableOffline")}
{overallStatus === "unknown" && t("resourcesTableUnknown")}
{overallStatus === "online" &&
t("resourcesTableHealthy")}
{overallStatus === "degraded" &&
t("resourcesTableDegraded")}
{overallStatus === "offline" &&
t("resourcesTableOffline")}
{overallStatus === "unknown" &&
t("resourcesTableUnknown")}
</span>
<ChevronDown className="h-3 w-3" />
</Button>
@@ -770,7 +775,11 @@ export default function ResourcesTable({
<div className="flex flex-col items-end gap-1 p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0">
<Button
variant="ghost"
size="sm"
className="h-7 w-7 p-0"
>
<Columns className="h-4 w-4" />
<span className="sr-only">
{t("columns") || "Columns"}
@@ -786,10 +795,14 @@ export default function ResourcesTable({
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
const columnDef = column.columnDef as any;
const friendlyName = columnDef.friendlyName;
const displayName = friendlyName ||
(typeof columnDef.header === "string"
const columnDef =
column.columnDef as any;
const friendlyName =
columnDef.friendlyName;
const displayName =
friendlyName ||
(typeof columnDef.header ===
"string"
? columnDef.header
: column.id);
return (
@@ -798,9 +811,13 @@ export default function ResourcesTable({
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
column.toggleVisibility(
!!value
)
}
onSelect={(e) =>
e.preventDefault()
}
onSelect={(e) => e.preventDefault()}
>
{displayName}
</DropdownMenuCheckboxItem>
@@ -925,23 +942,24 @@ export default function ResourcesTable({
let displayText: string;
let copyText: string;
if (
resourceRow.mode === "port" &&
resourceRow.protocol &&
resourceRow.proxyPort &&
resourceRow.destinationPort
) {
const protocol = resourceRow.protocol.toUpperCase();
// For port mode: site part uses alias or site address, destination part uses destination IP
// If site address has CIDR notation, extract just the IP address
let siteAddress = resourceRow.siteAddress;
if (siteAddress && siteAddress.includes("/")) {
siteAddress = siteAddress.split("/")[0];
}
const siteDisplay = resourceRow.alias || siteAddress;
displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`;
copyText = `${siteDisplay}:${resourceRow.proxyPort}`;
} else if (resourceRow.mode === "host") {
// if (
// resourceRow.mode === "port" &&
// // resourceRow.protocol &&
// // resourceRow.proxyPort &&
// // resourceRow.destinationPort
// ) {
// // const protocol = resourceRow.protocol.toUpperCase();
// // For port mode: site part uses alias or site address, destination part uses destination IP
// // If site address has CIDR notation, extract just the IP address
// let siteAddress = resourceRow.siteAddress;
// if (siteAddress && siteAddress.includes("/")) {
// siteAddress = siteAddress.split("/")[0];
// }
// const siteDisplay = resourceRow.alias || siteAddress;
// // displayText = `${protocol} ${siteDisplay}:${resourceRow.proxyPort} -> ${resourceRow.destination}:${resourceRow.destinationPort}`;
// // copyText = `${siteDisplay}:${resourceRow.proxyPort}`;
// } else if (resourceRow.mode === "host") {
if (resourceRow.mode === "host") {
// For host mode: use alias if available, otherwise use destination
const destinationDisplay =
resourceRow.alias || resourceRow.destination;
@@ -981,7 +999,11 @@ export default function ResourcesTable({
<div className="flex flex-col items-end gap-1 p-3">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="h-7 w-7 p-0">
<Button
variant="outline"
size="sm"
className="h-7 w-7 p-0"
>
<Columns className="h-4 w-4" />
<span className="sr-only">
{t("columns") || "Columns"}
@@ -997,10 +1019,14 @@ export default function ResourcesTable({
.getAllColumns()
.filter((column) => column.getCanHide())
.map((column) => {
const columnDef = column.columnDef as any;
const friendlyName = columnDef.friendlyName;
const displayName = friendlyName ||
(typeof columnDef.header === "string"
const columnDef =
column.columnDef as any;
const friendlyName =
columnDef.friendlyName;
const displayName =
friendlyName ||
(typeof columnDef.header ===
"string"
? columnDef.header
: column.id);
return (
@@ -1009,9 +1035,13 @@ export default function ResourcesTable({
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
column.toggleVisibility(
!!value
)
}
onSelect={(e) =>
e.preventDefault()
}
onSelect={(e) => e.preventDefault()}
>
{displayName}
</DropdownMenuCheckboxItem>
@@ -1230,99 +1260,111 @@ export default function ResourcesTable({
<TabsContent value="proxy">
<div className="overflow-x-auto">
<Table>
<TableHeader>
{proxyTable
.getHeaderGroups()
.map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((header) =>
header.column.getIsVisible()
)
.map((header) => (
<TableHead
key={header.id}
className={`whitespace-nowrap ${
header.column.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: header.column.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{proxyTable.getRowModel().rows
?.length ? (
proxyTable
.getRowModel()
.rows.map((row) => (
<TableHeader>
{proxyTable
.getHeaderGroups()
.map((headerGroup) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
key={headerGroup.id}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
{headerGroup.headers
.filter((header) =>
header.column.getIsVisible()
)
.map((header) => (
<TableHead
key={
cell.id
header.id
}
className={`whitespace-nowrap ${
cell.column.id ===
header
.column
.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell.column.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
: header
.column
.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={
proxyColumns.length
}
className="h-24 text-center"
>
{t(
"resourcesTableNoProxyResourcesFound"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))}
</TableHeader>
<TableBody>
{proxyTable.getRowModel().rows
?.length ? (
proxyTable
.getRowModel()
.rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
key={
cell.id
}
className={`whitespace-nowrap ${
cell
.column
.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell
.column
.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={
proxyColumns.length
}
className="h-24 text-center"
>
{t(
"resourcesTableNoProxyResourcesFound"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="mt-4">
<DataTablePagination
@@ -1336,99 +1378,111 @@ export default function ResourcesTable({
<TabsContent value="internal">
<div className="overflow-x-auto">
<Table>
<TableHeader>
{internalTable
.getHeaderGroups()
.map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers
.filter((header) =>
header.column.getIsVisible()
)
.map((header) => (
<TableHead
key={header.id}
className={`whitespace-nowrap ${
header.column.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: header.column.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{internalTable.getRowModel().rows
?.length ? (
internalTable
.getRowModel()
.rows.map((row) => (
<TableHeader>
{internalTable
.getHeaderGroups()
.map((headerGroup) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
key={headerGroup.id}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
{headerGroup.headers
.filter((header) =>
header.column.getIsVisible()
)
.map((header) => (
<TableHead
key={
cell.id
header.id
}
className={`whitespace-nowrap ${
cell.column.id ===
header
.column
.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell.column.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
: header
.column
.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
{header.isPlaceholder
? null
: flexRender(
header
.column
.columnDef
.header,
header.getContext()
)}
</TableHead>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={
internalColumns.length
}
className="h-24 text-center"
>
{t(
"resourcesTableNoInternalResourcesFound"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
))}
</TableHeader>
<TableBody>
{internalTable.getRowModel().rows
?.length ? (
internalTable
.getRowModel()
.rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
"selected"
}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
key={
cell.id
}
className={`whitespace-nowrap ${
cell
.column
.id ===
"actions"
? "sticky right-0 z-10 w-auto min-w-fit bg-card"
: cell
.column
.id ===
"name"
? "md:sticky md:left-0 z-10 bg-card"
: ""
}`}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext()
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={
internalColumns.length
}
className="h-24 text-center"
>
{t(
"resourcesTableNoInternalResourcesFound"
)}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="mt-4">
<DataTablePagination