mirror of
https://github.com/diced/zipline.git
synced 2025-05-11 18:36:02 +02:00
feat: metrics/stats page
This commit is contained in:
parent
2a2ffaaffe
commit
01f94245c8
22 changed files with 1204 additions and 11 deletions
|
@ -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
526
pnpm-lock.yaml
generated
|
@ -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'}
|
||||
|
|
|
@ -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' />,
|
||||
|
|
89
src/components/pages/metrics/index.tsx
Normal file
89
src/components/pages/metrics/index.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
40
src/components/pages/metrics/parts/FilesUrlsCountGraph.tsx
Normal file
40
src/components/pages/metrics/parts/FilesUrlsCountGraph.tsx
Normal 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>
|
||||
);
|
||||
}
|
50
src/components/pages/metrics/parts/StatsCards.tsx
Normal file
50
src/components/pages/metrics/parts/StatsCards.tsx
Normal 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>
|
||||
);
|
||||
}
|
83
src/components/pages/metrics/parts/StatsTables.tsx
Normal file
83
src/components/pages/metrics/parts/StatsTables.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
38
src/components/pages/metrics/parts/StorageGraph.tsx
Normal file
38
src/components/pages/metrics/parts/StorageGraph.tsx
Normal 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>
|
||||
);
|
||||
}
|
41
src/components/pages/metrics/parts/TypesPieChart.tsx
Normal file
41
src/components/pages/metrics/parts/TypesPieChart.tsx
Normal 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' }]}
|
||||
/>
|
||||
);
|
||||
}
|
45
src/components/pages/metrics/parts/ViewsGraph.tsx
Normal file
45
src/components/pages/metrics/parts/ViewsGraph.tsx
Normal 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>
|
||||
);
|
||||
}
|
40
src/components/pages/metrics/useStats.tsx
Normal file
40
src/components/pages/metrics/useStats.tsx
Normal 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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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();
|
||||
|
|
45
src/lib/db/models/metric.ts
Normal file
45
src/lib/db/models/metric.ts
Normal 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;
|
||||
}
|
|
@ -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,
|
||||
|
|
19
src/lib/scheduler/jobs/metrics.ts
Normal file
19
src/lib/scheduler/jobs/metrics.ts
Normal 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
85
src/lib/stats.ts
Normal 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
40
src/pages/api/stats.ts
Normal 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);
|
23
src/pages/dashboard/metrics.tsx
Normal file
23
src/pages/dashboard/metrics.tsx
Normal 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();
|
|
@ -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();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue