mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Merge branch 'dev' into dependabot/npm_and_yarn/next-15.5.10
This commit is contained in:
commit
9e7285cf2b
160 changed files with 3216 additions and 859 deletions
10
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
10
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
|
|
@ -24,13 +24,13 @@ body:
|
||||||
render: shell
|
render: shell
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Which Umami version are you using? (if relevant)
|
label: Which Umami version are you using?
|
||||||
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
|
description: 'For example: 2.18.0, 2.15.1, 1.39.0, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: Which browser are you using? (if relevant)
|
label: How are you deploying your application?
|
||||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
description: 'For example: Vercel, Railway, Docker, etc'
|
||||||
- type: input
|
- type: input
|
||||||
attributes:
|
attributes:
|
||||||
label: How are you deploying your application? (if relevant)
|
label: Which browser are you using?
|
||||||
description: 'For example: Vercel, Railway, Docker, etc'
|
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||||
|
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -21,6 +21,7 @@ package-lock.json
|
||||||
/dist
|
/dist
|
||||||
/generated
|
/generated
|
||||||
/src/generated
|
/src/generated
|
||||||
|
pm2.yml
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
@ -30,6 +31,8 @@ package-lock.json
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
.claude
|
||||||
|
nul
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const cloudMode = process.env.CLOUD_MODE || '';
|
||||||
const cloudUrl = process.env.CLOUD_URL || '';
|
const cloudUrl = process.env.CLOUD_URL || '';
|
||||||
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
||||||
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
||||||
|
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
|
||||||
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
||||||
const forceSSL = process.env.FORCE_SSL || '';
|
const forceSSL = process.env.FORCE_SSL || '';
|
||||||
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||||
|
|
@ -170,6 +171,7 @@ export default {
|
||||||
cloudMode,
|
cloudMode,
|
||||||
cloudUrl,
|
cloudUrl,
|
||||||
currentVersion: pkg.version,
|
currentVersion: pkg.version,
|
||||||
|
defaultCurrency,
|
||||||
defaultLocale,
|
defaultLocale,
|
||||||
},
|
},
|
||||||
basePath,
|
basePath,
|
||||||
|
|
|
||||||
10
package.json
10
package.json
|
|
@ -11,7 +11,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3001 --turbo",
|
"dev": "next dev -p 3002 --turbo",
|
||||||
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||||
|
|
@ -97,7 +97,7 @@
|
||||||
"is-docker": "^3.0.0",
|
"is-docker": "^3.0.0",
|
||||||
"is-localhost-ip": "^2.0.0",
|
"is-localhost-ip": "^2.0.0",
|
||||||
"isbot": "^5.1.31",
|
"isbot": "^5.1.31",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"jszip": "^3.10.1",
|
"jszip": "^3.10.1",
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"lucide-react": "^0.543.0",
|
"lucide-react": "^0.543.0",
|
||||||
|
|
@ -143,8 +143,8 @@
|
||||||
"@types/react-window": "^1.8.8",
|
"@types/react-window": "^1.8.8",
|
||||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||||
"cross-env": "^10.1.0",
|
"cross-env": "^10.1.0",
|
||||||
"cypress": "^13.6.6",
|
"cypress": "^15.8.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^5.0.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"lint-staged": "^16.2.6",
|
"lint-staged": "^16.2.6",
|
||||||
|
|
@ -164,7 +164,7 @@
|
||||||
"stylelint-config-css-modules": "^4.5.1",
|
"stylelint-config-css-modules": "^4.5.1",
|
||||||
"stylelint-config-prettier": "^9.0.3",
|
"stylelint-config-prettier": "^9.0.3",
|
||||||
"stylelint-config-recommended": "^14.0.0",
|
"stylelint-config-recommended": "^14.0.0",
|
||||||
"tar": "^6.1.2",
|
"tar": "^7.5.4",
|
||||||
"ts-jest": "^29.4.6",
|
"ts-jest": "^29.4.6",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsup": "^8.5.0",
|
"tsup": "^8.5.0",
|
||||||
|
|
|
||||||
278
pnpm-lock.yaml
generated
278
pnpm-lock.yaml
generated
|
|
@ -117,8 +117,8 @@ importers:
|
||||||
specifier: ^5.1.31
|
specifier: ^5.1.31
|
||||||
version: 5.1.32
|
version: 5.1.32
|
||||||
jsonwebtoken:
|
jsonwebtoken:
|
||||||
specifier: ^9.0.2
|
specifier: ^9.0.3
|
||||||
version: 9.0.2
|
version: 9.0.3
|
||||||
jszip:
|
jszip:
|
||||||
specifier: ^3.10.1
|
specifier: ^3.10.1
|
||||||
version: 3.10.1
|
version: 3.10.1
|
||||||
|
|
@ -250,11 +250,11 @@ importers:
|
||||||
specifier: ^10.1.0
|
specifier: ^10.1.0
|
||||||
version: 10.1.0
|
version: 10.1.0
|
||||||
cypress:
|
cypress:
|
||||||
specifier: ^13.6.6
|
specifier: ^15.8.0
|
||||||
version: 13.17.0
|
version: 15.9.0
|
||||||
extract-react-intl-messages:
|
extract-react-intl-messages:
|
||||||
specifier: ^4.1.1
|
specifier: ^5.0.0
|
||||||
version: 4.1.1(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
|
version: 5.0.0(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
|
||||||
husky:
|
husky:
|
||||||
specifier: ^9.1.7
|
specifier: ^9.1.7
|
||||||
version: 9.1.7
|
version: 9.1.7
|
||||||
|
|
@ -313,8 +313,8 @@ importers:
|
||||||
specifier: ^14.0.0
|
specifier: ^14.0.0
|
||||||
version: 14.0.1(stylelint@15.11.0(typescript@5.9.3))
|
version: 14.0.1(stylelint@15.11.0(typescript@5.9.3))
|
||||||
tar:
|
tar:
|
||||||
specifier: ^6.1.2
|
specifier: ^7.5.4
|
||||||
version: 6.2.1
|
version: 7.5.4
|
||||||
ts-jest:
|
ts-jest:
|
||||||
specifier: ^29.4.6
|
specifier: ^29.4.6
|
||||||
version: 29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3)
|
version: 29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3)
|
||||||
|
|
@ -566,10 +566,6 @@ packages:
|
||||||
resolution: {integrity: sha512-co2spjR7wZoZ3Ck0H/jv76bpiuO3oJHtOmq9/gxFiod2DcT9NFg01u/hXcG8MJFnEJuMB6e3vGqS6IOnLwHqRw==}
|
resolution: {integrity: sha512-co2spjR7wZoZ3Ck0H/jv76bpiuO3oJHtOmq9/gxFiod2DcT9NFg01u/hXcG8MJFnEJuMB6e3vGqS6IOnLwHqRw==}
|
||||||
engines: {node: '>=16'}
|
engines: {node: '>=16'}
|
||||||
|
|
||||||
'@colors/colors@1.5.0':
|
|
||||||
resolution: {integrity: sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==}
|
|
||||||
engines: {node: '>=0.1.90'}
|
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
@ -687,8 +683,8 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
postcss-selector-parser: ^6.0.13
|
postcss-selector-parser: ^6.0.13
|
||||||
|
|
||||||
'@cypress/request@3.0.9':
|
'@cypress/request@3.0.10':
|
||||||
resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==}
|
resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
'@cypress/xvfb@1.2.4':
|
'@cypress/xvfb@1.2.4':
|
||||||
|
|
@ -1442,6 +1438,10 @@ packages:
|
||||||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
|
||||||
'@istanbuljs/load-nyc-config@1.1.0':
|
'@istanbuljs/load-nyc-config@1.1.0':
|
||||||
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
@ -2811,6 +2811,9 @@ packages:
|
||||||
'@types/stack-utils@2.0.3':
|
'@types/stack-utils@2.0.3':
|
||||||
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==}
|
||||||
|
|
||||||
|
'@types/tmp@0.2.6':
|
||||||
|
resolution: {integrity: sha512-chhaNf2oKHlRkDGt+tiKE2Z5aJ6qalm7Z9rlLdBwmOiAAf09YQvvoLXjWK4HWPF1xU/fqvMgfNfpVoBscA/tKA==}
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6':
|
'@types/use-sync-external-store@0.0.6':
|
||||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||||
|
|
||||||
|
|
@ -2972,9 +2975,6 @@ packages:
|
||||||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
async@3.2.6:
|
|
||||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
|
||||||
|
|
||||||
asynckit@0.4.0:
|
asynckit@0.4.0:
|
||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
|
|
||||||
|
|
@ -3183,17 +3183,13 @@ packages:
|
||||||
chart.js: '>=2.8.0'
|
chart.js: '>=2.8.0'
|
||||||
date-fns: '>=2.0.0'
|
date-fns: '>=2.0.0'
|
||||||
|
|
||||||
check-more-types@2.24.0:
|
|
||||||
resolution: {integrity: sha512-Pj779qHxV2tuapviy1bSZNEL1maXr13bPYpsvSDB68HlYcYuhlDrmGd63i0JHMCLKzc7rUSNIrpdJlhVlNwrxA==}
|
|
||||||
engines: {node: '>= 0.8.0'}
|
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
chownr@2.0.0:
|
chownr@3.0.0:
|
||||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
ci-info@3.9.0:
|
ci-info@3.9.0:
|
||||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||||
|
|
@ -3224,8 +3220,8 @@ packages:
|
||||||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
cli-table3@0.6.5:
|
cli-table3@0.6.1:
|
||||||
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
|
resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==}
|
||||||
engines: {node: 10.* || >= 12.*}
|
engines: {node: 10.* || >= 12.*}
|
||||||
|
|
||||||
cli-truncate@2.1.0:
|
cli-truncate@2.1.0:
|
||||||
|
|
@ -3280,6 +3276,10 @@ packages:
|
||||||
colorette@2.0.20:
|
colorette@2.0.20:
|
||||||
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==}
|
||||||
|
|
||||||
|
colors@1.4.0:
|
||||||
|
resolution: {integrity: sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==}
|
||||||
|
engines: {node: '>=0.1.90'}
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
@ -3478,9 +3478,9 @@ packages:
|
||||||
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
|
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
cypress@13.17.0:
|
cypress@15.9.0:
|
||||||
resolution: {integrity: sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==}
|
resolution: {integrity: sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==}
|
||||||
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
|
engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
d3-array@2.12.1:
|
d3-array@2.12.1:
|
||||||
|
|
@ -3845,9 +3845,9 @@ packages:
|
||||||
extend@3.0.2:
|
extend@3.0.2:
|
||||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||||
|
|
||||||
extract-react-intl-messages@4.1.1:
|
extract-react-intl-messages@5.0.0:
|
||||||
resolution: {integrity: sha512-dPogci5X7HVtV7VbUxajH/1YgfNRaW2VtEiVidZ/31Tq8314uzOtzVMNo0IrAPD2E+H1wHoPiu/j565TZsyIZg==}
|
resolution: {integrity: sha512-7K1aA3WxhhjBXsuZ2buZm5MLuPHjzkbErV2qqhf0m0K9RMqdwe6mYrOAMZ+1z1bfrngwQ2Iv44+RLjILO8qPdA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=20'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
extract-zip@2.0.1:
|
extract-zip@2.0.1:
|
||||||
|
|
@ -3906,6 +3906,10 @@ packages:
|
||||||
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
|
file-entry-cache@5.0.1:
|
||||||
|
resolution: {integrity: sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
file-entry-cache@7.0.2:
|
file-entry-cache@7.0.2:
|
||||||
resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==}
|
resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
@ -3925,6 +3929,10 @@ packages:
|
||||||
fix-dts-default-cjs-exports@1.0.1:
|
fix-dts-default-cjs-exports@1.0.1:
|
||||||
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==}
|
||||||
|
|
||||||
|
flat-cache@2.0.1:
|
||||||
|
resolution: {integrity: sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
flat-cache@3.2.0:
|
flat-cache@3.2.0:
|
||||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||||
engines: {node: ^10.12.0 || >=12.0.0}
|
engines: {node: ^10.12.0 || >=12.0.0}
|
||||||
|
|
@ -3933,6 +3941,9 @@ packages:
|
||||||
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
|
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
flatted@2.0.2:
|
||||||
|
resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==}
|
||||||
|
|
||||||
flatted@3.3.3:
|
flatted@3.3.3:
|
||||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||||
|
|
||||||
|
|
@ -3974,10 +3985,6 @@ packages:
|
||||||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
fs-minipass@2.1.0:
|
|
||||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
|
||||||
engines: {node: '>= 8'}
|
|
||||||
|
|
||||||
fs.realpath@1.0.0:
|
fs.realpath@1.0.0:
|
||||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||||
|
|
||||||
|
|
@ -4042,9 +4049,6 @@ packages:
|
||||||
get-tsconfig@4.13.0:
|
get-tsconfig@4.13.0:
|
||||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||||
|
|
||||||
getos@3.2.1:
|
|
||||||
resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
|
|
||||||
|
|
||||||
getpass@0.1.7:
|
getpass@0.1.7:
|
||||||
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
||||||
|
|
||||||
|
|
@ -4147,6 +4151,10 @@ packages:
|
||||||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
hasha@5.2.2:
|
||||||
|
resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
@ -4730,8 +4738,8 @@ packages:
|
||||||
jsonify@0.0.1:
|
jsonify@0.0.1:
|
||||||
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
|
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
jsonwebtoken@9.0.3:
|
||||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
||||||
engines: {node: '>=12', npm: '>=6'}
|
engines: {node: '>=12', npm: '>=6'}
|
||||||
|
|
||||||
jsprim@2.0.2:
|
jsprim@2.0.2:
|
||||||
|
|
@ -4741,11 +4749,11 @@ packages:
|
||||||
jszip@3.10.1:
|
jszip@3.10.1:
|
||||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||||
|
|
||||||
jwa@1.4.2:
|
jwa@2.0.1:
|
||||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||||
|
|
||||||
jws@3.2.2:
|
jws@4.0.1:
|
||||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||||
|
|
||||||
kafkajs@2.2.4:
|
kafkajs@2.2.4:
|
||||||
resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==}
|
resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==}
|
||||||
|
|
@ -4768,10 +4776,6 @@ packages:
|
||||||
known-css-properties@0.36.0:
|
known-css-properties@0.36.0:
|
||||||
resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==}
|
resolution: {integrity: sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==}
|
||||||
|
|
||||||
lazy-ass@1.6.0:
|
|
||||||
resolution: {integrity: sha512-cc8oEVoctTvsFZ/Oje/kGnHbpWHYBe8IAJe4C0QNc3t8uM/0Y8+erSz/7Y1ALuXTEZTMvxXwO6YbX1ey3ujiZw==}
|
|
||||||
engines: {node: '> 0.8'}
|
|
||||||
|
|
||||||
leven@3.1.0:
|
leven@3.1.0:
|
||||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
@ -4865,10 +4869,6 @@ packages:
|
||||||
lodash.once@4.1.1:
|
lodash.once@4.1.1:
|
||||||
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
|
||||||
|
|
||||||
lodash.pick@4.4.0:
|
|
||||||
resolution: {integrity: sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q==}
|
|
||||||
deprecated: This package is deprecated. Use destructuring assignment syntax instead.
|
|
||||||
|
|
||||||
lodash.truncate@4.4.2:
|
lodash.truncate@4.4.2:
|
||||||
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
|
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
|
||||||
|
|
||||||
|
|
@ -5054,21 +5054,17 @@ packages:
|
||||||
minimist@1.2.8:
|
minimist@1.2.8:
|
||||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||||
|
|
||||||
minipass@3.3.6:
|
|
||||||
resolution: {integrity: sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
minipass@5.0.0:
|
|
||||||
resolution: {integrity: sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==}
|
|
||||||
engines: {node: '>=8'}
|
|
||||||
|
|
||||||
minipass@7.1.2:
|
minipass@7.1.2:
|
||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
minizlib@2.1.2:
|
minizlib@3.1.0:
|
||||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 18'}
|
||||||
|
|
||||||
|
mkdirp@0.5.6:
|
||||||
|
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
mkdirp@1.0.4:
|
mkdirp@1.0.4:
|
||||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
|
|
@ -5952,8 +5948,8 @@ packages:
|
||||||
pure-rand@7.0.1:
|
pure-rand@7.0.1:
|
||||||
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
|
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
|
||||||
|
|
||||||
qs@6.14.0:
|
qs@6.14.1:
|
||||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||||
engines: {node: '>=0.6'}
|
engines: {node: '>=0.6'}
|
||||||
|
|
||||||
queue-microtask@1.2.3:
|
queue-microtask@1.2.3:
|
||||||
|
|
@ -6177,6 +6173,11 @@ packages:
|
||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
|
rimraf@2.6.3:
|
||||||
|
resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==}
|
||||||
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||||
|
|
@ -6603,14 +6604,19 @@ packages:
|
||||||
engines: {node: '>=14.0.0'}
|
engines: {node: '>=14.0.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
systeminformation@5.30.5:
|
||||||
|
resolution: {integrity: sha512-DpWmpCckhwR3hG+6udb6/aQB7PpiqVnvSljrjbKxNSvTRsGsg7NVE3/vouoYf96xgwMxXFKcS4Ux+cnkFwYM7A==}
|
||||||
|
engines: {node: '>=8.0.0'}
|
||||||
|
os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android]
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
table@6.9.0:
|
table@6.9.0:
|
||||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
|
||||||
tar@6.2.1:
|
tar@7.5.4:
|
||||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=18'}
|
||||||
deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exhorbitant rates) by contacting i@izs.me
|
|
||||||
|
|
||||||
terser@5.43.1:
|
terser@5.43.1:
|
||||||
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
|
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
|
||||||
|
|
@ -6995,6 +7001,10 @@ packages:
|
||||||
resolution: {integrity: sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==}
|
resolution: {integrity: sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==}
|
||||||
engines: {node: '>=8.3'}
|
engines: {node: '>=8.3'}
|
||||||
|
|
||||||
|
write@1.0.3:
|
||||||
|
resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==}
|
||||||
|
engines: {node: '>=4'}
|
||||||
|
|
||||||
xtend@4.0.2:
|
xtend@4.0.2:
|
||||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||||
engines: {node: '>=0.4'}
|
engines: {node: '>=0.4'}
|
||||||
|
|
@ -7009,6 +7019,10 @@ packages:
|
||||||
yallist@4.0.0:
|
yallist@4.0.0:
|
||||||
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
|
||||||
|
|
||||||
|
yallist@5.0.0:
|
||||||
|
resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
yaml@1.10.2:
|
yaml@1.10.2:
|
||||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
@ -7305,9 +7319,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@clickhouse/client-common': 1.14.0
|
'@clickhouse/client-common': 1.14.0
|
||||||
|
|
||||||
'@colors/colors@1.5.0':
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
'@cspotcode/source-map-support@0.8.1':
|
'@cspotcode/source-map-support@0.8.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/trace-mapping': 0.3.9
|
'@jridgewell/trace-mapping': 0.3.9
|
||||||
|
|
@ -7405,7 +7416,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
postcss-selector-parser: 6.1.2
|
postcss-selector-parser: 6.1.2
|
||||||
|
|
||||||
'@cypress/request@3.0.9':
|
'@cypress/request@3.0.10':
|
||||||
dependencies:
|
dependencies:
|
||||||
aws-sign2: 0.7.0
|
aws-sign2: 0.7.0
|
||||||
aws4: 1.13.2
|
aws4: 1.13.2
|
||||||
|
|
@ -7420,7 +7431,7 @@ snapshots:
|
||||||
json-stringify-safe: 5.0.1
|
json-stringify-safe: 5.0.1
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
performance-now: 2.1.0
|
performance-now: 2.1.0
|
||||||
qs: 6.14.0
|
qs: 6.14.1
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
tough-cookie: 5.1.2
|
tough-cookie: 5.1.2
|
||||||
tunnel-agent: 0.6.0
|
tunnel-agent: 0.6.0
|
||||||
|
|
@ -8011,6 +8022,10 @@ snapshots:
|
||||||
wrap-ansi: 8.1.0
|
wrap-ansi: 8.1.0
|
||||||
wrap-ansi-cjs: wrap-ansi@7.0.0
|
wrap-ansi-cjs: wrap-ansi@7.0.0
|
||||||
|
|
||||||
|
'@isaacs/fs-minipass@4.0.1':
|
||||||
|
dependencies:
|
||||||
|
minipass: 7.1.2
|
||||||
|
|
||||||
'@istanbuljs/load-nyc-config@1.1.0':
|
'@istanbuljs/load-nyc-config@1.1.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
camelcase: 5.3.1
|
camelcase: 5.3.1
|
||||||
|
|
@ -9864,6 +9879,8 @@ snapshots:
|
||||||
|
|
||||||
'@types/stack-utils@2.0.3': {}
|
'@types/stack-utils@2.0.3': {}
|
||||||
|
|
||||||
|
'@types/tmp@0.2.6': {}
|
||||||
|
|
||||||
'@types/use-sync-external-store@0.0.6': {}
|
'@types/use-sync-external-store@0.0.6': {}
|
||||||
|
|
||||||
'@types/yargs-parser@21.0.3': {}
|
'@types/yargs-parser@21.0.3': {}
|
||||||
|
|
@ -10070,8 +10087,6 @@ snapshots:
|
||||||
|
|
||||||
async-function@1.0.0: {}
|
async-function@1.0.0: {}
|
||||||
|
|
||||||
async@3.2.6: {}
|
|
||||||
|
|
||||||
asynckit@0.4.0: {}
|
asynckit@0.4.0: {}
|
||||||
|
|
||||||
at-least-node@1.0.0: {}
|
at-least-node@1.0.0: {}
|
||||||
|
|
@ -10323,13 +10338,11 @@ snapshots:
|
||||||
chart.js: 4.5.1
|
chart.js: 4.5.1
|
||||||
date-fns: 2.30.0
|
date-fns: 2.30.0
|
||||||
|
|
||||||
check-more-types@2.24.0: {}
|
|
||||||
|
|
||||||
chokidar@4.0.3:
|
chokidar@4.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
chownr@2.0.0: {}
|
chownr@3.0.0: {}
|
||||||
|
|
||||||
ci-info@3.9.0: {}
|
ci-info@3.9.0: {}
|
||||||
|
|
||||||
|
|
@ -10353,11 +10366,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
restore-cursor: 5.1.0
|
restore-cursor: 5.1.0
|
||||||
|
|
||||||
cli-table3@0.6.5:
|
cli-table3@0.6.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@colors/colors': 1.5.0
|
colors: 1.4.0
|
||||||
|
|
||||||
cli-truncate@2.1.0:
|
cli-truncate@2.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -10403,6 +10416,9 @@ snapshots:
|
||||||
|
|
||||||
colorette@2.0.20: {}
|
colorette@2.0.20: {}
|
||||||
|
|
||||||
|
colors@1.4.0:
|
||||||
|
optional: true
|
||||||
|
|
||||||
combined-stream@1.0.8:
|
combined-stream@1.0.8:
|
||||||
dependencies:
|
dependencies:
|
||||||
delayed-stream: 1.0.0
|
delayed-stream: 1.0.0
|
||||||
|
|
@ -10618,22 +10634,22 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
array-find-index: 1.0.2
|
array-find-index: 1.0.2
|
||||||
|
|
||||||
cypress@13.17.0:
|
cypress@15.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@cypress/request': 3.0.9
|
'@cypress/request': 3.0.10
|
||||||
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
|
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
|
||||||
'@types/sinonjs__fake-timers': 8.1.1
|
'@types/sinonjs__fake-timers': 8.1.1
|
||||||
'@types/sizzle': 2.3.9
|
'@types/sizzle': 2.3.9
|
||||||
|
'@types/tmp': 0.2.6
|
||||||
arch: 2.2.0
|
arch: 2.2.0
|
||||||
blob-util: 2.0.2
|
blob-util: 2.0.2
|
||||||
bluebird: 3.7.2
|
bluebird: 3.7.2
|
||||||
buffer: 5.7.1
|
buffer: 5.7.1
|
||||||
cachedir: 2.4.0
|
cachedir: 2.4.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
check-more-types: 2.24.0
|
|
||||||
ci-info: 4.3.0
|
ci-info: 4.3.0
|
||||||
cli-cursor: 3.1.0
|
cli-cursor: 3.1.0
|
||||||
cli-table3: 0.6.5
|
cli-table3: 0.6.1
|
||||||
commander: 6.2.1
|
commander: 6.2.1
|
||||||
common-tags: 1.8.2
|
common-tags: 1.8.2
|
||||||
dayjs: 1.11.13
|
dayjs: 1.11.13
|
||||||
|
|
@ -10645,9 +10661,8 @@ snapshots:
|
||||||
extract-zip: 2.0.1(supports-color@8.1.1)
|
extract-zip: 2.0.1(supports-color@8.1.1)
|
||||||
figures: 3.2.0
|
figures: 3.2.0
|
||||||
fs-extra: 9.1.0
|
fs-extra: 9.1.0
|
||||||
getos: 3.2.1
|
hasha: 5.2.2
|
||||||
is-installed-globally: 0.4.0
|
is-installed-globally: 0.4.0
|
||||||
lazy-ass: 1.6.0
|
|
||||||
listr2: 3.14.0(enquirer@2.4.1)
|
listr2: 3.14.0(enquirer@2.4.1)
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
log-symbols: 4.1.0
|
log-symbols: 4.1.0
|
||||||
|
|
@ -10657,8 +10672,8 @@ snapshots:
|
||||||
process: 0.11.10
|
process: 0.11.10
|
||||||
proxy-from-env: 1.0.0
|
proxy-from-env: 1.0.0
|
||||||
request-progress: 3.0.0
|
request-progress: 3.0.0
|
||||||
semver: 7.7.3
|
|
||||||
supports-color: 8.1.1
|
supports-color: 8.1.1
|
||||||
|
systeminformation: 5.30.5
|
||||||
tmp: 0.2.5
|
tmp: 0.2.5
|
||||||
tree-kill: 1.2.2
|
tree-kill: 1.2.2
|
||||||
untildify: 4.0.0
|
untildify: 4.0.0
|
||||||
|
|
@ -11131,17 +11146,17 @@ snapshots:
|
||||||
|
|
||||||
extend@3.0.2: {}
|
extend@3.0.2: {}
|
||||||
|
|
||||||
extract-react-intl-messages@4.1.1(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3)):
|
extract-react-intl-messages@5.0.0(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/core': 7.28.3
|
'@babel/core': 7.28.3
|
||||||
babel-plugin-react-intl: 7.9.4(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
|
babel-plugin-react-intl: 7.9.4(ts-jest@29.4.6(@babel/core@7.28.3)(@jest/transform@29.7.0)(@jest/types@30.0.5)(babel-jest@29.7.0(@babel/core@7.28.3))(esbuild@0.25.12)(jest-util@30.0.5)(jest@29.7.0(@types/node@24.10.1)(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(typescript@5.9.3))
|
||||||
|
file-entry-cache: 5.0.1
|
||||||
flat: 5.0.2
|
flat: 5.0.2
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
js-yaml: 3.14.1
|
js-yaml: 3.14.1
|
||||||
load-json-file: 6.2.0
|
load-json-file: 6.2.0
|
||||||
lodash.merge: 4.6.2
|
lodash.merge: 4.6.2
|
||||||
lodash.mergewith: 4.6.2
|
lodash.mergewith: 4.6.2
|
||||||
lodash.pick: 4.4.0
|
|
||||||
meow: 6.1.1
|
meow: 6.1.1
|
||||||
mkdirp: 1.0.4
|
mkdirp: 1.0.4
|
||||||
pify: 5.0.0
|
pify: 5.0.0
|
||||||
|
|
@ -11209,6 +11224,10 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
escape-string-regexp: 1.0.5
|
escape-string-regexp: 1.0.5
|
||||||
|
|
||||||
|
file-entry-cache@5.0.1:
|
||||||
|
dependencies:
|
||||||
|
flat-cache: 2.0.1
|
||||||
|
|
||||||
file-entry-cache@7.0.2:
|
file-entry-cache@7.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
flat-cache: 3.2.0
|
flat-cache: 3.2.0
|
||||||
|
|
@ -11233,6 +11252,12 @@ snapshots:
|
||||||
mlly: 1.8.0
|
mlly: 1.8.0
|
||||||
rollup: 4.53.3
|
rollup: 4.53.3
|
||||||
|
|
||||||
|
flat-cache@2.0.1:
|
||||||
|
dependencies:
|
||||||
|
flatted: 2.0.2
|
||||||
|
rimraf: 2.6.3
|
||||||
|
write: 1.0.3
|
||||||
|
|
||||||
flat-cache@3.2.0:
|
flat-cache@3.2.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
flatted: 3.3.3
|
flatted: 3.3.3
|
||||||
|
|
@ -11241,6 +11266,8 @@ snapshots:
|
||||||
|
|
||||||
flat@5.0.2: {}
|
flat@5.0.2: {}
|
||||||
|
|
||||||
|
flatted@2.0.2: {}
|
||||||
|
|
||||||
flatted@3.3.3: {}
|
flatted@3.3.3: {}
|
||||||
|
|
||||||
for-each@0.3.5:
|
for-each@0.3.5:
|
||||||
|
|
@ -11293,10 +11320,6 @@ snapshots:
|
||||||
jsonfile: 6.2.0
|
jsonfile: 6.2.0
|
||||||
universalify: 2.0.1
|
universalify: 2.0.1
|
||||||
|
|
||||||
fs-minipass@2.1.0:
|
|
||||||
dependencies:
|
|
||||||
minipass: 3.3.6
|
|
||||||
|
|
||||||
fs.realpath@1.0.0: {}
|
fs.realpath@1.0.0: {}
|
||||||
|
|
||||||
fsevents@2.3.3:
|
fsevents@2.3.3:
|
||||||
|
|
@ -11363,10 +11386,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
getos@3.2.1:
|
|
||||||
dependencies:
|
|
||||||
async: 3.2.6
|
|
||||||
|
|
||||||
getpass@0.1.7:
|
getpass@0.1.7:
|
||||||
dependencies:
|
dependencies:
|
||||||
assert-plus: 1.0.0
|
assert-plus: 1.0.0
|
||||||
|
|
@ -11501,6 +11520,11 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
has-symbols: 1.1.0
|
has-symbols: 1.1.0
|
||||||
|
|
||||||
|
hasha@5.2.2:
|
||||||
|
dependencies:
|
||||||
|
is-stream: 2.0.1
|
||||||
|
type-fest: 0.8.1
|
||||||
|
|
||||||
hasown@2.0.2:
|
hasown@2.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
|
|
@ -12236,9 +12260,9 @@ snapshots:
|
||||||
|
|
||||||
jsonify@0.0.1: {}
|
jsonify@0.0.1: {}
|
||||||
|
|
||||||
jsonwebtoken@9.0.2:
|
jsonwebtoken@9.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
jws: 3.2.2
|
jws: 4.0.1
|
||||||
lodash.includes: 4.3.0
|
lodash.includes: 4.3.0
|
||||||
lodash.isboolean: 3.0.3
|
lodash.isboolean: 3.0.3
|
||||||
lodash.isinteger: 4.0.4
|
lodash.isinteger: 4.0.4
|
||||||
|
|
@ -12263,15 +12287,15 @@ snapshots:
|
||||||
readable-stream: 2.3.8
|
readable-stream: 2.3.8
|
||||||
setimmediate: 1.0.5
|
setimmediate: 1.0.5
|
||||||
|
|
||||||
jwa@1.4.2:
|
jwa@2.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
buffer-equal-constant-time: 1.0.1
|
buffer-equal-constant-time: 1.0.1
|
||||||
ecdsa-sig-formatter: 1.0.11
|
ecdsa-sig-formatter: 1.0.11
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
jws@3.2.2:
|
jws@4.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
jwa: 1.4.2
|
jwa: 2.0.1
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
kafkajs@2.2.4: {}
|
kafkajs@2.2.4: {}
|
||||||
|
|
@ -12289,8 +12313,6 @@ snapshots:
|
||||||
known-css-properties@0.36.0:
|
known-css-properties@0.36.0:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
lazy-ass@1.6.0: {}
|
|
||||||
|
|
||||||
leven@3.1.0: {}
|
leven@3.1.0: {}
|
||||||
|
|
||||||
lie@3.3.0:
|
lie@3.3.0:
|
||||||
|
|
@ -12383,8 +12405,6 @@ snapshots:
|
||||||
|
|
||||||
lodash.once@4.1.1: {}
|
lodash.once@4.1.1: {}
|
||||||
|
|
||||||
lodash.pick@4.4.0: {}
|
|
||||||
|
|
||||||
lodash.truncate@4.4.2: {}
|
lodash.truncate@4.4.2: {}
|
||||||
|
|
||||||
lodash.uniq@4.5.0: {}
|
lodash.uniq@4.5.0: {}
|
||||||
|
|
@ -12575,18 +12595,15 @@ snapshots:
|
||||||
|
|
||||||
minimist@1.2.8: {}
|
minimist@1.2.8: {}
|
||||||
|
|
||||||
minipass@3.3.6:
|
|
||||||
dependencies:
|
|
||||||
yallist: 4.0.0
|
|
||||||
|
|
||||||
minipass@5.0.0: {}
|
|
||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
minizlib@2.1.2:
|
minizlib@3.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
minipass: 3.3.6
|
minipass: 7.1.2
|
||||||
yallist: 4.0.0
|
|
||||||
|
mkdirp@0.5.6:
|
||||||
|
dependencies:
|
||||||
|
minimist: 1.2.8
|
||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
|
|
@ -13431,7 +13448,7 @@ snapshots:
|
||||||
|
|
||||||
pure-rand@7.0.1: {}
|
pure-rand@7.0.1: {}
|
||||||
|
|
||||||
qs@6.14.0:
|
qs@6.14.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
side-channel: 1.1.0
|
side-channel: 1.1.0
|
||||||
|
|
||||||
|
|
@ -13769,6 +13786,10 @@ snapshots:
|
||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
|
rimraf@2.6.3:
|
||||||
|
dependencies:
|
||||||
|
glob: 7.2.3
|
||||||
|
|
||||||
rimraf@3.0.2:
|
rimraf@3.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
@ -14336,6 +14357,8 @@ snapshots:
|
||||||
csso: 5.0.5
|
csso: 5.0.5
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
|
|
||||||
|
systeminformation@5.30.5: {}
|
||||||
|
|
||||||
table@6.9.0:
|
table@6.9.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
ajv: 8.17.1
|
ajv: 8.17.1
|
||||||
|
|
@ -14344,14 +14367,13 @@ snapshots:
|
||||||
string-width: 4.2.3
|
string-width: 4.2.3
|
||||||
strip-ansi: 6.0.1
|
strip-ansi: 6.0.1
|
||||||
|
|
||||||
tar@6.2.1:
|
tar@7.5.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr: 2.0.0
|
'@isaacs/fs-minipass': 4.0.1
|
||||||
fs-minipass: 2.1.0
|
chownr: 3.0.0
|
||||||
minipass: 5.0.0
|
minipass: 7.1.2
|
||||||
minizlib: 2.1.2
|
minizlib: 3.1.0
|
||||||
mkdirp: 1.0.4
|
yallist: 5.0.0
|
||||||
yallist: 4.0.0
|
|
||||||
|
|
||||||
terser@5.43.1:
|
terser@5.43.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -14758,6 +14780,10 @@ snapshots:
|
||||||
sort-keys: 4.2.0
|
sort-keys: 4.2.0
|
||||||
write-file-atomic: 3.0.3
|
write-file-atomic: 3.0.3
|
||||||
|
|
||||||
|
write@1.0.3:
|
||||||
|
dependencies:
|
||||||
|
mkdirp: 0.5.6
|
||||||
|
|
||||||
xtend@4.0.2: {}
|
xtend@4.0.2: {}
|
||||||
|
|
||||||
y18n@5.0.8: {}
|
y18n@5.0.8: {}
|
||||||
|
|
@ -14766,6 +14792,8 @@ snapshots:
|
||||||
|
|
||||||
yallist@4.0.0: {}
|
yallist@4.0.0: {}
|
||||||
|
|
||||||
|
yallist@5.0.0: {}
|
||||||
|
|
||||||
yaml@1.10.2: {}
|
yaml@1.10.2: {}
|
||||||
|
|
||||||
yaml@2.8.1: {}
|
yaml@2.8.1: {}
|
||||||
|
|
|
||||||
40
prisma/migrations/15_add_share/migration.sql
Normal file
40
prisma/migrations/15_add_share/migration.sql
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "share" (
|
||||||
|
"share_id" UUID NOT NULL,
|
||||||
|
"entity_id" UUID NOT NULL,
|
||||||
|
"name" VARCHAR(200) NOT NULL,
|
||||||
|
"share_type" INTEGER NOT NULL,
|
||||||
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
|
"parameters" JSONB NOT NULL,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "share_pkey" PRIMARY KEY ("share_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "share_slug_key" ON "share"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "share_entity_id_idx" ON "share"("entity_id");
|
||||||
|
|
||||||
|
-- MigrateData
|
||||||
|
INSERT INTO "share" (share_id, entity_id, name, share_type, slug, parameters, created_at)
|
||||||
|
SELECT gen_random_uuid(),
|
||||||
|
website_id,
|
||||||
|
name,
|
||||||
|
1,
|
||||||
|
share_id,
|
||||||
|
'{"overview":true}'::jsonb,
|
||||||
|
now()
|
||||||
|
FROM "website"
|
||||||
|
WHERE share_id IS NOT NULL;
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "website_share_id_idx";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "website_share_id_key";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "website" DROP COLUMN "share_id";
|
||||||
30
prisma/migrations/16_boards/migration.sql
Normal file
30
prisma/migrations/16_boards/migration.sql
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "board" (
|
||||||
|
"board_id" UUID NOT NULL,
|
||||||
|
"type" VARCHAR(50) NOT NULL,
|
||||||
|
"name" VARCHAR(200) NOT NULL,
|
||||||
|
"description" VARCHAR(500) NOT NULL,
|
||||||
|
"parameters" JSONB NOT NULL,
|
||||||
|
"slug" VARCHAR(100) NOT NULL,
|
||||||
|
"user_id" UUID,
|
||||||
|
"team_id" UUID,
|
||||||
|
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updated_at" TIMESTAMPTZ(6),
|
||||||
|
|
||||||
|
CONSTRAINT "board_pkey" PRIMARY KEY ("board_id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "board_slug_key" ON "board"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_slug_idx" ON "board"("slug");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_user_id_idx" ON "board"("user_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_team_id_idx" ON "board"("team_id");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "board_created_at_idx" ON "board"("created_at");
|
||||||
29
prisma/migrations/17_remove_duplicate_key/migration.sql
Normal file
29
prisma/migrations/17_remove_duplicate_key/migration.sql
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "link_link_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "pixel_pixel_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "report_report_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "revenue_revenue_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "segment_segment_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "session_session_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "team_team_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "team_user_team_user_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "user_user_id_key";
|
||||||
|
|
||||||
|
-- DropIndex
|
||||||
|
DROP INDEX "website_website_id_key";
|
||||||
|
|
@ -11,7 +11,7 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @unique @map("user_id") @db.Uuid
|
id String @id() @map("user_id") @db.Uuid
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
role String @map("role") @db.VarChar(50)
|
role String @map("role") @db.VarChar(50)
|
||||||
|
|
@ -27,12 +27,13 @@ model User {
|
||||||
pixels Pixel[] @relation("user")
|
pixels Pixel[] @relation("user")
|
||||||
teams TeamUser[]
|
teams TeamUser[]
|
||||||
reports Report[]
|
reports Report[]
|
||||||
|
boards Board[] @relation("user")
|
||||||
|
|
||||||
@@map("user")
|
@@map("user")
|
||||||
}
|
}
|
||||||
|
|
||||||
model Session {
|
model Session {
|
||||||
id String @id @unique @map("session_id") @db.Uuid
|
id String @id() @map("session_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
browser String? @db.VarChar(20)
|
browser String? @db.VarChar(20)
|
||||||
os String? @db.VarChar(20)
|
os String? @db.VarChar(20)
|
||||||
|
|
@ -64,10 +65,9 @@ model Session {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Website {
|
model Website {
|
||||||
id String @id @unique @map("website_id") @db.Uuid
|
id String @id() @map("website_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
domain String? @db.VarChar(500)
|
domain String? @db.VarChar(500)
|
||||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
|
||||||
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
||||||
userId String? @map("user_id") @db.Uuid
|
userId String? @map("user_id") @db.Uuid
|
||||||
teamId String? @map("team_id") @db.Uuid
|
teamId String? @map("team_id") @db.Uuid
|
||||||
|
|
@ -88,7 +88,6 @@ model Website {
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([teamId])
|
@@index([teamId])
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@index([shareId])
|
|
||||||
@@index([createdBy])
|
@@index([createdBy])
|
||||||
@@map("website")
|
@@map("website")
|
||||||
}
|
}
|
||||||
|
|
@ -187,7 +186,7 @@ model SessionData {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Team {
|
model Team {
|
||||||
id String @id() @unique() @map("team_id") @db.Uuid
|
id String @id() @map("team_id") @db.Uuid
|
||||||
name String @db.VarChar(50)
|
name String @db.VarChar(50)
|
||||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||||
|
|
@ -199,13 +198,14 @@ model Team {
|
||||||
members TeamUser[]
|
members TeamUser[]
|
||||||
links Link[]
|
links Link[]
|
||||||
pixels Pixel[]
|
pixels Pixel[]
|
||||||
|
boards Board[]
|
||||||
|
|
||||||
@@index([accessCode])
|
@@index([accessCode])
|
||||||
@@map("team")
|
@@map("team")
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamUser {
|
model TeamUser {
|
||||||
id String @id() @unique() @map("team_user_id") @db.Uuid
|
id String @id() @map("team_user_id") @db.Uuid
|
||||||
teamId String @map("team_id") @db.Uuid
|
teamId String @map("team_id") @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
role String @db.VarChar(50)
|
role String @db.VarChar(50)
|
||||||
|
|
@ -221,7 +221,7 @@ model TeamUser {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Report {
|
model Report {
|
||||||
id String @id() @unique() @map("report_id") @db.Uuid
|
id String @id() @map("report_id") @db.Uuid
|
||||||
userId String @map("user_id") @db.Uuid
|
userId String @map("user_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
type String @db.VarChar(50)
|
type String @db.VarChar(50)
|
||||||
|
|
@ -242,7 +242,7 @@ model Report {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Segment {
|
model Segment {
|
||||||
id String @id() @unique() @map("segment_id") @db.Uuid
|
id String @id() @map("segment_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
type String @db.VarChar(50)
|
type String @db.VarChar(50)
|
||||||
name String @db.VarChar(200)
|
name String @db.VarChar(200)
|
||||||
|
|
@ -257,7 +257,7 @@ model Segment {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Revenue {
|
model Revenue {
|
||||||
id String @id() @unique() @map("revenue_id") @db.Uuid
|
id String @id() @map("revenue_id") @db.Uuid
|
||||||
websiteId String @map("website_id") @db.Uuid
|
websiteId String @map("website_id") @db.Uuid
|
||||||
sessionId String @map("session_id") @db.Uuid
|
sessionId String @map("session_id") @db.Uuid
|
||||||
eventId String @map("event_id") @db.Uuid
|
eventId String @map("event_id") @db.Uuid
|
||||||
|
|
@ -277,7 +277,7 @@ model Revenue {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Link {
|
model Link {
|
||||||
id String @id() @unique() @map("link_id") @db.Uuid
|
id String @id() @map("link_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
slug String @unique() @db.VarChar(100)
|
slug String @unique() @db.VarChar(100)
|
||||||
|
|
@ -298,7 +298,7 @@ model Link {
|
||||||
}
|
}
|
||||||
|
|
||||||
model Pixel {
|
model Pixel {
|
||||||
id String @id() @unique() @map("pixel_id") @db.Uuid
|
id String @id() @map("pixel_id") @db.Uuid
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
slug String @unique() @db.VarChar(100)
|
slug String @unique() @db.VarChar(100)
|
||||||
userId String? @map("user_id") @db.Uuid
|
userId String? @map("user_id") @db.Uuid
|
||||||
|
|
@ -316,3 +316,39 @@ model Pixel {
|
||||||
@@index([createdAt])
|
@@index([createdAt])
|
||||||
@@map("pixel")
|
@@map("pixel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model Board {
|
||||||
|
id String @id() @map("board_id") @db.Uuid
|
||||||
|
type String @db.VarChar(50)
|
||||||
|
name String @db.VarChar(200)
|
||||||
|
description String @db.VarChar(500)
|
||||||
|
parameters Json
|
||||||
|
slug String @unique() @db.VarChar(100)
|
||||||
|
userId String? @map("user_id") @db.Uuid
|
||||||
|
teamId String? @map("team_id") @db.Uuid
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
user User? @relation("user", fields: [userId], references: [id])
|
||||||
|
team Team? @relation(fields: [teamId], references: [id])
|
||||||
|
|
||||||
|
@@index([slug])
|
||||||
|
@@index([userId])
|
||||||
|
@@index([teamId])
|
||||||
|
@@index([createdAt])
|
||||||
|
@@map("board")
|
||||||
|
}
|
||||||
|
|
||||||
|
model Share {
|
||||||
|
id String @id() @map("share_id") @db.Uuid
|
||||||
|
entityId String @map("entity_id") @db.Uuid
|
||||||
|
name String @db.VarChar(200)
|
||||||
|
shareType Int @map("share_type") @db.Integer
|
||||||
|
slug String @unique() @db.VarChar(100)
|
||||||
|
parameters Json
|
||||||
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
|
updatedAt DateTime? @updatedAt @map("updated_at") @db.Timestamptz(6)
|
||||||
|
|
||||||
|
@@index([entityId])
|
||||||
|
@@map("share")
|
||||||
|
}
|
||||||
|
|
|
||||||
BIN
public/images/country/t1.png
Normal file
BIN
public/images/country/t1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
|
|
@ -5,6 +5,18 @@
|
||||||
"value": "访问代码"
|
"value": "访问代码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "账户"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.action": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "行为"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.actions": [
|
"label.actions": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -35,12 +47,24 @@
|
||||||
"value": "添加描述"
|
"value": "添加描述"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.add-link": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "添加链接"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.add-member": [
|
"label.add-member": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "添加成员"
|
"value": "添加成员"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.add-pixel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "添加像素"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.add-step": [
|
"label.add-step": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -83,12 +107,24 @@
|
||||||
"value": "所有时间段"
|
"value": "所有时间段"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.analysis": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "分析"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.analytics": [
|
"label.analytics": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "分析"
|
"value": "分析"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.application": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "应用"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.apply": [
|
"label.apply": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -107,6 +143,12 @@
|
||||||
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
|
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.audience": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "受众"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.average": [
|
"label.average": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -125,6 +167,12 @@
|
||||||
"value": "之前"
|
"value": "之前"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.behavior": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "行为"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.boards": [
|
"label.boards": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -173,12 +221,24 @@
|
||||||
"value": "修改密码"
|
"value": "修改密码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.channel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "渠道"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.channels": [
|
"label.channels": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "渠道"
|
"value": "渠道"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.chart": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "图表"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.cities": [
|
"label.cities": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -203,6 +263,12 @@
|
||||||
"value": "队列"
|
"value": "队列"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.cohorts": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "队列"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.compare": [
|
"label.compare": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -317,6 +383,12 @@
|
||||||
"value": "创建者"
|
"value": "创建者"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.criteria": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "条件"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.currency": [
|
"label.currency": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -419,6 +491,12 @@
|
||||||
"value": "台式机"
|
"value": "台式机"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.destination-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "目标URL"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.details": [
|
"label.details": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -455,6 +533,12 @@
|
||||||
"value": "唯一ID"
|
"value": "唯一ID"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.documentation": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "文档"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.does-not-contain": [
|
"label.does-not-contain": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -479,6 +563,12 @@
|
||||||
"value": "域名"
|
"value": "域名"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.download": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "下载"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.dropoff": [
|
"label.dropoff": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -506,7 +596,7 @@
|
||||||
"label.email": [
|
"label.email": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Email"
|
"value": "邮箱"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.enable-share-url": [
|
"label.enable-share-url": [
|
||||||
|
|
@ -527,6 +617,12 @@
|
||||||
"value": "入口 URL"
|
"value": "入口 URL"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.environment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "环境"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.event": [
|
"label.event": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -671,6 +767,12 @@
|
||||||
"value": "分组"
|
"value": "分组"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.growth": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "增长"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.hostname": [
|
"label.hostname": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -701,6 +803,12 @@
|
||||||
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
|
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.invalid-url": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "无效URL"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.is": [
|
"label.is": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -863,12 +971,24 @@
|
||||||
"value": "少于等于"
|
"value": "少于等于"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.link": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "链接"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.links": [
|
"label.links": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "链接"
|
"value": "链接"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.location": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "位置"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.login": [
|
"label.login": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1020,7 +1140,7 @@
|
||||||
"label.online": [
|
"label.online": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Online"
|
"value": "在线"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.organic-search": [
|
"label.organic-search": [
|
||||||
|
|
@ -1165,6 +1285,12 @@
|
||||||
"value": "路径"
|
"value": "路径"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.pixel": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "像素"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.pixels": [
|
"label.pixels": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1185,6 +1311,12 @@
|
||||||
"value": " 提供支持"
|
"value": " 提供支持"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.preferences": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "偏好"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.previous": [
|
"label.previous": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1209,6 +1341,12 @@
|
||||||
"value": "个人资料"
|
"value": "个人资料"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.profiles": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "个人资料"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.properties": [
|
"label.properties": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1248,7 +1386,7 @@
|
||||||
"label.referral": [
|
"label.referral": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Referral"
|
"value": "来源"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.referrer": [
|
"label.referrer": [
|
||||||
|
|
@ -1371,6 +1509,24 @@
|
||||||
"value": "保存"
|
"value": "保存"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.save-cohort": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "保存为群组"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.save-segment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "保存为细分"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.screen": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "屏幕"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.screens": [
|
"label.screens": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1383,6 +1539,18 @@
|
||||||
"value": "搜索"
|
"value": "搜索"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.segment": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "细分"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.segments": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "细分"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.select": [
|
"label.select": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1485,6 +1653,24 @@
|
||||||
"value": "总和"
|
"value": "总和"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.support": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "支持"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.switch-account": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "切换账户"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.table": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "表格"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.tablet": [
|
"label.tablet": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1635,6 +1821,12 @@
|
||||||
"value": "跟踪代码"
|
"value": "跟踪代码"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.traffic": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "流量"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.transactions": [
|
"label.transactions": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
@ -1846,7 +2038,7 @@
|
||||||
"message.bad-request": [
|
"message.bad-request": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Bad request"
|
"value": "请求错误"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.collected-data": [
|
"message.collected-data": [
|
||||||
|
|
@ -1946,7 +2138,7 @@
|
||||||
"message.forbidden": [
|
"message.forbidden": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Forbidden"
|
"value": "禁止访问"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.go-to-settings": [
|
"message.go-to-settings": [
|
||||||
|
|
@ -2046,13 +2238,13 @@
|
||||||
"message.not-found": [
|
"message.not-found": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Not found"
|
"value": "未找到"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.nothing-selected": [
|
"message.nothing-selected": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Nothing selected."
|
"value": "未选择"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.page-not-found": [
|
"message.page-not-found": [
|
||||||
|
|
@ -2090,7 +2282,7 @@
|
||||||
"message.sever-error": [
|
"message.sever-error": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Server error"
|
"value": "服务器错误"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.share-url": [
|
"message.share-url": [
|
||||||
|
|
@ -2158,7 +2350,7 @@
|
||||||
"message.unauthorized": [
|
"message.unauthorized": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Unauthorized"
|
"value": "未授权"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.user-deleted": [
|
"message.user-deleted": [
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
"AD-06": "Sant Julia de Loria",
|
"AD-06": "Sant Julia de Loria",
|
||||||
"AD-07": "Andorra la Vella",
|
"AD-07": "Andorra la Vella",
|
||||||
"AD-08": "Escaldes-Engordany",
|
"AD-08": "Escaldes-Engordany",
|
||||||
"AE-AJ": "'Ajman",
|
"AE-AJ": "Ajman",
|
||||||
"AE-AZ": "Abu Zaby",
|
"AE-AZ": "Abu Dhabi",
|
||||||
"AE-DU": "Dubayy",
|
"AE-DU": "Dubai",
|
||||||
"AE-FU": "Al Fujayrah",
|
"AE-FU": "Al Fujairah",
|
||||||
"AE-RK": "Ra's al Khaymah",
|
"AE-RK": "Ras al Khaimah",
|
||||||
"AE-SH": "Ash Shariqah",
|
"AE-SH": "Sharjah",
|
||||||
"AE-UQ": "Umm al Qaywayn",
|
"AE-UQ": "Umm al Quwain",
|
||||||
"AF-BAL": "Balkh",
|
"AF-BAL": "Balkh",
|
||||||
"AF-BAM": "Bamyan",
|
"AF-BAM": "Bamyan",
|
||||||
"AF-BDG": "Badghis",
|
"AF-BDG": "Badghis",
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import 'dotenv/config';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import tar from 'tar';
|
import { list } from 'tar';
|
||||||
import zlib from 'zlib';
|
import zlib from 'zlib';
|
||||||
|
|
||||||
if (process.env.VERCEL && !process.env.BUILD_GEO) {
|
if (process.env.VERCEL && !process.env.BUILD_GEO) {
|
||||||
|
|
@ -40,7 +40,7 @@ const isDirectMmdb = url.endsWith('.mmdb');
|
||||||
const downloadCompressed = url =>
|
const downloadCompressed = url =>
|
||||||
new Promise(resolve => {
|
new Promise(resolve => {
|
||||||
https.get(url, res => {
|
https.get(url, res => {
|
||||||
resolve(res.pipe(zlib.createGunzip({})).pipe(tar.t()));
|
resolve(res.pipe(zlib.createGunzip({})).pipe(list()));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,19 @@ import { Column } from '@umami/react-zen';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { TeamsAddButton } from '../../teams/TeamsAddButton';
|
||||||
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
|
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
|
||||||
|
|
||||||
export function AdminTeamsPage() {
|
export function AdminTeamsPage() {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
const handleSave = () => {};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="6" margin="2">
|
<Column gap="6" margin="2">
|
||||||
<PageHeader title={formatMessage(labels.teams)} />
|
<PageHeader title={formatMessage(labels.teams)}>
|
||||||
|
<TeamsAddButton onSave={handleSave} isAdmin={true} />
|
||||||
|
</PageHeader>
|
||||||
<Panel>
|
<Panel>
|
||||||
<AdminTeamsDataTable />
|
<AdminTeamsDataTable />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
export function UserAddForm({ onSave, onClose }) {
|
export function UserAddForm({ onSave, onClose }) {
|
||||||
|
|
@ -37,7 +38,10 @@ export function UserAddForm({ onSave, onClose }) {
|
||||||
<FormField
|
<FormField
|
||||||
label={formatMessage(labels.password)}
|
label={formatMessage(labels.password)}
|
||||||
name="password"
|
name="password"
|
||||||
rules={{ required: formatMessage(labels.required) }}
|
rules={{
|
||||||
|
required: formatMessage(labels.required),
|
||||||
|
minLength: { value: 8, message: formatMessage(messages.minPasswordLength, { n: '8' }) },
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<PasswordField autoComplete="new-password" data-test="input-password" />
|
<PasswordField autoComplete="new-password" data-test="input-password" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,11 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getMessage(error?.code)} values={user}>
|
<Form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
error={getMessage(error?.code)}
|
||||||
|
values={{ ...user, password: '' }}
|
||||||
|
>
|
||||||
<FormField name="username" label={formatMessage(labels.username)}>
|
<FormField name="username" label={formatMessage(labels.username)}>
|
||||||
<TextField data-test="input-username" />
|
<TextField data-test="input-username" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||||
import { useDeleteQuery, useMessages } from '@/components/hooks';
|
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||||
import { Trash } from '@/components/icons';
|
import { Trash } from '@/components/icons';
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
import { messages } from '@/components/messages';
|
import { messages } from '@/components/messages';
|
||||||
|
|
@ -15,7 +15,8 @@ export function LinkDeleteButton({
|
||||||
onSave?: () => void;
|
onSave?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
|
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
|
||||||
const { mutateAsync, isPending, error, touch } = useDeleteQuery(`/links/${linkId}`);
|
const { mutateAsync, isPending, error } = useDeleteQuery(`/links/${linkId}`);
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
const handleConfirm = async (close: () => void) => {
|
const handleConfirm = async (close: () => void) => {
|
||||||
await mutateAsync(null, {
|
await mutateAsync(null, {
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,14 @@ import {
|
||||||
Form,
|
Form,
|
||||||
FormField,
|
FormField,
|
||||||
FormSubmitButton,
|
FormSubmitButton,
|
||||||
|
Grid,
|
||||||
Icon,
|
Icon,
|
||||||
Label,
|
Label,
|
||||||
Loading,
|
Loading,
|
||||||
Row,
|
Row,
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useEffect, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
||||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||||
import { RefreshCw } from '@/components/icons';
|
import { RefreshCw } from '@/components/icons';
|
||||||
|
|
@ -42,27 +43,20 @@ export function LinkEditForm({
|
||||||
const { linksUrl } = useConfig();
|
const { linksUrl } = useConfig();
|
||||||
const hostUrl = linksUrl || LINKS_URL;
|
const hostUrl = linksUrl || LINKS_URL;
|
||||||
const { data, isLoading } = useLinkQuery(linkId);
|
const { data, isLoading } = useLinkQuery(linkId);
|
||||||
const [slug, setSlug] = useState(generateId());
|
const [defaultSlug] = useState(generateId());
|
||||||
|
|
||||||
const handleSubmit = async (data: any) => {
|
const handleSubmit = async (data: any) => {
|
||||||
await mutateAsync(data, {
|
await mutateAsync(data, {
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast(formatMessage(messages.saved));
|
toast(formatMessage(messages.saved));
|
||||||
touch('links');
|
touch('links');
|
||||||
|
touch(`link:${linkId}`);
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSlug = () => {
|
|
||||||
const slug = generateId();
|
|
||||||
|
|
||||||
setSlug(slug);
|
|
||||||
|
|
||||||
return slug;
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkUrl = (url: string) => {
|
const checkUrl = (url: string) => {
|
||||||
if (!isValidUrl(url)) {
|
if (!isValidUrl(url)) {
|
||||||
return formatMessage(labels.invalidUrl);
|
return formatMessage(labels.invalidUrl);
|
||||||
|
|
@ -70,19 +64,19 @@ export function LinkEditForm({
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
setSlug(data.slug);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
if (linkId && isLoading) {
|
if (linkId && isLoading) {
|
||||||
return <Loading placement="absolute" />;
|
return <Loading placement="absolute" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}>
|
<Form
|
||||||
{({ setValue }) => {
|
onSubmit={handleSubmit}
|
||||||
|
error={getErrorMessage(error)}
|
||||||
|
defaultValues={{ slug: defaultSlug, ...data }}
|
||||||
|
>
|
||||||
|
{({ setValue, watch }) => {
|
||||||
|
const slug = watch('slug');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormField
|
<FormField
|
||||||
|
|
@ -101,15 +95,25 @@ export function LinkEditForm({
|
||||||
<TextField placeholder="https://example.com" autoComplete="off" />
|
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField
|
<Grid columns="1fr auto" alignItems="end" gap>
|
||||||
name="slug"
|
<FormField
|
||||||
rules={{
|
name="slug"
|
||||||
required: formatMessage(labels.required),
|
label={formatMessage({ id: 'label.slug', defaultMessage: 'Slug' })}
|
||||||
}}
|
rules={{
|
||||||
style={{ display: 'none' }}
|
required: formatMessage(labels.required),
|
||||||
>
|
}}
|
||||||
<input type="hidden" />
|
>
|
||||||
</FormField>
|
<TextField autoComplete="off" />
|
||||||
|
</FormField>
|
||||||
|
<Button
|
||||||
|
variant="quiet"
|
||||||
|
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
|
||||||
|
>
|
||||||
|
<Icon>
|
||||||
|
<RefreshCw />
|
||||||
|
</Icon>
|
||||||
|
</Button>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Column>
|
<Column>
|
||||||
<Label>{formatMessage(labels.link)}</Label>
|
<Label>{formatMessage(labels.link)}</Label>
|
||||||
|
|
@ -121,14 +125,6 @@ export function LinkEditForm({
|
||||||
allowCopy
|
allowCopy
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
/>
|
||||||
<Button
|
|
||||||
variant="quiet"
|
|
||||||
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
|
|
||||||
>
|
|
||||||
<Icon>
|
|
||||||
<RefreshCw />
|
|
||||||
</Icon>
|
|
||||||
</Button>
|
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { DataGrid } from '@/components/common/DataGrid';
|
||||||
import { useLinksQuery, useNavigation } from '@/components/hooks';
|
import { useLinksQuery, useNavigation } from '@/components/hooks';
|
||||||
import { LinksTable } from './LinksTable';
|
import { LinksTable } from './LinksTable';
|
||||||
|
|
||||||
export function LinksDataTable() {
|
export function LinksDataTable({ showActions = false }: { showActions?: boolean }) {
|
||||||
const { teamId } = useNavigation();
|
const { teamId } = useNavigation();
|
||||||
const query = useLinksQuery({ teamId });
|
const query = useLinksQuery({ teamId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
||||||
{({ data }) => <LinksTable data={data} />}
|
{({ data }) => <LinksTable data={data} showActions={showActions} />}
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,30 @@ import { LinksDataTable } from '@/app/(main)/links/LinksDataTable';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useNavigation, useTeamMembersQuery } from '@/components/hooks';
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
import { LinkAddButton } from './LinkAddButton';
|
import { LinkAddButton } from './LinkAddButton';
|
||||||
|
|
||||||
export function LinksPage() {
|
export function LinksPage() {
|
||||||
|
const { user } = useLoginQuery();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { teamId } = useNavigation();
|
const { teamId } = useNavigation();
|
||||||
|
const { data } = useTeamMembersQuery(teamId);
|
||||||
|
|
||||||
|
const showActions =
|
||||||
|
(teamId &&
|
||||||
|
data?.data.filter(team => team.userId === user.id && team.role !== ROLES.teamViewOnly)
|
||||||
|
.length > 0) ||
|
||||||
|
(!teamId && user.role !== ROLES.viewOnly);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Column gap="6" margin="2">
|
<Column gap="6" margin="2">
|
||||||
<PageHeader title={formatMessage(labels.links)}>
|
<PageHeader title={formatMessage(labels.links)}>
|
||||||
<LinkAddButton teamId={teamId} />
|
{showActions && <LinkAddButton teamId={teamId} />}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Panel>
|
<Panel>
|
||||||
<LinksDataTable />
|
<LinksDataTable showActions={showActions} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||||
import { LinkDeleteButton } from './LinkDeleteButton';
|
import { LinkDeleteButton } from './LinkDeleteButton';
|
||||||
import { LinkEditButton } from './LinkEditButton';
|
import { LinkEditButton } from './LinkEditButton';
|
||||||
|
|
||||||
export function LinksTable(props: DataTableProps) {
|
export interface LinksTableProps extends DataTableProps {
|
||||||
|
showActions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LinksTable({ showActions, ...props }: LinksTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { websiteId, renderUrl } = useNavigation();
|
const { websiteId, renderUrl } = useNavigation();
|
||||||
const { getSlugUrl } = useSlug('link');
|
const { getSlugUrl } = useSlug('link');
|
||||||
|
|
@ -36,16 +40,18 @@ export function LinksTable(props: DataTableProps) {
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="action" align="end" width="100px">
|
{showActions && (
|
||||||
{({ id, name }: any) => {
|
<DataColumn id="action" align="end" width="100px">
|
||||||
return (
|
{({ id, name }: any) => {
|
||||||
<Row>
|
return (
|
||||||
<LinkEditButton linkId={id} />
|
<Row>
|
||||||
<LinkDeleteButton linkId={id} websiteId={websiteId} name={name} />
|
<LinkEditButton linkId={id} />
|
||||||
</Row>
|
<LinkDeleteButton linkId={id} websiteId={websiteId} name={name} />
|
||||||
);
|
</Row>
|
||||||
}}
|
);
|
||||||
</DataColumn>
|
}}
|
||||||
|
</DataColumn>
|
||||||
|
)}
|
||||||
</DataTable>
|
</DataTable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { getLink } from '@/queries/prisma';
|
||||||
import { LinkPage } from './LinkPage';
|
import { LinkPage } from './LinkPage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
|
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
|
||||||
const { linkId } = await params;
|
const { linkId } = await params;
|
||||||
|
const link = await getLink(linkId);
|
||||||
|
|
||||||
|
if (!link || link?.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <LinkPage linkId={linkId} />;
|
return <LinkPage linkId={linkId} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ export function PixelEditForm({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
toast(formatMessage(messages.saved));
|
toast(formatMessage(messages.saved));
|
||||||
touch('pixels');
|
touch('pixels');
|
||||||
|
touch(`pixel:${pixelId}`);
|
||||||
onSave?.();
|
onSave?.();
|
||||||
onClose?.();
|
onClose?.();
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@ import { DataGrid } from '@/components/common/DataGrid';
|
||||||
import { useNavigation, usePixelsQuery } from '@/components/hooks';
|
import { useNavigation, usePixelsQuery } from '@/components/hooks';
|
||||||
import { PixelsTable } from './PixelsTable';
|
import { PixelsTable } from './PixelsTable';
|
||||||
|
|
||||||
export function PixelsDataTable() {
|
export function PixelsDataTable({ showActions = false }: { showActions?: boolean }) {
|
||||||
const { teamId } = useNavigation();
|
const { teamId } = useNavigation();
|
||||||
const query = usePixelsQuery({ teamId });
|
const query = usePixelsQuery({ teamId });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
||||||
{({ data }) => <PixelsTable data={data} />}
|
{({ data }) => <PixelsTable data={data} showActions={showActions} />}
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,31 @@ import { Column } from '@umami/react-zen';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useNavigation, useTeamMembersQuery } from '@/components/hooks';
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
import { PixelAddButton } from './PixelAddButton';
|
import { PixelAddButton } from './PixelAddButton';
|
||||||
import { PixelsDataTable } from './PixelsDataTable';
|
import { PixelsDataTable } from './PixelsDataTable';
|
||||||
|
|
||||||
export function PixelsPage() {
|
export function PixelsPage() {
|
||||||
|
const { user } = useLoginQuery();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { teamId } = useNavigation();
|
const { teamId } = useNavigation();
|
||||||
|
const { data } = useTeamMembersQuery(teamId);
|
||||||
|
|
||||||
|
const showActions =
|
||||||
|
(teamId &&
|
||||||
|
data?.data.filter(team => team.userId === user.id && team.role !== ROLES.teamViewOnly)
|
||||||
|
.length > 0) ||
|
||||||
|
(!teamId && user.role !== ROLES.viewOnly);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Column gap="6" margin="2">
|
<Column gap="6" margin="2">
|
||||||
<PageHeader title={formatMessage(labels.pixels)}>
|
<PageHeader title={formatMessage(labels.pixels)}>
|
||||||
<PixelAddButton teamId={teamId} />
|
{showActions && <PixelAddButton teamId={teamId} />}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Panel>
|
<Panel>
|
||||||
<PixelsDataTable />
|
<PixelsDataTable showActions={showActions} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,11 @@ import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
||||||
import { PixelDeleteButton } from './PixelDeleteButton';
|
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||||
import { PixelEditButton } from './PixelEditButton';
|
import { PixelEditButton } from './PixelEditButton';
|
||||||
|
|
||||||
export function PixelsTable(props: DataTableProps) {
|
export interface PixelsTableProps extends DataTableProps {
|
||||||
|
showActions?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PixelsTable({ showActions, ...props }: PixelsTableProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { renderUrl } = useNavigation();
|
const { renderUrl } = useNavigation();
|
||||||
const { getSlugUrl } = useSlug('pixel');
|
const { getSlugUrl } = useSlug('pixel');
|
||||||
|
|
@ -31,18 +35,20 @@ export function PixelsTable(props: DataTableProps) {
|
||||||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
<DataColumn id="action" align="end" width="100px">
|
{showActions && (
|
||||||
{(row: any) => {
|
<DataColumn id="action" align="end" width="100px">
|
||||||
const { id, name } = row;
|
{(row: any) => {
|
||||||
|
const { id, name } = row;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row>
|
<Row>
|
||||||
<PixelEditButton pixelId={id} />
|
<PixelEditButton pixelId={id} />
|
||||||
<PixelDeleteButton pixelId={id} name={name} />
|
<PixelDeleteButton pixelId={id} name={name} />
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</DataColumn>
|
</DataColumn>
|
||||||
|
)}
|
||||||
</DataTable>
|
</DataTable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
|
import { getPixel } from '@/queries/prisma';
|
||||||
import { PixelPage } from './PixelPage';
|
import { PixelPage } from './PixelPage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: { pixelId: string } }) {
|
export default async function ({ params }: { params: { pixelId: string } }) {
|
||||||
const { pixelId } = await params;
|
const { pixelId } = await params;
|
||||||
|
const pixel = await getPixel(pixelId);
|
||||||
|
|
||||||
|
if (!pixel || pixel?.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <PixelPage pixelId={pixelId} />;
|
return <PixelPage pixelId={pixelId} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { DateRangeSetting } from './DateRangeSetting';
|
||||||
import { LanguageSetting } from './LanguageSetting';
|
import { LanguageSetting } from './LanguageSetting';
|
||||||
import { ThemeSetting } from './ThemeSetting';
|
import { ThemeSetting } from './ThemeSetting';
|
||||||
import { TimezoneSetting } from './TimezoneSetting';
|
import { TimezoneSetting } from './TimezoneSetting';
|
||||||
|
import { VersionSetting } from './VersionSetting';
|
||||||
|
|
||||||
export function PreferenceSettings() {
|
export function PreferenceSettings() {
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
|
|
@ -31,6 +32,10 @@ export function PreferenceSettings() {
|
||||||
<Label>{formatMessage(labels.theme)}</Label>
|
<Label>{formatMessage(labels.theme)}</Label>
|
||||||
<ThemeSetting />
|
<ThemeSetting />
|
||||||
</Column>
|
</Column>
|
||||||
|
<Column>
|
||||||
|
<Label>{formatMessage(labels.version)}</Label>
|
||||||
|
<VersionSetting />
|
||||||
|
</Column>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
8
src/app/(main)/settings/preferences/VersionSetting.tsx
Normal file
8
src/app/(main)/settings/preferences/VersionSetting.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { Text } from '@umami/react-zen';
|
||||||
|
import { CURRENT_VERSION } from '@/lib/constants';
|
||||||
|
|
||||||
|
export function VersionSetting() {
|
||||||
|
return <Text>{CURRENT_VERSION}</Text>;
|
||||||
|
}
|
||||||
|
|
@ -7,8 +7,17 @@ import {
|
||||||
TextField,
|
TextField,
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { UserSelect } from '@/components/input/UserSelect';
|
||||||
|
|
||||||
export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose: () => void }) {
|
export function TeamAddForm({
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
isAdmin,
|
||||||
|
}: {
|
||||||
|
onSave: () => void;
|
||||||
|
onClose: () => void;
|
||||||
|
isAdmin: boolean;
|
||||||
|
}) {
|
||||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
|
const { mutateAsync, error, isPending } = useUpdateQuery('/teams');
|
||||||
|
|
||||||
|
|
@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
|
||||||
<FormField name="name" label={formatMessage(labels.name)}>
|
<FormField name="name" label={formatMessage(labels.name)}>
|
||||||
<TextField autoComplete="off" />
|
<TextField autoComplete="off" />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
{isAdmin && (
|
||||||
|
<FormField name="ownerId" label={formatMessage(labels.teamOwner)}>
|
||||||
|
<UserSelect buttonProps={{ style: { outline: 'none' } }} />
|
||||||
|
</FormField>
|
||||||
|
)}
|
||||||
<FormButtons>
|
<FormButtons>
|
||||||
<Button isDisabled={isPending} onPress={onClose}>
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
{formatMessage(labels.cancel)}
|
{formatMessage(labels.cancel)}
|
||||||
|
|
|
||||||
76
src/app/(main)/teams/TeamMemberAddForm.tsx
Normal file
76
src/app/(main)/teams/TeamMemberAddForm.tsx
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Form,
|
||||||
|
FormButtons,
|
||||||
|
FormField,
|
||||||
|
FormSubmitButton,
|
||||||
|
ListItem,
|
||||||
|
Select,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||||
|
import { UserSelect } from '@/components/input/UserSelect';
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
|
|
||||||
|
const roles = [ROLES.teamManager, ROLES.teamMember, ROLES.teamViewOnly];
|
||||||
|
|
||||||
|
export function TeamMemberAddForm({
|
||||||
|
teamId,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
|
const { mutateAsync, error, isPending } = useUpdateQuery(`/teams/${teamId}/users`);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
await mutateAsync(data, {
|
||||||
|
onSuccess: async () => {
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderRole = role => {
|
||||||
|
switch (role) {
|
||||||
|
case ROLES.teamManager:
|
||||||
|
return formatMessage(labels.manager);
|
||||||
|
case ROLES.teamMember:
|
||||||
|
return formatMessage(labels.member);
|
||||||
|
case ROLES.teamViewOnly:
|
||||||
|
return formatMessage(labels.viewOnly);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)}>
|
||||||
|
<FormField
|
||||||
|
name="userId"
|
||||||
|
label={formatMessage(labels.username)}
|
||||||
|
rules={{ required: 'Required' }}
|
||||||
|
>
|
||||||
|
<UserSelect teamId={teamId} />
|
||||||
|
</FormField>
|
||||||
|
<FormField name="role" label={formatMessage(labels.role)} rules={{ required: 'Required' }}>
|
||||||
|
<Select items={roles} renderValue={value => renderRole(value as any)}>
|
||||||
|
{roles.map(value => (
|
||||||
|
<ListItem key={value} id={value}>
|
||||||
|
{renderRole(value)}
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</FormField>
|
||||||
|
<FormButtons>
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
<FormSubmitButton variant="primary" isDisabled={isPending}>
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</FormSubmitButton>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,7 +4,13 @@ import { Plus } from '@/components/icons';
|
||||||
import { messages } from '@/components/messages';
|
import { messages } from '@/components/messages';
|
||||||
import { TeamAddForm } from './TeamAddForm';
|
import { TeamAddForm } from './TeamAddForm';
|
||||||
|
|
||||||
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
export function TeamsAddButton({
|
||||||
|
onSave,
|
||||||
|
isAdmin = false,
|
||||||
|
}: {
|
||||||
|
onSave?: () => void;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const { touch } = useModified();
|
const { touch } = useModified();
|
||||||
|
|
@ -25,7 +31,7 @@ export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
||||||
</Button>
|
</Button>
|
||||||
<Modal>
|
<Modal>
|
||||||
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
|
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
|
||||||
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} />}
|
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} isAdmin={isAdmin} />}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Modal>
|
</Modal>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
|
|
|
||||||
40
src/app/(main)/teams/TeamsMemberAddButton.tsx
Normal file
40
src/app/(main)/teams/TeamsMemberAddButton.tsx
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
import { Button, Dialog, DialogTrigger, Icon, Modal, Text, useToast } from '@umami/react-zen';
|
||||||
|
import { useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { Plus } from '@/components/icons';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
|
import { TeamMemberAddForm } from './TeamMemberAddForm';
|
||||||
|
|
||||||
|
export function TeamsMemberAddButton({
|
||||||
|
teamId,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
teamId: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
isAdmin?: boolean;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { toast } = useToast();
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
toast(formatMessage(messages.saved));
|
||||||
|
touch('teams:members');
|
||||||
|
onSave?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button>
|
||||||
|
<Icon>
|
||||||
|
<Plus />
|
||||||
|
</Icon>
|
||||||
|
<Text>{formatMessage(labels.addMember)}</Text>
|
||||||
|
</Button>
|
||||||
|
<Modal>
|
||||||
|
<Dialog title={formatMessage(labels.addMember)} style={{ width: 400 }}>
|
||||||
|
{({ close }) => <TeamMemberAddForm teamId={teamId} onSave={handleSave} onClose={close} />}
|
||||||
|
</Dialog>
|
||||||
|
</Modal>
|
||||||
|
</DialogTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column, Heading, Row } from '@umami/react-zen';
|
||||||
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
|
import { TeamLeaveButton } from '@/app/(main)/teams/TeamLeaveButton';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useLoginQuery, useNavigation, useTeam } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useNavigation, useTeam } from '@/components/hooks';
|
||||||
import { Users } from '@/components/icons';
|
import { Users } from '@/components/icons';
|
||||||
|
import { labels } from '@/components/messages';
|
||||||
import { ROLES } from '@/lib/constants';
|
import { ROLES } from '@/lib/constants';
|
||||||
|
import { TeamsMemberAddButton } from '../TeamsMemberAddButton';
|
||||||
import { TeamEditForm } from './TeamEditForm';
|
import { TeamEditForm } from './TeamEditForm';
|
||||||
import { TeamManage } from './TeamManage';
|
import { TeamManage } from './TeamManage';
|
||||||
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
||||||
|
|
@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
||||||
const team: any = useTeam();
|
const team: any = useTeam();
|
||||||
const { user } = useLoginQuery();
|
const { user } = useLoginQuery();
|
||||||
const { pathname } = useNavigation();
|
const { pathname } = useNavigation();
|
||||||
|
const { formatMessage } = useMessages();
|
||||||
|
|
||||||
const isAdmin = pathname.includes('/admin');
|
const isAdmin = pathname.includes('/admin');
|
||||||
|
|
||||||
|
|
@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
||||||
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
|
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
|
<Row alignItems="center" justifyContent="space-between">
|
||||||
|
<Heading size="2">{formatMessage(labels.members)}</Heading>
|
||||||
|
{isAdmin && <TeamsMemberAddButton teamId={teamId} />}
|
||||||
|
</Row>
|
||||||
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
|
<TeamMembersDataTable teamId={teamId} allowEdit={canEdit} />
|
||||||
</Panel>
|
</Panel>
|
||||||
{isTeamOwner && (
|
{isTeamOwner && (
|
||||||
|
|
|
||||||
|
|
@ -3,22 +3,31 @@ import { Column } from '@umami/react-zen';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useLoginQuery, useMessages, useNavigation, useTeamMembersQuery } from '@/components/hooks';
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
import { WebsiteAddButton } from './WebsiteAddButton';
|
import { WebsiteAddButton } from './WebsiteAddButton';
|
||||||
import { WebsitesDataTable } from './WebsitesDataTable';
|
import { WebsitesDataTable } from './WebsitesDataTable';
|
||||||
|
|
||||||
export function WebsitesPage() {
|
export function WebsitesPage() {
|
||||||
|
const { user } = useLoginQuery();
|
||||||
const { teamId } = useNavigation();
|
const { teamId } = useNavigation();
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { data } = useTeamMembersQuery(teamId);
|
||||||
|
|
||||||
|
const showActions =
|
||||||
|
(teamId &&
|
||||||
|
data?.data.filter(team => team.userId === user.id && team.role !== ROLES.teamViewOnly)
|
||||||
|
.length > 0) ||
|
||||||
|
(!teamId && user.role !== ROLES.viewOnly);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageBody>
|
<PageBody>
|
||||||
<Column gap="6" margin="2">
|
<Column gap="6" margin="2">
|
||||||
<PageHeader title={formatMessage(labels.websites)}>
|
<PageHeader title={formatMessage(labels.websites)}>
|
||||||
<WebsiteAddButton teamId={teamId} />
|
{showActions && <WebsiteAddButton teamId={teamId} />}
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsitesDataTable teamId={teamId} />
|
<WebsitesDataTable teamId={teamId} showActions={showActions} />
|
||||||
</Panel>
|
</Panel>
|
||||||
</Column>
|
</Column>
|
||||||
</PageBody>
|
</PageBody>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
||||||
import { File, User } from '@/components/icons';
|
import { File, User } from '@/components/icons';
|
||||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||||
|
|
@ -20,6 +20,8 @@ type FunnelResult = {
|
||||||
|
|
||||||
export function Funnel({ id, name, type, parameters, websiteId }) {
|
export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
const { data, error, isLoading } = useResultQuery(type, {
|
const { data, error, isLoading } = useResultQuery(type, {
|
||||||
websiteId,
|
websiteId,
|
||||||
...parameters,
|
...parameters,
|
||||||
|
|
@ -36,21 +38,22 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
{!isSharePage && (
|
||||||
<ReportEditButton id={id} name={name} type={type}>
|
<Column>
|
||||||
{({ close }) => {
|
<ReportEditButton id={id} name={name} type={type}>
|
||||||
return (
|
{({ close }) => {
|
||||||
<Dialog
|
return (
|
||||||
title={formatMessage(labels.funnel)}
|
<Dialog
|
||||||
variant="modal"
|
title={formatMessage(labels.funnel)}
|
||||||
style={{ minHeight: 300, minWidth: 400 }}
|
style={{ minHeight: 300, minWidth: 400 }}
|
||||||
>
|
>
|
||||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
</ReportEditButton>
|
</ReportEditButton>
|
||||||
</Column>
|
</Column>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
{data?.map(
|
{data?.map(
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||||
import { Funnel } from './Funnel';
|
import { Funnel } from './Funnel';
|
||||||
import { FunnelAddButton } from './FunnelAddButton';
|
import { FunnelAddButton } from './FunnelAddButton';
|
||||||
|
|
||||||
|
|
@ -13,13 +13,17 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<SectionHeader>
|
{!isSharePage && (
|
||||||
<FunnelAddButton websiteId={websiteId} />
|
<SectionHeader>
|
||||||
</SectionHeader>
|
<FunnelAddButton websiteId={websiteId} />
|
||||||
|
</SectionHeader>
|
||||||
|
)}
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
<Grid gap>
|
<Grid gap>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
||||||
import { File, User } from '@/components/icons';
|
import { File, User } from '@/components/icons';
|
||||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||||
import { Lightning } from '@/components/svg';
|
import { Lightning } from '@/components/svg';
|
||||||
|
|
@ -25,6 +25,8 @@ export type GoalData = { num: number; total: number };
|
||||||
|
|
||||||
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
||||||
websiteId,
|
websiteId,
|
||||||
startDate,
|
startDate,
|
||||||
|
|
@ -45,21 +47,23 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</Column>
|
</Column>
|
||||||
<Column>
|
{!isSharePage && (
|
||||||
<ReportEditButton id={id} name={name} type={type}>
|
<Column>
|
||||||
{({ close }) => {
|
<ReportEditButton id={id} name={name} type={type}>
|
||||||
return (
|
{({ close }) => {
|
||||||
<Dialog
|
return (
|
||||||
title={formatMessage(labels.goal)}
|
<Dialog
|
||||||
variant="modal"
|
title={formatMessage(labels.goal)}
|
||||||
style={{ minHeight: 300, minWidth: 400 }}
|
variant="modal"
|
||||||
>
|
style={{ minHeight: 300, minWidth: 400 }}
|
||||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
>
|
||||||
</Dialog>
|
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||||
);
|
</Dialog>
|
||||||
}}
|
);
|
||||||
</ReportEditButton>
|
}}
|
||||||
</Column>
|
</ReportEditButton>
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
<Row alignItems="center" justifyContent="space-between" gap>
|
<Row alignItems="center" justifyContent="space-between" gap>
|
||||||
<Text color="muted">
|
<Text color="muted">
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
||||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||||
import { Goal } from './Goal';
|
import { Goal } from './Goal';
|
||||||
import { GoalAddButton } from './GoalAddButton';
|
import { GoalAddButton } from './GoalAddButton';
|
||||||
|
|
||||||
|
|
@ -13,13 +13,17 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
const isSharePage = pathname.includes('/share/');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<SectionHeader>
|
{!isSharePage && (
|
||||||
<GoalAddButton websiteId={websiteId} />
|
<SectionHeader>
|
||||||
</SectionHeader>
|
<GoalAddButton websiteId={websiteId} />
|
||||||
|
</SectionHeader>
|
||||||
|
)}
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,15 @@ export interface JourneyProps {
|
||||||
steps: number;
|
steps: number;
|
||||||
startStep?: string;
|
startStep?: string;
|
||||||
endStep?: string;
|
endStep?: string;
|
||||||
|
view: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps) {
|
const EVENT_TYPES = {
|
||||||
|
views: 1,
|
||||||
|
events: 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Journey({ websiteId, steps, startStep, endStep, view }: JourneyProps) {
|
||||||
const [selectedNode, setSelectedNode] = useState(null);
|
const [selectedNode, setSelectedNode] = useState(null);
|
||||||
const [activeNode, setActiveNode] = useState(null);
|
const [activeNode, setActiveNode] = useState(null);
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
@ -32,6 +38,8 @@ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps)
|
||||||
steps,
|
steps,
|
||||||
startStep,
|
startStep,
|
||||||
endStep,
|
endStep,
|
||||||
|
view,
|
||||||
|
eventType: EVENT_TYPES[view],
|
||||||
});
|
});
|
||||||
|
|
||||||
useEscapeKey(() => setSelectedNode(null));
|
useEscapeKey(() => setSelectedNode(null));
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Grid, ListItem, SearchField, Select } from '@umami/react-zen';
|
import { Column, Grid, ListItem, Row, SearchField, Select } from '@umami/react-zen';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useDateRange, useMessages } from '@/components/hooks';
|
import { useDateRange, useMessages } from '@/components/hooks';
|
||||||
|
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||||
import { Journey } from './Journey';
|
import { Journey } from './Journey';
|
||||||
|
|
||||||
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
|
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
|
||||||
|
|
@ -14,10 +15,26 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
||||||
const {
|
const {
|
||||||
dateRange: { startDate, endDate },
|
dateRange: { startDate, endDate },
|
||||||
} = useDateRange();
|
} = useDateRange();
|
||||||
|
const [view, setView] = useState('all');
|
||||||
const [steps, setSteps] = useState(DEFAULT_STEP);
|
const [steps, setSteps] = useState(DEFAULT_STEP);
|
||||||
const [startStep, setStartStep] = useState('');
|
const [startStep, setStartStep] = useState('');
|
||||||
const [endStep, setEndStep] = useState('');
|
const [endStep, setEndStep] = useState('');
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
{
|
||||||
|
id: 'all',
|
||||||
|
label: formatMessage(labels.all),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'views',
|
||||||
|
label: formatMessage(labels.views),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'events',
|
||||||
|
label: formatMessage(labels.events),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
|
|
@ -52,6 +69,9 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
||||||
/>
|
/>
|
||||||
</Column>
|
</Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<Row justifyContent="flex-end">
|
||||||
|
<FilterButtons items={buttons} value={view} onChange={setView} />
|
||||||
|
</Row>
|
||||||
<Panel height="900px" allowFullscreen>
|
<Panel height="900px" allowFullscreen>
|
||||||
<Journey
|
<Journey
|
||||||
websiteId={websiteId}
|
websiteId={websiteId}
|
||||||
|
|
@ -60,6 +80,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
||||||
steps={steps}
|
steps={steps}
|
||||||
startStep={startStep}
|
startStep={startStep}
|
||||||
endStep={endStep}
|
endStep={endStep}
|
||||||
|
view={view}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,10 @@ import { ListTable } from '@/components/metrics/ListTable';
|
||||||
import { MetricCard } from '@/components/metrics/MetricCard';
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
import { renderDateLabels } from '@/lib/charts';
|
import { renderDateLabels } from '@/lib/charts';
|
||||||
import { CHART_COLORS } from '@/lib/constants';
|
import { CHART_COLORS, CURRENCY_CONFIG, DEFAULT_CURRENCY } from '@/lib/constants';
|
||||||
import { generateTimeSeries } from '@/lib/date';
|
import { generateTimeSeries } from '@/lib/date';
|
||||||
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
import { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||||
|
import { getItem, setItem } from '@/lib/storage';
|
||||||
|
|
||||||
export interface RevenueProps {
|
export interface RevenueProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
|
|
@ -24,7 +25,15 @@ export interface RevenueProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
const [currency, setCurrency] = useState('USD');
|
const [currency, setCurrency] = useState(
|
||||||
|
getItem(CURRENCY_CONFIG) || process.env.defaultCurrency || DEFAULT_CURRENCY,
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleCurrencyChange = (value: string) => {
|
||||||
|
setCurrency(value);
|
||||||
|
setItem(CURRENCY_CONFIG, value);
|
||||||
|
};
|
||||||
|
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
const { locale, dateLocale } = useLocale();
|
const { locale, dateLocale } = useLocale();
|
||||||
const { countryNames } = useCountryNames(locale);
|
const { countryNames } = useCountryNames(locale);
|
||||||
|
|
@ -107,7 +116,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<Grid columns="280px" gap>
|
<Grid columns="280px" gap>
|
||||||
<CurrencySelect value={currency} onChange={setCurrency} />
|
<CurrencySelect value={currency} onChange={handleCurrencyChange} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
{data && (
|
{data && (
|
||||||
|
|
|
||||||
|
|
@ -21,30 +21,28 @@ export function WebsiteChart({
|
||||||
const { pageviews, sessions, compare } = (data || {}) as any;
|
const { pageviews, sessions, compare } = (data || {}) as any;
|
||||||
|
|
||||||
const chartData = useMemo(() => {
|
const chartData = useMemo(() => {
|
||||||
if (data) {
|
if (!data) {
|
||||||
const result = {
|
return { pageviews: [], sessions: [] };
|
||||||
pageviews,
|
}
|
||||||
sessions,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (compare) {
|
return {
|
||||||
result.compare = {
|
pageviews,
|
||||||
pageviews: result.pageviews.map(({ x }, i) => ({
|
sessions,
|
||||||
|
...(compare && {
|
||||||
|
compare: {
|
||||||
|
pageviews: pageviews.map(({ x }, i) => ({
|
||||||
x,
|
x,
|
||||||
y: compare.pageviews[i]?.y,
|
y: compare.pageviews[i]?.y,
|
||||||
d: compare.pageviews[i]?.x,
|
d: compare.pageviews[i]?.x,
|
||||||
})),
|
})),
|
||||||
sessions: result.sessions.map(({ x }, i) => ({
|
sessions: sessions.map(({ x }, i) => ({
|
||||||
x,
|
x,
|
||||||
y: compare.sessions[i]?.y,
|
y: compare.sessions[i]?.y,
|
||||||
d: compare.sessions[i]?.x,
|
d: compare.sessions[i]?.x,
|
||||||
})),
|
})),
|
||||||
};
|
},
|
||||||
}
|
}),
|
||||||
|
};
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return { pageviews: [], sessions: [] };
|
|
||||||
}, [data, startDate, endDate, unit]);
|
}, [data, startDate, endDate, unit]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -169,6 +169,12 @@ export function WebsiteExpandedMenu({
|
||||||
path: updateParams({ view: 'hostname' }),
|
path: updateParams({ view: 'hostname' }),
|
||||||
icon: <Network />,
|
icon: <Network />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'distinctId',
|
||||||
|
label: formatMessage(labels.distinctId),
|
||||||
|
path: updateParams({ view: 'distinctId' }),
|
||||||
|
icon: <Tag />,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tag',
|
id: 'tag',
|
||||||
label: formatMessage(labels.tag),
|
label: formatMessage(labels.tag),
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
import { Icon, Row, Text } from '@umami/react-zen';
|
import { Icon, Row, Text } from '@umami/react-zen';
|
||||||
import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
|
|
||||||
import { Favicon } from '@/components/common/Favicon';
|
import { Favicon } from '@/components/common/Favicon';
|
||||||
import { LinkButton } from '@/components/common/LinkButton';
|
import { LinkButton } from '@/components/common/LinkButton';
|
||||||
import { PageHeader } from '@/components/common/PageHeader';
|
import { PageHeader } from '@/components/common/PageHeader';
|
||||||
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||||
import { Edit, Share } from '@/components/icons';
|
import { Edit } from '@/components/icons';
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
|
||||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||||
|
|
||||||
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
export function WebsiteHeader({
|
||||||
|
showActions,
|
||||||
|
allowLink = true,
|
||||||
|
}: {
|
||||||
|
showActions?: boolean;
|
||||||
|
allowLink?: boolean;
|
||||||
|
}) {
|
||||||
const website = useWebsite();
|
const website = useWebsite();
|
||||||
const { renderUrl, pathname } = useNavigation();
|
const { renderUrl, pathname } = useNavigation();
|
||||||
const isSettings = pathname.endsWith('/settings');
|
const isSettings = pathname.endsWith('/settings');
|
||||||
|
|
@ -23,35 +27,20 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={website.name}
|
title={website.name}
|
||||||
icon={<Favicon domain={website.domain} />}
|
icon={<Favicon domain={website.domain} />}
|
||||||
titleHref={renderUrl(`/websites/${website.id}`, false)}
|
titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
|
||||||
>
|
>
|
||||||
<Row alignItems="center" gap="6" wrap="wrap">
|
<Row alignItems="center" gap="6" wrap="wrap">
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<Row alignItems="center" gap>
|
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
<Icon>
|
||||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
<Edit />
|
||||||
<Icon>
|
</Icon>
|
||||||
<Edit />
|
<Text>{formatMessage(labels.edit)}</Text>
|
||||||
</Icon>
|
</LinkButton>
|
||||||
<Text>{formatMessage(labels.edit)}</Text>
|
|
||||||
</LinkButton>
|
|
||||||
</Row>
|
|
||||||
)}
|
)}
|
||||||
</Row>
|
</Row>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShareButton = ({ websiteId, shareId }) => {
|
|
||||||
const { formatMessage, labels } = useMessages();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DialogButton icon={<Share />} label={formatMessage(labels.share)} width="800px">
|
|
||||||
{({ close }) => {
|
|
||||||
return <WebsiteShareForm websiteId={websiteId} shareId={shareId} onClose={close} />;
|
|
||||||
}}
|
|
||||||
</DialogButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ import {
|
||||||
} from '@umami/react-zen';
|
} from '@umami/react-zen';
|
||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { useMessages, useNavigation } from '@/components/hooks';
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
import { Edit, More, Share } from '@/components/icons';
|
import { Edit, MoreHorizontal, Share } from '@/components/icons';
|
||||||
|
|
||||||
export function WebsiteMenu({ websiteId }: { websiteId: string }) {
|
export function WebsiteMenu({ websiteId }: { websiteId: string }) {
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
@ -33,7 +33,7 @@ export function WebsiteMenu({ websiteId }: { websiteId: string }) {
|
||||||
<MenuTrigger>
|
<MenuTrigger>
|
||||||
<Button variant="quiet">
|
<Button variant="quiet">
|
||||||
<Icon>
|
<Icon>
|
||||||
<More />
|
<MoreHorizontal />
|
||||||
</Icon>
|
</Icon>
|
||||||
</Button>
|
</Button>
|
||||||
<Popover placement="bottom">
|
<Popover placement="bottom">
|
||||||
|
|
|
||||||
|
|
@ -7,14 +7,18 @@ import { formatLongNumber, formatShortTime } from '@/lib/format';
|
||||||
|
|
||||||
export function WebsiteMetricsBar({
|
export function WebsiteMetricsBar({
|
||||||
websiteId,
|
websiteId,
|
||||||
|
compareMode,
|
||||||
}: {
|
}: {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
showChange?: boolean;
|
showChange?: boolean;
|
||||||
compareMode?: boolean;
|
compareMode?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { isAllTime } = useDateRange();
|
const { isAllTime, dateCompare } = useDateRange();
|
||||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery(websiteId);
|
const { data, isLoading, isFetching, error } = useWebsiteStatsQuery({
|
||||||
|
websiteId,
|
||||||
|
compare: compareMode ? dateCompare?.compare : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
|
const { pageviews, visitors, visits, bounces, totaltime, comparison } = data || {};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export function WebsiteNav({
|
||||||
event: undefined,
|
event: undefined,
|
||||||
compare: undefined,
|
compare: undefined,
|
||||||
view: undefined,
|
view: undefined,
|
||||||
|
unit: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = [
|
const items = [
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column, Row } from '@umami/react-zen';
|
||||||
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
|
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
|
import { UnitFilter } from '@/components/input/UnitFilter';
|
||||||
import { WebsiteChart } from './WebsiteChart';
|
import { WebsiteChart } from './WebsiteChart';
|
||||||
import { WebsiteControls } from './WebsiteControls';
|
import { WebsiteControls } from './WebsiteControls';
|
||||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||||
|
|
@ -13,6 +14,9 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
||||||
<Panel minHeight="520px">
|
<Panel minHeight="520px">
|
||||||
|
<Row justifyContent="end">
|
||||||
|
<UnitFilter />
|
||||||
|
</Row>
|
||||||
<WebsiteChart websiteId={websiteId} />
|
<WebsiteChart websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<WebsitePanels websiteId={websiteId} />
|
<WebsitePanels websiteId={websiteId} />
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) {
|
||||||
return (
|
return (
|
||||||
<Column gap>
|
<Column gap>
|
||||||
<WebsiteControls websiteId={websiteId} allowCompare={true} />
|
<WebsiteControls websiteId={websiteId} allowCompare={true} />
|
||||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showChange={true} />
|
||||||
<Panel minHeight="520px">
|
<Panel minHeight="520px">
|
||||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,11 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
||||||
label: formatMessage(labels.hostname),
|
label: formatMessage(labels.hostname),
|
||||||
path: renderPath('hostname'),
|
path: renderPath('hostname'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'distinctId',
|
||||||
|
label: formatMessage(labels.distinctId),
|
||||||
|
path: renderPath('distinctId'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tag',
|
id: 'tag',
|
||||||
label: formatMessage(labels.tags),
|
label: formatMessage(labels.tags),
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
import { Column, Tab, TabList, TabPanel, Tabs } from '@umami/react-zen';
|
||||||
import { type Key, useState } from 'react';
|
import locale from 'date-fns/locale/af';
|
||||||
|
import { type Key, useMemo, useState } from 'react';
|
||||||
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
|
import { SessionModal } from '@/app/(main)/websites/[websiteId]/sessions/SessionModal';
|
||||||
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||||
|
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useMessages } from '@/components/hooks';
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { useEventStatsQuery } from '@/components/hooks/queries/useEventStatsQuery';
|
||||||
import { EventsChart } from '@/components/metrics/EventsChart';
|
import { EventsChart } from '@/components/metrics/EventsChart';
|
||||||
|
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||||
|
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||||
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
import { MetricsTable } from '@/components/metrics/MetricsTable';
|
||||||
|
import { formatLongNumber } from '@/lib/format';
|
||||||
import { getItem, setItem } from '@/lib/storage';
|
import { getItem, setItem } from '@/lib/storage';
|
||||||
import { EventProperties } from './EventProperties';
|
import { EventProperties } from './EventProperties';
|
||||||
import { EventsDataTable } from './EventsDataTable';
|
import { EventsDataTable } from './EventsDataTable';
|
||||||
|
|
@ -15,16 +21,61 @@ const KEY_NAME = 'umami.events.tab';
|
||||||
|
|
||||||
export function EventsPage({ websiteId }) {
|
export function EventsPage({ websiteId }) {
|
||||||
const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
|
const [tab, setTab] = useState(getItem(KEY_NAME) || 'chart');
|
||||||
const { formatMessage, labels } = useMessages();
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
|
const { data, isLoading, isFetching, error } = useEventStatsQuery({
|
||||||
|
websiteId,
|
||||||
|
});
|
||||||
|
|
||||||
const handleSelect = (value: Key) => {
|
const handleSelect = (value: Key) => {
|
||||||
setItem(KEY_NAME, value);
|
setItem(KEY_NAME, value);
|
||||||
setTab(value);
|
setTab(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const metrics = useMemo(() => {
|
||||||
|
if (!data) return [];
|
||||||
|
|
||||||
|
const { events, visitors, visits, uniqueEvents } = data || {};
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
value: visitors,
|
||||||
|
label: formatMessage(labels.visitors),
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: visits,
|
||||||
|
label: formatMessage(labels.visits),
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: events,
|
||||||
|
label: formatMessage(labels.events),
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: uniqueEvents,
|
||||||
|
label: formatMessage(labels.uniqueEvents),
|
||||||
|
formatValue: formatLongNumber,
|
||||||
|
},
|
||||||
|
] as any;
|
||||||
|
}, [data, locale]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="3">
|
<Column gap="3">
|
||||||
<WebsiteControls websiteId={websiteId} />
|
<WebsiteControls websiteId={websiteId} />
|
||||||
|
<LoadingPanel
|
||||||
|
data={metrics}
|
||||||
|
isLoading={isLoading}
|
||||||
|
isFetching={isFetching}
|
||||||
|
error={getErrorMessage(error)}
|
||||||
|
minHeight="136px"
|
||||||
|
>
|
||||||
|
<MetricsBar>
|
||||||
|
{metrics?.map(({ label, value, formatValue }) => {
|
||||||
|
return <MetricCard key={label} value={value} label={label} formatValue={formatValue} />;
|
||||||
|
})}
|
||||||
|
</MetricsBar>
|
||||||
|
</LoadingPanel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<Tabs selectedKey={tab} onSelectionChange={key => handleSelect(key)}>
|
<Tabs selectedKey={tab} onSelectionChange={key => handleSelect(key)}>
|
||||||
<TabList>
|
<TabList>
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,19 @@ export function EventsTable(props: DataTableProps) {
|
||||||
const { updateParams } = useNavigation();
|
const { updateParams } = useNavigation();
|
||||||
const { formatValue } = useFormat();
|
const { formatValue } = useFormat();
|
||||||
|
|
||||||
|
const renderLink = (label: string, hostname: string) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`//${hostname}${label}`}
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataTable {...props}>
|
<DataTable {...props}>
|
||||||
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
||||||
|
|
@ -43,7 +56,7 @@ export function EventsTable(props: DataTableProps) {
|
||||||
title={row.eventName || row.urlPath}
|
title={row.eventName || row.urlPath}
|
||||||
truncate
|
truncate
|
||||||
>
|
>
|
||||||
{row.eventName || row.urlPath}
|
{row.eventName || renderLink(row.urlPath, row.hostname)}
|
||||||
</Text>
|
</Text>
|
||||||
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
|
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Metadata } from 'next';
|
import type { Metadata } from 'next';
|
||||||
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
|
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
|
||||||
|
import { getWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
export default async function ({
|
export default async function ({
|
||||||
children,
|
children,
|
||||||
|
|
@ -9,6 +10,11 @@ export default async function ({
|
||||||
params: Promise<{ websiteId: string }>;
|
params: Promise<{ websiteId: string }>;
|
||||||
}) {
|
}) {
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
|
const website = await getWebsite(websiteId);
|
||||||
|
|
||||||
|
if (!website || website?.deletedAt) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
|
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -74,8 +74,9 @@ export function RealtimeLog({ data }: { data: any }) {
|
||||||
os: string;
|
os: string;
|
||||||
country: string;
|
country: string;
|
||||||
device: string;
|
device: string;
|
||||||
|
hostname: string;
|
||||||
}) => {
|
}) => {
|
||||||
const { __type, eventName, urlPath, browser, os, country, device } = log;
|
const { __type, eventName, urlPath, browser, os, country, device, hostname } = log;
|
||||||
|
|
||||||
if (__type === TYPE_EVENT) {
|
if (__type === TYPE_EVENT) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -86,7 +87,8 @@ export function RealtimeLog({ data }: { data: any }) {
|
||||||
url: (
|
url: (
|
||||||
<a
|
<a
|
||||||
key="a"
|
key="a"
|
||||||
href={`//${website?.domain}${urlPath}`}
|
href={`//${hostname}${urlPath}`}
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
|
|
@ -100,7 +102,12 @@ export function RealtimeLog({ data }: { data: any }) {
|
||||||
|
|
||||||
if (__type === TYPE_PAGEVIEW) {
|
if (__type === TYPE_PAGEVIEW) {
|
||||||
return (
|
return (
|
||||||
<a href={`//${website?.domain}${urlPath}`} target="_blank" rel="noreferrer noopener">
|
<a
|
||||||
|
href={`//${hostname}${urlPath}`}
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
{urlPath}
|
{urlPath}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -39,10 +39,23 @@ export function SessionActivity({
|
||||||
const { isMobile } = useMobile();
|
const { isMobile } = useMobile();
|
||||||
let lastDay = null;
|
let lastDay = null;
|
||||||
|
|
||||||
|
const renderLink = (label: string, hostname: string) => {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={`//${hostname}${label}`}
|
||||||
|
style={{ fontWeight: 'bold' }}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||||
<Column gap>
|
<Column gap>
|
||||||
{data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hasData }) => {
|
{data?.map(({ eventId, createdAt, urlPath, eventName, visitId, hostname, hasData }) => {
|
||||||
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
|
const showHeader = !lastDay || !isSameDay(new Date(lastDay), new Date(createdAt));
|
||||||
lastDay = createdAt;
|
lastDay = createdAt;
|
||||||
|
|
||||||
|
|
@ -61,7 +74,7 @@ export function SessionActivity({
|
||||||
: formatMessage(labels.viewedPage)}
|
: formatMessage(labels.viewedPage)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text weight="bold" style={{ maxWidth: isMobile ? '400px' : null }} truncate>
|
<Text weight="bold" style={{ maxWidth: isMobile ? '400px' : null }} truncate>
|
||||||
{eventName || urlPath}
|
{eventName || renderLink(urlPath, hostname)}
|
||||||
</Text>
|
</Text>
|
||||||
{hasData > 0 && <PropertiesButton websiteId={websiteId} eventId={eventId} />}
|
{hasData > 0 && <PropertiesButton websiteId={websiteId} eventId={eventId} />}
|
||||||
</Row>
|
</Row>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { ConfirmationForm } from '@/components/common/ConfirmationForm';
|
||||||
|
import { useDeleteQuery, useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { Trash } from '@/components/icons';
|
||||||
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
|
import { messages } from '@/components/messages';
|
||||||
|
|
||||||
|
export function ShareDeleteButton({
|
||||||
|
shareId,
|
||||||
|
slug,
|
||||||
|
onSave,
|
||||||
|
}: {
|
||||||
|
shareId: string;
|
||||||
|
slug: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels, getErrorMessage, FormattedMessage } = useMessages();
|
||||||
|
const { mutateAsync, isPending, error } = useDeleteQuery(`/share/id/${shareId}`);
|
||||||
|
const { touch } = useModified();
|
||||||
|
|
||||||
|
const handleConfirm = async (close: () => void) => {
|
||||||
|
await mutateAsync(null, {
|
||||||
|
onSuccess: () => {
|
||||||
|
touch('shares');
|
||||||
|
onSave?.();
|
||||||
|
close();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogButton
|
||||||
|
icon={<Trash />}
|
||||||
|
title={formatMessage(labels.confirm)}
|
||||||
|
variant="quiet"
|
||||||
|
width="400px"
|
||||||
|
>
|
||||||
|
{({ close }) => (
|
||||||
|
<ConfirmationForm
|
||||||
|
message={
|
||||||
|
<FormattedMessage
|
||||||
|
{...messages.confirmRemove}
|
||||||
|
values={{
|
||||||
|
target: <b>{slug}</b>,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
isLoading={isPending}
|
||||||
|
error={getErrorMessage(error)}
|
||||||
|
onConfirm={handleConfirm.bind(null, close)}
|
||||||
|
onClose={close}
|
||||||
|
buttonLabel={formatMessage(labels.delete)}
|
||||||
|
buttonVariant="danger"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DialogButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { useMessages } from '@/components/hooks';
|
||||||
|
import { Edit } from '@/components/icons';
|
||||||
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
|
import { ShareEditForm } from './ShareEditForm';
|
||||||
|
|
||||||
|
export function ShareEditButton({ shareId }: { shareId: string }) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DialogButton icon={<Edit />} title={formatMessage(labels.share)} variant="quiet" width="600px">
|
||||||
|
{({ close }) => {
|
||||||
|
return <ShareEditForm shareId={shareId} onClose={close} />;
|
||||||
|
}}
|
||||||
|
</DialogButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
164
src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
Normal file
164
src/app/(main)/websites/[websiteId]/settings/ShareEditForm.tsx
Normal file
|
|
@ -0,0 +1,164 @@
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Checkbox,
|
||||||
|
Column,
|
||||||
|
Form,
|
||||||
|
FormField,
|
||||||
|
FormSubmitButton,
|
||||||
|
Grid,
|
||||||
|
Label,
|
||||||
|
Loading,
|
||||||
|
Row,
|
||||||
|
Text,
|
||||||
|
TextField,
|
||||||
|
} from '@umami/react-zen';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
||||||
|
import { SHARE_NAV_ITEMS } from './constants';
|
||||||
|
|
||||||
|
export function ShareEditForm({
|
||||||
|
shareId,
|
||||||
|
websiteId,
|
||||||
|
onSave,
|
||||||
|
onClose,
|
||||||
|
}: {
|
||||||
|
shareId?: string;
|
||||||
|
websiteId?: string;
|
||||||
|
onSave?: () => void;
|
||||||
|
onClose?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||||
|
const { cloudMode } = useConfig();
|
||||||
|
const { get, post } = useApi();
|
||||||
|
const { touch } = useModified();
|
||||||
|
const { modified } = useModified('shares');
|
||||||
|
const [share, setShare] = useState<any>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(!!shareId);
|
||||||
|
const [isPending, setIsPending] = useState(false);
|
||||||
|
const [error, setError] = useState<any>(null);
|
||||||
|
|
||||||
|
const isEditing = !!shareId;
|
||||||
|
|
||||||
|
const getUrl = (slug: string) => {
|
||||||
|
if (cloudMode) {
|
||||||
|
return `${process.env.cloudUrl}/share/${slug}`;
|
||||||
|
}
|
||||||
|
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!shareId) return;
|
||||||
|
|
||||||
|
const loadShare = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await get(`/share/id/${shareId}`);
|
||||||
|
setShare(data);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadShare();
|
||||||
|
}, [shareId, modified]);
|
||||||
|
|
||||||
|
const handleSubmit = async (data: any) => {
|
||||||
|
const parameters: Record<string, boolean> = {};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
parameters[item.id] = data[item.id] ?? false;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setIsPending(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (isEditing) {
|
||||||
|
await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters });
|
||||||
|
} else {
|
||||||
|
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
|
||||||
|
}
|
||||||
|
touch('shares');
|
||||||
|
onSave?.();
|
||||||
|
onClose?.();
|
||||||
|
} catch (e) {
|
||||||
|
setError(e);
|
||||||
|
} finally {
|
||||||
|
setIsPending(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <Loading placement="absolute" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = isEditing ? getUrl(share?.slug || '') : null;
|
||||||
|
|
||||||
|
// Build default values from share parameters
|
||||||
|
const defaultValues: Record<string, any> = {
|
||||||
|
name: share?.name || '',
|
||||||
|
};
|
||||||
|
SHARE_NAV_ITEMS.forEach(section => {
|
||||||
|
section.items.forEach(item => {
|
||||||
|
const defaultSelected = item.id === 'overview' || item.id === 'events';
|
||||||
|
defaultValues[item.id] = share?.parameters?.[item.id] ?? defaultSelected;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all item ids for validation
|
||||||
|
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
||||||
|
{({ watch }) => {
|
||||||
|
const values = watch();
|
||||||
|
const hasSelection = allItemIds.some(id => values[id]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column gap="6">
|
||||||
|
{url && (
|
||||||
|
<Column>
|
||||||
|
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||||
|
<TextField value={url} isReadOnly allowCopy />
|
||||||
|
</Column>
|
||||||
|
)}
|
||||||
|
<FormField
|
||||||
|
label={formatMessage(labels.name)}
|
||||||
|
name="name"
|
||||||
|
rules={{ required: formatMessage(labels.required) }}
|
||||||
|
>
|
||||||
|
<TextField autoComplete="off" autoFocus={!isEditing} />
|
||||||
|
</FormField>
|
||||||
|
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
|
||||||
|
{SHARE_NAV_ITEMS.map(section => (
|
||||||
|
<Column key={section.section} gap="3">
|
||||||
|
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
||||||
|
<Column gap="1">
|
||||||
|
{section.items.map(item => (
|
||||||
|
<FormField key={item.id} name={item.id}>
|
||||||
|
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||||
|
</FormField>
|
||||||
|
))}
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||||
|
{onClose && (
|
||||||
|
<Button isDisabled={isPending} onPress={onClose}>
|
||||||
|
{formatMessage(labels.cancel)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<FormSubmitButton
|
||||||
|
variant="primary"
|
||||||
|
isDisabled={isPending || !hasSelection || !values.name}
|
||||||
|
>
|
||||||
|
{formatMessage(labels.save)}
|
||||||
|
</FormSubmitButton>
|
||||||
|
</Row>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</Form>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
Normal file
46
src/app/(main)/websites/[websiteId]/settings/SharesTable.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen';
|
||||||
|
import { DateDistance } from '@/components/common/DateDistance';
|
||||||
|
import { ExternalLink } from '@/components/common/ExternalLink';
|
||||||
|
import { useConfig, useMessages } from '@/components/hooks';
|
||||||
|
import { ShareDeleteButton } from './ShareDeleteButton';
|
||||||
|
import { ShareEditButton } from './ShareEditButton';
|
||||||
|
|
||||||
|
export function SharesTable(props: DataTableProps) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { cloudMode } = useConfig();
|
||||||
|
|
||||||
|
const getUrl = (slug: string) => {
|
||||||
|
return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataTable {...props}>
|
||||||
|
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||||
|
{({ name }: any) => name}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
||||||
|
{({ slug }: any) => {
|
||||||
|
const url = getUrl(slug);
|
||||||
|
return (
|
||||||
|
<ExternalLink href={url} prefetch={false}>
|
||||||
|
{url}
|
||||||
|
</ExternalLink>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="created" label={formatMessage(labels.created)} width="200px">
|
||||||
|
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||||
|
</DataColumn>
|
||||||
|
<DataColumn id="action" align="end" width="100px">
|
||||||
|
{({ id, slug }: any) => {
|
||||||
|
return (
|
||||||
|
<Row>
|
||||||
|
<ShareEditButton shareId={id} />
|
||||||
|
<ShareDeleteButton shareId={id} slug={slug} />
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</DataColumn>
|
||||||
|
</DataTable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,11 @@
|
||||||
import { Column } from '@umami/react-zen';
|
import { Column } from '@umami/react-zen';
|
||||||
import { Panel } from '@/components/common/Panel';
|
import { Panel } from '@/components/common/Panel';
|
||||||
import { useWebsite } from '@/components/hooks';
|
|
||||||
import { WebsiteData } from './WebsiteData';
|
import { WebsiteData } from './WebsiteData';
|
||||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||||
|
|
||||||
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
||||||
const website = useWebsite();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column gap="6">
|
<Column gap="6">
|
||||||
<Panel>
|
<Panel>
|
||||||
|
|
@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
|
||||||
<WebsiteTrackingCode websiteId={websiteId} />
|
<WebsiteTrackingCode websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteShareForm websiteId={websiteId} shareId={website.shareId} />
|
<WebsiteShareForm websiteId={websiteId} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel>
|
<Panel>
|
||||||
<WebsiteData websiteId={websiteId} />
|
<WebsiteData websiteId={websiteId} />
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,43 @@
|
||||||
import {
|
import { Column, Heading, Row, Text } from '@umami/react-zen';
|
||||||
Button,
|
import { Plus } from 'lucide-react';
|
||||||
Column,
|
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||||
Form,
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
FormButtons,
|
import { ShareEditForm } from './ShareEditForm';
|
||||||
FormSubmitButton,
|
import { SharesTable } from './SharesTable';
|
||||||
IconLabel,
|
|
||||||
Label,
|
|
||||||
Row,
|
|
||||||
Switch,
|
|
||||||
TextField,
|
|
||||||
} from '@umami/react-zen';
|
|
||||||
import { RefreshCcw } from 'lucide-react';
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { useConfig, useMessages, useUpdateQuery } from '@/components/hooks';
|
|
||||||
import { getRandomChars } from '@/lib/generate';
|
|
||||||
|
|
||||||
const generateId = () => getRandomChars(16);
|
|
||||||
|
|
||||||
export interface WebsiteShareFormProps {
|
export interface WebsiteShareFormProps {
|
||||||
websiteId: string;
|
websiteId: string;
|
||||||
shareId?: string;
|
|
||||||
onSave?: () => void;
|
|
||||||
onClose?: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
const { formatMessage, labels, messages } = useMessages();
|
||||||
const [currentId, setCurrentId] = useState(shareId);
|
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
||||||
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
|
||||||
const { cloudMode } = useConfig();
|
|
||||||
|
|
||||||
const getUrl = (shareId: string) => {
|
const shares = data?.data || [];
|
||||||
if (cloudMode) {
|
const hasShares = shares.length > 0;
|
||||||
return `${process.env.cloudUrl}/share/${shareId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${shareId}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const url = getUrl(currentId);
|
|
||||||
|
|
||||||
const handleGenerate = () => {
|
|
||||||
setCurrentId(generateId());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSwitch = () => {
|
|
||||||
setCurrentId(currentId ? null : generateId());
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
const data = {
|
|
||||||
shareId: currentId,
|
|
||||||
};
|
|
||||||
await mutateAsync(data, {
|
|
||||||
onSuccess: async () => {
|
|
||||||
toast(formatMessage(messages.saved));
|
|
||||||
touch(`website:${websiteId}`);
|
|
||||||
onSave?.();
|
|
||||||
onClose?.();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form onSubmit={handleSave} error={getErrorMessage(error)} values={{ url }}>
|
<Column gap="4">
|
||||||
<Column gap>
|
<Row justifyContent="space-between" alignItems="center">
|
||||||
<Switch isSelected={!!currentId} onChange={handleSwitch}>
|
<Heading>{formatMessage(labels.share)}</Heading>
|
||||||
{formatMessage(labels.enableShareUrl)}
|
<DialogButton
|
||||||
</Switch>
|
icon={<Plus size={16} />}
|
||||||
{currentId && (
|
label={formatMessage(labels.add)}
|
||||||
<Row alignItems="flex-end" gap>
|
title={formatMessage(labels.share)}
|
||||||
<Column flexGrow={1}>
|
variant="primary"
|
||||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
width="600px"
|
||||||
<TextField value={url} isReadOnly allowCopy />
|
>
|
||||||
</Column>
|
{({ close }) => <ShareEditForm websiteId={websiteId} onClose={close} />}
|
||||||
<Column>
|
</DialogButton>
|
||||||
<Button onPress={handleGenerate}>
|
</Row>
|
||||||
<IconLabel icon={<RefreshCcw />} label={formatMessage(labels.regenerate)} />
|
{hasShares ? (
|
||||||
</Button>
|
<>
|
||||||
</Column>
|
<Text>{formatMessage(messages.shareUrl)}</Text>
|
||||||
</Row>
|
<SharesTable data={shares} />
|
||||||
)}
|
</>
|
||||||
<FormButtons justifyContent="flex-end">
|
) : (
|
||||||
<Row alignItems="center" gap>
|
<Text color="muted">{formatMessage(messages.noDataAvailable)}</Text>
|
||||||
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
)}
|
||||||
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
|
</Column>
|
||||||
</Row>
|
|
||||||
</FormButtons>
|
|
||||||
</Column>
|
|
||||||
</Form>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
30
src/app/(main)/websites/[websiteId]/settings/constants.ts
Normal file
30
src/app/(main)/websites/[websiteId]/settings/constants.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
export const SHARE_NAV_ITEMS = [
|
||||||
|
{
|
||||||
|
section: 'traffic',
|
||||||
|
items: [
|
||||||
|
{ id: 'overview', label: 'overview' },
|
||||||
|
{ id: 'events', label: 'events' },
|
||||||
|
{ id: 'sessions', label: 'sessions' },
|
||||||
|
{ id: 'realtime', label: 'realtime' },
|
||||||
|
{ id: 'compare', label: 'compare' },
|
||||||
|
{ id: 'breakdown', label: 'breakdown' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'behavior',
|
||||||
|
items: [
|
||||||
|
{ id: 'goals', label: 'goals' },
|
||||||
|
{ id: 'funnels', label: 'funnels' },
|
||||||
|
{ id: 'journeys', label: 'journeys' },
|
||||||
|
{ id: 'retention', label: 'retention' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'growth',
|
||||||
|
items: [
|
||||||
|
{ id: 'utm', label: 'utm' },
|
||||||
|
{ id: 'revenue', label: 'revenue' },
|
||||||
|
{ id: 'attribution', label: 'attribution' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
|
import { parseRequest } from '@/lib/request';
|
||||||
import { ok } from '@/lib/response';
|
import { ok } from '@/lib/response';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
|
const { error } = await parseRequest(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
if (redis.enabled) {
|
if (redis.enabled) {
|
||||||
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { saveAuth } from '@/lib/auth';
|
import { saveAuth } from '@/lib/auth';
|
||||||
import redis from '@/lib/redis';
|
import redis from '@/lib/redis';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { json } from '@/lib/response';
|
import { json, serverError } from '@/lib/response';
|
||||||
|
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const { auth, error } = await parseRequest(request);
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
@ -10,9 +10,13 @@ export async function POST(request: Request) {
|
||||||
return error();
|
return error();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (redis.enabled) {
|
if (!redis.enabled) {
|
||||||
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
return serverError({
|
||||||
|
message: 'Redis is disabled',
|
||||||
return json({ user: auth.user, token });
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
||||||
|
|
||||||
|
return json({ user: auth.user, token });
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,5 +17,6 @@ export async function GET(request: Request) {
|
||||||
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
||||||
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
||||||
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
||||||
|
currentVersion: !!process.env.currentVersion,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,16 @@ export async function POST(request: Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId, parameters, filters } = body;
|
const { websiteId, parameters, filters } = body;
|
||||||
|
const { eventType } = parameters;
|
||||||
|
|
||||||
if (!(await canViewWebsite(auth, websiteId))) {
|
if (!(await canViewWebsite(auth, websiteId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType) {
|
||||||
|
filters.eventType = eventType;
|
||||||
|
}
|
||||||
|
|
||||||
const queryFilters = await getQueryFilters(filters, websiteId);
|
const queryFilters = await getQueryFilters(filters, websiteId);
|
||||||
|
|
||||||
const data = await getJourney(websiteId, parameters, queryFilters);
|
const data = await getJourney(websiteId, parameters, queryFilters);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { secret } from '@/lib/crypto';
|
|
||||||
import { createToken } from '@/lib/jwt';
|
|
||||||
import { json, notFound } from '@/lib/response';
|
|
||||||
import { getSharedWebsite } from '@/queries/prisma';
|
|
||||||
|
|
||||||
export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
|
||||||
const { shareId } = await params;
|
|
||||||
|
|
||||||
const website = await getSharedWebsite(shareId);
|
|
||||||
|
|
||||||
if (!website) {
|
|
||||||
return notFound();
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = { websiteId: website.id };
|
|
||||||
const token = createToken(data, secret());
|
|
||||||
|
|
||||||
return json({ ...data, token });
|
|
||||||
}
|
|
||||||
76
src/app/api/share/[slug]/route.ts
Normal file
76
src/app/api/share/[slug]/route.ts
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
|
import { secret } from '@/lib/crypto';
|
||||||
|
import { createToken } from '@/lib/jwt';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import redis from '@/lib/redis';
|
||||||
|
import { json, notFound } from '@/lib/response';
|
||||||
|
import type { WhiteLabel } from '@/lib/types';
|
||||||
|
import { getShareByCode, getWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
|
async function getAccountId(website: { userId?: string; teamId?: string }): Promise<string | null> {
|
||||||
|
if (website.userId) {
|
||||||
|
return website.userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (website.teamId) {
|
||||||
|
const teamOwner = await prisma.client.teamUser.findFirst({
|
||||||
|
where: {
|
||||||
|
teamId: website.teamId,
|
||||||
|
role: ROLES.teamOwner,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
userId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return teamOwner?.userId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWhiteLabel(accountId: string): Promise<WhiteLabel | null> {
|
||||||
|
if (!redis.enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await redis.client.get(`white-label:${accountId}`);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
return data as WhiteLabel;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||||
|
const { slug } = await params;
|
||||||
|
|
||||||
|
const share = await getShareByCode(slug);
|
||||||
|
|
||||||
|
if (!share) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const website = await getWebsite(share.entityId);
|
||||||
|
|
||||||
|
const data: Record<string, any> = {
|
||||||
|
shareId: share.id,
|
||||||
|
websiteId: share.entityId,
|
||||||
|
parameters: share.parameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
data.token = createToken(data, secret());
|
||||||
|
|
||||||
|
const accountId = await getAccountId(website);
|
||||||
|
|
||||||
|
if (accountId) {
|
||||||
|
const whiteLabel = await getWhiteLabel(accountId);
|
||||||
|
|
||||||
|
if (whiteLabel) {
|
||||||
|
data.whiteLabel = whiteLabel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(data);
|
||||||
|
}
|
||||||
82
src/app/api/share/id/[shareId]/route.ts
Normal file
82
src/app/api/share/id/[shareId]/route.ts
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import z from 'zod';
|
||||||
|
import { parseRequest } from '@/lib/request';
|
||||||
|
import { json, notFound, ok, unauthorized } from '@/lib/response';
|
||||||
|
import { anyObjectParam } from '@/lib/schema';
|
||||||
|
import { canDeleteEntity, canUpdateEntity, canViewEntity } from '@/permissions';
|
||||||
|
import { deleteShare, getShare, updateShare } from '@/queries/prisma';
|
||||||
|
|
||||||
|
export async function GET(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
||||||
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { shareId } = await params;
|
||||||
|
|
||||||
|
const share = await getShare(shareId);
|
||||||
|
|
||||||
|
if (!(await canViewEntity(auth, share.entityId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
return json(share);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().max(200),
|
||||||
|
slug: z.string().max(100),
|
||||||
|
parameters: anyObjectParam,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { shareId } = await params;
|
||||||
|
const { name, slug, parameters } = body;
|
||||||
|
|
||||||
|
const share = await getShare(shareId);
|
||||||
|
|
||||||
|
if (!share) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(await canUpdateEntity(auth, share.entityId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await updateShare(shareId, {
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
parameters,
|
||||||
|
} as any);
|
||||||
|
|
||||||
|
return json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function DELETE(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ shareId: string }> },
|
||||||
|
) {
|
||||||
|
const { auth, error } = await parseRequest(request);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { shareId } = await params;
|
||||||
|
|
||||||
|
const share = await getShare(shareId);
|
||||||
|
|
||||||
|
if (!(await canDeleteEntity(auth, share.entityId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteShare(shareId);
|
||||||
|
|
||||||
|
return ok();
|
||||||
|
}
|
||||||
39
src/app/api/share/route.ts
Normal file
39
src/app/api/share/route.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
import z from 'zod';
|
||||||
|
import { uuid } from '@/lib/crypto';
|
||||||
|
import { getRandomChars } from '@/lib/generate';
|
||||||
|
import { parseRequest } from '@/lib/request';
|
||||||
|
import { json, unauthorized } from '@/lib/response';
|
||||||
|
import { anyObjectParam } from '@/lib/schema';
|
||||||
|
import { canUpdateEntity } from '@/permissions';
|
||||||
|
import { createShare } from '@/queries/prisma';
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const schema = z.object({
|
||||||
|
entityId: z.uuid(),
|
||||||
|
shareType: z.coerce.number().int(),
|
||||||
|
slug: z.string().max(100).optional(),
|
||||||
|
parameters: anyObjectParam,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { entityId, shareType, slug, parameters } = body;
|
||||||
|
|
||||||
|
if (!(await canUpdateEntity(auth, entityId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const share = await createShare({
|
||||||
|
id: uuid(),
|
||||||
|
entityId,
|
||||||
|
shareType,
|
||||||
|
slug: slug || getRandomChars(16),
|
||||||
|
parameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(share);
|
||||||
|
}
|
||||||
|
|
@ -28,6 +28,7 @@ export async function GET(request: Request) {
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().max(50),
|
name: z.string().max(50),
|
||||||
|
ownerId: z.uuid().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -40,7 +41,7 @@ export async function POST(request: Request) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { name } = body;
|
const { name, ownerId } = body;
|
||||||
|
|
||||||
const team = await createTeam(
|
const team = await createTeam(
|
||||||
{
|
{
|
||||||
|
|
@ -48,7 +49,7 @@ export async function POST(request: Request) {
|
||||||
name,
|
name,
|
||||||
accessCode: `team_${getRandomChars(16)}`,
|
accessCode: `team_${getRandomChars(16)}`,
|
||||||
},
|
},
|
||||||
auth.user.id,
|
ownerId ?? auth.user.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return json(team);
|
return json(team);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { hashPassword } from '@/lib/password';
|
import { hashPassword } from '@/lib/password';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { badRequest, json, ok, unauthorized } from '@/lib/response';
|
import { badRequest, json, notFound, ok, unauthorized } from '@/lib/response';
|
||||||
import { userRoleParam } from '@/lib/schema';
|
import { userRoleParam } from '@/lib/schema';
|
||||||
import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
|
import { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
|
||||||
import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma';
|
import { deleteUser, getUser, getUserByUsername, updateUser } from '@/queries/prisma';
|
||||||
|
|
@ -27,7 +27,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ user
|
||||||
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
export async function POST(request: Request, { params }: { params: Promise<{ userId: string }> }) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
username: z.string().max(255).optional(),
|
username: z.string().max(255).optional(),
|
||||||
password: z.string().max(255).optional(),
|
password: z.string().min(8).max(255).optional(),
|
||||||
role: userRoleParam.optional(),
|
role: userRoleParam.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -47,6 +47,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ use
|
||||||
|
|
||||||
const user = await getUser(userId);
|
const user = await getUser(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
const data: any = {};
|
const data: any = {};
|
||||||
|
|
||||||
if (password) {
|
if (password) {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { uuid } from '@/lib/crypto';
|
||||||
import { hashPassword } from '@/lib/password';
|
import { hashPassword } from '@/lib/password';
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { badRequest, json, unauthorized } from '@/lib/response';
|
import { badRequest, json, unauthorized } from '@/lib/response';
|
||||||
|
import { userRoleParam } from '@/lib/schema';
|
||||||
import { canCreateUser } from '@/permissions';
|
import { canCreateUser } from '@/permissions';
|
||||||
import { createUser, getUserByUsername } from '@/queries/prisma';
|
import { createUser, getUserByUsername } from '@/queries/prisma';
|
||||||
|
|
||||||
|
|
@ -11,8 +12,8 @@ export async function POST(request: Request) {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
id: z.uuid().optional(),
|
id: z.uuid().optional(),
|
||||||
username: z.string().max(255),
|
username: z.string().max(255),
|
||||||
password: z.string(),
|
password: z.string().min(8).max(255),
|
||||||
role: z.string().regex(/admin|user|view-only/i),
|
role: userRoleParam,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
34
src/app/api/websites/[websiteId]/events/stats/route.ts
Normal file
34
src/app/api/websites/[websiteId]/events/stats/route.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
|
import { json, unauthorized } from '@/lib/response';
|
||||||
|
import { dateRangeParams, filterParams } from '@/lib/schema';
|
||||||
|
import { canViewWebsite } from '@/permissions';
|
||||||
|
import { getWebsiteEventStats } from '@/queries/sql/events/getWebsiteEventStats';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const schema = z.object({
|
||||||
|
...dateRangeParams,
|
||||||
|
...filterParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
|
||||||
|
if (!(await canViewWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = await getQueryFilters(query, websiteId);
|
||||||
|
|
||||||
|
const data = await getWebsiteEventStats(websiteId, filters);
|
||||||
|
|
||||||
|
return json({ data });
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { SHARE_ID_REGEX } from '@/lib/constants';
|
|
||||||
import { parseRequest } from '@/lib/request';
|
import { parseRequest } from '@/lib/request';
|
||||||
import { badRequest, json, ok, serverError, unauthorized } from '@/lib/response';
|
import { json, ok, unauthorized } from '@/lib/response';
|
||||||
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
|
import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||||
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
|
import { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
|
|
@ -33,7 +32,6 @@ export async function POST(
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
domain: z.string().optional(),
|
domain: z.string().optional(),
|
||||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
@ -43,23 +41,15 @@ export async function POST(
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = await params;
|
const { websiteId } = await params;
|
||||||
const { name, domain, shareId } = body;
|
const { name, domain } = body;
|
||||||
|
|
||||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
return unauthorized();
|
return unauthorized();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const website = await updateWebsite(websiteId, { name, domain });
|
||||||
const website = await updateWebsite(websiteId, { name, domain, shareId });
|
|
||||||
|
|
||||||
return Response.json(website);
|
return Response.json(website);
|
||||||
} catch (e: any) {
|
|
||||||
if (e.message.toLowerCase().includes('unique constraint') && e.message.includes('share_id')) {
|
|
||||||
return badRequest({ message: 'That share ID is already taken.' });
|
|
||||||
}
|
|
||||||
|
|
||||||
return serverError(e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function DELETE(
|
export async function DELETE(
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { anyObjectParam, searchParams, segmentTypeParam } from '@/lib/schema';
|
import { searchParams, segmentParamSchema, segmentTypeParam } from '@/lib/schema';
|
||||||
import { canUpdateWebsite, canViewWebsite } from '@/permissions';
|
import { canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||||
import { createSegment, getWebsiteSegments } from '@/queries/prisma';
|
import { createSegment, getWebsiteSegments } from '@/queries/prisma';
|
||||||
|
|
||||||
|
|
@ -42,7 +42,7 @@ export async function POST(
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
type: segmentTypeParam,
|
type: segmentTypeParam,
|
||||||
name: z.string().max(200),
|
name: z.string().max(200),
|
||||||
parameters: anyObjectParam,
|
parameters: segmentParamSchema,
|
||||||
});
|
});
|
||||||
|
|
||||||
const { auth, body, error } = await parseRequest(request, schema);
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
|
||||||
77
src/app/api/websites/[websiteId]/shares/route.ts
Normal file
77
src/app/api/websites/[websiteId]/shares/route.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { ENTITY_TYPE } from '@/lib/constants';
|
||||||
|
import { uuid } from '@/lib/crypto';
|
||||||
|
import { getRandomChars } from '@/lib/generate';
|
||||||
|
import { parseRequest } from '@/lib/request';
|
||||||
|
import { json, unauthorized } from '@/lib/response';
|
||||||
|
import { anyObjectParam, filterParams, pagingParams } from '@/lib/schema';
|
||||||
|
import { canUpdateWebsite, canViewWebsite } from '@/permissions';
|
||||||
|
import { createShare, getSharesByEntityId } from '@/queries/prisma';
|
||||||
|
|
||||||
|
export async function GET(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const schema = z.object({
|
||||||
|
...filterParams,
|
||||||
|
...pagingParams,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, query, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
const { page, pageSize, search } = query;
|
||||||
|
|
||||||
|
if (!(await canViewWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await getSharesByEntityId(websiteId, {
|
||||||
|
page,
|
||||||
|
pageSize,
|
||||||
|
search,
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: Request,
|
||||||
|
{ params }: { params: Promise<{ websiteId: string }> },
|
||||||
|
) {
|
||||||
|
const schema = z.object({
|
||||||
|
name: z.string().max(200),
|
||||||
|
parameters: anyObjectParam.optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { auth, body, error } = await parseRequest(request, schema);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return error();
|
||||||
|
}
|
||||||
|
|
||||||
|
const { websiteId } = await params;
|
||||||
|
const { name, parameters } = body;
|
||||||
|
const shareParameters = parameters ?? {};
|
||||||
|
|
||||||
|
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||||
|
return unauthorized();
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = getRandomChars(16);
|
||||||
|
|
||||||
|
const share = await createShare({
|
||||||
|
id: uuid(),
|
||||||
|
entityId: websiteId,
|
||||||
|
shareType: ENTITY_TYPE.website,
|
||||||
|
name,
|
||||||
|
slug,
|
||||||
|
parameters: shareParameters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return json(share);
|
||||||
|
}
|
||||||
|
|
@ -31,7 +31,11 @@ export async function GET(
|
||||||
|
|
||||||
const data = await getWebsiteStats(websiteId, filters);
|
const data = await getWebsiteStats(websiteId, filters);
|
||||||
|
|
||||||
const { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate);
|
const { startDate, endDate } = getCompareDate(
|
||||||
|
filters.compare ?? 'prev',
|
||||||
|
filters.startDate,
|
||||||
|
filters.endDate,
|
||||||
|
);
|
||||||
|
|
||||||
const comparison = await getWebsiteStats(websiteId, {
|
const comparison = await getWebsiteStats(websiteId, {
|
||||||
...filters,
|
...filters,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { uuid } from '@/lib/crypto';
|
import { uuid } from '@/lib/crypto';
|
||||||
import redis from '@/lib/redis';
|
import { fetchAccount } from '@/lib/load';
|
||||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||||
import { json, unauthorized } from '@/lib/response';
|
import { json, unauthorized } from '@/lib/response';
|
||||||
import { pagingParams, searchParams } from '@/lib/schema';
|
import { pagingParams, searchParams } from '@/lib/schema';
|
||||||
|
|
@ -52,7 +52,7 @@ export async function POST(request: Request) {
|
||||||
const { id, name, domain, shareId, teamId } = body;
|
const { id, name, domain, shareId, teamId } = body;
|
||||||
|
|
||||||
if (process.env.CLOUD_MODE && !teamId) {
|
if (process.env.CLOUD_MODE && !teamId) {
|
||||||
const account = await redis.client.get(`account:${auth.user.id}`);
|
const account = await fetchAccount(auth.user.id);
|
||||||
|
|
||||||
if (!account?.hasSubscription) {
|
if (!account?.hasSubscription) {
|
||||||
const count = await getWebsiteCount(auth.user.id);
|
const count = await getWebsiteCount(auth.user.id);
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import { Row, Text } from '@umami/react-zen';
|
|
||||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
|
||||||
|
|
||||||
export function Footer() {
|
|
||||||
return (
|
|
||||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
|
||||||
<a href={HOMEPAGE_URL} target="_blank">
|
|
||||||
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
|
||||||
</a>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
|
||||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
|
||||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
|
||||||
import { Logo } from '@/components/svg';
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
return (
|
|
||||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
|
||||||
<a href="https://umami.is" target="_blank" rel="noopener">
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<Icon>
|
|
||||||
<Logo />
|
|
||||||
</Icon>
|
|
||||||
<Text weight="bold">umami</Text>
|
|
||||||
</Row>
|
|
||||||
</a>
|
|
||||||
<Row alignItems="center" gap>
|
|
||||||
<ThemeButton />
|
|
||||||
<LanguageButton />
|
|
||||||
<PreferencesButton />
|
|
||||||
</Row>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
23
src/app/share/[...shareId]/ShareFooter.tsx
Normal file
23
src/app/share/[...shareId]/ShareFooter.tsx
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { Row, Text } from '@umami/react-zen';
|
||||||
|
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
||||||
|
import type { WhiteLabel } from '@/lib/types';
|
||||||
|
|
||||||
|
export function ShareFooter({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
||||||
|
if (whiteLabel) {
|
||||||
|
return (
|
||||||
|
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||||
|
<a href={whiteLabel.url} target="_blank">
|
||||||
|
<Text weight="bold">{whiteLabel.name}</Text>
|
||||||
|
</a>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||||
|
<a href={HOMEPAGE_URL} target="_blank">
|
||||||
|
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
||||||
|
</a>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
src/app/share/[...shareId]/ShareHeader.tsx
Normal file
33
src/app/share/[...shareId]/ShareHeader.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
||||||
|
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||||
|
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||||
|
import { Logo } from '@/components/svg';
|
||||||
|
import type { WhiteLabel } from '@/lib/types';
|
||||||
|
|
||||||
|
export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
||||||
|
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
||||||
|
const logoName = whiteLabel?.name || 'umami';
|
||||||
|
const logoImage = whiteLabel?.image;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
||||||
|
<a href={logoUrl} target="_blank" rel="noopener">
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
{logoImage ? (
|
||||||
|
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
||||||
|
) : (
|
||||||
|
<Icon>
|
||||||
|
<Logo />
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
|
<Text weight="bold">{logoName}</Text>
|
||||||
|
</Row>
|
||||||
|
</a>
|
||||||
|
<Row alignItems="center" gap>
|
||||||
|
<ThemeButton />
|
||||||
|
<LanguageButton />
|
||||||
|
<PreferencesButton />
|
||||||
|
</Row>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
143
src/app/share/[...shareId]/ShareNav.tsx
Normal file
143
src/app/share/[...shareId]/ShareNav.tsx
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
'use client';
|
||||||
|
import { Column } from '@umami/react-zen';
|
||||||
|
import { SideMenu } from '@/components/common/SideMenu';
|
||||||
|
import { useMessages, useNavigation } from '@/components/hooks';
|
||||||
|
import { AlignEndHorizontal, Clock, Eye, Sheet, Tag, User } from '@/components/icons';
|
||||||
|
import { Funnel, Lightning, Magnet, Money, Network, Path, Target } from '@/components/svg';
|
||||||
|
|
||||||
|
export function ShareNav({
|
||||||
|
shareId,
|
||||||
|
parameters,
|
||||||
|
onItemClick,
|
||||||
|
}: {
|
||||||
|
shareId: string;
|
||||||
|
parameters: Record<string, boolean>;
|
||||||
|
onItemClick?: () => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage, labels } = useMessages();
|
||||||
|
const { pathname } = useNavigation();
|
||||||
|
|
||||||
|
const renderPath = (path: string) => `/share/${shareId}${path}`;
|
||||||
|
|
||||||
|
const allItems = [
|
||||||
|
{
|
||||||
|
section: 'traffic',
|
||||||
|
label: formatMessage(labels.traffic),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'overview',
|
||||||
|
label: formatMessage(labels.overview),
|
||||||
|
icon: <Eye />,
|
||||||
|
path: renderPath(''),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'events',
|
||||||
|
label: formatMessage(labels.events),
|
||||||
|
icon: <Lightning />,
|
||||||
|
path: renderPath('/events'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sessions',
|
||||||
|
label: formatMessage(labels.sessions),
|
||||||
|
icon: <User />,
|
||||||
|
path: renderPath('/sessions'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'realtime',
|
||||||
|
label: formatMessage(labels.realtime),
|
||||||
|
icon: <Clock />,
|
||||||
|
path: renderPath('/realtime'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'compare',
|
||||||
|
label: formatMessage(labels.compare),
|
||||||
|
icon: <AlignEndHorizontal />,
|
||||||
|
path: renderPath('/compare'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'breakdown',
|
||||||
|
label: formatMessage(labels.breakdown),
|
||||||
|
icon: <Sheet />,
|
||||||
|
path: renderPath('/breakdown'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'behavior',
|
||||||
|
label: formatMessage(labels.behavior),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'goals',
|
||||||
|
label: formatMessage(labels.goals),
|
||||||
|
icon: <Target />,
|
||||||
|
path: renderPath('/goals'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'funnels',
|
||||||
|
label: formatMessage(labels.funnels),
|
||||||
|
icon: <Funnel />,
|
||||||
|
path: renderPath('/funnels'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'journeys',
|
||||||
|
label: formatMessage(labels.journeys),
|
||||||
|
icon: <Path />,
|
||||||
|
path: renderPath('/journeys'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'retention',
|
||||||
|
label: formatMessage(labels.retention),
|
||||||
|
icon: <Magnet />,
|
||||||
|
path: renderPath('/retention'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
section: 'growth',
|
||||||
|
label: formatMessage(labels.growth),
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
id: 'utm',
|
||||||
|
label: formatMessage(labels.utm),
|
||||||
|
icon: <Tag />,
|
||||||
|
path: renderPath('/utm'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'revenue',
|
||||||
|
label: formatMessage(labels.revenue),
|
||||||
|
icon: <Money />,
|
||||||
|
path: renderPath('/revenue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'attribution',
|
||||||
|
label: formatMessage(labels.attribution),
|
||||||
|
icon: <Network />,
|
||||||
|
path: renderPath('/attribution'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filter items based on parameters
|
||||||
|
const items = allItems
|
||||||
|
.map(section => ({
|
||||||
|
label: section.label,
|
||||||
|
items: section.items.filter(item => parameters[item.id] !== false),
|
||||||
|
}))
|
||||||
|
.filter(section => section.items.length > 0);
|
||||||
|
|
||||||
|
const selectedKey = items
|
||||||
|
.flatMap(e => e.items)
|
||||||
|
.find(({ path }) => path && pathname.endsWith(path.split('?')[0]))?.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Column padding="3" position="sticky" top="0" gap>
|
||||||
|
<SideMenu
|
||||||
|
items={items}
|
||||||
|
selectedKey={selectedKey}
|
||||||
|
allowMinimize={false}
|
||||||
|
onItemClick={onItemClick}
|
||||||
|
/>
|
||||||
|
</Column>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,74 @@
|
||||||
'use client';
|
'use client';
|
||||||
import { Column, useTheme } from '@umami/react-zen';
|
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
||||||
import { useEffect } from 'react';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
||||||
|
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
||||||
|
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
||||||
|
import { GoalsPage } from '@/app/(main)/websites/[websiteId]/(reports)/goals/GoalsPage';
|
||||||
|
import { JourneysPage } from '@/app/(main)/websites/[websiteId]/(reports)/journeys/JourneysPage';
|
||||||
|
import { RetentionPage } from '@/app/(main)/websites/[websiteId]/(reports)/retention/RetentionPage';
|
||||||
|
import { RevenuePage } from '@/app/(main)/websites/[websiteId]/(reports)/revenue/RevenuePage';
|
||||||
|
import { UTMPage } from '@/app/(main)/websites/[websiteId]/(reports)/utm/UTMPage';
|
||||||
|
import { ComparePage } from '@/app/(main)/websites/[websiteId]/compare/ComparePage';
|
||||||
|
import { EventsPage } from '@/app/(main)/websites/[websiteId]/events/EventsPage';
|
||||||
|
import { RealtimePage } from '@/app/(main)/websites/[websiteId]/realtime/RealtimePage';
|
||||||
|
import { SessionsPage } from '@/app/(main)/websites/[websiteId]/sessions/SessionsPage';
|
||||||
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
import { WebsiteHeader } from '@/app/(main)/websites/[websiteId]/WebsiteHeader';
|
||||||
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
||||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||||
import { PageBody } from '@/components/common/PageBody';
|
import { PageBody } from '@/components/common/PageBody';
|
||||||
import { useShareTokenQuery } from '@/components/hooks';
|
import { useShareTokenQuery } from '@/components/hooks';
|
||||||
import { Footer } from './Footer';
|
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||||
import { Header } from './Header';
|
import { ShareFooter } from './ShareFooter';
|
||||||
|
import { ShareHeader } from './ShareHeader';
|
||||||
|
import { ShareNav } from './ShareNav';
|
||||||
|
|
||||||
export function SharePage({ shareId }) {
|
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
||||||
|
'': WebsitePage,
|
||||||
|
overview: WebsitePage,
|
||||||
|
events: EventsPage,
|
||||||
|
sessions: SessionsPage,
|
||||||
|
realtime: RealtimePage,
|
||||||
|
compare: ComparePage,
|
||||||
|
breakdown: BreakdownPage,
|
||||||
|
goals: GoalsPage,
|
||||||
|
funnels: FunnelsPage,
|
||||||
|
journeys: JourneysPage,
|
||||||
|
retention: RetentionPage,
|
||||||
|
utm: UTMPage,
|
||||||
|
revenue: RevenuePage,
|
||||||
|
attribution: AttributionPage,
|
||||||
|
};
|
||||||
|
|
||||||
|
// All section IDs that can be enabled/disabled via parameters
|
||||||
|
const ALL_SECTION_IDS = [
|
||||||
|
'overview',
|
||||||
|
'events',
|
||||||
|
'sessions',
|
||||||
|
'realtime',
|
||||||
|
'compare',
|
||||||
|
'breakdown',
|
||||||
|
'goals',
|
||||||
|
'funnels',
|
||||||
|
'journeys',
|
||||||
|
'retention',
|
||||||
|
'utm',
|
||||||
|
'revenue',
|
||||||
|
'attribution',
|
||||||
|
];
|
||||||
|
|
||||||
|
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
||||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
||||||
const { setTheme } = useTheme();
|
const { setTheme } = useTheme();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
// Calculate allowed sections
|
||||||
|
const allowedSections = useMemo(() => {
|
||||||
|
if (!shareToken?.parameters) return [];
|
||||||
|
const params = shareToken.parameters;
|
||||||
|
return ALL_SECTION_IDS.filter(id => params[id] !== false);
|
||||||
|
}, [shareToken?.parameters]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const url = new URL(window?.location?.href);
|
const url = new URL(window?.location?.href);
|
||||||
|
|
@ -22,20 +79,77 @@ export function SharePage({ shareId }) {
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Redirect to the only allowed section if there's just one and we're on the base path
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
allowedSections.length === 1 &&
|
||||||
|
allowedSections[0] !== 'overview' &&
|
||||||
|
(path === '' || path === 'overview')
|
||||||
|
) {
|
||||||
|
router.replace(`/share/${shareId}/${allowedSections[0]}`);
|
||||||
|
}
|
||||||
|
}, [allowedSections, shareId, path, router]);
|
||||||
|
|
||||||
if (isLoading || !shareToken) {
|
if (isLoading || !shareToken) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { websiteId, parameters = {}, whiteLabel } = shareToken;
|
||||||
|
|
||||||
|
// Redirect to only allowed section - return null while redirecting
|
||||||
|
if (
|
||||||
|
allowedSections.length === 1 &&
|
||||||
|
allowedSections[0] !== 'overview' &&
|
||||||
|
(path === '' || path === 'overview')
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the requested path is allowed
|
||||||
|
const pageKey = path || '';
|
||||||
|
const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false;
|
||||||
|
|
||||||
|
if (!isAllowed) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PageComponent = PAGE_COMPONENTS[pageKey] || WebsitePage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column backgroundColor="2">
|
<Column backgroundColor="2">
|
||||||
<PageBody gap>
|
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||||
<Header />
|
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
||||||
<WebsiteHeader showActions={false} />
|
<MobileMenuButton>
|
||||||
<WebsitePage websiteId={shareToken.websiteId} />
|
{({ close }) => {
|
||||||
</WebsiteProvider>
|
return <ShareNav shareId={shareId} parameters={parameters} onItemClick={close} />;
|
||||||
<Footer />
|
}}
|
||||||
</PageBody>
|
</MobileMenuButton>
|
||||||
|
</Grid>
|
||||||
|
</Row>
|
||||||
|
<Column
|
||||||
|
display={{ xs: 'none', lg: 'flex' }}
|
||||||
|
width="240px"
|
||||||
|
height="100%"
|
||||||
|
border="right"
|
||||||
|
backgroundColor
|
||||||
|
marginRight="2"
|
||||||
|
>
|
||||||
|
<Column display={{ xs: 'none', lg: 'flex' }}>
|
||||||
|
<ShareNav shareId={shareId} parameters={parameters} />
|
||||||
|
</Column>
|
||||||
|
</Column>
|
||||||
|
<PageBody gap>
|
||||||
|
<WebsiteProvider websiteId={websiteId}>
|
||||||
|
<ShareHeader whiteLabel={whiteLabel} />
|
||||||
|
<Column>
|
||||||
|
<WebsiteHeader showActions={false} />
|
||||||
|
<PageComponent websiteId={websiteId} />
|
||||||
|
</Column>
|
||||||
|
<ShareFooter whiteLabel={whiteLabel} />
|
||||||
|
</WebsiteProvider>
|
||||||
|
</PageBody>
|
||||||
|
</Grid>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import { SharePage } from './SharePage';
|
||||||
|
|
||||||
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
||||||
const { shareId } = await params;
|
const { shareId } = await params;
|
||||||
|
const [slug, ...path] = shareId;
|
||||||
|
|
||||||
return <SharePage shareId={shareId[0]} />;
|
return <SharePage shareId={slug} path={path.join('/')} />;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ export function PageBody({
|
||||||
<Column
|
<Column
|
||||||
{...props}
|
{...props}
|
||||||
width="100%"
|
width="100%"
|
||||||
|
minHeight="100vh"
|
||||||
paddingBottom="6"
|
paddingBottom="6"
|
||||||
maxWidth={maxWidth}
|
maxWidth={maxWidth}
|
||||||
paddingX={{ xs: '3', md: '6' }}
|
paddingX={{ xs: '3', md: '6' }}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ export * from './queries/useWebsiteSegmentsQuery';
|
||||||
export * from './queries/useWebsiteSessionQuery';
|
export * from './queries/useWebsiteSessionQuery';
|
||||||
export * from './queries/useWebsiteSessionStatsQuery';
|
export * from './queries/useWebsiteSessionStatsQuery';
|
||||||
export * from './queries/useWebsiteSessionsQuery';
|
export * from './queries/useWebsiteSessionsQuery';
|
||||||
|
export * from './queries/useWebsiteSharesQuery';
|
||||||
export * from './queries/useWebsiteStatsQuery';
|
export * from './queries/useWebsiteStatsQuery';
|
||||||
export * from './queries/useWebsitesQuery';
|
export * from './queries/useWebsitesQuery';
|
||||||
export * from './queries/useWebsiteValuesQuery';
|
export * from './queries/useWebsiteValuesQuery';
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ export function useEventDataValuesQuery(
|
||||||
return useQuery<any>({
|
return useQuery<any>({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'websites:event-data:values',
|
'websites:event-data:values',
|
||||||
{ websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
|
{ websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName },
|
||||||
],
|
],
|
||||||
queryFn: () =>
|
queryFn: () =>
|
||||||
get(`/websites/${websiteId}/event-data/values`, {
|
get(`/websites/${websiteId}/event-data/values`, {
|
||||||
|
|
|
||||||
37
src/components/hooks/queries/useEventStatsQuery.ts
Normal file
37
src/components/hooks/queries/useEventStatsQuery.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
import type { UseQueryOptions } from '@tanstack/react-query';
|
||||||
|
import { useDateParameters } from '@/components/hooks/useDateParameters';
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { useFilterParameters } from '../useFilterParameters';
|
||||||
|
|
||||||
|
export interface EventStatsData {
|
||||||
|
events: number;
|
||||||
|
visitors: number;
|
||||||
|
visits: number;
|
||||||
|
uniqueEvents: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
type EventStatsApiResponse = {
|
||||||
|
data: EventStatsData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function useEventStatsQuery(
|
||||||
|
{ websiteId }: { websiteId: string },
|
||||||
|
options?: UseQueryOptions<EventStatsApiResponse, Error, EventStatsData>,
|
||||||
|
) {
|
||||||
|
const { get, useQuery } = useApi();
|
||||||
|
const { startAt, endAt } = useDateParameters();
|
||||||
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
|
return useQuery<EventStatsApiResponse, Error, EventStatsData>({
|
||||||
|
queryKey: ['websites:events:stats', { websiteId, startAt, endAt, ...filters }],
|
||||||
|
queryFn: () =>
|
||||||
|
get(`/websites/${websiteId}/events/stats`, {
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
...filters,
|
||||||
|
}),
|
||||||
|
select: response => response.data,
|
||||||
|
enabled: !!websiteId,
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -10,7 +10,7 @@ export function useResultQuery<T = any>(
|
||||||
) {
|
) {
|
||||||
const { websiteId, ...parameters } = params;
|
const { websiteId, ...parameters } = params;
|
||||||
const { post, useQuery } = useApi();
|
const { post, useQuery } = useApi();
|
||||||
const { startDate, endDate, timezone } = useDateParameters();
|
const { startDate, endDate, timezone, unit } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<T>({
|
return useQuery<T>({
|
||||||
|
|
@ -22,6 +22,7 @@ export function useResultQuery<T = any>(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
timezone,
|
timezone,
|
||||||
|
unit,
|
||||||
...params,
|
...params,
|
||||||
...filters,
|
...filters,
|
||||||
},
|
},
|
||||||
|
|
@ -35,6 +36,7 @@ export function useResultQuery<T = any>(
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
timezone,
|
timezone,
|
||||||
|
unit,
|
||||||
...parameters,
|
...parameters,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { useApi } from '../useApi';
|
||||||
|
|
||||||
const selector = (state: { shareToken: string }) => state.shareToken;
|
const selector = (state: { shareToken: string }) => state.shareToken;
|
||||||
|
|
||||||
export function useShareTokenQuery(shareId: string): {
|
export function useShareTokenQuery(slug: string): {
|
||||||
shareToken: any;
|
shareToken: any;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
error?: Error;
|
error?: Error;
|
||||||
|
|
@ -11,9 +11,9 @@ export function useShareTokenQuery(shareId: string): {
|
||||||
const shareToken = useApp(selector);
|
const shareToken = useApp(selector);
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { isLoading, error } = useQuery({
|
const { isLoading, error } = useQuery({
|
||||||
queryKey: ['share', shareId],
|
queryKey: ['share', slug],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const data = await get(`/share/${shareId}`);
|
const data = await get(`/share/${slug}`);
|
||||||
|
|
||||||
setShareToken(data);
|
setShareToken(data);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
|
||||||
options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
|
options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
|
||||||
) {
|
) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
const { startAt, endAt } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<WebsiteExpandedMetricsData>({
|
return useQuery<WebsiteExpandedMetricsData>({
|
||||||
|
|
@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery(
|
||||||
websiteId,
|
websiteId,
|
||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
unit,
|
|
||||||
timezone,
|
|
||||||
...filters,
|
...filters,
|
||||||
...params,
|
...params,
|
||||||
},
|
},
|
||||||
|
|
@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery(
|
||||||
get(`/websites/${websiteId}/metrics/expanded`, {
|
get(`/websites/${websiteId}/metrics/expanded`, {
|
||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
unit,
|
|
||||||
timezone,
|
|
||||||
...filters,
|
...filters,
|
||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
|
||||||
options?: ReactQueryOptions<WebsiteMetricsData>,
|
options?: ReactQueryOptions<WebsiteMetricsData>,
|
||||||
) {
|
) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
const { startAt, endAt } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<WebsiteMetricsData>({
|
return useQuery<WebsiteMetricsData>({
|
||||||
|
|
@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery(
|
||||||
websiteId,
|
websiteId,
|
||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
unit,
|
|
||||||
timezone,
|
|
||||||
...filters,
|
...filters,
|
||||||
...params,
|
...params,
|
||||||
},
|
},
|
||||||
|
|
@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery(
|
||||||
get(`/websites/${websiteId}/metrics`, {
|
get(`/websites/${websiteId}/metrics`, {
|
||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
unit,
|
|
||||||
timezone,
|
|
||||||
...filters,
|
...filters,
|
||||||
...params,
|
...params,
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
20
src/components/hooks/queries/useWebsiteSharesQuery.ts
Normal file
20
src/components/hooks/queries/useWebsiteSharesQuery.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ReactQueryOptions } from '@/lib/types';
|
||||||
|
import { useApi } from '../useApi';
|
||||||
|
import { useModified } from '../useModified';
|
||||||
|
import { usePagedQuery } from '../usePagedQuery';
|
||||||
|
|
||||||
|
export function useWebsiteSharesQuery(
|
||||||
|
{ websiteId }: { websiteId: string },
|
||||||
|
options?: ReactQueryOptions,
|
||||||
|
) {
|
||||||
|
const { modified } = useModified('shares');
|
||||||
|
const { get } = useApi();
|
||||||
|
|
||||||
|
return usePagedQuery({
|
||||||
|
queryKey: ['websiteShares', { websiteId, modified }],
|
||||||
|
queryFn: pageParams => {
|
||||||
|
return get(`/websites/${websiteId}/shares`, pageParams);
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -19,17 +19,16 @@ export interface WebsiteStatsData {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWebsiteStatsQuery(
|
export function useWebsiteStatsQuery(
|
||||||
websiteId: string,
|
{ websiteId, compare }: { websiteId: string; compare?: string },
|
||||||
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
|
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
|
||||||
) {
|
) {
|
||||||
const { get, useQuery } = useApi();
|
const { get, useQuery } = useApi();
|
||||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
const { startAt, endAt } = useDateParameters();
|
||||||
const filters = useFilterParameters();
|
const filters = useFilterParameters();
|
||||||
|
|
||||||
return useQuery<WebsiteStatsData>({
|
return useQuery<WebsiteStatsData>({
|
||||||
queryKey: ['websites:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
|
queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }],
|
||||||
queryFn: () =>
|
queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }),
|
||||||
get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, ...filters }),
|
|
||||||
enabled: !!websiteId,
|
enabled: !!websiteId,
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string,
|
||||||
return useQuery({
|
return useQuery({
|
||||||
queryKey: [
|
queryKey: [
|
||||||
'sessions',
|
'sessions',
|
||||||
{ websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
|
{ websiteId, modified, startAt, endAt, timezone, ...params, ...filters },
|
||||||
],
|
],
|
||||||
queryFn: () => {
|
queryFn: () => {
|
||||||
return get(`/websites/${websiteId}/sessions/weekly`, {
|
return get(`/websites/${websiteId}/sessions/weekly`, {
|
||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
unit,
|
|
||||||
timezone,
|
timezone,
|
||||||
...params,
|
...params,
|
||||||
...filters,
|
...filters,
|
||||||
|
|
|
||||||
|
|
@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage';
|
||||||
|
|
||||||
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
|
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
|
||||||
const {
|
const {
|
||||||
query: { date = '', offset = 0, compare = 'prev' },
|
query: { date = '', unit = '', offset = 0, compare = 'prev' },
|
||||||
} = useNavigation();
|
} = useNavigation();
|
||||||
const { locale } = useLocale();
|
const { locale } = useLocale();
|
||||||
|
|
||||||
const dateRange = useMemo(() => {
|
const dateRange = useMemo(() => {
|
||||||
const dateRangeObject = parseDateRange(
|
const dateRangeObject = parseDateRange(
|
||||||
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||||
|
unit,
|
||||||
locale,
|
locale,
|
||||||
options.timezone,
|
options.timezone,
|
||||||
);
|
);
|
||||||
|
|
@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin
|
||||||
return !options.ignoreOffset && offset
|
return !options.ignoreOffset && offset
|
||||||
? getOffsetDateRange(dateRangeObject, +offset)
|
? getOffsetDateRange(dateRangeObject, +offset)
|
||||||
: dateRangeObject;
|
: dateRangeObject;
|
||||||
}, [date, offset, options]);
|
}, [date, unit, offset, options]);
|
||||||
|
|
||||||
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
|
unit,
|
||||||
offset,
|
offset,
|
||||||
compare,
|
compare,
|
||||||
isAllTime: date.endsWith(`:all`),
|
isAllTime: date.endsWith(`:all`),
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ export function useFields() {
|
||||||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||||
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
|
{ name: 'hostname', type: 'string', label: formatMessage(labels.hostname) },
|
||||||
|
{ name: 'distinctId', type: 'string', label: formatMessage(labels.distinctId) },
|
||||||
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
{ name: 'tag', type: 'string', label: formatMessage(labels.tag) },
|
||||||
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
|
{ name: 'event', type: 'string', label: formatMessage(labels.event) },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue