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
|
||||
- type: input
|
||||
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'
|
||||
- type: input
|
||||
attributes:
|
||||
label: Which browser are you using? (if relevant)
|
||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||
label: How are you deploying your application?
|
||||
description: 'For example: Vercel, Railway, Docker, etc'
|
||||
- type: input
|
||||
attributes:
|
||||
label: How are you deploying your application? (if relevant)
|
||||
description: 'For example: Vercel, Railway, Docker, etc'
|
||||
label: Which browser are you using?
|
||||
description: 'For example: Chrome, Edge, Firefox, etc'
|
||||
|
|
|
|||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -21,6 +21,7 @@ package-lock.json
|
|||
/dist
|
||||
/generated
|
||||
/src/generated
|
||||
pm2.yml
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
|
@ -30,6 +31,8 @@ package-lock.json
|
|||
*.log
|
||||
.vscode
|
||||
.tool-versions
|
||||
.claude
|
||||
nul
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ const cloudMode = process.env.CLOUD_MODE || '';
|
|||
const cloudUrl = process.env.CLOUD_URL || '';
|
||||
const collectApiEndpoint = process.env.COLLECT_API_ENDPOINT || '';
|
||||
const corsMaxAge = process.env.CORS_MAX_AGE || '';
|
||||
const defaultCurrency = process.env.DEFAULT_CURRENCY || '';
|
||||
const defaultLocale = process.env.DEFAULT_LOCALE || '';
|
||||
const forceSSL = process.env.FORCE_SSL || '';
|
||||
const frameAncestors = process.env.ALLOWED_FRAME_URLS || '';
|
||||
|
|
@ -170,6 +171,7 @@ export default {
|
|||
cloudMode,
|
||||
cloudUrl,
|
||||
currentVersion: pkg.version,
|
||||
defaultCurrency,
|
||||
defaultLocale,
|
||||
},
|
||||
basePath,
|
||||
|
|
|
|||
10
package.json
10
package.json
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"type": "module",
|
||||
"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",
|
||||
"start": "next start",
|
||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
"is-docker": "^3.0.0",
|
||||
"is-localhost-ip": "^2.0.0",
|
||||
"isbot": "^5.1.31",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"jszip": "^3.10.1",
|
||||
"kafkajs": "^2.1.0",
|
||||
"lucide-react": "^0.543.0",
|
||||
|
|
@ -143,8 +143,8 @@
|
|||
"@types/react-window": "^1.8.8",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.2",
|
||||
"cross-env": "^10.1.0",
|
||||
"cypress": "^13.6.6",
|
||||
"extract-react-intl-messages": "^4.1.1",
|
||||
"cypress": "^15.8.0",
|
||||
"extract-react-intl-messages": "^5.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"jest": "^29.7.0",
|
||||
"lint-staged": "^16.2.6",
|
||||
|
|
@ -164,7 +164,7 @@
|
|||
"stylelint-config-css-modules": "^4.5.1",
|
||||
"stylelint-config-prettier": "^9.0.3",
|
||||
"stylelint-config-recommended": "^14.0.0",
|
||||
"tar": "^6.1.2",
|
||||
"tar": "^7.5.4",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsup": "^8.5.0",
|
||||
|
|
|
|||
278
pnpm-lock.yaml
generated
278
pnpm-lock.yaml
generated
|
|
@ -117,8 +117,8 @@ importers:
|
|||
specifier: ^5.1.31
|
||||
version: 5.1.32
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
specifier: ^9.0.3
|
||||
version: 9.0.3
|
||||
jszip:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
|
|
@ -250,11 +250,11 @@ importers:
|
|||
specifier: ^10.1.0
|
||||
version: 10.1.0
|
||||
cypress:
|
||||
specifier: ^13.6.6
|
||||
version: 13.17.0
|
||||
specifier: ^15.8.0
|
||||
version: 15.9.0
|
||||
extract-react-intl-messages:
|
||||
specifier: ^4.1.1
|
||||
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))
|
||||
specifier: ^5.0.0
|
||||
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:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
|
|
@ -313,8 +313,8 @@ importers:
|
|||
specifier: ^14.0.0
|
||||
version: 14.0.1(stylelint@15.11.0(typescript@5.9.3))
|
||||
tar:
|
||||
specifier: ^6.1.2
|
||||
version: 6.2.1
|
||||
specifier: ^7.5.4
|
||||
version: 7.5.4
|
||||
ts-jest:
|
||||
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)
|
||||
|
|
@ -566,10 +566,6 @@ packages:
|
|||
resolution: {integrity: sha512-co2spjR7wZoZ3Ck0H/jv76bpiuO3oJHtOmq9/gxFiod2DcT9NFg01u/hXcG8MJFnEJuMB6e3vGqS6IOnLwHqRw==}
|
||||
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':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
|
@ -687,8 +683,8 @@ packages:
|
|||
peerDependencies:
|
||||
postcss-selector-parser: ^6.0.13
|
||||
|
||||
'@cypress/request@3.0.9':
|
||||
resolution: {integrity: sha512-I3l7FdGRXluAS44/0NguwWlO83J18p0vlr2FYHrJkWdNYhgVoiYo61IXPqaOsL+vNxU1ZqMACzItGK3/KKDsdw==}
|
||||
'@cypress/request@3.0.10':
|
||||
resolution: {integrity: sha512-hauBrOdvu08vOsagkZ/Aju5XuiZx6ldsLfByg1htFeldhex+PeMrYauANzFsMJeAA0+dyPLbDoX2OYuvVoLDkQ==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
'@cypress/xvfb@1.2.4':
|
||||
|
|
@ -1442,6 +1438,10 @@ packages:
|
|||
resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
|
||||
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':
|
||||
resolution: {integrity: sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -2811,6 +2811,9 @@ packages:
|
|||
'@types/stack-utils@2.0.3':
|
||||
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':
|
||||
resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==}
|
||||
|
||||
|
|
@ -2972,9 +2975,6 @@ packages:
|
|||
resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
|
|
@ -3183,17 +3183,13 @@ packages:
|
|||
chart.js: '>=2.8.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:
|
||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||
engines: {node: '>= 14.16.0'}
|
||||
|
||||
chownr@2.0.0:
|
||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||
engines: {node: '>=10'}
|
||||
chownr@3.0.0:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
ci-info@3.9.0:
|
||||
resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==}
|
||||
|
|
@ -3224,8 +3220,8 @@ packages:
|
|||
resolution: {integrity: sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
cli-table3@0.6.5:
|
||||
resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==}
|
||||
cli-table3@0.6.1:
|
||||
resolution: {integrity: sha512-w0q/enDHhPLq44ovMGdQeeDLvwxwavsJX7oQGYt/LrBlYsyaxyDnp6z3QzFut/6kLLKnlcUVJLrpB7KBfgG/RA==}
|
||||
engines: {node: 10.* || >= 12.*}
|
||||
|
||||
cli-truncate@2.1.0:
|
||||
|
|
@ -3280,6 +3276,10 @@ packages:
|
|||
colorette@2.0.20:
|
||||
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:
|
||||
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
|
@ -3478,9 +3478,9 @@ packages:
|
|||
resolution: {integrity: sha512-/fITjgjGU50vjQ4FH6eUoYu+iUoUKIXws2hL15JJpIR+BbTxaXQsMuuyjtNh2WqsSBS5nsaZHFsFecyw5CCAng==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
cypress@13.17.0:
|
||||
resolution: {integrity: sha512-5xWkaPurwkIljojFidhw8lFScyxhtiFHl/i/3zov+1Z5CmY4t9tjIdvSXfu82Y3w7wt0uR9KkucbhkVvJZLQSA==}
|
||||
engines: {node: ^16.0.0 || ^18.0.0 || >=20.0.0}
|
||||
cypress@15.9.0:
|
||||
resolution: {integrity: sha512-Ks6Bdilz3TtkLZtTQyqYaqtL/WT3X3APKaSLhTV96TmTyudzSjc6EJsJCHmBb7DxO+3R12q3Jkbjgm/iPgmwfg==}
|
||||
engines: {node: ^20.1.0 || ^22.0.0 || >=24.0.0}
|
||||
hasBin: true
|
||||
|
||||
d3-array@2.12.1:
|
||||
|
|
@ -3845,9 +3845,9 @@ packages:
|
|||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
extract-react-intl-messages@4.1.1:
|
||||
resolution: {integrity: sha512-dPogci5X7HVtV7VbUxajH/1YgfNRaW2VtEiVidZ/31Tq8314uzOtzVMNo0IrAPD2E+H1wHoPiu/j565TZsyIZg==}
|
||||
engines: {node: '>=10'}
|
||||
extract-react-intl-messages@5.0.0:
|
||||
resolution: {integrity: sha512-7K1aA3WxhhjBXsuZ2buZm5MLuPHjzkbErV2qqhf0m0K9RMqdwe6mYrOAMZ+1z1bfrngwQ2Iv44+RLjILO8qPdA==}
|
||||
engines: {node: '>=20'}
|
||||
hasBin: true
|
||||
|
||||
extract-zip@2.0.1:
|
||||
|
|
@ -3906,6 +3906,10 @@ packages:
|
|||
resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==}
|
||||
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:
|
||||
resolution: {integrity: sha512-TfW7/1iI4Cy7Y8L6iqNdZQVvdXn0f8B4QcIXmkIbtTIe/Okm/nSlHb4IwGzRVOd3WfSieCgvf5cMzEfySAIl0g==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
|
|
@ -3925,6 +3929,10 @@ packages:
|
|||
fix-dts-default-cjs-exports@1.0.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==}
|
||||
engines: {node: ^10.12.0 || >=12.0.0}
|
||||
|
|
@ -3933,6 +3941,9 @@ packages:
|
|||
resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==}
|
||||
hasBin: true
|
||||
|
||||
flatted@2.0.2:
|
||||
resolution: {integrity: sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==}
|
||||
|
||||
flatted@3.3.3:
|
||||
resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
|
||||
|
||||
|
|
@ -3974,10 +3985,6 @@ packages:
|
|||
resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
resolution: {integrity: sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
fs.realpath@1.0.0:
|
||||
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
|
||||
|
||||
|
|
@ -4042,9 +4049,6 @@ packages:
|
|||
get-tsconfig@4.13.0:
|
||||
resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==}
|
||||
|
||||
getos@3.2.1:
|
||||
resolution: {integrity: sha512-U56CfOK17OKgTVqozZjUKNdkfEv6jk5WISBJ8SHoagjE6L69zOwl3Z+O8myjY9MEW3i2HPWQBt/LTbCgcC973Q==}
|
||||
|
||||
getpass@0.1.7:
|
||||
resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==}
|
||||
|
||||
|
|
@ -4147,6 +4151,10 @@ packages:
|
|||
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
hasha@5.2.2:
|
||||
resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
hasown@2.0.2:
|
||||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
|
@ -4730,8 +4738,8 @@ packages:
|
|||
jsonify@0.0.1:
|
||||
resolution: {integrity: sha512-2/Ki0GcmuqSrgFyelQq9M05y7PS0mEwuIzrf3f1fPqkVDVRvZrPZtVSMHxdgo8Aq0sxAOb/cr2aqqA3LeWHVPg==}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==}
|
||||
jsonwebtoken@9.0.3:
|
||||
resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
|
||||
jsprim@2.0.2:
|
||||
|
|
@ -4741,11 +4749,11 @@ packages:
|
|||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
jwa@1.4.2:
|
||||
resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==}
|
||||
jwa@2.0.1:
|
||||
resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==}
|
||||
|
||||
jws@3.2.2:
|
||||
resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==}
|
||||
jws@4.0.1:
|
||||
resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==}
|
||||
|
||||
kafkajs@2.2.4:
|
||||
resolution: {integrity: sha512-j/YeapB1vfPT2iOIUn/vxdyKEuhuY2PxMBvf5JWux6iSaukAccrMtXEY/Lb7OvavDhOWME589bpLrEdnVHjfjA==}
|
||||
|
|
@ -4768,10 +4776,6 @@ packages:
|
|||
known-css-properties@0.36.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==}
|
||||
engines: {node: '>=6'}
|
||||
|
|
@ -4865,10 +4869,6 @@ packages:
|
|||
lodash.once@4.1.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==}
|
||||
|
||||
|
|
@ -5054,21 +5054,17 @@ packages:
|
|||
minimist@1.2.8:
|
||||
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:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minizlib@2.1.2:
|
||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||
engines: {node: '>= 8'}
|
||||
minizlib@3.1.0:
|
||||
resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
mkdirp@0.5.6:
|
||||
resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
|
||||
hasBin: true
|
||||
|
||||
mkdirp@1.0.4:
|
||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||
|
|
@ -5952,8 +5948,8 @@ packages:
|
|||
pure-rand@7.0.1:
|
||||
resolution: {integrity: sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==}
|
||||
|
||||
qs@6.14.0:
|
||||
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
|
||||
qs@6.14.1:
|
||||
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
|
||||
engines: {node: '>=0.6'}
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
|
|
@ -6177,6 +6173,11 @@ packages:
|
|||
rfdc@1.4.1:
|
||||
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:
|
||||
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
|
|
@ -6603,14 +6604,19 @@ packages:
|
|||
engines: {node: '>=14.0.0'}
|
||||
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:
|
||||
resolution: {integrity: sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
|
||||
tar@6.2.1:
|
||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||
engines: {node: '>=10'}
|
||||
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
|
||||
tar@7.5.4:
|
||||
resolution: {integrity: sha512-AN04xbWGrSTDmVwlI4/GTlIIwMFk/XEv7uL8aa57zuvRy6s4hdBed+lVq2fAZ89XDa7Us3ANXcE3Tvqvja1kTA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
terser@5.43.1:
|
||||
resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==}
|
||||
|
|
@ -6995,6 +7001,10 @@ packages:
|
|||
resolution: {integrity: sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ==}
|
||||
engines: {node: '>=8.3'}
|
||||
|
||||
write@1.0.3:
|
||||
resolution: {integrity: sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
xtend@4.0.2:
|
||||
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
|
||||
engines: {node: '>=0.4'}
|
||||
|
|
@ -7009,6 +7019,10 @@ packages:
|
|||
yallist@4.0.0:
|
||||
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:
|
||||
resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -7305,9 +7319,6 @@ snapshots:
|
|||
dependencies:
|
||||
'@clickhouse/client-common': 1.14.0
|
||||
|
||||
'@colors/colors@1.5.0':
|
||||
optional: true
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
|
@ -7405,7 +7416,7 @@ snapshots:
|
|||
dependencies:
|
||||
postcss-selector-parser: 6.1.2
|
||||
|
||||
'@cypress/request@3.0.9':
|
||||
'@cypress/request@3.0.10':
|
||||
dependencies:
|
||||
aws-sign2: 0.7.0
|
||||
aws4: 1.13.2
|
||||
|
|
@ -7420,7 +7431,7 @@ snapshots:
|
|||
json-stringify-safe: 5.0.1
|
||||
mime-types: 2.1.35
|
||||
performance-now: 2.1.0
|
||||
qs: 6.14.0
|
||||
qs: 6.14.1
|
||||
safe-buffer: 5.2.1
|
||||
tough-cookie: 5.1.2
|
||||
tunnel-agent: 0.6.0
|
||||
|
|
@ -8011,6 +8022,10 @@ snapshots:
|
|||
wrap-ansi: 8.1.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':
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
|
|
@ -9864,6 +9879,8 @@ snapshots:
|
|||
|
||||
'@types/stack-utils@2.0.3': {}
|
||||
|
||||
'@types/tmp@0.2.6': {}
|
||||
|
||||
'@types/use-sync-external-store@0.0.6': {}
|
||||
|
||||
'@types/yargs-parser@21.0.3': {}
|
||||
|
|
@ -10070,8 +10087,6 @@ snapshots:
|
|||
|
||||
async-function@1.0.0: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
at-least-node@1.0.0: {}
|
||||
|
|
@ -10323,13 +10338,11 @@ snapshots:
|
|||
chart.js: 4.5.1
|
||||
date-fns: 2.30.0
|
||||
|
||||
check-more-types@2.24.0: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
|
||||
chownr@2.0.0: {}
|
||||
chownr@3.0.0: {}
|
||||
|
||||
ci-info@3.9.0: {}
|
||||
|
||||
|
|
@ -10353,11 +10366,11 @@ snapshots:
|
|||
dependencies:
|
||||
restore-cursor: 5.1.0
|
||||
|
||||
cli-table3@0.6.5:
|
||||
cli-table3@0.6.1:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
optionalDependencies:
|
||||
'@colors/colors': 1.5.0
|
||||
colors: 1.4.0
|
||||
|
||||
cli-truncate@2.1.0:
|
||||
dependencies:
|
||||
|
|
@ -10403,6 +10416,9 @@ snapshots:
|
|||
|
||||
colorette@2.0.20: {}
|
||||
|
||||
colors@1.4.0:
|
||||
optional: true
|
||||
|
||||
combined-stream@1.0.8:
|
||||
dependencies:
|
||||
delayed-stream: 1.0.0
|
||||
|
|
@ -10618,22 +10634,22 @@ snapshots:
|
|||
dependencies:
|
||||
array-find-index: 1.0.2
|
||||
|
||||
cypress@13.17.0:
|
||||
cypress@15.9.0:
|
||||
dependencies:
|
||||
'@cypress/request': 3.0.9
|
||||
'@cypress/request': 3.0.10
|
||||
'@cypress/xvfb': 1.2.4(supports-color@8.1.1)
|
||||
'@types/sinonjs__fake-timers': 8.1.1
|
||||
'@types/sizzle': 2.3.9
|
||||
'@types/tmp': 0.2.6
|
||||
arch: 2.2.0
|
||||
blob-util: 2.0.2
|
||||
bluebird: 3.7.2
|
||||
buffer: 5.7.1
|
||||
cachedir: 2.4.0
|
||||
chalk: 4.1.2
|
||||
check-more-types: 2.24.0
|
||||
ci-info: 4.3.0
|
||||
cli-cursor: 3.1.0
|
||||
cli-table3: 0.6.5
|
||||
cli-table3: 0.6.1
|
||||
commander: 6.2.1
|
||||
common-tags: 1.8.2
|
||||
dayjs: 1.11.13
|
||||
|
|
@ -10645,9 +10661,8 @@ snapshots:
|
|||
extract-zip: 2.0.1(supports-color@8.1.1)
|
||||
figures: 3.2.0
|
||||
fs-extra: 9.1.0
|
||||
getos: 3.2.1
|
||||
hasha: 5.2.2
|
||||
is-installed-globally: 0.4.0
|
||||
lazy-ass: 1.6.0
|
||||
listr2: 3.14.0(enquirer@2.4.1)
|
||||
lodash: 4.17.21
|
||||
log-symbols: 4.1.0
|
||||
|
|
@ -10657,8 +10672,8 @@ snapshots:
|
|||
process: 0.11.10
|
||||
proxy-from-env: 1.0.0
|
||||
request-progress: 3.0.0
|
||||
semver: 7.7.3
|
||||
supports-color: 8.1.1
|
||||
systeminformation: 5.30.5
|
||||
tmp: 0.2.5
|
||||
tree-kill: 1.2.2
|
||||
untildify: 4.0.0
|
||||
|
|
@ -11131,17 +11146,17 @@ snapshots:
|
|||
|
||||
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:
|
||||
'@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))
|
||||
file-entry-cache: 5.0.1
|
||||
flat: 5.0.2
|
||||
glob: 7.2.3
|
||||
js-yaml: 3.14.1
|
||||
load-json-file: 6.2.0
|
||||
lodash.merge: 4.6.2
|
||||
lodash.mergewith: 4.6.2
|
||||
lodash.pick: 4.4.0
|
||||
meow: 6.1.1
|
||||
mkdirp: 1.0.4
|
||||
pify: 5.0.0
|
||||
|
|
@ -11209,6 +11224,10 @@ snapshots:
|
|||
dependencies:
|
||||
escape-string-regexp: 1.0.5
|
||||
|
||||
file-entry-cache@5.0.1:
|
||||
dependencies:
|
||||
flat-cache: 2.0.1
|
||||
|
||||
file-entry-cache@7.0.2:
|
||||
dependencies:
|
||||
flat-cache: 3.2.0
|
||||
|
|
@ -11233,6 +11252,12 @@ snapshots:
|
|||
mlly: 1.8.0
|
||||
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:
|
||||
dependencies:
|
||||
flatted: 3.3.3
|
||||
|
|
@ -11241,6 +11266,8 @@ snapshots:
|
|||
|
||||
flat@5.0.2: {}
|
||||
|
||||
flatted@2.0.2: {}
|
||||
|
||||
flatted@3.3.3: {}
|
||||
|
||||
for-each@0.3.5:
|
||||
|
|
@ -11293,10 +11320,6 @@ snapshots:
|
|||
jsonfile: 6.2.0
|
||||
universalify: 2.0.1
|
||||
|
||||
fs-minipass@2.1.0:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
|
||||
fs.realpath@1.0.0: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
|
|
@ -11363,10 +11386,6 @@ snapshots:
|
|||
dependencies:
|
||||
resolve-pkg-maps: 1.0.0
|
||||
|
||||
getos@3.2.1:
|
||||
dependencies:
|
||||
async: 3.2.6
|
||||
|
||||
getpass@0.1.7:
|
||||
dependencies:
|
||||
assert-plus: 1.0.0
|
||||
|
|
@ -11501,6 +11520,11 @@ snapshots:
|
|||
dependencies:
|
||||
has-symbols: 1.1.0
|
||||
|
||||
hasha@5.2.2:
|
||||
dependencies:
|
||||
is-stream: 2.0.1
|
||||
type-fest: 0.8.1
|
||||
|
||||
hasown@2.0.2:
|
||||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
|
@ -12236,9 +12260,9 @@ snapshots:
|
|||
|
||||
jsonify@0.0.1: {}
|
||||
|
||||
jsonwebtoken@9.0.2:
|
||||
jsonwebtoken@9.0.3:
|
||||
dependencies:
|
||||
jws: 3.2.2
|
||||
jws: 4.0.1
|
||||
lodash.includes: 4.3.0
|
||||
lodash.isboolean: 3.0.3
|
||||
lodash.isinteger: 4.0.4
|
||||
|
|
@ -12263,15 +12287,15 @@ snapshots:
|
|||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
jwa@1.4.2:
|
||||
jwa@2.0.1:
|
||||
dependencies:
|
||||
buffer-equal-constant-time: 1.0.1
|
||||
ecdsa-sig-formatter: 1.0.11
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
jws@3.2.2:
|
||||
jws@4.0.1:
|
||||
dependencies:
|
||||
jwa: 1.4.2
|
||||
jwa: 2.0.1
|
||||
safe-buffer: 5.2.1
|
||||
|
||||
kafkajs@2.2.4: {}
|
||||
|
|
@ -12289,8 +12313,6 @@ snapshots:
|
|||
known-css-properties@0.36.0:
|
||||
optional: true
|
||||
|
||||
lazy-ass@1.6.0: {}
|
||||
|
||||
leven@3.1.0: {}
|
||||
|
||||
lie@3.3.0:
|
||||
|
|
@ -12383,8 +12405,6 @@ snapshots:
|
|||
|
||||
lodash.once@4.1.1: {}
|
||||
|
||||
lodash.pick@4.4.0: {}
|
||||
|
||||
lodash.truncate@4.4.2: {}
|
||||
|
||||
lodash.uniq@4.5.0: {}
|
||||
|
|
@ -12575,18 +12595,15 @@ snapshots:
|
|||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass@3.3.6:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
|
||||
minipass@5.0.0: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@2.1.2:
|
||||
minizlib@3.1.0:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
yallist: 4.0.0
|
||||
minipass: 7.1.2
|
||||
|
||||
mkdirp@0.5.6:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
|
||||
mkdirp@1.0.4: {}
|
||||
|
||||
|
|
@ -13431,7 +13448,7 @@ snapshots:
|
|||
|
||||
pure-rand@7.0.1: {}
|
||||
|
||||
qs@6.14.0:
|
||||
qs@6.14.1:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
|
|
@ -13769,6 +13786,10 @@ snapshots:
|
|||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rimraf@2.6.3:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
rimraf@3.0.2:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
|
@ -14336,6 +14357,8 @@ snapshots:
|
|||
csso: 5.0.5
|
||||
picocolors: 1.1.1
|
||||
|
||||
systeminformation@5.30.5: {}
|
||||
|
||||
table@6.9.0:
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
|
|
@ -14344,14 +14367,13 @@ snapshots:
|
|||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
tar@6.2.1:
|
||||
tar@7.5.4:
|
||||
dependencies:
|
||||
chownr: 2.0.0
|
||||
fs-minipass: 2.1.0
|
||||
minipass: 5.0.0
|
||||
minizlib: 2.1.2
|
||||
mkdirp: 1.0.4
|
||||
yallist: 4.0.0
|
||||
'@isaacs/fs-minipass': 4.0.1
|
||||
chownr: 3.0.0
|
||||
minipass: 7.1.2
|
||||
minizlib: 3.1.0
|
||||
yallist: 5.0.0
|
||||
|
||||
terser@5.43.1:
|
||||
dependencies:
|
||||
|
|
@ -14758,6 +14780,10 @@ snapshots:
|
|||
sort-keys: 4.2.0
|
||||
write-file-atomic: 3.0.3
|
||||
|
||||
write@1.0.3:
|
||||
dependencies:
|
||||
mkdirp: 0.5.6
|
||||
|
||||
xtend@4.0.2: {}
|
||||
|
||||
y18n@5.0.8: {}
|
||||
|
|
@ -14766,6 +14792,8 @@ snapshots:
|
|||
|
||||
yallist@4.0.0: {}
|
||||
|
||||
yallist@5.0.0: {}
|
||||
|
||||
yaml@1.10.2: {}
|
||||
|
||||
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 {
|
||||
id String @id @unique @map("user_id") @db.Uuid
|
||||
id String @id() @map("user_id") @db.Uuid
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
role String @map("role") @db.VarChar(50)
|
||||
|
|
@ -27,12 +27,13 @@ model User {
|
|||
pixels Pixel[] @relation("user")
|
||||
teams TeamUser[]
|
||||
reports Report[]
|
||||
boards Board[] @relation("user")
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
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
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
|
|
@ -64,10 +65,9 @@ model Session {
|
|||
}
|
||||
|
||||
model Website {
|
||||
id String @id @unique @map("website_id") @db.Uuid
|
||||
id String @id() @map("website_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
shareId String? @unique @map("share_id") @db.VarChar(50)
|
||||
resetAt DateTime? @map("reset_at") @db.Timestamptz(6)
|
||||
userId String? @map("user_id") @db.Uuid
|
||||
teamId String? @map("team_id") @db.Uuid
|
||||
|
|
@ -88,7 +88,6 @@ model Website {
|
|||
@@index([userId])
|
||||
@@index([teamId])
|
||||
@@index([createdAt])
|
||||
@@index([shareId])
|
||||
@@index([createdBy])
|
||||
@@map("website")
|
||||
}
|
||||
|
|
@ -187,7 +186,7 @@ model SessionData {
|
|||
}
|
||||
|
||||
model Team {
|
||||
id String @id() @unique() @map("team_id") @db.Uuid
|
||||
id String @id() @map("team_id") @db.Uuid
|
||||
name String @db.VarChar(50)
|
||||
accessCode String? @unique @map("access_code") @db.VarChar(50)
|
||||
logoUrl String? @map("logo_url") @db.VarChar(2183)
|
||||
|
|
@ -199,13 +198,14 @@ model Team {
|
|||
members TeamUser[]
|
||||
links Link[]
|
||||
pixels Pixel[]
|
||||
boards Board[]
|
||||
|
||||
@@index([accessCode])
|
||||
@@map("team")
|
||||
}
|
||||
|
||||
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
|
||||
userId String @map("user_id") @db.Uuid
|
||||
role String @db.VarChar(50)
|
||||
|
|
@ -221,7 +221,7 @@ model TeamUser {
|
|||
}
|
||||
|
||||
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
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
type String @db.VarChar(50)
|
||||
|
|
@ -242,7 +242,7 @@ model Report {
|
|||
}
|
||||
|
||||
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
|
||||
type String @db.VarChar(50)
|
||||
name String @db.VarChar(200)
|
||||
|
|
@ -257,7 +257,7 @@ model Segment {
|
|||
}
|
||||
|
||||
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
|
||||
sessionId String @map("session_id") @db.Uuid
|
||||
eventId String @map("event_id") @db.Uuid
|
||||
|
|
@ -277,7 +277,7 @@ model Revenue {
|
|||
}
|
||||
|
||||
model Link {
|
||||
id String @id() @unique() @map("link_id") @db.Uuid
|
||||
id String @id() @map("link_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
url String @db.VarChar(500)
|
||||
slug String @unique() @db.VarChar(100)
|
||||
|
|
@ -298,7 +298,7 @@ model Link {
|
|||
}
|
||||
|
||||
model Pixel {
|
||||
id String @id() @unique() @map("pixel_id") @db.Uuid
|
||||
id String @id() @map("pixel_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
slug String @unique() @db.VarChar(100)
|
||||
userId String? @map("user_id") @db.Uuid
|
||||
|
|
@ -316,3 +316,39 @@ model Pixel {
|
|||
@@index([createdAt])
|
||||
@@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": "访问代码"
|
||||
}
|
||||
],
|
||||
"label.account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "账户"
|
||||
}
|
||||
],
|
||||
"label.action": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "行为"
|
||||
}
|
||||
],
|
||||
"label.actions": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -35,12 +47,24 @@
|
|||
"value": "添加描述"
|
||||
}
|
||||
],
|
||||
"label.add-link": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "添加链接"
|
||||
}
|
||||
],
|
||||
"label.add-member": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "添加成员"
|
||||
}
|
||||
],
|
||||
"label.add-pixel": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "添加像素"
|
||||
}
|
||||
],
|
||||
"label.add-step": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -83,12 +107,24 @@
|
|||
"value": "所有时间段"
|
||||
}
|
||||
],
|
||||
"label.analysis": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "分析"
|
||||
}
|
||||
],
|
||||
"label.analytics": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "分析"
|
||||
}
|
||||
],
|
||||
"label.application": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "应用"
|
||||
}
|
||||
],
|
||||
"label.apply": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -107,6 +143,12 @@
|
|||
"value": "查看用户如何与您的营销互动,以及是什么促成了转化。"
|
||||
}
|
||||
],
|
||||
"label.audience": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "受众"
|
||||
}
|
||||
],
|
||||
"label.average": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -125,6 +167,12 @@
|
|||
"value": "之前"
|
||||
}
|
||||
],
|
||||
"label.behavior": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "行为"
|
||||
}
|
||||
],
|
||||
"label.boards": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -173,12 +221,24 @@
|
|||
"value": "修改密码"
|
||||
}
|
||||
],
|
||||
"label.channel": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "渠道"
|
||||
}
|
||||
],
|
||||
"label.channels": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "渠道"
|
||||
}
|
||||
],
|
||||
"label.chart": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "图表"
|
||||
}
|
||||
],
|
||||
"label.cities": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -203,6 +263,12 @@
|
|||
"value": "队列"
|
||||
}
|
||||
],
|
||||
"label.cohorts": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "队列"
|
||||
}
|
||||
],
|
||||
"label.compare": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -317,6 +383,12 @@
|
|||
"value": "创建者"
|
||||
}
|
||||
],
|
||||
"label.criteria": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "条件"
|
||||
}
|
||||
],
|
||||
"label.currency": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -419,6 +491,12 @@
|
|||
"value": "台式机"
|
||||
}
|
||||
],
|
||||
"label.destination-url": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "目标URL"
|
||||
}
|
||||
],
|
||||
"label.details": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -455,6 +533,12 @@
|
|||
"value": "唯一ID"
|
||||
}
|
||||
],
|
||||
"label.documentation": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "文档"
|
||||
}
|
||||
],
|
||||
"label.does-not-contain": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -479,6 +563,12 @@
|
|||
"value": "域名"
|
||||
}
|
||||
],
|
||||
"label.download": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "下载"
|
||||
}
|
||||
],
|
||||
"label.dropoff": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -506,7 +596,7 @@
|
|||
"label.email": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Email"
|
||||
"value": "邮箱"
|
||||
}
|
||||
],
|
||||
"label.enable-share-url": [
|
||||
|
|
@ -527,6 +617,12 @@
|
|||
"value": "入口 URL"
|
||||
}
|
||||
],
|
||||
"label.environment": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "环境"
|
||||
}
|
||||
],
|
||||
"label.event": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -671,6 +767,12 @@
|
|||
"value": "分组"
|
||||
}
|
||||
],
|
||||
"label.growth": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "增长"
|
||||
}
|
||||
],
|
||||
"label.hostname": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -701,6 +803,12 @@
|
|||
"value": "通过使用筛选器和划分时间段来更深入地研究数据。"
|
||||
}
|
||||
],
|
||||
"label.invalid-url": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "无效URL"
|
||||
}
|
||||
],
|
||||
"label.is": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -863,12 +971,24 @@
|
|||
"value": "少于等于"
|
||||
}
|
||||
],
|
||||
"label.link": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "链接"
|
||||
}
|
||||
],
|
||||
"label.links": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "链接"
|
||||
}
|
||||
],
|
||||
"label.location": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "位置"
|
||||
}
|
||||
],
|
||||
"label.login": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1020,7 +1140,7 @@
|
|||
"label.online": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Online"
|
||||
"value": "在线"
|
||||
}
|
||||
],
|
||||
"label.organic-search": [
|
||||
|
|
@ -1165,6 +1285,12 @@
|
|||
"value": "路径"
|
||||
}
|
||||
],
|
||||
"label.pixel": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "像素"
|
||||
}
|
||||
],
|
||||
"label.pixels": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1185,6 +1311,12 @@
|
|||
"value": " 提供支持"
|
||||
}
|
||||
],
|
||||
"label.preferences": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "偏好"
|
||||
}
|
||||
],
|
||||
"label.previous": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1209,6 +1341,12 @@
|
|||
"value": "个人资料"
|
||||
}
|
||||
],
|
||||
"label.profiles": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "个人资料"
|
||||
}
|
||||
],
|
||||
"label.properties": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1248,7 +1386,7 @@
|
|||
"label.referral": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Referral"
|
||||
"value": "来源"
|
||||
}
|
||||
],
|
||||
"label.referrer": [
|
||||
|
|
@ -1371,6 +1509,24 @@
|
|||
"value": "保存"
|
||||
}
|
||||
],
|
||||
"label.save-cohort": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "保存为群组"
|
||||
}
|
||||
],
|
||||
"label.save-segment": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "保存为细分"
|
||||
}
|
||||
],
|
||||
"label.screen": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "屏幕"
|
||||
}
|
||||
],
|
||||
"label.screens": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1383,6 +1539,18 @@
|
|||
"value": "搜索"
|
||||
}
|
||||
],
|
||||
"label.segment": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "细分"
|
||||
}
|
||||
],
|
||||
"label.segments": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "细分"
|
||||
}
|
||||
],
|
||||
"label.select": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1485,6 +1653,24 @@
|
|||
"value": "总和"
|
||||
}
|
||||
],
|
||||
"label.support": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "支持"
|
||||
}
|
||||
],
|
||||
"label.switch-account": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "切换账户"
|
||||
}
|
||||
],
|
||||
"label.table": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "表格"
|
||||
}
|
||||
],
|
||||
"label.tablet": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1635,6 +1821,12 @@
|
|||
"value": "跟踪代码"
|
||||
}
|
||||
],
|
||||
"label.traffic": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "流量"
|
||||
}
|
||||
],
|
||||
"label.transactions": [
|
||||
{
|
||||
"type": 0,
|
||||
|
|
@ -1846,7 +2038,7 @@
|
|||
"message.bad-request": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Bad request"
|
||||
"value": "请求错误"
|
||||
}
|
||||
],
|
||||
"message.collected-data": [
|
||||
|
|
@ -1946,7 +2138,7 @@
|
|||
"message.forbidden": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Forbidden"
|
||||
"value": "禁止访问"
|
||||
}
|
||||
],
|
||||
"message.go-to-settings": [
|
||||
|
|
@ -2046,13 +2238,13 @@
|
|||
"message.not-found": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Not found"
|
||||
"value": "未找到"
|
||||
}
|
||||
],
|
||||
"message.nothing-selected": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Nothing selected."
|
||||
"value": "未选择"
|
||||
}
|
||||
],
|
||||
"message.page-not-found": [
|
||||
|
|
@ -2090,7 +2282,7 @@
|
|||
"message.sever-error": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Server error"
|
||||
"value": "服务器错误"
|
||||
}
|
||||
],
|
||||
"message.share-url": [
|
||||
|
|
@ -2158,7 +2350,7 @@
|
|||
"message.unauthorized": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Unauthorized"
|
||||
"value": "未授权"
|
||||
}
|
||||
],
|
||||
"message.user-deleted": [
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@
|
|||
"AD-06": "Sant Julia de Loria",
|
||||
"AD-07": "Andorra la Vella",
|
||||
"AD-08": "Escaldes-Engordany",
|
||||
"AE-AJ": "'Ajman",
|
||||
"AE-AZ": "Abu Zaby",
|
||||
"AE-DU": "Dubayy",
|
||||
"AE-FU": "Al Fujayrah",
|
||||
"AE-RK": "Ra's al Khaymah",
|
||||
"AE-SH": "Ash Shariqah",
|
||||
"AE-UQ": "Umm al Qaywayn",
|
||||
"AE-AJ": "Ajman",
|
||||
"AE-AZ": "Abu Dhabi",
|
||||
"AE-DU": "Dubai",
|
||||
"AE-FU": "Al Fujairah",
|
||||
"AE-RK": "Ras al Khaimah",
|
||||
"AE-SH": "Sharjah",
|
||||
"AE-UQ": "Umm al Quwain",
|
||||
"AF-BAL": "Balkh",
|
||||
"AF-BAM": "Bamyan",
|
||||
"AF-BDG": "Badghis",
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import 'dotenv/config';
|
|||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import https from 'https';
|
||||
import tar from 'tar';
|
||||
import { list } from 'tar';
|
||||
import zlib from 'zlib';
|
||||
|
||||
if (process.env.VERCEL && !process.env.BUILD_GEO) {
|
||||
|
|
@ -40,7 +40,7 @@ const isDirectMmdb = url.endsWith('.mmdb');
|
|||
const downloadCompressed = url =>
|
||||
new Promise(resolve => {
|
||||
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 { Panel } from '@/components/common/Panel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { TeamsAddButton } from '../../teams/TeamsAddButton';
|
||||
import { AdminTeamsDataTable } from './AdminTeamsDataTable';
|
||||
|
||||
export function AdminTeamsPage() {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
||||
const handleSave = () => {};
|
||||
|
||||
return (
|
||||
<Column gap="6" margin="2">
|
||||
<PageHeader title={formatMessage(labels.teams)} />
|
||||
<PageHeader title={formatMessage(labels.teams)}>
|
||||
<TeamsAddButton onSave={handleSave} isAdmin={true} />
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<AdminTeamsDataTable />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { useMessages, useUpdateQuery } from '@/components/hooks';
|
||||
import { messages } from '@/components/messages';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
|
||||
export function UserAddForm({ onSave, onClose }) {
|
||||
|
|
@ -37,7 +38,10 @@ export function UserAddForm({ onSave, onClose }) {
|
|||
<FormField
|
||||
label={formatMessage(labels.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" />
|
||||
</FormField>
|
||||
|
|
|
|||
|
|
@ -30,7 +30,11 @@ export function UserEditForm({ userId, onSave }: { userId: string; onSave?: () =
|
|||
};
|
||||
|
||||
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)}>
|
||||
<TextField data-test="input-username" />
|
||||
</FormField>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 { DialogButton } from '@/components/input/DialogButton';
|
||||
import { messages } from '@/components/messages';
|
||||
|
|
@ -15,7 +15,8 @@ export function LinkDeleteButton({
|
|||
onSave?: () => void;
|
||||
}) {
|
||||
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) => {
|
||||
await mutateAsync(null, {
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import {
|
|||
Form,
|
||||
FormField,
|
||||
FormSubmitButton,
|
||||
Grid,
|
||||
Icon,
|
||||
Label,
|
||||
Loading,
|
||||
Row,
|
||||
TextField,
|
||||
} from '@umami/react-zen';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useConfig, useLinkQuery, useMessages } from '@/components/hooks';
|
||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||
import { RefreshCw } from '@/components/icons';
|
||||
|
|
@ -42,27 +43,20 @@ export function LinkEditForm({
|
|||
const { linksUrl } = useConfig();
|
||||
const hostUrl = linksUrl || LINKS_URL;
|
||||
const { data, isLoading } = useLinkQuery(linkId);
|
||||
const [slug, setSlug] = useState(generateId());
|
||||
const [defaultSlug] = useState(generateId());
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
await mutateAsync(data, {
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('links');
|
||||
touch(`link:${linkId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSlug = () => {
|
||||
const slug = generateId();
|
||||
|
||||
setSlug(slug);
|
||||
|
||||
return slug;
|
||||
};
|
||||
|
||||
const checkUrl = (url: string) => {
|
||||
if (!isValidUrl(url)) {
|
||||
return formatMessage(labels.invalidUrl);
|
||||
|
|
@ -70,19 +64,19 @@ export function LinkEditForm({
|
|||
return true;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
setSlug(data.slug);
|
||||
}
|
||||
}, [data]);
|
||||
|
||||
if (linkId && isLoading) {
|
||||
return <Loading placement="absolute" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={{ slug, ...data }}>
|
||||
{({ setValue }) => {
|
||||
<Form
|
||||
onSubmit={handleSubmit}
|
||||
error={getErrorMessage(error)}
|
||||
defaultValues={{ slug: defaultSlug, ...data }}
|
||||
>
|
||||
{({ setValue, watch }) => {
|
||||
const slug = watch('slug');
|
||||
|
||||
return (
|
||||
<>
|
||||
<FormField
|
||||
|
|
@ -101,15 +95,25 @@ export function LinkEditForm({
|
|||
<TextField placeholder="https://example.com" autoComplete="off" />
|
||||
</FormField>
|
||||
|
||||
<FormField
|
||||
name="slug"
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
}}
|
||||
style={{ display: 'none' }}
|
||||
>
|
||||
<input type="hidden" />
|
||||
</FormField>
|
||||
<Grid columns="1fr auto" alignItems="end" gap>
|
||||
<FormField
|
||||
name="slug"
|
||||
label={formatMessage({ id: 'label.slug', defaultMessage: 'Slug' })}
|
||||
rules={{
|
||||
required: formatMessage(labels.required),
|
||||
}}
|
||||
>
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
<Button
|
||||
variant="quiet"
|
||||
onPress={() => setValue('slug', generateId(), { shouldDirty: true })}
|
||||
>
|
||||
<Icon>
|
||||
<RefreshCw />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.link)}</Label>
|
||||
|
|
@ -121,14 +125,6 @@ export function LinkEditForm({
|
|||
allowCopy
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
<Button
|
||||
variant="quiet"
|
||||
onPress={() => setValue('slug', handleSlug(), { shouldDirty: true })}
|
||||
>
|
||||
<Icon>
|
||||
<RefreshCw />
|
||||
</Icon>
|
||||
</Button>
|
||||
</Row>
|
||||
</Column>
|
||||
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import { DataGrid } from '@/components/common/DataGrid';
|
|||
import { useLinksQuery, useNavigation } from '@/components/hooks';
|
||||
import { LinksTable } from './LinksTable';
|
||||
|
||||
export function LinksDataTable() {
|
||||
export function LinksDataTable({ showActions = false }: { showActions?: boolean }) {
|
||||
const { teamId } = useNavigation();
|
||||
const query = useLinksQuery({ teamId });
|
||||
|
||||
return (
|
||||
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
||||
{({ data }) => <LinksTable data={data} />}
|
||||
{({ data }) => <LinksTable data={data} showActions={showActions} />}
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,21 +4,30 @@ import { LinksDataTable } from '@/app/(main)/links/LinksDataTable';
|
|||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
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';
|
||||
|
||||
export function LinksPage() {
|
||||
const { user } = useLoginQuery();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
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 (
|
||||
<PageBody>
|
||||
<Column gap="6" margin="2">
|
||||
<PageHeader title={formatMessage(labels.links)}>
|
||||
<LinkAddButton teamId={teamId} />
|
||||
{showActions && <LinkAddButton teamId={teamId} />}
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<LinksDataTable />
|
||||
<LinksDataTable showActions={showActions} />
|
||||
</Panel>
|
||||
</Column>
|
||||
</PageBody>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
|||
import { LinkDeleteButton } from './LinkDeleteButton';
|
||||
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 { websiteId, renderUrl } = useNavigation();
|
||||
const { getSlugUrl } = useSlug('link');
|
||||
|
|
@ -36,16 +40,18 @@ export function LinksTable(props: DataTableProps) {
|
|||
<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, name }: any) => {
|
||||
return (
|
||||
<Row>
|
||||
<LinkEditButton linkId={id} />
|
||||
<LinkDeleteButton linkId={id} websiteId={websiteId} name={name} />
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
{showActions && (
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{({ id, name }: any) => {
|
||||
return (
|
||||
<Row>
|
||||
<LinkEditButton linkId={id} />
|
||||
<LinkDeleteButton linkId={id} websiteId={websiteId} name={name} />
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
)}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { getLink } from '@/queries/prisma';
|
||||
import { LinkPage } from './LinkPage';
|
||||
|
||||
export default async function ({ params }: { params: Promise<{ linkId: string }> }) {
|
||||
const { linkId } = await params;
|
||||
const link = await getLink(linkId);
|
||||
|
||||
if (!link || link?.deletedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <LinkPage linkId={linkId} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ export function PixelEditForm({
|
|||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('pixels');
|
||||
touch(`pixel:${pixelId}`);
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
|
|
|
|||
|
|
@ -2,13 +2,13 @@ import { DataGrid } from '@/components/common/DataGrid';
|
|||
import { useNavigation, usePixelsQuery } from '@/components/hooks';
|
||||
import { PixelsTable } from './PixelsTable';
|
||||
|
||||
export function PixelsDataTable() {
|
||||
export function PixelsDataTable({ showActions = false }: { showActions?: boolean }) {
|
||||
const { teamId } = useNavigation();
|
||||
const query = usePixelsQuery({ teamId });
|
||||
|
||||
return (
|
||||
<DataGrid query={query} allowSearch={true} autoFocus={false} allowPaging={true}>
|
||||
{({ data }) => <PixelsTable data={data} />}
|
||||
{({ data }) => <PixelsTable data={data} showActions={showActions} />}
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,22 +3,31 @@ import { Column } from '@umami/react-zen';
|
|||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
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 { PixelsDataTable } from './PixelsDataTable';
|
||||
|
||||
export function PixelsPage() {
|
||||
const { user } = useLoginQuery();
|
||||
const { formatMessage, labels } = useMessages();
|
||||
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 (
|
||||
<PageBody>
|
||||
<Column gap="6" margin="2">
|
||||
<PageHeader title={formatMessage(labels.pixels)}>
|
||||
<PixelAddButton teamId={teamId} />
|
||||
{showActions && <PixelAddButton teamId={teamId} />}
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<PixelsDataTable />
|
||||
<PixelsDataTable showActions={showActions} />
|
||||
</Panel>
|
||||
</Column>
|
||||
</PageBody>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,11 @@ import { useMessages, useNavigation, useSlug } from '@/components/hooks';
|
|||
import { PixelDeleteButton } from './PixelDeleteButton';
|
||||
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 { renderUrl } = useNavigation();
|
||||
const { getSlugUrl } = useSlug('pixel');
|
||||
|
|
@ -31,18 +35,20 @@ export function PixelsTable(props: DataTableProps) {
|
|||
<DataColumn id="created" label={formatMessage(labels.created)}>
|
||||
{(row: any) => <DateDistance date={new Date(row.createdAt)} />}
|
||||
</DataColumn>
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{(row: any) => {
|
||||
const { id, name } = row;
|
||||
{showActions && (
|
||||
<DataColumn id="action" align="end" width="100px">
|
||||
{(row: any) => {
|
||||
const { id, name } = row;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<PixelEditButton pixelId={id} />
|
||||
<PixelDeleteButton pixelId={id} name={name} />
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
return (
|
||||
<Row>
|
||||
<PixelEditButton pixelId={id} />
|
||||
<PixelDeleteButton pixelId={id} name={name} />
|
||||
</Row>
|
||||
);
|
||||
}}
|
||||
</DataColumn>
|
||||
)}
|
||||
</DataTable>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,14 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { getPixel } from '@/queries/prisma';
|
||||
import { PixelPage } from './PixelPage';
|
||||
|
||||
export default async function ({ params }: { params: { pixelId: string } }) {
|
||||
const { pixelId } = await params;
|
||||
const pixel = await getPixel(pixelId);
|
||||
|
||||
if (!pixel || pixel?.deletedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <PixelPage pixelId={pixelId} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { DateRangeSetting } from './DateRangeSetting';
|
|||
import { LanguageSetting } from './LanguageSetting';
|
||||
import { ThemeSetting } from './ThemeSetting';
|
||||
import { TimezoneSetting } from './TimezoneSetting';
|
||||
import { VersionSetting } from './VersionSetting';
|
||||
|
||||
export function PreferenceSettings() {
|
||||
const { user } = useLoginQuery();
|
||||
|
|
@ -31,6 +32,10 @@ export function PreferenceSettings() {
|
|||
<Label>{formatMessage(labels.theme)}</Label>
|
||||
<ThemeSetting />
|
||||
</Column>
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.version)}</Label>
|
||||
<VersionSetting />
|
||||
</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,
|
||||
} from '@umami/react-zen';
|
||||
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 { mutateAsync, error, isPending } = useUpdateQuery('/teams');
|
||||
|
||||
|
|
@ -26,6 +35,11 @@ export function TeamAddForm({ onSave, onClose }: { onSave: () => void; onClose:
|
|||
<FormField name="name" label={formatMessage(labels.name)}>
|
||||
<TextField autoComplete="off" />
|
||||
</FormField>
|
||||
{isAdmin && (
|
||||
<FormField name="ownerId" label={formatMessage(labels.teamOwner)}>
|
||||
<UserSelect buttonProps={{ style: { outline: 'none' } }} />
|
||||
</FormField>
|
||||
)}
|
||||
<FormButtons>
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{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 { TeamAddForm } from './TeamAddForm';
|
||||
|
||||
export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
||||
export function TeamsAddButton({
|
||||
onSave,
|
||||
isAdmin = false,
|
||||
}: {
|
||||
onSave?: () => void;
|
||||
isAdmin?: boolean;
|
||||
}) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { toast } = useToast();
|
||||
const { touch } = useModified();
|
||||
|
|
@ -25,7 +31,7 @@ export function TeamsAddButton({ onSave }: { onSave?: () => void }) {
|
|||
</Button>
|
||||
<Modal>
|
||||
<Dialog title={formatMessage(labels.createTeam)} style={{ width: 400 }}>
|
||||
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} />}
|
||||
{({ close }) => <TeamAddForm onSave={handleSave} onClose={close} isAdmin={isAdmin} />}
|
||||
</Dialog>
|
||||
</Modal>
|
||||
</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 { PageHeader } from '@/components/common/PageHeader';
|
||||
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 { labels } from '@/components/messages';
|
||||
import { ROLES } from '@/lib/constants';
|
||||
import { TeamsMemberAddButton } from '../TeamsMemberAddButton';
|
||||
import { TeamEditForm } from './TeamEditForm';
|
||||
import { TeamManage } from './TeamManage';
|
||||
import { TeamMembersDataTable } from './TeamMembersDataTable';
|
||||
|
|
@ -13,6 +15,7 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
|||
const team: any = useTeam();
|
||||
const { user } = useLoginQuery();
|
||||
const { pathname } = useNavigation();
|
||||
const { formatMessage } = useMessages();
|
||||
|
||||
const isAdmin = pathname.includes('/admin');
|
||||
|
||||
|
|
@ -37,6 +40,10 @@ export function TeamSettings({ teamId }: { teamId: string }) {
|
|||
<TeamEditForm teamId={teamId} allowEdit={canEdit} showAccessCode={canEdit} />
|
||||
</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} />
|
||||
</Panel>
|
||||
{isTeamOwner && (
|
||||
|
|
|
|||
|
|
@ -3,22 +3,31 @@ import { Column } from '@umami/react-zen';
|
|||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
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 { WebsitesDataTable } from './WebsitesDataTable';
|
||||
|
||||
export function WebsitesPage() {
|
||||
const { user } = useLoginQuery();
|
||||
const { teamId } = useNavigation();
|
||||
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 (
|
||||
<PageBody>
|
||||
<Column gap="6" margin="2">
|
||||
<PageHeader title={formatMessage(labels.websites)}>
|
||||
<WebsiteAddButton teamId={teamId} />
|
||||
{showActions && <WebsiteAddButton teamId={teamId} />}
|
||||
</PageHeader>
|
||||
<Panel>
|
||||
<WebsitesDataTable teamId={teamId} />
|
||||
<WebsitesDataTable teamId={teamId} showActions={showActions} />
|
||||
</Panel>
|
||||
</Column>
|
||||
</PageBody>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||
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 { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||
|
|
@ -20,6 +20,8 @@ type FunnelResult = {
|
|||
|
||||
export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const { data, error, isLoading } = useResultQuery(type, {
|
||||
websiteId,
|
||||
...parameters,
|
||||
|
|
@ -36,21 +38,22 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
|
|||
</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.funnel)}
|
||||
variant="modal"
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
{!isSharePage && (
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.funnel)}
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
)}
|
||||
</Grid>
|
||||
{data?.map(
|
||||
(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
|||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
||||
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||
import { Funnel } from './Funnel';
|
||||
import { FunnelAddButton } from './FunnelAddButton';
|
||||
|
||||
|
|
@ -13,13 +13,17 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<SectionHeader>
|
||||
<FunnelAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
{!isSharePage && (
|
||||
<SectionHeader>
|
||||
<FunnelAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
)}
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
<Grid gap>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||
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 { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||
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) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
||||
websiteId,
|
||||
startDate,
|
||||
|
|
@ -45,21 +47,23 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
|
|||
</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.goal)}
|
||||
variant="modal"
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
{!isSharePage && (
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.goal)}
|
||||
variant="modal"
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
)}
|
||||
</Grid>
|
||||
<Row alignItems="center" justifyContent="space-between" gap>
|
||||
<Text color="muted">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
|||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
||||
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||
import { Goal } from './Goal';
|
||||
import { GoalAddButton } from './GoalAddButton';
|
||||
|
||||
|
|
@ -13,13 +13,17 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
<SectionHeader>
|
||||
<GoalAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
{!isSharePage && (
|
||||
<SectionHeader>
|
||||
<GoalAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
)}
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||
|
|
|
|||
|
|
@ -21,9 +21,15 @@ export interface JourneyProps {
|
|||
steps: number;
|
||||
startStep?: 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 [activeNode, setActiveNode] = useState(null);
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -32,6 +38,8 @@ export function Journey({ websiteId, steps, startStep, endStep }: JourneyProps)
|
|||
steps,
|
||||
startStep,
|
||||
endStep,
|
||||
view,
|
||||
eventType: EVENT_TYPES[view],
|
||||
});
|
||||
|
||||
useEscapeKey(() => setSelectedNode(null));
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
'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 { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useDateRange, useMessages } from '@/components/hooks';
|
||||
import { FilterButtons } from '@/components/input/FilterButtons';
|
||||
import { Journey } from './Journey';
|
||||
|
||||
const JOURNEY_STEPS = [2, 3, 4, 5, 6, 7];
|
||||
|
|
@ -14,10 +15,26 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const [view, setView] = useState('all');
|
||||
const [steps, setSteps] = useState(DEFAULT_STEP);
|
||||
const [startStep, setStartStep] = 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 (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
|
|
@ -52,6 +69,9 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
|||
/>
|
||||
</Column>
|
||||
</Grid>
|
||||
<Row justifyContent="flex-end">
|
||||
<FilterButtons items={buttons} value={view} onChange={setView} />
|
||||
</Row>
|
||||
<Panel height="900px" allowFullscreen>
|
||||
<Journey
|
||||
websiteId={websiteId}
|
||||
|
|
@ -60,6 +80,7 @@ export function JourneysPage({ websiteId }: { websiteId: string }) {
|
|||
steps={steps}
|
||||
startStep={startStep}
|
||||
endStep={endStep}
|
||||
view={view}
|
||||
/>
|
||||
</Panel>
|
||||
</Column>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,10 @@ import { ListTable } from '@/components/metrics/ListTable';
|
|||
import { MetricCard } from '@/components/metrics/MetricCard';
|
||||
import { MetricsBar } from '@/components/metrics/MetricsBar';
|
||||
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 { formatLongCurrency, formatLongNumber } from '@/lib/format';
|
||||
import { getItem, setItem } from '@/lib/storage';
|
||||
|
||||
export interface RevenueProps {
|
||||
websiteId: string;
|
||||
|
|
@ -24,7 +25,15 @@ export interface 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 { locale, dateLocale } = useLocale();
|
||||
const { countryNames } = useCountryNames(locale);
|
||||
|
|
@ -107,7 +116,7 @@ export function Revenue({ websiteId, startDate, endDate, unit }: RevenueProps) {
|
|||
return (
|
||||
<Column gap>
|
||||
<Grid columns="280px" gap>
|
||||
<CurrencySelect value={currency} onChange={setCurrency} />
|
||||
<CurrencySelect value={currency} onChange={handleCurrencyChange} />
|
||||
</Grid>
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
|
|
|
|||
|
|
@ -21,30 +21,28 @@ export function WebsiteChart({
|
|||
const { pageviews, sessions, compare } = (data || {}) as any;
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
if (data) {
|
||||
const result = {
|
||||
pageviews,
|
||||
sessions,
|
||||
};
|
||||
if (!data) {
|
||||
return { pageviews: [], sessions: [] };
|
||||
}
|
||||
|
||||
if (compare) {
|
||||
result.compare = {
|
||||
pageviews: result.pageviews.map(({ x }, i) => ({
|
||||
return {
|
||||
pageviews,
|
||||
sessions,
|
||||
...(compare && {
|
||||
compare: {
|
||||
pageviews: pageviews.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.pageviews[i]?.y,
|
||||
d: compare.pageviews[i]?.x,
|
||||
})),
|
||||
sessions: result.sessions.map(({ x }, i) => ({
|
||||
sessions: sessions.map(({ x }, i) => ({
|
||||
x,
|
||||
y: compare.sessions[i]?.y,
|
||||
d: compare.sessions[i]?.x,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
return { pageviews: [], sessions: [] };
|
||||
},
|
||||
}),
|
||||
};
|
||||
}, [data, startDate, endDate, unit]);
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -169,6 +169,12 @@ export function WebsiteExpandedMenu({
|
|||
path: updateParams({ view: 'hostname' }),
|
||||
icon: <Network />,
|
||||
},
|
||||
{
|
||||
id: 'distinctId',
|
||||
label: formatMessage(labels.distinctId),
|
||||
path: updateParams({ view: 'distinctId' }),
|
||||
icon: <Tag />,
|
||||
},
|
||||
{
|
||||
id: 'tag',
|
||||
label: formatMessage(labels.tag),
|
||||
|
|
|
|||
|
|
@ -1,14 +1,18 @@
|
|||
import { Icon, Row, Text } from '@umami/react-zen';
|
||||
import { WebsiteShareForm } from '@/app/(main)/websites/[websiteId]/settings/WebsiteShareForm';
|
||||
import { Favicon } from '@/components/common/Favicon';
|
||||
import { LinkButton } from '@/components/common/LinkButton';
|
||||
import { PageHeader } from '@/components/common/PageHeader';
|
||||
import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
||||
import { Edit, Share } from '@/components/icons';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { Edit } from '@/components/icons';
|
||||
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 { renderUrl, pathname } = useNavigation();
|
||||
const isSettings = pathname.endsWith('/settings');
|
||||
|
|
@ -23,35 +27,20 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
|||
<PageHeader
|
||||
title={website.name}
|
||||
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">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
|
||||
{showActions && (
|
||||
<Row alignItems="center" gap>
|
||||
<ShareButton websiteId={website.id} shareId={website.shareId} />
|
||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</LinkButton>
|
||||
</Row>
|
||||
<LinkButton href={renderUrl(`/websites/${website.id}/settings`, false)}>
|
||||
<Icon>
|
||||
<Edit />
|
||||
</Icon>
|
||||
<Text>{formatMessage(labels.edit)}</Text>
|
||||
</LinkButton>
|
||||
)}
|
||||
</Row>
|
||||
</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';
|
||||
import { Fragment } from 'react';
|
||||
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 }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
|
|
@ -33,7 +33,7 @@ export function WebsiteMenu({ websiteId }: { websiteId: string }) {
|
|||
<MenuTrigger>
|
||||
<Button variant="quiet">
|
||||
<Icon>
|
||||
<More />
|
||||
<MoreHorizontal />
|
||||
</Icon>
|
||||
</Button>
|
||||
<Popover placement="bottom">
|
||||
|
|
|
|||
|
|
@ -7,14 +7,18 @@ import { formatLongNumber, formatShortTime } from '@/lib/format';
|
|||
|
||||
export function WebsiteMetricsBar({
|
||||
websiteId,
|
||||
compareMode,
|
||||
}: {
|
||||
websiteId: string;
|
||||
showChange?: boolean;
|
||||
compareMode?: boolean;
|
||||
}) {
|
||||
const { isAllTime } = useDateRange();
|
||||
const { isAllTime, dateCompare } = useDateRange();
|
||||
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 || {};
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export function WebsiteNav({
|
|||
event: undefined,
|
||||
compare: undefined,
|
||||
view: undefined,
|
||||
unit: undefined,
|
||||
});
|
||||
|
||||
const items = [
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
'use client';
|
||||
import { Column } from '@umami/react-zen';
|
||||
import { Column, Row } from '@umami/react-zen';
|
||||
import { ExpandedViewModal } from '@/app/(main)/websites/[websiteId]/ExpandedViewModal';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { UnitFilter } from '@/components/input/UnitFilter';
|
||||
import { WebsiteChart } from './WebsiteChart';
|
||||
import { WebsiteControls } from './WebsiteControls';
|
||||
import { WebsiteMetricsBar } from './WebsiteMetricsBar';
|
||||
|
|
@ -13,6 +14,9 @@ export function WebsitePage({ websiteId }: { websiteId: string }) {
|
|||
<WebsiteControls websiteId={websiteId} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
||||
<Panel minHeight="520px">
|
||||
<Row justifyContent="end">
|
||||
<UnitFilter />
|
||||
</Row>
|
||||
<WebsiteChart websiteId={websiteId} />
|
||||
</Panel>
|
||||
<WebsitePanels websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ export function ComparePage({ websiteId }: { websiteId: string }) {
|
|||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} allowCompare={true} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} showChange={true} />
|
||||
<WebsiteMetricsBar websiteId={websiteId} compareMode={true} showChange={true} />
|
||||
<Panel minHeight="520px">
|
||||
<WebsiteChart websiteId={websiteId} compareMode={true} />
|
||||
</Panel>
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@ export function CompareTables({ websiteId }: { websiteId: string }) {
|
|||
label: formatMessage(labels.hostname),
|
||||
path: renderPath('hostname'),
|
||||
},
|
||||
{
|
||||
id: 'distinctId',
|
||||
label: formatMessage(labels.distinctId),
|
||||
path: renderPath('distinctId'),
|
||||
},
|
||||
{
|
||||
id: 'tag',
|
||||
label: formatMessage(labels.tags),
|
||||
|
|
|
|||
|
|
@ -1,12 +1,18 @@
|
|||
'use client';
|
||||
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 { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { useMessages } from '@/components/hooks';
|
||||
import { useEventStatsQuery } from '@/components/hooks/queries/useEventStatsQuery';
|
||||
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 { formatLongNumber } from '@/lib/format';
|
||||
import { getItem, setItem } from '@/lib/storage';
|
||||
import { EventProperties } from './EventProperties';
|
||||
import { EventsDataTable } from './EventsDataTable';
|
||||
|
|
@ -15,16 +21,61 @@ const KEY_NAME = 'umami.events.tab';
|
|||
|
||||
export function EventsPage({ websiteId }) {
|
||||
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) => {
|
||||
setItem(KEY_NAME, 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 (
|
||||
<Column gap="3">
|
||||
<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>
|
||||
<Tabs selectedKey={tab} onSelectionChange={key => handleSelect(key)}>
|
||||
<TabList>
|
||||
|
|
|
|||
|
|
@ -25,6 +25,19 @@ export function EventsTable(props: DataTableProps) {
|
|||
const { updateParams } = useNavigation();
|
||||
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 (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="event" label={formatMessage(labels.event)} width="2fr">
|
||||
|
|
@ -43,7 +56,7 @@ export function EventsTable(props: DataTableProps) {
|
|||
title={row.eventName || row.urlPath}
|
||||
truncate
|
||||
>
|
||||
{row.eventName || row.urlPath}
|
||||
{row.eventName || renderLink(row.urlPath, row.hostname)}
|
||||
</Text>
|
||||
{row.hasData > 0 && <PropertiesButton websiteId={row.websiteId} eventId={row.id} />}
|
||||
</Row>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Metadata } from 'next';
|
||||
import { WebsiteLayout } from '@/app/(main)/websites/[websiteId]/WebsiteLayout';
|
||||
import { getWebsite } from '@/queries/prisma';
|
||||
|
||||
export default async function ({
|
||||
children,
|
||||
|
|
@ -9,6 +10,11 @@ export default async function ({
|
|||
params: Promise<{ websiteId: string }>;
|
||||
}) {
|
||||
const { websiteId } = await params;
|
||||
const website = await getWebsite(websiteId);
|
||||
|
||||
if (!website || website?.deletedAt) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <WebsiteLayout websiteId={websiteId}>{children}</WebsiteLayout>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,8 +74,9 @@ export function RealtimeLog({ data }: { data: any }) {
|
|||
os: string;
|
||||
country: 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) {
|
||||
return (
|
||||
|
|
@ -86,7 +87,8 @@ export function RealtimeLog({ data }: { data: any }) {
|
|||
url: (
|
||||
<a
|
||||
key="a"
|
||||
href={`//${website?.domain}${urlPath}`}
|
||||
href={`//${hostname}${urlPath}`}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
|
|
@ -100,7 +102,12 @@ export function RealtimeLog({ data }: { data: any }) {
|
|||
|
||||
if (__type === TYPE_PAGEVIEW) {
|
||||
return (
|
||||
<a href={`//${website?.domain}${urlPath}`} target="_blank" rel="noreferrer noopener">
|
||||
<a
|
||||
href={`//${hostname}${urlPath}`}
|
||||
style={{ fontWeight: 'bold' }}
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{urlPath}
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -39,10 +39,23 @@ export function SessionActivity({
|
|||
const { isMobile } = useMobile();
|
||||
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 (
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
<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));
|
||||
lastDay = createdAt;
|
||||
|
||||
|
|
@ -61,7 +74,7 @@ export function SessionActivity({
|
|||
: formatMessage(labels.viewedPage)}
|
||||
</Text>
|
||||
<Text weight="bold" style={{ maxWidth: isMobile ? '400px' : null }} truncate>
|
||||
{eventName || urlPath}
|
||||
{eventName || renderLink(urlPath, hostname)}
|
||||
</Text>
|
||||
{hasData > 0 && <PropertiesButton websiteId={websiteId} eventId={eventId} />}
|
||||
</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 { Panel } from '@/components/common/Panel';
|
||||
import { useWebsite } from '@/components/hooks';
|
||||
import { WebsiteData } from './WebsiteData';
|
||||
import { WebsiteEditForm } from './WebsiteEditForm';
|
||||
import { WebsiteShareForm } from './WebsiteShareForm';
|
||||
import { WebsiteTrackingCode } from './WebsiteTrackingCode';
|
||||
|
||||
export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) {
|
||||
const website = useWebsite();
|
||||
|
||||
return (
|
||||
<Column gap="6">
|
||||
<Panel>
|
||||
|
|
@ -18,7 +15,7 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal
|
|||
<WebsiteTrackingCode websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WebsiteShareForm websiteId={websiteId} shareId={website.shareId} />
|
||||
<WebsiteShareForm websiteId={websiteId} />
|
||||
</Panel>
|
||||
<Panel>
|
||||
<WebsiteData websiteId={websiteId} />
|
||||
|
|
|
|||
|
|
@ -1,93 +1,43 @@
|
|||
import {
|
||||
Button,
|
||||
Column,
|
||||
Form,
|
||||
FormButtons,
|
||||
FormSubmitButton,
|
||||
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);
|
||||
import { Column, Heading, Row, Text } from '@umami/react-zen';
|
||||
import { Plus } from 'lucide-react';
|
||||
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { ShareEditForm } from './ShareEditForm';
|
||||
import { SharesTable } from './SharesTable';
|
||||
|
||||
export interface WebsiteShareFormProps {
|
||||
websiteId: string;
|
||||
shareId?: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function WebsiteShareForm({ websiteId, shareId, onSave, onClose }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
||||
const [currentId, setCurrentId] = useState(shareId);
|
||||
const { mutateAsync, error, touch, toast } = useUpdateQuery(`/websites/${websiteId}`);
|
||||
const { cloudMode } = useConfig();
|
||||
export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
||||
const { formatMessage, labels, messages } = useMessages();
|
||||
const { data, isLoading } = useWebsiteSharesQuery({ websiteId });
|
||||
|
||||
const getUrl = (shareId: string) => {
|
||||
if (cloudMode) {
|
||||
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?.();
|
||||
},
|
||||
});
|
||||
};
|
||||
const shares = data?.data || [];
|
||||
const hasShares = shares.length > 0;
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSave} error={getErrorMessage(error)} values={{ url }}>
|
||||
<Column gap>
|
||||
<Switch isSelected={!!currentId} onChange={handleSwitch}>
|
||||
{formatMessage(labels.enableShareUrl)}
|
||||
</Switch>
|
||||
{currentId && (
|
||||
<Row alignItems="flex-end" gap>
|
||||
<Column flexGrow={1}>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
<Column>
|
||||
<Button onPress={handleGenerate}>
|
||||
<IconLabel icon={<RefreshCcw />} label={formatMessage(labels.regenerate)} />
|
||||
</Button>
|
||||
</Column>
|
||||
</Row>
|
||||
)}
|
||||
<FormButtons justifyContent="flex-end">
|
||||
<Row alignItems="center" gap>
|
||||
{onClose && <Button onPress={onClose}>{formatMessage(labels.cancel)}</Button>}
|
||||
<FormSubmitButton isDisabled={false}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</Row>
|
||||
</FormButtons>
|
||||
</Column>
|
||||
</Form>
|
||||
<Column gap="4">
|
||||
<Row justifyContent="space-between" alignItems="center">
|
||||
<Heading>{formatMessage(labels.share)}</Heading>
|
||||
<DialogButton
|
||||
icon={<Plus size={16} />}
|
||||
label={formatMessage(labels.add)}
|
||||
title={formatMessage(labels.share)}
|
||||
variant="primary"
|
||||
width="600px"
|
||||
>
|
||||
{({ close }) => <ShareEditForm websiteId={websiteId} onClose={close} />}
|
||||
</DialogButton>
|
||||
</Row>
|
||||
{hasShares ? (
|
||||
<>
|
||||
<Text>{formatMessage(messages.shareUrl)}</Text>
|
||||
<SharesTable data={shares} />
|
||||
</>
|
||||
) : (
|
||||
<Text color="muted">{formatMessage(messages.noDataAvailable)}</Text>
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
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 { parseRequest } from '@/lib/request';
|
||||
import { ok } from '@/lib/response';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { error } = await parseRequest(request);
|
||||
|
||||
if (error) {
|
||||
return error();
|
||||
}
|
||||
|
||||
if (redis.enabled) {
|
||||
const token = request.headers.get('authorization')?.split(' ')?.[1];
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { saveAuth } from '@/lib/auth';
|
||||
import redis from '@/lib/redis';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { json } from '@/lib/response';
|
||||
import { json, serverError } from '@/lib/response';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { auth, error } = await parseRequest(request);
|
||||
|
|
@ -10,9 +10,13 @@ export async function POST(request: Request) {
|
|||
return error();
|
||||
}
|
||||
|
||||
if (redis.enabled) {
|
||||
const token = await saveAuth({ userId: auth.user.id }, 86400);
|
||||
|
||||
return json({ user: auth.user, token });
|
||||
if (!redis.enabled) {
|
||||
return serverError({
|
||||
message: 'Redis is disabled',
|
||||
});
|
||||
}
|
||||
|
||||
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,
|
||||
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
||||
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 { eventType } = parameters;
|
||||
|
||||
if (!(await canViewWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
if (eventType) {
|
||||
filters.eventType = eventType;
|
||||
}
|
||||
|
||||
const queryFilters = await getQueryFilters(filters, websiteId);
|
||||
|
||||
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) {
|
||||
const schema = z.object({
|
||||
name: z.string().max(50),
|
||||
ownerId: z.uuid().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
@ -40,7 +41,7 @@ export async function POST(request: Request) {
|
|||
return unauthorized();
|
||||
}
|
||||
|
||||
const { name } = body;
|
||||
const { name, ownerId } = body;
|
||||
|
||||
const team = await createTeam(
|
||||
{
|
||||
|
|
@ -48,7 +49,7 @@ export async function POST(request: Request) {
|
|||
name,
|
||||
accessCode: `team_${getRandomChars(16)}`,
|
||||
},
|
||||
auth.user.id,
|
||||
ownerId ?? auth.user.id,
|
||||
);
|
||||
|
||||
return json(team);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { z } from 'zod';
|
||||
import { hashPassword } from '@/lib/password';
|
||||
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 { canDeleteUser, canUpdateUser, canViewUser } from '@/permissions';
|
||||
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 }> }) {
|
||||
const schema = z.object({
|
||||
username: z.string().max(255).optional(),
|
||||
password: z.string().max(255).optional(),
|
||||
password: z.string().min(8).max(255).optional(),
|
||||
role: userRoleParam.optional(),
|
||||
});
|
||||
|
||||
|
|
@ -47,6 +47,10 @@ export async function POST(request: Request, { params }: { params: Promise<{ use
|
|||
|
||||
const user = await getUser(userId);
|
||||
|
||||
if (!user) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const data: any = {};
|
||||
|
||||
if (password) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { uuid } from '@/lib/crypto';
|
|||
import { hashPassword } from '@/lib/password';
|
||||
import { parseRequest } from '@/lib/request';
|
||||
import { badRequest, json, unauthorized } from '@/lib/response';
|
||||
import { userRoleParam } from '@/lib/schema';
|
||||
import { canCreateUser } from '@/permissions';
|
||||
import { createUser, getUserByUsername } from '@/queries/prisma';
|
||||
|
||||
|
|
@ -11,8 +12,8 @@ export async function POST(request: Request) {
|
|||
const schema = z.object({
|
||||
id: z.uuid().optional(),
|
||||
username: z.string().max(255),
|
||||
password: z.string(),
|
||||
role: z.string().regex(/admin|user|view-only/i),
|
||||
password: z.string().min(8).max(255),
|
||||
role: userRoleParam,
|
||||
});
|
||||
|
||||
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 { SHARE_ID_REGEX } from '@/lib/constants';
|
||||
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 { deleteWebsite, getWebsite, updateWebsite } from '@/queries/prisma';
|
||||
|
||||
|
|
@ -33,7 +32,6 @@ export async function POST(
|
|||
const schema = z.object({
|
||||
name: z.string().optional(),
|
||||
domain: z.string().optional(),
|
||||
shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(),
|
||||
});
|
||||
|
||||
const { auth, body, error } = await parseRequest(request, schema);
|
||||
|
|
@ -43,23 +41,15 @@ export async function POST(
|
|||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { name, domain, shareId } = body;
|
||||
const { name, domain } = body;
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
}
|
||||
|
||||
try {
|
||||
const website = await updateWebsite(websiteId, { name, domain, shareId });
|
||||
const website = await updateWebsite(websiteId, { name, domain });
|
||||
|
||||
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);
|
||||
}
|
||||
return Response.json(website);
|
||||
}
|
||||
|
||||
export async function DELETE(
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||
import { uuid } from '@/lib/crypto';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
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 { createSegment, getWebsiteSegments } from '@/queries/prisma';
|
||||
|
||||
|
|
@ -42,7 +42,7 @@ export async function POST(
|
|||
const schema = z.object({
|
||||
type: segmentTypeParam,
|
||||
name: z.string().max(200),
|
||||
parameters: anyObjectParam,
|
||||
parameters: segmentParamSchema,
|
||||
});
|
||||
|
||||
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 { startDate, endDate } = getCompareDate('prev', filters.startDate, filters.endDate);
|
||||
const { startDate, endDate } = getCompareDate(
|
||||
filters.compare ?? 'prev',
|
||||
filters.startDate,
|
||||
filters.endDate,
|
||||
);
|
||||
|
||||
const comparison = await getWebsiteStats(websiteId, {
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { z } from 'zod';
|
||||
import { uuid } from '@/lib/crypto';
|
||||
import redis from '@/lib/redis';
|
||||
import { fetchAccount } from '@/lib/load';
|
||||
import { getQueryFilters, parseRequest } from '@/lib/request';
|
||||
import { json, unauthorized } from '@/lib/response';
|
||||
import { pagingParams, searchParams } from '@/lib/schema';
|
||||
|
|
@ -52,7 +52,7 @@ export async function POST(request: Request) {
|
|||
const { id, name, domain, shareId, teamId } = body;
|
||||
|
||||
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) {
|
||||
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';
|
||||
import { Column, useTheme } from '@umami/react-zen';
|
||||
import { useEffect } from 'react';
|
||||
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
||||
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 { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
||||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { useShareTokenQuery } from '@/components/hooks';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||
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 { 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(() => {
|
||||
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) {
|
||||
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 (
|
||||
<Column backgroundColor="2">
|
||||
<PageBody gap>
|
||||
<Header />
|
||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||
<WebsiteHeader showActions={false} />
|
||||
<WebsitePage websiteId={shareToken.websiteId} />
|
||||
</WebsiteProvider>
|
||||
<Footer />
|
||||
</PageBody>
|
||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
||||
<MobileMenuButton>
|
||||
{({ close }) => {
|
||||
return <ShareNav shareId={shareId} parameters={parameters} onItemClick={close} />;
|
||||
}}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { SharePage } from './SharePage';
|
|||
|
||||
export default async function ({ params }: { params: Promise<{ shareId: string[] }> }) {
|
||||
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
|
||||
{...props}
|
||||
width="100%"
|
||||
minHeight="100vh"
|
||||
paddingBottom="6"
|
||||
maxWidth={maxWidth}
|
||||
paddingX={{ xs: '3', md: '6' }}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ export * from './queries/useWebsiteSegmentsQuery';
|
|||
export * from './queries/useWebsiteSessionQuery';
|
||||
export * from './queries/useWebsiteSessionStatsQuery';
|
||||
export * from './queries/useWebsiteSessionsQuery';
|
||||
export * from './queries/useWebsiteSharesQuery';
|
||||
export * from './queries/useWebsiteStatsQuery';
|
||||
export * from './queries/useWebsitesQuery';
|
||||
export * from './queries/useWebsiteValuesQuery';
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function useEventDataValuesQuery(
|
|||
return useQuery<any>({
|
||||
queryKey: [
|
||||
'websites:event-data:values',
|
||||
{ websiteId, event, propertyName, startAt, endAt, unit, timezone, ...filters },
|
||||
{ websiteId, startAt, endAt, unit, timezone, ...filters, event, propertyName },
|
||||
],
|
||||
queryFn: () =>
|
||||
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 { post, useQuery } = useApi();
|
||||
const { startDate, endDate, timezone } = useDateParameters();
|
||||
const { startDate, endDate, timezone, unit } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<T>({
|
||||
|
|
@ -22,6 +22,7 @@ export function useResultQuery<T = any>(
|
|||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
unit,
|
||||
...params,
|
||||
...filters,
|
||||
},
|
||||
|
|
@ -35,6 +36,7 @@ export function useResultQuery<T = any>(
|
|||
startDate,
|
||||
endDate,
|
||||
timezone,
|
||||
unit,
|
||||
...parameters,
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useApi } from '../useApi';
|
|||
|
||||
const selector = (state: { shareToken: string }) => state.shareToken;
|
||||
|
||||
export function useShareTokenQuery(shareId: string): {
|
||||
export function useShareTokenQuery(slug: string): {
|
||||
shareToken: any;
|
||||
isLoading?: boolean;
|
||||
error?: Error;
|
||||
|
|
@ -11,9 +11,9 @@ export function useShareTokenQuery(shareId: string): {
|
|||
const shareToken = useApp(selector);
|
||||
const { get, useQuery } = useApi();
|
||||
const { isLoading, error } = useQuery({
|
||||
queryKey: ['share', shareId],
|
||||
queryKey: ['share', slug],
|
||||
queryFn: async () => {
|
||||
const data = await get(`/share/${shareId}`);
|
||||
const data = await get(`/share/${slug}`);
|
||||
|
||||
setShareToken(data);
|
||||
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteExpandedMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteExpandedMetricsData>({
|
||||
|
|
@ -29,8 +29,6 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
websiteId,
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
},
|
||||
|
|
@ -39,8 +37,6 @@ export function useWebsiteExpandedMetricsQuery(
|
|||
get(`/websites/${websiteId}/metrics/expanded`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export function useWebsiteMetricsQuery(
|
|||
options?: ReactQueryOptions<WebsiteMetricsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteMetricsData>({
|
||||
|
|
@ -25,8 +25,6 @@ export function useWebsiteMetricsQuery(
|
|||
websiteId,
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...params,
|
||||
},
|
||||
|
|
@ -35,8 +33,6 @@ export function useWebsiteMetricsQuery(
|
|||
get(`/websites/${websiteId}/metrics`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...filters,
|
||||
...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(
|
||||
websiteId: string,
|
||||
{ websiteId, compare }: { websiteId: string; compare?: string },
|
||||
options?: UseQueryOptions<WebsiteStatsData, Error, WebsiteStatsData>,
|
||||
) {
|
||||
const { get, useQuery } = useApi();
|
||||
const { startAt, endAt, unit, timezone } = useDateParameters();
|
||||
const { startAt, endAt } = useDateParameters();
|
||||
const filters = useFilterParameters();
|
||||
|
||||
return useQuery<WebsiteStatsData>({
|
||||
queryKey: ['websites:stats', { websiteId, startAt, endAt, unit, timezone, ...filters }],
|
||||
queryFn: () =>
|
||||
get(`/websites/${websiteId}/stats`, { startAt, endAt, unit, timezone, ...filters }),
|
||||
queryKey: ['websites:stats', { websiteId, compare, startAt, endAt, ...filters }],
|
||||
queryFn: () => get(`/websites/${websiteId}/stats`, { compare, startAt, endAt, ...filters }),
|
||||
enabled: !!websiteId,
|
||||
...options,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,13 +12,12 @@ export function useWeeklyTrafficQuery(websiteId: string, params?: Record<string,
|
|||
return useQuery({
|
||||
queryKey: [
|
||||
'sessions',
|
||||
{ websiteId, modified, startAt, endAt, unit, timezone, ...params, ...filters },
|
||||
{ websiteId, modified, startAt, endAt, timezone, ...params, ...filters },
|
||||
],
|
||||
queryFn: () => {
|
||||
return get(`/websites/${websiteId}/sessions/weekly`, {
|
||||
startAt,
|
||||
endAt,
|
||||
unit,
|
||||
timezone,
|
||||
...params,
|
||||
...filters,
|
||||
|
|
|
|||
|
|
@ -7,13 +7,13 @@ import { getItem } from '@/lib/storage';
|
|||
|
||||
export function useDateRange(options: { ignoreOffset?: boolean; timezone?: string } = {}) {
|
||||
const {
|
||||
query: { date = '', offset = 0, compare = 'prev' },
|
||||
query: { date = '', unit = '', offset = 0, compare = 'prev' },
|
||||
} = useNavigation();
|
||||
const { locale } = useLocale();
|
||||
|
||||
const dateRange = useMemo(() => {
|
||||
const dateRangeObject = parseDateRange(
|
||||
date || getItem(DATE_RANGE_CONFIG) || DEFAULT_DATE_RANGE_VALUE,
|
||||
unit,
|
||||
locale,
|
||||
options.timezone,
|
||||
);
|
||||
|
|
@ -21,12 +21,13 @@ export function useDateRange(options: { ignoreOffset?: boolean; timezone?: strin
|
|||
return !options.ignoreOffset && offset
|
||||
? getOffsetDateRange(dateRangeObject, +offset)
|
||||
: dateRangeObject;
|
||||
}, [date, offset, options]);
|
||||
}, [date, unit, offset, options]);
|
||||
|
||||
const dateCompare = getCompareDate(compare, dateRange.startDate, dateRange.endDate);
|
||||
|
||||
return {
|
||||
date,
|
||||
unit,
|
||||
offset,
|
||||
compare,
|
||||
isAllTime: date.endsWith(`:all`),
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export function useFields() {
|
|||
{ name: 'region', type: 'string', label: formatMessage(labels.region) },
|
||||
{ name: 'city', type: 'string', label: formatMessage(labels.city) },
|
||||
{ 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: '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