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