mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-13 00:16:39 +00:00
Merge branch 'dev' into refactor/save-button-positions
This commit is contained in:
@@ -196,7 +196,7 @@ export default function IdpTable({ idps }: Props) {
|
||||
setSelectedIdp(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
{t("idpQuestionRemove", {
|
||||
name: selectedIdp.name
|
||||
|
||||
@@ -313,7 +313,7 @@ export default function UsersTable({ users }: Props) {
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
{t("userQuestionRemove", {
|
||||
selectedUser:
|
||||
|
||||
@@ -182,7 +182,7 @@ export default function ApiKeysTable({ apiKeys }: ApiKeyTableProps) {
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("apiKeysQuestionRemove")}</p>
|
||||
|
||||
<p>{t("apiKeysMessageRemove")}</p>
|
||||
|
||||
@@ -284,7 +284,7 @@ export default function ClientResourcesTable({
|
||||
setSelectedInternalResource(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("resourceQuestionRemove")}</p>
|
||||
<p>{t("resourceMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,8 @@ interface DataTablePaginationProps<TData> {
|
||||
isServerPagination?: boolean;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
pageSize?: number;
|
||||
pageIndex?: number;
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
@@ -33,10 +35,26 @@ export function DataTablePagination<TData>({
|
||||
totalCount,
|
||||
isServerPagination = false,
|
||||
isLoading = false,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
pageSize: controlledPageSize,
|
||||
pageIndex: controlledPageIndex
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const t = useTranslations();
|
||||
|
||||
// Use controlled values if provided, otherwise fall back to table state
|
||||
const pageSize = controlledPageSize ?? table.getState().pagination.pageSize;
|
||||
const pageIndex =
|
||||
controlledPageIndex ?? table.getState().pagination.pageIndex;
|
||||
|
||||
// Calculate page boundaries based on controlled state
|
||||
// For server-side pagination, use totalCount if available for accurate page count
|
||||
const pageCount =
|
||||
isServerPagination && totalCount !== undefined
|
||||
? Math.ceil(totalCount / pageSize)
|
||||
: table.getPageCount();
|
||||
const canNextPage = pageIndex < pageCount - 1;
|
||||
const canPreviousPage = pageIndex > 0;
|
||||
|
||||
const handlePageSizeChange = (value: string) => {
|
||||
const newPageSize = Number(value);
|
||||
table.setPageSize(newPageSize);
|
||||
@@ -51,7 +69,7 @@ export function DataTablePagination<TData>({
|
||||
action: "first" | "previous" | "next" | "last"
|
||||
) => {
|
||||
if (isServerPagination && onPageChange) {
|
||||
const currentPage = table.getState().pagination.pageIndex;
|
||||
const currentPage = pageIndex;
|
||||
const pageCount = table.getPageCount();
|
||||
|
||||
let newPage: number;
|
||||
@@ -77,18 +95,24 @@ export function DataTablePagination<TData>({
|
||||
}
|
||||
} else {
|
||||
// Use table's built-in navigation for client-side pagination
|
||||
// But add bounds checking to prevent going beyond page boundaries
|
||||
const pageCount = table.getPageCount();
|
||||
switch (action) {
|
||||
case "first":
|
||||
table.setPageIndex(0);
|
||||
break;
|
||||
case "previous":
|
||||
table.previousPage();
|
||||
if (pageIndex > 0) {
|
||||
table.previousPage();
|
||||
}
|
||||
break;
|
||||
case "next":
|
||||
table.nextPage();
|
||||
if (pageIndex < pageCount - 1) {
|
||||
table.nextPage();
|
||||
}
|
||||
break;
|
||||
case "last":
|
||||
table.setPageIndex(table.getPageCount() - 1);
|
||||
table.setPageIndex(Math.max(0, pageCount - 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -98,14 +122,12 @@ export function DataTablePagination<TData>({
|
||||
<div className="flex items-center justify-between text-muted-foreground">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
value={`${pageSize}`}
|
||||
onValueChange={handlePageSizeChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
|
||||
<SelectValue
|
||||
placeholder={table.getState().pagination.pageSize}
|
||||
/>
|
||||
<SelectValue placeholder={pageSize} />
|
||||
</SelectTrigger>
|
||||
<SelectContent side="bottom">
|
||||
{[10, 20, 30, 40, 50, 100].map((pageSize) => (
|
||||
@@ -121,16 +143,11 @@ export function DataTablePagination<TData>({
|
||||
<div className="flex items-center justify-center text-sm font-medium">
|
||||
{isServerPagination && totalCount !== undefined
|
||||
? t("paginator", {
|
||||
current:
|
||||
table.getState().pagination.pageIndex + 1,
|
||||
last: Math.ceil(
|
||||
totalCount /
|
||||
table.getState().pagination.pageSize
|
||||
)
|
||||
current: pageIndex + 1,
|
||||
last: Math.ceil(totalCount / pageSize)
|
||||
})
|
||||
: t("paginator", {
|
||||
current:
|
||||
table.getState().pagination.pageIndex + 1,
|
||||
current: pageIndex + 1,
|
||||
last: table.getPageCount()
|
||||
})}
|
||||
</div>
|
||||
@@ -139,9 +156,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => handlePageNavigation("first")}
|
||||
disabled={
|
||||
!table.getCanPreviousPage() || isLoading || disabled
|
||||
}
|
||||
disabled={!canPreviousPage || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t("paginatorToFirst")}</span>
|
||||
<DoubleArrowLeftIcon className="h-4 w-4" />
|
||||
@@ -150,9 +165,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePageNavigation("previous")}
|
||||
disabled={
|
||||
!table.getCanPreviousPage() || isLoading || disabled
|
||||
}
|
||||
disabled={!canPreviousPage || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">
|
||||
{t("paginatorToPrevious")}
|
||||
@@ -163,9 +176,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePageNavigation("next")}
|
||||
disabled={
|
||||
!table.getCanNextPage() || isLoading || disabled
|
||||
}
|
||||
disabled={!canNextPage || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t("paginatorToNext")}</span>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
@@ -174,9 +185,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => handlePageNavigation("last")}
|
||||
disabled={
|
||||
!table.getCanNextPage() || isLoading || disabled
|
||||
}
|
||||
disabled={!canNextPage || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t("paginatorToLast")}</span>
|
||||
<DoubleArrowRightIcon className="h-4 w-4" />
|
||||
|
||||
@@ -304,7 +304,7 @@ export default function DomainsTable({ domains, orgId }: Props) {
|
||||
setSelectedDomain(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("domainQuestionRemove")}</p>
|
||||
<p>{t("domainMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -182,7 +182,7 @@ export default function InvitationsTable({
|
||||
setSelectedInvitation(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("inviteQuestionRemove")}</p>
|
||||
<p>{t("inviteMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -25,6 +25,7 @@ import { useEffect, useState } from "react";
|
||||
import { FaGithub } from "react-icons/fa";
|
||||
import SidebarLicenseButton from "./SidebarLicenseButton";
|
||||
import { SidebarSupportButton } from "./SidebarSupportButton";
|
||||
import { is } from "drizzle-orm";
|
||||
|
||||
const ProductUpdates = dynamic(() => import("./ProductUpdates"), {
|
||||
ssr: false
|
||||
@@ -52,7 +53,7 @@ export function LayoutSidebar({
|
||||
const pathname = usePathname();
|
||||
const isAdminPage = pathname?.startsWith("/admin");
|
||||
const { user } = useUserContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const { isUnlocked, licenseStatus } = useLicenseStatusContext();
|
||||
const { env } = useEnvContext();
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -226,6 +227,18 @@ export function LayoutSidebar({
|
||||
<FaGithub size={12} />
|
||||
</Link>
|
||||
</div>
|
||||
{build === "enterprise" &&
|
||||
isUnlocked() &&
|
||||
licenseStatus?.tier === "personal" ? (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{t("personalUseOnly")}
|
||||
</div>
|
||||
) : null}
|
||||
{build === "enterprise" && !isUnlocked() ? (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
{t("unlicensed")}
|
||||
</div>
|
||||
) : null}
|
||||
{env?.app?.version && (
|
||||
<div className="text-xs text-muted-foreground text-center">
|
||||
<Link
|
||||
|
||||
@@ -542,6 +542,8 @@ export function LogDataTable<TData, TValue>({
|
||||
isServerPagination={isServerPagination}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
pageSize={pageSize}
|
||||
pageIndex={currentPage}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
@@ -354,7 +354,7 @@ export default function MachineClientsTable({
|
||||
setSelectedClient(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("deleteClientQuestion")}</p>
|
||||
<p>{t("clientMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function OrgApiKeysTable({
|
||||
setSelected(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("apiKeysQuestionRemove")}</p>
|
||||
|
||||
<p>{t("apiKeysMessageRemove")}</p>
|
||||
|
||||
@@ -535,7 +535,7 @@ export default function ProxyResourcesTable({
|
||||
setSelectedResource(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("resourceQuestionRemove")}</p>
|
||||
<p>{t("resourceMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -93,7 +93,7 @@ type ResourceAuthPortalProps = {
|
||||
export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const { isUnlocked, licenseStatus } = useLicenseStatusContext();
|
||||
|
||||
const getNumMethods = () => {
|
||||
let colLength = 0;
|
||||
@@ -737,6 +737,22 @@ export default function ResourceAuthPortal(props: ResourceAuthPortalProps) {
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{build === "enterprise" && !isUnlocked() ? (
|
||||
<div className="text-center mt-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("instanceIsUnlicensed")}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
{build === "enterprise" &&
|
||||
isUnlocked() &&
|
||||
licenseStatus?.tier === "personal" ? (
|
||||
<div className="text-center mt-2">
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("loginPageLicenseWatermark")}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<ResourceAccessDenied />
|
||||
|
||||
@@ -412,7 +412,7 @@ export default function SitesTable({ sites, orgId }: SitesTableProps) {
|
||||
setSelectedSite(null);
|
||||
}}
|
||||
dialog={
|
||||
<div className="">
|
||||
<div className="space-y-2">
|
||||
<p>{t("siteQuestionRemove")}</p>
|
||||
<p>{t("siteMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -401,7 +401,7 @@ export default function UserDevicesTable({ userClients }: ClientTableProps) {
|
||||
setSelectedClient(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("deleteClientQuestion")}</p>
|
||||
<p>{t("clientMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -258,7 +258,7 @@ export default function UsersTable({ users: u }: UsersTableProps) {
|
||||
setSelectedUser(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("userQuestionOrgRemove")}</p>
|
||||
<p>{t("userMessageOrgRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -224,7 +224,7 @@ export default function ViewDevicesDialog({
|
||||
}
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>
|
||||
{t("deviceQuestionRemove") ||
|
||||
"Are you sure you want to delete this device?"}
|
||||
|
||||
@@ -177,7 +177,7 @@ export default function IdpTable({ idps, orgId }: Props) {
|
||||
setSelectedIdp(null);
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p>{t("idpQuestionRemove")}</p>
|
||||
<p>{t("idpMessageRemove")}</p>
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,8 @@ import {
|
||||
getSortedRowModel,
|
||||
ColumnFiltersState,
|
||||
getFilteredRowModel,
|
||||
VisibilityState
|
||||
VisibilityState,
|
||||
PaginationState
|
||||
} from "@tanstack/react-table";
|
||||
|
||||
// Extended ColumnDef type that includes optional friendlyName for column visibility dropdown
|
||||
@@ -227,6 +228,10 @@ export function DataTable<TData, TValue>({
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
|
||||
initialColumnVisibility
|
||||
);
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: pageSize
|
||||
});
|
||||
const [activeTab, setActiveTab] = useState<string>(
|
||||
defaultTab || tabs?.[0]?.id || ""
|
||||
);
|
||||
@@ -256,6 +261,7 @@ export function DataTable<TData, TValue>({
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
onPaginationChange: setPagination,
|
||||
initialState: {
|
||||
pagination: {
|
||||
pageSize: pageSize,
|
||||
@@ -267,21 +273,18 @@ export function DataTable<TData, TValue>({
|
||||
sorting,
|
||||
columnFilters,
|
||||
globalFilter,
|
||||
columnVisibility
|
||||
columnVisibility,
|
||||
pagination
|
||||
}
|
||||
});
|
||||
|
||||
// Persist pageSize to localStorage when it changes
|
||||
useEffect(() => {
|
||||
const currentPageSize = table.getState().pagination.pageSize;
|
||||
if (currentPageSize !== pageSize) {
|
||||
table.setPageSize(pageSize);
|
||||
|
||||
// Persist to localStorage if enabled
|
||||
if (persistPageSize) {
|
||||
setStoredPageSize(pageSize, tableId);
|
||||
}
|
||||
if (persistPageSize && pagination.pageSize !== pageSize) {
|
||||
setStoredPageSize(pagination.pageSize, tableId);
|
||||
setPageSize(pagination.pageSize);
|
||||
}
|
||||
}, [pageSize, table, persistPageSize, tableId]);
|
||||
}, [pagination.pageSize, persistPageSize, tableId, pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
// Persist column visibility to localStorage when it changes
|
||||
@@ -293,13 +296,17 @@ export function DataTable<TData, TValue>({
|
||||
const handleTabChange = (value: string) => {
|
||||
setActiveTab(value);
|
||||
// Reset to first page when changing tabs
|
||||
table.setPageIndex(0);
|
||||
setPagination((prev) => ({ ...prev, pageIndex: 0 }));
|
||||
};
|
||||
|
||||
// Enhanced pagination component that updates our local state
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
setPagination((prev) => ({
|
||||
...prev,
|
||||
pageSize: newPageSize,
|
||||
pageIndex: 0
|
||||
}));
|
||||
setPageSize(newPageSize);
|
||||
table.setPageSize(newPageSize);
|
||||
|
||||
// Persist immediately when changed
|
||||
if (persistPageSize) {
|
||||
@@ -614,6 +621,8 @@ export function DataTable<TData, TValue>({
|
||||
<DataTablePagination
|
||||
table={table}
|
||||
onPageSizeChange={handlePageSizeChange}
|
||||
pageSize={pagination.pageSize}
|
||||
pageIndex={pagination.pageIndex}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user