mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-17 10:26:39 +00:00
✨ paginate, search & filter resources by enabled
This commit is contained in:
@@ -31,8 +31,14 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
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 z from "zod";
|
||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||
|
||||
export type TargetHealth = {
|
||||
targetId: number;
|
||||
@@ -117,18 +123,22 @@ function StatusIcon({
|
||||
type ProxyResourcesTableProps = {
|
||||
resources: ResourceRow[];
|
||||
orgId: string;
|
||||
defaultSort?: {
|
||||
id: string;
|
||||
desc: boolean;
|
||||
};
|
||||
pagination: PaginationState;
|
||||
rowCount: number;
|
||||
};
|
||||
|
||||
export default function ProxyResourcesTable({
|
||||
resources,
|
||||
orgId,
|
||||
defaultSort
|
||||
pagination,
|
||||
rowCount
|
||||
}: ProxyResourcesTableProps) {
|
||||
const router = useRouter();
|
||||
const {
|
||||
navigate: filter,
|
||||
isNavigating: isFiltering,
|
||||
searchParams
|
||||
} = useNavigationContext();
|
||||
const t = useTranslations();
|
||||
|
||||
const { env } = useEnvContext();
|
||||
@@ -140,6 +150,7 @@ export default function ProxyResourcesTable({
|
||||
useState<ResourceRow | null>();
|
||||
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||
|
||||
const refreshData = () => {
|
||||
startTransition(() => {
|
||||
@@ -236,7 +247,7 @@ export default function ProxyResourcesTable({
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start" className="min-w-[280px]">
|
||||
<DropdownMenuContent align="start" className="min-w-70">
|
||||
{monitoredTargets.length > 0 && (
|
||||
<>
|
||||
{monitoredTargets.map((target) => (
|
||||
@@ -456,7 +467,24 @@ export default function ProxyResourcesTable({
|
||||
{
|
||||
accessorKey: "enabled",
|
||||
friendlyName: t("enabled"),
|
||||
header: () => <span className="p-3">{t("enabled")}</span>,
|
||||
header: () => (
|
||||
<ColumnFilterButton
|
||||
options={[
|
||||
{ value: "true", label: t("enabled") },
|
||||
{ value: "false", label: t("disabled") }
|
||||
]}
|
||||
selectedValue={booleanSearchFilterSchema.parse(
|
||||
searchParams.get("enabled")
|
||||
)}
|
||||
onValueChange={(value) =>
|
||||
handleFilterChange("enabled", value)
|
||||
}
|
||||
searchPlaceholder={t("searchPlaceholder")}
|
||||
emptyMessage={t("emptySearchOptions")}
|
||||
label={t("enabled")}
|
||||
className="p-3"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Switch
|
||||
defaultChecked={
|
||||
@@ -525,6 +553,42 @@ export default function ProxyResourcesTable({
|
||||
}
|
||||
];
|
||||
|
||||
const booleanSearchFilterSchema = z
|
||||
.enum(["true", "false"])
|
||||
.optional()
|
||||
.catch(undefined);
|
||||
|
||||
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 (
|
||||
<>
|
||||
{selectedResource && (
|
||||
@@ -547,21 +611,27 @@ export default function ProxyResourcesTable({
|
||||
/>
|
||||
)}
|
||||
|
||||
<DataTable
|
||||
<ControlledDataTable
|
||||
columns={proxyColumns}
|
||||
data={resources}
|
||||
persistPageSize="proxy-resources"
|
||||
rows={resources}
|
||||
tableId="proxy-resources"
|
||||
searchPlaceholder={t("resourcesSearch")}
|
||||
searchColumn="name"
|
||||
pagination={pagination}
|
||||
rowCount={rowCount}
|
||||
onSearch={handleSearchChange}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
onAdd={() =>
|
||||
router.push(`/${orgId}/settings/resources/proxy/create`)
|
||||
startNavigation(() => {
|
||||
router.push(
|
||||
`/${orgId}/settings/resources/proxy/create`
|
||||
);
|
||||
})
|
||||
}
|
||||
addButtonText={t("resourceAdd")}
|
||||
onRefresh={refreshData}
|
||||
isRefreshing={isRefreshing}
|
||||
defaultSort={defaultSort}
|
||||
enableColumnVisibility={true}
|
||||
persistColumnVisibility="proxy-resources"
|
||||
isRefreshing={isRefreshing || isFiltering}
|
||||
isNavigatingToAddPage={isNavigatingToAddPage}
|
||||
enableColumnVisibility
|
||||
columnVisibility={{ niceId: false }}
|
||||
stickyLeftColumn="name"
|
||||
stickyRightColumn="actions"
|
||||
|
||||
@@ -33,9 +33,9 @@ import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useState, useTransition } from "react";
|
||||
import { useDebouncedCallback } from "use-debounce";
|
||||
import {
|
||||
ManualDataTable,
|
||||
ControlledDataTable,
|
||||
type ExtendedColumnDef
|
||||
} from "./ui/manual-data-table";
|
||||
} from "./ui/controlled-data-table";
|
||||
import { ColumnFilter } from "./ColumnFilter";
|
||||
import { ColumnFilterButton } from "./ColumnFilterButton";
|
||||
import z from "zod";
|
||||
@@ -77,6 +77,7 @@ export default function SitesTable({
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const [selectedSite, setSelectedSite] = useState<SiteRow | null>(null);
|
||||
const [isRefreshing, startTransition] = useTransition();
|
||||
const [isNavigatingToAddPage, startNavigation] = useTransition();
|
||||
|
||||
const [getSortDirection, toggleSorting] = useSortColumn();
|
||||
|
||||
@@ -460,14 +461,19 @@ export default function SitesTable({
|
||||
/>
|
||||
)}
|
||||
|
||||
<ManualDataTable
|
||||
<ControlledDataTable
|
||||
columns={columns}
|
||||
rows={sites}
|
||||
tableId="sites-table"
|
||||
searchPlaceholder={t("searchSitesProgress")}
|
||||
pagination={pagination}
|
||||
onPaginationChange={handlePaginationChange}
|
||||
onAdd={() => router.push(`/${orgId}/settings/sites/create`)}
|
||||
onAdd={() =>
|
||||
startNavigation(() =>
|
||||
router.push(`/${orgId}/settings/sites/create`)
|
||||
)
|
||||
}
|
||||
isNavigatingToAddPage={isNavigatingToAddPage}
|
||||
searchQuery={searchParams.get("query")?.toString()}
|
||||
onSearch={handleSearchChange}
|
||||
addButtonText={t("siteAdd")}
|
||||
|
||||
@@ -64,7 +64,7 @@ type DataTableFilter = {
|
||||
|
||||
export type DataTablePaginationUpdateFn = (newPage: PaginationState) => void;
|
||||
|
||||
type ManualDataTableProps<TData, TValue> = {
|
||||
type ControlledDataTableProps<TData, TValue> = {
|
||||
columns: ExtendedColumnDef<TData, TValue>[];
|
||||
rows: TData[];
|
||||
tableId: string;
|
||||
@@ -72,12 +72,13 @@ type ManualDataTableProps<TData, TValue> = {
|
||||
onAdd?: () => void;
|
||||
onRefresh?: () => void;
|
||||
isRefreshing?: boolean;
|
||||
isNavigatingToAddPage?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
filters?: DataTableFilter[];
|
||||
filterDisplayMode?: "label" | "calculated"; // Global filter display mode (can be overridden per filter)
|
||||
columnVisibility?: Record<string, boolean>;
|
||||
enableColumnVisibility?: boolean;
|
||||
onSearch: (input: string) => void;
|
||||
onSearch?: (input: string) => void;
|
||||
searchQuery?: string;
|
||||
onPaginationChange: DataTablePaginationUpdateFn;
|
||||
stickyLeftColumn?: string; // Column ID or accessorKey for left sticky column
|
||||
@@ -86,7 +87,7 @@ type ManualDataTableProps<TData, TValue> = {
|
||||
pagination: PaginationState;
|
||||
};
|
||||
|
||||
export function ManualDataTable<TData, TValue>({
|
||||
export function ControlledDataTable<TData, TValue>({
|
||||
columns,
|
||||
rows,
|
||||
addButtonText,
|
||||
@@ -105,8 +106,9 @@ export function ManualDataTable<TData, TValue>({
|
||||
searchQuery,
|
||||
onPaginationChange,
|
||||
stickyRightColumn,
|
||||
rowCount
|
||||
}: ManualDataTableProps<TData, TValue>) {
|
||||
rowCount,
|
||||
isNavigatingToAddPage
|
||||
}: ControlledDataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
@@ -217,17 +219,20 @@ export function ManualDataTable<TData, TValue>({
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4">
|
||||
<div className="flex flex-row space-y-3 w-full sm:mr-2 gap-2">
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) =>
|
||||
onSearch(e.currentTarget.value)
|
||||
}
|
||||
className="w-full pl-8"
|
||||
/>
|
||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||
</div>
|
||||
{onSearch && (
|
||||
<div className="relative w-full sm:max-w-sm">
|
||||
<Input
|
||||
placeholder={searchPlaceholder}
|
||||
defaultValue={searchQuery}
|
||||
onChange={(e) =>
|
||||
onSearch(e.currentTarget.value)
|
||||
}
|
||||
className="w-full pl-8"
|
||||
/>
|
||||
<Search className="h-4 w-4 absolute left-2 top-1/2 transform -translate-y-1/2 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{filters && filters.length > 0 && (
|
||||
<div className="flex gap-2">
|
||||
{filters.map((filter) => {
|
||||
@@ -326,7 +331,10 @@ export function ManualDataTable<TData, TValue>({
|
||||
)}
|
||||
{onAdd && addButtonText && (
|
||||
<div>
|
||||
<Button onClick={onAdd}>
|
||||
<Button
|
||||
onClick={onAdd}
|
||||
loading={isNavigatingToAddPage}
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
{addButtonText}
|
||||
</Button>
|
||||
Reference in New Issue
Block a user