Merge branch 'dev' into refactor/save-button-positions

This commit is contained in:
Fred KISSIE
2025-12-15 17:04:58 +01:00
37 changed files with 218 additions and 140 deletions

View File

@@ -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

View File

@@ -313,7 +313,7 @@ export default function UsersTable({ users }: Props) {
setSelected(null);
}}
dialog={
<div>
<div className="space-y-2">
<p>
{t("userQuestionRemove", {
selectedUser:

View File

@@ -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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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

View File

@@ -542,6 +542,8 @@ export function LogDataTable<TData, TValue>({
isServerPagination={isServerPagination}
isLoading={isLoading}
disabled={disabled}
pageSize={pageSize}
pageIndex={currentPage}
/>
</div>
</CardContent>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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 />

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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?"}

View File

@@ -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>

View File

@@ -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>