mirror of
https://github.com/fosrl/pangolin.git
synced 2026-03-11 05:06:39 +00:00
✨ world map
This commit is contained in:
@@ -436,6 +436,13 @@
|
|||||||
"inviteEmailSent": "Send invite email to user",
|
"inviteEmailSent": "Send invite email to user",
|
||||||
"inviteValid": "Valid For",
|
"inviteValid": "Valid For",
|
||||||
"selectDuration": "Select duration",
|
"selectDuration": "Select duration",
|
||||||
|
"selectResource": "Select Resource",
|
||||||
|
"filterByResource": "Filter By Resource",
|
||||||
|
"resetFilters": "Reset Filters",
|
||||||
|
"totalBlocked": "Requests Blocked By Pangolin",
|
||||||
|
"totalRequests": "Total Requests",
|
||||||
|
"requestsByCountry": "Requests By Country",
|
||||||
|
"topCountries": "Top Countries",
|
||||||
"accessRoleSelect": "Select role",
|
"accessRoleSelect": "Select role",
|
||||||
"inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.",
|
"inviteEmailSentDescription": "An email has been sent to the user with the access link below. They must access the link to accept the invitation.",
|
||||||
"inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.",
|
"inviteSentDescription": "The user has been invited. They must access the link below to accept the invitation.",
|
||||||
@@ -1167,7 +1174,7 @@
|
|||||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||||
"sidebarBluePrints": "Blueprints",
|
"sidebarBluePrints": "Blueprints",
|
||||||
"sidebarOrganization": "Organization",
|
"sidebarOrganization": "Organization",
|
||||||
"sidebarLogsAnalytics": "Log Analytics",
|
"sidebarLogsAnalytics": "Request Analytics",
|
||||||
"blueprints": "Blueprints",
|
"blueprints": "Blueprints",
|
||||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||||
"blueprintAdd": "Add Blueprint",
|
"blueprintAdd": "Add Blueprint",
|
||||||
@@ -2002,6 +2009,7 @@
|
|||||||
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
||||||
"sidebarLogs": "Logs",
|
"sidebarLogs": "Logs",
|
||||||
"request": "Request",
|
"request": "Request",
|
||||||
|
"requests": "Requests",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
||||||
"searchLogs": "Search logs...",
|
"searchLogs": "Search logs...",
|
||||||
|
|||||||
783
package-lock.json
generated
783
package-lock.json
generated
@@ -56,6 +56,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",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"drizzle-orm": "0.44.7",
|
"drizzle-orm": "0.44.7",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
@@ -100,9 +101,11 @@
|
|||||||
"stripe": "18.2.1",
|
"stripe": "18.2.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
|
"visionscarto-world-atlas": "^1.0.0",
|
||||||
"winston": "3.18.3",
|
"winston": "3.18.3",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
@@ -121,6 +124,7 @@
|
|||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.5",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
@@ -134,6 +138,7 @@
|
|||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.34",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
@@ -9046,6 +9051,290 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3": {
|
||||||
|
"version": "7.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz",
|
||||||
|
"integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "*",
|
||||||
|
"@types/d3-axis": "*",
|
||||||
|
"@types/d3-brush": "*",
|
||||||
|
"@types/d3-chord": "*",
|
||||||
|
"@types/d3-color": "*",
|
||||||
|
"@types/d3-contour": "*",
|
||||||
|
"@types/d3-delaunay": "*",
|
||||||
|
"@types/d3-dispatch": "*",
|
||||||
|
"@types/d3-drag": "*",
|
||||||
|
"@types/d3-dsv": "*",
|
||||||
|
"@types/d3-ease": "*",
|
||||||
|
"@types/d3-fetch": "*",
|
||||||
|
"@types/d3-force": "*",
|
||||||
|
"@types/d3-format": "*",
|
||||||
|
"@types/d3-geo": "*",
|
||||||
|
"@types/d3-hierarchy": "*",
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-path": "*",
|
||||||
|
"@types/d3-polygon": "*",
|
||||||
|
"@types/d3-quadtree": "*",
|
||||||
|
"@types/d3-random": "*",
|
||||||
|
"@types/d3-scale": "*",
|
||||||
|
"@types/d3-scale-chromatic": "*",
|
||||||
|
"@types/d3-selection": "*",
|
||||||
|
"@types/d3-shape": "*",
|
||||||
|
"@types/d3-time": "*",
|
||||||
|
"@types/d3-time-format": "*",
|
||||||
|
"@types/d3-timer": "*",
|
||||||
|
"@types/d3-transition": "*",
|
||||||
|
"@types/d3-zoom": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-axis": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-brush": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-chord": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-contour": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "*",
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-delaunay": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-dispatch": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-drag": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-dsv": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-fetch": {
|
||||||
|
"version": "3.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||||
|
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-dsv": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-force": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-format": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-geo": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-hierarchy": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-polygon": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-quadtree": {
|
||||||
|
"version": "3.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||||
|
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-random": {
|
||||||
|
"version": "3.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||||
|
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale-chromatic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-selection": {
|
||||||
|
"version": "3.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz",
|
||||||
|
"integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time-format": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-transition": {
|
||||||
|
"version": "3.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz",
|
||||||
|
"integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-zoom": {
|
||||||
|
"version": "3.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz",
|
||||||
|
"integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-interpolate": "*",
|
||||||
|
"@types/d3-selection": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
@@ -9109,6 +9398,13 @@
|
|||||||
"@types/express": "*"
|
"@types/express": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/http-errors": {
|
"node_modules/@types/http-errors": {
|
||||||
"version": "2.0.5",
|
"version": "2.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||||
@@ -9306,6 +9602,27 @@
|
|||||||
"@types/serve-static": "*"
|
"@types/serve-static": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/topojson-client": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*",
|
||||||
|
"@types/topojson-specification": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/topojson-specification": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/triple-beam": {
|
"node_modules/@types/triple-beam": {
|
||||||
"version": "1.3.5",
|
"version": "1.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
|
||||||
@@ -11378,6 +11695,416 @@
|
|||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/d3": {
|
||||||
|
"version": "7.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz",
|
||||||
|
"integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "3",
|
||||||
|
"d3-axis": "3",
|
||||||
|
"d3-brush": "3",
|
||||||
|
"d3-chord": "3",
|
||||||
|
"d3-color": "3",
|
||||||
|
"d3-contour": "4",
|
||||||
|
"d3-delaunay": "6",
|
||||||
|
"d3-dispatch": "3",
|
||||||
|
"d3-drag": "3",
|
||||||
|
"d3-dsv": "3",
|
||||||
|
"d3-ease": "3",
|
||||||
|
"d3-fetch": "3",
|
||||||
|
"d3-force": "3",
|
||||||
|
"d3-format": "3",
|
||||||
|
"d3-geo": "3",
|
||||||
|
"d3-hierarchy": "3",
|
||||||
|
"d3-interpolate": "3",
|
||||||
|
"d3-path": "3",
|
||||||
|
"d3-polygon": "3",
|
||||||
|
"d3-quadtree": "3",
|
||||||
|
"d3-random": "3",
|
||||||
|
"d3-scale": "4",
|
||||||
|
"d3-scale-chromatic": "3",
|
||||||
|
"d3-selection": "3",
|
||||||
|
"d3-shape": "3",
|
||||||
|
"d3-time": "3",
|
||||||
|
"d3-time-format": "4",
|
||||||
|
"d3-timer": "3",
|
||||||
|
"d3-transition": "3",
|
||||||
|
"d3-zoom": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-axis": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-brush": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "3",
|
||||||
|
"d3-transition": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-chord": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-contour": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "^3.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-delaunay": {
|
||||||
|
"version": "6.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz",
|
||||||
|
"integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"delaunator": "5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dispatch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-drag": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-selection": "3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dsv": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "7",
|
||||||
|
"iconv-lite": "0.6",
|
||||||
|
"rw": "1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"csv2json": "bin/dsv2json.js",
|
||||||
|
"csv2tsv": "bin/dsv2dsv.js",
|
||||||
|
"dsv2dsv": "bin/dsv2dsv.js",
|
||||||
|
"dsv2json": "bin/dsv2json.js",
|
||||||
|
"json2csv": "bin/json2dsv.js",
|
||||||
|
"json2dsv": "bin/json2dsv.js",
|
||||||
|
"json2tsv": "bin/json2dsv.js",
|
||||||
|
"tsv2csv": "bin/dsv2dsv.js",
|
||||||
|
"tsv2json": "bin/dsv2json.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-dsv/node_modules/commander": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-fetch": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dsv": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-force": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-quadtree": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-geo": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.5.0 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-hierarchy": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-polygon": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-quadtree": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-random": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale-chromatic": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-selection": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-transition": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3",
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-ease": "1 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-timer": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"d3-selection": "2 - 3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-zoom": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-dispatch": "1 - 3",
|
||||||
|
"d3-drag": "2 - 3",
|
||||||
|
"d3-interpolate": "1 - 3",
|
||||||
|
"d3-selection": "2 - 3",
|
||||||
|
"d3-transition": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@@ -11592,6 +12319,15 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delaunator": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"robust-predicates": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -14559,6 +15295,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/intl-messageformat": {
|
"node_modules/intl-messageformat": {
|
||||||
"version": "10.7.18",
|
"version": "10.7.18",
|
||||||
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
|
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
|
||||||
@@ -21386,6 +22131,12 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/robust-predicates": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==",
|
||||||
|
"license": "Unlicense"
|
||||||
|
},
|
||||||
"node_modules/router": {
|
"node_modules/router": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||||
@@ -21425,6 +22176,12 @@
|
|||||||
"queue-microtask": "^1.2.2"
|
"queue-microtask": "^1.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rw": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/safe-array-concat": {
|
"node_modules/safe-array-concat": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
|
||||||
@@ -22956,6 +23713,26 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/topojson-client": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"topo2geo": "bin/topo2geo",
|
||||||
|
"topomerge": "bin/topomerge",
|
||||||
|
"topoquantize": "bin/topoquantize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/topojson-client/node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/triple-beam": {
|
"node_modules/triple-beam": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz",
|
||||||
@@ -24045,6 +24822,12 @@
|
|||||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/visionscarto-world-atlas": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/visionscarto-world-atlas/-/visionscarto-world-atlas-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-jHl/NQgASfw5ZML3cnbjdfr/gXK5zO8a2xKSoCVe+5+EsIaO9tMTh7SsnfhESnCpZ+Xb6XBeU91wiuyERUPshQ==",
|
||||||
|
"license": "BSD-3-Clause"
|
||||||
|
},
|
||||||
"node_modules/watchpack": {
|
"node_modules/watchpack": {
|
||||||
"version": "2.4.4",
|
"version": "2.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
|
||||||
|
|||||||
@@ -79,6 +79,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",
|
||||||
|
"d3": "^7.9.0",
|
||||||
"date-fns": "4.1.0",
|
"date-fns": "4.1.0",
|
||||||
"drizzle-orm": "0.44.7",
|
"drizzle-orm": "0.44.7",
|
||||||
"eslint": "9.39.1",
|
"eslint": "9.39.1",
|
||||||
@@ -123,9 +124,11 @@
|
|||||||
"stripe": "18.2.1",
|
"stripe": "18.2.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.3.1",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.3.8",
|
||||||
"uuid": "^13.0.0",
|
"uuid": "^13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
|
"visionscarto-world-atlas": "^1.0.0",
|
||||||
"winston": "3.18.3",
|
"winston": "3.18.3",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.3",
|
||||||
@@ -144,6 +147,7 @@
|
|||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
|
"@types/d3": "^7.4.3",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.5",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "^1.18.2",
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
@@ -157,6 +161,7 @@
|
|||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.2",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "^7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.34",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ async function query(query: Q) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [totalRequests] = await db
|
const [all] = await db
|
||||||
.select({ total: count() })
|
.select({ total: count() })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(baseConditions);
|
.where(baseConditions);
|
||||||
|
|
||||||
const [totalBlocked] = await db
|
const [blocked] = await db
|
||||||
.select({ blocked: count() })
|
.select({ total: count() })
|
||||||
.from(requestAuditLog)
|
.from(requestAuditLog)
|
||||||
.where(and(baseConditions, eq(requestAuditLog.action, false)));
|
.where(and(baseConditions, eq(requestAuditLog.action, false)));
|
||||||
|
|
||||||
@@ -92,7 +92,11 @@ async function query(query: Q) {
|
|||||||
.where(baseConditions)
|
.where(baseConditions)
|
||||||
.groupBy(requestAuditLog.location);
|
.groupBy(requestAuditLog.location);
|
||||||
|
|
||||||
return { requestsPerCountry, totalBlocked, totalRequests };
|
return {
|
||||||
|
requestsPerCountry,
|
||||||
|
totalBlocked: blocked.total,
|
||||||
|
totalRequests: all.total
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
|
import { LogAnalyticsData } from "@app/components/LogAnalyticsData";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
import { Card, CardContent, CardHeader } from "@app/components/ui/card";
|
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
|
||||||
export interface AnalyticsPageProps {}
|
export interface AnalyticsPageProps {
|
||||||
|
params: Promise<{ orgId: string }>;
|
||||||
|
searchParams: Promise<Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
export default async function AnalyticsPage(props: AnalyticsPageProps) {
|
export default async function AnalyticsPage(props: AnalyticsPageProps) {
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
|
const orgId = (await props.params).orgId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SettingsSectionTitle
|
<SettingsSectionTitle
|
||||||
@@ -14,10 +21,7 @@ export default async function AnalyticsPage(props: AnalyticsPageProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="container mx-auto max-w-12xl">
|
<div className="container mx-auto max-w-12xl">
|
||||||
<Card className="">
|
<LogAnalyticsData orgId={orgId} />
|
||||||
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-center sm:justify-between sm:space-y-0 pb-4"></CardHeader>
|
|
||||||
<CardContent></CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import { useTranslations } from "next-intl";
|
|||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
FormDescription,
|
|
||||||
FormField,
|
FormField,
|
||||||
FormItem,
|
FormItem,
|
||||||
FormLabel,
|
FormLabel,
|
||||||
|
|||||||
@@ -7,209 +7,220 @@ 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 {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger
|
||||||
} from "@app/components/ui/popover";
|
} from "@app/components/ui/popover";
|
||||||
import { cn } from "@app/lib/cn";
|
import { cn } from "@app/lib/cn";
|
||||||
import { ChangeEvent, useEffect, useState } from "react";
|
import { ChangeEvent, useEffect, useState } from "react";
|
||||||
|
|
||||||
export interface DateTimeValue {
|
export interface DateTimeValue {
|
||||||
date?: Date;
|
date?: Date;
|
||||||
time?: string;
|
time?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateTimePickerProps {
|
export interface DateTimePickerProps {
|
||||||
label?: string;
|
label?: string;
|
||||||
value?: DateTimeValue;
|
value?: DateTimeValue;
|
||||||
onChange?: (value: DateTimeValue) => void;
|
onChange?: (value: DateTimeValue) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
showTime?: boolean;
|
showTime?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DateTimePicker({
|
export function DateTimePicker({
|
||||||
label,
|
label,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
placeholder = "Select date & time",
|
placeholder = "Select date & time",
|
||||||
className,
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
showTime = true,
|
showTime = true
|
||||||
}: DateTimePickerProps) {
|
}: DateTimePickerProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [internalDate, setInternalDate] = useState<Date | undefined>(value?.date);
|
const [internalDate, setInternalDate] = useState<Date | undefined>(
|
||||||
const [internalTime, setInternalTime] = useState<string>(value?.time || "");
|
value?.date
|
||||||
|
);
|
||||||
|
const [internalTime, setInternalTime] = useState<string>(value?.time || "");
|
||||||
|
|
||||||
// Sync internal state with external value prop
|
// Sync internal state with external value prop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setInternalDate(value?.date);
|
setInternalDate(value?.date);
|
||||||
setInternalTime(value?.time || "");
|
setInternalTime(value?.time || "");
|
||||||
}, [value?.date, value?.time]);
|
}, [value?.date, value?.time]);
|
||||||
|
|
||||||
const handleDateChange = (date: Date | undefined) => {
|
const handleDateChange = (date: Date | undefined) => {
|
||||||
setInternalDate(date);
|
setInternalDate(date);
|
||||||
const newValue = { date, time: internalTime };
|
const newValue = { date, time: internalTime };
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
|
const handleTimeChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||||
const time = event.target.value;
|
const time = event.target.value;
|
||||||
setInternalTime(time);
|
setInternalTime(time);
|
||||||
const newValue = { date: internalDate, time };
|
const newValue = { date: internalDate, time };
|
||||||
onChange?.(newValue);
|
onChange?.(newValue);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDisplayText = () => {
|
const getDisplayText = () => {
|
||||||
if (!internalDate) return placeholder;
|
if (!internalDate) return placeholder;
|
||||||
|
|
||||||
const dateStr = internalDate.toLocaleDateString();
|
|
||||||
if (!showTime || !internalTime) return dateStr;
|
|
||||||
|
|
||||||
// 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 dateStr = internalDate.toLocaleDateString();
|
||||||
|
if (!showTime || !internalTime) return dateStr;
|
||||||
|
|
||||||
return (
|
// Parse time and format in local timezone
|
||||||
<div className={cn("flex gap-4", className)}>
|
const [hours, minutes, seconds] = internalTime.split(":");
|
||||||
<div className="flex flex-col gap-2">
|
const timeDate = new Date();
|
||||||
{label && (
|
timeDate.setHours(
|
||||||
<Label htmlFor="date-picker">
|
parseInt(hours, 10),
|
||||||
{label}
|
parseInt(minutes, 10),
|
||||||
</Label>
|
parseInt(seconds || "0", 10)
|
||||||
)}
|
);
|
||||||
<div className="flex gap-2">
|
const timeStr = timeDate.toLocaleTimeString([], {
|
||||||
<Popover open={open} onOpenChange={setOpen}>
|
hour: "2-digit",
|
||||||
<PopoverTrigger asChild>
|
minute: "2-digit"
|
||||||
<Button
|
});
|
||||||
variant="outline"
|
|
||||||
id="date-picker"
|
return `${dateStr} ${timeStr}`;
|
||||||
disabled={disabled}
|
};
|
||||||
className={cn(
|
|
||||||
"justify-between font-normal",
|
const hasValue = internalDate || (showTime && internalTime);
|
||||||
showTime ? "w-48" : "w-32",
|
|
||||||
!hasValue && "text-muted-foreground"
|
return (
|
||||||
)}
|
<div className={cn("flex gap-4", className)}>
|
||||||
>
|
<div className="flex flex-col gap-2">
|
||||||
{getDisplayText()}
|
{label && <Label htmlFor="date-picker">{label}</Label>}
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
<div className="flex gap-2">
|
||||||
</Button>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
</PopoverTrigger>
|
<PopoverTrigger asChild>
|
||||||
<PopoverContent className="w-auto overflow-hidden p-0" align="start">
|
<Button
|
||||||
{showTime ? (
|
variant="outline"
|
||||||
<div className="flex">
|
id="date-picker"
|
||||||
<Calendar
|
disabled={disabled}
|
||||||
mode="single"
|
className={cn(
|
||||||
selected={internalDate}
|
"justify-between font-normal",
|
||||||
captionLayout="dropdown"
|
showTime ? "w-48" : "w-32",
|
||||||
onSelect={(date) => {
|
!hasValue && "text-muted-foreground"
|
||||||
handleDateChange(date);
|
)}
|
||||||
if (!showTime) {
|
>
|
||||||
setOpen(false);
|
{getDisplayText()}
|
||||||
}
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
}}
|
</Button>
|
||||||
className="flex-grow w-[250px]"
|
</PopoverTrigger>
|
||||||
/>
|
<PopoverContent
|
||||||
<div className="p-3 border-l">
|
className="w-auto overflow-hidden p-0"
|
||||||
<div className="flex flex-col gap-3">
|
align="start"
|
||||||
<Label htmlFor="time-input" className="text-sm font-medium">
|
>
|
||||||
Time
|
{showTime ? (
|
||||||
</Label>
|
<div className="flex">
|
||||||
<Input
|
<Calendar
|
||||||
id="time-input"
|
mode="single"
|
||||||
type="time"
|
selected={internalDate}
|
||||||
step="1"
|
captionLayout="dropdown"
|
||||||
value={internalTime}
|
onSelect={(date) => {
|
||||||
onChange={handleTimeChange}
|
handleDateChange(date);
|
||||||
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
if (!showTime) {
|
||||||
/>
|
setOpen(false);
|
||||||
</div>
|
}
|
||||||
</div>
|
}}
|
||||||
|
className="grow w-[250px]"
|
||||||
|
/>
|
||||||
|
<div className="p-3 border-l">
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<Label
|
||||||
|
htmlFor="time-input"
|
||||||
|
className="text-sm font-medium"
|
||||||
|
>
|
||||||
|
Time
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
id="time-input"
|
||||||
|
type="time"
|
||||||
|
step="1"
|
||||||
|
value={internalTime}
|
||||||
|
onChange={handleTimeChange}
|
||||||
|
className="bg-background appearance-none [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-calendar-picker-indicator]:appearance-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={internalDate}
|
||||||
|
captionLayout="dropdown"
|
||||||
|
onSelect={(date) => {
|
||||||
|
handleDateChange(date);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<Calendar
|
|
||||||
mode="single"
|
|
||||||
selected={internalDate}
|
|
||||||
captionLayout="dropdown"
|
|
||||||
onSelect={(date) => {
|
|
||||||
handleDateChange(date);
|
|
||||||
setOpen(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DateRangePickerProps {
|
export interface DateRangePickerProps {
|
||||||
startLabel?: string;
|
startLabel?: string;
|
||||||
endLabel?: string;
|
endLabel?: string;
|
||||||
startValue?: DateTimeValue;
|
startValue?: DateTimeValue;
|
||||||
endValue?: DateTimeValue;
|
endValue?: DateTimeValue;
|
||||||
onStartChange?: (value: DateTimeValue) => void;
|
onStartChange?: (value: DateTimeValue) => void;
|
||||||
onEndChange?: (value: DateTimeValue) => void;
|
onEndChange?: (value: DateTimeValue) => void;
|
||||||
onRangeChange?: (start: DateTimeValue, end: DateTimeValue) => void;
|
onRangeChange?: (start: DateTimeValue, end: DateTimeValue) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
showTime?: boolean;
|
showTime?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DateRangePicker({
|
export function DateRangePicker({
|
||||||
// startLabel = "From",
|
// startLabel = "From",
|
||||||
// endLabel = "To",
|
// endLabel = "To",
|
||||||
startValue,
|
startValue,
|
||||||
endValue,
|
endValue,
|
||||||
onStartChange,
|
onStartChange,
|
||||||
onEndChange,
|
onEndChange,
|
||||||
onRangeChange,
|
onRangeChange,
|
||||||
className,
|
className,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
showTime = true,
|
showTime = true
|
||||||
}: DateRangePickerProps) {
|
}: DateRangePickerProps) {
|
||||||
const handleStartChange = (value: DateTimeValue) => {
|
const handleStartChange = (value: DateTimeValue) => {
|
||||||
onStartChange?.(value);
|
onStartChange?.(value);
|
||||||
if (onRangeChange && endValue) {
|
if (onRangeChange && endValue) {
|
||||||
onRangeChange(value, endValue);
|
onRangeChange(value, endValue);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEndChange = (value: DateTimeValue) => {
|
const handleEndChange = (value: DateTimeValue) => {
|
||||||
onEndChange?.(value);
|
onEndChange?.(value);
|
||||||
if (onRangeChange && startValue) {
|
if (onRangeChange && startValue) {
|
||||||
onRangeChange(startValue, value);
|
onRangeChange(startValue, value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex gap-4 items-center", className)}>
|
<div className={cn("flex gap-4 items-center", className)}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="Start"
|
label="Start"
|
||||||
value={startValue}
|
value={startValue}
|
||||||
onChange={handleStartChange}
|
onChange={handleStartChange}
|
||||||
placeholder="Start date & time"
|
placeholder="Start date & time"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
/>
|
/>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
label="End"
|
label="End"
|
||||||
value={endValue}
|
value={endValue}
|
||||||
onChange={handleEndChange}
|
onChange={handleEndChange}
|
||||||
placeholder="End date & time"
|
placeholder="End date & time"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
showTime={showTime}
|
showTime={showTime}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export function InfoSections({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`grid md:grid-cols-[var(--columns)] md:gap-4 gap-2 md:items-start grid-cols-1`}
|
className={`grid md:grid-cols-(--columns) md:gap-4 gap-2 md:items-start grid-cols-1`}
|
||||||
style={{
|
style={{
|
||||||
// @ts-expect-error dynamic props don't work with tailwind, but we can set the
|
// @ts-expect-error dynamic props don't work with tailwind, but we can set the
|
||||||
// value of a CSS variable at runtime and tailwind will just reuse that value
|
// value of a CSS variable at runtime and tailwind will just reuse that value
|
||||||
|
|||||||
288
src/components/LogAnalyticsData.tsx
Normal file
288
src/components/LogAnalyticsData.tsx
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
import { createApiClient } from "@app/lib/api";
|
||||||
|
import {
|
||||||
|
logAnalyticsFiltersSchema,
|
||||||
|
logQueries,
|
||||||
|
resourceQueries
|
||||||
|
} from "@app/lib/queries";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Card, CardContent, CardHeader } from "./ui/card";
|
||||||
|
import { RefreshCw, XIcon } from "lucide-react";
|
||||||
|
import { DateRangePicker, type DateTimeValue } from "./DateTimePicker";
|
||||||
|
import { Button } from "./ui/button";
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue
|
||||||
|
} from "./ui/select";
|
||||||
|
import { Label } from "./ui/label";
|
||||||
|
import { Separator } from "./ui/separator";
|
||||||
|
import {
|
||||||
|
InfoSection,
|
||||||
|
InfoSectionContent,
|
||||||
|
InfoSections,
|
||||||
|
InfoSectionTitle
|
||||||
|
} from "./InfoSection";
|
||||||
|
import { WorldMap } from "./WorldMap";
|
||||||
|
|
||||||
|
export type AnalyticsContentProps = {
|
||||||
|
orgId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function LogAnalyticsData(props: AnalyticsContentProps) {
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
const path = usePathname();
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
const filters = logAnalyticsFiltersSchema.parse(
|
||||||
|
Object.fromEntries(searchParams.entries())
|
||||||
|
);
|
||||||
|
|
||||||
|
const isEmptySearchParams =
|
||||||
|
!filters.resourceId && !filters.timeStart && !filters.timeEnd;
|
||||||
|
|
||||||
|
const env = useEnvContext();
|
||||||
|
const [api] = useState(() => createApiClient(env));
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const dateRange = {
|
||||||
|
startDate: filters.timeStart ? new Date(filters.timeStart) : undefined,
|
||||||
|
endDate: filters.timeEnd ? new Date(filters.timeEnd) : undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
const { data: resources = [], isFetching: isFetchingResources } = useQuery(
|
||||||
|
resourceQueries.listNamesPerOrg(props.orgId, api)
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: stats,
|
||||||
|
isFetching: isFetchingAnalytics,
|
||||||
|
refetch: refreshAnalytics
|
||||||
|
} = useQuery(
|
||||||
|
logQueries.requestAnalytics({
|
||||||
|
orgId: props.orgId,
|
||||||
|
api,
|
||||||
|
filters
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const percentBlocked = stats
|
||||||
|
? new Intl.NumberFormat(navigator.language, {
|
||||||
|
maximumFractionDigits: 5
|
||||||
|
}).format(stats.totalBlocked / stats.totalRequests)
|
||||||
|
: null;
|
||||||
|
const totalRequests = stats
|
||||||
|
? new Intl.NumberFormat(navigator.language, {
|
||||||
|
maximumFractionDigits: 0
|
||||||
|
}).format(stats.totalRequests)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
function handleTimeRangeUpdate(start: DateTimeValue, end: DateTimeValue) {
|
||||||
|
const newSearch = new URLSearchParams(searchParams);
|
||||||
|
const timeRegex =
|
||||||
|
/^(?<hours>\d{1,2})\:(?<minutes>\d{1,2})(\:(?<seconds>\d{1,2}))?$/;
|
||||||
|
|
||||||
|
if (start.date) {
|
||||||
|
const startDate = new Date(start.date);
|
||||||
|
if (start.time) {
|
||||||
|
const time = timeRegex.exec(start.time);
|
||||||
|
const groups = time?.groups ?? {};
|
||||||
|
startDate.setHours(Number(groups.hours));
|
||||||
|
startDate.setMinutes(Number(groups.minutes));
|
||||||
|
if (groups.seconds) {
|
||||||
|
startDate.setSeconds(Number(groups.seconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newSearch.set("timeStart", startDate.toISOString());
|
||||||
|
}
|
||||||
|
if (end.date) {
|
||||||
|
const endDate = new Date(end.date);
|
||||||
|
|
||||||
|
if (end.time) {
|
||||||
|
const time = timeRegex.exec(end.time);
|
||||||
|
const groups = time?.groups ?? {};
|
||||||
|
endDate.setHours(Number(groups.hours));
|
||||||
|
endDate.setMinutes(Number(groups.minutes));
|
||||||
|
if (groups.seconds) {
|
||||||
|
endDate.setSeconds(Number(groups.seconds));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log({
|
||||||
|
endDate
|
||||||
|
});
|
||||||
|
newSearch.set("timeEnd", endDate.toISOString());
|
||||||
|
}
|
||||||
|
router.replace(`${path}?${newSearch.toString()}`);
|
||||||
|
}
|
||||||
|
function getDateTime(date: Date) {
|
||||||
|
return `${date.getHours()}:${date.getMinutes()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-5">
|
||||||
|
<Card className="">
|
||||||
|
<CardHeader className="flex flex-col space-y-4 sm:flex-row sm:items-start lg:items-end sm:justify-between sm:space-y-0 pb-4">
|
||||||
|
<div className="flex flex-col lg:flex-row items-start lg:items-end w-full sm:mr-2 gap-2">
|
||||||
|
<DateRangePicker
|
||||||
|
startValue={{
|
||||||
|
date: dateRange.startDate,
|
||||||
|
time: dateRange.startDate
|
||||||
|
? getDateTime(dateRange.startDate)
|
||||||
|
: undefined
|
||||||
|
}}
|
||||||
|
endValue={{
|
||||||
|
date: dateRange.endDate,
|
||||||
|
time: dateRange.endDate
|
||||||
|
? getDateTime(dateRange.endDate)
|
||||||
|
: undefined
|
||||||
|
}}
|
||||||
|
onRangeChange={handleTimeRangeUpdate}
|
||||||
|
className="flex-wrap gap-2"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Separator className="w-px h-6 self-end relative bottom-1.5 hidden lg:block" />
|
||||||
|
|
||||||
|
<div className="flex items-end gap-2">
|
||||||
|
<div className="flex flex-col items-start gap-2 w-48">
|
||||||
|
<Label htmlFor="resourceId">
|
||||||
|
{t("filterByResource")}
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
onValueChange={(newValue) => {
|
||||||
|
const newSearch = new URLSearchParams(
|
||||||
|
searchParams
|
||||||
|
);
|
||||||
|
newSearch.set("resourceId", newValue);
|
||||||
|
|
||||||
|
router.replace(
|
||||||
|
`${path}?${newSearch.toString()}`
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={filters.resourceId?.toString()}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="resourceId"
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<SelectValue
|
||||||
|
placeholder={t("selectResource")}
|
||||||
|
/>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="w-full">
|
||||||
|
{resources.map((resource) => (
|
||||||
|
<SelectItem
|
||||||
|
key={resource.resourceId}
|
||||||
|
value={resource.resourceId.toString()}
|
||||||
|
>
|
||||||
|
{resource.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!isEmptySearchParams && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
onClick={() => {
|
||||||
|
router.replace(path);
|
||||||
|
}}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<XIcon className="size-4" />
|
||||||
|
{t("resetFilters")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-start gap-2 sm:justify-end">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => refreshAnalytics()}
|
||||||
|
disabled={isFetchingAnalytics}
|
||||||
|
className=" relative top-6 lg:static gap-2"
|
||||||
|
>
|
||||||
|
<RefreshCw
|
||||||
|
className={cn(
|
||||||
|
"size-4",
|
||||||
|
isFetchingAnalytics && "animate-spin"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{t("refresh")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-col gap-4">
|
||||||
|
<InfoSections cols={2}>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle className="text-muted-foreground">
|
||||||
|
{t("totalRequests")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
{totalRequests ?? "--"}
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
<InfoSection>
|
||||||
|
<InfoSectionTitle className="text-muted-foreground">
|
||||||
|
{t("totalBlocked")}
|
||||||
|
</InfoSectionTitle>
|
||||||
|
<InfoSectionContent>
|
||||||
|
<span>{stats?.totalBlocked ?? "--"}</span>
|
||||||
|
(
|
||||||
|
<span>{percentBlocked ?? "--"}</span>
|
||||||
|
<span className="text-muted-foreground">%</span>
|
||||||
|
)
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
</InfoSections>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="flex flex-col lg:flex-row items-stretch gap-5">
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-col gap-4">
|
||||||
|
<h3 className="font-medium">
|
||||||
|
{t("requestsByCountry")}
|
||||||
|
</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
<WorldMap
|
||||||
|
data={
|
||||||
|
stats?.requestsPerCountry.map((item) => ({
|
||||||
|
count: item.total,
|
||||||
|
code: item.country_code ?? "US"
|
||||||
|
})) ?? []
|
||||||
|
}
|
||||||
|
label={{
|
||||||
|
singular: "request",
|
||||||
|
plural: "requests"
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader className="flex flex-col gap-4">
|
||||||
|
<h3 className="font-medium">{t("topCountries")}</h3>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex flex-col gap-4">
|
||||||
|
{/* ... */}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
281
src/components/WorldMap.tsx
Normal file
281
src/components/WorldMap.tsx
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/**
|
||||||
|
* Inspired from plausible: https://github.com/plausible/analytics/blob/1df08a25b4a536c9cc1e03855ddcfeac1d1cf6e5/assets/js/dashboard/stats/locations/map.tsx
|
||||||
|
*/
|
||||||
|
import { cn } from "@app/lib/cn";
|
||||||
|
import worldJson from "visionscarto-world-atlas/world/110m.json";
|
||||||
|
import * as topojson from "topojson-client";
|
||||||
|
import * as d3 from "d3";
|
||||||
|
import { useRef, type ComponentRef, useState, useEffect, useMemo } from "react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { COUNTRY_CODE_LIST } from "@app/lib/countryCodeList";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
type CountryData = {
|
||||||
|
alpha_3: string;
|
||||||
|
name: string;
|
||||||
|
count: number;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WorldMapProps = {
|
||||||
|
data: Pick<CountryData, "code" | "count">[];
|
||||||
|
label: {
|
||||||
|
singular: string;
|
||||||
|
plural: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function WorldMap({ data, label }: WorldMapProps) {
|
||||||
|
const svgRef = useRef<ComponentRef<"svg">>(null);
|
||||||
|
const [tooltip, setTooltip] = useState<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
hoveredCountryAlpha3Code: string | null;
|
||||||
|
}>({ x: 0, y: 0, hoveredCountryAlpha3Code: null });
|
||||||
|
const { theme, systemTheme } = useTheme();
|
||||||
|
|
||||||
|
const t = useTranslations();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!svgRef.current) return;
|
||||||
|
const svg = drawInteractiveCountries(svgRef.current, setTooltip);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
svg.selectAll("*").remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const displayNames = new Intl.DisplayNames(navigator.language, {
|
||||||
|
type: "region",
|
||||||
|
fallback: "code"
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxValue = Math.max(...data.map((item) => item.count));
|
||||||
|
const dataByCountryCode = useMemo(() => {
|
||||||
|
const byCountryCode = new Map<string, CountryData>();
|
||||||
|
for (const country of data) {
|
||||||
|
const countryISOData = COUNTRY_CODE_LIST[country.code];
|
||||||
|
|
||||||
|
if (countryISOData) {
|
||||||
|
byCountryCode.set(countryISOData.alpha3, {
|
||||||
|
...country,
|
||||||
|
name: displayNames.of(country.code)!,
|
||||||
|
alpha_3: countryISOData.alpha3
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return byCountryCode;
|
||||||
|
}, [data]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (svgRef.current) {
|
||||||
|
const palette =
|
||||||
|
colorScales[theme ?? "light"] ??
|
||||||
|
colorScales[systemTheme ?? "light"];
|
||||||
|
|
||||||
|
const getColorForValue = d3
|
||||||
|
.scaleLinear<string>()
|
||||||
|
.domain([0, maxValue])
|
||||||
|
.range(palette);
|
||||||
|
|
||||||
|
colorInCountriesWithValues(
|
||||||
|
svgRef.current,
|
||||||
|
getColorForValue,
|
||||||
|
dataByCountryCode
|
||||||
|
).on("click", (_event, countryPath) => {
|
||||||
|
console.log({
|
||||||
|
_event,
|
||||||
|
countryPath
|
||||||
|
});
|
||||||
|
// onCountryClick(countryPath as unknown as WorldJsonCountryData);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [theme, systemTheme, maxValue, dataByCountryCode]);
|
||||||
|
|
||||||
|
const hoveredCountryData = tooltip.hoveredCountryAlpha3Code
|
||||||
|
? dataByCountryCode.get(tooltip.hoveredCountryAlpha3Code)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-4 w-full relative">
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${width} ${height}`}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!!hoveredCountryData && (
|
||||||
|
<MapTooltip
|
||||||
|
x={tooltip.x}
|
||||||
|
y={tooltip.y}
|
||||||
|
name={hoveredCountryData.name}
|
||||||
|
value={Intl.NumberFormat(navigator.language).format(
|
||||||
|
hoveredCountryData.count
|
||||||
|
)}
|
||||||
|
label={
|
||||||
|
hoveredCountryData.count === 1
|
||||||
|
? t(label.singular)
|
||||||
|
: t(label.plural)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MapTooltipProps {
|
||||||
|
name: string;
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MapTooltip({ name, value, label, x, y }: MapTooltipProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"absolute z-50 p-2 translate-x-2 translate-y-2",
|
||||||
|
"pointer-events-none rounded-sm",
|
||||||
|
"bg-white dark:bg-popover shadow border border-border"
|
||||||
|
)}
|
||||||
|
style={{
|
||||||
|
left: x,
|
||||||
|
top: y
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="font-semibold">{name}</div>
|
||||||
|
<strong className="text-primary">{value}</strong> {label}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const width = 475;
|
||||||
|
const height = 335;
|
||||||
|
const sharedCountryClass = cn("transition-colors");
|
||||||
|
|
||||||
|
const colorScales: Record<string, [string, string]> = {
|
||||||
|
dark: ["#4F4444", "#f36117"],
|
||||||
|
light: ["#FFF5F3", "#f36117"]
|
||||||
|
};
|
||||||
|
|
||||||
|
const countryClass = cn(
|
||||||
|
sharedCountryClass,
|
||||||
|
"stroke-1",
|
||||||
|
"fill-[#fafafa]",
|
||||||
|
"stroke-[#dae1e7]",
|
||||||
|
"dark:fill-[#323236]",
|
||||||
|
"dark:stroke-[#18181b]"
|
||||||
|
);
|
||||||
|
|
||||||
|
const highlightedCountryClass = cn(
|
||||||
|
sharedCountryClass,
|
||||||
|
"stroke-2",
|
||||||
|
"fill-[#f4f4f5]",
|
||||||
|
"stroke-[#f36117]",
|
||||||
|
"dark:fill-[#3f3f46]"
|
||||||
|
);
|
||||||
|
|
||||||
|
function setupProjetionPath() {
|
||||||
|
const projection = d3
|
||||||
|
.geoMercator()
|
||||||
|
.scale(75)
|
||||||
|
.translate([width / 2, height / 1.5]);
|
||||||
|
|
||||||
|
const path = d3.geoPath().projection(projection);
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @returns the d3 selected svg element */
|
||||||
|
function drawInteractiveCountries(
|
||||||
|
element: SVGSVGElement,
|
||||||
|
setTooltip: React.Dispatch<
|
||||||
|
React.SetStateAction<{
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
hoveredCountryAlpha3Code: string | null;
|
||||||
|
}>
|
||||||
|
>
|
||||||
|
) {
|
||||||
|
const path = setupProjetionPath();
|
||||||
|
const data = parseWorldTopoJsonToGeoJsonFeatures();
|
||||||
|
const svg = d3.select(element);
|
||||||
|
|
||||||
|
svg.selectAll("path")
|
||||||
|
.data(data)
|
||||||
|
.enter()
|
||||||
|
.append("path")
|
||||||
|
.attr("class", countryClass)
|
||||||
|
.attr("d", path as never)
|
||||||
|
|
||||||
|
.on("mouseover", function (event, country) {
|
||||||
|
const [x, y] = d3.pointer(event, svg.node()?.parentNode);
|
||||||
|
setTooltip({
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
hoveredCountryAlpha3Code: country.properties.a3
|
||||||
|
});
|
||||||
|
// brings country to front
|
||||||
|
this.parentNode?.appendChild(this);
|
||||||
|
d3.select(this).attr("class", highlightedCountryClass);
|
||||||
|
})
|
||||||
|
|
||||||
|
.on("mousemove", function (event) {
|
||||||
|
const [x, y] = d3.pointer(event, svg.node()?.parentNode);
|
||||||
|
setTooltip((currentState) => ({ ...currentState, x, y }));
|
||||||
|
})
|
||||||
|
|
||||||
|
.on("mouseout", function () {
|
||||||
|
setTooltip({ x: 0, y: 0, hoveredCountryAlpha3Code: null });
|
||||||
|
d3.select(this).attr("class", countryClass);
|
||||||
|
});
|
||||||
|
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorldJsonCountryData = { properties: { name: string; a3: string } };
|
||||||
|
|
||||||
|
function parseWorldTopoJsonToGeoJsonFeatures(): Array<WorldJsonCountryData> {
|
||||||
|
const collection = topojson.feature(
|
||||||
|
// @ts-expect-error strings in worldJson not recongizable as the enum values declared in library
|
||||||
|
worldJson,
|
||||||
|
worldJson.objects.countries
|
||||||
|
);
|
||||||
|
// @ts-expect-error topojson.feature return type incorrectly inferred as not a collection
|
||||||
|
return collection.features;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to color the countries
|
||||||
|
* @returns the svg elements represeting countries
|
||||||
|
*/
|
||||||
|
function colorInCountriesWithValues(
|
||||||
|
element: SVGSVGElement,
|
||||||
|
getColorForValue: d3.ScaleLinear<string, string, never>,
|
||||||
|
dataByCountryCode: Map<string, CountryData>
|
||||||
|
) {
|
||||||
|
function getCountryByCountryPath(countryPath: unknown) {
|
||||||
|
return dataByCountryCode.get(
|
||||||
|
(countryPath as unknown as WorldJsonCountryData).properties.a3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const svg = d3.select(element);
|
||||||
|
|
||||||
|
return svg
|
||||||
|
.selectAll("path")
|
||||||
|
.style("fill", (countryPath) => {
|
||||||
|
const country = getCountryByCountryPath(countryPath);
|
||||||
|
if (!country?.count) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return getColorForValue(country.count);
|
||||||
|
})
|
||||||
|
.style("cursor", (countryPath) => {
|
||||||
|
const country = getCountryByCountryPath(countryPath);
|
||||||
|
if (!country?.count) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return "pointer";
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Env } from "@app/lib/types/env";
|
import type { Env } from "@app/lib/types/env";
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
interface EnvContextType {
|
export interface EnvContextType {
|
||||||
env: Env;
|
env: Env;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1002
src/lib/countryCodeList.ts
Normal file
1002
src/lib/countryCodeList.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,10 @@ import { durationToMs } from "./durationToMs";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { remote } from "./api";
|
import { remote } from "./api";
|
||||||
import type ResponseT from "@server/types/Response";
|
import type ResponseT from "@server/types/Response";
|
||||||
|
import z from "zod";
|
||||||
|
import type { AxiosInstance, AxiosResponse } from "axios";
|
||||||
|
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
||||||
|
import type { ListResourceNamesResponse } from "@server/routers/resource";
|
||||||
|
|
||||||
export type ProductUpdate = {
|
export type ProductUpdate = {
|
||||||
link: string | null;
|
link: string | null;
|
||||||
@@ -65,3 +69,65 @@ export const productUpdatesQueries = {
|
|||||||
// because we don't need to listen for new versions there
|
// because we don't need to listen for new versions there
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const logAnalyticsFiltersSchema = z.object({
|
||||||
|
timeStart: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => !isNaN(Date.parse(val)), {
|
||||||
|
error: "timeStart must be a valid ISO date string"
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
timeEnd: z
|
||||||
|
.string()
|
||||||
|
.refine((val) => !isNaN(Date.parse(val)), {
|
||||||
|
error: "timeEnd must be a valid ISO date string"
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
resourceId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.transform(Number)
|
||||||
|
.pipe(z.int().positive())
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
|
||||||
|
|
||||||
|
export const logQueries = {
|
||||||
|
requestAnalytics: ({
|
||||||
|
orgId,
|
||||||
|
filters,
|
||||||
|
api
|
||||||
|
}: {
|
||||||
|
orgId: string;
|
||||||
|
filters: LogAnalyticsFilters;
|
||||||
|
api: AxiosInstance;
|
||||||
|
}) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const,
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const res = await api.get<
|
||||||
|
AxiosResponse<QueryRequestAnalyticsResponse>
|
||||||
|
>(`/org/${orgId}/logs/analytics`, {
|
||||||
|
params: filters,
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resourceQueries = {
|
||||||
|
listNamesPerOrg: (orgId: string, api: AxiosInstance) =>
|
||||||
|
queryOptions({
|
||||||
|
queryKey: ["RESOURCES_NAMES", orgId] as const,
|
||||||
|
queryFn: async ({ signal }) => {
|
||||||
|
const res = await api.get<
|
||||||
|
AxiosResponse<ListResourceNamesResponse>
|
||||||
|
>(`/org/${orgId}/resource-names`, {
|
||||||
|
signal
|
||||||
|
});
|
||||||
|
return res.data.data;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user