mirror of
https://github.com/shlinkio/shlink-web-client.git
synced 2024-12-23 09:30:31 +03:00
Merge pull request #624 from acelaya-forks/feature/domain-visits
Feature/domain visits
This commit is contained in:
commit
5a0d67e409
31 changed files with 1245 additions and 219 deletions
|
@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
* *Nothing*
|
||||
* [#622](https://github.com/shlinkio/shlink-web-client/pull/622) Added support to load domain visits when consuming Shlink 3.1.0 or newer.
|
||||
|
||||
### Changed
|
||||
* [#616](https://github.com/shlinkio/shlink-web-client/pull/616) Updated to React 18.
|
||||
|
|
5
config/jest/setupTests.ts
Normal file
5
config/jest/setupTests.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
import '@testing-library/jest-dom';
|
||||
import 'jest-canvas-mock';
|
||||
import ResizeObserver from 'resize-observer-polyfill';
|
||||
|
||||
(global as any).ResizeObserver = ResizeObserver;
|
|
@ -15,8 +15,9 @@ module.exports = {
|
|||
lines: 85,
|
||||
},
|
||||
},
|
||||
setupFiles: [ '<rootDir>/config/jest/setupEnzyme.js' ],
|
||||
testMatch: [ '<rootDir>/test/**/*.test.{ts,tsx}' ],
|
||||
setupFiles: ['<rootDir>/config/jest/setupEnzyme.js'],
|
||||
setupFilesAfterEnv: ['<rootDir>/config/jest/setupTests.ts'],
|
||||
testMatch: ['<rootDir>/test/**/*.test.{ts,tsx}'],
|
||||
testEnvironment: 'jsdom',
|
||||
testURL: 'http://localhost',
|
||||
transform: {
|
||||
|
@ -33,5 +34,5 @@ module.exports = {
|
|||
// Reactstrap module resolution does not work in jest for some reason. Manually mapping it solves the problem
|
||||
'reactstrap': '<rootDir>/node_modules/reactstrap/dist/reactstrap.umd.js',
|
||||
},
|
||||
moduleFileExtensions: [ 'js', 'ts', 'tsx', 'json' ],
|
||||
moduleFileExtensions: ['js', 'ts', 'tsx', 'json'],
|
||||
};
|
||||
|
|
614
package-lock.json
generated
614
package-lock.json
generated
|
@ -26,12 +26,12 @@
|
|||
"leaflet": "^1.7.1",
|
||||
"qs": "^6.9.6",
|
||||
"ramda": "^0.27.2",
|
||||
"react": "^18.0.0",
|
||||
"react": "^18.1.0",
|
||||
"react-chartjs-2": "^3.3.0",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-datepicker": "^4.7.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-external-link": "^1.2.2",
|
||||
"react-leaflet": "^4.0.0",
|
||||
"react-redux": "^8.0.0",
|
||||
|
@ -55,6 +55,8 @@
|
|||
"@stryker-mutator/core": "^5.6.1",
|
||||
"@stryker-mutator/jest-runner": "^5.6.1",
|
||||
"@stryker-mutator/typescript-checker": "^5.6.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/enzyme": "^3.10.11",
|
||||
"@types/jest": "^27.4.1",
|
||||
|
@ -62,11 +64,11 @@
|
|||
"@types/leaflet": "^1.7.9",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ramda": "0.27.38",
|
||||
"@types/react": "^18.0.6",
|
||||
"@types/react": "^18.0.8",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/react-datepicker": "^4.3.4",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"@types/react-dom": "^18.0.3",
|
||||
"@types/react-tag-autocomplete": "^6.1.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
|
||||
|
@ -78,7 +80,9 @@
|
|||
"eslint": "^8.12.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-canvas-mock": "^2.4.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.49.9",
|
||||
"serve": "^13.0.2",
|
||||
"stryker-cli": "^1.0.2",
|
||||
|
@ -4486,6 +4490,208 @@
|
|||
"node": ">=8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.13.0.tgz",
|
||||
"integrity": "sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^4.2.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.4.4",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/aria-query": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/dom/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom": {
|
||||
"version": "5.16.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz",
|
||||
"integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^3.0.0",
|
||||
"css": "^3.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8",
|
||||
"npm": ">=6",
|
||||
"yarn": ">=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/aria-query": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/jest-dom/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.1.1.tgz",
|
||||
"integrity": "sha512-8mirlAa0OKaUvnqnZF6MdAh2tReYA2KtWVw1PKvaF5EcCZqgK5pl8iF+3uW90JdG5Ua2c2c2E2wtLdaug3dsVg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^8.5.0",
|
||||
"@types/react-dom": "^18.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0",
|
||||
"react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
|
@ -4512,6 +4718,12 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz",
|
||||
|
@ -4835,9 +5047,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.6.tgz",
|
||||
"integrity": "sha512-bPqwzJRzKtfI0mVYr5R+1o9BOE8UEXefwc1LwcBtfnaAn6OoqMhLa/91VA8aeWfDPJt1kHvYKI8RHcQybZLHHA==",
|
||||
"version": "18.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.8.tgz",
|
||||
"integrity": "sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
@ -4904,9 +5116,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.2.tgz",
|
||||
"integrity": "sha512-UxeS+Wtj5bvLRREz9tIgsK4ntCuLDo0EcAcACgw3E+9wE8ePDr9uQpq53MfcyxyIS55xJ+0B6mDS8c4qkkHLBg==",
|
||||
"version": "18.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz",
|
||||
"integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==",
|
||||
"devOptional": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
|
@ -5000,6 +5212,15 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@types/testing-library__jest-dom": {
|
||||
"version": "5.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz",
|
||||
"integrity": "sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
|
@ -8047,6 +8268,17 @@
|
|||
"node": ">=4.8"
|
||||
}
|
||||
},
|
||||
"node_modules/css": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
|
||||
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"inherits": "^2.0.4",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-blank-pseudo": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz",
|
||||
|
@ -8150,6 +8382,29 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/css/node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/css/node_modules/source-map-resolve": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
|
||||
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
|
||||
"deprecated": "See https://github.com/lydell/source-map-resolve#deprecated",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cssdb": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.5.0.tgz",
|
||||
|
@ -8168,6 +8423,12 @@
|
|||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/cssfontparser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
|
||||
"integrity": "sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/cssnano-utils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
|
||||
|
@ -8848,6 +9109,12 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-accessibility-api": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz",
|
||||
"integrity": "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/dom-converter": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||
|
@ -13314,6 +13581,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/jest-canvas-mock": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz",
|
||||
"integrity": "sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cssfontparser": "^1.2.1",
|
||||
"moo-color": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/jest-changed-files": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
|
||||
|
@ -16160,6 +16437,15 @@
|
|||
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
|
@ -16611,6 +16897,15 @@
|
|||
"integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/moo-color": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
|
||||
"integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -18776,9 +19071,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz",
|
||||
"integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==",
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz",
|
||||
"integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
|
@ -19390,21 +19685,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.0.0.tgz",
|
||||
"integrity": "sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==",
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz",
|
||||
"integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.21.0"
|
||||
"scheduler": "^0.22.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.0.0"
|
||||
"react": "^18.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom/node_modules/scheduler": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
||||
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz",
|
||||
"integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
|
@ -21734,6 +22029,12 @@
|
|||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
|
||||
|
@ -30117,6 +30418,152 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/dom": {
|
||||
"version": "8.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.13.0.tgz",
|
||||
"integrity": "sha512-9VHgfIatKNXQNaZTtLnalIy0jNZzY35a4S3oi08YAt9Hv1VsfZ/DfA45lM8D/UhtHBGJ4/lGwp0PZkVndRkoOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@types/aria-query": "^4.2.0",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^4.1.0",
|
||||
"dom-accessibility-api": "^0.5.9",
|
||||
"lz-string": "^1.4.4",
|
||||
"pretty-format": "^27.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/jest-dom": {
|
||||
"version": "5.16.4",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.16.4.tgz",
|
||||
"integrity": "sha512-Gy+IoFutbMQcky0k+bqqumXZ1cTGswLsFqmNLzNdSKkU9KGV2u9oXhukCbbJ9/LRPKiqwxEE8VpV/+YZlfkPUA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"@types/testing-library__jest-dom": "^5.9.1",
|
||||
"aria-query": "^5.0.0",
|
||||
"chalk": "^3.0.0",
|
||||
"css": "^3.0.0",
|
||||
"css.escape": "^1.5.1",
|
||||
"dom-accessibility-api": "^0.5.6",
|
||||
"lodash": "^4.17.15",
|
||||
"redent": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"aria-query": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.0.0.tgz",
|
||||
"integrity": "sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@testing-library/react": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.1.1.tgz",
|
||||
"integrity": "sha512-8mirlAa0OKaUvnqnZF6MdAh2tReYA2KtWVw1PKvaF5EcCZqgK5pl8iF+3uW90JdG5Ua2c2c2E2wtLdaug3dsVg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@testing-library/dom": "^8.5.0",
|
||||
"@types/react-dom": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz",
|
||||
|
@ -30137,6 +30584,12 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@types/aria-query": {
|
||||
"version": "4.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-4.2.2.tgz",
|
||||
"integrity": "sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/babel__core": {
|
||||
"version": "7.1.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.16.tgz",
|
||||
|
@ -30459,9 +30912,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@types/react": {
|
||||
"version": "18.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.6.tgz",
|
||||
"integrity": "sha512-bPqwzJRzKtfI0mVYr5R+1o9BOE8UEXefwc1LwcBtfnaAn6OoqMhLa/91VA8aeWfDPJt1kHvYKI8RHcQybZLHHA==",
|
||||
"version": "18.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.8.tgz",
|
||||
"integrity": "sha512-+j2hk9BzCOrrOSJASi5XiOyBbERk9jG5O73Ya4M0env5Ixi6vUNli4qy994AINcEF+1IEHISYFfIT4zwr++LKw==",
|
||||
"requires": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
|
@ -30523,9 +30976,9 @@
|
|||
}
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"version": "18.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.2.tgz",
|
||||
"integrity": "sha512-UxeS+Wtj5bvLRREz9tIgsK4ntCuLDo0EcAcACgw3E+9wE8ePDr9uQpq53MfcyxyIS55xJ+0B6mDS8c4qkkHLBg==",
|
||||
"version": "18.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.3.tgz",
|
||||
"integrity": "sha512-1RRW9kst+67gveJRYPxGmVy8eVJ05O43hg77G2j5m76/RFJtMbcfAs2viQ2UNsvvDg8F7OfQZx8qQcl6ymygaQ==",
|
||||
"devOptional": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
|
@ -30619,6 +31072,15 @@
|
|||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"@types/testing-library__jest-dom": {
|
||||
"version": "5.14.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.14.3.tgz",
|
||||
"integrity": "sha512-oKZe+Mf4ioWlMuzVBaXQ9WDnEm1+umLx0InILg+yvZVBBDmzV5KfZyLrCvadtWcx8+916jLmHafcmqqffl+iIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/jest": "*"
|
||||
}
|
||||
},
|
||||
"@types/trusted-types": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.2.tgz",
|
||||
|
@ -32959,6 +33421,35 @@
|
|||
"which": "^1.2.9"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz",
|
||||
"integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"inherits": "^2.0.4",
|
||||
"source-map": "^0.6.1",
|
||||
"source-map-resolve": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"dev": true
|
||||
},
|
||||
"source-map-resolve": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz",
|
||||
"integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"atob": "^2.1.2",
|
||||
"decode-uri-component": "^0.2.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"css-blank-pseudo": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-3.0.3.tgz",
|
||||
|
@ -33026,6 +33517,12 @@
|
|||
"integrity": "sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg==",
|
||||
"dev": true
|
||||
},
|
||||
"css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha1-QuJ9T6BK4y+TGktNQZH6nN3ul8s=",
|
||||
"dev": true
|
||||
},
|
||||
"cssdb": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/cssdb/-/cssdb-6.5.0.tgz",
|
||||
|
@ -33038,6 +33535,12 @@
|
|||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true
|
||||
},
|
||||
"cssfontparser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
|
||||
"integrity": "sha1-9AIvyPlwDGgCnVQghK+69CWj8+M=",
|
||||
"dev": true
|
||||
},
|
||||
"cssnano-utils": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-3.1.0.tgz",
|
||||
|
@ -33561,6 +34064,12 @@
|
|||
"esutils": "^2.0.2"
|
||||
}
|
||||
},
|
||||
"dom-accessibility-api": {
|
||||
"version": "0.5.13",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.13.tgz",
|
||||
"integrity": "sha512-R305kwb5CcMDIpSHUnLyIAp7SrSPBx6F0VfQFB3M75xVMHhXJJIdePYgbPPh1o57vCHNu5QztokWUPsLjWzFqw==",
|
||||
"dev": true
|
||||
},
|
||||
"dom-converter": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz",
|
||||
|
@ -36981,6 +37490,16 @@
|
|||
"jest-cli": "^27.5.1"
|
||||
}
|
||||
},
|
||||
"jest-canvas-mock": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.4.0.tgz",
|
||||
"integrity": "sha512-mmMpZzpmLzn5vepIaHk5HoH3Ka4WykbSoLuG/EKoJd0x0ID/t+INo1l8ByfcUJuDM+RIsL4QDg/gDnBbrj2/IQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cssfontparser": "^1.2.1",
|
||||
"moo-color": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"jest-changed-files": {
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-27.5.1.tgz",
|
||||
|
@ -39077,6 +39596,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"lz-string": {
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
|
||||
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
|
||||
"dev": true
|
||||
},
|
||||
"magic-string": {
|
||||
"version": "0.25.9",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
|
||||
|
@ -39425,6 +39950,15 @@
|
|||
"integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==",
|
||||
"dev": true
|
||||
},
|
||||
"moo-color": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
|
||||
"integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "^1.1.4"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
|
@ -41053,9 +41587,9 @@
|
|||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.0.0.tgz",
|
||||
"integrity": "sha512-x+VL6wbT4JRVPm7EGxXhZ8w8LTROaxPXOqhlGyVSrv0sB1jkyFGgXxJ8LVoPRLvPR6/CIZGFmfzqUa2NYeMr2A==",
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.1.0.tgz",
|
||||
"integrity": "sha512-4oL8ivCz5ZEPyclFQXaNksK3adutVS8l2xzZU0cqEFrE9Sb7fC0EFK5uEk74wIreL1DERyjvsU915j1pcT2uEQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
|
@ -41475,18 +42009,18 @@
|
|||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "18.0.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.0.0.tgz",
|
||||
"integrity": "sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw==",
|
||||
"version": "18.1.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.1.0.tgz",
|
||||
"integrity": "sha512-fU1Txz7Budmvamp7bshe4Zi32d0ll7ect+ccxNu9FlObT605GOEB8BfO4tmRJ39R5Zj831VCpvQ05QPBW5yb+w==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.21.0"
|
||||
"scheduler": "^0.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"scheduler": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.21.0.tgz",
|
||||
"integrity": "sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ==",
|
||||
"version": "0.22.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.22.0.tgz",
|
||||
"integrity": "sha512-6QAm1BgQI88NPYymgGQLCZgvep4FyePDWFpXVK+zNSUgHwlqpJy8VEh8Et0KxTACS4VWwMousBElAZOH9nkkoQ==",
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
|
@ -43068,6 +43602,12 @@
|
|||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=",
|
||||
"dev": true
|
||||
},
|
||||
"resize-observer-polyfill": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"dev": true
|
||||
},
|
||||
"resolve": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.0.tgz",
|
||||
|
|
12
package.json
12
package.json
|
@ -42,12 +42,12 @@
|
|||
"leaflet": "^1.7.1",
|
||||
"qs": "^6.9.6",
|
||||
"ramda": "^0.27.2",
|
||||
"react": "^18.0.0",
|
||||
"react": "^18.1.0",
|
||||
"react-chartjs-2": "^3.3.0",
|
||||
"react-colorful": "^5.5.1",
|
||||
"react-copy-to-clipboard": "^5.0.4",
|
||||
"react-datepicker": "^4.7.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-dom": "^18.1.0",
|
||||
"react-external-link": "^1.2.2",
|
||||
"react-leaflet": "^4.0.0",
|
||||
"react-redux": "^8.0.0",
|
||||
|
@ -71,6 +71,8 @@
|
|||
"@stryker-mutator/core": "^5.6.1",
|
||||
"@stryker-mutator/jest-runner": "^5.6.1",
|
||||
"@stryker-mutator/typescript-checker": "^5.6.1",
|
||||
"@testing-library/jest-dom": "^5.16.4",
|
||||
"@testing-library/react": "^13.1.1",
|
||||
"@types/classnames": "^2.3.1",
|
||||
"@types/enzyme": "^3.10.11",
|
||||
"@types/jest": "^27.4.1",
|
||||
|
@ -78,11 +80,11 @@
|
|||
"@types/leaflet": "^1.7.9",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/ramda": "0.27.38",
|
||||
"@types/react": "^18.0.6",
|
||||
"@types/react": "^18.0.8",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-copy-to-clipboard": "^5.0.2",
|
||||
"@types/react-datepicker": "^4.3.4",
|
||||
"@types/react-dom": "^18.0.2",
|
||||
"@types/react-dom": "^18.0.3",
|
||||
"@types/react-tag-autocomplete": "^6.1.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.6.5",
|
||||
|
@ -94,7 +96,9 @@
|
|||
"eslint": "^8.12.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-canvas-mock": "^2.4.0",
|
||||
"react-scripts": "^5.0.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"sass": "^1.49.9",
|
||||
"serve": "^13.0.2",
|
||||
"stryker-cli": "^1.0.2",
|
||||
|
|
|
@ -56,6 +56,10 @@ export default class ShlinkApiClient {
|
|||
this.performRequest<{ visits: ShlinkVisits }>(`/tags/${tag}/visits`, 'GET', query)
|
||||
.then(({ data }) => data.visits);
|
||||
|
||||
public readonly getDomainVisits = async (domain: string, query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
||||
this.performRequest<{ visits: ShlinkVisits }>(`/domains/${domain}/visits`, 'GET', query)
|
||||
.then(({ data }) => data.visits);
|
||||
|
||||
public readonly getOrphanVisits = async (query?: Omit<ShlinkVisitsParams, 'domain'>): Promise<ShlinkVisits> =>
|
||||
this.performRequest<{ visits: ShlinkVisits }>('/visits/orphan', 'GET', query)
|
||||
.then(({ data }) => data.visits);
|
||||
|
|
|
@ -5,7 +5,12 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
|||
import classNames from 'classnames';
|
||||
import { withSelectedServer } from '../servers/helpers/withSelectedServer';
|
||||
import { useSwipeable, useToggle } from '../utils/helpers/hooks';
|
||||
import { supportsDomainRedirects, supportsNonOrphanVisits, supportsOrphanVisits } from '../utils/helpers/features';
|
||||
import {
|
||||
supportsDomainRedirects,
|
||||
supportsDomainVisits,
|
||||
supportsNonOrphanVisits,
|
||||
supportsOrphanVisits,
|
||||
} from '../utils/helpers/features';
|
||||
import { isReachableServer } from '../servers/data';
|
||||
import NotFound from './NotFound';
|
||||
import { AsideMenuProps } from './AsideMenu';
|
||||
|
@ -23,6 +28,7 @@ const MenuLayout = (
|
|||
CreateShortUrl: FC,
|
||||
ShortUrlVisits: FC,
|
||||
TagVisits: FC,
|
||||
DomainVisits: FC,
|
||||
OrphanVisits: FC,
|
||||
NonOrphanVisits: FC,
|
||||
ServerError: FC,
|
||||
|
@ -48,6 +54,7 @@ const MenuLayout = (
|
|||
const addOrphanVisitsRoute = supportsOrphanVisits(selectedServer);
|
||||
const addNonOrphanVisitsRoute = supportsNonOrphanVisits(selectedServer);
|
||||
const addManageDomainsRoute = supportsDomainRedirects(selectedServer);
|
||||
const addDomainVisitsRoute = supportsDomainVisits(selectedServer);
|
||||
const burgerClasses = classNames('menu-layout__burger-icon', { 'menu-layout__burger-icon--active': sidebarVisible });
|
||||
const swipeableProps = useSwipeable(showSidebar, hideSidebar);
|
||||
|
||||
|
@ -68,6 +75,7 @@ const MenuLayout = (
|
|||
<Route path="/short-code/:shortCode/visits/*" element={<ShortUrlVisits />} />
|
||||
<Route path="/short-code/:shortCode/edit" element={<EditShortUrl />} />
|
||||
<Route path="/tag/:tag/visits/*" element={<TagVisits />} />
|
||||
{addDomainVisitsRoute && <Route path="/domain/:domain/visits/*" element={<DomainVisits />} />}
|
||||
{addOrphanVisitsRoute && <Route path="/orphan-visits/*" element={<OrphanVisits />} />}
|
||||
{addNonOrphanVisitsRoute && <Route path="/non-orphan-visits/*" element={<NonOrphanVisits />} />}
|
||||
<Route path="/manage-tags" element={<TagsList />} />
|
||||
|
|
|
@ -40,6 +40,7 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
'CreateShortUrl',
|
||||
'ShortUrlVisits',
|
||||
'TagVisits',
|
||||
'DomainVisits',
|
||||
'OrphanVisits',
|
||||
'NonOrphanVisits',
|
||||
'ServerError',
|
||||
|
|
|
@ -15,6 +15,7 @@ import { DomainsList } from '../domains/reducers/domainsList';
|
|||
import { VisitsOverview } from '../visits/reducers/visitsOverview';
|
||||
import { VisitsInfo } from '../visits/types';
|
||||
import { Sidebar } from '../common/reducers/sidebar';
|
||||
import { DomainVisits } from '../visits/reducers/domainVisits';
|
||||
|
||||
export interface ShlinkState {
|
||||
servers: ServersMap;
|
||||
|
@ -25,6 +26,7 @@ export interface ShlinkState {
|
|||
shortUrlEdition: ShortUrlEdition;
|
||||
shortUrlVisits: ShortUrlVisits;
|
||||
tagVisits: TagVisits;
|
||||
domainVisits: DomainVisits;
|
||||
orphanVisits: VisitsInfo;
|
||||
nonOrphanVisits: VisitsInfo;
|
||||
shortUrlDetail: ShortUrlDetail;
|
||||
|
|
|
@ -1,19 +1,13 @@
|
|||
import { FC, useEffect } from 'react';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import { UncontrolledTooltip } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {
|
||||
faBan as forbiddenIcon,
|
||||
faDotCircle as defaultDomainIcon,
|
||||
faEdit as editIcon,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import { faDotCircle as defaultDomainIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ShlinkDomainRedirects } from '../api/types';
|
||||
import { useToggle } from '../utils/helpers/hooks';
|
||||
import { OptionalString } from '../utils/utils';
|
||||
import { SelectedServer } from '../servers/data';
|
||||
import { supportsDefaultDomainRedirectsEdition } from '../utils/helpers/features';
|
||||
import { EditDomainRedirectsModal } from './helpers/EditDomainRedirectsModal';
|
||||
import { Domain } from './data';
|
||||
import { DomainStatusIcon } from './helpers/DomainStatusIcon';
|
||||
import { DomainDropdown } from './helpers/DomainDropdown';
|
||||
|
||||
interface DomainRowProps {
|
||||
domain: Domain;
|
||||
|
@ -39,9 +33,7 @@ const DefaultDomain: FC = () => (
|
|||
export const DomainRow: FC<DomainRowProps> = (
|
||||
{ domain, editDomainRedirects, checkDomainHealth, defaultRedirects, selectedServer },
|
||||
) => {
|
||||
const [isOpen, toggle] = useToggle();
|
||||
const { domain: authority, isDefault, redirects, status } = domain;
|
||||
const canEditDomain = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
||||
|
||||
useEffect(() => {
|
||||
checkDomainHealth(domain.domain);
|
||||
|
@ -64,25 +56,8 @@ export const DomainRow: FC<DomainRowProps> = (
|
|||
<DomainStatusIcon status={status} />
|
||||
</td>
|
||||
<td className="responsive-table__cell text-end">
|
||||
<span id={!canEditDomain ? 'defaultDomainBtn' : undefined}>
|
||||
<Button outline size="sm" disabled={!canEditDomain} onClick={!canEditDomain ? undefined : toggle}>
|
||||
<FontAwesomeIcon fixedWidth icon={!canEditDomain ? forbiddenIcon : editIcon} />
|
||||
</Button>
|
||||
</span>
|
||||
{!canEditDomain && (
|
||||
<UncontrolledTooltip target="defaultDomainBtn" placement="left">
|
||||
Redirects for default domain cannot be edited here.
|
||||
<br />
|
||||
Use config options or env vars directly on the server.
|
||||
</UncontrolledTooltip>
|
||||
)}
|
||||
<DomainDropdown domain={domain} editDomainRedirects={editDomainRedirects} selectedServer={selectedServer} />
|
||||
</td>
|
||||
<EditDomainRedirectsModal
|
||||
domain={domain}
|
||||
isOpen={isOpen}
|
||||
toggle={toggle}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
/>
|
||||
</tr>
|
||||
);
|
||||
};
|
||||
|
|
51
src/domains/helpers/DomainDropdown.tsx
Normal file
51
src/domains/helpers/DomainDropdown.tsx
Normal file
|
@ -0,0 +1,51 @@
|
|||
import { FC } from 'react';
|
||||
import { DropdownItem } from 'reactstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { faChartPie as pieChartIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useToggle } from '../../utils/helpers/hooks';
|
||||
import { DropdownBtnMenu } from '../../utils/DropdownBtnMenu';
|
||||
import { EditDomainRedirectsModal } from './EditDomainRedirectsModal';
|
||||
import { Domain } from '../data';
|
||||
import { ShlinkDomainRedirects } from '../../api/types';
|
||||
import { supportsDefaultDomainRedirectsEdition, supportsDomainVisits } from '../../utils/helpers/features';
|
||||
import { getServerId, SelectedServer } from '../../servers/data';
|
||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||
|
||||
interface DomainDropdownProps {
|
||||
domain: Domain;
|
||||
editDomainRedirects: (domain: string, redirects: Partial<ShlinkDomainRedirects>) => Promise<void>;
|
||||
selectedServer: SelectedServer;
|
||||
}
|
||||
|
||||
export const DomainDropdown: FC<DomainDropdownProps> = ({ domain, editDomainRedirects, selectedServer }) => {
|
||||
const [isOpen, toggle] = useToggle();
|
||||
const [isModalOpen, toggleModal] = useToggle();
|
||||
const { isDefault } = domain;
|
||||
const canBeEdited = !isDefault || supportsDefaultDomainRedirectsEdition(selectedServer);
|
||||
const withVisits = supportsDomainVisits(selectedServer);
|
||||
const serverId = getServerId(selectedServer);
|
||||
|
||||
return (
|
||||
<DropdownBtnMenu isOpen={isOpen} toggle={toggle}>
|
||||
{withVisits && (
|
||||
<DropdownItem
|
||||
tag={Link}
|
||||
to={`/server/${serverId}/domain/${domain.domain}${domain.isDefault ? `_${DEFAULT_DOMAIN}` : ''}/visits`}
|
||||
>
|
||||
<FontAwesomeIcon icon={pieChartIcon} fixedWidth /> Visit stats
|
||||
</DropdownItem>
|
||||
)}
|
||||
<DropdownItem disabled={!canBeEdited} onClick={!canBeEdited ? undefined : toggleModal}>
|
||||
<FontAwesomeIcon fixedWidth icon={editIcon} /> Edit redirects
|
||||
</DropdownItem>
|
||||
|
||||
<EditDomainRedirectsModal
|
||||
domain={domain}
|
||||
isOpen={isModalOpen}
|
||||
toggle={toggleModal}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
/>
|
||||
</DropdownBtnMenu>
|
||||
);
|
||||
};
|
|
@ -7,6 +7,7 @@ import shortUrlDeletionReducer from '../short-urls/reducers/shortUrlDeletion';
|
|||
import shortUrlEditionReducer from '../short-urls/reducers/shortUrlEdition';
|
||||
import shortUrlVisitsReducer from '../visits/reducers/shortUrlVisits';
|
||||
import tagVisitsReducer from '../visits/reducers/tagVisits';
|
||||
import domainVisitsReducer from '../visits/reducers/domainVisits';
|
||||
import orphanVisitsReducer from '../visits/reducers/orphanVisits';
|
||||
import nonOrphanVisitsReducer from '../visits/reducers/nonOrphanVisits';
|
||||
import shortUrlDetailReducer from '../short-urls/reducers/shortUrlDetail';
|
||||
|
@ -30,6 +31,7 @@ export default combineReducers<ShlinkState>({
|
|||
shortUrlEdition: shortUrlEditionReducer,
|
||||
shortUrlVisits: shortUrlVisitsReducer,
|
||||
tagVisits: tagVisitsReducer,
|
||||
domainVisits: domainVisitsReducer,
|
||||
orphanVisits: orphanVisitsReducer,
|
||||
nonOrphanVisits: nonOrphanVisitsReducer,
|
||||
shortUrlDetail: shortUrlDetailReducer,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { isNil } from 'ramda';
|
||||
import { ShortUrl } from '../data';
|
||||
import { OptionalString } from '../../utils/utils';
|
||||
import { DEFAULT_DOMAIN } from '../../visits/reducers/domainVisits';
|
||||
|
||||
export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: OptionalString): boolean => {
|
||||
if (isNil(domain)) {
|
||||
|
@ -9,3 +10,11 @@ export const shortUrlMatches = (shortUrl: ShortUrl, shortCode: string, domain: O
|
|||
|
||||
return shortUrl.shortCode === shortCode && shortUrl.domain === domain;
|
||||
};
|
||||
|
||||
export const domainMatches = (shortUrl: ShortUrl, domain: string): boolean => {
|
||||
if (!shortUrl.domain && domain === DEFAULT_DOMAIN) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return shortUrl.domain === domain;
|
||||
};
|
||||
|
|
|
@ -17,3 +17,4 @@ export const supportsForwardQuery = serverMatchesVersions({ minVersion: '2.9.0'
|
|||
export const supportsDefaultDomainRedirectsEdition = serverMatchesVersions({ minVersion: '2.10.0' });
|
||||
export const supportsNonOrphanVisits = serverMatchesVersions({ minVersion: '3.0.0' });
|
||||
export const supportsAllTagsFiltering = supportsNonOrphanVisits;
|
||||
export const supportsDomainVisits = serverMatchesVersions({ minVersion: '3.1.0' });
|
||||
|
|
46
src/visits/DomainVisits.tsx
Normal file
46
src/visits/DomainVisits.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { useParams } from 'react-router-dom';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { ShlinkVisitsParams } from '../api/types';
|
||||
import { DomainVisits as DomainVisitsState } from './reducers/domainVisits';
|
||||
import { ReportExporter } from '../common/services/ReportExporter';
|
||||
import { boundToMercureHub } from '../mercure/helpers/boundToMercureHub';
|
||||
import { Topics } from '../mercure/helpers/Topics';
|
||||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import { toApiParams } from './types/helpers';
|
||||
import { NormalizedVisit } from './types';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import VisitsHeader from './VisitsHeader';
|
||||
|
||||
export interface DomainVisitsProps extends CommonVisitsProps {
|
||||
getDomainVisits: (domain: string, query?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
||||
domainVisits: DomainVisitsState;
|
||||
cancelGetDomainVisits: () => void;
|
||||
}
|
||||
|
||||
export const DomainVisits = ({ exportVisits }: ReportExporter) => boundToMercureHub(({
|
||||
getDomainVisits,
|
||||
domainVisits,
|
||||
cancelGetDomainVisits,
|
||||
settings,
|
||||
selectedServer,
|
||||
}: DomainVisitsProps) => {
|
||||
const goBack = useGoBack();
|
||||
const { domain = '' } = useParams();
|
||||
const [authority, domainId = authority] = domain.split('_');
|
||||
const loadVisits = (params: ShlinkVisitsParams, doIntervalFallback?: boolean) =>
|
||||
getDomainVisits(domainId, toApiParams(params), doIntervalFallback);
|
||||
const exportCsv = (visits: NormalizedVisit[]) => exportVisits(`domain_${authority}_visits.csv`, visits);
|
||||
|
||||
return (
|
||||
<VisitsStats
|
||||
getVisits={loadVisits}
|
||||
cancelGetVisits={cancelGetDomainVisits}
|
||||
visitsInfo={domainVisits}
|
||||
settings={settings}
|
||||
exportCsv={exportCsv}
|
||||
selectedServer={selectedServer}
|
||||
>
|
||||
<VisitsHeader goBack={goBack} visits={domainVisits.visits} title={`"${authority}" visits`} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [Topics.visits]);
|
|
@ -7,7 +7,7 @@ import VisitsStats from './VisitsStats';
|
|||
import { NormalizedVisit, VisitsInfo, VisitsParams } from './types';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { toApiParams } from './types/helpers';
|
||||
import { NonOrphanVisitsHeader } from './NonOrphanVisitsHeader';
|
||||
import VisitsHeader from './VisitsHeader';
|
||||
|
||||
export interface NonOrphanVisitsProps extends CommonVisitsProps {
|
||||
getNonOrphanVisits: (params?: ShlinkVisitsParams, doIntervalFallback?: boolean) => void;
|
||||
|
@ -36,7 +36,7 @@ export const NonOrphanVisits = ({ exportVisits }: ReportExporter) => boundToMerc
|
|||
exportCsv={exportCsv}
|
||||
selectedServer={selectedServer}
|
||||
>
|
||||
<NonOrphanVisitsHeader nonOrphanVisits={nonOrphanVisits} goBack={goBack} />
|
||||
<VisitsHeader title="Non-orphan visits" goBack={goBack} visits={nonOrphanVisits.visits} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [Topics.visits]);
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import VisitsHeader from './VisitsHeader';
|
||||
import { VisitsInfo } from './types';
|
||||
import './ShortUrlVisitsHeader.scss';
|
||||
|
||||
interface NonOrphanVisitsHeaderProps {
|
||||
nonOrphanVisits: VisitsInfo;
|
||||
goBack: () => void;
|
||||
}
|
||||
|
||||
export const NonOrphanVisitsHeader = ({ nonOrphanVisits, goBack }: NonOrphanVisitsHeaderProps) => {
|
||||
const { visits } = nonOrphanVisits;
|
||||
|
||||
return <VisitsHeader title="Non-orphan visits" goBack={goBack} visits={visits} />;
|
||||
};
|
|
@ -4,10 +4,10 @@ import { Topics } from '../mercure/helpers/Topics';
|
|||
import { useGoBack } from '../utils/helpers/hooks';
|
||||
import { ReportExporter } from '../common/services/ReportExporter';
|
||||
import VisitsStats from './VisitsStats';
|
||||
import { OrphanVisitsHeader } from './OrphanVisitsHeader';
|
||||
import { NormalizedVisit, OrphanVisitType, VisitsInfo, VisitsParams } from './types';
|
||||
import { CommonVisitsProps } from './types/CommonVisitsProps';
|
||||
import { toApiParams } from './types/helpers';
|
||||
import VisitsHeader from './VisitsHeader';
|
||||
|
||||
export interface OrphanVisitsProps extends CommonVisitsProps {
|
||||
getOrphanVisits: (
|
||||
|
@ -41,7 +41,7 @@ export const OrphanVisits = ({ exportVisits }: ReportExporter) => boundToMercure
|
|||
selectedServer={selectedServer}
|
||||
isOrphanVisits
|
||||
>
|
||||
<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />
|
||||
<VisitsHeader title="Orphan visits" goBack={goBack} visits={orphanVisits.visits} />
|
||||
</VisitsStats>
|
||||
);
|
||||
}, () => [Topics.orphanVisits]);
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
import VisitsHeader from './VisitsHeader';
|
||||
import { VisitsInfo } from './types';
|
||||
import './ShortUrlVisitsHeader.scss';
|
||||
|
||||
interface OrphanVisitsHeaderProps {
|
||||
orphanVisits: VisitsInfo;
|
||||
goBack: () => void;
|
||||
}
|
||||
|
||||
export const OrphanVisitsHeader = ({ orphanVisits, goBack }: OrphanVisitsHeaderProps) => {
|
||||
const { visits } = orphanVisits;
|
||||
|
||||
return <VisitsHeader title="Orphan visits" goBack={goBack} visits={visits} />;
|
||||
};
|
96
src/visits/reducers/domainVisits.ts
Normal file
96
src/visits/reducers/domainVisits.ts
Normal file
|
@ -0,0 +1,96 @@
|
|||
import { Action, Dispatch } from 'redux';
|
||||
import { Visit, VisitsFallbackIntervalAction, VisitsInfo, VisitsLoadProgressChangedAction } from '../types';
|
||||
import { buildActionCreator, buildReducer } from '../../utils/helpers/redux';
|
||||
import { ShlinkApiClientBuilder } from '../../api/services/ShlinkApiClientBuilder';
|
||||
import { GetState } from '../../container/types';
|
||||
import { ShlinkVisitsParams } from '../../api/types';
|
||||
import { ApiErrorAction } from '../../api/types/actions';
|
||||
import { isBetween } from '../../utils/helpers/date';
|
||||
import { getVisitsWithLoader, lastVisitLoaderForLoader } from './common';
|
||||
import { CREATE_VISITS, CreateVisitsAction } from './visitCreation';
|
||||
import { domainMatches } from '../../short-urls/helpers';
|
||||
|
||||
export const GET_DOMAIN_VISITS_START = 'shlink/domainVisits/GET_DOMAIN_VISITS_START';
|
||||
export const GET_DOMAIN_VISITS_ERROR = 'shlink/domainVisits/GET_DOMAIN_VISITS_ERROR';
|
||||
export const GET_DOMAIN_VISITS = 'shlink/domainVisits/GET_DOMAIN_VISITS';
|
||||
export const GET_DOMAIN_VISITS_LARGE = 'shlink/domainVisits/GET_DOMAIN_VISITS_LARGE';
|
||||
export const GET_DOMAIN_VISITS_CANCEL = 'shlink/domainVisits/GET_DOMAIN_VISITS_CANCEL';
|
||||
export const GET_DOMAIN_VISITS_PROGRESS_CHANGED = 'shlink/domainVisits/GET_DOMAIN_VISITS_PROGRESS_CHANGED';
|
||||
export const GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL = 'shlink/domainVisits/GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL';
|
||||
|
||||
export const DEFAULT_DOMAIN = 'DEFAULT';
|
||||
|
||||
export interface DomainVisits extends VisitsInfo {
|
||||
domain: string;
|
||||
}
|
||||
|
||||
export interface DomainVisitsAction extends Action<string> {
|
||||
visits: Visit[];
|
||||
domain: string;
|
||||
query?: ShlinkVisitsParams;
|
||||
}
|
||||
|
||||
type DomainVisitsCombinedAction = DomainVisitsAction
|
||||
& VisitsLoadProgressChangedAction
|
||||
& VisitsFallbackIntervalAction
|
||||
& CreateVisitsAction
|
||||
& ApiErrorAction;
|
||||
|
||||
const initialState: DomainVisits = {
|
||||
visits: [],
|
||||
domain: '',
|
||||
loading: false,
|
||||
loadingLarge: false,
|
||||
error: false,
|
||||
cancelLoad: false,
|
||||
progress: 0,
|
||||
};
|
||||
|
||||
export default buildReducer<DomainVisits, DomainVisitsCombinedAction>({
|
||||
[GET_DOMAIN_VISITS_START]: () => ({ ...initialState, loading: true }),
|
||||
[GET_DOMAIN_VISITS_ERROR]: (_, { errorData }) => ({ ...initialState, error: true, errorData }),
|
||||
[GET_DOMAIN_VISITS]: (state, { visits, domain, query }) => (
|
||||
{ ...state, visits, domain, query, loading: false, error: false }
|
||||
),
|
||||
[GET_DOMAIN_VISITS_LARGE]: (state) => ({ ...state, loadingLarge: true }),
|
||||
[GET_DOMAIN_VISITS_CANCEL]: (state) => ({ ...state, cancelLoad: true }),
|
||||
[GET_DOMAIN_VISITS_PROGRESS_CHANGED]: (state, { progress }) => ({ ...state, progress }),
|
||||
[GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL]: (state, { fallbackInterval }) => ({ ...state, fallbackInterval }),
|
||||
[CREATE_VISITS]: (state, { createdVisits }) => {
|
||||
const { domain, visits, query = {} } = state;
|
||||
const { startDate, endDate } = query;
|
||||
const newVisits = createdVisits
|
||||
.filter(({ shortUrl, visit }) =>
|
||||
shortUrl && domainMatches(shortUrl, domain) && isBetween(visit.date, startDate, endDate))
|
||||
.map(({ visit }) => visit);
|
||||
|
||||
return { ...state, visits: [...newVisits, ...visits] };
|
||||
},
|
||||
}, initialState);
|
||||
|
||||
export const getDomainVisits = (buildShlinkApiClient: ShlinkApiClientBuilder) => (
|
||||
domain: string,
|
||||
query: ShlinkVisitsParams = {},
|
||||
doIntervalFallback = false,
|
||||
) => async (dispatch: Dispatch, getState: GetState) => {
|
||||
const { getDomainVisits: getVisits } = buildShlinkApiClient(getState);
|
||||
const visitsLoader = async (page: number, itemsPerPage: number) => getVisits(
|
||||
domain,
|
||||
{ ...query, page, itemsPerPage },
|
||||
);
|
||||
const lastVisitLoader = lastVisitLoaderForLoader(doIntervalFallback, async (params) => getVisits(domain, params));
|
||||
const shouldCancel = () => getState().domainVisits.cancelLoad;
|
||||
const extraFinishActionData: Partial<DomainVisitsAction> = { domain, query };
|
||||
const actionMap = {
|
||||
start: GET_DOMAIN_VISITS_START,
|
||||
large: GET_DOMAIN_VISITS_LARGE,
|
||||
finish: GET_DOMAIN_VISITS,
|
||||
error: GET_DOMAIN_VISITS_ERROR,
|
||||
progress: GET_DOMAIN_VISITS_PROGRESS_CHANGED,
|
||||
fallbackToInterval: GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL,
|
||||
};
|
||||
|
||||
return getVisitsWithLoader(visitsLoader, lastVisitLoader, extraFinishActionData, actionMap, dispatch, shouldCancel);
|
||||
};
|
||||
|
||||
export const cancelGetDomainVisits = buildActionCreator(GET_DOMAIN_VISITS_CANCEL);
|
|
@ -7,11 +7,13 @@ import { OrphanVisits } from '../OrphanVisits';
|
|||
import { NonOrphanVisits } from '../NonOrphanVisits';
|
||||
import { cancelGetShortUrlVisits, getShortUrlVisits } from '../reducers/shortUrlVisits';
|
||||
import { cancelGetTagVisits, getTagVisits } from '../reducers/tagVisits';
|
||||
import { cancelGetDomainVisits, getDomainVisits } from '../reducers/domainVisits';
|
||||
import { cancelGetOrphanVisits, getOrphanVisits } from '../reducers/orphanVisits';
|
||||
import { cancelGetNonOrphanVisits, getNonOrphanVisits } from '../reducers/nonOrphanVisits';
|
||||
import { ConnectDecorator } from '../../container/types';
|
||||
import { loadVisitsOverview } from '../reducers/visitsOverview';
|
||||
import * as visitsParser from './VisitsParser';
|
||||
import { DomainVisits } from '../DomainVisits';
|
||||
|
||||
const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
||||
// Components
|
||||
|
@ -29,6 +31,12 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
['getTagVisits', 'cancelGetTagVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('DomainVisits', DomainVisits, 'ReportExporter');
|
||||
bottle.decorator('DomainVisits', connect(
|
||||
['domainVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
||||
['getDomainVisits', 'cancelGetDomainVisits', 'createNewVisits', 'loadMercureInfo'],
|
||||
));
|
||||
|
||||
bottle.serviceFactory('OrphanVisits', OrphanVisits, 'ReportExporter');
|
||||
bottle.decorator('OrphanVisits', connect(
|
||||
['orphanVisits', 'mercureInfo', 'settings', 'selectedServer'],
|
||||
|
@ -51,6 +59,9 @@ const provideServices = (bottle: Bottle, connect: ConnectDecorator) => {
|
|||
bottle.serviceFactory('getTagVisits', getTagVisits, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('cancelGetTagVisits', () => cancelGetTagVisits);
|
||||
|
||||
bottle.serviceFactory('getDomainVisits', getDomainVisits, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('cancelGetDomainVisits', () => cancelGetDomainVisits);
|
||||
|
||||
bottle.serviceFactory('getOrphanVisits', getOrphanVisits, 'buildShlinkApiClient');
|
||||
bottle.serviceFactory('cancelGetOrphanVisits', () => cancelGetOrphanVisits);
|
||||
|
||||
|
|
|
@ -115,6 +115,28 @@ describe('ShlinkApiClient', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('getDomainVisits', () => {
|
||||
it('properly returns domain visits', async () => {
|
||||
const expectedVisits = ['foo', 'bar'];
|
||||
const axiosSpy = createAxiosMock({
|
||||
data: {
|
||||
visits: {
|
||||
data: expectedVisits,
|
||||
},
|
||||
},
|
||||
});
|
||||
const { getDomainVisits } = new ShlinkApiClient(axiosSpy, '', '');
|
||||
|
||||
const actualVisits = await getDomainVisits('foo.com', {});
|
||||
|
||||
expect({ data: expectedVisits }).toEqual(actualVisits);
|
||||
expect(axiosSpy).toHaveBeenCalledWith(expect.objectContaining({
|
||||
url: '/domains/foo.com/visits',
|
||||
method: 'GET',
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getShortUrl', () => {
|
||||
it.each(shortCodesWithDomainCombinations)('properly returns short URL', async (shortCode, domain) => {
|
||||
const expectedShortUrl = { foo: 'bar' };
|
||||
|
|
|
@ -15,7 +15,7 @@ jest.mock('react-router-dom', () => ({
|
|||
describe('<MenuLayout />', () => {
|
||||
const ServerError = jest.fn();
|
||||
const C = jest.fn();
|
||||
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, C, ServerError, C, C, C);
|
||||
const MenuLayout = createMenuLayout(C, C, C, C, C, C, C, C, C, ServerError, C, C, C);
|
||||
let wrapper: ShallowWrapper;
|
||||
const createWrapper = (selectedServer: SelectedServer) => {
|
||||
(useParams as any).mockReturnValue({ serverId: 'abc123' });
|
||||
|
@ -59,6 +59,7 @@ describe('<MenuLayout />', () => {
|
|||
['2.8.0' as SemVer, 11],
|
||||
['2.10.0' as SemVer, 11],
|
||||
['3.0.0' as SemVer, 12],
|
||||
['3.1.0' as SemVer, 13],
|
||||
])('has expected amount of routes based on selected server\'s version', (version, expectedAmountOfRoutes) => {
|
||||
const selectedServer = Mock.of<ReachableServer>({ version });
|
||||
const wrapper = createWrapper(selectedServer).dive();
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import { shallow, ShallowWrapper } from 'enzyme';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { Button, UncontrolledTooltip } from 'reactstrap';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { faBan as forbiddenIcon, faEdit as editIcon } from '@fortawesome/free-solid-svg-icons';
|
||||
import { ShlinkDomainRedirects } from '../../src/api/types';
|
||||
import { DomainRow } from '../../src/domains/DomainRow';
|
||||
import { ReachableServer, SelectedServer } from '../../src/servers/data';
|
||||
import { SelectedServer } from '../../src/servers/data';
|
||||
import { Domain } from '../../src/domains/data';
|
||||
|
||||
describe('<DomainRow />', () => {
|
||||
|
@ -25,65 +22,6 @@ describe('<DomainRow />', () => {
|
|||
|
||||
afterEach(() => wrapper?.unmount());
|
||||
|
||||
it.each([
|
||||
[Mock.of<Domain>({ domain: '', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn'],
|
||||
[Mock.of<Domain>({ domain: '', isDefault: false }), undefined, 0, 0, undefined],
|
||||
[Mock.of<Domain>({ domain: 'foo.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn'],
|
||||
[Mock.of<Domain>({ domain: 'foo.bar.com', isDefault: true }), undefined, 1, 1, 'defaultDomainBtn'],
|
||||
[Mock.of<Domain>({ domain: 'foo.baz', isDefault: false }), undefined, 0, 0, undefined],
|
||||
[
|
||||
Mock.of<Domain>({ domain: 'foo.baz', isDefault: true }),
|
||||
Mock.of<ReachableServer>({ version: '2.10.0' }),
|
||||
1,
|
||||
0,
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
Mock.of<Domain>({ domain: 'foo.baz', isDefault: true }),
|
||||
Mock.of<ReachableServer>({ version: '2.9.0' }),
|
||||
1,
|
||||
1,
|
||||
'defaultDomainBtn',
|
||||
],
|
||||
[
|
||||
Mock.of<Domain>({ domain: 'foo.baz', isDefault: false }),
|
||||
Mock.of<ReachableServer>({ version: '2.9.0' }),
|
||||
0,
|
||||
0,
|
||||
undefined,
|
||||
],
|
||||
[
|
||||
Mock.of<Domain>({ domain: 'foo.baz', isDefault: false }),
|
||||
Mock.of<ReachableServer>({ version: '2.10.0' }),
|
||||
0,
|
||||
0,
|
||||
undefined,
|
||||
],
|
||||
])('shows proper components based on provided domain and selectedServer', (
|
||||
domain,
|
||||
selectedServer,
|
||||
expectedDefaultDomainIcons,
|
||||
expectedDisabledComps,
|
||||
expectedDomainId,
|
||||
) => {
|
||||
const wrapper = createWrapper(domain, selectedServer);
|
||||
const defaultDomainComp = wrapper.find('td').first().find('DefaultDomain');
|
||||
const disabledBtn = wrapper.find(Button).findWhere((btn) => !!btn.prop('disabled'));
|
||||
const tooltip = wrapper.find(UncontrolledTooltip);
|
||||
const button = wrapper.find(Button);
|
||||
const icon = wrapper.find(FontAwesomeIcon);
|
||||
|
||||
expect(defaultDomainComp).toHaveLength(expectedDefaultDomainIcons);
|
||||
expect(disabledBtn).toHaveLength(expectedDisabledComps);
|
||||
expect(button.prop('disabled')).toEqual(expectedDisabledComps > 0);
|
||||
expect(icon.prop('icon')).toEqual(expectedDisabledComps > 0 ? forbiddenIcon : editIcon);
|
||||
expect(tooltip).toHaveLength(expectedDisabledComps);
|
||||
|
||||
if (expectedDisabledComps > 0) {
|
||||
expect(tooltip.prop('target')).toEqual(expectedDomainId);
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined, 3],
|
||||
[Mock.of<ShlinkDomainRedirects>(), 3],
|
||||
|
|
85
test/domains/helpers/DomainDropdown.test.tsx
Normal file
85
test/domains/helpers/DomainDropdown.test.tsx
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { fireEvent, render, screen, waitForElementToBeRemoved } from '@testing-library/react';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { DomainDropdown } from '../../../src/domains/helpers/DomainDropdown';
|
||||
import { Domain } from '../../../src/domains/data';
|
||||
import { ReachableServer, SelectedServer } from '../../../src/servers/data';
|
||||
import { SemVer } from '../../../src/utils/helpers/version';
|
||||
|
||||
describe('<DomainDropdown />', () => {
|
||||
const editDomainRedirects = jest.fn().mockResolvedValue(undefined);
|
||||
const setUp = (domain?: Domain, selectedServer?: SelectedServer) => render(
|
||||
<MemoryRouter>
|
||||
<DomainDropdown
|
||||
domain={domain ?? Mock.all<Domain>()}
|
||||
selectedServer={selectedServer ?? Mock.all<SelectedServer>()}
|
||||
editDomainRedirects={editDomainRedirects}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
);
|
||||
|
||||
afterEach(jest.clearAllMocks);
|
||||
|
||||
it('renders expected menu items', () => {
|
||||
setUp();
|
||||
|
||||
expect(screen.queryByText('Visit stats')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Edit redirects')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, '_DEFAULT'],
|
||||
[false, ''],
|
||||
])('points first link to the proper section', (isDefault, expectedLink) => {
|
||||
setUp(
|
||||
Mock.of<Domain>({ domain: 'foo.com', isDefault }),
|
||||
Mock.of<ReachableServer>({ version: '3.1.0', id: '123' }),
|
||||
);
|
||||
|
||||
expect(screen.getByText('Visit stats')).toHaveAttribute('href', `/server/123/domain/foo.com${expectedLink}/visits`);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[true, '2.9.0' as SemVer, false],
|
||||
[true, '2.10.0' as SemVer, true],
|
||||
[false, '2.9.0' as SemVer, true],
|
||||
])('allows editing certain the domains', (isDefault, serverVersion, canBeEdited) => {
|
||||
setUp(
|
||||
Mock.of<Domain>({ domain: 'foo.com', isDefault }),
|
||||
Mock.of<ReachableServer>({ version: serverVersion, id: '123' }),
|
||||
);
|
||||
|
||||
if (canBeEdited) {
|
||||
expect(screen.getByText('Edit redirects')).not.toHaveAttribute('disabled');
|
||||
} else {
|
||||
expect(screen.getByText('Edit redirects')).toHaveAttribute('disabled');
|
||||
}
|
||||
});
|
||||
|
||||
it.each([
|
||||
['foo.com'],
|
||||
['bar.org'],
|
||||
['baz.net'],
|
||||
])('displays modal when editing redirects', async (domain) => {
|
||||
setUp(Mock.of<Domain>({ domain, isDefault: false }));
|
||||
|
||||
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('form')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByText('Edit redirects'));
|
||||
expect(await screen.findByRole('dialog')).toBeInTheDocument();
|
||||
|
||||
expect(editDomainRedirects).not.toHaveBeenCalled();
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
expect(editDomainRedirects).toHaveBeenCalledWith(domain, expect.anything());
|
||||
|
||||
await waitForElementToBeRemoved(() => screen.queryByRole('dialog'));
|
||||
});
|
||||
|
||||
it('displays dropdown when clicked', async () => {
|
||||
setUp();
|
||||
|
||||
expect(screen.queryByRole('menu')).not.toBeInTheDocument();
|
||||
fireEvent.click(screen.getByRole('button', { expanded: false }));
|
||||
expect(await screen.findByRole('menu')).toBeInTheDocument();
|
||||
});
|
||||
});
|
52
test/visits/DomainVisits.test.tsx
Normal file
52
test/visits/DomainVisits.test.tsx
Normal file
|
@ -0,0 +1,52 @@
|
|||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { DomainVisits as createDomainVisits } from '../../src/visits/DomainVisits';
|
||||
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import { DomainVisits } from '../../src/visits/reducers/domainVisits';
|
||||
import { Settings } from '../../src/settings/reducers/settings';
|
||||
import { SelectedServer } from '../../src/servers/data';
|
||||
import { Visit } from '../../src/visits/types';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useParams: jest.fn().mockReturnValue({ domain: 'foo.com_DEFAULT' }),
|
||||
}));
|
||||
|
||||
describe('<DomainVisits />', () => {
|
||||
const exportVisits = jest.fn();
|
||||
const getDomainVisits = jest.fn();
|
||||
const cancelGetDomainVisits = jest.fn();
|
||||
const domainVisits = Mock.of<DomainVisits>({ visits: [Mock.of<Visit>({ date: formatISO(new Date()) })] });
|
||||
const DomainVisits = createDomainVisits(Mock.of<ReportExporter>({ exportVisits }));
|
||||
|
||||
beforeEach(() => render(
|
||||
<MemoryRouter>
|
||||
<DomainVisits
|
||||
{...Mock.of<MercureBoundProps>({ mercureInfo: {} })}
|
||||
getDomainVisits={getDomainVisits}
|
||||
cancelGetDomainVisits={cancelGetDomainVisits}
|
||||
domainVisits={domainVisits}
|
||||
settings={Mock.all<Settings>()}
|
||||
selectedServer={Mock.all<SelectedServer>()}
|
||||
/>
|
||||
</MemoryRouter>,
|
||||
));
|
||||
|
||||
it('wraps visits stats and header', () => {
|
||||
expect(screen.getByRole('heading', { name: '"foo.com" visits' })).toBeInTheDocument();
|
||||
expect(getDomainVisits).toHaveBeenCalledWith('DEFAULT', expect.anything(), expect.anything());
|
||||
});
|
||||
|
||||
it('exports visits when clicking the button', () => {
|
||||
const btn = screen.getByRole('button', { name: 'Export (1)' });
|
||||
|
||||
expect(exportVisits).not.toHaveBeenCalled();
|
||||
expect(btn).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(btn);
|
||||
expect(exportVisits).toHaveBeenCalledWith('domain_foo.com_visits.csv', expect.anything());
|
||||
});
|
||||
});
|
|
@ -4,10 +4,10 @@ import { NonOrphanVisits as createNonOrphanVisits } from '../../src/visits/NonOr
|
|||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import { VisitsInfo } from '../../src/visits/types';
|
||||
import VisitsStats from '../../src/visits/VisitsStats';
|
||||
import { NonOrphanVisitsHeader } from '../../src/visits/NonOrphanVisitsHeader';
|
||||
import { Settings } from '../../src/settings/reducers/settings';
|
||||
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { SelectedServer } from '../../src/servers/data';
|
||||
import VisitsHeader from '../../src/visits/VisitsHeader';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
@ -33,14 +33,14 @@ describe('<NonOrphanVisits />', () => {
|
|||
/>,
|
||||
).dive();
|
||||
const stats = wrapper.find(VisitsStats);
|
||||
const header = wrapper.find(NonOrphanVisitsHeader);
|
||||
const header = wrapper.find(VisitsHeader);
|
||||
|
||||
expect(stats).toHaveLength(1);
|
||||
expect(header).toHaveLength(1);
|
||||
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetNonOrphanVisits);
|
||||
expect(stats.prop('visitsInfo')).toEqual(nonOrphanVisits);
|
||||
expect(stats.prop('isOrphanVisits')).not.toBeDefined();
|
||||
expect(header.prop('nonOrphanVisits')).toEqual(nonOrphanVisits);
|
||||
expect(header.prop('visits')).toEqual(nonOrphanVisits.visits);
|
||||
expect(header.prop('goBack')).toEqual(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { NonOrphanVisitsHeader } from '../../src/visits/NonOrphanVisitsHeader';
|
||||
import VisitsHeader from '../../src/visits/VisitsHeader';
|
||||
import { Visit, VisitsInfo } from '../../src/visits/types';
|
||||
|
||||
describe('<NonOrphanVisitsHeader />', () => {
|
||||
it('wraps a VisitsHeader with provided data', () => {
|
||||
const visits: Visit[] = [];
|
||||
const orphanVisits = Mock.of<VisitsInfo>({ visits });
|
||||
const goBack = jest.fn();
|
||||
|
||||
const wrapper = shallow(<NonOrphanVisitsHeader nonOrphanVisits={orphanVisits} goBack={goBack} />);
|
||||
const visitsHeader = wrapper.find(VisitsHeader);
|
||||
|
||||
expect(visitsHeader).toHaveLength(1);
|
||||
expect(visitsHeader.prop('visits')).toEqual(visits);
|
||||
expect(visitsHeader.prop('goBack')).toEqual(goBack);
|
||||
expect(visitsHeader.prop('title')).toEqual('Non-orphan visits');
|
||||
});
|
||||
});
|
|
@ -4,10 +4,10 @@ import { OrphanVisits as createOrphanVisits } from '../../src/visits/OrphanVisit
|
|||
import { MercureBoundProps } from '../../src/mercure/helpers/boundToMercureHub';
|
||||
import { VisitsInfo } from '../../src/visits/types';
|
||||
import VisitsStats from '../../src/visits/VisitsStats';
|
||||
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
||||
import { Settings } from '../../src/settings/reducers/settings';
|
||||
import { ReportExporter } from '../../src/common/services/ReportExporter';
|
||||
import { SelectedServer } from '../../src/servers/data';
|
||||
import VisitsHeader from '../../src/visits/VisitsHeader';
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
|
@ -33,14 +33,14 @@ describe('<OrphanVisits />', () => {
|
|||
/>,
|
||||
).dive();
|
||||
const stats = wrapper.find(VisitsStats);
|
||||
const header = wrapper.find(OrphanVisitsHeader);
|
||||
const header = wrapper.find(VisitsHeader);
|
||||
|
||||
expect(stats).toHaveLength(1);
|
||||
expect(header).toHaveLength(1);
|
||||
expect(stats.prop('cancelGetVisits')).toEqual(cancelGetOrphanVisits);
|
||||
expect(stats.prop('visitsInfo')).toEqual(orphanVisits);
|
||||
expect(stats.prop('isOrphanVisits')).toEqual(true);
|
||||
expect(header.prop('orphanVisits')).toEqual(orphanVisits);
|
||||
expect(header.prop('visits')).toEqual(orphanVisits.visits);
|
||||
expect(header.prop('goBack')).toEqual(expect.any(Function));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,21 +0,0 @@
|
|||
import { shallow } from 'enzyme';
|
||||
import { Mock } from 'ts-mockery';
|
||||
import { OrphanVisitsHeader } from '../../src/visits/OrphanVisitsHeader';
|
||||
import VisitsHeader from '../../src/visits/VisitsHeader';
|
||||
import { Visit, VisitsInfo } from '../../src/visits/types';
|
||||
|
||||
describe('<OrphanVisitsHeader />', () => {
|
||||
it('wraps a VisitsHeader with provided data', () => {
|
||||
const visits: Visit[] = [];
|
||||
const orphanVisits = Mock.of<VisitsInfo>({ visits });
|
||||
const goBack = jest.fn();
|
||||
|
||||
const wrapper = shallow(<OrphanVisitsHeader orphanVisits={orphanVisits} goBack={goBack} />);
|
||||
const visitsHeader = wrapper.find(VisitsHeader);
|
||||
|
||||
expect(visitsHeader).toHaveLength(1);
|
||||
expect(visitsHeader.prop('visits')).toEqual(visits);
|
||||
expect(visitsHeader.prop('goBack')).toEqual(goBack);
|
||||
expect(visitsHeader.prop('title')).toEqual('Orphan visits');
|
||||
});
|
||||
});
|
242
test/visits/reducers/domainVisits.test.ts
Normal file
242
test/visits/reducers/domainVisits.test.ts
Normal file
|
@ -0,0 +1,242 @@
|
|||
import { Mock } from 'ts-mockery';
|
||||
import { addDays, formatISO, subDays } from 'date-fns';
|
||||
import reducer, {
|
||||
getDomainVisits,
|
||||
cancelGetDomainVisits,
|
||||
GET_DOMAIN_VISITS_START,
|
||||
GET_DOMAIN_VISITS_ERROR,
|
||||
GET_DOMAIN_VISITS,
|
||||
GET_DOMAIN_VISITS_LARGE,
|
||||
GET_DOMAIN_VISITS_CANCEL,
|
||||
GET_DOMAIN_VISITS_PROGRESS_CHANGED,
|
||||
GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL,
|
||||
DomainVisits,
|
||||
DEFAULT_DOMAIN,
|
||||
} from '../../../src/visits/reducers/domainVisits';
|
||||
import { CREATE_VISITS } from '../../../src/visits/reducers/visitCreation';
|
||||
import { rangeOf } from '../../../src/utils/utils';
|
||||
import { Visit } from '../../../src/visits/types';
|
||||
import { ShlinkVisits } from '../../../src/api/types';
|
||||
import ShlinkApiClient from '../../../src/api/services/ShlinkApiClient';
|
||||
import { ShlinkState } from '../../../src/container/types';
|
||||
import { formatIsoDate } from '../../../src/utils/helpers/date';
|
||||
import { DateInterval } from '../../../src/utils/dates/types';
|
||||
import { ShortUrl } from '../../../src/short-urls/data';
|
||||
|
||||
describe('domainVisitsReducer', () => {
|
||||
const now = new Date();
|
||||
const visitsMocks = rangeOf(2, () => Mock.all<Visit>());
|
||||
|
||||
describe('reducer', () => {
|
||||
const buildState = (data: Partial<DomainVisits>) => Mock.of<DomainVisits>(data);
|
||||
|
||||
it('returns loading on GET_DOMAIN_VISITS_START', () => {
|
||||
const state = reducer(buildState({ loading: false }), { type: GET_DOMAIN_VISITS_START } as any);
|
||||
const { loading } = state;
|
||||
|
||||
expect(loading).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns loadingLarge on GET_DOMAIN_VISITS_LARGE', () => {
|
||||
const state = reducer(buildState({ loadingLarge: false }), { type: GET_DOMAIN_VISITS_LARGE } as any);
|
||||
const { loadingLarge } = state;
|
||||
|
||||
expect(loadingLarge).toEqual(true);
|
||||
});
|
||||
|
||||
it('returns cancelLoad on GET_DOMAIN_VISITS_CANCEL', () => {
|
||||
const state = reducer(buildState({ cancelLoad: false }), { type: GET_DOMAIN_VISITS_CANCEL } as any);
|
||||
const { cancelLoad } = state;
|
||||
|
||||
expect(cancelLoad).toEqual(true);
|
||||
});
|
||||
|
||||
it('stops loading and returns error on GET_DOMAIN_VISITS_ERROR', () => {
|
||||
const state = reducer(buildState({ loading: true, error: false }), { type: GET_DOMAIN_VISITS_ERROR } as any);
|
||||
const { loading, error } = state;
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(true);
|
||||
});
|
||||
|
||||
it('return visits on GET_DOMAIN_VISITS', () => {
|
||||
const actionVisits = [{}, {}];
|
||||
const state = reducer(
|
||||
buildState({ loading: true, error: false }),
|
||||
{ type: GET_DOMAIN_VISITS, visits: actionVisits } as any,
|
||||
);
|
||||
const { loading, error, visits } = state;
|
||||
|
||||
expect(loading).toEqual(false);
|
||||
expect(error).toEqual(false);
|
||||
expect(visits).toEqual(actionVisits);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ domain: 'foo.com' }, 'foo.com', visitsMocks.length + 1],
|
||||
[{ domain: 'bar.com' }, 'foo.com', visitsMocks.length],
|
||||
[Mock.of<DomainVisits>({ domain: 'foo.com' }), 'foo.com', visitsMocks.length + 1],
|
||||
[Mock.of<DomainVisits>({ domain: DEFAULT_DOMAIN }), null, visitsMocks.length + 1],
|
||||
[
|
||||
Mock.of<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: { endDate: formatIsoDate(subDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
Mock.of<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: { startDate: formatIsoDate(addDays(now, 1)) ?? undefined },
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
Mock.of<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(subDays(now, 2)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
[
|
||||
Mock.of<DomainVisits>({
|
||||
domain: 'foo.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length + 1,
|
||||
],
|
||||
[
|
||||
Mock.of<DomainVisits>({
|
||||
domain: 'bar.com',
|
||||
query: {
|
||||
startDate: formatIsoDate(subDays(now, 5)) ?? undefined,
|
||||
endDate: formatIsoDate(addDays(now, 3)) ?? undefined,
|
||||
},
|
||||
}),
|
||||
'foo.com',
|
||||
visitsMocks.length,
|
||||
],
|
||||
])('prepends new visits on CREATE_VISIT', (state, shortUrlDomain, expectedVisits) => {
|
||||
const shortUrl = Mock.of<ShortUrl>({ domain: shortUrlDomain });
|
||||
const prevState = buildState({
|
||||
...state,
|
||||
visits: visitsMocks,
|
||||
});
|
||||
|
||||
const { visits } = reducer(prevState, {
|
||||
type: CREATE_VISITS,
|
||||
createdVisits: [{ shortUrl, visit: { date: formatIsoDate(now) ?? undefined } }],
|
||||
} as any);
|
||||
|
||||
expect(visits).toHaveLength(expectedVisits);
|
||||
});
|
||||
|
||||
it('returns new progress on GET_DOMAIN_VISITS_PROGRESS_CHANGED', () => {
|
||||
const state = reducer(undefined, { type: GET_DOMAIN_VISITS_PROGRESS_CHANGED, progress: 85 } as any);
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ progress: 85 }));
|
||||
});
|
||||
|
||||
it('returns fallbackInterval on GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL', () => {
|
||||
const fallbackInterval: DateInterval = 'last30Days';
|
||||
const state = reducer(undefined, { type: GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval } as any);
|
||||
|
||||
expect(state).toEqual(expect.objectContaining({ fallbackInterval }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDomainVisits', () => {
|
||||
type GetVisitsReturn = Promise<ShlinkVisits> | ((shortCode: string, query: any) => Promise<ShlinkVisits>);
|
||||
|
||||
const buildApiClientMock = (returned: GetVisitsReturn) => Mock.of<ShlinkApiClient>({
|
||||
getDomainVisits: jest.fn(typeof returned === 'function' ? returned : async () => returned),
|
||||
});
|
||||
const dispatchMock = jest.fn();
|
||||
const getState = () => Mock.of<ShlinkState>({
|
||||
domainVisits: { cancelLoad: false },
|
||||
});
|
||||
const domain = 'foo.com';
|
||||
|
||||
beforeEach(jest.clearAllMocks);
|
||||
|
||||
it('dispatches start and error when promise is rejected', async () => {
|
||||
const shlinkApiClient = buildApiClientMock(Promise.reject(new Error()));
|
||||
|
||||
await getDomainVisits(() => shlinkApiClient)('foo.com')(dispatchMock, getState);
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_DOMAIN_VISITS_START });
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_DOMAIN_VISITS_ERROR });
|
||||
expect(shlinkApiClient.getDomainVisits).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[undefined],
|
||||
[{}],
|
||||
])('dispatches start and success when promise is resolved', async (query) => {
|
||||
const visits = visitsMocks;
|
||||
const shlinkApiClient = buildApiClientMock(Promise.resolve({
|
||||
data: visitsMocks,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
}));
|
||||
|
||||
await getDomainVisits(() => shlinkApiClient)(domain, query)(dispatchMock, getState);
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_DOMAIN_VISITS_START });
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, { type: GET_DOMAIN_VISITS, visits, domain, query: query ?? {} });
|
||||
expect(shlinkApiClient.getDomainVisits).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 20)) })],
|
||||
{ type: GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last30Days' },
|
||||
],
|
||||
[
|
||||
[Mock.of<Visit>({ date: formatISO(subDays(new Date(), 100)) })],
|
||||
{ type: GET_DOMAIN_VISITS_FALLBACK_TO_INTERVAL, fallbackInterval: 'last180Days' },
|
||||
],
|
||||
[[], expect.objectContaining({ type: GET_DOMAIN_VISITS })],
|
||||
])('dispatches fallback interval when the list of visits is empty', async (lastVisits, expectedSecondDispatch) => {
|
||||
const buildVisitsResult = (data: Visit[] = []): ShlinkVisits => ({
|
||||
data,
|
||||
pagination: {
|
||||
currentPage: 1,
|
||||
pagesCount: 1,
|
||||
totalItems: 1,
|
||||
},
|
||||
});
|
||||
const getShlinkDomainVisits = jest.fn()
|
||||
.mockResolvedValueOnce(buildVisitsResult())
|
||||
.mockResolvedValueOnce(buildVisitsResult(lastVisits));
|
||||
const ShlinkApiClient = Mock.of<ShlinkApiClient>({ getDomainVisits: getShlinkDomainVisits });
|
||||
|
||||
await getDomainVisits(() => ShlinkApiClient)(domain, {}, true)(dispatchMock, getState);
|
||||
|
||||
expect(dispatchMock).toHaveBeenCalledTimes(2);
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(1, { type: GET_DOMAIN_VISITS_START });
|
||||
expect(dispatchMock).toHaveBeenNthCalledWith(2, expectedSecondDispatch);
|
||||
expect(getShlinkDomainVisits).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cancelGetDomainVisits', () => {
|
||||
it('just returns the action with proper type', () =>
|
||||
expect(cancelGetDomainVisits()).toEqual({ type: GET_DOMAIN_VISITS_CANCEL }));
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue