mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2025-01-08 17:27:32 +03:00
Merge pull request #815 from acelaya-forks/feature/overview-bots
Feature/overview bots
This commit is contained in:
commit
a6d000714b
17 changed files with 531 additions and 205 deletions
|
@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
## [Unreleased]
|
||||
### Added
|
||||
* [#807](https://github.com/shlinkio/shlink-web-client/issues/807) Add support for device-specific long-URLs when creating or editing short URLs.
|
||||
* [#808](https://github.com/shlinkio/shlink-web-client/issues/808) Respect settings on excluding bots in the overview section, for visits cards.
|
||||
|
||||
### Changed
|
||||
* Update to Vite 4.1
|
||||
|
|
267
package-lock.json
generated
267
package-lock.json
generated
|
@ -9,7 +9,7 @@
|
|||
"dependencies": {
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||
|
@ -74,7 +74,7 @@
|
|||
"@types/uuid": "^8.3.4",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"babel-jest": "^29.3.1",
|
||||
"babel-jest": "^29.5.0",
|
||||
"chalk": "^5.2.0",
|
||||
"eslint": "^8.30.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
|
@ -257,15 +257,17 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.20.7",
|
||||
"license": "MIT",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz",
|
||||
"integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.19.0",
|
||||
"@babel/helper-member-expression-to-functions": "^7.20.7",
|
||||
"@babel/helper-function-name": "^7.21.0",
|
||||
"@babel/helper-member-expression-to-functions": "^7.21.0",
|
||||
"@babel/helper-optimise-call-expression": "^7.18.6",
|
||||
"@babel/helper-replace-supers": "^7.20.7",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -369,10 +371,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-member-expression-to-functions": {
|
||||
"version": "7.20.7",
|
||||
"license": "MIT",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz",
|
||||
"integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/types": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -498,8 +501,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.18.6",
|
||||
"license": "MIT",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz",
|
||||
"integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ==",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
|
@ -1008,10 +1012,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-syntax-typescript": {
|
||||
"version": "7.18.6",
|
||||
"license": "MIT",
|
||||
"version": "7.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
|
||||
"integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6"
|
||||
"@babel/helper-plugin-utils": "^7.19.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -1522,12 +1527,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typescript": {
|
||||
"version": "7.19.3",
|
||||
"license": "MIT",
|
||||
"version": "7.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz",
|
||||
"integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==",
|
||||
"dependencies": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.19.0",
|
||||
"@babel/helper-plugin-utils": "^7.19.0",
|
||||
"@babel/plugin-syntax-typescript": "^7.18.6"
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
"@babel/helper-create-class-features-plugin": "^7.21.0",
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/plugin-syntax-typescript": "^7.20.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -1690,12 +1697,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/preset-typescript": {
|
||||
"version": "7.18.6",
|
||||
"license": "MIT",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz",
|
||||
"integrity": "sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6",
|
||||
"@babel/helper-validator-option": "^7.18.6",
|
||||
"@babel/plugin-transform-typescript": "^7.18.6"
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/helper-validator-option": "^7.21.0",
|
||||
"@babel/plugin-transform-typescript": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
@ -2861,11 +2869,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jest/schemas": {
|
||||
"version": "29.0.0",
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
|
||||
"integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.24.1"
|
||||
"@sinclair/typebox": "^0.25.16"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
|
@ -2913,25 +2922,26 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jest/transform": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz",
|
||||
"integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@jest/types": "^29.3.1",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.15",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"chalk": "^4.0.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"jest-haste-map": "^29.3.1",
|
||||
"jest-regex-util": "^29.2.0",
|
||||
"jest-util": "^29.3.1",
|
||||
"jest-haste-map": "^29.5.0",
|
||||
"jest-regex-util": "^29.4.3",
|
||||
"jest-util": "^29.5.0",
|
||||
"micromatch": "^4.0.4",
|
||||
"pirates": "^4.0.4",
|
||||
"slash": "^3.0.0",
|
||||
"write-file-atomic": "^4.0.1"
|
||||
"write-file-atomic": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
|
@ -3014,11 +3024,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@jest/types": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz",
|
||||
"integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.0.0",
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
|
@ -3301,9 +3312,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@sinclair/typebox": {
|
||||
"version": "0.24.44",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@sinonjs/commons": {
|
||||
"version": "1.8.6",
|
||||
|
@ -4585,14 +4597,15 @@
|
|||
"peer": true
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz",
|
||||
"integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/transform": "^29.3.1",
|
||||
"@jest/transform": "^29.5.0",
|
||||
"@types/babel__core": "^7.1.14",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"babel-preset-jest": "^29.2.0",
|
||||
"babel-preset-jest": "^29.5.0",
|
||||
"chalk": "^4.0.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"slash": "^3.0.0"
|
||||
|
@ -4679,9 +4692,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/babel-plugin-jest-hoist": {
|
||||
"version": "29.2.0",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz",
|
||||
"integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.3.3",
|
||||
"@babel/types": "^7.3.3",
|
||||
|
@ -4755,11 +4769,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/babel-preset-jest": {
|
||||
"version": "29.2.0",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz",
|
||||
"integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"babel-plugin-jest-hoist": "^29.2.0",
|
||||
"babel-plugin-jest-hoist": "^29.5.0",
|
||||
"babel-preset-current-node-syntax": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -8324,19 +8339,20 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jest-haste-map": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz",
|
||||
"integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "^29.3.1",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/graceful-fs": "^4.1.3",
|
||||
"@types/node": "*",
|
||||
"anymatch": "^3.0.3",
|
||||
"fb-watchman": "^2.0.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"jest-regex-util": "^29.2.0",
|
||||
"jest-util": "^29.3.1",
|
||||
"jest-worker": "^29.3.1",
|
||||
"jest-regex-util": "^29.4.3",
|
||||
"jest-util": "^29.5.0",
|
||||
"jest-worker": "^29.5.0",
|
||||
"micromatch": "^4.0.4",
|
||||
"walker": "^1.0.8"
|
||||
},
|
||||
|
@ -8627,9 +8643,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jest-regex-util": {
|
||||
"version": "29.2.0",
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz",
|
||||
"integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||
}
|
||||
|
@ -9073,11 +9090,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jest-util": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz",
|
||||
"integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jest/types": "^29.3.1",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.0.0",
|
||||
"ci-info": "^3.2.0",
|
||||
|
@ -9329,12 +9347,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/jest-worker": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz",
|
||||
"integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"jest-util": "^29.3.1",
|
||||
"jest-util": "^29.5.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.0.0"
|
||||
},
|
||||
|
@ -13033,14 +13052,17 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-create-class-features-plugin": {
|
||||
"version": "7.20.7",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.21.0.tgz",
|
||||
"integrity": "sha512-Q8wNiMIdwsv5la5SPxNYzzkPnjgC0Sy0i7jLkVOCdllu/xcVNkr3TeZzbHBJrj+XXRqzX5uCyCoV9eu6xUG7KQ==",
|
||||
"requires": {
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
"@babel/helper-environment-visitor": "^7.18.9",
|
||||
"@babel/helper-function-name": "^7.19.0",
|
||||
"@babel/helper-member-expression-to-functions": "^7.20.7",
|
||||
"@babel/helper-function-name": "^7.21.0",
|
||||
"@babel/helper-member-expression-to-functions": "^7.21.0",
|
||||
"@babel/helper-optimise-call-expression": "^7.18.6",
|
||||
"@babel/helper-replace-supers": "^7.20.7",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.20.0",
|
||||
"@babel/helper-split-export-declaration": "^7.18.6"
|
||||
}
|
||||
},
|
||||
|
@ -13099,9 +13121,11 @@
|
|||
}
|
||||
},
|
||||
"@babel/helper-member-expression-to-functions": {
|
||||
"version": "7.20.7",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.21.0.tgz",
|
||||
"integrity": "sha512-Muu8cdZwNN6mRRNG6lAYErJ5X3bRevgYR2O8wN0yn7jJSnGDu6eG59RfT29JHxGUovyfrh6Pj0XzmR7drNVL3Q==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.20.7"
|
||||
"@babel/types": "^7.21.0"
|
||||
}
|
||||
},
|
||||
"@babel/helper-module-imports": {
|
||||
|
@ -13177,7 +13201,9 @@
|
|||
"version": "7.19.1"
|
||||
},
|
||||
"@babel/helper-validator-option": {
|
||||
"version": "7.18.6"
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.21.0.tgz",
|
||||
"integrity": "sha512-rmL/B8/f0mKS2baE9ZpyTcTavvEuWhTTW8amjzXNvYG4AwBsqTLikfXsEofsJEfKHf+HQVQbFOHy6o+4cnC/fQ=="
|
||||
},
|
||||
"@babel/helper-wrap-function": {
|
||||
"version": "7.20.5",
|
||||
|
@ -13456,9 +13482,11 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-syntax-typescript": {
|
||||
"version": "7.18.6",
|
||||
"version": "7.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz",
|
||||
"integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6"
|
||||
"@babel/helper-plugin-utils": "^7.19.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-arrow-functions": {
|
||||
|
@ -13711,11 +13739,14 @@
|
|||
}
|
||||
},
|
||||
"@babel/plugin-transform-typescript": {
|
||||
"version": "7.19.3",
|
||||
"version": "7.21.3",
|
||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.21.3.tgz",
|
||||
"integrity": "sha512-RQxPz6Iqt8T0uw/WsJNReuBpWpBqs/n7mNo18sKLoTbMp+UrEekhH+pKSVC7gWz+DNjo9gryfV8YzCiT45RgMw==",
|
||||
"requires": {
|
||||
"@babel/helper-create-class-features-plugin": "^7.19.0",
|
||||
"@babel/helper-plugin-utils": "^7.19.0",
|
||||
"@babel/plugin-syntax-typescript": "^7.18.6"
|
||||
"@babel/helper-annotate-as-pure": "^7.18.6",
|
||||
"@babel/helper-create-class-features-plugin": "^7.21.0",
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/plugin-syntax-typescript": "^7.20.0"
|
||||
}
|
||||
},
|
||||
"@babel/plugin-transform-unicode-escapes": {
|
||||
|
@ -13838,11 +13869,13 @@
|
|||
}
|
||||
},
|
||||
"@babel/preset-typescript": {
|
||||
"version": "7.18.6",
|
||||
"version": "7.21.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.21.0.tgz",
|
||||
"integrity": "sha512-myc9mpoVA5m1rF8K8DgLEatOYFDpwC+RkMkjZ0Du6uI62YvDe8uxIEYVs/VCdSJ097nlALiU/yBC7//3nI+hNg==",
|
||||
"requires": {
|
||||
"@babel/helper-plugin-utils": "^7.18.6",
|
||||
"@babel/helper-validator-option": "^7.18.6",
|
||||
"@babel/plugin-transform-typescript": "^7.18.6"
|
||||
"@babel/helper-plugin-utils": "^7.20.2",
|
||||
"@babel/helper-validator-option": "^7.21.0",
|
||||
"@babel/plugin-transform-typescript": "^7.21.0"
|
||||
}
|
||||
},
|
||||
"@babel/runtime": {
|
||||
|
@ -14508,10 +14541,12 @@
|
|||
}
|
||||
},
|
||||
"@jest/schemas": {
|
||||
"version": "29.0.0",
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.4.3.tgz",
|
||||
"integrity": "sha512-VLYKXQmtmuEz6IxJsrZwzG9NvtkQsWNnWMsKxqWNu3+CnfzJQhp0WDDKWLVV9hLKr0l3SLLFRqcYHjhtyuDVxg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@sinclair/typebox": "^0.24.1"
|
||||
"@sinclair/typebox": "^0.25.16"
|
||||
}
|
||||
},
|
||||
"@jest/source-map": {
|
||||
|
@ -14544,24 +14579,26 @@
|
|||
}
|
||||
},
|
||||
"@jest/transform": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.5.0.tgz",
|
||||
"integrity": "sha512-8vbeZWqLJOvHaDfeMuoHITGKSz5qWc9u04lnWrQE3VyuSw604PzQM824ZeX9XSjUCeDiE3GuxZe5UKa8J61NQw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@jest/types": "^29.3.1",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.15",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"chalk": "^4.0.0",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"jest-haste-map": "^29.3.1",
|
||||
"jest-regex-util": "^29.2.0",
|
||||
"jest-util": "^29.3.1",
|
||||
"jest-haste-map": "^29.5.0",
|
||||
"jest-regex-util": "^29.4.3",
|
||||
"jest-util": "^29.5.0",
|
||||
"micromatch": "^4.0.4",
|
||||
"pirates": "^4.0.4",
|
||||
"slash": "^3.0.0",
|
||||
"write-file-atomic": "^4.0.1"
|
||||
"write-file-atomic": "^4.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
|
@ -14612,10 +14649,12 @@
|
|||
}
|
||||
},
|
||||
"@jest/types": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.5.0.tgz",
|
||||
"integrity": "sha512-qbu7kN6czmVRc3xWFQcAN03RAUamgppVUdXrvl1Wr3jlNF93o9mJbGcDWrwGB6ht44u7efB1qCFgVQmca24Uog==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/schemas": "^29.0.0",
|
||||
"@jest/schemas": "^29.4.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
"@types/istanbul-reports": "^3.0.0",
|
||||
"@types/node": "*",
|
||||
|
@ -14782,7 +14821,9 @@
|
|||
}
|
||||
},
|
||||
"@sinclair/typebox": {
|
||||
"version": "0.24.44",
|
||||
"version": "0.25.24",
|
||||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.25.24.tgz",
|
||||
"integrity": "sha512-XJfwUVUKDHF5ugKwIcxEgc9k8b7HbznCp6eUfWgu710hMPNIO4aw4/zB5RogDQz8nd6gyCDpU9O/m6qYEWY6yQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@sinonjs/commons": {
|
||||
|
@ -15627,13 +15668,15 @@
|
|||
"peer": true
|
||||
},
|
||||
"babel-jest": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.5.0.tgz",
|
||||
"integrity": "sha512-mA4eCDh5mSo2EcA9xQjVTpmbbNk32Zb3Q3QFQsNhaK56Q+yoXowzFodLux30HRgyOho5rsQ6B0P9QpMkvvnJ0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/transform": "^29.3.1",
|
||||
"@jest/transform": "^29.5.0",
|
||||
"@types/babel__core": "^7.1.14",
|
||||
"babel-plugin-istanbul": "^6.1.1",
|
||||
"babel-preset-jest": "^29.2.0",
|
||||
"babel-preset-jest": "^29.5.0",
|
||||
"chalk": "^4.0.0",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"slash": "^3.0.0"
|
||||
|
@ -15686,7 +15729,9 @@
|
|||
}
|
||||
},
|
||||
"babel-plugin-jest-hoist": {
|
||||
"version": "29.2.0",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.5.0.tgz",
|
||||
"integrity": "sha512-zSuuuAlTMT4mzLj2nPnUm6fsE6270vdOfnpbJ+RmruU75UhLFvL0N2NgI7xpeS7NaB6hGqmd5pVpGTDYvi4Q3w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/template": "^7.3.3",
|
||||
|
@ -15740,10 +15785,12 @@
|
|||
}
|
||||
},
|
||||
"babel-preset-jest": {
|
||||
"version": "29.2.0",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.5.0.tgz",
|
||||
"integrity": "sha512-JOMloxOqdiBSxMAzjRaH023/vvcaSaec49zvg+2LmNsktC7ei39LTJGw02J+9uUtTZUq6xbLyJ4dxe9sSmIuAg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"babel-plugin-jest-hoist": "^29.2.0",
|
||||
"babel-plugin-jest-hoist": "^29.5.0",
|
||||
"babel-preset-current-node-syntax": "^1.0.0"
|
||||
}
|
||||
},
|
||||
|
@ -17968,19 +18015,21 @@
|
|||
"dev": true
|
||||
},
|
||||
"jest-haste-map": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.5.0.tgz",
|
||||
"integrity": "sha512-IspOPnnBro8YfVYSw6yDRKh/TiCdRngjxeacCps1cQ9cgVN6+10JUcuJ1EabrgYLOATsIAigxA0rLR9x/YlrSA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^29.3.1",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/graceful-fs": "^4.1.3",
|
||||
"@types/node": "*",
|
||||
"anymatch": "^3.0.3",
|
||||
"fb-watchman": "^2.0.0",
|
||||
"fsevents": "^2.3.2",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"jest-regex-util": "^29.2.0",
|
||||
"jest-util": "^29.3.1",
|
||||
"jest-worker": "^29.3.1",
|
||||
"jest-regex-util": "^29.4.3",
|
||||
"jest-util": "^29.5.0",
|
||||
"jest-worker": "^29.5.0",
|
||||
"micromatch": "^4.0.4",
|
||||
"walker": "^1.0.8"
|
||||
}
|
||||
|
@ -18160,7 +18209,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"jest-regex-util": {
|
||||
"version": "29.2.0",
|
||||
"version": "29.4.3",
|
||||
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.4.3.tgz",
|
||||
"integrity": "sha512-O4FglZaMmWXbGHSQInfXewIsd1LMn9p3ZXB/6r4FOkyhX2/iP/soMG98jGvk/A3HAN78+5VWcBGO0BJAPRh4kg==",
|
||||
"dev": true
|
||||
},
|
||||
"jest-resolve": {
|
||||
|
@ -18457,10 +18508,12 @@
|
|||
}
|
||||
},
|
||||
"jest-util": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.5.0.tgz",
|
||||
"integrity": "sha512-RYMgG/MTadOr5t8KdhejfvUU82MxsCu5MF6KuDUHl+NuwzUt+Sm6jJWxTJVrDR1j5M/gJVCPKQEpWXY+yIQ6lQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jest/types": "^29.3.1",
|
||||
"@jest/types": "^29.5.0",
|
||||
"@types/node": "*",
|
||||
"chalk": "^4.0.0",
|
||||
"ci-info": "^3.2.0",
|
||||
|
@ -18619,11 +18672,13 @@
|
|||
}
|
||||
},
|
||||
"jest-worker": {
|
||||
"version": "29.3.1",
|
||||
"version": "29.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.5.0.tgz",
|
||||
"integrity": "sha512-NcrQnevGoSp4b5kg+akIpthoAFHxPBcb5P6mYPY0fUNT+sSvmtu6jlkEle3anczUKIKEbMxFimk9oTP/tpIPgA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*",
|
||||
"jest-util": "^29.3.1",
|
||||
"jest-util": "^29.5.0",
|
||||
"merge-stream": "^2.0.0",
|
||||
"supports-color": "^8.0.0"
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"dependencies": {
|
||||
"@babel/preset-env": "^7.20.2",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.18.6",
|
||||
"@babel/preset-typescript": "^7.21.0",
|
||||
"@fortawesome/fontawesome-free": "^6.3.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^6.3.0",
|
||||
"@fortawesome/free-brands-svg-icons": "^6.3.0",
|
||||
|
@ -90,7 +90,7 @@
|
|||
"@types/uuid": "^8.3.4",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"adm-zip": "^0.5.10",
|
||||
"babel-jest": "^29.3.1",
|
||||
"babel-jest": "^29.5.0",
|
||||
"chalk": "^5.2.0",
|
||||
"eslint": "^8.30.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
|
|
|
@ -40,13 +40,24 @@ export interface ShlinkPaginator {
|
|||
totalItems: number;
|
||||
}
|
||||
|
||||
export interface ShlinkVisitsSummary {
|
||||
total: number;
|
||||
nonBots: number;
|
||||
bots: number;
|
||||
}
|
||||
|
||||
export interface ShlinkVisits {
|
||||
data: Visit[];
|
||||
pagination: ShlinkPaginator;
|
||||
}
|
||||
|
||||
export interface ShlinkVisitsOverview {
|
||||
nonOrphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
|
||||
orphanVisits?: ShlinkVisitsSummary; // Optional only before Shlink 3.5.0
|
||||
|
||||
/** @deprecated */
|
||||
visitsCount: number;
|
||||
/** @deprecated */
|
||||
orphanVisitsCount: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Card, CardBody, CardHeader, Row } from 'reactstrap';
|
|||
import type { ShlinkShortUrlsListParams } from '../api/types';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import type { Settings } from '../settings/reducers/settings';
|
||||
import type { CreateShortUrlProps } from '../short-urls/CreateShortUrl';
|
||||
import type { ShortUrlsList as ShortUrlsListState } from '../short-urls/reducers/shortUrlsList';
|
||||
import { ITEMS_IN_OVERVIEW_PAGE } from '../short-urls/reducers/shortUrlsList';
|
||||
|
@ -16,6 +17,7 @@ import type { VisitsOverview } from '../visits/reducers/visitsOverview';
|
|||
import type { SelectedServer } from './data';
|
||||
import { getServerId } from './data';
|
||||
import { HighlightCard } from './helpers/HighlightCard';
|
||||
import { VisitsHighlightCard } from './helpers/VisitsHighlightCard';
|
||||
|
||||
interface OverviewConnectProps {
|
||||
shortUrlsList: ShortUrlsListState;
|
||||
|
@ -25,6 +27,7 @@ interface OverviewConnectProps {
|
|||
selectedServer: SelectedServer;
|
||||
visitsOverview: VisitsOverview;
|
||||
loadVisitsOverview: Function;
|
||||
settings: Settings;
|
||||
}
|
||||
|
||||
export const Overview = (
|
||||
|
@ -38,10 +41,11 @@ export const Overview = (
|
|||
selectedServer,
|
||||
loadVisitsOverview,
|
||||
visitsOverview,
|
||||
settings: { visits },
|
||||
}: OverviewConnectProps) => {
|
||||
const { loading, shortUrls } = shortUrlsList;
|
||||
const { loading: loadingTags } = tagsList;
|
||||
const { loading: loadingVisits, visitsCount, orphanVisitsCount } = visitsOverview;
|
||||
const { loading: loadingVisits, nonOrphanVisits, orphanVisits } = visitsOverview;
|
||||
const serverId = getServerId(selectedServer);
|
||||
const linkToNonOrphanVisits = useFeature('nonOrphanVisits', selectedServer);
|
||||
const navigate = useNavigate();
|
||||
|
@ -56,14 +60,22 @@ export const Overview = (
|
|||
<>
|
||||
<Row>
|
||||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<HighlightCard title="Visits" link={linkToNonOrphanVisits && `/server/${serverId}/non-orphan-visits`}>
|
||||
{loadingVisits ? 'Loading...' : prettify(visitsCount)}
|
||||
</HighlightCard>
|
||||
<VisitsHighlightCard
|
||||
title="Visits"
|
||||
link={linkToNonOrphanVisits ? `/server/${serverId}/non-orphan-visits` : undefined}
|
||||
excludeBots={visits?.excludeBots ?? false}
|
||||
loading={loadingVisits}
|
||||
visitsSummary={nonOrphanVisits}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<HighlightCard title="Orphan visits" link={`/server/${serverId}/orphan-visits`}>
|
||||
{loadingVisits ? 'Loading...' : prettify(orphanVisitsCount)}
|
||||
</HighlightCard>
|
||||
<VisitsHighlightCard
|
||||
title="Orphan visits"
|
||||
link={`/server/${serverId}/orphan-visits`}
|
||||
excludeBots={visits?.excludeBots ?? false}
|
||||
loading={loadingVisits}
|
||||
visitsSummary={orphanVisits}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-lg-6 col-xl-3 mb-3">
|
||||
<HighlightCard title="Short URLs" link={`/server/${serverId}/list-short-urls/1`}>
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import { faArrowAltCircleRight as linkIcon } from '@fortawesome/free-regular-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import type { FC, PropsWithChildren } from 'react';
|
||||
import type { FC, PropsWithChildren, ReactNode } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Card, CardText, CardTitle } from 'reactstrap';
|
||||
import { Card, CardText, CardTitle, UncontrolledTooltip } from 'reactstrap';
|
||||
import { useElementRef } from '../../utils/helpers/hooks';
|
||||
import './HighlightCard.scss';
|
||||
|
||||
export type HighlightCardProps = PropsWithChildren<{
|
||||
title: string;
|
||||
link?: string | false;
|
||||
link?: string;
|
||||
tooltip?: ReactNode;
|
||||
}>;
|
||||
|
||||
const buildExtraProps = (link?: string | false) => (!link ? {} : { tag: Link, to: link });
|
||||
const buildExtraProps = (link?: string) => (!link ? {} : { tag: Link, to: link });
|
||||
|
||||
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link }) => (
|
||||
<Card className="highlight-card" body {...buildExtraProps(link)}>
|
||||
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
|
||||
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
|
||||
<CardText tag="h2">{children}</CardText>
|
||||
</Card>
|
||||
);
|
||||
export const HighlightCard: FC<HighlightCardProps> = ({ children, title, link, tooltip }) => {
|
||||
const ref = useElementRef<HTMLElement>();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card innerRef={ref} className="highlight-card" body {...buildExtraProps(link)}>
|
||||
{link && <FontAwesomeIcon size="3x" className="highlight-card__link-icon" icon={linkIcon} />}
|
||||
<CardTitle tag="h5" className="highlight-card__title">{title}</CardTitle>
|
||||
<CardText tag="h2">{children}</CardText>
|
||||
</Card>
|
||||
{tooltip && <UncontrolledTooltip target={ref} placement="bottom">{tooltip}</UncontrolledTooltip>}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
26
src/servers/helpers/VisitsHighlightCard.tsx
Normal file
26
src/servers/helpers/VisitsHighlightCard.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import type { FC } from 'react';
|
||||
import { prettify } from '../../utils/helpers/numbers';
|
||||
import type { PartialVisitsSummary } from '../../visits/reducers/visitsOverview';
|
||||
import type { HighlightCardProps } from './HighlightCard';
|
||||
import { HighlightCard } from './HighlightCard';
|
||||
|
||||
export type VisitsHighlightCardProps = Omit<HighlightCardProps, 'tooltip' | 'children'> & {
|
||||
loading: boolean;
|
||||
excludeBots: boolean;
|
||||
visitsSummary: PartialVisitsSummary;
|
||||
};
|
||||
|
||||
export const VisitsHighlightCard: FC<VisitsHighlightCardProps> = ({ loading, excludeBots, visitsSummary, ...rest }) => (
|
||||
<HighlightCard
|
||||
tooltip={
|
||||
visitsSummary.bots !== undefined
|
||||
? <>{excludeBots ? 'Plus' : 'Including'} <b>{prettify(visitsSummary.bots)}</b> potential bot visits</>
|
||||
: undefined
|
||||
}
|
||||
{...rest}
|
||||
>
|
||||
{loading ? 'Loading...' : prettify(
|
||||
excludeBots && visitsSummary.nonBots ? visitsSummary.nonBots : visitsSummary.total,
|
||||
)}
|
||||
</HighlightCard>
|
||||
);
|
|
@ -65,7 +65,7 @@ export const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
|
||||
bottle.serviceFactory('Overview', Overview, 'ShortUrlsTable', 'CreateShortUrl');
|
||||
bottle.decorator('Overview', connect(
|
||||
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview'],
|
||||
['shortUrlsList', 'tagsList', 'selectedServer', 'mercureInfo', 'visitsOverview', 'settings'],
|
||||
['listShortUrls', 'listTags', 'createNewVisits', 'loadMercureInfo', 'loadVisitsOverview'],
|
||||
));
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { ShlinkVisitsSummary } from '../../api/types';
|
||||
import type { Order } from '../../utils/helpers/ordering';
|
||||
import type { Nullable, OptionalString } from '../../utils/utils';
|
||||
|
||||
|
@ -41,7 +42,7 @@ export interface ShortUrl {
|
|||
dateCreated: string;
|
||||
/** @deprecated */
|
||||
visitsCount: number; // Deprecated since Shlink 3.4.0
|
||||
visitsSummary?: ShortUrlVisitsSummary; // Optional only before Shlink 3.4.0
|
||||
visitsSummary?: ShlinkVisitsSummary; // Optional only before Shlink 3.4.0
|
||||
meta: Required<Nullable<ShortUrlMeta>>;
|
||||
tags: string[];
|
||||
domain: string | null;
|
||||
|
@ -56,12 +57,6 @@ export interface ShortUrlMeta {
|
|||
maxVisits?: number;
|
||||
}
|
||||
|
||||
export interface ShortUrlVisitsSummary {
|
||||
total: number;
|
||||
nonBots: number;
|
||||
bots: number;
|
||||
}
|
||||
|
||||
export interface ShortUrlModalProps {
|
||||
shortUrl: ShortUrl;
|
||||
isOpen: boolean;
|
||||
|
|
|
@ -3,14 +3,24 @@ import { createSlice } from '@reduxjs/toolkit';
|
|||
import type { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import type { ShlinkVisitsOverview } from '../../api/types';
|
||||
import { createAsyncThunk } from '../../utils/helpers/redux';
|
||||
import type { CreateVisit } from '../types';
|
||||
import { groupNewVisitsByType } from '../types/helpers';
|
||||
import { createNewVisits } from './visitCreation';
|
||||
|
||||
const REDUCER_PREFIX = 'shlink/visitsOverview';
|
||||
|
||||
export interface VisitsOverview {
|
||||
visitsCount: number;
|
||||
orphanVisitsCount: number;
|
||||
export type PartialVisitsSummary = {
|
||||
total: number;
|
||||
nonBots?: number;
|
||||
bots?: number;
|
||||
};
|
||||
|
||||
export type ParsedVisitsOverview = {
|
||||
nonOrphanVisits: PartialVisitsSummary;
|
||||
orphanVisits: PartialVisitsSummary;
|
||||
};
|
||||
|
||||
export interface VisitsOverview extends ParsedVisitsOverview {
|
||||
loading: boolean;
|
||||
error: boolean;
|
||||
}
|
||||
|
@ -18,15 +28,34 @@ export interface VisitsOverview {
|
|||
export type GetVisitsOverviewAction = PayloadAction<ShlinkVisitsOverview>;
|
||||
|
||||
const initialState: VisitsOverview = {
|
||||
visitsCount: 0,
|
||||
orphanVisitsCount: 0,
|
||||
nonOrphanVisits: {
|
||||
total: 0,
|
||||
},
|
||||
orphanVisits: {
|
||||
total: 0,
|
||||
},
|
||||
loading: false,
|
||||
error: false,
|
||||
};
|
||||
|
||||
const countBots = (visits: CreateVisit[]) => visits.filter(({ visit }) => visit.potentialBot).length;
|
||||
|
||||
export const loadVisitsOverview = (buildShlinkApiClient: ShlinkApiClientBuilder) => createAsyncThunk(
|
||||
`${REDUCER_PREFIX}/loadVisitsOverview`,
|
||||
(_: void, { getState }): Promise<ShlinkVisitsOverview> => buildShlinkApiClient(getState).getVisitsOverview(),
|
||||
(_: void, { getState }): Promise<ParsedVisitsOverview> => buildShlinkApiClient(getState).getVisitsOverview().then(
|
||||
(resp) => ({
|
||||
nonOrphanVisits: {
|
||||
total: resp.nonOrphanVisits?.total ?? resp.visitsCount,
|
||||
nonBots: resp.nonOrphanVisits?.nonBots,
|
||||
bots: resp.nonOrphanVisits?.bots,
|
||||
},
|
||||
orphanVisits: {
|
||||
total: resp.orphanVisits?.total ?? resp.orphanVisitsCount,
|
||||
nonBots: resp.orphanVisits?.nonBots,
|
||||
bots: resp.orphanVisits?.bots,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
export const visitsOverviewReducerCreator = (
|
||||
|
@ -40,13 +69,31 @@ export const visitsOverviewReducerCreator = (
|
|||
builder.addCase(loadVisitsOverviewThunk.rejected, () => ({ ...initialState, error: true }));
|
||||
builder.addCase(loadVisitsOverviewThunk.fulfilled, (_, { payload }) => ({ ...initialState, ...payload }));
|
||||
|
||||
builder.addCase(createNewVisits, ({ visitsCount, orphanVisitsCount = 0, ...rest }, { payload }) => {
|
||||
const { createdVisits } = payload;
|
||||
const { regularVisits, orphanVisits } = groupNewVisitsByType(createdVisits);
|
||||
builder.addCase(createNewVisits, ({ nonOrphanVisits, orphanVisits, ...rest }, { payload }) => {
|
||||
const { nonOrphanVisits: newNonOrphanVisits, orphanVisits: newOrphanVisits } = groupNewVisitsByType(
|
||||
payload.createdVisits,
|
||||
);
|
||||
|
||||
const newNonOrphanTotalVisits = newNonOrphanVisits.length;
|
||||
const newNonOrphanBotVisits = countBots(newNonOrphanVisits);
|
||||
const newNonOrphanNonBotVisits = newNonOrphanTotalVisits - newNonOrphanBotVisits;
|
||||
|
||||
const newOrphanTotalVisits = newOrphanVisits.length;
|
||||
const newOrphanBotVisits = countBots(newOrphanVisits);
|
||||
const newOrphanNonBotVisits = newOrphanTotalVisits - newOrphanBotVisits;
|
||||
|
||||
return {
|
||||
...rest,
|
||||
visitsCount: visitsCount + regularVisits.length,
|
||||
orphanVisitsCount: orphanVisitsCount + orphanVisits.length,
|
||||
nonOrphanVisits: {
|
||||
total: nonOrphanVisits.total + newNonOrphanTotalVisits,
|
||||
bots: nonOrphanVisits.bots && nonOrphanVisits.bots + newNonOrphanBotVisits,
|
||||
nonBots: nonOrphanVisits.nonBots && nonOrphanVisits.nonBots + newNonOrphanNonBotVisits,
|
||||
},
|
||||
orphanVisits: {
|
||||
total: orphanVisits.total + newOrphanTotalVisits,
|
||||
bots: orphanVisits.bots && orphanVisits.bots + newOrphanBotVisits,
|
||||
nonBots: orphanVisits.nonBots && orphanVisits.nonBots + newOrphanNonBotVisits,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
|
|
@ -10,13 +10,13 @@ export const isNormalizedOrphanVisit = (visit: NormalizedVisit): visit is Normal
|
|||
|
||||
export interface GroupedNewVisits {
|
||||
orphanVisits: CreateVisit[];
|
||||
regularVisits: CreateVisit[];
|
||||
nonOrphanVisits: CreateVisit[];
|
||||
}
|
||||
|
||||
export const groupNewVisitsByType = pipe(
|
||||
groupBy((newVisit: CreateVisit) => (isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'regularVisits')),
|
||||
groupBy((newVisit: CreateVisit) => (isOrphanVisit(newVisit.visit) ? 'orphanVisits' : 'nonOrphanVisits')),
|
||||
// @ts-expect-error Type declaration on groupBy is not correct. It can return undefined props
|
||||
(result): GroupedNewVisits => ({ orphanVisits: [], regularVisits: [], ...result }),
|
||||
(result): GroupedNewVisits => ({ orphanVisits: [], nonOrphanVisits: [], ...result }),
|
||||
);
|
||||
|
||||
export type HighlightableProps<T extends NormalizedVisit> = T extends NormalizedOrphanVisit
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import type { MercureInfo } from '../../src/mercure/reducers/mercureInfo';
|
||||
import type { ReachableServer } from '../../src/servers/data';
|
||||
import { Overview as overviewCreator } from '../../src/servers/Overview';
|
||||
import type { Settings } from '../../src/settings/reducers/settings';
|
||||
import type { ShortUrlsList as ShortUrlsListState } from '../../src/short-urls/reducers/shortUrlsList';
|
||||
import type { TagsList } from '../../src/tags/reducers/tagsList';
|
||||
import { prettify } from '../../src/utils/helpers/numbers';
|
||||
import type { VisitsOverview } from '../../src/visits/reducers/visitsOverview';
|
||||
import { renderWithEvents } from '../__helpers__/setUpTest';
|
||||
|
||||
describe('<Overview />', () => {
|
||||
const ShortUrlsTable = () => <>ShortUrlsTable</>;
|
||||
|
@ -20,7 +22,7 @@ describe('<Overview />', () => {
|
|||
pagination: { totalItems: 83710 },
|
||||
};
|
||||
const serverId = '123';
|
||||
const setUp = (loading = false) => render(
|
||||
const setUp = (loading = false, excludeBots = false) => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<Overview
|
||||
listShortUrls={listShortUrls}
|
||||
|
@ -28,11 +30,16 @@ describe('<Overview />', () => {
|
|||
loadVisitsOverview={loadVisitsOverview}
|
||||
shortUrlsList={Mock.of<ShortUrlsListState>({ loading, shortUrls })}
|
||||
tagsList={Mock.of<TagsList>({ loading, tags: ['foo', 'bar', 'baz'] })}
|
||||
visitsOverview={Mock.of<VisitsOverview>({ loading, visitsCount: 3456, orphanVisitsCount: 28 })}
|
||||
visitsOverview={Mock.of<VisitsOverview>({
|
||||
loading,
|
||||
nonOrphanVisits: { total: 3456, bots: 1000, nonBots: 2456 },
|
||||
orphanVisits: { total: 28, bots: 15, nonBots: 13 },
|
||||
})}
|
||||
selectedServer={Mock.of<ReachableServer>({ id: serverId })}
|
||||
createNewVisits={jest.fn()}
|
||||
loadMercureInfo={jest.fn()}
|
||||
mercureInfo={Mock.all<MercureInfo>()}
|
||||
settings={Mock.of<Settings>({ visits: { excludeBots } })}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
@ -42,16 +49,19 @@ describe('<Overview />', () => {
|
|||
expect(screen.getAllByText('Loading...')).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('displays amounts in cards after finishing loading', () => {
|
||||
setUp();
|
||||
it.each([
|
||||
[false, 3456, 28],
|
||||
[true, 2456, 13],
|
||||
])('displays amounts in cards after finishing loading', (excludeBots, expectedVisits, expectedOrphanVisits) => {
|
||||
setUp(false, excludeBots);
|
||||
|
||||
const headingElements = screen.getAllByRole('heading');
|
||||
|
||||
expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
|
||||
expect(headingElements[0]).toHaveTextContent('Visits');
|
||||
expect(headingElements[1]).toHaveTextContent(prettify(3456));
|
||||
expect(headingElements[1]).toHaveTextContent(prettify(expectedVisits));
|
||||
expect(headingElements[2]).toHaveTextContent('Orphan visits');
|
||||
expect(headingElements[3]).toHaveTextContent(prettify(28));
|
||||
expect(headingElements[3]).toHaveTextContent(prettify(expectedOrphanVisits));
|
||||
expect(headingElements[4]).toHaveTextContent('Short URLs');
|
||||
expect(headingElements[5]).toHaveTextContent(prettify(83710));
|
||||
expect(headingElements[6]).toHaveTextContent('Tags');
|
||||
|
@ -77,4 +87,20 @@ describe('<Overview />', () => {
|
|||
expect(links[3]).toHaveAttribute('href', `/server/${serverId}/create-short-url`);
|
||||
expect(links[4]).toHaveAttribute('href', `/server/${serverId}/list-short-urls/1`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true],
|
||||
[false],
|
||||
])('displays amounts of bots when hovering visits cards', async (excludeBots) => {
|
||||
const { user } = setUp(false, excludeBots);
|
||||
const expectTooltipToBeInTheDocument = async (tooltip: string) => waitFor(
|
||||
() => expect(screen.getByText(/potential bot visits$/)).toHaveTextContent(tooltip),
|
||||
);
|
||||
|
||||
await user.hover(screen.getByText(/^Visits/));
|
||||
await expectTooltipToBeInTheDocument(`${excludeBots ? 'Plus' : 'Including'} 1,000 potential bot visits`);
|
||||
|
||||
await user.hover(screen.getByText(/^Orphan visits/));
|
||||
await expectTooltipToBeInTheDocument(`${excludeBots ? 'Plus' : 'Including'} 15 potential bot visits`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import type { ReactNode } from 'react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import type { HighlightCardProps } from '../../../src/servers/helpers/HighlightCard';
|
||||
import { HighlightCard } from '../../../src/servers/helpers/HighlightCard';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<HighlightCard />', () => {
|
||||
const setUp = (props: HighlightCardProps & { children?: ReactNode }) => render(
|
||||
const setUp = (props: HighlightCardProps & { children?: ReactNode }) => renderWithEvents(
|
||||
<MemoryRouter>
|
||||
<HighlightCard {...props} />
|
||||
</MemoryRouter>,
|
||||
|
@ -13,9 +14,9 @@ describe('<HighlightCard />', () => {
|
|||
|
||||
it.each([
|
||||
[undefined],
|
||||
[false],
|
||||
[''],
|
||||
])('does not render icon when there is no link', (link) => {
|
||||
setUp({ title: 'foo', link: link as undefined | false });
|
||||
setUp({ title: 'foo', link });
|
||||
|
||||
expect(screen.queryByRole('img', { hidden: true })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
||||
|
@ -27,7 +28,7 @@ describe('<HighlightCard />', () => {
|
|||
['baz'],
|
||||
])('renders provided title', (title) => {
|
||||
setUp({ title });
|
||||
expect(screen.getByText(title)).toHaveAttribute('class', expect.stringContaining('highlight-card__title'));
|
||||
expect(screen.getByText(title)).toHaveClass('highlight-card__title');
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
@ -36,7 +37,7 @@ describe('<HighlightCard />', () => {
|
|||
['baz'],
|
||||
])('renders provided children', (children) => {
|
||||
setUp({ title: 'title', children });
|
||||
expect(screen.getByText(children)).toHaveAttribute('class', expect.stringContaining('card-text'));
|
||||
expect(screen.getByText(children)).toHaveClass('card-text');
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
@ -49,4 +50,11 @@ describe('<HighlightCard />', () => {
|
|||
expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument();
|
||||
expect(screen.getByRole('link')).toHaveAttribute('href', `/${link}`);
|
||||
});
|
||||
|
||||
it('renders tooltip when provided', async () => {
|
||||
const { user } = setUp({ title: 'title', children: 'Foo', tooltip: 'This is the tooltip' });
|
||||
|
||||
await user.hover(screen.getByText('Foo'));
|
||||
await waitFor(() => expect(screen.getByText('This is the tooltip')).toBeInTheDocument());
|
||||
});
|
||||
});
|
||||
|
|
60
test/servers/helpers/VisitsHighlightCard.test.tsx
Normal file
60
test/servers/helpers/VisitsHighlightCard.test.tsx
Normal file
|
@ -0,0 +1,60 @@
|
|||
import { screen, waitFor } from '@testing-library/react';
|
||||
import type { VisitsHighlightCardProps } from '../../../src/servers/helpers/VisitsHighlightCard';
|
||||
import { VisitsHighlightCard } from '../../../src/servers/helpers/VisitsHighlightCard';
|
||||
import { renderWithEvents } from '../../__helpers__/setUpTest';
|
||||
|
||||
describe('<VisitsHighlightCard />', () => {
|
||||
const setUp = (props: Partial<VisitsHighlightCardProps> = {}) => renderWithEvents(
|
||||
<VisitsHighlightCard loading={false} visitsSummary={{ total: 0 }} excludeBots={false} title="" {...props} />,
|
||||
);
|
||||
|
||||
it.each([
|
||||
[true, () => expect(screen.getByText('Loading...')).toBeInTheDocument()],
|
||||
[false, () => expect(screen.queryByText('Loading...')).not.toBeInTheDocument()],
|
||||
])('displays loading message on loading', (loading, assert) => {
|
||||
setUp({ loading });
|
||||
assert();
|
||||
});
|
||||
|
||||
it('does not render tooltip when summary has no bots', async () => {
|
||||
const { user } = setUp({ title: 'Foo' });
|
||||
|
||||
await user.hover(screen.getByText('Foo'));
|
||||
await waitFor(() => expect(screen.queryByText(/potential bot visits$/)).not.toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renders tooltip when summary has bots', async () => {
|
||||
const { user } = setUp({
|
||||
title: 'Foo',
|
||||
visitsSummary: { total: 50, bots: 30 },
|
||||
});
|
||||
|
||||
await user.hover(screen.getByText('Foo'));
|
||||
await waitFor(() => expect(screen.getByText(/potential bot visits$/)).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, 20, () => {
|
||||
expect(screen.getByText('20')).toBeInTheDocument();
|
||||
expect(screen.queryByText('50')).not.toBeInTheDocument();
|
||||
}],
|
||||
[true, undefined, () => {
|
||||
expect(screen.getByText('50')).toBeInTheDocument();
|
||||
expect(screen.queryByText('20')).not.toBeInTheDocument();
|
||||
}],
|
||||
[false, 20, () => {
|
||||
expect(screen.getByText('50')).toBeInTheDocument();
|
||||
expect(screen.queryByText('20')).not.toBeInTheDocument();
|
||||
}],
|
||||
[false, undefined, () => {
|
||||
expect(screen.getByText('50')).toBeInTheDocument();
|
||||
expect(screen.queryByText('20')).not.toBeInTheDocument();
|
||||
}],
|
||||
])('displays non-bots when present and bots are excluded', (excludeBots, nonBots, assert) => {
|
||||
setUp({
|
||||
excludeBots,
|
||||
visitsSummary: { total: 50, nonBots },
|
||||
});
|
||||
assert();
|
||||
});
|
||||
});
|
|
@ -1,7 +1,8 @@
|
|||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import type { ShortUrl, ShortUrlMeta, ShortUrlVisitsSummary } from '../../../src/short-urls/data';
|
||||
import type { ShlinkVisitsSummary } from '../../../src/api/types';
|
||||
import type { ShortUrl, ShortUrlMeta } from '../../../src/short-urls/data';
|
||||
import { ShortUrlStatus } from '../../../src/short-urls/helpers/ShortUrlStatus';
|
||||
|
||||
describe('<ShortUrlStatus />', () => {
|
||||
|
@ -23,12 +24,12 @@ describe('<ShortUrlStatus />', () => {
|
|||
],
|
||||
[
|
||||
Mock.of<ShortUrlMeta>({ maxVisits: 10 }),
|
||||
Mock.of<ShortUrlVisitsSummary>({ total: 10 }),
|
||||
Mock.of<ShlinkVisitsSummary>({ total: 10 }),
|
||||
'This short URL cannot be currently visited because it has reached the maximum amount of 10 visits.',
|
||||
],
|
||||
[
|
||||
Mock.of<ShortUrlMeta>({ maxVisits: 1 }),
|
||||
Mock.of<ShortUrlVisitsSummary>({ total: 1 }),
|
||||
Mock.of<ShlinkVisitsSummary>({ total: 1 }),
|
||||
'This short URL cannot be currently visited because it has reached the maximum amount of 1 visit.',
|
||||
],
|
||||
[{}, {}, 'This short URL can be visited normally.'],
|
||||
|
@ -36,7 +37,7 @@ describe('<ShortUrlStatus />', () => {
|
|||
[Mock.of<ShortUrlMeta>({ validSince: '2020-01-01T10:30:15' }), {}, 'This short URL can be visited normally.'],
|
||||
[
|
||||
Mock.of<ShortUrlMeta>({ maxVisits: 10 }),
|
||||
Mock.of<ShortUrlVisitsSummary>({ total: 1 }),
|
||||
Mock.of<ShlinkVisitsSummary>({ total: 1 }),
|
||||
'This short URL can be visited normally.',
|
||||
],
|
||||
])('shows expected tooltip', async (meta, visitsSummary, expectedTooltip) => {
|
||||
|
|
|
@ -5,8 +5,8 @@ import type { ShlinkState } from '../../../src/container/types';
|
|||
import type { CreateVisitsAction } from '../../../src/visits/reducers/visitCreation';
|
||||
import { createNewVisits } from '../../../src/visits/reducers/visitCreation';
|
||||
import type {
|
||||
GetVisitsOverviewAction,
|
||||
VisitsOverview } from '../../../src/visits/reducers/visitsOverview';
|
||||
GetVisitsOverviewAction, ParsedVisitsOverview,
|
||||
PartialVisitsSummary, VisitsOverview } from '../../../src/visits/reducers/visitsOverview';
|
||||
import {
|
||||
loadVisitsOverview as loadVisitsOverviewCreator,
|
||||
visitsOverviewReducerCreator,
|
||||
|
@ -46,45 +46,98 @@ describe('visitsOverviewReducer', () => {
|
|||
});
|
||||
|
||||
it('return visits overview on GET_OVERVIEW', () => {
|
||||
const { loading, error, visitsCount } = reducer(state({ loading: true, error: false }), {
|
||||
type: loadVisitsOverview.fulfilled.toString(),
|
||||
payload: { visitsCount: 100 },
|
||||
});
|
||||
const action = loadVisitsOverview.fulfilled(Mock.of<ParsedVisitsOverview>({
|
||||
nonOrphanVisits: { total: 100 },
|
||||
}), 'requestId');
|
||||
const { loading, error, nonOrphanVisits } = reducer(state({ loading: true, error: false }), action);
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visitsCount).toEqual(100);
|
||||
expect(nonOrphanVisits.total).toEqual(100);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[50, 53],
|
||||
[0, 3],
|
||||
[undefined, 3],
|
||||
])('returns updated amounts on CREATE_VISITS', (providedOrphanVisitsCount, expectedOrphanVisitsCount) => {
|
||||
const { visitsCount, orphanVisitsCount } = reducer(
|
||||
state({ visitsCount: 100, orphanVisitsCount: providedOrphanVisitsCount }),
|
||||
{
|
||||
type: createNewVisits.toString(),
|
||||
payload: {
|
||||
createdVisits: [
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
],
|
||||
},
|
||||
} as unknown as GetVisitsOverviewAction & CreateVisitsAction,
|
||||
const { nonOrphanVisits, orphanVisits } = reducer(
|
||||
state({
|
||||
nonOrphanVisits: { total: 100 },
|
||||
orphanVisits: { total: providedOrphanVisitsCount },
|
||||
}),
|
||||
createNewVisits([
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(visitsCount).toEqual(102);
|
||||
expect(orphanVisitsCount).toEqual(expectedOrphanVisitsCount);
|
||||
expect(nonOrphanVisits.total).toEqual(102);
|
||||
expect(orphanVisits.total).toEqual(expectedOrphanVisitsCount);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
{} satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{} satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103 } satisfies PartialVisitsSummary,
|
||||
{ total: 203 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ bots: 35 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ bots: 35 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, bots: 37 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, bots: 36 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ nonBots: 41, bots: 85 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ nonBots: 63, bots: 27 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, nonBots: 42, bots: 87 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, nonBots: 65, bots: 28 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
[
|
||||
{ nonBots: 56 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ nonBots: 99 } satisfies Omit<PartialVisitsSummary, 'total'>,
|
||||
{ total: 103, nonBots: 57 } satisfies PartialVisitsSummary,
|
||||
{ total: 203, nonBots: 101 } satisfies PartialVisitsSummary,
|
||||
],
|
||||
])('takes bots and non-bots into consideration when creating visits', (
|
||||
initialNonOrphanVisits,
|
||||
initialOrphanVisits,
|
||||
expectedNonOrphanVisits,
|
||||
expectedOrphanVisits,
|
||||
) => {
|
||||
const { nonOrphanVisits, orphanVisits } = reducer(
|
||||
state({
|
||||
nonOrphanVisits: { total: 100, ...initialNonOrphanVisits },
|
||||
orphanVisits: { total: 200, ...initialOrphanVisits },
|
||||
}),
|
||||
createNewVisits([
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.of<Visit>({ potentialBot: true }) }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.of<Visit>({ potentialBot: true }) }),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
Mock.of<CreateVisit>({
|
||||
visit: Mock.of<OrphanVisit>({ visitedUrl: '', potentialBot: true }),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
expect(nonOrphanVisits).toEqual(expectedNonOrphanVisits);
|
||||
expect(orphanVisits).toEqual(expectedOrphanVisits);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -109,8 +162,30 @@ describe('visitsOverviewReducer', () => {
|
|||
expect(getVisitsOverview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('dispatches start and success when promise is resolved', async () => {
|
||||
const resolvedOverview = Mock.of<ShlinkVisitsOverview>({ visitsCount: 50 });
|
||||
it.each([
|
||||
[
|
||||
// Shlink <3.5.0
|
||||
{ visitsCount: 50, orphanVisitsCount: 20 } satisfies ShlinkVisitsOverview,
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: undefined, bots: undefined },
|
||||
orphanVisits: { total: 20, nonBots: undefined, bots: undefined },
|
||||
},
|
||||
],
|
||||
[
|
||||
// Shlink >=3.5.0
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
orphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
visitsCount: 3,
|
||||
orphanVisitsCount: 3,
|
||||
} satisfies ShlinkVisitsOverview,
|
||||
{
|
||||
nonOrphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
orphanVisits: { total: 50, nonBots: 20, bots: 30 },
|
||||
},
|
||||
],
|
||||
])('dispatches start and success when promise is resolved', async (serverResult, dispatchedPayload) => {
|
||||
const resolvedOverview = Mock.of<ShlinkVisitsOverview>(serverResult);
|
||||
getVisitsOverview.mockResolvedValue(resolvedOverview);
|
||||
|
||||
await loadVisitsOverview()(dispatchMock, getState, {});
|
||||
|
@ -121,7 +196,7 @@ describe('visitsOverviewReducer', () => {
|
|||
}));
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expect.objectContaining({
|
||||
type: loadVisitsOverview.fulfilled.toString(),
|
||||
payload: { visitsCount: 50 },
|
||||
payload: dispatchedPayload,
|
||||
}));
|
||||
expect(getVisitsOverview).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@ import { groupNewVisitsByType, toApiParams } from '../../../src/visits/types/hel
|
|||
describe('visitsTypeHelpers', () => {
|
||||
describe('groupNewVisitsByType', () => {
|
||||
it.each([
|
||||
[[], { orphanVisits: [], regularVisits: [] }],
|
||||
[[], { orphanVisits: [], nonOrphanVisits: [] }],
|
||||
((): [CreateVisit[], GroupedNewVisits] => {
|
||||
const orphanVisits: CreateVisit[] = [
|
||||
Mock.of<CreateVisit>({
|
||||
|
@ -18,7 +18,7 @@ describe('visitsTypeHelpers', () => {
|
|||
visit: Mock.of<OrphanVisit>({ visitedUrl: '' }),
|
||||
}),
|
||||
];
|
||||
const regularVisits: CreateVisit[] = [
|
||||
const nonOrphanVisits: CreateVisit[] = [
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
|
@ -27,8 +27,8 @@ describe('visitsTypeHelpers', () => {
|
|||
];
|
||||
|
||||
return [
|
||||
[...orphanVisits, ...regularVisits],
|
||||
{ orphanVisits, regularVisits },
|
||||
[...orphanVisits, ...nonOrphanVisits],
|
||||
{ orphanVisits, nonOrphanVisits },
|
||||
];
|
||||
})(),
|
||||
((): [CreateVisit[], GroupedNewVisits] => {
|
||||
|
@ -44,16 +44,16 @@ describe('visitsTypeHelpers', () => {
|
|||
}),
|
||||
];
|
||||
|
||||
return [orphanVisits, { orphanVisits, regularVisits: [] }];
|
||||
return [orphanVisits, { orphanVisits, nonOrphanVisits: [] }];
|
||||
})(),
|
||||
((): [CreateVisit[], GroupedNewVisits] => {
|
||||
const regularVisits: CreateVisit[] = [
|
||||
const nonOrphanVisits: CreateVisit[] = [
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
Mock.of<CreateVisit>({ visit: Mock.all<Visit>() }),
|
||||
];
|
||||
|
||||
return [regularVisits, { orphanVisits: [], regularVisits }];
|
||||
return [nonOrphanVisits, { orphanVisits: [], nonOrphanVisits }];
|
||||
})(),
|
||||
])('groups new visits as expected', (createdVisits, expectedResult) => {
|
||||
expect(groupNewVisitsByType(createdVisits)).toEqual(expectedResult);
|
||||
|
|
Loading…
Reference in a new issue