mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-10 04:36:38 +00:00
✨serverside filter+paginate client resources table
This commit is contained in:
@@ -219,7 +219,7 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||||
protocol: varchar("protocol"), // only for port mode
|
protocol: varchar("protocol"), // only for port mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
|
|||||||
@@ -69,38 +69,6 @@ const listResourcesSchema = z.object({
|
|||||||
.catch(undefined)
|
.catch(undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
// (resource fields + a single joined target)
|
|
||||||
type JoinedRow = {
|
|
||||||
resourceId: number;
|
|
||||||
niceId: string;
|
|
||||||
name: string;
|
|
||||||
ssl: boolean;
|
|
||||||
fullDomain: string | null;
|
|
||||||
passwordId: number | null;
|
|
||||||
sso: boolean;
|
|
||||||
pincodeId: number | null;
|
|
||||||
whitelist: boolean;
|
|
||||||
http: boolean;
|
|
||||||
protocol: string;
|
|
||||||
proxyPort: number | null;
|
|
||||||
enabled: boolean;
|
|
||||||
domainId: string | null;
|
|
||||||
headerAuthId: number | null;
|
|
||||||
|
|
||||||
// total_targets: number;
|
|
||||||
// healthy_targets: number;
|
|
||||||
// unhealthy_targets: number;
|
|
||||||
// unknown_targets: number;
|
|
||||||
|
|
||||||
// targetId: number | null;
|
|
||||||
// targetIp: string | null;
|
|
||||||
// targetPort: number | null;
|
|
||||||
// targetEnabled: boolean | null;
|
|
||||||
|
|
||||||
// hcHealth: string | null;
|
|
||||||
// hcEnabled: boolean | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
// grouped by resource with targets[])
|
// grouped by resource with targets[])
|
||||||
export type ResourceWithTargets = {
|
export type ResourceWithTargets = {
|
||||||
resourceId: number;
|
resourceId: number;
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export async function listSites(
|
|||||||
const baseQuery = querySitesBase().where(conditions);
|
const baseQuery = querySitesBase().where(conditions);
|
||||||
|
|
||||||
// we need to add `as` so that drizzle filters the result as a subquery
|
// we need to add `as` so that drizzle filters the result as a subquery
|
||||||
const countQuery = db.$count(baseQuery.as("filtered_sites"));
|
const countQuery = db.$count(querySitesBase().where(conditions));
|
||||||
|
|
||||||
const siteListQuery = baseQuery
|
const siteListQuery = baseQuery
|
||||||
.limit(pageSize)
|
.limit(pageSize)
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, resources } from "@server/db";
|
||||||
import { siteResources, sites, SiteResource } from "@server/db";
|
import { siteResources, sites, SiteResource } from "@server/db";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and, asc, ilike, or } from "drizzle-orm";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
@@ -15,18 +15,22 @@ const listAllSiteResourcesByOrgParamsSchema = z.strictObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const listAllSiteResourcesByOrgQuerySchema = z.object({
|
const listAllSiteResourcesByOrgQuerySchema = z.object({
|
||||||
limit: z
|
pageSize: z.coerce
|
||||||
.string()
|
.number<string>() // for prettier formatting
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
.optional()
|
.optional()
|
||||||
.default("1000")
|
.catch(20)
|
||||||
.transform(Number)
|
.default(20),
|
||||||
.pipe(z.int().positive()),
|
page: z.coerce
|
||||||
offset: z
|
.number<string>() // for prettier formatting
|
||||||
.string()
|
.int()
|
||||||
|
.min(0)
|
||||||
.optional()
|
.optional()
|
||||||
.default("0")
|
.catch(1)
|
||||||
.transform(Number)
|
.default(1),
|
||||||
.pipe(z.int().nonnegative())
|
query: z.string().optional(),
|
||||||
|
mode: z.enum(["host", "cidr"]).optional().catch(undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type ListAllSiteResourcesByOrgResponse = {
|
export type ListAllSiteResourcesByOrgResponse = {
|
||||||
@@ -35,8 +39,36 @@ export type ListAllSiteResourcesByOrgResponse = {
|
|||||||
siteNiceId: string;
|
siteNiceId: string;
|
||||||
siteAddress: string | null;
|
siteAddress: string | null;
|
||||||
})[];
|
})[];
|
||||||
|
pagination: { total: number; pageSize: number; page: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function querySiteResourcesBase() {
|
||||||
|
return db
|
||||||
|
.select({
|
||||||
|
siteResourceId: siteResources.siteResourceId,
|
||||||
|
siteId: siteResources.siteId,
|
||||||
|
orgId: siteResources.orgId,
|
||||||
|
niceId: siteResources.niceId,
|
||||||
|
name: siteResources.name,
|
||||||
|
mode: siteResources.mode,
|
||||||
|
protocol: siteResources.protocol,
|
||||||
|
proxyPort: siteResources.proxyPort,
|
||||||
|
destinationPort: siteResources.destinationPort,
|
||||||
|
destination: siteResources.destination,
|
||||||
|
enabled: siteResources.enabled,
|
||||||
|
alias: siteResources.alias,
|
||||||
|
aliasAddress: siteResources.aliasAddress,
|
||||||
|
tcpPortRangeString: siteResources.tcpPortRangeString,
|
||||||
|
udpPortRangeString: siteResources.udpPortRangeString,
|
||||||
|
disableIcmp: siteResources.disableIcmp,
|
||||||
|
siteName: sites.name,
|
||||||
|
siteNiceId: sites.niceId,
|
||||||
|
siteAddress: sites.address
|
||||||
|
})
|
||||||
|
.from(siteResources)
|
||||||
|
.innerJoin(sites, eq(siteResources.siteId, sites.siteId));
|
||||||
|
}
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/site-resources",
|
path: "/org/{orgId}/site-resources",
|
||||||
@@ -80,39 +112,50 @@ export async function listAllSiteResourcesByOrg(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { orgId } = parsedParams.data;
|
const { orgId } = parsedParams.data;
|
||||||
const { limit, offset } = parsedQuery.data;
|
const { page, pageSize, query, mode } = parsedQuery.data;
|
||||||
|
|
||||||
|
let conditions = and(eq(siteResources.orgId, orgId));
|
||||||
|
if (query) {
|
||||||
|
conditions = and(
|
||||||
|
conditions,
|
||||||
|
or(
|
||||||
|
ilike(siteResources.name, "%" + query + "%"),
|
||||||
|
ilike(siteResources.destination, "%" + query + "%"),
|
||||||
|
ilike(siteResources.alias, "%" + query + "%"),
|
||||||
|
ilike(siteResources.aliasAddress, "%" + query + "%"),
|
||||||
|
ilike(sites.name, "%" + query + "%")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mode) {
|
||||||
|
conditions = and(conditions, eq(siteResources.mode, mode));
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseQuery = querySiteResourcesBase().where(conditions);
|
||||||
|
|
||||||
|
const countQuery = db.$count(
|
||||||
|
querySiteResourcesBase().where(conditions)
|
||||||
|
);
|
||||||
|
|
||||||
// Get all site resources for the org with site names
|
// Get all site resources for the org with site names
|
||||||
const siteResourcesList = await db
|
const [siteResourcesList, totalCount] = await Promise.all([
|
||||||
.select({
|
baseQuery
|
||||||
siteResourceId: siteResources.siteResourceId,
|
.limit(pageSize)
|
||||||
siteId: siteResources.siteId,
|
.offset(pageSize * (page - 1))
|
||||||
orgId: siteResources.orgId,
|
.orderBy(asc(siteResources.siteResourceId)),
|
||||||
niceId: siteResources.niceId,
|
countQuery
|
||||||
name: siteResources.name,
|
]);
|
||||||
mode: siteResources.mode,
|
|
||||||
protocol: siteResources.protocol,
|
|
||||||
proxyPort: siteResources.proxyPort,
|
|
||||||
destinationPort: siteResources.destinationPort,
|
|
||||||
destination: siteResources.destination,
|
|
||||||
enabled: siteResources.enabled,
|
|
||||||
alias: siteResources.alias,
|
|
||||||
aliasAddress: siteResources.aliasAddress,
|
|
||||||
tcpPortRangeString: siteResources.tcpPortRangeString,
|
|
||||||
udpPortRangeString: siteResources.udpPortRangeString,
|
|
||||||
disableIcmp: siteResources.disableIcmp,
|
|
||||||
siteName: sites.name,
|
|
||||||
siteNiceId: sites.niceId,
|
|
||||||
siteAddress: sites.address
|
|
||||||
})
|
|
||||||
.from(siteResources)
|
|
||||||
.innerJoin(sites, eq(siteResources.siteId, sites.siteId))
|
|
||||||
.where(eq(siteResources.orgId, orgId))
|
|
||||||
.limit(limit)
|
|
||||||
.offset(offset);
|
|
||||||
|
|
||||||
return response(res, {
|
return response<ListAllSiteResourcesByOrgResponse>(res, {
|
||||||
data: { siteResources: siteResourcesList },
|
data: {
|
||||||
|
siteResources: siteResourcesList,
|
||||||
|
pagination: {
|
||||||
|
total: totalCount,
|
||||||
|
pageSize,
|
||||||
|
page
|
||||||
|
}
|
||||||
|
},
|
||||||
success: true,
|
success: true,
|
||||||
error: false,
|
error: false,
|
||||||
message: "Site resources retrieved successfully",
|
message: "Site resources retrieved successfully",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { redirect } from "next/navigation";
|
|||||||
|
|
||||||
export interface ClientResourcesPageProps {
|
export interface ClientResourcesPageProps {
|
||||||
params: Promise<{ orgId: string }>;
|
params: Promise<{ orgId: string }>;
|
||||||
searchParams: Promise<{ view?: string }>;
|
searchParams: Promise<Record<string, string>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function ClientResourcesPage(
|
export default async function ClientResourcesPage(
|
||||||
@@ -22,22 +22,24 @@ export default async function ClientResourcesPage(
|
|||||||
) {
|
) {
|
||||||
const params = await props.params;
|
const params = await props.params;
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
const searchParams = new URLSearchParams(await props.searchParams);
|
||||||
let resources: ListResourcesResponse["resources"] = [];
|
|
||||||
try {
|
|
||||||
const res = await internal.get<AxiosResponse<ListResourcesResponse>>(
|
|
||||||
`/org/${params.orgId}/resources`,
|
|
||||||
await authCookieHeader()
|
|
||||||
);
|
|
||||||
resources = res.data.data.resources;
|
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = [];
|
let siteResources: ListAllSiteResourcesByOrgResponse["siteResources"] = [];
|
||||||
|
let pagination: ListResourcesResponse["pagination"] = {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20
|
||||||
|
};
|
||||||
try {
|
try {
|
||||||
const res = await internal.get<
|
const res = await internal.get<
|
||||||
AxiosResponse<ListAllSiteResourcesByOrgResponse>
|
AxiosResponse<ListAllSiteResourcesByOrgResponse>
|
||||||
>(`/org/${params.orgId}/site-resources`, await authCookieHeader());
|
>(
|
||||||
siteResources = res.data.data.siteResources;
|
`/org/${params.orgId}/site-resources?${searchParams.toString()}`,
|
||||||
|
await authCookieHeader()
|
||||||
|
);
|
||||||
|
const responseData = res.data.data;
|
||||||
|
siteResources = responseData.siteResources;
|
||||||
|
pagination = responseData.pagination;
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
let org = null;
|
let org = null;
|
||||||
@@ -89,9 +91,10 @@ export default async function ClientResourcesPage(
|
|||||||
<ClientResourcesTable
|
<ClientResourcesTable
|
||||||
internalResources={internalResourceRows}
|
internalResources={internalResourceRows}
|
||||||
orgId={params.orgId}
|
orgId={params.orgId}
|
||||||
defaultSort={{
|
rowCount={pagination.total}
|
||||||
id: "name",
|
pagination={{
|
||||||
desc: false
|
pageIndex: pagination.page - 1,
|
||||||
|
pageSize: pagination.pageSize
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ import CreateInternalResourceDialog from "@app/components/CreateInternalResource
|
|||||||
import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog";
|
import EditInternalResourceDialog from "@app/components/EditInternalResourceDialog";
|
||||||
import { orgQueries } from "@app/lib/queries";
|
import { orgQueries } from "@app/lib/queries";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
|
|
||||||
export type InternalResourceRow = {
|
export type InternalResourceRow = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -51,18 +56,22 @@ export type InternalResourceRow = {
|
|||||||
type ClientResourcesTableProps = {
|
type ClientResourcesTableProps = {
|
||||||
internalResources: InternalResourceRow[];
|
internalResources: InternalResourceRow[];
|
||||||
orgId: string;
|
orgId: string;
|
||||||
defaultSort?: {
|
pagination: PaginationState;
|
||||||
id: string;
|
rowCount: number;
|
||||||
desc: boolean;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ClientResourcesTable({
|
export default function ClientResourcesTable({
|
||||||
internalResources,
|
internalResources,
|
||||||
orgId,
|
orgId,
|
||||||
defaultSort
|
pagination,
|
||||||
|
rowCount
|
||||||
}: ClientResourcesTableProps) {
|
}: ClientResourcesTableProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const {
|
||||||
|
navigate: filter,
|
||||||
|
isNavigating: isFiltering,
|
||||||
|
searchParams
|
||||||
|
} = useNavigationContext();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
@@ -180,9 +189,24 @@ export default function ClientResourcesTable({
|
|||||||
accessorKey: "mode",
|
accessorKey: "mode",
|
||||||
friendlyName: t("editInternalResourceDialogMode"),
|
friendlyName: t("editInternalResourceDialogMode"),
|
||||||
header: () => (
|
header: () => (
|
||||||
<span className="p-3">
|
<ColumnFilterButton
|
||||||
{t("editInternalResourceDialogMode")}
|
options={[
|
||||||
</span>
|
{
|
||||||
|
value: "host",
|
||||||
|
label: t("editInternalResourceDialogModeHost")
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "cidr",
|
||||||
|
label: t("editInternalResourceDialogModeCidr")
|
||||||
|
}
|
||||||
|
]}
|
||||||
|
selectedValue={searchParams.get("mode") ?? undefined}
|
||||||
|
onValueChange={(value) => handleFilterChange("mode", value)}
|
||||||
|
searchPlaceholder={t("searchPlaceholder")}
|
||||||
|
emptyMessage={t("emptySearchOptions")}
|
||||||
|
label={t("editInternalResourceDialogMode")}
|
||||||
|
className="p-3"
|
||||||
|
/>
|
||||||
),
|
),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
@@ -300,6 +324,37 @@ export default function ClientResourcesTable({
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
function handleFilterChange(
|
||||||
|
column: string,
|
||||||
|
value: string | undefined | null
|
||||||
|
) {
|
||||||
|
searchParams.delete(column);
|
||||||
|
searchParams.delete("page");
|
||||||
|
|
||||||
|
if (value) {
|
||||||
|
searchParams.set(column, value);
|
||||||
|
}
|
||||||
|
filter({
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePaginationChange = (newPage: PaginationState) => {
|
||||||
|
searchParams.set("page", (newPage.pageIndex + 1).toString());
|
||||||
|
searchParams.set("pageSize", newPage.pageSize.toString());
|
||||||
|
filter({
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSearchChange = useDebouncedCallback((query: string) => {
|
||||||
|
searchParams.set("query", query);
|
||||||
|
searchParams.delete("page");
|
||||||
|
filter({
|
||||||
|
searchParams
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedInternalResource && (
|
{selectedInternalResource && (
|
||||||
@@ -327,19 +382,20 @@ export default function ClientResourcesTable({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<DataTable
|
<ControlledDataTable
|
||||||
columns={internalColumns}
|
columns={internalColumns}
|
||||||
data={internalResources}
|
rows={internalResources}
|
||||||
persistPageSize="internal-resources"
|
tableId="internal-resources"
|
||||||
searchPlaceholder={t("resourcesSearch")}
|
searchPlaceholder={t("resourcesSearch")}
|
||||||
searchColumn="name"
|
|
||||||
onAdd={() => setIsCreateDialogOpen(true)}
|
onAdd={() => setIsCreateDialogOpen(true)}
|
||||||
addButtonText={t("resourceAdd")}
|
addButtonText={t("resourceAdd")}
|
||||||
|
onSearch={handleSearchChange}
|
||||||
onRefresh={refreshData}
|
onRefresh={refreshData}
|
||||||
|
onPaginationChange={handlePaginationChange}
|
||||||
|
pagination={pagination}
|
||||||
|
rowCount={rowCount}
|
||||||
isRefreshing={isRefreshing}
|
isRefreshing={isRefreshing}
|
||||||
defaultSort={defaultSort}
|
enableColumnVisibility
|
||||||
enableColumnVisibility={true}
|
|
||||||
persistColumnVisibility="internal-resources"
|
|
||||||
columnVisibility={{
|
columnVisibility={{
|
||||||
niceId: false,
|
niceId: false,
|
||||||
aliasAddress: false
|
aliasAddress: false
|
||||||
|
|||||||
@@ -2,9 +2,8 @@
|
|||||||
|
|
||||||
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
import ConfirmDeleteDialog from "@app/components/ConfirmDeleteDialog";
|
||||||
import CopyToClipboard from "@app/components/CopyToClipboard";
|
import CopyToClipboard from "@app/components/CopyToClipboard";
|
||||||
import { DataTable } from "@app/components/ui/data-table";
|
|
||||||
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
|
import { ExtendedColumnDef } from "@app/components/ui/data-table";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -14,13 +13,14 @@ import {
|
|||||||
import { InfoPopup } from "@app/components/ui/info-popup";
|
import { InfoPopup } from "@app/components/ui/info-popup";
|
||||||
import { Switch } from "@app/components/ui/switch";
|
import { Switch } from "@app/components/ui/switch";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { UpdateResourceResponse } from "@server/routers/resource";
|
import { UpdateResourceResponse } from "@server/routers/resource";
|
||||||
|
import type { PaginationState } from "@tanstack/react-table";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
ArrowUpDown,
|
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Clock,
|
Clock,
|
||||||
@@ -31,14 +31,12 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState, useTransition } from "react";
|
import { useState, useTransition } from "react";
|
||||||
import { ControlledDataTable } from "./ui/controlled-data-table";
|
|
||||||
import type { PaginationState } from "@tanstack/react-table";
|
|
||||||
import { useNavigationContext } from "@app/hooks/useNavigationContext";
|
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||||
|
import { ControlledDataTable } from "./ui/controlled-data-table";
|
||||||
|
|
||||||
export type TargetHealth = {
|
export type TargetHealth = {
|
||||||
targetId: number;
|
targetId: number;
|
||||||
@@ -584,10 +582,6 @@ export default function ProxyResourcesTable({
|
|||||||
});
|
});
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
||||||
console.log({
|
|
||||||
rowCount
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{selectedResource && (
|
{selectedResource && (
|
||||||
|
|||||||
@@ -130,7 +130,8 @@ export function ControlledDataTable<TData, TValue>({
|
|||||||
});
|
});
|
||||||
|
|
||||||
console.log({
|
console.log({
|
||||||
pagination
|
pagination,
|
||||||
|
rowCount
|
||||||
});
|
});
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
|
|||||||
Reference in New Issue
Block a user