Date picker working

This commit is contained in:
Owen
2025-10-21 20:15:43 -07:00
parent bdc3b2425b
commit 1142d6ac48
10 changed files with 555 additions and 109 deletions

View File

@@ -1901,5 +1901,6 @@
"actor": "Actor", "actor": "Actor",
"timestamp": "Timestamp", "timestamp": "Timestamp",
"accessLogs": "Access Logs", "accessLogs": "Access Logs",
"exportCsv": "Export CSV" "exportCsv": "Export CSV",
"actorId": "Actor ID"
} }

77
package-lock.json generated
View File

@@ -52,6 +52,7 @@
"cookies": "^0.9.1", "cookies": "^0.9.1",
"cors": "2.8.5", "cors": "2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "4.1.0",
"drizzle-orm": "0.44.6", "drizzle-orm": "0.44.6",
"eslint": "9.37.0", "eslint": "9.37.0",
"eslint-config-next": "15.5.6", "eslint-config-next": "15.5.6",
@@ -81,6 +82,7 @@
"posthog-node": "^5.9.5", "posthog-node": "^5.9.5",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.0", "react": "19.2.0",
"react-day-picker": "9.11.1",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-easy-sort": "^1.8.0", "react-easy-sort": "^1.8.0",
"react-hook-form": "7.65.0", "react-hook-form": "7.65.0",
@@ -1628,6 +1630,7 @@
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.0", "@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2", "@babel/code-frame": "^7.26.2",
@@ -1979,6 +1982,12 @@
"kuler": "^2.0.0" "kuler": "^2.0.0"
} }
}, },
"node_modules/@date-fns/tz": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/@date-fns/tz/-/tz-1.4.1.tgz",
"integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==",
"license": "MIT"
},
"node_modules/@dotenvx/dotenvx": { "node_modules/@dotenvx/dotenvx": {
"version": "1.51.0", "version": "1.51.0",
"resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.51.0.tgz",
@@ -4015,6 +4024,7 @@
"integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": "^14.21.3 || >=16" "node": "^14.21.3 || >=16"
}, },
@@ -6844,6 +6854,7 @@
"integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==", "integrity": "sha512-JuRQ9KXLEjaUNjTWpzuR231Z2WpIwczOkBEIvbHNCzQefFIT0L8IqE6NV6ULLyC1SI/i234JnDoMkfg+RjQj2g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -7049,6 +7060,7 @@
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -7059,6 +7071,7 @@
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==", "integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.25.0" "scheduler": "^0.25.0"
}, },
@@ -8490,6 +8503,7 @@
"integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==", "integrity": "sha512-fnQmj8lELIj7BSrZQAdBMHEHX8OZLYIHXqAKT1O7tDfLxaINzf00PMjw22r3N/xXh0w/sGHlO6SVaCQ2mj78lg==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -8576,6 +8590,7 @@
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/body-parser": "*", "@types/body-parser": "*",
"@types/express-serve-static-core": "^5.0.0", "@types/express-serve-static-core": "^5.0.0",
@@ -8669,6 +8684,7 @@
"integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.14.0" "undici-types": "~7.14.0"
} }
@@ -8697,6 +8713,7 @@
"integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==", "integrity": "sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
"pg-protocol": "*", "pg-protocol": "*",
@@ -8730,6 +8747,7 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==", "integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
@@ -8740,6 +8758,7 @@
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
@@ -8883,6 +8902,7 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz",
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.1",
@@ -9556,6 +9576,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -10086,6 +10107,7 @@
"integrity": "sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==", "integrity": "sha512-mXpa5jnIKKHeoGzBrUJrc65cXFKcILGZpU3FXR0pradUEm9MA7UZz02qfEejaMcm9iXrSOCenwwYMJ/tZ1y5Ig==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"bindings": "^1.5.0", "bindings": "^1.5.0",
"prebuild-install": "^7.1.1" "prebuild-install": "^7.1.1"
@@ -10199,6 +10221,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.8.9", "baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746", "caniuse-lite": "^1.0.30001746",
@@ -10936,6 +10959,22 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-jalali": {
"version": "4.1.0-0",
"resolved": "https://registry.npmjs.org/date-fns-jalali/-/date-fns-jalali-4.1.0-0.tgz",
"integrity": "sha512-hTIP/z+t+qKwBDcmmsnmjWTduxCg+5KfdqWQvb2X/8C9+knYY6epN/pfxdDuyVlSVeFz0sM5eEfwIUQ70U4ckg==",
"license": "MIT"
},
"node_modules/debounce": { "node_modules/debounce": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz", "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.2.0.tgz",
@@ -11839,6 +11878,7 @@
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"esbuild": "bin/esbuild" "esbuild": "bin/esbuild"
}, },
@@ -11935,6 +11975,7 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.37.0.tgz",
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==", "integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
@@ -12102,6 +12143,7 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@rtsao/scc": "^1.1.0", "@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9", "array-includes": "^3.1.9",
@@ -12391,6 +12433,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"accepts": "^2.0.0", "accepts": "^2.0.0",
"body-parser": "^2.2.0", "body-parser": "^2.2.0",
@@ -17514,6 +17557,7 @@
"version": "4.0.3", "version": "4.0.3",
"inBundle": true, "inBundle": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -18492,6 +18536,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"pg-connection-string": "^2.9.1", "pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1", "pg-pool": "^3.10.1",
@@ -18668,6 +18713,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
@@ -19119,15 +19165,38 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-day-picker": {
"version": "9.11.1",
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.11.1.tgz",
"integrity": "sha512-l3ub6o8NlchqIjPKrRFUCkTUEq6KwemQlfv3XZzzwpUeGwmDJ+0u0Upmt38hJyd7D/vn2dQoOoLV/qAp0o3uUw==",
"license": "MIT",
"dependencies": {
"@date-fns/tz": "^1.4.1",
"date-fns": "^4.1.0",
"date-fns-jalali": "^4.1.0-0"
},
"engines": {
"node": ">=18"
},
"funding": {
"type": "individual",
"url": "https://github.com/sponsors/gpbl"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "19.2.0", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"scheduler": "^0.27.0" "scheduler": "^0.27.0"
}, },
@@ -19419,6 +19488,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz",
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==", "integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -19903,6 +19973,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1", "fast-uri": "^3.0.1",
@@ -21068,7 +21139,8 @@
"version": "4.1.14", "version": "4.1.14",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.14.tgz",
"integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==", "integrity": "sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.3.0", "version": "2.3.0",
@@ -21625,6 +21697,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -22130,6 +22203,7 @@
"resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz",
"integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@colors/colors": "^1.6.0", "@colors/colors": "^1.6.0",
"@dabh/diagnostics": "^2.0.8", "@dabh/diagnostics": "^2.0.8",
@@ -22437,6 +22511,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }

View File

@@ -75,6 +75,7 @@
"cookies": "^0.9.1", "cookies": "^0.9.1",
"cors": "2.8.5", "cors": "2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"date-fns": "4.1.0",
"drizzle-orm": "0.44.6", "drizzle-orm": "0.44.6",
"eslint": "9.37.0", "eslint": "9.37.0",
"eslint-config-next": "15.5.6", "eslint-config-next": "15.5.6",
@@ -104,6 +105,7 @@
"posthog-node": "^5.9.5", "posthog-node": "^5.9.5",
"qrcode.react": "4.2.0", "qrcode.react": "4.2.0",
"react": "19.2.0", "react": "19.2.0",
"react-day-picker": "9.11.1",
"react-dom": "19.2.0", "react-dom": "19.2.0",
"react-easy-sort": "^1.8.0", "react-easy-sort": "^1.8.0",
"react-hook-form": "7.65.0", "react-hook-form": "7.65.0",

View File

@@ -65,6 +65,7 @@ export function querySites(timeStart: number, timeEnd: number, orgId: string) {
orgId: actionAuditLog.orgId, orgId: actionAuditLog.orgId,
action: actionAuditLog.action, action: actionAuditLog.action,
actorType: actionAuditLog.actorType, actorType: actionAuditLog.actorType,
actorId: actionAuditLog.actorId,
timestamp: actionAuditLog.timestamp, timestamp: actionAuditLog.timestamp,
actor: actionAuditLog.actor actor: actionAuditLog.actor
}) })

View File

@@ -23,22 +23,31 @@ export default function GeneralPage() {
const [isRefreshing, setIsRefreshing] = useState(false); const [isRefreshing, setIsRefreshing] = useState(false);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
// Pagination state
const [totalCount, setTotalCount] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(20);
const [isLoading, setIsLoading] = useState(false);
// Set default date range to last 24 hours // Set default date range to last 24 hours
const getDefaultDateRange = () => { const getDefaultDateRange = () => {
const now = new Date(); const now = new Date();
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000); const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000);
return { return {
startDate: { startDate: {
date: yesterday, date: yesterday
}, },
endDate: { endDate: {
date: now, date: now
} }
}; };
}; };
const [dateRange, setDateRange] = useState<{ startDate: DateTimeValue; endDate: DateTimeValue }>(getDefaultDateRange()); const [dateRange, setDateRange] = useState<{
startDate: DateTimeValue;
endDate: DateTimeValue;
}>(getDefaultDateRange());
// Trigger search with default values on component mount // Trigger search with default values on component mount
useEffect(() => { useEffect(() => {
@@ -51,21 +60,42 @@ export default function GeneralPage() {
endDate: DateTimeValue endDate: DateTimeValue
) => { ) => {
setDateRange({ startDate, endDate }); setDateRange({ startDate, endDate });
queryDateTime(startDate, endDate); setCurrentPage(0); // Reset to first page when filtering
queryDateTime(startDate, endDate, 0, pageSize);
};
// Handle page changes
const handlePageChange = (newPage: number) => {
setCurrentPage(newPage);
queryDateTime(
dateRange.startDate,
dateRange.endDate,
newPage,
pageSize
);
};
// Handle page size changes
const handlePageSizeChange = (newPageSize: number) => {
setPageSize(newPageSize);
setCurrentPage(0); // Reset to first page when changing page size
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
}; };
const queryDateTime = async ( const queryDateTime = async (
startDate: DateTimeValue, startDate: DateTimeValue,
endDate: DateTimeValue endDate: DateTimeValue,
page: number = currentPage,
size: number = pageSize
) => { ) => {
console.log("Date range changed:", { startDate, endDate }); console.log("Date range changed:", { startDate, endDate, page, size });
setIsRefreshing(true); setIsLoading(true);
try { try {
// Convert the date/time values to API parameters // Convert the date/time values to API parameters
let params: any = { let params: any = {
limit: 20, limit: size,
offset: 0 offset: page * size
}; };
if (startDate?.date) { if (startDate?.date) {
@@ -89,14 +119,20 @@ export default function GeneralPage() {
} else { } else {
// If no time is specified, set to NOW // If no time is specified, set to NOW
const now = new Date(); const now = new Date();
endDateTime.setHours(now.getHours(), now.getMinutes(), now.getSeconds(), now.getMilliseconds()); endDateTime.setHours(
now.getHours(),
now.getMinutes(),
now.getSeconds(),
now.getMilliseconds()
);
} }
params.timeEnd = endDateTime.toISOString(); params.timeEnd = endDateTime.toISOString();
} }
const res = await api.get(`/org/${orgId}/logs/action`, { params }); const res = await api.get(`/org/${orgId}/logs/action`, { params });
if (res.status === 200) { if (res.status === 200) {
setRows(res.data.data.log); setRows(res.data.data.log || []);
setTotalCount(res.data.data.pagination?.total || 0);
console.log("Fetched logs:", res.data); console.log("Fetched logs:", res.data);
} }
} catch (error) { } catch (error) {
@@ -106,17 +142,21 @@ export default function GeneralPage() {
variant: "destructive" variant: "destructive"
}); });
} finally { } finally {
setIsRefreshing(false); setIsLoading(false);
} }
}; };
const refreshData = async () => { const refreshData = async () => {
console.log("Data refreshed"); console.log("Data refreshed");
setIsRefreshing(true); setIsRefreshing(true);
try { try {
await new Promise((resolve) => setTimeout(resolve, 200)); // Refresh data with current date range and pagination
router.refresh(); await queryDateTime(
dateRange.startDate,
dateRange.endDate,
currentPage,
pageSize
);
} catch (error) { } catch (error) {
toast({ toast({
title: t("error"), title: t("error"),
@@ -139,8 +179,8 @@ export default function GeneralPage() {
: undefined, : undefined,
timeEnd: dateRange.endDate?.date timeEnd: dateRange.endDate?.date
? new Date(dateRange.endDate.date).toISOString() ? new Date(dateRange.endDate.date).toISOString()
: undefined, : undefined
}, }
}); });
// Create a URL for the blob and trigger a download // Create a URL for the blob and trigger a download
@@ -148,7 +188,10 @@ export default function GeneralPage() {
const link = document.createElement("a"); const link = document.createElement("a");
link.href = url; link.href = url;
const epoch = Math.floor(Date.now() / 1000); const epoch = Math.floor(Date.now() / 1000);
link.setAttribute("download", `access_audit_logs_${orgId}_${epoch}.csv`); link.setAttribute(
"download",
`access_audit_logs_${orgId}_${epoch}.csv`
);
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
link.parentNode?.removeChild(link); link.parentNode?.removeChild(link);
@@ -166,21 +209,11 @@ export default function GeneralPage() {
{ {
accessorKey: "timestamp", accessorKey: "timestamp",
header: ({ column }) => { header: ({ column }) => {
return ( return t("timestamp");
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
className="hidden md:flex whitespace-nowrap"
>
{t("timestamp")}
</Button>
);
}, },
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<div className="whitespace-nowrap"> <div className="whitespace-nowrap">
{new Date( {new Date(
row.original.timestamp * 1000 row.original.timestamp * 1000
).toLocaleString()} ).toLocaleString()}
@@ -191,48 +224,48 @@ export default function GeneralPage() {
{ {
accessorKey: "action", accessorKey: "action",
header: ({ column }) => { header: ({ column }) => {
return ( return t("action");
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("action")}
</Button>
);
}, },
// make the value capitalized // make the value capitalized
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<span className="hitespace-nowrap"> <span className="hitespace-nowrap">
{row.original.action.charAt(0).toUpperCase() + row.original.action.slice(1)} {row.original.action.charAt(0).toUpperCase() +
row.original.action.slice(1)}
</span> </span>
); );
}, }
}, },
{ {
accessorKey: "actor", accessorKey: "actor",
header: ({ column }) => { header: ({ column }) => {
return ( return t("actor");
<Button
variant="ghost"
onClick={() =>
column.toggleSorting(column.getIsSorted() === "asc")
}
>
{t("actor")}
</Button>
);
}, },
cell: ({ row }) => { cell: ({ row }) => {
return ( return (
<span className="flex items-center gap-1"> <span className="flex items-center gap-1">
{row.original.actorType == "user" ? <User className="h-4 w-4" /> : <Key className="h-4 w-4" />} {row.original.actorType == "user" ? (
<User className="h-4 w-4" />
) : (
<Key className="h-4 w-4" />
)}
{row.original.actor} {row.original.actor}
</span> </span>
); );
} }
},
{
accessorKey: "actorId",
header: ({ column }) => {
return t("actorId");
},
cell: ({ row }) => {
return (
<span className="flex items-center gap-1">
{row.original.actorId}
</span>
);
}
} }
]; ];
@@ -258,6 +291,13 @@ export default function GeneralPage() {
id: "timestamp", id: "timestamp",
desc: false desc: false
}} }}
// Server-side pagination props
totalCount={totalCount}
currentPage={currentPage}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
isLoading={isLoading}
defaultPageSize={pageSize}
/> />
</> </>
); );

View File

@@ -34,8 +34,8 @@ export default async function GeneralSettingsPage({
const navItems = [ const navItems = [
{ {
title: t("access"), title: t("action"),
href: `/{orgId}/settings/logs/access` href: `/{orgId}/settings/logs/action`
}, },
{ {
title: t("request"), title: t("request"),

View File

@@ -19,11 +19,19 @@ import { useTranslations } from "next-intl";
interface DataTablePaginationProps<TData> { interface DataTablePaginationProps<TData> {
table: Table<TData>; table: Table<TData>;
onPageSizeChange?: (pageSize: number) => void; onPageSizeChange?: (pageSize: number) => void;
onPageChange?: (pageIndex: number) => void;
totalCount?: number;
isServerPagination?: boolean;
isLoading?: boolean;
} }
export function DataTablePagination<TData>({ export function DataTablePagination<TData>({
table, table,
onPageSizeChange onPageSizeChange,
onPageChange,
totalCount,
isServerPagination = false,
isLoading = false
}: DataTablePaginationProps<TData>) { }: DataTablePaginationProps<TData>) {
const t = useTranslations(); const t = useTranslations();
@@ -37,6 +45,51 @@ export function DataTablePagination<TData>({
} }
}; };
const handlePageNavigation = (action: 'first' | 'previous' | 'next' | 'last') => {
if (isServerPagination && onPageChange) {
const currentPage = table.getState().pagination.pageIndex;
const pageCount = table.getPageCount();
let newPage: number;
switch (action) {
case 'first':
newPage = 0;
break;
case 'previous':
newPage = Math.max(0, currentPage - 1);
break;
case 'next':
newPage = Math.min(pageCount - 1, currentPage + 1);
break;
case 'last':
newPage = pageCount - 1;
break;
default:
return;
}
if (newPage !== currentPage) {
onPageChange(newPage);
}
} else {
// Use table's built-in navigation for client-side pagination
switch (action) {
case 'first':
table.setPageIndex(0);
break;
case 'previous':
table.previousPage();
break;
case 'next':
table.nextPage();
break;
case 'last':
table.setPageIndex(table.getPageCount() - 1);
break;
}
}
};
return ( return (
<div className="flex items-center justify-between text-muted-foreground"> <div className="flex items-center justify-between text-muted-foreground">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -61,14 +114,21 @@ export function DataTablePagination<TData>({
<div className="flex items-center space-x-3 lg:space-x-8"> <div className="flex items-center space-x-3 lg:space-x-8">
<div className="flex items-center justify-center text-sm font-medium"> <div className="flex items-center justify-center text-sm font-medium">
{t('paginator', {current: table.getState().pagination.pageIndex + 1, last: table.getPageCount()})} {isServerPagination && totalCount !== undefined ? (
t('paginator', {
current: table.getState().pagination.pageIndex + 1,
last: Math.ceil(totalCount / table.getState().pagination.pageSize)
})
) : (
t('paginator', {current: table.getState().pagination.pageIndex + 1, last: table.getPageCount()})
)}
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Button <Button
variant="outline" variant="outline"
className="hidden h-8 w-8 p-0 lg:flex" className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)} onClick={() => handlePageNavigation('first')}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage() || isLoading}
> >
<span className="sr-only">{t('paginatorToFirst')}</span> <span className="sr-only">{t('paginatorToFirst')}</span>
<DoubleArrowLeftIcon className="h-4 w-4" /> <DoubleArrowLeftIcon className="h-4 w-4" />
@@ -76,8 +136,8 @@ export function DataTablePagination<TData>({
<Button <Button
variant="outline" variant="outline"
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
onClick={() => table.previousPage()} onClick={() => handlePageNavigation('previous')}
disabled={!table.getCanPreviousPage()} disabled={!table.getCanPreviousPage() || isLoading}
> >
<span className="sr-only">{t('paginatorToPrevious')}</span> <span className="sr-only">{t('paginatorToPrevious')}</span>
<ChevronLeftIcon className="h-4 w-4" /> <ChevronLeftIcon className="h-4 w-4" />
@@ -85,8 +145,8 @@ export function DataTablePagination<TData>({
<Button <Button
variant="outline" variant="outline"
className="h-8 w-8 p-0" className="h-8 w-8 p-0"
onClick={() => table.nextPage()} onClick={() => handlePageNavigation('next')}
disabled={!table.getCanNextPage()} disabled={!table.getCanNextPage() || isLoading}
> >
<span className="sr-only">{t('paginatorToNext')}</span> <span className="sr-only">{t('paginatorToNext')}</span>
<ChevronRightIcon className="h-4 w-4" /> <ChevronRightIcon className="h-4 w-4" />
@@ -94,10 +154,8 @@ export function DataTablePagination<TData>({
<Button <Button
variant="outline" variant="outline"
className="hidden h-8 w-8 p-0 lg:flex" className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => onClick={() => handlePageNavigation('last')}
table.setPageIndex(table.getPageCount() - 1) disabled={!table.getCanNextPage() || isLoading}
}
disabled={!table.getCanNextPage()}
> >
<span className="sr-only">{t('paginatorToLast')}</span> <span className="sr-only">{t('paginatorToLast')}</span>
<DoubleArrowRightIcon className="h-4 w-4" /> <DoubleArrowRightIcon className="h-4 w-4" />

View File

@@ -3,6 +3,7 @@
import { ChevronDownIcon, CalendarIcon } from "lucide-react"; import { ChevronDownIcon, CalendarIcon } from "lucide-react";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { Calendar } from "@app/components/ui/calendar";
import { Input } from "@app/components/ui/input"; import { Input } from "@app/components/ui/input";
import { Label } from "@app/components/ui/label"; import { Label } from "@app/components/ui/label";
import { import {
@@ -60,14 +61,20 @@ export function DateTimePicker({
onChange?.(newValue); onChange?.(newValue);
}; };
const getDisplayText = () => { const getDisplayText = () => {
if (!internalDate) return placeholder; if (!internalDate) return placeholder;
const dateStr = internalDate.toLocaleDateString(); const dateStr = internalDate.toLocaleDateString();
if (!showTime || !internalTime) return dateStr; if (!showTime || !internalTime) return dateStr;
return `${dateStr} ${internalTime}`; // Parse time and format in local timezone
}; const [hours, minutes, seconds] = internalTime.split(':');
const timeDate = new Date();
timeDate.setHours(parseInt(hours, 10), parseInt(minutes, 10), parseInt(seconds || '0', 10));
const timeStr = timeDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
return `${dateStr} ${timeStr}`;
};
const hasValue = internalDate || (showTime && internalTime); const hasValue = internalDate || (showTime && internalTime);
@@ -93,36 +100,26 @@ export function DateTimePicker({
)} )}
> >
{getDisplayText()} {getDisplayText()}
<CalendarIcon className="h-4 w-4" /> <ChevronDownIcon className="h-4 w-4" />
</Button> </Button>
</PopoverTrigger> </PopoverTrigger>
<PopoverContent className="w-auto overflow-hidden p-0" align="start"> <PopoverContent className="w-auto overflow-hidden p-0" align="start">
<div className="p-3"> {showTime ? (
<div className="space-y-3"> <div className="flex">
<div> <Calendar
<Label htmlFor="date-input" className="text-sm font-medium"> mode="single"
Date selected={internalDate}
</Label> captionLayout="dropdown"
<Input onSelect={(date) => {
id="date-input" handleDateChange(date);
type="date" if (!showTime) {
value={internalDate ? setOpen(false);
`${internalDate.getFullYear()}-${String(internalDate.getMonth() + 1).padStart(2, '0')}-${String(internalDate.getDate()).padStart(2, '0')}` }
: ''} }}
onChange={(e) => { className="flex-grow w-[250px]"
let dateValue = undefined; />
if (e.target.value) { <div className="p-3 border-l">
// Create date in local timezone to avoid offset issues <div className="flex flex-col gap-3">
const parts = e.target.value.split('-');
dateValue = new Date(parseInt(parts[0]), parseInt(parts[1]) - 1, parseInt(parts[2]));
}
handleDateChange(dateValue);
}}
className="mt-1"
/>
</div>
{showTime && (
<div>
<Label htmlFor="time-input" className="text-sm font-medium"> <Label htmlFor="time-input" className="text-sm font-medium">
Time Time
</Label> </Label>
@@ -132,12 +129,22 @@ export function DateTimePicker({
step="1" step="1"
value={internalTime} value={internalTime}
onChange={handleTimeChange} onChange={handleTimeChange}
className="mt-1 bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none" className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
/> />
</div> </div>
)} </div>
</div> </div>
</div> ) : (
<Calendar
mode="single"
selected={internalDate}
captionLayout="dropdown"
onSelect={(date) => {
handleDateChange(date);
setOpen(false);
}}
/>
)}
</PopoverContent> </PopoverContent>
</Popover> </Popover>
</div> </div>

View File

@@ -103,6 +103,12 @@ type DataTableProps<TData, TValue> = {
start: DateTimeValue; start: DateTimeValue;
end: DateTimeValue; end: DateTimeValue;
}; };
// Server-side pagination props
totalCount?: number;
currentPage?: number;
onPageChange?: (page: number) => void;
onPageSizeChange?: (pageSize: number) => void;
isLoading?: boolean;
}; };
export function LogDataTable<TData, TValue>({ export function LogDataTable<TData, TValue>({
@@ -121,7 +127,12 @@ export function LogDataTable<TData, TValue>({
persistPageSize = false, persistPageSize = false,
defaultPageSize = 20, defaultPageSize = 20,
onDateRangeChange, onDateRangeChange,
dateRange dateRange,
totalCount,
currentPage = 0,
onPageChange,
onPageSizeChange: onPageSizeChangeProp,
isLoading = false
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const t = useTranslations(); const t = useTranslations();
@@ -175,26 +186,39 @@ export function LogDataTable<TData, TValue>({
return data.filter(activeTabFilter.filterFn); return data.filter(activeTabFilter.filterFn);
}, [data, tabs, activeTab]); }, [data, tabs, activeTab]);
// Determine if using server-side pagination
const isServerPagination = totalCount !== undefined;
const table = useReactTable({ const table = useReactTable({
data: filteredData, data: filteredData,
columns, columns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), // Only use client-side pagination if totalCount is not provided
...(isServerPagination ? {} : { getPaginationRowModel: getPaginationRowModel() }),
onSortingChange: setSorting, onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(), getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters, onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(), getFilteredRowModel: getFilteredRowModel(),
onGlobalFilterChange: setGlobalFilter, onGlobalFilterChange: setGlobalFilter,
// Configure pagination state
...(isServerPagination ? {
manualPagination: true,
pageCount: totalCount ? Math.ceil(totalCount / pageSize) : 0,
} : {}),
initialState: { initialState: {
pagination: { pagination: {
pageSize: pageSize, pageSize: pageSize,
pageIndex: 0 pageIndex: currentPage
} }
}, },
state: { state: {
sorting, sorting,
columnFilters, columnFilters,
globalFilter globalFilter,
pagination: {
pageSize: pageSize,
pageIndex: currentPage
}
} }
}); });
@@ -210,6 +234,16 @@ export function LogDataTable<TData, TValue>({
} }
}, [pageSize, table, persistPageSize, tableId]); }, [pageSize, table, persistPageSize, tableId]);
// Update table page index when currentPage prop changes (server pagination)
useEffect(() => {
if (isServerPagination) {
const currentPageIndex = table.getState().pagination.pageIndex;
if (currentPageIndex !== currentPage) {
table.setPageIndex(currentPage);
}
}
}, [currentPage, table, isServerPagination]);
const handleTabChange = (value: string) => { const handleTabChange = (value: string) => {
setActiveTab(value); setActiveTab(value);
// Reset to first page when changing tabs // Reset to first page when changing tabs
@@ -225,6 +259,18 @@ export function LogDataTable<TData, TValue>({
if (persistPageSize) { if (persistPageSize) {
setStoredPageSize(newPageSize, tableId); setStoredPageSize(newPageSize, tableId);
} }
// For server pagination, notify parent component
if (isServerPagination && onPageSizeChangeProp) {
onPageSizeChangeProp(newPageSize);
}
};
// Handle page changes for server pagination
const handlePageChange = (newPageIndex: number) => {
if (isServerPagination && onPageChange) {
onPageChange(newPageIndex);
}
}; };
const handleDateRangeChange = ( const handleDateRangeChange = (
@@ -276,7 +322,6 @@ export function LogDataTable<TData, TValue>({
)} )}
{onExport && ( {onExport && (
<Button <Button
variant="outline"
onClick={onExport} onClick={onExport}
disabled={isExporting} disabled={isExporting}
> >
@@ -342,6 +387,10 @@ export function LogDataTable<TData, TValue>({
<DataTablePagination <DataTablePagination
table={table} table={table}
onPageSizeChange={handlePageSizeChange} onPageSizeChange={handlePageSizeChange}
onPageChange={isServerPagination ? handlePageChange : undefined}
totalCount={totalCount}
isServerPagination={isServerPagination}
isLoading={isLoading}
/> />
</div> </div>
</CardContent> </CardContent>

View File

@@ -0,0 +1,213 @@
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@app/lib/cn"
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
const defaultClassNames = getDefaultClassNames()
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(
"bg-background group/calendar p-3 [--cell-size:2rem] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
className
)}
captionLayout={captionLayout}
formatters={{
formatMonthDropdown: (date) =>
date.toLocaleString("default", { month: "short" }),
...formatters,
}}
classNames={{
root: cn("w-fit", defaultClassNames.root),
months: cn(
"relative flex flex-col gap-4 md:flex-row",
defaultClassNames.months
),
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
nav: cn(
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
defaultClassNames.nav
),
button_previous: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_next
),
month_caption: cn(
"flex h-[--cell-size] w-full items-center justify-center px-[--cell-size]",
defaultClassNames.month_caption
),
dropdowns: cn(
"flex h-[--cell-size] w-full items-center justify-center gap-1.5 text-sm font-medium",
defaultClassNames.dropdowns
),
dropdown_root: cn(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn(
"bg-popover absolute inset-0 opacity-0",
defaultClassNames.dropdown
),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
defaultClassNames.weekday
),
week: cn("mt-2 flex w-full", defaultClassNames.week),
week_number_header: cn(
"w-[--cell-size] select-none",
defaultClassNames.week_number_header
),
week_number: cn(
"text-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"group/day relative aspect-square h-full w-full select-none p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
defaultClassNames.today
),
outside: cn(
"text-muted-foreground aria-selected:text-muted-foreground",
defaultClassNames.outside
),
disabled: cn(
"text-muted-foreground opacity-50",
defaultClassNames.disabled
),
hidden: cn("invisible", defaultClassNames.hidden),
...classNames,
}}
components={{
Root: ({ className, rootRef, ...props }) => {
return (
<div
data-slot="calendar"
ref={rootRef}
className={cn(className)}
{...props}
/>
)
},
Chevron: ({ className, orientation, ...props }) => {
if (orientation === "left") {
return (
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
)
}
if (orientation === "right") {
return (
<ChevronRightIcon
className={cn("size-4", className)}
{...props}
/>
)
}
return (
<ChevronDownIcon className={cn("size-4", className)} {...props} />
)
},
DayButton: CalendarDayButton,
WeekNumber: ({ children, ...props }) => {
return (
<td {...props}>
<div className="flex size-[--cell-size] items-center justify-center text-center">
{children}
</div>
</td>
)
},
...components,
}}
{...props}
/>
)
}
function CalendarDayButton({
className,
day,
modifiers,
...props
}: React.ComponentProps<typeof DayButton>) {
const defaultClassNames = getDefaultClassNames()
const ref = React.useRef<HTMLButtonElement>(null)
React.useEffect(() => {
if (modifiers.focused) ref.current?.focus()
}, [modifiers.focused])
return (
<Button
ref={ref}
variant="ghost"
size="icon"
data-day={day.date.toLocaleDateString()}
data-selected-single={
modifiers.selected &&
!modifiers.range_start &&
!modifiers.range_end &&
!modifiers.range_middle
}
data-range-start={modifiers.range_start}
data-range-end={modifiers.range_end}
data-range-middle={modifiers.range_middle}
className={cn(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-auto w-full min-w-[--cell-size] flex-col gap-1 font-normal leading-none data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }