Compare commits

...

187 Commits

Author SHA1 Message Date
Mark McDowall c664eaa9b5 New: Don't treat 400 responses from Notifiarr as errors
(cherry picked from commit 5eb420bbe12f59d0a5392abf3d351be28ca210e6)
2023-10-07 23:02:44 +03:00
Bogdan b7e57f0c08 Fixed: (Nebulance) Filter releases by season and episode for ID based searches 2023-10-07 03:09:44 +03:00
Bogdan c06bf0e4ea Fixed: (TorrentDay) Update categories
Fixes #1888
2023-10-05 22:41:59 +03:00
Bogdan c6db30c35a Parse description in RSS Parser 2023-10-05 01:54:40 +03:00
Bogdan 75c30dd318 Add year to XML results 2023-10-04 19:51:49 +03:00
Bogdan 6e7bf55dbd Add poster URL to PassThePopcorn 2023-10-04 19:16:55 +03:00
Bogdan eb642dd2f9 Fix document being disposed before returning 2023-10-04 18:36:31 +03:00
Bogdan 19a196e2c7 Ensure the correct use of disposable parsed documents 2023-10-04 18:11:23 +03:00
Weblate 93ec6cf89b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dimitri <dimitridroeck@gmail.com>
Co-authored-by: Garkus98 <ivan12061998@gmail.com>
Co-authored-by: Oskari Lavinto <olavinto@protonmail.com>
Co-authored-by: RicardoVelaC <ricardovelac@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-10-04 07:00:00 +03:00
Bogdan 52c6b56a4c Cleanup BooleanConverter to STJson 2023-10-04 05:19:03 +03:00
Bogdan 82688d8a55 Use ExecuteAuth in AvistazBase 2023-10-04 04:27:04 +03:00
Bogdan c81cbc801a Fixed: (AvistaZBase) Parse response with STJson
Also ensure GetToken is using a proxied request and rate limit
2023-10-04 02:27:32 +03:00
Bogdan 993d189c61 Fixed: (Nebulance) Parse response with STJson 2023-10-04 02:27:29 +03:00
Bogdan 1901af5a51 Fixed: (BeyondHD) Parse response with STJson 2023-10-04 02:27:24 +03:00
Bogdan c1b399be39 Fixed: (FileList) Parse response with STJson 2023-10-04 02:27:19 +03:00
Bogdan 2100e96570 Fixed: (PassThePopcorn) Use UTC for publish dates 2023-10-02 04:35:57 +03:00
Bogdan 3ff144421d Fixed: (PassThePopcorn) Cleanup and ensure pagination is working in Radarr 2023-10-02 02:55:34 +03:00
Bogdan f37ccba3f9 Fixed: (Shizaproject) Title improvements 2023-10-02 00:47:33 +03:00
Bogdan 181cb2e0fe Revert "New: (Orpheus) Add options to prevent downloads without FL tokens"
This reverts commit 93c81bb7d3.
2023-10-01 19:47:28 +03:00
Bogdan 93c81bb7d3 New: (Orpheus) Add options to prevent downloads without FL tokens 2023-10-01 17:03:43 +03:00
Weblate 7dd289b5f9 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Jaspils <jasperkemper@gmail.com>
Co-authored-by: Stevie Robinson <stevie.robinson@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-10-01 17:03:03 +03:00
Bogdan 09cef8cf94 Bump version to 1.9.3 2023-10-01 17:01:35 +03:00
Bogdan ca08c818e6 Fixed: (TorrentPotato) Use full IMDb Id 2023-09-30 22:41:25 +03:00
Bogdan 3e95bc4056 Fixed: (TorrentPotato) Title not being decoded 2023-09-30 22:15:25 +03:00
ilike2burnthing e241112915 Fixed: (Shizaproject) Available again 2023-09-29 22:13:14 +03:00
Bogdan 0d98c12fa2 Fix the description for Use Filenames for Single Episodes 2023-09-29 20:45:04 +03:00
Bogdan a0bcf5c9ae Allow using filename for single episodes along with generated titles in AnimeBytes 2023-09-29 20:26:35 +03:00
Bogdan e318a47b3a Extend the torrent settings interface in TorrentPotato settings class 2023-09-27 23:23:43 +03:00
Bogdan b8df720c6c Bump version to 1.9.2 2023-09-24 16:22:57 +03:00
Bogdan 9625be723d Fixed: (Search) Releases deduplication 2023-09-22 19:54:11 +03:00
Bogdan d4b037db78 New: (AlphaRatio) Add pagination support 2023-09-21 20:46:34 +03:00
Bogdan add2988789 Fixed: (Cardigann) Fallback variables to empty string to prevent NullRef 2023-09-20 02:54:25 +03:00
Bogdan 9869c2272a Avoid returning null in static resource mapper Task 2023-09-19 20:17:05 +03:00
Bogdan 4c8b0c9eec Fixed: Ignore releases without title 2023-09-19 14:06:15 +03:00
Bogdan 43cb22ff2b Bump migration timeout to 10 minutes 2023-09-19 00:57:06 +03:00
Bogdan 3cabc0589a Simplify use the group name when the release name is empty 2023-09-18 21:17:36 +03:00
Bogdan cdb3ed36f6 Fixed: (Nebulance) Use the group name when the release name is empty 2023-09-18 21:16:13 +03:00
Bogdan 840f2ae3e6 Sync static resource controller with upstream
(cherry picked from commit ad1f185330a30a2a9d27c9d3f18d384e66727c2a)
2023-09-18 03:53:14 +03:00
Bogdan 3ed6ef0336 Use await on reading the response content
(cherry picked from commit 82d586e7015d7ea06356ca436024a8af5a4fb677)
2023-09-18 03:49:31 +03:00
Bogdan c2ae0cce03 Bump version to 1.9.1 2023-09-17 12:01:01 +03:00
Weblate 934b908b37 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Co-authored-by: 宿命 <331874545@qq.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-09-16 08:51:49 -04:00
Weblate 6c831f11a6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: leotpp <yangdom_li@126.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2023-09-15 21:49:25 -05:00
Bogdan 9adbfd2391 Return 1 seeder as fallback only in Torrent RSS Feed 2023-09-14 21:48:32 +03:00
bakerboy448 4a7cc82f0d Correction to Improve tags helptext and add warnings 2023-09-14 20:56:24 +03:00
bakerboy448 c061c309bd Improve tags helptext and add warnings 2023-09-14 20:48:28 +03:00
Bogdan 0f3a77c336 Return 1 seeder as fallback in Torrent RSS Feed 2023-09-14 18:55:07 +03:00
Bogdan 478d5a624f Fixed: (Animedia) Removed
Co-authored-by: ilike2burnthing <59480337+ilike2burnthing@users.noreply.github.com>
2023-09-14 18:42:41 +03:00
Bogdan 3283d144f5 Ignore invalid cookies when adding response cookies to CookieContainer
Fixes #1868
2023-09-14 18:22:53 +03:00
Bogdan 1a9ec4febd Fixed: (Apps) Check if the indexers have valid settings 2023-09-14 17:50:54 +03:00
Bogdan 0598211319 Fixed: Ignore inaccessible mount points
(cherry picked from commit 60f18249b05daa20523542beef54bc126d963d1e)
2023-09-14 12:15:37 +03:00
Bogdan 0b0d6b7590 Fixed: (SubsPlease) Update category mappings for movie releases
Co-authored-by: Lemres <45440100+Calemy@users.noreply.github.com>

Fixes #1866
2023-09-13 12:30:06 +03:00
Servarr 86cec51ebe Automated API Docs update 2023-09-12 22:59:27 +03:00
Bogdan 80e5ac4aa9 New: Add custom filter by protocol for indexer stats 2023-09-12 15:25:48 +03:00
Bogdan ee5ed0c91b Sonarr > Prowlarr 2023-09-11 11:59:51 +03:00
Denis Gheorghescu ba278930ed New: Pushcut notifications
(cherry picked from commit 5f09f2b25f9144666b8e5182e20e263e74d5f5ca)
2023-09-11 11:57:35 +03:00
Mark McDowall 6449b89eb6 Fixed parsing of multiple languages from Newznab indexer releases
(cherry picked from commit 2a241294b5eeb9e95c46e030828191da09d05e88)
2023-09-11 11:29:07 +03:00
Weblate 73b85e240e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_TW/
Translation: Servarr/Prowlarr
2023-09-10 20:30:01 +03:00
Bogdan 6338460ff4 Bump version to 1.9.0 2023-09-10 19:09:26 +03:00
Bogdan 0463e66881 Rename ISearchForNzb to IReleaseSearchService 2023-09-10 11:19:17 +03:00
Bogdan bd75621437 Update magnet trackers 2023-09-10 09:46:39 +03:00
Weblate 9615c1183d Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-09-10 08:33:44 +03:00
Weblate bbf042ed55 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: ChewyGlitter <lulu3dddsss@gmail.com>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-09-10 00:31:50 -05:00
Bogdan 98e948dbb2 Fix indexer flags column label 2023-09-09 14:15:58 +03:00
Bogdan 2af9f7eb8d Fixed: macOS version detection
(cherry picked from commit 060be6177a5477c94823e6a423c42064dedc1afb)
2023-09-08 05:10:05 +03:00
Bogdan 96413f99c7 New: Add hour as interval unit for indexer limits 2023-09-06 17:08:33 +03:00
Bogdan d44b946d30 Add hourly limits as defaults for FL and BTN 2023-09-06 17:08:33 +03:00
Bogdan fe9cad5697 Move clone button to the left 2023-09-04 11:00:37 +03:00
Servarr 098be3cff6 Automated API Docs update 2023-09-04 07:03:50 +03:00
Bogdan 8f2fea0be8 Fixed: (Nebulance) Add fallback TV search without season/episode 2023-09-04 06:58:24 +03:00
Qstick 8d035c6c1f New: Add support for MaxAge to Newznab API 2023-09-03 22:36:11 -05:00
Bogdan 7dbfa74c40 Add external links for indexer history 2023-09-04 05:53:33 +03:00
Qstick caaf50ed9c New: Save categories to history for grabs 2023-09-03 20:30:44 -05:00
Bogdan b472a022a6 Add history parameters to indexer history 2023-09-04 04:12:56 +03:00
Qstick 0a439a4a96 Show all queries in Total Queries block 2023-09-03 20:11:58 -05:00
Bogdan 4410636b97 Use method group in History controller 2023-09-04 02:22:23 +03:00
Servarr ba3ebc7574 Automated API Docs update 2023-09-04 02:14:55 +03:00
Bogdan 2ce49a0785 Migrate to merged proposals now included in babel/present-env
Bump core-js too.
2023-09-04 02:09:04 +03:00
Qstick d7df946c2b New: Summary Stats 2023-09-03 18:04:39 -05:00
Qstick 3dd3c80b54 New: Indexer history in indexer info modal 2023-09-03 18:04:39 -05:00
Qstick 0f160707d3 Chart style updates 2023-09-03 18:04:39 -05:00
Qstick b608e38454 New: Custom Filters for Stats 2023-09-03 18:04:39 -05:00
Bogdan c873b3ffac New: (UI) Add clone indexer in info modal 2023-09-03 23:08:15 +03:00
Bogdan 07b98f4137 Move scene flag to TorrentInfo and return tag in response 2023-09-03 23:07:59 +03:00
Bogdan 09606af351 Bump dotnet to 6.0.21 2023-09-03 10:33:50 -05:00
Bogdan 1d79b92fca Bump version to 1.8.6 2023-09-03 07:26:15 +03:00
Stevie Robinson fbcf1b03c5 Translate Frontend Store
(cherry picked from commit d31fcbb2dfe98a540a359b98b204d101d554cf03)
2023-09-02 06:04:57 +03:00
Bogdan dee98ac46f Display a not allowed cursor for disabled select options 2023-09-02 03:47:07 +03:00
Bogdan 4267b8a244 Cleanse API Keys from sync requests sent to applications 2023-09-02 02:07:26 +03:00
Bogdan 00dc55996c Remove Label and Unit columns from Apps Fields 2023-09-02 01:59:11 +03:00
Bogdan b912cc6110 Some logging improvements for filtered releases
Closes #1847
2023-09-02 00:47:47 +03:00
Bogdan 56f0c137f8 Fixed translations for history parameters 2023-09-01 15:50:22 +03:00
Bogdan 1b8ff9b989 Simplify user check in Authentication Required for newer installs 2023-08-29 19:12:06 +03:00
Bogdan bfecf35a8b Fix user check in Authentication Required for newer installs 2023-08-29 18:56:21 +03:00
Bogdan 80da5ce165 Add missing app name token for translation 2023-08-29 03:18:11 +03:00
Bogdan 60ca0db26f Improve messaging in Authentication Required modal
Co-authored-by: bakerboy448 <55419169+bakerboy448@users.noreply.github.com>
2023-08-29 03:09:42 +03:00
Bogdan 288a3d1495 Fix loading translations 2023-08-29 03:05:51 +03:00
Weblate 4c42907eb2 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: AlexR-sf <omg.portal.supp@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: ChewyGlitter <lulu3dddsss@gmail.com>
Co-authored-by: DavidJares <david.jares@me.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: He Zhu <zhuhe202@qq.com>
Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-27 19:21:30 +03:00
Bogdan 6300eb1442 Bump version to 1.8.5 2023-08-27 08:00:53 +03:00
Bogdan e4c0edf24c Remove Reddit from issue templates 2023-08-26 21:14:23 +03:00
Bogdan 74a9fa784a Fixed: (Nebulance) Detect TV UHD releases 2023-08-26 20:21:41 +03:00
ilike2burnthing 1b0c9adf24 Remove Reddit from issues config 2023-08-22 00:30:49 +03:00
Bogdan 0eaa538e8a Allow deselecting values when using search through url query params 2023-08-21 21:32:26 +03:00
Bogdan 39a54eb8f6 Prevent health checks warnings for disabled notifications 2023-08-21 21:04:30 +03:00
Bogdan 5ad6237785 Improve music and console search results for AnimeBytes
Also prevent duplicate categories showing in the indexer info modal
2023-08-21 18:08:43 +03:00
Bogdan 9fee4f914f Bump version to 1.8.4 2023-08-20 12:23:24 +03:00
Weblate ba2aab6bb3 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Robert A. Viana <robert.abreu@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: w2861 <hfagfc@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-20 03:52:43 +03:00
Bogdan 5c8ae82f11 Prevent useless builds 2023-08-19 16:23:16 +03:00
Servarr bcbeac1e83 Automated API Docs update [skip ci] 2023-08-19 13:55:35 +03:00
Mark McDowall b36d793d85 Allow decimals in number inputs
(cherry picked from commit 7f5ddff568ce9f87bd45420cbd36690b190bd633)
2023-08-19 13:49:18 +03:00
Mark McDowall b0162ccc5b New: Success check mark on blue buttons is now white instead of green
(cherry picked from commit 566fae9d5857a10bd69c718368e7847e5a733faa)
2023-08-19 13:47:56 +03:00
Robin Dadswell f0892eb4b8 Adds Pipeline testing for Postgres15 Databases 2023-08-18 21:12:09 +03:00
Robin Dadswell e456979467 bump Npgsql to 7.0.4 2023-08-18 21:12:09 +03:00
Bogdan 66ca47b615 Fix flaky automation tests 2023-08-18 20:36:44 +03:00
Weblate 2b7771bfe0 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Robert A. Viana <robert.abreu@outlook.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: w2861 <hfagfc@163.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ar/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/bg/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ca/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/cs/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/da/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/de/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/el/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hu/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/id/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/is/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ja/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ko/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ru/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/sv/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/th/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/tr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/uk/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/vi/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-18 11:40:45 +03:00
Mark McDowall 955bc472a1 Fixed: Hidden files being ignored
(cherry picked from commit d493f8762fcb1684b44e182753c21d7a493db787)
2023-08-18 11:33:21 +03:00
Bogdan e024bba6b6 Replace docker detection for cgroup v2
(cherry picked from commit 78d4dee4610c5f3f90cc69469004008aa64900b8)
2023-08-18 11:32:39 +03:00
Bogdan aeb3b7d8b5 Add retry to flaky automation tests 2023-08-17 14:38:48 +03:00
bakerboy448 a7b25b8b93 Remove reddit from readme 2023-08-16 17:25:12 +03:00
Qstick 130257fdd4 New: Notifications (Connect) Status
(cherry picked from commit e3545801721e00d4e5cac3fa534e66dcbe9d2d05)
2023-08-14 16:07:23 +03:00
Qstick b618f23bc0 Cleanup other provider status code
(cherry picked from commit c281a7818adce8db728d2a104f4444cb9c0baf2c)
2023-08-14 16:07:23 +03:00
Bogdan a758161e31 New: Default name when adding providers 2023-08-14 16:07:23 +03:00
Weblate 27928103c5 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Weblate <noreply@weblate.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translation: Servarr/Prowlarr
2023-08-14 14:06:06 +03:00
Weblate d5b3961e8a Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Anonymous <noreply@weblate.org>
Co-authored-by: David Molero <contact@dolvem.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: deepserket <deepserket@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/es/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-08-14 13:48:57 +03:00
Stevie Robinson 307adf053e Translate Updated and Connection Lost Modals in frontend
(cherry picked from commit 074aa6f4457bf83173e6ba7209c452a6e0659a35)
2023-08-14 01:22:09 +03:00
Bogdan 31261f66ad Use named tokens in frontend translate function 2023-08-13 22:59:56 +03:00
Bogdan 5dbb59dfaa Bump version to 1.8.3 2023-08-13 13:04:41 +03:00
Weblate 25c1803d0e Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Nir Israel Hen <nirisraelh@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: wilfriedarma <wilfriedarma.collet@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/he/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-08-13 12:51:24 +03:00
Bogdan 9f4c9d3344 Show successful grabs in Search with green icon 2023-08-12 12:12:51 +03:00
Bogdan dfb00d9bb1 Fixed: Ensure grab notifications are sent according to tags requirements 2023-08-12 12:07:17 +03:00
Bogdan f7727855b5 Rework adding one minute back-off level for all providers
(cherry picked from commit d8f314ff0ef64e8d90b21b7865e46be74db5e570)
2023-08-12 09:32:07 +03:00
Stepan Goremykin 1e4c67dcdb Update FluentAssertions
(cherry picked from commit 951a9ade00d7c9105f03608cb598450d706b826f)
2023-08-12 09:28:42 +03:00
Robin Dadswell 26afcb0071 Fixed: PostgreSQL timezone issues
(cherry picked from commit d55864f86914199aa0c4ee37df1e42e6ad71ef4f)
2023-08-10 23:03:27 +01:00
Bogdan 7a937e85a4 Fixed: Retain user settings not-affiliated with Prowlarr 2023-08-10 18:05:00 +03:00
Bogdan 7cd82321b4 Bump Npgsql version to 6.0.9
Fixes #1819
2023-08-09 23:41:09 +03:00
Bogdan 8c9adba516 Fixed color for links 2023-08-09 19:30:25 +03:00
Bogdan 03fa9254e3 Prevent NullRef in IsPathValid for null paths 2023-08-09 13:35:13 +03:00
Weblate e66ecf5c95 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: byakurau <byakurau1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-08-08 18:49:41 +03:00
Bogdan e0dddfa215 Remove Order and Help columns from Apps Fields 2023-08-08 18:30:25 +03:00
Bogdan bcb8afadf8 New: Add Content Summary for requests to apps 2023-08-08 18:26:09 +03:00
Bogdan fc4a0979c3 Fixed: Detect Docker when using control group v2 2023-08-07 19:15:21 +03:00
Bogdan 5f643b2ced Fixed: (Indexers) Don't fetch releases when using unsupported capabilities 2023-08-06 20:30:59 +03:00
Bogdan 6f09b0f4f5 Bump version to 1.8.2 2023-08-06 08:42:24 +03:00
Qstick 95c2531107 Filter user issues from Sentry
(cherry picked from commit 03d361f5537bfc0caba1b86085f974570942fdbc)
2023-08-05 21:38:44 +03:00
Bogdan f83828cc22 Fixed border for actions in health status 2023-08-05 17:56:13 +03:00
Bogdan cdea548ce2 New: Add internal links for apps and download clients health checks 2023-08-05 14:26:53 +03:00
Bogdan cae1da0ce2 Fixed: (Apps) Lower the severity for testing messages 2023-08-05 14:12:06 +03:00
Bogdan 765f354c51 New: Add test all action for apps and download clients to status health 2023-08-05 14:11:50 +03:00
Bogdan 5cbbffb018 Fix translation typo in sync level options 2023-08-05 14:11:50 +03:00
Bogdan b2c5448cbf Fixed: Run health checks for applications and download clients on bulk events 2023-08-05 11:40:15 +03:00
Bogdan 3dae84705c Fixed: Ensure failing providers are marked as failed when testing all 2023-08-05 10:02:55 +03:00
Weblate 2321d278d6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Stjepan <stjepstjepanovic@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-04 21:07:31 +03:00
TwentyNine78 ea73466f6a Fixed: Compatibility with the new Download Station API
(cherry picked from commit 49e90463e57929e7b9885f1b7b0eb05bd7cc3ebe)
2023-08-04 21:06:45 +03:00
Bogdan 6961c5a1c6 Fixed: (AlphaRatio) Use FL tokens only if canUseToken is true
Fixes #1811
2023-08-04 17:20:28 +03:00
Mark McDowall 141f1597dc New: Ignore inaccessible files with getting files
(cherry picked from commit e5aa8584100d96a2077c57f74ae5b2ceab63de19)
2023-08-04 13:17:37 +03:00
Bogdan 1100f350ae Fix translations for option values 2023-08-04 07:05:31 +03:00
Bogdan 3c5eefc349 New: Health check for indexers with invalid download client
(cherry picked from commit 377fce6fe15c0875c4bd33f1371a31af79c9310c)
2023-08-04 06:50:48 +03:00
Mark McDowall 0bfb557470 Prevent NullRef in ContainsInvalidPathChars
(cherry picked from commit 5f7217844533907d7fc6287a48efb31987736c4c)
2023-08-04 06:43:40 +03:00
Servarr c93d6cff63 Automated API Docs update [skip ci] 2023-08-03 17:40:31 +03:00
Bogdan 7e4980b855 New: Add translations for columns 2023-08-03 17:20:36 +03:00
Bogdan 419ef4b3bf New: More translations for columns
(cherry picked from commit aee8579d1823b7dfb94c0055fe33b5fb5a7fbf17)
2023-08-03 16:10:13 +03:00
Mark McDowall c56d49ab60 Fixed: Translations for columns
(cherry picked from commit 6d53d2a153a98070c42d0619c15902b6bd5dfab4)
2023-08-03 16:07:00 +03:00
Mark McDowall 1a40924db3 Fixed: Improve translation loading
(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)
2023-08-03 16:05:39 +03:00
Mark McDowall d55906d49a UI loading improvements
Fixed: Caching for dynamically loaded JS files
Fixed: Incorrect caching of initialize.js
(cherry picked from commit f0cb5b81f140c67fa84162e094cc4e0f3476f5da)
2023-08-03 15:57:52 +03:00
Bogdan bc53fab966 Fixed: Don't fetch capabilities for disabled Newznab/Torznab indexers on create
Also prevent NullRef in GetProxy since definition is null when using FetchCapabilities on add
2023-08-02 14:54:01 +03:00
Bogdan d897b50f80 New: (UI) Show Magnet Link in search results if any 2023-08-01 13:29:12 +03:00
Bogdan cc66cee71c Fixed: (Apps) Avoid force saving remote indexers when it's not necessary 2023-07-31 10:33:57 +03:00
Bogdan f5e96f3f51 Ensure yarn packages are installed when running only LintUI 2023-07-31 08:18:45 +03:00
Mark McDowall d52e1259a1 Re-order frontend build steps
(cherry picked from commit 97ad6682f7d54af8886144bc5a179fa7242f1f1f)
2023-07-31 07:56:20 +03:00
Bogdan 72e6d66269 New: (Apps) Add force sync indexers for applications 2023-07-31 07:22:08 +03:00
Bogdan e51b85449d Convert store selectors to Typescript 2023-07-30 21:06:44 +03:00
Bogdan efd5e92ca5 Support categories with Transmission 2023-07-30 12:38:33 +03:00
Bogdan d153746a98 Bump version to 1.8.1 2023-07-30 10:45:08 +03:00
Bogdan a1927e1e0f Sort indexers by name in search footer dropdown 2023-07-29 13:48:10 +03:00
Bogdan 630a4ce800 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)
2023-07-29 12:14:58 +03:00
Bogdan 8b1dd78300 Fixed: (Apps) Ensure populated capabilities for Torznab/Newznab definitions 2023-07-29 12:08:48 +03:00
Bogdan cab50b35aa Convert some selectors to Typescript 2023-07-29 03:14:47 +03:00
Weblate eee1be784b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-28 12:55:09 +03:00
Bogdan 269dc5688b New: (IPTorrents) Add new base url 2023-07-27 12:18:49 +03:00
Weblate 9bed795c89 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-07-27 12:14:26 +03:00
Bogdan 3b5f151252 New: Set default names for providers in Add Modals 2023-07-27 02:58:07 +03:00
Weblate b3a541c9ff Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dortlix <jeremy.boely@hotmail.fr>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aguillement <adrien.guillement@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-26 07:18:25 +03:00
Bogdan bc90fa2d3f Add unit to history cleanup days option 2023-07-26 07:17:06 +03:00
Bogdan 4b0a896434 Fixed: (Cardigann) Improvements to automatic logins with captcha 2023-07-26 05:35:42 +03:00
Bogdan 6be0e08635 Convert Delete Indexer to Typescript 2023-07-25 05:50:45 +03:00
Bogdan f618901048 Convert Indexer Stats to Typescript 2023-07-25 05:50:45 +03:00
Weblate 809ed940e6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-07-25 01:05:11 +03:00
Qstick 7b14c2ee66 Bump version to 1.8.0 2023-07-23 23:25:30 -05:00
478 changed files with 7663 additions and 3863 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
name: Bug Report name: Bug Report
description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Reddit or Discord first' description: 'Report a new bug, if you are not 100% certain this is a bug please go to our Discord first'
labels: ['Type: Bug', 'Status: Needs Triage'] labels: ['Type: Bug', 'Status: Needs Triage']
body: body:
- type: checkboxes - type: checkboxes
-3
View File
@@ -6,6 +6,3 @@ contact_links:
- name: Support via Discord - name: Support via Discord
url: https://prowlarr.com/discord url: https://prowlarr.com/discord
about: Chat with users and devs on support and setup related topics. about: Chat with users and devs on support and setup related topics.
- name: Support via Reddit
url: https://reddit.com/r/prowlarr
about: Discuss and search thru support topics.
+1 -2
View File
@@ -4,8 +4,7 @@
comment: > comment: >
:wave: @{issue-author}, we use the issue tracker exclusively :wave: @{issue-author}, we use the issue tracker exclusively
for bug reports and feature requests. However, this issue appears for bug reports and feature requests. However, this issue appears
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord) to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord).
or [Subreddit](https://reddit.com/r/prowlarr)
close: true close: true
close-reason: 'not planned' close-reason: 'not planned'
-1
View File
@@ -29,7 +29,6 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
[![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr) [![Wiki](https://img.shields.io/badge/servarr-wiki-181717.svg?maxAge=60)](https://wiki.servarr.com/prowlarr)
[![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord) [![Discord](https://img.shields.io/badge/discord-chat-7289DA.svg?maxAge=60)](https://prowlarr.com/discord)
[![Reddit](https://img.shields.io/badge/reddit-discussion-FF4500.svg?maxAge=60)](https://www.reddit.com/r/Prowlarr)
Note: GitHub Issues are for Bugs and Feature Requests Only Note: GitHub Issues are for Bugs and Feature Requests Only
+135 -12
View File
@@ -9,15 +9,15 @@ variables:
testsFolder: './_tests' testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.7.4' majorVersion: '1.9.3'
minorVersion: $[counter('minorVersion', 1)] minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)' prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)' buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr' sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com' sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408' dotnetVersion: '6.0.413'
nodeVersion: '16.X'
innoVersion: '6.2.0' innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022' windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04' linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11' macImage: 'macOS-11'
@@ -27,6 +27,10 @@ trigger:
include: include:
- develop - develop
- master - master
paths:
exclude:
- .github
- src/Prowlarr.Api.*/openapi.json
pr: pr:
branches: branches:
@@ -34,8 +38,9 @@ pr:
- develop - develop
paths: paths:
exclude: exclude:
- .github
- src/NzbDrone.Core/Localization/Core - src/NzbDrone.Core/Localization/Core
- src/Prowlarr.API.*/openapi.json - src/Prowlarr.Api.*/openapi.json
stages: stages:
- stage: Setup - stage: Setup
@@ -349,7 +354,7 @@ stages:
includeRootFolder: false includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0 rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2 - task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar displayName: Create freebsd-x64 tar
inputs: inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).freebsd-core-x64.tar.gz' archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar' archiveType: 'tar'
@@ -528,8 +533,8 @@ stages:
testRunTitle: '$(testName) Unit Tests' testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres - job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres Database displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
@@ -565,6 +570,7 @@ stages:
-e POSTGRES_PASSWORD=prowlarr \ -e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \ -e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \ -p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14 postgres:14
displayName: Start postgres displayName: Start postgres
- bash: | - bash: |
@@ -577,7 +583,60 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres Unit Tests' testRunTitle: 'LinuxCore Postgres14 Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres15
displayName: Unit Native LinuxCore with Postgres15 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
artifactName: linux-x64-tests
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: ${{ variables.linuxImage }}
timeoutInMinutes: 10
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: $(artifactName)
targetPath: $(testsFolder)
- bash: find ${TESTSFOLDER} -name "Prowlarr.Test.Dummy" -exec chmod a+x {} \;
displayName: Make Test Dummy Executable
condition: and(succeeded(), ne(variables['osName'], 'Windows'))
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
ls -lR ${TESTSFOLDER}
${TESTSFOLDER}/test.sh Linux Unit Test
displayName: Run Tests
- task: PublishTestResults@2
displayName: Publish Test Results
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'LinuxCore Postgres15 Unit Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
- stage: Integration - stage: Integration
@@ -663,8 +722,8 @@ stages:
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres - job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres Database displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0')) condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables: variables:
@@ -710,6 +769,7 @@ stages:
-e POSTGRES_PASSWORD=prowlarr \ -e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \ -e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \ -p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14 postgres:14
displayName: Start postgres displayName: Start postgres
- bash: | - bash: |
@@ -720,7 +780,70 @@ stages:
inputs: inputs:
testResultsFormat: 'NUnit' testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml' testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres Database Integration Tests' testRunTitle: 'Integration LinuxCore Postgres14 Database Integration Tests'
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres15
displayName: Integration Native LinuxCore with Postgres Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
pattern: 'Prowlarr.*.linux-core-x64.tar.gz'
Prowlarr__Postgres__Host: 'localhost'
Prowlarr__Postgres__Port: '5432'
Prowlarr__Postgres__User: 'prowlarr'
Prowlarr__Postgres__Password: 'prowlarr'
pool:
vmImage: ${{ variables.linuxImage }}
steps:
- task: UseDotNet@2
displayName: 'Install .net core'
inputs:
version: $(dotnetVersion)
- checkout: none
- task: DownloadPipelineArtifact@2
displayName: Download Test Artifact
inputs:
buildType: 'current'
artifactName: 'linux-x64-tests'
targetPath: $(testsFolder)
- task: DownloadPipelineArtifact@2
displayName: Download Build Artifact
inputs:
buildType: 'current'
artifactName: Packages
itemPattern: '**/$(pattern)'
targetPath: $(Build.ArtifactStagingDirectory)
- task: ExtractFiles@1
inputs:
archiveFilePatterns: '$(Build.ArtifactStagingDirectory)/**/$(pattern)'
destinationFolder: '$(Build.ArtifactStagingDirectory)/bin'
displayName: Extract Package
- bash: |
mkdir -p ./bin/
cp -r -v ${BUILD_ARTIFACTSTAGINGDIRECTORY}/bin/Prowlarr/. ./bin/
displayName: Move Package Contents
- bash: |
docker run -d --name=postgres15 \
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:15
displayName: Start postgres
- bash: |
chmod a+x ${TESTSFOLDER}/test.sh
${TESTSFOLDER}/test.sh Linux Integration Test
displayName: Run Integration Tests
- task: PublishTestResults@2
inputs:
testResultsFormat: 'NUnit'
testResultsFiles: '**/TestResult.xml'
testRunTitle: 'Integration LinuxCore Postgres15 Database Integration Tests'
failTaskOnFailedTests: true failTaskOnFailedTests: true
displayName: Publish Test Results displayName: Publish Test Results
@@ -1003,7 +1126,7 @@ stages:
git add . git add .
if git status | grep -q modified if git status | grep -q modified
then then
git commit -am 'Automated API Docs update [skip ci]' git commit -am 'Automated API Docs update'
git push -f --set-upstream origin api-docs git push -f --set-upstream origin api-docs
curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}' curl -X POST -H "Authorization: token ${GITHUBTOKEN}" -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/prowlarr/prowlarr/pulls -d '{"head":"api-docs","base":"develop","title":"Update API docs"}'
else else
+6 -7
View File
@@ -392,22 +392,21 @@ then
fi fi
fi fi
if [ "$FRONTEND" = "YES" ]; if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then then
YarnInstall YarnInstall
RunWebpack
fi fi
if [ "$LINT" = "YES" ]; if [ "$LINT" = "YES" ];
then then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI LintUI
fi fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ]; if [ "$PACKAGES" = "YES" ];
then then
UpdateVersionNumber UpdateVersionNumber
+2 -1
View File
@@ -26,7 +26,8 @@ module.exports = {
globals: { globals: {
expect: false, expect: false,
chai: false, chai: false,
sinon: false sinon: false,
JSX: true
}, },
parserOptions: { parserOptions: {
+4 -4
View File
@@ -4,14 +4,14 @@ module.exports = {
plugins: [ plugins: [
// Stage 1 // Stage 1
'@babel/plugin-proposal-export-default-from', '@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }], ['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }], ['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2 // Stage 2
'@babel/plugin-proposal-export-namespace-from', '@babel/plugin-transform-export-namespace-from',
// Stage 3 // Stage 3
['@babel/plugin-proposal-class-properties', { loose }], ['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import' '@babel/plugin-syntax-dynamic-import'
], ],
env: { env: {
+3 -2
View File
@@ -35,7 +35,7 @@ module.exports = (env) => {
}, },
entry: { entry: {
index: 'index.js' index: 'index.ts'
}, },
resolve: { resolve: {
@@ -96,7 +96,8 @@ module.exports = (env) => {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs', template: 'frontend/src/index.ejs',
filename: 'index.html', filename: 'index.html',
publicPath: '/' publicPath: '/',
inject: false
}), }),
new FileManagerPlugin({ new FileManagerPlugin({
+3 -4
View File
@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme'; import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes'; import AppRoutes from './AppRoutes';
function App({ store, history, hasTranslationsError }) { function App({ store, history }) {
return ( return (
<DocumentTitle title={window.Prowlarr.instanceName}> <DocumentTitle title={window.Prowlarr.instanceName}>
<Provider store={store}> <Provider store={store}>
<ConnectedRouter history={history}> <ConnectedRouter history={history}>
<ApplyTheme> <ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}> <PageConnector>
<AppRoutes app={App} /> <AppRoutes app={App} />
</PageConnector> </PageConnector>
</ApplyTheme> </ApplyTheme>
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
App.propTypes = { App.propTypes = {
store: PropTypes.object.isRequired, store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired, history: PropTypes.object.isRequired
hasTranslationsError: PropTypes.bool.isRequired
}; };
export default App; export default App;
+4 -4
View File
@@ -5,9 +5,9 @@ import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch'; import Switch from 'Components/Router/Switch';
import HistoryConnector from 'History/HistoryConnector'; import HistoryConnector from 'History/HistoryConnector';
import IndexerIndex from 'Indexer/Index/IndexerIndex'; import IndexerIndex from 'Indexer/Index/IndexerIndex';
import StatsConnector from 'Indexer/Stats/StatsConnector'; import IndexerStats from 'Indexer/Stats/IndexerStats';
import SearchIndexConnector from 'Search/SearchIndexConnector'; import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector'; import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector'; import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector'; import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector'; import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
@@ -60,7 +60,7 @@ function AppRoutes(props) {
<Route <Route
path="/indexers/stats" path="/indexers/stats"
component={StatsConnector} component={IndexerStats}
/> />
{/* {/*
@@ -98,7 +98,7 @@ function AppRoutes(props) {
<Route <Route
path="/settings/applications" path="/settings/applications"
component={ApplicationSettingsConnector} component={ApplicationSettings}
/> />
<Route <Route
@@ -1,6 +1,7 @@
.version { .version {
margin: 0 3px; margin: 0 3px;
font-weight: bold; font-weight: bold;
font-family: var(--defaultFontFamily);
} }
.maintenance { .maintenance {
+7 -8
View File
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
@@ -64,12 +65,12 @@ function AppUpdatedModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
Prowlarr Updated {translate('AppUpdated', { appName: 'Prowlarr' })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
<div> <div>
Version <span className={styles.version}>{version}</span> of Prowlarr has been installed, in order to get the latest changes you'll need to reload Prowlarr. <InlineMarkdown data={translate('AppUpdatedVersion', { appName: 'Prowlarr', version })} blockClassName={styles.version} />
</div> </div>
{ {
@@ -77,16 +78,14 @@ function AppUpdatedModalContent(props) {
<div> <div>
{ {
!update.changes && !update.changes &&
<div className={styles.maintenance}> <div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
{translate('MaintenanceRelease')}
</div>
} }
{ {
!!update.changes && !!update.changes &&
<div> <div>
<div className={styles.changes}> <div className={styles.changes}>
What's new? {translate('WhatsNew')}
</div> </div>
<UpdateChanges <UpdateChanges
@@ -113,14 +112,14 @@ function AppUpdatedModalContent(props) {
<Button <Button
onPress={onSeeChangesPress} onPress={onSeeChangesPress}
> >
Recent Changes {translate('RecentChanges')}
</Button> </Button>
<Button <Button
kind={kinds.PRIMARY} kind={kinds.PRIMARY}
onPress={onModalClose} onPress={onModalClose}
> >
Reload {translate('Reload')}
</Button> </Button>
</ModalFooter> </ModalFooter>
</ModalContent> </ModalContent>
+2 -2
View File
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody> <ModalBody>
<div> <div>
{translate('ConnectionLostMessage')} {translate('ConnectionLostToBackend', { appName: 'Prowlarr' })}
</div> </div>
<div className={styles.automatic}> <div className={styles.automatic}>
{translate('ConnectionLostAutomaticMessage')} {translate('ConnectionLostReconnect', { appName: 'Prowlarr' })}
</div> </div>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
+15 -1
View File
@@ -1,5 +1,13 @@
import IndexerAppState, { IndexerIndexAppState } from './IndexerAppState'; import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import IndexerAppState, {
IndexerHistoryAppState,
IndexerIndexAppState,
IndexerStatusAppState,
} from './IndexerAppState';
import IndexerStatsAppState from './IndexerStatsAppState';
import SettingsAppState from './SettingsAppState'; import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState'; import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption { interface FilterBuilderPropOption {
@@ -35,9 +43,15 @@ export interface CustomFilter {
} }
interface AppState { interface AppState {
commands: CommandAppState;
history: HistoryAppState;
indexerHistory: IndexerHistoryAppState;
indexerIndex: IndexerIndexAppState; indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexerStatus: IndexerStatusAppState;
indexers: IndexerAppState; indexers: IndexerAppState;
settings: SettingsAppState; settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState; tags: TagsAppState;
} }
@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;
+10
View File
@@ -0,0 +1,10 @@
import AppSectionState from 'App/State/AppSectionState';
import Column from 'Components/Table/Column';
import History from 'typings/History';
interface HistoryAppState extends AppSectionState<History> {
pageSize: number;
columns: Column[];
}
export default HistoryAppState;
+9 -2
View File
@@ -1,6 +1,7 @@
import Column from 'Components/Table/Column'; import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection'; import SortDirection from 'Helpers/Props/SortDirection';
import Indexer from 'Indexer/Indexer'; import Indexer, { IndexerStatus } from 'Indexer/Indexer';
import History from 'typings/History';
import AppSectionState, { import AppSectionState, {
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState, AppSectionSaveState,
@@ -28,6 +29,12 @@ export interface IndexerIndexAppState {
interface IndexerAppState interface IndexerAppState
extends AppSectionState<Indexer>, extends AppSectionState<Indexer>,
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState {} AppSectionSaveState {
itemMap: Record<number, number>;
}
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
export type IndexerHistoryAppState = AppSectionState<History>;
export default IndexerAppState; export default IndexerAppState;
@@ -0,0 +1,13 @@
import { AppSectionItemState } from 'App/State/AppSectionState';
import { Filter, FilterBuilderProp } from 'App/State/AppState';
import Indexer from 'Indexer/Indexer';
import { IndexerStats } from 'typings/IndexerStats';
export interface IndexerStatsAppState
extends AppSectionItemState<IndexerStats> {
filterBuilderProps: FilterBuilderProp<Indexer>[];
selectedFilterKey: string;
filters: Filter[];
}
export default IndexerStatsAppState;
+10
View File
@@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
import Release from 'typings/Release';
interface ReleaseAppState
extends AppSectionState<Release>,
AppSectionDeleteState {}
export default ReleaseAppState;
+12 -3
View File
@@ -1,5 +1,6 @@
import AppSectionState, { import AppSectionState, {
AppSectionDeleteState, AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState, AppSectionSaveState,
} from 'App/State/AppSectionState'; } from 'App/State/AppSectionState';
import Application from 'typings/Application'; import Application from 'typings/Application';
@@ -7,11 +8,18 @@ import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification'; import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings'; import { UiSettings } from 'typings/UiSettings';
export interface ApplicationAppState export interface AppProfileAppState
extends AppSectionState<Application>, extends AppSectionState<Application>,
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState {} AppSectionSaveState {}
export interface ApplicationAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
export interface DownloadClientAppState export interface DownloadClientAppState
extends AppSectionState<DownloadClient>, extends AppSectionState<DownloadClient>,
AppSectionDeleteState, AppSectionDeleteState,
@@ -21,13 +29,14 @@ export interface NotificationAppState
extends AppSectionState<Notification>, extends AppSectionState<Notification>,
AppSectionDeleteState {} AppSectionDeleteState {}
export type UiSettingsAppState = AppSectionState<UiSettings>; export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState { interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState; applications: ApplicationAppState;
downloadClients: DownloadClientAppState; downloadClients: DownloadClientAppState;
notifications: NotificationAppState; notifications: NotificationAppState;
uiSettings: UiSettingsAppState; ui: UiSettingsAppState;
} }
export default SettingsAppState; export default SettingsAppState;
+10
View File
@@ -0,0 +1,10 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
interface SystemAppState {
status: SystemStatusAppState;
}
export default SystemAppState;
+17 -1
View File
@@ -1,12 +1,28 @@
import ModelBase from 'App/ModelBase'; import ModelBase from 'App/ModelBase';
import AppSectionState, { import AppSectionState, {
AppSectionDeleteState, AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState'; } from 'App/State/AppSectionState';
export interface Tag extends ModelBase { export interface Tag extends ModelBase {
label: string; label: string;
} }
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {} export interface TagDetail extends ModelBase {
label: string;
applicationIds: number[];
indexerIds: number[];
indexerProxyIds: number[];
notificationIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
export default TagsAppState; export default TagsAppState;
+37
View File
@@ -0,0 +1,37 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
seriesId?: number;
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;
+10 -1
View File
@@ -2,6 +2,7 @@ import Chart from 'chart.js/auto';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import { defaultFontFamily } from 'Styles/Variables/fonts';
function getColors(kind) { function getColors(kind) {
@@ -39,7 +40,15 @@ class BarChart extends Component {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: this.props.title align: 'start',
text: this.props.title,
padding: {
bottom: 30
},
font: {
size: 14,
family: defaultFontFamily
}
}, },
legend: { legend: {
display: this.props.legend display: this.props.legend
+10 -1
View File
@@ -1,6 +1,7 @@
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { defaultFontFamily } from 'Styles/Variables/fonts';
function getColors(kind) { function getColors(kind) {
@@ -22,7 +23,15 @@ class DoughnutChart extends Component {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: this.props.title align: 'start',
text: this.props.title,
padding: {
bottom: 30
},
font: {
size: 14,
family: defaultFontFamily
}
}, },
legend: { legend: {
position: 'bottom' position: 'bottom'
@@ -1,6 +1,7 @@
import Chart from 'chart.js/auto'; import Chart from 'chart.js/auto';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { defaultFontFamily } from 'Styles/Variables/fonts';
function getColors(index) { function getColors(index) {
@@ -36,7 +37,15 @@ class StackedBarChart extends Component {
plugins: { plugins: {
title: { title: {
display: true, display: true,
text: this.props.title align: 'start',
text: this.props.title,
padding: {
bottom: 30
},
font: {
size: 14,
family: defaultFontFamily
}
} }
} }
}, },
@@ -20,12 +20,12 @@ import styles from './FileBrowserModalContent.css';
const columns = [ const columns = [
{ {
name: 'type', name: 'type',
label: translate('Type'), label: () => translate('Type'),
isVisible: true isVisible: true
}, },
{ {
name: 'name', name: 'name',
label: translate('Name'), label: () => translate('Name'),
isVisible: true isVisible: true
} }
]; ];
@@ -198,9 +198,11 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp; const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => { const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return { return {
key: availablePropFilter.name, key: name,
value: availablePropFilter.label value: typeof label === 'function' ? label() : label
}; };
}).sort((a, b) => a.value.localeCompare(b.value)); }).sort((a, b) => a.value.localeCompare(b.value));
@@ -3,9 +3,24 @@ import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue'; import FilterBuilderRowValue from './FilterBuilderRowValue';
const privacyTypes = [ const privacyTypes = [
{ id: 'public', name: translate('Public') }, {
{ id: 'private', name: translate('Private') }, id: 'public',
{ id: 'semiPrivate', name: translate('SemiPrivate') } get name() {
return translate('Public');
}
},
{
id: 'private',
get name() {
return translate('Private');
}
},
{
id: 'semiPrivate',
get name() {
return translate('SemiPrivate');
}
}
]; ];
function PrivacyFilterBuilderRowValue(props) { function PrivacyFilterBuilderRowValue(props) {
@@ -9,6 +9,10 @@
&:hover { &:hover {
background-color: var(--inputHoverBackgroundColor); background-color: var(--inputHoverBackgroundColor);
} }
&.isDisabled {
cursor: not-allowed;
}
} }
.optionCheck { .optionCheck {
@@ -270,6 +270,7 @@ FormInputGroup.propTypes = {
helpTexts: PropTypes.arrayOf(PropTypes.string), helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string, helpTextWarning: PropTypes.string,
helpLink: PropTypes.string, helpLink: PropTypes.string,
autoFocus: PropTypes.bool,
includeNoChange: PropTypes.bool, includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool, includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object, selectedValueOptions: PropTypes.object,
@@ -33,7 +33,7 @@ function HintedSelectInputOption(props) {
isMobile && styles.isMobile isMobile && styles.isMobile
)} )}
> >
<div>{value}</div> <div>{typeof value === 'function' ? value() : value}</div>
{ {
hint != null && hint != null &&
@@ -48,7 +48,7 @@ function HintedSelectInputOption(props) {
HintedSelectInputOption.propTypes = { HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
hint: PropTypes.node, hint: PropTypes.node,
depth: PropTypes.number, depth: PropTypes.number,
isSelected: PropTypes.bool.isRequired, isSelected: PropTypes.bool.isRequired,
@@ -3,13 +3,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import titleCase from 'Utilities/String/titleCase'; import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput'; import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() { function createMapStateToProps() {
return createSelector( return createSelector(
(state, { value }) => value, (state, { value }) => value,
(state) => state.indexers, createSortedSectionSelector('indexers', sortByName),
(value, indexers) => { (value, indexers) => {
const values = []; const values = [];
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val })); const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));
+1 -1
View File
@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
const { value } = this.props; const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) { if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({ this.setState({
value: value == null ? '' : value.toString() value: value == null ? '' : value.toString()
}); });
@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
} }
PathInputConnector.propTypes = { PathInputConnector.propTypes = {
...PathInput.props,
includeFiles: PropTypes.bool.isRequired, includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired, dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired dispatchClearPaths: PropTypes.func.isRequired
+2 -2
View File
@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key} value={key}
{...otherOptionProps} {...otherOptionProps}
> >
{optionValue} {typeof optionValue === 'function' ? optionValue() : optionValue}
</option> </option>
); );
}) })
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string, className: PropTypes.string,
disabledClassName: PropTypes.string, disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired, values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool, isDisabled: PropTypes.bool,
hasError: PropTypes.bool, hasError: PropTypes.bool,
+2 -2
View File
@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return ( return (
<span <span
className={containerClassName} className={containerClassName}
title={title} title={typeof title === 'function' ? title() : title}
> >
{icon} {icon}
</span> </span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired, name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired, kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired, size: PropTypes.number.isRequired,
title: PropTypes.string, title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired fixedWidth: PropTypes.bool.isRequired
}; };
@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() { render() {
const { const {
kind,
isSpinning, isSpinning,
error, error,
children, children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError; const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK; let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS; let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) { if (hasWarning) {
iconName = icons.WARNING; iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return ( return (
<SpinnerButton <SpinnerButton
kind={kind}
isSpinning={isSpinning} isSpinning={isSpinning}
{...otherProps} {...otherProps}
> >
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
} }
SpinnerErrorButton.propTypes = { SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired, isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object, error: PropTypes.object,
children: PropTypes.node.isRequired children: PropTypes.node.isRequired
@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
render() { render() {
const { const {
className, className,
data data,
blockClassName
} = this.props; } = this.props;
// For now only replace links // For now only replace links or code blocks (not both)
const markdownBlocks = []; const markdownBlocks = [];
if (data) { if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g); const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0; let endIndex = 0;
let match = null; let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) { if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex)); markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
} }
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>); markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
endIndex = match.index + match[0].length; endIndex = match.index + match[0].length;
} }
if (endIndex !== data.length) { if (endIndex !== data.length && markdownBlocks.length > 0) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex)); markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
} }
const codeRegex = RegExp(/(?=`)`(?!`)[^`]*(?=`)`(?!`)/g);
endIndex = 0;
match = null;
let matchedCode = false;
while ((match = codeRegex.exec(data)) !== null) {
matchedCode = true;
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<code key={`code-${match.index}`} className={blockClassName ?? null}>{match[0].substring(1, match[0].length - 1)}</code>);
endIndex = match.index + match[0].length;
}
if (endIndex !== data.length && markdownBlocks.length > 0 && matchedCode) {
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
}
if (markdownBlocks.length === 0) {
markdownBlocks.push(data);
}
} }
return <span className={className}>{markdownBlocks}</span>; return <span className={className}>{markdownBlocks}</span>;
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = { InlineMarkdown.propTypes = {
className: PropTypes.string, className: PropTypes.string,
data: PropTypes.string data: PropTypes.string,
blockClassName: PropTypes.string
}; };
export default InlineMarkdown; export default InlineMarkdown;
@@ -33,7 +33,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey} selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect} onPress={onFilterSelect}
> >
{filter.label} {typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem> </FilterMenuItem>
); );
}) })
+4 -4
View File
@@ -7,7 +7,7 @@ function ErrorPage(props) {
const { const {
version, version,
isLocalStorageSupported, isLocalStorageSupported,
hasTranslationsError, translationsError,
indexersError, indexersError,
indexerStatusError, indexerStatusError,
indexerCategoriesError, indexerCategoriesError,
@@ -22,8 +22,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) { if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.'; errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (hasTranslationsError) { } else if (translationsError) {
errorMessage = 'Failed to load translations from API'; errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (indexersError) { } else if (indexersError) {
errorMessage = getErrorMessage(indexersError, 'Failed to load indexers from API'); errorMessage = getErrorMessage(indexersError, 'Failed to load indexers from API');
} else if (indexerStatusError) { } else if (indexerStatusError) {
@@ -58,7 +58,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = { ErrorPage.propTypes = {
version: PropTypes.string.isRequired, version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired, isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired, translationsError: PropTypes.object,
indexersError: PropTypes.object, indexersError: PropTypes.object,
indexerStatusError: PropTypes.object, indexerStatusError: PropTypes.object,
indexerCategoriesError: PropTypes.object, indexerCategoriesError: PropTypes.object,
+20 -10
View File
@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions'; import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions'; import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions'; import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions'; import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
@@ -54,6 +54,7 @@ const selectIsPopulated = createSelector(
(state) => state.indexerStatus.isPopulated, (state) => state.indexerStatus.isPopulated,
(state) => state.settings.indexerCategories.isPopulated, (state) => state.settings.indexerCategories.isPopulated,
(state) => state.system.status.isPopulated, (state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
( (
customFiltersIsPopulated, customFiltersIsPopulated,
tagsIsPopulated, tagsIsPopulated,
@@ -63,7 +64,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated, indexersIsPopulated,
indexerStatusIsPopulated, indexerStatusIsPopulated,
indexerCategoriesIsPopulated, indexerCategoriesIsPopulated,
systemStatusIsPopulated systemStatusIsPopulated,
translationsIsPopulated
) => { ) => {
return ( return (
customFiltersIsPopulated && customFiltersIsPopulated &&
@@ -74,7 +76,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated && indexersIsPopulated &&
indexerStatusIsPopulated && indexerStatusIsPopulated &&
indexerCategoriesIsPopulated && indexerCategoriesIsPopulated &&
systemStatusIsPopulated systemStatusIsPopulated &&
translationsIsPopulated
); );
} }
); );
@@ -89,6 +92,7 @@ const selectErrors = createSelector(
(state) => state.indexerStatus.error, (state) => state.indexerStatus.error,
(state) => state.settings.indexerCategories.error, (state) => state.settings.indexerCategories.error,
(state) => state.system.status.error, (state) => state.system.status.error,
(state) => state.app.translations.error,
( (
customFiltersError, customFiltersError,
tagsError, tagsError,
@@ -98,7 +102,8 @@ const selectErrors = createSelector(
indexersError, indexersError,
indexerStatusError, indexerStatusError,
indexerCategoriesError, indexerCategoriesError,
systemStatusError systemStatusError,
translationsError
) => { ) => {
const hasError = !!( const hasError = !!(
customFiltersError || customFiltersError ||
@@ -109,7 +114,8 @@ const selectErrors = createSelector(
indexersError || indexersError ||
indexerStatusError || indexerStatusError ||
indexerCategoriesError || indexerCategoriesError ||
systemStatusError systemStatusError ||
translationsError
); );
return { return {
@@ -122,7 +128,8 @@ const selectErrors = createSelector(
indexersError, indexersError,
indexerStatusError, indexerStatusError,
indexerCategoriesError, indexerCategoriesError,
systemStatusError systemStatusError,
translationsError
}; };
} }
); );
@@ -184,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() { dispatchFetchStatus() {
dispatch(fetchStatus()); dispatch(fetchStatus());
}, },
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) { onResize(dimensions) {
dispatch(saveDimensions(dimensions)); dispatch(saveDimensions(dimensions));
}, },
@@ -217,6 +227,7 @@ class PageConnector extends Component {
this.props.dispatchFetchUISettings(); this.props.dispatchFetchUISettings();
this.props.dispatchFetchGeneralSettings(); this.props.dispatchFetchGeneralSettings();
this.props.dispatchFetchStatus(); this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
} }
} }
@@ -232,7 +243,6 @@ class PageConnector extends Component {
render() { render() {
const { const {
hasTranslationsError,
isPopulated, isPopulated,
hasError, hasError,
dispatchFetchTags, dispatchFetchTags,
@@ -243,15 +253,15 @@ class PageConnector extends Component {
dispatchFetchUISettings, dispatchFetchUISettings,
dispatchFetchGeneralSettings, dispatchFetchGeneralSettings,
dispatchFetchStatus, dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps ...otherProps
} = this.props; } = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) { if (hasError || !this.state.isLocalStorageSupported) {
return ( return (
<ErrorPage <ErrorPage
{...this.state} {...this.state}
{...otherProps} {...otherProps}
hasTranslationsError={hasTranslationsError}
/> />
); );
} }
@@ -272,7 +282,6 @@ class PageConnector extends Component {
} }
PageConnector.propTypes = { PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired, isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired, hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired, isSidebarVisible: PropTypes.bool.isRequired,
@@ -285,6 +294,7 @@ PageConnector.propTypes = {
dispatchFetchUISettings: PropTypes.func.isRequired, dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired, dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired, dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired onSidebarVisibleChange: PropTypes.func.isRequired
}; };
@@ -20,12 +20,12 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [ const links = [
{ {
iconName: icons.MOVIE_CONTINUING, iconName: icons.MOVIE_CONTINUING,
title: translate('Indexers'), title: () => translate('Indexers'),
to: '/', to: '/',
alias: '/indexers', alias: '/indexers',
children: [ children: [
{ {
title: translate('Stats'), title: () => translate('Stats'),
to: '/indexers/stats' to: '/indexers/stats'
} }
] ]
@@ -33,47 +33,47 @@ const links = [
{ {
iconName: icons.SEARCH, iconName: icons.SEARCH,
title: translate('Search'), title: () => translate('Search'),
to: '/search' to: '/search'
}, },
{ {
iconName: icons.ACTIVITY, iconName: icons.ACTIVITY,
title: translate('History'), title: () => translate('History'),
to: '/history' to: '/history'
}, },
{ {
iconName: icons.SETTINGS, iconName: icons.SETTINGS,
title: translate('Settings'), title: () => translate('Settings'),
to: '/settings', to: '/settings',
children: [ children: [
{ {
title: translate('Indexers'), title: () => translate('Indexers'),
to: '/settings/indexers' to: '/settings/indexers'
}, },
{ {
title: translate('Apps'), title: () => translate('Apps'),
to: '/settings/applications' to: '/settings/applications'
}, },
{ {
title: translate('DownloadClients'), title: () => translate('DownloadClients'),
to: '/settings/downloadclients' to: '/settings/downloadclients'
}, },
{ {
title: translate('Connect'), title: () => translate('Connect'),
to: '/settings/connect' to: '/settings/connect'
}, },
{ {
title: translate('Tags'), title: () => translate('Tags'),
to: '/settings/tags' to: '/settings/tags'
}, },
{ {
title: translate('General'), title: () => translate('General'),
to: '/settings/general' to: '/settings/general'
}, },
{ {
title: translate('UI'), title: () => translate('UI'),
to: '/settings/ui' to: '/settings/ui'
} }
] ]
@@ -81,32 +81,32 @@ const links = [
{ {
iconName: icons.SYSTEM, iconName: icons.SYSTEM,
title: translate('System'), title: () => translate('System'),
to: '/system/status', to: '/system/status',
children: [ children: [
{ {
title: translate('Status'), title: () => translate('Status'),
to: '/system/status', to: '/system/status',
statusComponent: HealthStatusConnector statusComponent: HealthStatusConnector
}, },
{ {
title: translate('Tasks'), title: () => translate('Tasks'),
to: '/system/tasks' to: '/system/tasks'
}, },
{ {
title: translate('Backup'), title: () => translate('Backup'),
to: '/system/backup' to: '/system/backup'
}, },
{ {
title: translate('Updates'), title: () => translate('Updates'),
to: '/system/updates' to: '/system/updates'
}, },
{ {
title: translate('Events'), title: () => translate('Events'),
to: '/system/events' to: '/system/events'
}, },
{ {
title: translate('LogFiles'), title: () => translate('LogFiles'),
to: '/system/logs/files' to: '/system/logs/files'
} }
] ]
@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
} }
<span className={isChildItem ? styles.noIcon : null}> <span className={isChildItem ? styles.noIcon : null}>
{title} {typeof title === 'function' ? title() : title}
</span> </span>
{ {
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = { PageSidebarItem.propTypes = {
iconName: PropTypes.object, iconName: PropTypes.object,
title: PropTypes.string.isRequired, title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired, to: PropTypes.string.isRequired,
isActive: PropTypes.bool, isActive: PropTypes.bool,
isActiveParent: PropTypes.bool, isActiveParent: PropTypes.bool,
@@ -1,58 +1,66 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React, { PureComponent } from 'react'; import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDateTime from 'Utilities/Date/formatDateTime'; import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate'; import getRelativeDate from 'Utilities/Date/getRelativeDate';
import TableRowCell from './TableRowCell'; import TableRowCell from './TableRowCell';
import styles from './RelativeDateCell.css'; import styles from './RelativeDateCell.css';
class RelativeDateCell extends PureComponent { function createRelativeDateCellSelector() {
return createSelector(createUISettingsSelector(), (uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
});
}
function RelativeDateCell(props) {
// //
// Render // Render
render() { const {
const { className,
className, date,
date, includeSeconds,
includeSeconds, component: Component,
showRelativeDates, dispatch,
shortDateFormat, ...otherProps
longDateFormat, } = props;
timeFormat,
component: Component,
dispatch,
...otherProps
} = this.props;
if (!date) { const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
return ( useSelector(createRelativeDateCellSelector());
<Component
className={className}
{...otherProps}
/>
);
}
return ( if (!date) {
<Component return <Component className={className} {...otherProps} />;
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
</Component>
);
} }
return (
<Component
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, {
includeSeconds,
includeRelativeDay: !showRelativeDates
})}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, {
timeFormat,
includeSeconds,
timeForToday: true
})}
</Component>
);
} }
RelativeDateCell.propTypes = { RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired, className: PropTypes.string.isRequired,
date: PropTypes.string, date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired, includeSeconds: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
component: PropTypes.elementType, component: PropTypes.elementType,
dispatch: PropTypes.func dispatch: PropTypes.func
}; };
@@ -1,7 +1,7 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'markAsFailedButton': string; 'cell': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
function TableRowCellButton({ className, ...otherProps }) {
return (
<Link
className={className}
component={TableRowCell}
{...otherProps}
/>
);
}
TableRowCellButton.propTypes = {
className: PropTypes.string.isRequired
};
TableRowCellButton.defaultProps = {
className: styles.cell
};
export default TableRowCellButton;
@@ -0,0 +1,19 @@
import React, { ReactNode } from 'react';
import Link, { LinkProps } from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
interface TableRowCellButtonProps extends LinkProps {
className?: string;
children: ReactNode;
}
function TableRowCellButton(props: TableRowCellButtonProps) {
const { className = styles.cell, ...otherProps } = props;
return (
<Link className={className} component={TableRowCell} {...otherProps} />
);
}
export default TableRowCellButton;
+3 -1
View File
@@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
type PropertyFunction<T> = () => T;
interface Column { interface Column {
name: string; name: string;
label: string | React.ReactNode; label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string; columnLabel?: string;
isSortable?: boolean; isSortable?: boolean;
isVisible: boolean; isVisible: boolean;
+1 -1
View File
@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)} {...getTableHeaderCellProps(otherProps)}
{...column} {...column}
> >
{column.label} {typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell> </TableHeaderCell>
); );
}) })
@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const { const {
className, className,
name, name,
label,
columnLabel, columnLabel,
isSortable, isSortable,
isVisible, isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps} {...otherProps}
component="th" component="th"
className={className} className={className}
title={columnLabel} label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress} onPress={this.onPress}
> >
{children} {children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = { TableHeaderCell.propTypes = {
className: PropTypes.string, className: PropTypes.string,
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
columnLabel: PropTypes.string, label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool, isSortable: PropTypes.bool,
isVisible: PropTypes.bool, isVisible: PropTypes.bool,
isModifiable: PropTypes.bool, isModifiable: PropTypes.bool,
@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false} isDisabled={isModifiable === false}
onChange={onVisibleChange} onChange={onVisibleChange}
/> />
{label} {typeof label === 'function' ? label() : label}
</label> </label>
{ {
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = { TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn <TableOptionsColumn
name={name} name={name}
label={label} label={typeof label === 'function' ? label() : label}
isVisible={isVisible} isVisible={isVisible}
isModifiable={isModifiable} isModifiable={isModifiable}
index={index} index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = { TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired, label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired, isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired, isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired, index: PropTypes.number.isRequired,
@@ -192,7 +192,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumnDragSource <TableOptionsColumnDragSource
key={name} key={name}
name={name} name={name}
label={label || columnLabel} label={columnLabel || label}
isVisible={isVisible} isVisible={isVisible}
isModifiable={true} isModifiable={true}
index={index} index={index}
@@ -210,7 +210,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumn <TableOptionsColumn
key={name} key={name}
name={name} name={name}
label={label || columnLabel} label={columnLabel || label}
isVisible={isVisible} isVisible={isVisible}
index={index} index={index}
isModifiable={false} isModifiable={false}
+21 -7
View File
@@ -6,37 +6,51 @@ import translate from 'Utilities/String/translate';
export const shortcuts = { export const shortcuts = {
OPEN_KEYBOARD_SHORTCUTS_MODAL: { OPEN_KEYBOARD_SHORTCUTS_MODAL: {
key: '?', key: '?',
name: translate('OpenThisModal') get name() {
return translate('OpenThisModal');
}
}, },
CLOSE_MODAL: { CLOSE_MODAL: {
key: 'Esc', key: 'Esc',
name: translate('CloseCurrentModal') get name() {
return translate('CloseCurrentModal');
}
}, },
ACCEPT_CONFIRM_MODAL: { ACCEPT_CONFIRM_MODAL: {
key: 'Enter', key: 'Enter',
name: translate('AcceptConfirmationModal') get name() {
return translate('AcceptConfirmationModal');
}
}, },
MOVIE_SEARCH_INPUT: { MOVIE_SEARCH_INPUT: {
key: 's', key: 's',
name: translate('FocusSearchBox') get name() {
return translate('FocusSearchBox');
}
}, },
SAVE_SETTINGS: { SAVE_SETTINGS: {
key: 'mod+s', key: 'mod+s',
name: translate('SaveSettings') get name() {
return translate('SaveSettings');
}
}, },
SCROLL_TOP: { SCROLL_TOP: {
key: 'mod+home', key: 'mod+home',
name: translate('MovieIndexScrollTop') get name() {
return translate('MovieIndexScrollTop');
}
}, },
SCROLL_BOTTOM: { SCROLL_BOTTOM: {
key: 'mod+end', key: 'mod+end',
name: translate('MovieIndexScrollBottom') get name() {
return translate('MovieIndexScrollBottom');
}
} }
}; };
@@ -11,7 +11,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { inputTypes, kinds } from 'Helpers/Props'; import { inputTypes, kinds } from 'Helpers/Props';
import { authenticationMethodOptions, authenticationRequiredOptions, authenticationRequiredWarning } from 'Settings/General/SecuritySettings'; import { authenticationMethodOptions, authenticationRequiredOptions } from 'Settings/General/SecuritySettings';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './AuthenticationRequiredModalContent.css'; import styles from './AuthenticationRequiredModalContent.css';
@@ -63,71 +63,63 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert} className={styles.authRequiredAlert}
kind={kinds.WARNING} kind={kinds.WARNING}
> >
{authenticationRequiredWarning} {translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
</Alert> </Alert>
{ {
isPopulated && !error ? isPopulated && !error ?
<div> <div>
<FormGroup> <FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel> <FormLabel>{translate('AuthenticationMethod')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="authenticationMethod" name="authenticationMethod"
values={authenticationMethodOptions} values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')} helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
helpTextWarning={authenticationMethod.value === 'none' ? translate('AuthenticationMethodHelpTextWarning') : undefined}
helpLink="https://wiki.servarr.com/prowlarr/faq#forced-authentication"
onChange={onInputChange} onChange={onInputChange}
{...authenticationMethod} {...authenticationMethod}
/> />
</FormGroup> </FormGroup>
{ <FormGroup>
authenticationEnabled ? <FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.SELECT} type={inputTypes.SELECT}
name="authenticationRequired" name="authenticationRequired"
values={authenticationRequiredOptions} values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')} helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange} onChange={onInputChange}
{...authenticationRequired} {...authenticationRequired}
/> />
</FormGroup> : </FormGroup>
null
}
{ <FormGroup>
authenticationEnabled ? <FormLabel>{translate('Username')}</FormLabel>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.TEXT} type={inputTypes.TEXT}
name="username" name="username"
onChange={onInputChange} onChange={onInputChange}
{...username} helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
/> {...username}
</FormGroup> : />
null </FormGroup>
}
{ <FormGroup>
authenticationEnabled ? <FormLabel>{translate('Password')}</FormLabel>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup <FormInputGroup
type={inputTypes.PASSWORD} type={inputTypes.PASSWORD}
name="password" name="password"
onChange={onInputChange} onChange={onInputChange}
{...password} helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
/> {...password}
</FormGroup> : />
null </FormGroup>
}
</div> : </div> :
null null
} }
+2
View File
@@ -75,6 +75,7 @@ import {
faListCheck as fasListCheck, faListCheck as fasListCheck,
faLocationArrow as fasLocationArrow, faLocationArrow as fasLocationArrow,
faLock as fasLock, faLock as fasLock,
faMagnet as fasMagnet,
faMedkit as fasMedkit, faMedkit as fasMedkit,
faMinus as fasMinus, faMinus as fasMinus,
faMusic as fasMusic, faMusic as fasMusic,
@@ -181,6 +182,7 @@ export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard; export const KEYBOARD = farKeyboard;
export const LOCK = fasLock; export const LOCK = fasLock;
export const LOGOUT = fasSignOutAlt; export const LOGOUT = fasSignOutAlt;
export const MAGNET = fasMagnet;
export const MANAGE = fasListCheck; export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice; export const MEDIA_INFO = farFileInvoice;
export const MISSING = fasExclamationTriangle; export const MISSING = fasExclamationTriangle;
+2
View File
@@ -10,6 +10,7 @@ export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect'; export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect'; export const CATEGORY_SELECT = 'newznabCategorySelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect'; export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const FLOAT = 'float';
export const NUMBER = 'number'; export const NUMBER = 'number';
export const OAUTH = 'oauth'; export const OAUTH = 'oauth';
export const PASSWORD = 'password'; export const PASSWORD = 'password';
@@ -35,6 +36,7 @@ export const all = [
INFO, INFO,
MOVIE_MONITORED_SELECT, MOVIE_MONITORED_SELECT,
CATEGORY_SELECT, CATEGORY_SELECT,
FLOAT,
NUMBER, NUMBER,
OAUTH, OAUTH,
PASSWORD, PASSWORD,
@@ -1,5 +0,0 @@
.markAsFailedButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}
@@ -1,16 +1,13 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal'; import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody'; import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent'; import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter'; import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader'; import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails'; import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) { function getHeaderTitle(eventType) {
switch (eventType) { switch (eventType) {
@@ -33,10 +30,8 @@ function HistoryDetailsModal(props) {
eventType, eventType,
indexer, indexer,
data, data,
isMarkingAsFailed,
shortDateFormat, shortDateFormat,
timeFormat, timeFormat,
onMarkAsFailedPress,
onModalClose onModalClose
} = props; } = props;
@@ -61,18 +56,6 @@ function HistoryDetailsModal(props) {
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter>
{
eventType === 'grabbed' &&
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
Mark as Failed
</SpinnerButton>
}
<Button <Button
onPress={onModalClose} onPress={onModalClose}
> >
@@ -89,10 +72,8 @@ HistoryDetailsModal.propTypes = {
eventType: PropTypes.string.isRequired, eventType: PropTypes.string.isRequired,
indexer: PropTypes.object.isRequired, indexer: PropTypes.object.isRequired,
data: PropTypes.object.isRequired, data: PropTypes.object.isRequired,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired, shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired, timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired onModalClose: PropTypes.func.isRequired
}; };
+1
View File
@@ -62,6 +62,7 @@ class HistoryOptions extends Component {
<FormInputGroup <FormInputGroup
type={inputTypes.NUMBER} type={inputTypes.NUMBER}
name="historyCleanupDays" name="historyCleanupDays"
unit={translate('days')}
value={historyCleanupDays} value={historyCleanupDays}
helpText={translate('HistoryCleanupDaysHelpText')} helpText={translate('HistoryCleanupDaysHelpText')}
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')} helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}
+69 -14
View File
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell'; import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow'; import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props'; import { icons, kinds } from 'Helpers/Props';
@@ -14,24 +14,79 @@ import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryRowParameter from './HistoryRowParameter'; import HistoryRowParameter from './HistoryRowParameter';
import styles from './HistoryRow.css'; import styles from './HistoryRow.css';
const historyParameters = [ export const historyParameters = [
{ key: historyDataTypes.IMDB_ID, title: 'IMDb' }, { key: historyDataTypes.IMDB_ID, title: 'IMDb' },
{ key: historyDataTypes.TMDB_ID, title: 'TMDb' }, { key: historyDataTypes.TMDB_ID, title: 'TMDb' },
{ key: historyDataTypes.TVDB_ID, title: 'TVDb' }, { key: historyDataTypes.TVDB_ID, title: 'TVDb' },
{ key: historyDataTypes.TRAKT_ID, title: 'Trakt' }, { key: historyDataTypes.TRAKT_ID, title: 'Trakt' },
{ key: historyDataTypes.R_ID, title: 'TvRage' }, { key: historyDataTypes.R_ID, title: 'TvRage' },
{ key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' }, { key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' },
{ key: historyDataTypes.SEASON, title: translate('Season') }, {
{ key: historyDataTypes.EPISODE, title: translate('Episode') }, key: historyDataTypes.SEASON,
{ key: historyDataTypes.ARTIST, title: translate('Artist') }, get title() {
{ key: historyDataTypes.ALBUM, title: translate('Album') }, return translate('Season');
{ key: historyDataTypes.LABEL, title: translate('Label') }, }
{ key: historyDataTypes.TRACK, title: translate('Track') }, },
{ key: historyDataTypes.YEAR, title: translate('Year') }, {
{ key: historyDataTypes.GENRE, title: translate('Genre') }, key: historyDataTypes.EPISODE,
{ key: historyDataTypes.AUTHOR, title: translate('Author') }, get title() {
{ key: historyDataTypes.TITLE, title: translate('Title') }, return translate('Episode');
{ key: historyDataTypes.PUBLISHER, title: translate('Publisher') } }
},
{
key: historyDataTypes.ARTIST,
get title() {
return translate('Artist');
}
},
{
key: historyDataTypes.ALBUM,
get title() {
return translate('Album');
}
},
{
key: historyDataTypes.LABEL,
get title() {
return translate('Label');
}
},
{
key: historyDataTypes.TRACK,
get title() {
return translate('Track');
}
},
{
key: historyDataTypes.YEAR,
get title() {
return translate('Year');
}
},
{
key: historyDataTypes.GENRE,
get title() {
return translate('Genre');
}
},
{
key: historyDataTypes.AUTHOR,
get title() {
return translate('Author');
}
},
{
key: historyDataTypes.TITLE,
get title() {
return translate('Title');
}
},
{
key: historyDataTypes.PUBLISHER,
get title() {
return translate('Publisher');
}
}
]; ];
class HistoryRow extends Component { class HistoryRow extends Component {
@@ -298,7 +353,7 @@ class HistoryRow extends Component {
if (name === 'date') { if (name === 'date') {
return ( return (
<RelativeDateCellConnector <RelativeDateCell
key={name} key={name}
date={date} date={date}
className={styles.date} className={styles.date}
@@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import styles from './HistoryRowParameter.css';
class HistoryRowParameter extends Component {
//
// Render
render() {
const {
title,
value
} = this.props;
return (
<div className={styles.parameter}>
<div className={styles.info}>
<span>
{
title
}
</span>
</div>
<div
className={styles.value}
>
{
value
}
</div>
</div>
);
}
}
HistoryRowParameter.propTypes = {
title: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
};
export default HistoryRowParameter;
@@ -0,0 +1,44 @@
import React from 'react';
import Link from 'Components/Link/Link';
import styles from './HistoryRowParameter.css';
interface HistoryRowParameterProps {
title: string;
value: string;
}
function HistoryRowParameter(props: HistoryRowParameterProps) {
const { title, value } = props;
const type = title.toLowerCase();
let link = null;
if (type === 'imdb') {
link = <Link to={`https://imdb.com/title/${value}/`}>{value}</Link>;
} else if (type === 'tmdb') {
link = (
<Link to={`https://www.themoviedb.org/movie/${value}`}>{value}</Link>
);
} else if (type === 'tvdb') {
link = (
<Link to={`https://www.thetvdb.com/?tab=series&id=${value}`}>
{value}
</Link>
);
} else if (type === 'tvmaze') {
link = <Link to={`https://www.tvmaze.com/shows/${value}/_`}>{value}</Link>;
}
return (
<div className={styles.parameter}>
<div className={styles.info}>
<span>{title}</span>
</div>
<div className={styles.value}>{link ? link : value}</div>
</div>
);
}
export default HistoryRowParameter;
@@ -22,31 +22,31 @@ import styles from './AddIndexerModalContent.css';
const columns = [ const columns = [
{ {
name: 'protocol', name: 'protocol',
label: translate('Protocol'), label: () => translate('Protocol'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'sortName', name: 'sortName',
label: translate('Name'), label: () => translate('Name'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'language', name: 'language',
label: translate('Language'), label: () => translate('Language'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
}, },
{ {
name: 'description', name: 'description',
label: translate('Description'), label: () => translate('Description'),
isSortable: false, isSortable: false,
isVisible: true isVisible: true
}, },
{ {
name: 'privacy', name: 'privacy',
label: translate('Privacy'), label: () => translate('Privacy'),
isSortable: true, isSortable: true,
isVisible: true isVisible: true
} }
@@ -66,15 +66,21 @@ const protocols = [
const privacyLevels = [ const privacyLevels = [
{ {
key: 'private', key: 'private',
value: translate('Private') get value() {
return translate('Private');
}
}, },
{ {
key: 'semiPrivate', key: 'semiPrivate',
value: translate('SemiPrivate') get value() {
return translate('SemiPrivate');
}
}, },
{ {
key: 'public', key: 'public',
value: translate('Public') get value() {
return translate('Public');
}
} }
]; ];
@@ -257,6 +263,7 @@ class AddIndexerModalContent extends Component {
<SelectIndexerRowConnector <SelectIndexerRowConnector
key={`${indexer.implementation}-${indexer.name}`} key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation} implementation={indexer.implementation}
implementationName={indexer.implementationName}
{...indexer} {...indexer}
onIndexerSelect={onIndexerSelect} onIndexerSelect={onIndexerSelect}
/> />
@@ -282,7 +289,7 @@ class AddIndexerModalContent extends Component {
<div className={styles.available}> <div className={styles.available}>
{ {
isPopulated ? isPopulated ?
translate('CountIndexersAvailable', [filteredIndexers.length]) : translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
null null
} }
</div> </div>
@@ -49,8 +49,8 @@ class AddIndexerModalContentConnector extends Component {
// //
// Listeners // Listeners
onIndexerSelect = ({ implementation, name }) => { onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, name }); this.props.selectIndexerSchema({ implementation, implementationName, name });
this.props.onSelectIndexer(); this.props.onSelectIndexer();
}; };
@@ -10,12 +10,14 @@ class AddIndexerPresetMenuItem extends Component {
onPress = () => { onPress = () => {
const { const {
name, name,
implementation implementation,
implementationName
} = this.props; } = this.props;
this.props.onPress({ this.props.onPress({
name, name,
implementation implementation,
implementationName
}); });
}; };
@@ -26,6 +28,7 @@ class AddIndexerPresetMenuItem extends Component {
const { const {
name, name,
implementation, implementation,
implementationName,
...otherProps ...otherProps
} = this.props; } = this.props;
@@ -43,6 +46,7 @@ class AddIndexerPresetMenuItem extends Component {
AddIndexerPresetMenuItem.propTypes = { AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired, name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired, implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired onPress: PropTypes.func.isRequired
}; };
+3 -1
View File
@@ -17,10 +17,11 @@ class SelectIndexerRow extends Component {
onPress = () => { onPress = () => {
const { const {
implementation, implementation,
implementationName,
name name
} = this.props; } = this.props;
this.props.onIndexerSelect({ implementation, name }); this.props.onIndexerSelect({ implementation, implementationName, name });
}; };
// //
@@ -81,6 +82,7 @@ SelectIndexerRow.propTypes = {
language: PropTypes.string.isRequired, language: PropTypes.string.isRequired,
description: PropTypes.string.isRequired, description: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired, implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired, onIndexerSelect: PropTypes.func.isRequired,
isExistingIndexer: PropTypes.bool.isRequired isExistingIndexer: PropTypes.bool.isRequired
}; };
@@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContentConnector from './DeleteIndexerModalContentConnector';
function DeleteIndexerModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<DeleteIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteIndexerModal.propTypes = {
...DeleteIndexerModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModal;
@@ -0,0 +1,25 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
interface DeleteIndexerModalProps {
isOpen: boolean;
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModal(props: DeleteIndexerModalProps) {
const { isOpen, indexerId, onModalClose } = props;
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<DeleteIndexerModalContent
indexerId={indexerId}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default DeleteIndexerModal;
@@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class DeleteIndexerModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportExclusion: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onAddImportExclusionChange = ({ value }) => {
this.setState({ addImportExclusion: value });
};
onDeleteMovieConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.state.addImportExclusion;
this.setState({ deleteFiles: false, addImportExclusion: false });
this.props.onDeletePress(deleteFiles, addImportExclusion);
};
//
// Render
render() {
const {
name,
onModalClose
} = this.props;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Delete - {name}
</ModalHeader>
<ModalBody>
{`Are you sure you want to delete ${name} from Prowlarr`}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteMovieConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteIndexerModalContent.propTypes = {
name: PropTypes.string.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModalContent;
@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import Indexer from 'Indexer/Indexer';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
interface DeleteIndexerModalContentProps {
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerId, onModalClose } = props;
const { name } = useSelector(
createIndexerSelectorForHook(indexerId)
) as Indexer;
const dispatch = useDispatch();
const onConfirmDelete = useCallback(() => {
dispatch(deleteIndexer({ id: indexerId }));
onModalClose();
}, [indexerId, dispatch, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Delete')} - {name}
</ModalHeader>
<ModalBody>
{translate('AreYouSureYouWantToDeleteIndexer', { name })}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={onConfirmDelete}>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteIndexerModalContent;
@@ -1,57 +0,0 @@
import { push } from 'connected-react-router';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
(indexer) => {
return indexer;
}
);
}
const mapDispatchToProps = {
deleteIndexer,
push
};
class DeleteIndexerModalContentConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteIndexer({
id: this.props.indexerId
});
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<DeleteIndexerModalContent
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
DeleteIndexerModalContentConnector.propTypes = {
indexerId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired,
push: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector);
@@ -61,7 +61,7 @@ function EditIndexerModalContent(props) {
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader> <ModalHeader>
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`} {id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })}
</ModalHeader> </ModalHeader>
<ModalBody> <ModalBody>
@@ -187,6 +187,7 @@ function EditIndexerModalContent(props) {
type={inputTypes.TAG} type={inputTypes.TAG}
name="tags" name="tags"
helpText={translate('IndexerTagsHelpText')} helpText={translate('IndexerTagsHelpText')}
helpTextWarning={translate('IndexerTagsHelpTextWarning')}
{...tags} {...tags}
onChange={onInputChange} onChange={onInputChange}
/> />
+21 -12
View File
@@ -22,7 +22,7 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer'; import NoIndexer from 'Indexer/NoIndexer';
import { executeCommand } from 'Store/Actions/commandActions'; import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions'; import { cloneIndexer, testAllIndexers } from 'Store/Actions/indexerActions';
import { import {
setIndexerFilter, setIndexerFilter,
setIndexerSort, setIndexerSort,
@@ -45,9 +45,7 @@ import IndexerIndexTable from './Table/IndexerIndexTable';
import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions'; import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions';
import styles from './IndexerIndex.css'; import styles from './IndexerIndex.css';
function getViewComponent() { const getViewComponent = () => IndexerIndexTable;
return IndexerIndexTable;
}
interface IndexerIndexProps { interface IndexerIndexProps {
initialScrollTop?: number; initialScrollTop?: number;
@@ -84,14 +82,6 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
); );
const [isSelectMode, setIsSelectMode] = useState(false); const [isSelectMode, setIsSelectMode] = useState(false);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
})
);
}, [dispatch]);
const onAddIndexerPress = useCallback(() => { const onAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true); setIsAddIndexerModalOpen(true);
}, [setIsAddIndexerModalOpen]); }, [setIsAddIndexerModalOpen]);
@@ -108,6 +98,24 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
setIsEditIndexerModalOpen(false); setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]); }, [setIsEditIndexerModalOpen]);
const onCloneIndexerPress = useCallback(
(id: number) => {
dispatch(cloneIndexer({ id }));
setIsEditIndexerModalOpen(true);
},
[dispatch, setIsEditIndexerModalOpen]
);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
forceSync: true,
})
);
}, [dispatch]);
const onTestAllPress = useCallback(() => { const onTestAllPress = useCallback(() => {
dispatch(testAllIndexers()); dispatch(testAllIndexers());
}, [dispatch]); }, [dispatch]);
@@ -304,6 +312,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
jumpToCharacter={jumpToCharacter} jumpToCharacter={jumpToCharacter}
isSelectMode={isSelectMode} isSelectMode={isSelectMode}
isSmallScreen={isSmallScreen} isSmallScreen={isSmallScreen}
onCloneIndexerPress={onCloneIndexerPress}
/> />
<IndexerIndexFooter /> <IndexerIndexFooter />
@@ -48,7 +48,9 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
<ModalBody> <ModalBody>
<div className={styles.message}> <div className={styles.message}>
{translate('DeleteSelectedIndexersMessageText', [indexers.length])} {translate('DeleteSelectedIndexersMessageText', {
count: indexers.length,
})}
</div> </div>
<ul> <ul>
@@ -30,9 +30,25 @@ interface EditIndexerModalContentProps {
const NO_CHANGE = 'noChange'; const NO_CHANGE = 'noChange';
const enableOptions = [ const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true }, {
{ key: 'true', value: translate('Enabled') }, key: NO_CHANGE,
{ key: 'false', value: translate('Disabled') }, get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'true',
get value() {
return translate('Enabled');
},
},
{
key: 'false',
get value() {
return translate('Disabled');
},
},
]; ];
function EditIndexerModalContent(props: EditIndexerModalContentProps) { function EditIndexerModalContent(props: EditIndexerModalContentProps) {
@@ -241,7 +257,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
<ModalFooter className={styles.modalFooter}> <ModalFooter className={styles.modalFooter}>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])} {translate('CountIndexersSelected', { count: selectedCount })}
</div> </div>
<div> <div>
@@ -165,7 +165,7 @@ function IndexerIndexSelectFooter() {
</div> </div>
<div className={styles.selected}> <div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])} {translate('CountIndexersSelected', { count: selectedCount })}
</div> </div>
<EditIndexerModal <EditIndexerModal
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext'; import { useSelect } from 'App/SelectContext';
import Label from 'Components/Label'; import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton'; import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector'; import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell'; import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell'; import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column'; import Column from 'Components/Table/Column';
@@ -12,6 +12,7 @@ import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal'; import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector'; import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector'; import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
import Indexer from 'Indexer/Indexer';
import IndexerTitleLink from 'Indexer/IndexerTitleLink'; import IndexerTitleLink from 'Indexer/IndexerTitleLink';
import { SelectStateInputProps } from 'typings/props'; import { SelectStateInputProps } from 'typings/props';
import firstCharToUpper from 'Utilities/String/firstCharToUpper'; import firstCharToUpper from 'Utilities/String/firstCharToUpper';
@@ -26,10 +27,11 @@ interface IndexerIndexRowProps {
sortKey: string; sortKey: string;
columns: Column[]; columns: Column[];
isSelectMode: boolean; isSelectMode: boolean;
onCloneIndexerPress(id: number): void;
} }
function IndexerIndexRow(props: IndexerIndexRowProps) { function IndexerIndexRow(props: IndexerIndexRowProps) {
const { indexerId, columns, isSelectMode } = props; const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props;
const { indexer, appProfile, status, longDateFormat, timeFormat } = const { indexer, appProfile, status, longDateFormat, timeFormat } =
useSelector(createIndexerIndexItemSelector(props.indexerId)); useSelector(createIndexerIndexItemSelector(props.indexerId));
@@ -47,7 +49,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields, fields,
added, added,
capabilities, capabilities,
} = indexer; } = indexer as Indexer;
const baseUrl = const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ?? fields.find((field) => field.name === 'baseUrl')?.value ??
@@ -152,6 +154,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
<IndexerTitleLink <IndexerTitleLink
indexerId={indexerId} indexerId={indexerId}
indexerName={indexerName} indexerName={indexerName}
onCloneIndexerPress={onCloneIndexerPress}
/> />
</VirtualTableRowCell> </VirtualTableRowCell>
); );
@@ -201,7 +204,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
return ( return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739) // @ts-ignore ts(2739)
<RelativeDateCellConnector <RelativeDateCell
key={name} key={name}
className={styles[name]} className={styles[name]}
date={added.toString()} date={added.toString()}
@@ -214,7 +217,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
return ( return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739) // @ts-ignore ts(2739)
<RelativeDateCellConnector <RelativeDateCell
key={name} key={name}
className={styles[name]} className={styles[name]}
date={vipExpiration} date={vipExpiration}
@@ -26,6 +26,7 @@ interface RowItemData {
sortKey: string; sortKey: string;
columns: Column[]; columns: Column[];
isSelectMode: boolean; isSelectMode: boolean;
onCloneIndexerPress(id: number): void;
} }
interface IndexerIndexTableProps { interface IndexerIndexTableProps {
@@ -37,6 +38,7 @@ interface IndexerIndexTableProps {
scrollerRef: RefObject<HTMLElement>; scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean; isSelectMode: boolean;
isSmallScreen: boolean; isSmallScreen: boolean;
onCloneIndexerPress(id: number): void;
} }
const columnsSelector = createSelector( const columnsSelector = createSelector(
@@ -49,7 +51,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
style, style,
data, data,
}) => { }) => {
const { items, sortKey, columns, isSelectMode } = data; const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
if (index >= items.length) { if (index >= items.length) {
return null; return null;
@@ -71,6 +73,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
sortKey={sortKey} sortKey={sortKey}
columns={columns} columns={columns}
isSelectMode={isSelectMode} isSelectMode={isSelectMode}
onCloneIndexerPress={onCloneIndexerPress}
/> />
</div> </div>
); );
@@ -89,6 +92,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
isSelectMode, isSelectMode,
isSmallScreen, isSmallScreen,
scrollerRef, scrollerRef,
onCloneIndexerPress,
} = props; } = props;
const columns = useSelector(columnsSelector); const columns = useSelector(columnsSelector);
@@ -198,6 +202,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
sortKey, sortKey,
columns, columns,
isSelectMode, isSelectMode,
onCloneIndexerPress,
}} }}
> >
{Row} {Row}
@@ -103,7 +103,7 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
isSortable={isSortable} isSortable={isSortable}
onSortPress={onSortPress} onSortPress={onSortPress}
> >
{label} {typeof label === 'function' ? label() : label}
</VirtualTableHeaderCell> </VirtualTableHeaderCell>
); );
})} })}
@@ -12,7 +12,7 @@ interface IndexerStatusCellProps {
className: string; className: string;
enabled: boolean; enabled: boolean;
redirect: boolean; redirect: boolean;
status: IndexerStatus; status?: IndexerStatus;
longDateFormat: string; longDateFormat: string;
timeFormat: string; timeFormat: string;
component?: React.ElementType; component?: React.ElementType;
@@ -1,5 +1,4 @@
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import Indexer from 'Indexer/Indexer';
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector'; import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector'; import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector'; import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector';
@@ -11,7 +10,7 @@ function createIndexerIndexItemSelector(indexerId: number) {
createIndexerAppProfileSelector(indexerId), createIndexerAppProfileSelector(indexerId),
createIndexerStatusSelector(indexerId), createIndexerStatusSelector(indexerId),
createUISettingsSelector(), createUISettingsSelector(),
(indexer: Indexer, appProfile, status, uiSettings) => { (indexer, appProfile, status, uiSettings) => {
return { return {
indexer, indexer,
appProfile, appProfile,
+12
View File
@@ -25,11 +25,13 @@ export interface IndexerCapabilities extends ModelBase {
} }
export interface IndexerField extends ModelBase { export interface IndexerField extends ModelBase {
order: number;
name: string; name: string;
label: string; label: string;
advanced: boolean; advanced: boolean;
type: string; type: string;
value: string; value: string;
privacy: string;
} }
interface Indexer extends ModelBase { interface Indexer extends ModelBase {
@@ -40,6 +42,10 @@ interface Indexer extends ModelBase {
added: Date; added: Date;
enable: boolean; enable: boolean;
redirect: boolean; redirect: boolean;
supportsRss: boolean;
supportsSearch: boolean;
supportsRedirect: boolean;
supportsPagination: boolean;
protocol: string; protocol: string;
privacy: string; privacy: string;
priority: number; priority: number;
@@ -49,6 +55,12 @@ interface Indexer extends ModelBase {
status: IndexerStatus; status: IndexerStatus;
capabilities: IndexerCapabilities; capabilities: IndexerCapabilities;
indexerUrls: string[]; indexerUrls: string[];
legacyUrls: string[];
appProfileId: number;
implementationName: string;
implementation: string;
configContract: string;
infoLink: string;
} }
export default Indexer; export default Indexer;
+3 -1
View File
@@ -7,10 +7,11 @@ import styles from './IndexerTitleLink.css';
interface IndexerTitleLinkProps { interface IndexerTitleLinkProps {
indexerName: string; indexerName: string;
indexerId: number; indexerId: number;
onCloneIndexerPress(id: number): void;
} }
function IndexerTitleLink(props: IndexerTitleLinkProps) { function IndexerTitleLink(props: IndexerTitleLinkProps) {
const { indexerName, indexerId } = props; const { indexerName, indexerId, onCloneIndexerPress } = props;
const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false); const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false);
@@ -32,6 +33,7 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) {
indexerId={indexerId} indexerId={indexerId}
isOpen={isIndexerInfoModalOpen} isOpen={isIndexerInfoModalOpen}
onModalClose={onIndexerInfoModalClose} onModalClose={onIndexerInfoModalClose}
onCloneIndexerPress={onCloneIndexerPress}
/> />
</div> </div>
); );
@@ -0,0 +1,139 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { IndexerHistoryAppState } from 'App/State/IndexerAppState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import Indexer from 'Indexer/Indexer';
import {
clearIndexerHistory,
fetchIndexerHistory,
} from 'Store/Actions/indexerHistoryActions';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import translate from 'Utilities/String/translate';
import IndexerHistoryRow from './IndexerHistoryRow';
const columns = [
{
name: 'eventType',
isVisible: true,
},
{
name: 'query',
label: () => translate('Query'),
isVisible: true,
},
{
name: 'parameters',
label: () => translate('Parameters'),
isVisible: true,
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true,
},
{
name: 'source',
label: () => translate('Source'),
isVisible: true,
},
{
name: 'details',
label: () => translate('Details'),
isVisible: true,
},
];
function createIndexerHistorySelector() {
return createSelector(
(state: AppState) => state.indexerHistory,
createUISettingsSelector(),
(state: AppState) => state.history.pageSize,
(indexerHistory: IndexerHistoryAppState, uiSettings, pageSize) => {
return {
...indexerHistory,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat,
pageSize,
};
}
);
}
interface IndexerHistoryProps {
indexerId: number;
}
function IndexerHistory(props: IndexerHistoryProps) {
const {
isFetching,
isPopulated,
error,
items,
shortDateFormat,
timeFormat,
pageSize,
} = useSelector(createIndexerHistorySelector());
const indexer = useSelector(
createIndexerSelectorForHook(props.indexerId)
) as Indexer;
const dispatch = useDispatch();
useEffect(() => {
dispatch(
fetchIndexerHistory({ indexerId: props.indexerId, limit: pageSize })
);
return () => {
dispatch(clearIndexerHistory());
};
}, [props, pageSize, dispatch]);
const hasItems = !!items.length;
if (isFetching) {
return <LoadingIndicator />;
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>{translate('IndexerHistoryLoadError')}</Alert>
);
}
if (isPopulated && !hasItems && !error) {
return <Alert kind={kinds.INFO}>{translate('NoIndexerHistory')}</Alert>;
}
if (isPopulated && hasItems && !error) {
return (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return (
<IndexerHistoryRow
key={item.id}
indexer={indexer}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...item}
/>
);
})}
</TableBody>
</Table>
);
}
return null;
}
export default IndexerHistory;
@@ -0,0 +1,23 @@
.query {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;
}
.elapsedTime,
.source {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.details {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}
.parametersContent {
display: flex;
flex-wrap: wrap;
}
@@ -0,0 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'details': string;
'elapsedTime': string;
'parametersContent': string;
'query': string;
'source': string;
}
export const cssExports: CssExports;
export default cssExports;
@@ -0,0 +1,104 @@
import React, { useCallback, useState } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import HistoryDetailsModal from 'History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'History/HistoryEventTypeCell';
import { historyParameters } from 'History/HistoryRow';
import HistoryRowParameter from 'History/HistoryRowParameter';
import Indexer from 'Indexer/Indexer';
import { HistoryData } from 'typings/History';
import translate from 'Utilities/String/translate';
import styles from './IndexerHistoryRow.css';
interface IndexerHistoryRowProps {
data: HistoryData;
date: string;
eventType: string;
successful: boolean;
indexer: Indexer;
shortDateFormat: string;
timeFormat: string;
}
function IndexerHistoryRow(props: IndexerHistoryRowProps) {
const {
data,
date,
eventType,
successful,
indexer,
shortDateFormat,
timeFormat,
} = props;
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const onDetailsModalPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const onDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
const parameters = historyParameters.filter(
(parameter) =>
parameter.key in data && data[parameter.key as keyof HistoryData]
);
return (
<TableRow>
<HistoryEventTypeCell
indexer={indexer}
eventType={eventType}
data={data}
successful={successful}
/>
<TableRowCell className={styles.query}>{data.query}</TableRowCell>
<TableRowCell>
<div className={styles.parametersContent}>
{parameters.map((parameter) => {
return (
<HistoryRowParameter
key={parameter.key}
title={parameter.title}
value={data[parameter.key as keyof HistoryData].toString()}
/>
);
})}
</div>
</TableRowCell>
<RelativeDateCell date={date} />
<TableRowCell className={styles.source}>
{data.source ? data.source : null}
</TableRowCell>
<TableRowCell className={styles.details}>
<IconButton
name={icons.INFO}
onPress={onDetailsModalPress}
title={translate('HistoryDetails')}
/>
</TableRowCell>
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
data={data}
indexer={indexer}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onModalClose={onDetailsModalClose}
/>
</TableRow>
);
}
export default IndexerHistoryRow;
@@ -7,16 +7,18 @@ interface IndexerInfoModalProps {
isOpen: boolean; isOpen: boolean;
indexerId: number; indexerId: number;
onModalClose(): void; onModalClose(): void;
onCloneIndexerPress(id: number): void;
} }
function IndexerInfoModal(props: IndexerInfoModalProps) { function IndexerInfoModal(props: IndexerInfoModalProps) {
const { isOpen, onModalClose, indexerId } = props; const { isOpen, indexerId, onModalClose, onCloneIndexerPress } = props;
return ( return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}> <Modal size={sizes.LARGE} isOpen={isOpen} onModalClose={onModalClose}>
<IndexerInfoModalContent <IndexerInfoModalContent
indexerId={indexerId} indexerId={indexerId}
onModalClose={onModalClose} onModalClose={onModalClose}
onCloneIndexerPress={onCloneIndexerPress}
/> />
</Modal> </Modal>
); );
@@ -9,3 +9,47 @@
margin-right: auto; margin-right: auto;
} }
.tabs {
margin-top: -32px;
}
.tabList {
margin: 0;
padding: 0;
}
.tab {
position: relative;
bottom: -1px;
display: inline-block;
padding: 6px 12px;
border: 1px solid transparent;
border-top: none;
list-style: none;
cursor: pointer;
}
.selectedTab {
border-color: var(--borderColor);
border-radius: 0 0 5px 5px;
background-color: rgba(239, 239, 239, 0.4);
color: var(--black);
}
.tabContent {
margin-top: 20px;
}
.modalFooter {
composes: modalFooter from '~Components/Modal/ModalFooter.css';
justify-content: space-between;
}
@media only screen and (max-width: $breakpointExtraSmall) {
.modalFooter {
flex-direction: column;
gap: 10px;
}
}
@@ -3,6 +3,12 @@
interface CssExports { interface CssExports {
'deleteButton': string; 'deleteButton': string;
'description': string; 'description': string;
'modalFooter': string;
'selectedTab': string;
'tab': string;
'tabContent': string;
'tabList': string;
'tabs': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
@@ -1,5 +1,7 @@
import { uniqBy } from 'lodash';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
import DescriptionList from 'Components/DescriptionList/DescriptionList'; import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem'; import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -24,12 +26,13 @@ import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import Indexer from 'Indexer/Indexer'; import Indexer from 'Indexer/Indexer';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector'; import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import IndexerHistory from './History/IndexerHistory';
import styles from './IndexerInfoModalContent.css'; import styles from './IndexerInfoModalContent.css';
function createIndexerInfoItemSelector(indexerId: number) { function createIndexerInfoItemSelector(indexerId: number) {
return createSelector( return createSelector(
createIndexerSelectorForHook(indexerId), createIndexerSelectorForHook(indexerId),
(indexer: Indexer) => { (indexer?: Indexer) => {
return { return {
indexer, indexer,
}; };
@@ -37,15 +40,18 @@ function createIndexerInfoItemSelector(indexerId: number) {
); );
} }
const tabs = ['details', 'categories', 'history', 'stats'];
interface IndexerInfoModalContentProps { interface IndexerInfoModalContentProps {
indexerId: number; indexerId: number;
onModalClose(): void; onModalClose(): void;
onCloneIndexerPress(id: number): void;
} }
function IndexerInfoModalContent(props: IndexerInfoModalContentProps) { function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
const { indexer } = useSelector( const { indexerId, onCloneIndexerPress } = props;
createIndexerInfoItemSelector(props.indexerId)
); const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId));
const { const {
id, id,
@@ -58,7 +64,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
tags, tags,
protocol, protocol,
capabilities, capabilities,
} = indexer; } = indexer as Indexer;
const { onModalClose } = props; const { onModalClose } = props;
@@ -69,10 +75,19 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
const vipExpiration = const vipExpiration =
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined; fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
const [selectedTab, setSelectedTab] = useState(tabs[0]);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false); const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] = const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false); useState(false);
const onTabSelect = useCallback(
(index: number) => {
const selectedTab = tabs[index];
setSelectedTab(selectedTab);
},
[setSelectedTab]
);
const onEditIndexerPress = useCallback(() => { const onEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true); setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]); }, [setIsEditIndexerModalOpen]);
@@ -91,222 +106,265 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
onModalClose(); onModalClose();
}, [setIsDeleteIndexerModalOpen, onModalClose]); }, [setIsDeleteIndexerModalOpen, onModalClose]);
const onCloneIndexerPressWrapper = useCallback(() => {
onCloneIndexerPress(id);
onModalClose();
}, [id, onCloneIndexerPress, onModalClose]);
return ( return (
<ModalContent onModalClose={onModalClose}> <ModalContent onModalClose={onModalClose}>
<ModalHeader>{`${name}`}</ModalHeader> <ModalHeader>{`${name}`}</ModalHeader>
<ModalBody> <ModalBody>
<FieldSet legend={translate('IndexerDetails')}> <Tabs
<div> className={styles.tabs}
<DescriptionList> selectedIndex={tabs.indexOf(selectedTab)}
<DescriptionListItem onSelect={onTabSelect}
descriptionClassName={styles.description} >
title={translate('Id')} <TabList className={styles.tabList}>
data={id} <Tab className={styles.tab} selectedClassName={styles.selectedTab}>
/> {translate('Details')}
<DescriptionListItem </Tab>
descriptionClassName={styles.description}
title={translate('Description')}
data={description ? description : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Encoding')}
data={encoding ? encoding : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Language')}
data={language ?? '-'}
/>
{vipExpiration ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('VipExpiration')}
data={vipExpiration}
/>
) : null}
<DescriptionListItemTitle>
{translate('IndexerSite')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{baseUrl ? (
<Link to={baseUrl}>
{baseUrl.replace(/(:\/\/)api\./, '$1')}
</Link>
) : (
'-'
)}
</DescriptionListItemDescription>
<DescriptionListItemTitle>
{protocol === 'usenet'
? translate('NewznabUrl')
: translate('TorznabUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
{tags.length > 0 ? (
<>
<DescriptionListItemTitle>
{translate('Tags')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<TagListConnector tags={tags} />
</DescriptionListItemDescription>
</>
) : null}
</DescriptionList>
</div>
</FieldSet>
<FieldSet legend={translate('SearchCapabilities')}> <Tab className={styles.tab} selectedClassName={styles.selectedTab}>
<div> {translate('Categories')}
<DescriptionList> </Tab>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('RawSearchSupported')}
data={
capabilities.supportsRawSearch
? translate('Yes')
: translate('No')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('SearchTypes')}
data={
capabilities.searchParams.length === 0 ? (
translate('NotSupported')
) : (
<Label kind={kinds.PRIMARY}>
{capabilities.searchParams[0]}
</Label>
)
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('TVSearchTypes')}
data={
capabilities.tvSearchParams.length === 0
? translate('NotSupported')
: capabilities.tvSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieSearchTypes')}
data={
capabilities.movieSearchParams.length === 0
? translate('NotSupported')
: capabilities.movieSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('BookSearchTypes')}
data={
capabilities.bookSearchParams.length === 0
? translate('NotSupported')
: capabilities.bookSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MusicSearchTypes')}
data={
capabilities.musicSearchParams.length === 0
? translate('NotSupported')
: capabilities.musicSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
</DescriptionList>
</div>
</FieldSet>
{capabilities.categories !== null && <Tab className={styles.tab} selectedClassName={styles.selectedTab}>
capabilities.categories.length > 0 ? ( {translate('History')}
<FieldSet legend={translate('IndexerCategories')}> </Tab>
<Table </TabList>
columns={[ <TabPanel>
{ <div className={styles.tabContent}>
name: 'id', <FieldSet legend={translate('IndexerDetails')}>
label: translate('Id'), <div>
isVisible: true, <DescriptionList>
}, <DescriptionListItem
{ descriptionClassName={styles.description}
name: 'name', title={translate('Id')}
label: translate('Name'), data={id}
isVisible: true, />
}, <DescriptionListItem
]} descriptionClassName={styles.description}
> title={translate('Description')}
{capabilities.categories data={description ? description : '-'}
.sort((a, b) => a.id - b.id) />
.map((category) => { <DescriptionListItem
return ( descriptionClassName={styles.description}
<TableBody key={category.id}> title={translate('Encoding')}
<TableRow key={category.id}> data={encoding ? encoding : '-'}
<TableRowCell>{category.id}</TableRowCell> />
<TableRowCell>{category.name}</TableRowCell> <DescriptionListItem
</TableRow> descriptionClassName={styles.description}
{category.subCategories !== null && title={translate('Language')}
category.subCategories.length > 0 data={language ?? '-'}
? category.subCategories />
.sort((a, b) => a.id - b.id) {vipExpiration ? (
.map((subCategory) => { <DescriptionListItem
descriptionClassName={styles.description}
title={translate('VipExpiration')}
data={vipExpiration}
/>
) : null}
<DescriptionListItemTitle>
{translate('IndexerSite')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{baseUrl ? (
<Link to={baseUrl}>
{baseUrl.replace(/(:\/\/)api\./, '$1')}
</Link>
) : (
'-'
)}
</DescriptionListItemDescription>
<DescriptionListItemTitle>
{protocol === 'usenet'
? translate('NewznabUrl')
: translate('TorznabUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
{tags.length > 0 ? (
<>
<DescriptionListItemTitle>
{translate('Tags')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<TagListConnector tags={tags} />
</DescriptionListItemDescription>
</>
) : null}
</DescriptionList>
</div>
</FieldSet>
<FieldSet legend={translate('SearchCapabilities')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('RawSearchSupported')}
data={
capabilities.supportsRawSearch
? translate('Yes')
: translate('No')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('SearchTypes')}
data={
capabilities.searchParams.length === 0 ? (
translate('NotSupported')
) : (
<Label kind={kinds.PRIMARY}>
{capabilities.searchParams[0]}
</Label>
)
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('TVSearchTypes')}
data={
capabilities.tvSearchParams.length === 0
? translate('NotSupported')
: capabilities.tvSearchParams.map((p) => {
return ( return (
<TableRow key={subCategory.id}> <Label key={p} kind={kinds.PRIMARY}>
<TableRowCell>{subCategory.id}</TableRowCell> {p}
<TableRowCell> </Label>
{subCategory.name}
</TableRowCell>
</TableRow>
); );
}) })
: null} }
</TableBody> />
); <DescriptionListItem
})} descriptionClassName={styles.description}
</Table> title={translate('MovieSearchTypes')}
</FieldSet> data={
) : null} capabilities.movieSearchParams.length === 0
? translate('NotSupported')
: capabilities.movieSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('BookSearchTypes')}
data={
capabilities.bookSearchParams.length === 0
? translate('NotSupported')
: capabilities.bookSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MusicSearchTypes')}
data={
capabilities.musicSearchParams.length === 0
? translate('NotSupported')
: capabilities.musicSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
</DescriptionList>
</div>
</FieldSet>
</div>
</TabPanel>
<TabPanel>
<div className={styles.tabContent}>
{capabilities?.categories?.length > 0 ? (
<FieldSet legend={translate('IndexerCategories')}>
<Table
columns={[
{
name: 'id',
label: translate('Id'),
isVisible: true,
},
{
name: 'name',
label: translate('Name'),
isVisible: true,
},
]}
>
{uniqBy(capabilities.categories, 'id')
.sort((a, b) => a.id - b.id)
.map((category) => {
return (
<TableBody key={category.id}>
<TableRow key={category.id}>
<TableRowCell>{category.id}</TableRowCell>
<TableRowCell>{category.name}</TableRowCell>
</TableRow>
{category?.subCategories?.length > 0
? uniqBy(category.subCategories, 'id')
.sort((a, b) => a.id - b.id)
.map((subCategory) => {
return (
<TableRow key={subCategory.id}>
<TableRowCell>
{subCategory.id}
</TableRowCell>
<TableRowCell>
{subCategory.name}
</TableRowCell>
</TableRow>
);
})
: null}
</TableBody>
);
})}
</Table>
</FieldSet>
) : null}
</div>
</TabPanel>
<TabPanel>
<div className={styles.tabContent}>
<IndexerHistory indexerId={id} />
</div>
</TabPanel>
</Tabs>
</ModalBody> </ModalBody>
<ModalFooter> <ModalFooter className={styles.modalFooter}>
<Button <div>
className={styles.deleteButton} <Button
kind={kinds.DANGER} className={styles.deleteButton}
onPress={onDeleteIndexerPress} kind={kinds.DANGER}
> onPress={onDeleteIndexerPress}
{translate('Delete')} >
</Button> {translate('Delete')}
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button> </Button>
<Button onPress={onModalClose}>{translate('Close')}</Button> <Button onPress={onCloneIndexerPressWrapper}>
{translate('Clone')}
</Button>
</div>
<div>
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</div>
</ModalFooter> </ModalFooter>
<EditIndexerModalConnector <EditIndexerModalConnector
@@ -1,15 +1,16 @@
import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from 'Components/Link/Button'; import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props'; import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate'; import translate from 'Utilities/String/translate';
import styles from './NoIndexer.css'; import styles from './NoIndexer.css';
function NoIndexer(props) { interface NoIndexerProps {
const { totalItems: number;
totalItems, onAddIndexerPress(): void;
onAddIndexerPress }
} = props;
function NoIndexer(props: NoIndexerProps) {
const { totalItems, onAddIndexerPress } = props;
if (totalItems > 0) { if (totalItems > 0) {
return ( return (
@@ -28,10 +29,7 @@ function NoIndexer(props) {
</div> </div>
<div className={styles.buttonContainer}> <div className={styles.buttonContainer}>
<Button <Button onPress={onAddIndexerPress} kind={kinds.PRIMARY}>
onPress={onAddIndexerPress}
kind={kinds.PRIMARY}
>
{translate('AddNewIndexer')} {translate('AddNewIndexer')}
</Button> </Button>
</div> </div>
@@ -39,9 +37,4 @@ function NoIndexer(props) {
); );
} }
NoIndexer.propTypes = {
totalItems: PropTypes.number.isRequired,
onAddIndexerPress: PropTypes.func.isRequired
};
export default NoIndexer; export default NoIndexer;
@@ -0,0 +1,52 @@
.fullWidthChart {
display: inline-block;
width: 100%;
}
.halfWidthChart {
display: inline-block;
width: 50%;
}
.quarterWidthChart {
display: inline-block;
width: 25%;
}
.chartContainer {
margin: 5px;
padding: 15px 25px;
height: 300px;
border-radius: 10px;
background-color: var(--chartBackgroundColor);
}
.statContainer {
margin: 5px;
padding: 15px 25px;
height: 150px;
border-radius: 10px;
background-color: var(--chartBackgroundColor);
}
.statTitle {
font-weight: bold;
font-size: 14px;
}
.stat {
font-weight: bold;
font-size: 60px;
}
@media only screen and (max-width: $breakpointSmall) {
.halfWidthChart {
display: inline-block;
width: 100%;
}
.quarterWidthChart {
display: inline-block;
width: 100%;
}
}
@@ -1,8 +1,13 @@
// This file is automatically generated. // This file is automatically generated.
// Please do not change this file! // Please do not change this file!
interface CssExports { interface CssExports {
'chartContainer': string;
'fullWidthChart': string; 'fullWidthChart': string;
'halfWidthChart': string; 'halfWidthChart': string;
'quarterWidthChart': string;
'stat': string;
'statContainer': string;
'statTitle': string;
} }
export const cssExports: CssExports; export const cssExports: CssExports;
export default cssExports; export default cssExports;
+359
View File
@@ -0,0 +1,359 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import IndexerStatsAppState from 'App/State/IndexerStatsAppState';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import FilterMenu from 'Components/Menu/FilterMenu';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import {
fetchIndexerStats,
setIndexerStatsFilter,
} from 'Store/Actions/indexerStatsActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import {
IndexerStatsHost,
IndexerStatsIndexer,
IndexerStatsUserAgent,
} from 'typings/IndexerStats';
import abbreviateNumber from 'Utilities/Number/abbreviateNumber';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import IndexerStatsFilterModal from './IndexerStatsFilterModal';
import styles from './IndexerStats.css';
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.averageResponseTime,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value:
(indexer.numberOfFailedQueries +
indexer.numberOfFailedRssQueries +
indexer.numberOfFailedAuthQueries +
indexer.numberOfFailedGrabs) /
(indexer.numberOfQueries +
indexer.numberOfRssQueries +
indexer.numberOfAuthQueries +
indexer.numberOfGrabs),
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
const data = {
labels: indexerStats.map((indexer) => indexer.indexerName),
datasets: [
{
label: translate('SearchQueries'),
data: indexerStats.map((indexer) => indexer.numberOfQueries),
},
{
label: translate('RssQueries'),
data: indexerStats.map((indexer) => indexer.numberOfRssQueries),
},
{
label: translate('AuthQueries'),
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries),
},
],
};
return data;
}
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostGrabsData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostQueryData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
const indexerStatsSelector = () => {
return createSelector(
(state: AppState) => state.indexerStats,
createCustomFiltersSelector('indexerStats'),
(indexerStats: IndexerStatsAppState, customFilters) => {
return {
...indexerStats,
customFilters,
};
}
);
};
function IndexerStats() {
const {
isFetching,
isPopulated,
item,
error,
filters,
customFilters,
selectedFilterKey,
} = useSelector(indexerStatsSelector());
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchIndexerStats());
}, [dispatch]);
const onFilterSelect = useCallback(
(value: string) => {
dispatch(setIndexerStatsFilter({ selectedFilterKey: value }));
},
[dispatch]
);
const isLoaded = !error && isPopulated;
const indexerCount = item.indexers?.length ?? 0;
const userAgentCount = item.userAgents?.length ?? 0;
const queryCount =
item.indexers?.reduce((total, indexer) => {
return (
total +
indexer.numberOfQueries +
indexer.numberOfRssQueries +
indexer.numberOfAuthQueries
);
}, 0) ?? 0;
const grabCount =
item.indexers?.reduce((total, indexer) => {
return total + indexer.numberOfGrabs;
}, 0) ?? 0;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
onFilterSelect={onFilterSelect}
filterModalConnectorComponent={IndexerStatsFilterModal}
isDisabled={false}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated && <LoadingIndicator />}
{!isFetching && !!error && (
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</Alert>
)}
{isLoaded && (
<div>
<div className={styles.quarterWidthChart}>
<div className={styles.statContainer}>
<div className={styles.statTitle}>
{translate('ActiveIndexers')}
</div>
<div className={styles.stat}>{indexerCount}</div>
</div>
</div>
<div className={styles.quarterWidthChart}>
<div className={styles.statContainer}>
<div className={styles.statTitle}>
{translate('TotalQueries')}
</div>
<div className={styles.stat}>
{abbreviateNumber(queryCount)}
</div>
</div>
</div>
<div className={styles.quarterWidthChart}>
<div className={styles.statContainer}>
<div className={styles.statTitle}>
{translate('TotalGrabs')}
</div>
<div className={styles.stat}>{abbreviateNumber(grabCount)}</div>
</div>
</div>
<div className={styles.quarterWidthChart}>
<div className={styles.statContainer}>
<div className={styles.statTitle}>
{translate('ActiveApps')}
</div>
<div className={styles.stat}>{userAgentCount}</div>
</div>
</div>
<div className={styles.fullWidthChart}>
<div className={styles.chartContainer}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
stepSize={100}
/>
</div>
</div>
<div className={styles.fullWidthChart}>
<div className={styles.chartContainer}>
<BarChart
data={getFailureRateData(item.indexers)}
title={translate('IndexerFailureRate')}
stepSize={0.1}
kind={kinds.WARNING}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<div className={styles.chartContainer}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<div className={styles.chartContainer}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<div className={styles.chartContainer}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<div className={styles.chartContainer}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<div className={styles.chartContainer}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<div className={styles.chartContainer}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>
</div>
</div>
)}
</PageContentBody>
</PageContent>
);
}
export default IndexerStats;
@@ -0,0 +1,56 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import FilterModal from 'Components/Filter/FilterModal';
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
function createIndexerStatsSelector() {
return createSelector(
(state: AppState) => state.indexerStats.item,
(indexerStats) => {
return indexerStats;
}
);
}
function createFilterBuilderPropsSelector() {
return createSelector(
(state: AppState) => state.indexerStats.filterBuilderProps,
(filterBuilderProps) => {
return filterBuilderProps;
}
);
}
interface IndexerStatsFilterModalProps {
isOpen: boolean;
}
export default function IndexerStatsFilterModal(
props: IndexerStatsFilterModalProps
) {
const sectionItems = [useSelector(createIndexerStatsSelector())];
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
const customFilterType = 'indexerStats';
const dispatch = useDispatch();
const dispatchSetFilter = useCallback(
(payload: unknown) => {
dispatch(setIndexerStatsFilter(payload));
},
[dispatch]
);
return (
<FilterModal
// TODO: Don't spread all the props
{...props}
sectionItems={sectionItems}
filterBuilderProps={filterBuilderProps}
customFilterType={customFilterType}
dispatchSetFilter={dispatchSetFilter}
/>
);
}
-22
View File
@@ -1,22 +0,0 @@
.fullWidthChart {
display: inline-block;
padding: 15px 25px;
width: 100%;
height: 300px;
}
.halfWidthChart {
display: inline-block;
padding: 15px 25px;
width: 50%;
height: 300px;
}
@media only screen and (max-width: $breakpointSmall) {
.halfWidthChart {
display: inline-block;
padding: 15px 25px;
width: 100%;
height: 300px;
}
}

Some files were not shown because too many files have changed in this diff Show More