feat: metrics/stats page

This commit is contained in:
diced 2023-09-16 15:35:46 -07:00
parent 2a2ffaaffe
commit 01f94245c8
No known key found for this signature in database
GPG key ID: 370BD1BA142842D1
22 changed files with 1204 additions and 11 deletions

View file

@ -17,11 +17,12 @@
"db:prototype": "prisma db push && prisma generate"
},
"dependencies": {
"@ant-design/plots": "^1.2.5",
"@emotion/react": "^11.11.1",
"@emotion/server": "^11.11.0",
"@github/webauthn-json": "^2.1.1",
"@mantine/core": "^6.0.14",
"@mantine/dates": "^6.0.14",
"@mantine/dates": "^6.0.19",
"@mantine/dropzone": "^6.0.14",
"@mantine/form": "^6.0.14",
"@mantine/hooks": "^6.0.14",

526
pnpm-lock.yaml generated
View file

@ -5,6 +5,9 @@ settings:
excludeLinksFromLockfile: false
dependencies:
'@ant-design/plots':
specifier: ^1.2.5
version: 1.2.5(react-dom@18.2.0)(react@18.2.0)
'@emotion/react':
specifier: ^11.11.1
version: 11.11.1(@types/react@18.2.21)(react@18.2.0)
@ -18,7 +21,7 @@ dependencies:
specifier: ^6.0.14
version: 6.0.19(@emotion/react@11.11.1)(@mantine/hooks@6.0.19)(@types/react@18.2.21)(react-dom@18.2.0)(react@18.2.0)
'@mantine/dates':
specifier: ^6.0.14
specifier: ^6.0.19
version: 6.0.19(@mantine/core@6.0.19)(@mantine/hooks@6.0.19)(dayjs@1.11.9)(react@18.2.0)
'@mantine/dropzone':
specifier: ^6.0.14
@ -209,11 +212,210 @@ packages:
'@jridgewell/trace-mapping': 0.3.19
dev: false
/@ant-design/plots@1.2.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-8Jvu2xC5y5/B38/9Qr6CBiXCZopsGEA3IR4pjLFlkLoT4OHIKr4y8oIvhahM9mh9ZATyjkrZLWJBI8yETrReGg==}
peerDependencies:
react: '>=16.8.4'
react-dom: '>=16.8.4'
dependencies:
'@antv/g2plot': 2.4.31
'@antv/util': 2.0.17
react: 18.2.0
react-content-loader: 5.1.4(react@18.2.0)
react-dom: 18.2.0(react@18.2.0)
dev: false
/@antfu/ni@0.21.5:
resolution: {integrity: sha512-rFmuqZMFa1OTRbxdu3vmfytsy1CtsIUFH0bO85rZ1xdu2uLoioSaEi6iOULDVTQUrnes50jMs+UW355Ndj7Oxg==}
hasBin: true
dev: false
/@antv/adjust@0.2.5:
resolution: {integrity: sha512-MfWZOkD9CqXRES6MBGRNe27Q577a72EIwyMnE29wIlPliFvJfWwsrONddpGU7lilMpVKecS3WAzOoip3RfPTRQ==}
dependencies:
'@antv/util': 2.0.17
tslib: 1.14.1
dev: false
/@antv/attr@0.3.5:
resolution: {integrity: sha512-wuj2gUo6C8Q2ASSMrVBuTcb5LcV+Tc0Egiy6bC42D0vxcQ+ta13CLxgMmHz8mjD0FxTPJDXSciyszRSC5TdLsg==}
dependencies:
'@antv/color-util': 2.0.6
'@antv/scale': 0.3.18
'@antv/util': 2.0.17
tslib: 2.6.2
dev: false
/@antv/color-util@2.0.6:
resolution: {integrity: sha512-KnPEaAH+XNJMjax9U35W67nzPI+QQ2x27pYlzmSIWrbj4/k8PGrARXfzDTjwoozHJY8qG62Z+Ww6Alhu2FctXQ==}
dependencies:
'@antv/util': 2.0.17
tslib: 2.6.2
dev: false
/@antv/component@0.8.35:
resolution: {integrity: sha512-VnRa5X77nBPI952o2xePEEMSNZ6g2mcUDrQY8mVL2kino/8TFhqDq5fTRmDXZyWyIYd4ulJTz5zgeSwAnX/INQ==}
dependencies:
'@antv/color-util': 2.0.6
'@antv/dom-util': 2.0.4
'@antv/g-base': 0.5.15
'@antv/matrix-util': 3.1.0-beta.3
'@antv/path-util': 2.0.15
'@antv/scale': 0.3.18
'@antv/util': 2.0.17
fecha: 4.2.3
tslib: 2.6.2
dev: false
/@antv/coord@0.3.1:
resolution: {integrity: sha512-rFE94C8Xzbx4xmZnHh2AnlB3Qm1n5x0VT3OROy257IH6Rm4cuzv1+tZaUBATviwZd99S+rOY9telw/+6C9GbRw==}
dependencies:
'@antv/matrix-util': 3.1.0-beta.3
'@antv/util': 2.0.17
tslib: 2.6.2
dev: false
/@antv/dom-util@2.0.4:
resolution: {integrity: sha512-2shXUl504fKwt82T3GkuT4Uoc6p9qjCKnJ8gXGLSW4T1W37dqf9AV28aCfoVPHp2BUXpSsB+PAJX2rG/jLHsLQ==}
dependencies:
tslib: 2.6.2
dev: false
/@antv/event-emitter@0.1.3:
resolution: {integrity: sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==}
dev: false
/@antv/g-base@0.5.15:
resolution: {integrity: sha512-QOtq50QpnKez9J75/Z8j2yZ7QDQdk8R8mVQJiHtaEO5eI7DM4ZbrsWff/Ew26JYmPWdq7nbRuARMAD4PX9uuLA==}
dependencies:
'@antv/event-emitter': 0.1.3
'@antv/g-math': 0.1.9
'@antv/matrix-util': 3.1.0-beta.3
'@antv/path-util': 2.0.15
'@antv/util': 2.0.17
'@types/d3-timer': 2.0.1
d3-ease: 1.0.7
d3-interpolate: 3.0.1
d3-timer: 1.0.10
detect-browser: 5.3.0
tslib: 2.6.2
dev: false
/@antv/g-canvas@0.5.14:
resolution: {integrity: sha512-IUGLEMIMAUYgaBMT8h3FTmYQYz7sjQkKWwh6Psqx+UPK86fySa+G8fMRrh1EqAL07jVB+GRnn6Ym+3FoFUgeFg==}
dependencies:
'@antv/g-base': 0.5.15
'@antv/g-math': 0.1.9
'@antv/matrix-util': 3.1.0-beta.3
'@antv/path-util': 2.0.15
'@antv/util': 2.0.17
gl-matrix: 3.4.3
tslib: 2.6.2
dev: false
/@antv/g-math@0.1.9:
resolution: {integrity: sha512-KHMSfPfZ5XHM1PZnG42Q2gxXfOitYveNTA7L61lR6mhZ8Y/aExsYmHqaKBsSarU0z+6WLrl9C07PQJZaw0uljQ==}
dependencies:
'@antv/util': 2.0.17
gl-matrix: 3.4.3
dev: false
/@antv/g-svg@0.5.7:
resolution: {integrity: sha512-jUbWoPgr4YNsOat2Y/rGAouNQYGpw4R0cvlN0YafwOyacFFYy2zC8RslNd6KkPhhR3XHNSqJOuCYZj/YmLUwYw==}
dependencies:
'@antv/g-base': 0.5.15
'@antv/g-math': 0.1.9
'@antv/util': 2.0.17
detect-browser: 5.3.0
tslib: 2.6.2
dev: false
/@antv/g2@4.2.10:
resolution: {integrity: sha512-/ZlJ/DFJBCvtEQgE6roxdd6sBml0fZ8ZVfzG+HdjGpA7/ceURb8XkxUcqa0E8NV+e4sFijnaAhBCdUm2whiuyA==}
dependencies:
'@antv/adjust': 0.2.5
'@antv/attr': 0.3.5
'@antv/color-util': 2.0.6
'@antv/component': 0.8.35
'@antv/coord': 0.3.1
'@antv/dom-util': 2.0.4
'@antv/event-emitter': 0.1.3
'@antv/g-base': 0.5.15
'@antv/g-canvas': 0.5.14
'@antv/g-svg': 0.5.7
'@antv/matrix-util': 3.1.0-beta.3
'@antv/path-util': 2.0.15
'@antv/scale': 0.3.18
'@antv/util': 2.0.17
tslib: 2.6.2
dev: false
/@antv/g2plot@2.4.31:
resolution: {integrity: sha512-SlWHYVsJgRN7E1Oe5Qk6yWBrSWmctmloknFmklaqe9vEeK+YB9ZLUffZvtAHT10mA2NZ+VjGUhlnMNgR9M1PQg==}
dependencies:
'@antv/color-util': 2.0.6
'@antv/event-emitter': 0.1.3
'@antv/g-base': 0.5.15
'@antv/g2': 4.2.10
'@antv/matrix-util': 3.1.0-beta.3
'@antv/path-util': 3.0.1
'@antv/scale': 0.3.18
'@antv/util': 2.0.17
d3-hierarchy: 2.0.0
d3-regression: 1.3.10
fmin: 0.0.2
pdfast: 0.2.0
size-sensor: 1.0.2
tslib: 2.6.2
dev: false
/@antv/matrix-util@3.0.4:
resolution: {integrity: sha512-BAPyu6dUliHcQ7fm9hZSGKqkwcjEDVLVAstlHULLvcMZvANHeLXgHEgV7JqcAV/GIhIz8aZChIlzM1ZboiXpYQ==}
dependencies:
'@antv/util': 2.0.17
gl-matrix: 3.4.3
tslib: 2.6.2
dev: false
/@antv/matrix-util@3.1.0-beta.3:
resolution: {integrity: sha512-W2R6Za3A6CmG51Y/4jZUM/tFgYSq7vTqJL1VD9dKrvwxS4sE0ZcXINtkp55CdyBwJ6Cwm8pfoRpnD4FnHahN0A==}
dependencies:
'@antv/util': 2.0.17
gl-matrix: 3.4.3
tslib: 2.6.2
dev: false
/@antv/path-util@2.0.15:
resolution: {integrity: sha512-R2VLZ5C8PLPtr3VciNyxtjKqJ0XlANzpFb5sE9GE61UQqSRuSVSzIakMxjEPrpqbgc+s+y8i+fmc89Snu7qbNw==}
dependencies:
'@antv/matrix-util': 3.0.4
'@antv/util': 2.0.17
tslib: 2.6.2
dev: false
/@antv/path-util@3.0.1:
resolution: {integrity: sha512-tpvAzMpF9Qm6ik2YSMqICNU5tco5POOW7S4XoxZAI/B0L26adU+Md/SmO0BBo2SpuywKvzPH3hPT3xmoyhr04Q==}
dependencies:
gl-matrix: 3.4.3
lodash-es: 4.17.21
tslib: 2.6.2
dev: false
/@antv/scale@0.3.18:
resolution: {integrity: sha512-GHwE6Lo7S/Q5fgaLPaCsW+CH+3zl4aXpnN1skOiEY0Ue9/u+s2EySv6aDXYkAqs//i0uilMDD/0/4n8caX9U9w==}
dependencies:
'@antv/util': 2.0.17
fecha: 4.2.3
tslib: 2.6.2
dev: false
/@antv/util@2.0.17:
resolution: {integrity: sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==}
dependencies:
csstype: 3.1.2
tslib: 2.6.2
dev: false
/@aws-crypto/crc32@3.0.0:
resolution: {integrity: sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA==}
requiresBuild: true
@ -2734,6 +2936,10 @@ packages:
'@types/node': 20.5.7
dev: false
/@types/d3-timer@2.0.1:
resolution: {integrity: sha512-TF8aoF5cHcLO7W7403blM7L1T+6NF3XMyN3fxyUolq2uOcFeicG/khQg/dGxiCJWoAcmYulYN7LYSRKO54IXaA==}
dev: false
/@types/debug@4.1.8:
resolution: {integrity: sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ==}
dependencies:
@ -3119,6 +3325,20 @@ packages:
uri-js: 4.4.1
dev: true
/align-text@0.1.4:
resolution: {integrity: sha512-GrTZLRpmp6wIC2ztrWW9MjjTgSKccffgFagbNDOX95/dcjEcYZibYTeaOntySQLcdw1ztBoFkviiUvTMbb9MYg==}
engines: {node: '>=0.10.0'}
dependencies:
kind-of: 3.2.2
longest: 1.0.1
repeat-string: 1.6.1
dev: false
/amdefine@1.0.1:
resolution: {integrity: sha512-S2Hw0TtNkMJhIabBwIojKL9YHO5T0n5eNqWJ7Lrlel/zDbftQpxpapi8tZs3X1HWa+u+QeydGmzzNU0m09+Rcg==}
engines: {node: '>=0.4.2'}
dev: false
/ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
@ -3126,10 +3346,20 @@ packages:
type-fest: 0.21.3
dev: false
/ansi-regex@2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
engines: {node: '>=0.10.0'}
dev: false
/ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
engines: {node: '>=8'}
/ansi-styles@2.2.1:
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
engines: {node: '>=0.10.0'}
dev: false
/ansi-styles@3.2.1:
resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==}
engines: {node: '>=4'}
@ -3552,6 +3782,11 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
/camelcase@1.2.1:
resolution: {integrity: sha512-wzLkDa4K/mzI1OSITC+DUyjgIl/ETNHE9QvYgy6J6Jvqyyz4C0Xfd+lQhb19sX2jMpZV4IssUn0VDVmglV+s4g==}
engines: {node: '>=0.10.0'}
dev: false
/camelcase@5.3.1:
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
engines: {node: '>=6'}
@ -3569,6 +3804,25 @@ packages:
resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==}
dev: false
/center-align@0.1.3:
resolution: {integrity: sha512-Baz3aNe2gd2LP2qk5U+sDk/m4oSuwSDcBfayTCTBoWpfIGO5XFxPmjILQII4NGiZjD6DoDI6kf7gKaxkf7s3VQ==}
engines: {node: '>=0.10.0'}
dependencies:
align-text: 0.1.4
lazy-cache: 1.0.4
dev: false
/chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
dependencies:
ansi-styles: 2.2.1
escape-string-regexp: 1.0.5
has-ansi: 2.0.0
strip-ansi: 3.0.1
supports-color: 2.0.0
dev: false
/chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@ -3660,6 +3914,14 @@ packages:
resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
dev: false
/cliui@2.1.0:
resolution: {integrity: sha512-GIOYRizG+TGoc7Wgc1LiOTLare95R3mzKgoln+Q/lE4ceiYH19gUpl0l0Ffq4lJDEf3FxujMe6IBfOCs7pfqNA==}
dependencies:
center-align: 0.1.3
right-align: 0.1.3
wordwrap: 0.0.2
dev: false
/cliui@6.0.0:
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
dependencies:
@ -3800,6 +4062,10 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/contour_plot@0.0.1:
resolution: {integrity: sha512-Nil2HI76Xux6sVGORvhSS8v66m+/h5CwFkBJDO+U5vWaMdNC0yXNCsGDPbzPhvqOEU5koebhdEvD372LI+IyLw==}
dev: false
/convert-source-map@1.9.0:
resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==}
dev: false
@ -3869,6 +4135,34 @@ packages:
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
/d3-color@3.1.0:
resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
engines: {node: '>=12'}
dev: false
/d3-ease@1.0.7:
resolution: {integrity: sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==}
dev: false
/d3-hierarchy@2.0.0:
resolution: {integrity: sha512-SwIdqM3HxQX2214EG9GTjgmCc/mbSx4mQBn+DuEETubhOw6/U3fmnji4uCVrmzOydMHSO1nZle5gh6HB/wdOzw==}
dev: false
/d3-interpolate@3.0.1:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
dependencies:
d3-color: 3.1.0
dev: false
/d3-regression@1.3.10:
resolution: {integrity: sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==}
dev: false
/d3-timer@1.0.10:
resolution: {integrity: sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==}
dev: false
/damerau-levenshtein@1.0.8:
resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
dev: true
@ -3941,6 +4235,17 @@ packages:
mimic-response: 3.1.0
dev: false
/deep-equal@1.1.1:
resolution: {integrity: sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==}
dependencies:
is-arguments: 1.1.1
is-date-object: 1.0.5
is-regex: 1.1.4
object-is: 1.1.5
object-keys: 1.1.1
regexp.prototype.flags: 1.5.0
dev: false
/deep-extend@0.6.0:
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
engines: {node: '>=4.0.0'}
@ -3991,6 +4296,10 @@ packages:
has-property-descriptors: 1.0.0
object-keys: 1.1.1
/defined@1.0.1:
resolution: {integrity: sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==}
dev: false
/del@6.1.1:
resolution: {integrity: sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==}
engines: {node: '>=10'}
@ -4033,6 +4342,10 @@ packages:
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dev: false
/detect-browser@5.3.0:
resolution: {integrity: sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==}
dev: false
/detect-libc@2.0.2:
resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
engines: {node: '>=8'}
@ -4126,6 +4439,13 @@ packages:
engines: {node: '>=12'}
dev: false
/dotignore@0.1.2:
resolution: {integrity: sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==}
hasBin: true
dependencies:
minimatch: 3.1.2
dev: false
/duplexer2@0.1.4:
resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==}
dependencies:
@ -4812,6 +5132,10 @@ packages:
dependencies:
reusify: 1.0.4
/fecha@4.2.3:
resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==}
dev: false
/ffmpeg-static@5.2.0:
resolution: {integrity: sha512-WrM7kLW+do9HLr+H6tk7LzQ7kPqbAgLjdzNE32+u3Ff11gXt9Kkkd2nusGFrlWMIe+XaA97t+I8JS7sZIrvRgA==}
engines: {node: '>=16'}
@ -4908,6 +5232,16 @@ packages:
resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==}
dev: true
/fmin@0.0.2:
resolution: {integrity: sha512-sSi6DzInhl9d8yqssDfGZejChO8d2bAGIpysPsvYsxFe898z89XhCZg6CPNV3nhUhFefeC/AXZK2bAJxlBjN6A==}
dependencies:
contour_plot: 0.0.1
json2module: 0.0.3
rollup: 0.25.8
tape: 4.16.2
uglify-js: 2.8.29
dev: false
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
dependencies:
@ -5052,6 +5386,10 @@ packages:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
dev: false
/gl-matrix@3.4.3:
resolution: {integrity: sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==}
dev: false
/glob-parent@5.1.2:
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
engines: {node: '>= 6'}
@ -5160,6 +5498,13 @@ packages:
resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
dev: true
/has-ansi@2.0.0:
resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
engines: {node: '>=0.10.0'}
dependencies:
ansi-regex: 2.1.1
dev: false
/has-bigints@1.0.2:
resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==}
@ -5437,6 +5782,14 @@ packages:
engines: {node: '>= 0.10'}
dev: false
/is-arguments@1.1.1:
resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
has-tostringtag: 1.0.0
dev: false
/is-array-buffer@3.0.2:
resolution: {integrity: sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==}
dependencies:
@ -5478,6 +5831,10 @@ packages:
call-bind: 1.0.2
has-tostringtag: 1.0.0
/is-buffer@1.1.6:
resolution: {integrity: sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==}
dev: false
/is-buffer@2.0.5:
resolution: {integrity: sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==}
engines: {node: '>=4'}
@ -5785,6 +6142,13 @@ packages:
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
dev: true
/json2module@0.0.3:
resolution: {integrity: sha512-qYGxqrRrt4GbB8IEOy1jJGypkNsjWoIMlZt4bAsmUScCA507Hbc2p1JOhBzqn45u3PWafUgH2OnzyNU7udO/GA==}
hasBin: true
dependencies:
rw: 1.3.3
dev: false
/json5@1.0.2:
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
hasBin: true
@ -5874,6 +6238,13 @@ packages:
json-buffer: 3.0.1
dev: true
/kind-of@3.2.2:
resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
engines: {node: '>=0.10.0'}
dependencies:
is-buffer: 1.1.6
dev: false
/kleur@3.0.3:
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
engines: {node: '>=6'}
@ -5899,6 +6270,11 @@ packages:
language-subtag-registry: 0.3.22
dev: true
/lazy-cache@1.0.4:
resolution: {integrity: sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==}
engines: {node: '>=0.10.0'}
dev: false
/lazystream@1.0.1:
resolution: {integrity: sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==}
engines: {node: '>= 0.6.3'}
@ -5948,6 +6324,10 @@ packages:
dependencies:
p-locate: 5.0.0
/lodash-es@4.17.21:
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
dev: false
/lodash.deburr@4.1.0:
resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==}
dev: false
@ -6006,6 +6386,11 @@ packages:
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
dev: false
/longest@1.0.1:
resolution: {integrity: sha512-k+yt5n3l48JU4k8ftnKG6V7u32wyH2NfKzeMto9F/QRE0amxy/LayxwlvjjkZEIzqR+19IrtFO8p5kB9QaYUFg==}
engines: {node: '>=0.10.0'}
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@ -6872,6 +7257,14 @@ packages:
/object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
/object-is@1.1.5:
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
engines: {node: '>= 0.4'}
dependencies:
call-bind: 1.0.2
define-properties: 1.2.0
dev: false
/object-keys@0.4.0:
resolution: {integrity: sha512-ncrLw+X55z7bkl5PnUvHwFK9FcGuFYo9gtjws2XtSzL+aZ8tm830P60WJ0dSmFVaSalWieW5MD7kEdnXda9yJw==}
dev: false
@ -7152,6 +7545,10 @@ packages:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
/pdfast@0.2.0:
resolution: {integrity: sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==}
dev: false
/pg-cloudflare@1.1.1:
resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==}
requiresBuild: true
@ -7444,6 +7841,15 @@ packages:
strip-json-comments: 2.0.1
dev: false
/react-content-loader@5.1.4(react@18.2.0):
resolution: {integrity: sha512-hTq7pZi2GKCK6a9d3u6XStozm0QGCEjw8cSqQReiWnh2up6IwCha5R5TF0o6SY5qUDpByloEZEZtnFxpJyENFw==}
engines: {node: '>=10'}
peerDependencies:
react: '>=16.0.0'
dependencies:
react: 18.2.0
dev: false
/react-dom@18.2.0(react@18.2.0):
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==}
peerDependencies:
@ -7706,6 +8112,11 @@ packages:
unified: 10.1.2
dev: false
/repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
dev: false
/replace-string@3.1.0:
resolution: {integrity: sha512-yPpxc4ZR2makceA9hy/jHNqc7QVkd4Je/N0WRHm6bs3PtivPuPynxE5ejU/mp5EhnCv8+uZL7vhz8rkluSlx+Q==}
engines: {node: '>=8'}
@ -7762,6 +8173,12 @@ packages:
signal-exit: 3.0.7
dev: false
/resumer@0.0.0:
resolution: {integrity: sha512-Fn9X8rX8yYF4m81rZCK/5VmrmsSbqS/i3rDLl6ZZHAXgC2nTAx3dhwG8q8odP/RmdLa2YrybDJaAMg+X1ajY3w==}
dependencies:
through: 2.3.8
dev: false
/retry@0.13.1:
resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==}
engines: {node: '>= 4'}
@ -7775,12 +8192,28 @@ packages:
resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==}
dev: false
/right-align@0.1.3:
resolution: {integrity: sha512-yqINtL/G7vs2v+dFIZmFUDbnVyFUJFKd6gK22Kgo6R4jfJGFtisKyncWDDULgjfqf4ASQuIQyjJ7XZ+3aWpsAg==}
engines: {node: '>=0.10.0'}
dependencies:
align-text: 0.1.4
dev: false
/rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
hasBin: true
dependencies:
glob: 7.2.3
/rollup@0.25.8:
resolution: {integrity: sha512-a2S4Bh3bgrdO4BhKr2E4nZkjTvrJ2m2bWjMTzVYtoqSCn0HnuxosXnaJUHrMEziOWr3CzL9GjilQQKcyCQpJoA==}
hasBin: true
dependencies:
chalk: 1.1.3
minimist: 1.2.8
source-map-support: 0.3.3
dev: false
/rollup@3.28.1:
resolution: {integrity: sha512-R9OMQmIHJm9znrU3m3cpE8uhN0fGdXiawME7aZIpQqvpS/85+Vt1Hq1/yVIcYfOmaQiHjvXkQAoJukvLpau6Yw==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
@ -7805,6 +8238,10 @@ packages:
dependencies:
queue-microtask: 1.2.3
/rw@1.3.3:
resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==}
dev: false
/sade@1.8.1:
resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==}
engines: {node: '>=6'}
@ -7980,6 +8417,10 @@ packages:
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
dev: false
/size-sensor@1.0.2:
resolution: {integrity: sha512-2NCmWxY7A9pYKGXNBfteo4hy14gWu47rg5692peVMst6lQLPKrVjhY+UTEsPI5ceFRJSl3gVgMYaUi/hKuaiKw==}
dev: false
/slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
@ -8020,6 +8461,19 @@ packages:
engines: {node: '>=0.10.0'}
dev: false
/source-map-support@0.3.3:
resolution: {integrity: sha512-9O4+y9n64RewmFoKUZ/5Tx9IHIcXM6Q+RTSw6ehnqybUz4a7iwR3Eaw80uLtqqQ5D0C+5H03D4KKGo9PdP33Gg==}
dependencies:
source-map: 0.1.32
dev: false
/source-map@0.1.32:
resolution: {integrity: sha512-htQyLrrRLkQ87Zfrir4/yN+vAUd6DNjVayEjTSHXu29AYQJw57I4/xEL/M6p6E/woPNJwvZt6rVlzc7gFEJccQ==}
engines: {node: '>=0.8.0'}
dependencies:
amdefine: 1.0.1
dev: false
/source-map@0.5.7:
resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==}
engines: {node: '>=0.10.0'}
@ -8157,6 +8611,13 @@ packages:
safe-buffer: 5.2.1
dev: false
/strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'}
dependencies:
ansi-regex: 2.1.1
dev: false
/strip-ansi@6.0.1:
resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
engines: {node: '>=8'}
@ -8254,6 +8715,11 @@ packages:
ts-interface-checker: 0.1.13
dev: true
/supports-color@2.0.0:
resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
engines: {node: '>=0.8.0'}
dev: false
/supports-color@5.5.0:
resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==}
engines: {node: '>=4'}
@ -8310,6 +8776,27 @@ packages:
engines: {node: '>=6'}
dev: true
/tape@4.16.2:
resolution: {integrity: sha512-TUChV+q0GxBBCEbfCYkGLkv8hDJYjMdSWdE0/Lr331sB389dsvFUHNV9ph5iQqKzt8Ss9drzcda/YeexclBFqg==}
hasBin: true
dependencies:
call-bind: 1.0.2
deep-equal: 1.1.1
defined: 1.0.1
dotignore: 0.1.2
for-each: 0.3.3
glob: 7.2.3
has: 1.0.3
inherits: 2.0.4
is-regex: 1.1.4
minimist: 1.2.8
object-inspect: 1.12.3
resolve: 1.22.4
resumer: 0.0.0
string.prototype.trim: 1.2.7
through: 2.3.8
dev: false
/tar-fs@2.1.1:
resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==}
dependencies:
@ -8545,7 +9032,6 @@ packages:
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
requiresBuild: true
dev: false
optional: true
/tslib@2.6.2:
resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
@ -8676,6 +9162,23 @@ packages:
hasBin: true
dev: true
/uglify-js@2.8.29:
resolution: {integrity: sha512-qLq/4y2pjcU3vhlhseXGGJ7VbFO4pBANu0kwl8VCa9KEI0V8VfZIx2Fy3w01iSTA/pGwKZSmu/+I4etLNDdt5w==}
engines: {node: '>=0.8.0'}
hasBin: true
dependencies:
source-map: 0.5.7
yargs: 3.10.0
optionalDependencies:
uglify-to-browserify: 1.0.2
dev: false
/uglify-to-browserify@1.0.2:
resolution: {integrity: sha512-vb2s1lYx2xBtUgy+ta+b2J/GLVUR+wmpINwHePmPRhOsIVCG2wDzKJ0n14GslH1BifsqVzSOwQhRaCAsZ/nI4Q==}
requiresBuild: true
dev: false
optional: true
/unbox-primitive@1.0.2:
resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==}
dependencies:
@ -9056,6 +9559,16 @@ packages:
string-width: 4.2.3
dev: false
/window-size@0.1.0:
resolution: {integrity: sha512-1pTPQDKTdd61ozlKGNCjhNRd+KPmgLSGa3mZTHoOliaGcESD8G1PXhh7c1fgiPjVbNVfgy2Faw4BI8/m0cC8Mg==}
engines: {node: '>= 0.8.0'}
dev: false
/wordwrap@0.0.2:
resolution: {integrity: sha512-xSBsCeh+g+dinoBv3GAOWM4LcVVO68wLXRanibtBSdUvkGWQRGeE9P7IwU9EmDDi4jA6L44lz15CGMwdw9N5+Q==}
engines: {node: '>=0.4.0'}
dev: false
/wrap-ansi@6.2.0:
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
engines: {node: '>=8'}
@ -9148,6 +9661,15 @@ packages:
yargs-parser: 18.1.3
dev: false
/yargs@3.10.0:
resolution: {integrity: sha512-QFzUah88GAGy9lyDKGBqZdkYApt63rCXYBGYnEP4xDJPXNqXXnBDACnbrXnViV6jRSqAePwrATi2i8mfYm4L1A==}
dependencies:
camelcase: 1.2.1
cliui: 2.1.0
decamelize: 1.2.0
window-size: 0.1.0
dev: false
/yocto-queue@0.1.0:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}

View file

@ -47,6 +47,7 @@ import Link from 'next/link';
import { useRouter } from 'next/router';
import { useState } from 'react';
import ConfigProvider from './ConfigProvider';
import { IconGraph } from '@tabler/icons-react';
type NavLinks = {
label: string;
@ -64,6 +65,13 @@ const navLinks: NavLinks[] = [
active: (path: string) => path === '/dashbaord',
href: '/dashboard',
},
{
label: 'Metrics',
icon: <IconGraph size='1rem' />,
active: (path: string) => path === '/dashboard/metrics',
href: '/dashboard/metrics',
if: (_, config) => config.features.metrics,
},
{
label: 'Files',
icon: <IconFiles size='1rem' />,

View file

@ -0,0 +1,89 @@
import { Box, Button, Group, Loader, Modal, Paper, SimpleGrid, Text, Title } from '@mantine/core';
import { DatePicker } from '@mantine/dates';
import { IconCalendarTime } from '@tabler/icons-react';
import { useState } from 'react';
import FilesUrlsCountGraph from './parts/FilesUrlsCountGraph';
import StatsCards from './parts/StatsCards';
import StatsTables from './parts/StatsTables';
import StorageGraph from './parts/StorageGraph';
import ViewsGraph from './parts/ViewsGraph';
import { useApiStats } from './useStats';
export default function DashboardMetrics() {
const [dateRange, setDateRange] = useState<[Date | null, Date | null]>([
new Date(Date.now() - 86400000),
new Date(),
]);
const [open, setOpen] = useState(false);
const { data, isLoading } = useApiStats({
from: dateRange[0]?.toISOString() ?? undefined,
to: dateRange[1]?.toISOString() ?? undefined,
});
return (
<>
<Modal title={<Title>Change Range</Title>} opened={open} onClose={() => setOpen(false)} size='auto'>
<Paper withBorder>
<DatePicker
type='range'
value={dateRange}
onChange={setDateRange}
allowSingleDateInRange={false}
maxDate={new Date(Date.now() + 86400000)}
/>
</Paper>
<Group mt='md'>
<Button fullWidth onClick={() => setOpen(false)}>
Close
</Button>
</Group>
</Modal>
<Group>
<Title>Metrics</Title>
<Button
compact
variant='outline'
leftIcon={<IconCalendarTime size='1rem' />}
onClick={() => setOpen(true)}
>
Change Date Range
</Button>
<Text size='sm' color='dimmed'>
{dateRange[0]?.toLocaleDateString()}{' '}
{dateRange[1] ? `to ${dateRange[1]?.toLocaleDateString()}` : ''}
</Text>
</Group>
<Box pos='relative' mih={300} my='sm'>
{isLoading ? (
<Loader />
) : data ? (
<div>
<StatsCards data={data!} />
<StatsTables data={data!} />
<SimpleGrid mt='md' cols={2} breakpoints={[{ maxWidth: 'sm', cols: 1 }]}>
<FilesUrlsCountGraph metrics={data!} />
<ViewsGraph metrics={data!} />
</SimpleGrid>
{/* :skull: this stops it from overflowing somehow */}
<SimpleGrid cols={1}>
<StorageGraph metrics={data!} />
</SimpleGrid>
</div>
) : (
<Text size='sm' color='dimmed'>
none
</Text>
)}
</Box>
</>
);
}

View file

@ -0,0 +1,40 @@
import { Metric } from '@/lib/db/models/metric';
import { Paper, Title } from '@mantine/core';
import dynamic from 'next/dynamic';
const Line = dynamic(() => import('@ant-design/plots').then(({ Line }) => Line), { ssr: false });
export default function FilesUrlsCountGraph({ metrics }: { metrics: Metric[] }) {
return (
<Paper radius='sm' withBorder p='sm'>
<Title order={3}>Count</Title>
<Line
data={[
...metrics.map((metric) => ({
date: metric.createdAt,
sum: metric.data.files,
type: 'Files',
})),
...metrics.map((metric) => ({
date: metric.createdAt,
sum: metric.data.urls,
type: 'URLs',
})),
]}
xField='date'
yField='sum'
seriesField='type'
xAxis={{
type: 'time',
mask: 'YYYY-MM-DD HH:mm:ss',
}}
legend={{
position: 'top',
}}
padding='auto'
smooth
/>
</Paper>
);
}

View file

@ -0,0 +1,50 @@
import { bytes } from '@/lib/bytes';
import { Metric } from '@/lib/db/models/metric';
import { Group, Paper, SimpleGrid, Text, Title } from '@mantine/core';
import {
IconDatabase,
IconEyeFilled,
IconFiles,
IconLink,
IconUsers,
Icon as TablerIcon,
} from '@tabler/icons-react';
function StatCard({ title, value, Icon }: { title: string; value: number | string; Icon: TablerIcon }) {
return (
<Paper radius='sm' withBorder p='sm'>
<Group position='apart'>
<Text size='xl' weight='bolder' color='dimmed'>
{title}
</Text>
<Icon size='1rem' />
</Group>
<Title order={1}>{value}</Title>
</Paper>
);
}
export default function StatsCards({ data }: { data: Metric[] }) {
if (!data.length) return null;
const recent = data[0];
return (
<SimpleGrid
cols={3}
breakpoints={[
{ maxWidth: 'sm', cols: 1 },
{ maxWidth: 'md', cols: 2 },
]}
mb='sm'
>
<StatCard title='Files' value={recent.data.files} Icon={IconFiles} />
<StatCard title='URLs' value={recent.data.urls} Icon={IconLink} />
<StatCard title='Storage Used' value={bytes(recent.data.storage)} Icon={IconDatabase} />
<StatCard title='Users' value={recent.data.users} Icon={IconUsers} />
<StatCard title='File Views' value={recent.data.fileViews} Icon={IconEyeFilled} />
<StatCard title='URL Views' value={recent.data.urlViews} Icon={IconEyeFilled} />
</SimpleGrid>
);
}

View file

@ -0,0 +1,83 @@
import { bytes } from '@/lib/bytes';
import { Metric } from '@/lib/db/models/metric';
import { Paper, SimpleGrid, Table } from '@mantine/core';
import TypesPieChart from './TypesPieChart';
export default function StatsTables({ data }: { data: Metric[] }) {
if (!data.length) return null;
const recent = data[0]; // it is sorted by desc so 0 is the first one.
return (
<>
<SimpleGrid cols={2} breakpoints={[{ maxWidth: 'sm', cols: 1 }]}>
<Paper radius='sm' withBorder>
<Table highlightOnHover>
<thead>
<tr>
<th>User</th>
<th>Files</th>
<th>Storage Used</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{recent.data.filesUsers.map((count, i) => (
<tr key={i}>
<td>{count.username}</td>
<td>{count.sum}</td>
<td>{bytes(count.storage)}</td>
<td>{count.views}</td>
</tr>
))}
</tbody>
</Table>
</Paper>
<Paper radius='sm' withBorder>
<Table highlightOnHover>
<thead>
<tr>
<th>User</th>
<th>URLs</th>
<th>Views</th>
</tr>
</thead>
<tbody>
{recent.data.urlsUsers.map((count, i) => (
<tr key={i}>
<td>{count.username}</td>
<td>{count.sum}</td>
<td>{count.views}</td>
</tr>
))}
</tbody>
</Table>
</Paper>
<Paper radius='sm' withBorder>
<Table highlightOnHover>
<thead>
<tr>
<th>Type</th>
<th>Files</th>
</tr>
</thead>
<tbody>
{recent.data.types.map((count, i) => (
<tr key={i}>
<td>{count.type}</td>
<td>{count.sum}</td>
</tr>
))}
</tbody>
</Table>
</Paper>
<Paper radius='sm' withBorder p='sm'>
<TypesPieChart metric={recent} />
</Paper>
</SimpleGrid>
</>
);
}

View file

@ -0,0 +1,38 @@
import { bytes } from '@/lib/bytes';
import { Metric } from '@/lib/db/models/metric';
import { Paper, Title } from '@mantine/core';
import dynamic from 'next/dynamic';
const Line = dynamic(() => import('@ant-design/plots').then(({ Line }) => Line), { ssr: false });
export default function StorageGraph({ metrics }: { metrics: Metric[] }) {
return (
<Paper radius='sm' withBorder p='sm' mt='md'>
<Title order={3} mb='sm'>
Storage Used
</Title>
<Line
data={metrics.map((metric) => ({
date: metric.createdAt,
storage: metric.data.storage,
}))}
xField='date'
yField='storage'
xAxis={{
type: 'time',
mask: 'YYYY-MM-DD HH:mm:ss',
}}
yAxis={{
label: {
formatter: (v) => bytes(Number(v)),
},
}}
tooltip={{
formatter: (v) => ({ name: 'Storage Used', value: bytes(Number(v.storage)) }),
}}
smooth
/>
</Paper>
);
}

View file

@ -0,0 +1,41 @@
import { Metric } from '@/lib/db/models/metric';
import dynamic from 'next/dynamic';
const Pie = dynamic(() => import('@ant-design/plots').then(({ Pie }) => Pie), { ssr: false });
export default function TypesPieChart({ metric }: { metric: Metric }) {
return (
<Pie
data={metric.data.types}
angleField='sum'
colorField='type'
radius={0.8}
label={{
type: 'outer',
content: '{name} - {percentage}',
}}
// legend={{
// position: 'bottom',
// pageNavigator: {
// marker: {
// style: {
// inactiveFill: theme.colorScheme === 'light' ? '#000' : '#fff',
// fill: theme.colorScheme === 'light' ? '#000' : '#fff',
// opacity: 0.8,
// size: 14,
// },
// },
// text: {
// style: {
// fill: theme.colorScheme === 'light' ? '#000' : '#fff',
// fontSize: 14,
// },
// },
// },
// maxWidth: isSmall ? 100 : 100,
// }}
legend={false}
interactions={[{ type: 'pie-legend-active' }, { type: 'element-active' }]}
/>
);
}

View file

@ -0,0 +1,45 @@
import { Metric } from '@/lib/db/models/metric';
import { Paper, Title } from '@mantine/core';
import dynamic from 'next/dynamic';
const Line = dynamic(() => import('@ant-design/plots').then(({ Line }) => Line), { ssr: false });
export default function ViewsGraph({ metrics }: { metrics: Metric[] }) {
return (
<Paper radius='sm' withBorder p='sm'>
<Title order={3}>Views</Title>
<Line
data={[
...metrics.map((metric) => ({
date: metric.createdAt,
views: metric.data.fileViews,
type: 'Files',
})),
...metrics.map((metric) => ({
date: metric.createdAt,
views: metric.data.urlViews,
type: 'URLs',
})),
]}
xField='date'
yField='views'
seriesField='type'
xAxis={{
type: 'time',
mask: 'YYYY-MM-DD HH:mm:ss',
}}
yAxis={{
label: {
formatter: (v) => `${v} views`,
},
}}
legend={{
position: 'top',
}}
padding='auto'
smooth
/>
</Paper>
);
}

View file

@ -0,0 +1,40 @@
import { Response } from '@/lib/api/response';
import useSWR from 'swr';
type ApiStatsOptions = {
from?: string;
to?: string;
};
const fetcher = async ({ options }: { options: ApiStatsOptions } = { options: {} }) => {
const searchParams = new URLSearchParams();
if (options.from) searchParams.append('from', options.from);
if (options.to) searchParams.append('to', options.to);
const res = await fetch(`/api/stats${searchParams.toString() ? `?${searchParams.toString()}` : ''}`);
if (!res.ok) {
const json = await res.json();
throw new Error(json.message);
}
return res.json();
};
export function useApiStats(options: ApiStatsOptions = {}) {
if (!options.from && !options.to)
return { data: undefined, error: undefined, isLoading: false, mutate: () => {} };
const { data, error, isLoading, mutate } = useSWR<Response['/api/stats']>(
{ key: '/api/stats', options },
fetcher,
);
return {
data,
error,
isLoading,
mutate,
};
}

View file

@ -7,6 +7,7 @@ import { ApiAuthRegisterResponse } from '@/pages/api/auth/register';
import { ApiAuthWebauthnResponse } from '@/pages/api/auth/webauthn';
import { ApiHealthcheckResponse } from '@/pages/api/healthcheck';
import { ApiSetupResponse } from '@/pages/api/setup';
import { ApiStatsResponse } from '@/pages/api/stats';
import { ApiUploadResponse } from '@/pages/api/upload';
import { ApiUserResponse } from '@/pages/api/user';
import { ApiUserFilesResponse } from '@/pages/api/user/files';
@ -54,4 +55,5 @@ export type Response = {
'/api/setup': ApiSetupResponse;
'/api/upload': ApiUploadResponse;
'/api/version': ApiVersionResponse;
'/api/stats': ApiStatsResponse;
};

View file

@ -19,6 +19,7 @@ export const rawConfig: any = {
clearInvitesInterval: undefined,
maxViewsInterval: undefined,
thumbnailsInterval: undefined,
metricsInterval: undefined,
},
files: {
route: undefined,
@ -50,6 +51,7 @@ export const rawConfig: any = {
enabled: undefined,
num_threads: undefined,
},
metrics: undefined,
},
invites: {
enabled: undefined,
@ -110,6 +112,7 @@ export const PROP_TO_ENV: Record<string, string> = {
'scheduler.clearInvitesInterval': 'SCHEDULER_CLEAR_INVITES_INTERVAL',
'scheduler.maxViewsInterval': 'SCHEDULER_MAX_VIEWS_INTERVAL',
'scheduler.thumbnailsInterval': 'SCHEDULER_THUMBNAILS_INTERVAL',
'scheduler.metricsInterval': 'SCHEDULER_METRICS_INTERVAL',
'files.route': 'FILES_ROUTE',
'files.length': 'FILES_LENGTH',
@ -145,6 +148,7 @@ export const PROP_TO_ENV: Record<string, string> = {
'features.deleteOnMaxViews': 'FEATURES_DELETE_ON_MAX_VIEWS',
'features.thumbails.enabled': 'FEATURES_THUMBNAILS_ENABLED',
'features.thumbnails.num_threads': 'FEATURES_THUMBNAILS_NUM_THREADS',
'features.metrics': 'FEATURES_METRICS',
'invites.enabled': 'INVITES_ENABLED',
'invites.length': 'INVITES_LENGTH',
@ -191,6 +195,7 @@ export function readEnv() {
env(PROP_TO_ENV['scheduler.clearInvitesInterval'], 'scheduler.clearInvitesInterval', 'ms'),
env(PROP_TO_ENV['scheduler.maxViewsInterval'], 'scheduler.maxViewsInterval', 'ms'),
env(PROP_TO_ENV['scheduler.thumbnailsInterval'], 'scheduler.thumbnailsInterval', 'ms'),
env(PROP_TO_ENV['scheduler.metricsInterval'], 'scheduler.metricsInterval', 'ms'),
env(PROP_TO_ENV['files.route'], 'files.route', 'string'),
env(PROP_TO_ENV['files.length'], 'files.length', 'number'),
@ -222,6 +227,7 @@ export function readEnv() {
env(PROP_TO_ENV['features.deleteOnMaxViews'], 'features.deleteOnMaxViews', 'boolean'),
env(PROP_TO_ENV['features.thumbnails.enabled'], 'features.thumbnails.enabled', 'boolean'),
env(PROP_TO_ENV['features.thumbnails.num_threads'], 'features.thumbnails.num_threads', 'number'),
env(PROP_TO_ENV['features.metrics'], 'features.metrics', 'boolean'),
env(PROP_TO_ENV['invites.enabled'], 'invites.enabled', 'boolean'),
env(PROP_TO_ENV['invites.length'], 'invites.length', 'number'),

View file

@ -44,7 +44,8 @@ export const schema = z.object({
deleteInterval: z.number().default(ms('30min')),
clearInvitesInterval: z.number().default(ms('30min')),
maxViewsInterval: z.number().default(ms('30min')),
thumbnailsInterval: z.number().default(ms('15s')),
thumbnailsInterval: z.number().default(ms('30min')),
metricsInterval: z.number().default(ms('30min')),
}),
files: z.object({
route: z.string().startsWith('/').nonempty().trim().toLowerCase().default('/u'),
@ -111,6 +112,7 @@ export const schema = z.object({
enabled: z.boolean().default(true),
num_threads: z.number().default(4),
}),
metrics: z.boolean().default(true),
}),
invites: z.object({
enabled: z.boolean().default(true),

View file

@ -1,6 +1,7 @@
import { log } from '@/lib/logger';
import { Prisma, PrismaClient } from '@prisma/client';
import { userViewSchema } from './models/user';
import { metricDataSchema } from './models/metric';
const building = !!process.env.ZIPLINE_BUILD;
@ -35,6 +36,14 @@ function getClient() {
},
},
},
metric: {
data: {
needs: { data: true },
compute({ data }: { data: Prisma.JsonValue }) {
return metricDataSchema.parse(data);
},
},
},
},
});
client.$connect();

View file

@ -0,0 +1,45 @@
import { z } from 'zod';
export type Metric = {
id: string;
createdAt: Date;
updatedAt: Date;
data: MetricData;
};
export type MetricData = z.infer<typeof metricDataSchema>;
export const metricDataSchema = z.object({
users: z.number(),
files: z.number(),
fileViews: z.number(),
urls: z.number(),
urlViews: z.number(),
storage: z.number(),
filesUsers: z.array(
z.object({
username: z.string(),
sum: z.number(),
storage: z.number(),
views: z.number(),
}),
),
urlsUsers: z.array(
z.object({
username: z.string(),
sum: z.number(),
views: z.number(),
}),
),
types: z.array(
z.object({
type: z.string(),
sum: z.number(),
}),
),
});
export function percentChange(a: number, b: number): number {
return ((b - a) / a) * 100;
}

View file

@ -99,7 +99,7 @@ export class Scheduler {
});
}
public addInterval(id: string, interval: number, func: () => void, start: boolean = false): void {
public interval(id: string, interval: number, func: () => void, start: boolean = false): void {
const len = this.jobs.push({
id,
interval,
@ -110,7 +110,7 @@ export class Scheduler {
if (start) this.startInterval(this.jobs[len - 1] as IntervalJob);
}
public addWorker<Data = any>(id: string, path: string, data: Data, start: boolean = false): void {
public worker<Data = any>(id: string, path: string, data: Data, start: boolean = false): void {
const len = this.jobs.push({
id,
path,

View file

@ -0,0 +1,19 @@
import { queryStats } from '@/lib/stats';
import { IntervalJob } from '..';
export default function metrics(prisma: typeof globalThis.__db__) {
return async function (this: IntervalJob) {
const stats = await queryStats();
const metric = await prisma.metric.create({
data: {
data: stats,
},
});
this.logger.debug('created metric', {
id: metric.id,
metric: stats,
});
};
}

85
src/lib/stats.ts Normal file
View file

@ -0,0 +1,85 @@
import { prisma } from './db';
import { MetricData } from './db/models/metric';
export async function queryStats(): Promise<MetricData> {
const file = await prisma.file.aggregate({
_sum: {
views: true,
size: true,
},
_count: true,
});
const url = await prisma.url.aggregate({
_sum: {
views: true,
},
_count: true,
});
const user = await prisma.user.aggregate({
_count: true,
});
const filesByUser = await prisma.file.groupBy({
by: ['userId'],
_count: true,
_sum: {
views: true,
size: true,
},
});
const urlsByUser = await prisma.url.groupBy({
by: ['userId'],
_count: true,
_sum: {
views: true,
},
});
for (let i = 0; i !== filesByUser.length; ++i) {
const user = await prisma.user.findUnique({
where: {
id: filesByUser[i].userId!,
},
});
filesByUser[i].userId = user?.username || 'unknown';
}
for (let i = 0; i !== urlsByUser.length; ++i) {
const user = await prisma.user.findUnique({
where: {
id: urlsByUser[i].userId!,
},
});
urlsByUser[i].userId = user?.username || 'unknown';
}
const types = await prisma.file.groupBy({
by: ['type'],
_count: true,
});
return {
files: file._count,
urls: url._count,
users: user._count,
storage: file._sum.size!,
fileViews: file._sum.views!,
urlViews: url._sum.views!,
filesUsers: filesByUser.map((x) => ({
username: x.userId!,
sum: x._count,
storage: x._sum.size!,
views: x._sum.views!,
})),
urlsUsers: urlsByUser.map((x) => ({ username: x.userId!, sum: x._count, views: x._sum.views! })),
types: types.map((x) => ({ type: x.type!, sum: x._count })),
};
}

40
src/pages/api/stats.ts Normal file
View file

@ -0,0 +1,40 @@
import { config } from '@/lib/config';
import { prisma } from '@/lib/db';
import { Metric } from '@/lib/db/models/metric';
import { combine } from '@/lib/middleware/combine';
import { method } from '@/lib/middleware/method';
import { NextApiReq, NextApiRes } from '@/lib/response';
export type ApiStatsResponse = Metric[];
type Query = {
from?: string;
to?: string;
};
export async function handler(req: NextApiReq<any, Query>, res: NextApiRes<ApiStatsResponse>) {
if (!config.features.metrics) return res.forbidden();
const { from, to } = req.query;
const fromDate = from ? new Date(from) : new Date(Date.now() - 86400000);
const toDate = to ? new Date(to) : new Date();
if (isNaN(fromDate.getTime()) || isNaN(toDate.getTime())) return res.badRequest('invalid date');
const stats = await prisma.metric.findMany({
where: {
createdAt: {
gte: fromDate,
lte: toDate,
},
},
orderBy: {
createdAt: 'desc',
},
});
return res.ok(stats);
}
export default combine([method(['GET'])], handler);

View file

@ -0,0 +1,23 @@
import Layout from '@/components/Layout';
import DashboardMetrics from '@/components/pages/metrics';
import useLogin from '@/lib/hooks/useLogin';
import { withSafeConfig } from '@/lib/middleware/next/withSafeConfig';
import { LoadingOverlay } from '@mantine/core';
import { InferGetServerSidePropsType } from 'next';
import { useRouter } from 'next/router';
export default function DashboardIndex({ config }: InferGetServerSidePropsType<typeof getServerSideProps>) {
const router = useRouter();
const { loading } = useLogin();
if (loading) return <LoadingOverlay visible />;
if (config.features.metrics === false) return router.push('/dashboard');
return (
<Layout config={config}>
<DashboardMetrics />
</Layout>
);
}
export const getServerSideProps = withSafeConfig();

View file

@ -18,6 +18,7 @@ import deleteFiles from '@/lib/scheduler/jobs/deleteFiles';
import clearInvites from '@/lib/scheduler/jobs/clearInvites';
import maxViews from '@/lib/scheduler/jobs/maxViews';
import thumbnails from '@/lib/scheduler/jobs/thumbnails';
import metrics from '@/lib/scheduler/jobs/metrics';
const MODE = process.env.NODE_ENV || 'production';
@ -128,19 +129,23 @@ async function main() {
port: config.core.port,
});
scheduler.addInterval('deletefiles', config.scheduler.deleteInterval, deleteFiles(prisma));
scheduler.addInterval('maxviews', config.scheduler.maxViewsInterval, maxViews(prisma));
scheduler.addInterval('thumbnails', config.scheduler.thumbnailsInterval, thumbnails(prisma));
scheduler.interval('deletefiles', config.scheduler.deleteInterval, deleteFiles(prisma));
scheduler.interval('maxviews', config.scheduler.maxViewsInterval, maxViews(prisma));
if (config.features.metrics)
scheduler.interval('metrics', config.scheduler.metricsInterval, metrics(prisma));
if (config.features.thumbnails.enabled) {
scheduler.interval('thumbnails', config.scheduler.thumbnailsInterval, thumbnails(prisma));
for (let i = 0; i !== config.features.thumbnails.num_threads; ++i) {
scheduler.addWorker(`thumbnail-${i}`, './build/offload/thumbnails.js', {
scheduler.worker(`thumbnail-${i}`, './build/offload/thumbnails.js', {
id: `thumbnail-${i}`,
enabled: config.features.thumbnails.enabled,
});
}
scheduler.addInterval('clearinvites', config.scheduler.clearInvitesInterval, clearInvites(prisma));
scheduler.interval('clearinvites', config.scheduler.clearInvitesInterval, clearInvites(prisma));
}
scheduler.start();