Compare commits

...

69 Commits

Author SHA1 Message Date
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
203 changed files with 3778 additions and 1682 deletions

View File

@@ -1,5 +1,5 @@
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']
body:
- type: checkboxes

View File

@@ -6,6 +6,3 @@ contact_links:
- name: Support via Discord
url: https://prowlarr.com/discord
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.

View File

@@ -4,8 +4,7 @@
comment: >
:wave: @{issue-author}, we use the issue tracker exclusively
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)
or [Subreddit](https://reddit.com/r/prowlarr)
to be a support request. Please hop over onto our [Discord](https://prowlarr.com/discord).
close: true
close-reason: 'not planned'

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)
[![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

View File

@@ -9,15 +9,15 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.8.2'
majorVersion: '1.8.6'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
sentryOrg: 'servarr'
sentryUrl: 'https://sentry.servarr.com'
dotnetVersion: '6.0.408'
dotnetVersion: '6.0.413'
nodeVersion: '16.X'
innoVersion: '6.2.0'
nodeVersion: '16.x'
windowsImage: 'windows-2022'
linuxImage: 'ubuntu-20.04'
macImage: 'macOS-11'
@@ -27,6 +27,10 @@ trigger:
include:
- develop
- master
paths:
exclude:
- .github
- src/Prowlarr.Api.*/openapi.json
pr:
branches:
@@ -34,8 +38,9 @@ pr:
- develop
paths:
exclude:
- .github
- src/NzbDrone.Core/Localization/Core
- src/Prowlarr.API.*/openapi.json
- src/Prowlarr.Api.*/openapi.json
stages:
- stage: Setup
@@ -349,7 +354,7 @@ stages:
includeRootFolder: false
rootFolderOrFile: $(artifactsFolder)/linux-musl-arm64/net6.0
- task: ArchiveFiles@2
displayName: Create FreeBSD Core Core tar
displayName: Create freebsd-x64 tar
inputs:
archiveFile: '$(Build.ArtifactStagingDirectory)/Prowlarr.$(buildName).freebsd-core-x64.tar.gz'
archiveType: 'tar'
@@ -528,8 +533,8 @@ stages:
testRunTitle: '$(testName) Unit Tests'
failTaskOnFailedTests: true
- job: Unit_LinuxCore_Postgres
displayName: Unit Native LinuxCore with Postgres Database
- job: Unit_LinuxCore_Postgres14
displayName: Unit Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
@@ -578,7 +583,60 @@ stages:
inputs:
testResultsFormat: 'NUnit'
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
- stage: Integration
@@ -664,8 +722,8 @@ stages:
failTaskOnFailedTests: true
displayName: Publish Test Results
- job: Integration_LinuxCore_Postgres
displayName: Integration Native LinuxCore with Postgres Database
- job: Integration_LinuxCore_Postgres14
displayName: Integration Native LinuxCore with Postgres14 Database
dependsOn: Prepare
condition: and(succeeded(), eq(dependencies.Prepare.outputs['setVar.backendNotUpdated'], '0'))
variables:
@@ -722,7 +780,70 @@ stages:
inputs:
testResultsFormat: 'NUnit'
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
displayName: Publish Test Results
@@ -1005,7 +1126,7 @@ stages:
git add .
if git status | grep -q modified
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
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

View File

@@ -4,14 +4,14 @@ module.exports = {
plugins: [
// Stage 1
'@babel/plugin-proposal-export-default-from',
['@babel/plugin-proposal-optional-chaining', { loose }],
['@babel/plugin-proposal-nullish-coalescing-operator', { loose }],
['@babel/plugin-transform-optional-chaining', { loose }],
['@babel/plugin-transform-nullish-coalescing-operator', { loose }],
// Stage 2
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-transform-export-namespace-from',
// Stage 3
['@babel/plugin-proposal-class-properties', { loose }],
['@babel/plugin-transform-class-properties', { loose }],
'@babel/plugin-syntax-dynamic-import'
],
env: {

View File

@@ -1,6 +1,7 @@
.version {
margin: 0 3px;
font-weight: bold;
font-family: var(--defaultFontFamily);
}
.maintenance {

View File

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import InlineMarkdown from 'Components/Markdown/InlineMarkdown';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
@@ -64,12 +65,12 @@ function AppUpdatedModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Prowlarr Updated
{translate('AppUpdated', { appName: 'Prowlarr' })}
</ModalHeader>
<ModalBody>
<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>
{
@@ -77,16 +78,14 @@ function AppUpdatedModalContent(props) {
<div>
{
!update.changes &&
<div className={styles.maintenance}>
{translate('MaintenanceRelease')}
</div>
<div className={styles.maintenance}>{translate('MaintenanceRelease')}</div>
}
{
!!update.changes &&
<div>
<div className={styles.changes}>
What's new?
{translate('WhatsNew')}
</div>
<UpdateChanges
@@ -113,14 +112,14 @@ function AppUpdatedModalContent(props) {
<Button
onPress={onSeeChangesPress}
>
Recent Changes
{translate('RecentChanges')}
</Button>
<Button
kind={kinds.PRIMARY}
onPress={onModalClose}
>
Reload
{translate('Reload')}
</Button>
</ModalFooter>
</ModalContent>

View File

@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
<ModalBody>
<div>
{translate('ConnectionLostMessage')}
{translate('ConnectionLostToBackend', { appName: 'Prowlarr' })}
</div>
<div className={styles.automatic}>
{translate('ConnectionLostAutomaticMessage')}
{translate('ConnectionLostReconnect', { appName: 'Prowlarr' })}
</div>
</ModalBody>
<ModalFooter>

View File

@@ -1,5 +1,7 @@
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import IndexerAppState, {
IndexerHistoryAppState,
IndexerIndexAppState,
IndexerStatusAppState,
} from './IndexerAppState';
@@ -42,6 +44,8 @@ export interface CustomFilter {
interface AppState {
commands: CommandAppState;
history: HistoryAppState;
indexerHistory: IndexerHistoryAppState;
indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexerStatus: IndexerStatusAppState;

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;

View File

@@ -1,6 +1,7 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
import History from 'typings/History';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
@@ -34,4 +35,6 @@ interface IndexerAppState
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
export type IndexerHistoryAppState = AppSectionState<History>;
export default IndexerAppState;

View File

@@ -1,9 +1,11 @@
import { AppSectionItemState } from 'App/State/AppSectionState';
import { Filter } from 'App/State/AppState';
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[];
}

View File

@@ -2,6 +2,7 @@ import Chart from 'chart.js/auto';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { kinds } from 'Helpers/Props';
import { defaultFontFamily } from 'Styles/Variables/fonts';
function getColors(kind) {
@@ -39,7 +40,15 @@ class BarChart extends Component {
plugins: {
title: {
display: true,
text: this.props.title
align: 'start',
text: this.props.title,
padding: {
bottom: 30
},
font: {
size: 14,
family: defaultFontFamily
}
},
legend: {
display: this.props.legend

View File

@@ -1,6 +1,7 @@
import Chart from 'chart.js/auto';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { defaultFontFamily } from 'Styles/Variables/fonts';
function getColors(kind) {
@@ -22,7 +23,15 @@ class DoughnutChart extends Component {
plugins: {
title: {
display: true,
text: this.props.title
align: 'start',
text: this.props.title,
padding: {
bottom: 30
},
font: {
size: 14,
family: defaultFontFamily
}
},
legend: {
position: 'bottom'

View File

@@ -1,6 +1,7 @@
import Chart from 'chart.js/auto';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { defaultFontFamily } from 'Styles/Variables/fonts';
function getColors(index) {
@@ -36,7 +37,15 @@ class StackedBarChart extends Component {
plugins: {
title: {
display: true,
text: this.props.title
align: 'start',
text: this.props.title,
padding: {
bottom: 30
},
font: {
size: 14,
family: defaultFontFamily
}
}
}
},

View File

@@ -3,9 +3,24 @@ import translate from 'Utilities/String/translate';
import FilterBuilderRowValue from './FilterBuilderRowValue';
const privacyTypes = [
{ id: 'public', name: translate('Public') },
{ id: 'private', name: translate('Private') },
{ id: 'semiPrivate', name: translate('SemiPrivate') }
{
id: 'public',
get name() {
return translate('Public');
}
},
{
id: 'private',
get name() {
return translate('Private');
}
},
{
id: 'semiPrivate',
get name() {
return translate('SemiPrivate');
}
}
];
function PrivacyFilterBuilderRowValue(props) {

View File

@@ -9,6 +9,10 @@
&:hover {
background-color: var(--inputHoverBackgroundColor);
}
&.isDisabled {
cursor: not-allowed;
}
}
.optionCheck {

View File

@@ -41,7 +41,7 @@ class NumberInput extends Component {
componentDidUpdate(prevProps, prevState) {
const { value } = this.props;
if (value !== prevProps.value && !this.state.isFocused) {
if (!isNaN(value) && value !== prevProps.value && !this.state.isFocused) {
this.setState({
value: value == null ? '' : value.toString()
});

View File

@@ -97,6 +97,7 @@ class SpinnerErrorButton extends Component {
render() {
const {
kind,
isSpinning,
error,
children,
@@ -112,7 +113,7 @@ class SpinnerErrorButton extends Component {
const showIcon = wasSuccessful || hasWarning || hasError;
let iconName = icons.CHECK;
let iconKind = kinds.SUCCESS;
let iconKind = kind === kinds.PRIMARY ? kinds.DEFAULT : kinds.SUCCESS;
if (hasWarning) {
iconName = icons.WARNING;
@@ -126,6 +127,7 @@ class SpinnerErrorButton extends Component {
return (
<SpinnerButton
kind={kind}
isSpinning={isSpinning}
{...otherProps}
>
@@ -154,6 +156,7 @@ class SpinnerErrorButton extends Component {
}
SpinnerErrorButton.propTypes = {
kind: PropTypes.oneOf(kinds.all),
isSpinning: PropTypes.bool.isRequired,
error: PropTypes.object,
children: PropTypes.node.isRequired

View File

@@ -10,27 +10,55 @@ class InlineMarkdown extends Component {
render() {
const {
className,
data
data,
blockClassName
} = this.props;
// For now only replace links
// For now only replace links or code blocks (not both)
const markdownBlocks = [];
if (data) {
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
const linkRegex = RegExp(/\[(.+?)\]\((.+?)\)/g);
let endIndex = 0;
let match = null;
while ((match = regex.exec(data)) !== null) {
while ((match = linkRegex.exec(data)) !== null) {
if (match.index > endIndex) {
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
}
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
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));
}
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>;
@@ -39,7 +67,8 @@ class InlineMarkdown extends Component {
InlineMarkdown.propTypes = {
className: PropTypes.string,
data: PropTypes.string
data: PropTypes.string,
blockClassName: PropTypes.string
};
export default InlineMarkdown;

View File

@@ -1,58 +1,66 @@
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 getRelativeDate from 'Utilities/Date/getRelativeDate';
import TableRowCell from './TableRowCell';
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() {
const {
className,
date,
includeSeconds,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
component: Component,
dispatch,
...otherProps
} = this.props;
const {
className,
date,
includeSeconds,
component: Component,
dispatch,
...otherProps
} = props;
if (!date) {
return (
<Component
className={className}
{...otherProps}
/>
);
}
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createRelativeDateCellSelector());
return (
<Component
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
</Component>
);
if (!date) {
return <Component className={className} {...otherProps} />;
}
return (
<Component
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, {
includeSeconds,
includeRelativeDay: !showRelativeDates
})}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, {
timeFormat,
includeSeconds,
timeForToday: true
})}
</Component>
);
}
RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired,
date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
component: PropTypes.elementType,
dispatch: PropTypes.func
};

View File

@@ -192,7 +192,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumnDragSource
key={name}
name={name}
label={label || columnLabel}
label={columnLabel || label}
isVisible={isVisible}
isModifiable={true}
index={index}
@@ -210,7 +210,7 @@ class TableOptionsModal extends Component {
<TableOptionsColumn
key={name}
name={name}
label={label || columnLabel}
label={columnLabel || label}
isVisible={isVisible}
index={index}
isModifiable={false}

View File

@@ -6,37 +6,51 @@ import translate from 'Utilities/String/translate';
export const shortcuts = {
OPEN_KEYBOARD_SHORTCUTS_MODAL: {
key: '?',
name: translate('OpenThisModal')
get name() {
return translate('OpenThisModal');
}
},
CLOSE_MODAL: {
key: 'Esc',
name: translate('CloseCurrentModal')
get name() {
return translate('CloseCurrentModal');
}
},
ACCEPT_CONFIRM_MODAL: {
key: 'Enter',
name: translate('AcceptConfirmationModal')
get name() {
return translate('AcceptConfirmationModal');
}
},
MOVIE_SEARCH_INPUT: {
key: 's',
name: translate('FocusSearchBox')
get name() {
return translate('FocusSearchBox');
}
},
SAVE_SETTINGS: {
key: 'mod+s',
name: translate('SaveSettings')
get name() {
return translate('SaveSettings');
}
},
SCROLL_TOP: {
key: 'mod+home',
name: translate('MovieIndexScrollTop')
get name() {
return translate('MovieIndexScrollTop');
}
},
SCROLL_BOTTOM: {
key: 'mod+end',
name: translate('MovieIndexScrollBottom')
get name() {
return translate('MovieIndexScrollBottom');
}
}
};

View File

@@ -11,7 +11,7 @@ import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
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 styles from './AuthenticationRequiredModalContent.css';
@@ -63,71 +63,63 @@ function AuthenticationRequiredModalContent(props) {
className={styles.authRequiredAlert}
kind={kinds.WARNING}
>
{authenticationRequiredWarning}
{translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
</Alert>
{
isPopulated && !error ?
<div>
<FormGroup>
<FormLabel>{translate('Authentication')}</FormLabel>
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationMethod"
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}
{...authenticationMethod}
/>
</FormGroup>
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormGroup>
<FormLabel>{translate('AuthenticationRequired')}</FormLabel>
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup> :
null
}
<FormInputGroup
type={inputTypes.SELECT}
name="authenticationRequired"
values={authenticationRequiredOptions}
helpText={translate('AuthenticationRequiredHelpText')}
onChange={onInputChange}
{...authenticationRequired}
/>
</FormGroup>
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormGroup>
<FormLabel>{translate('Username')}</FormLabel>
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
{...username}
/>
</FormGroup> :
null
}
<FormInputGroup
type={inputTypes.TEXT}
name="username"
onChange={onInputChange}
helpTextWarning={username?.value ? undefined : translate('AuthenticationRequiredUsernameHelpTextWarning')}
{...username}
/>
</FormGroup>
{
authenticationEnabled ?
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormGroup>
<FormLabel>{translate('Password')}</FormLabel>
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
{...password}
/>
</FormGroup> :
null
}
<FormInputGroup
type={inputTypes.PASSWORD}
name="password"
onChange={onInputChange}
helpTextWarning={password?.value ? undefined : translate('AuthenticationRequiredPasswordHelpTextWarning')}
{...password}
/>
</FormGroup>
</div> :
null
}

View File

@@ -10,6 +10,7 @@ export const INFO = 'info';
export const MOVIE_MONITORED_SELECT = 'movieMonitoredSelect';
export const CATEGORY_SELECT = 'newznabCategorySelect';
export const DOWNLOAD_CLIENT_SELECT = 'downloadClientSelect';
export const FLOAT = 'float';
export const NUMBER = 'number';
export const OAUTH = 'oauth';
export const PASSWORD = 'password';
@@ -35,6 +36,7 @@ export const all = [
INFO,
MOVIE_MONITORED_SELECT,
CATEGORY_SELECT,
FLOAT,
NUMBER,
OAUTH,
PASSWORD,

View File

@@ -1,5 +0,0 @@
.markAsFailedButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

View File

@@ -1,16 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal';
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';
import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
switch (eventType) {
@@ -33,10 +30,8 @@ function HistoryDetailsModal(props) {
eventType,
indexer,
data,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
onMarkAsFailedPress,
onModalClose
} = props;
@@ -61,18 +56,6 @@ function HistoryDetailsModal(props) {
</ModalBody>
<ModalFooter>
{
eventType === 'grabbed' &&
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
Mark as Failed
</SpinnerButton>
}
<Button
onPress={onModalClose}
>
@@ -89,10 +72,8 @@ HistoryDetailsModal.propTypes = {
eventType: PropTypes.string.isRequired,
indexer: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

View File

@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
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 TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
@@ -14,24 +14,79 @@ import HistoryEventTypeCell from './HistoryEventTypeCell';
import HistoryRowParameter from './HistoryRowParameter';
import styles from './HistoryRow.css';
const historyParameters = [
export const historyParameters = [
{ key: historyDataTypes.IMDB_ID, title: 'IMDb' },
{ key: historyDataTypes.TMDB_ID, title: 'TMDb' },
{ key: historyDataTypes.TVDB_ID, title: 'TVDb' },
{ key: historyDataTypes.TRAKT_ID, title: 'Trakt' },
{ key: historyDataTypes.R_ID, title: 'TvRage' },
{ key: historyDataTypes.TVMAZE_ID, title: 'TvMaze' },
{ key: historyDataTypes.SEASON, title: translate('Season') },
{ key: historyDataTypes.EPISODE, title: translate('Episode') },
{ key: historyDataTypes.ARTIST, title: translate('Artist') },
{ key: historyDataTypes.ALBUM, title: translate('Album') },
{ 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.AUTHOR, title: translate('Author') },
{ key: historyDataTypes.TITLE, title: translate('Title') },
{ key: historyDataTypes.PUBLISHER, title: translate('Publisher') }
{
key: historyDataTypes.SEASON,
get title() {
return translate('Season');
}
},
{
key: historyDataTypes.EPISODE,
get title() {
return translate('Episode');
}
},
{
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 {
@@ -298,7 +353,7 @@ class HistoryRow extends Component {
if (name === 'date') {
return (
<RelativeDateCellConnector
<RelativeDateCell
key={name}
date={date}
className={styles.date}

View File

@@ -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;

View File

@@ -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;

View File

@@ -263,6 +263,7 @@ class AddIndexerModalContent extends Component {
<SelectIndexerRowConnector
key={`${indexer.implementation}-${indexer.name}`}
implementation={indexer.implementation}
implementationName={indexer.implementationName}
{...indexer}
onIndexerSelect={onIndexerSelect}
/>
@@ -288,7 +289,7 @@ class AddIndexerModalContent extends Component {
<div className={styles.available}>
{
isPopulated ?
translate('CountIndexersAvailable', [filteredIndexers.length]) :
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
null
}
</div>

View File

@@ -49,8 +49,8 @@ class AddIndexerModalContentConnector extends Component {
//
// Listeners
onIndexerSelect = ({ implementation, name }) => {
this.props.selectIndexerSchema({ implementation, name });
onIndexerSelect = ({ implementation, implementationName, name }) => {
this.props.selectIndexerSchema({ implementation, implementationName, name });
this.props.onSelectIndexer();
};

View File

@@ -10,12 +10,14 @@ class AddIndexerPresetMenuItem extends Component {
onPress = () => {
const {
name,
implementation
implementation,
implementationName
} = this.props;
this.props.onPress({
name,
implementation
implementation,
implementationName
});
};
@@ -26,6 +28,7 @@ class AddIndexerPresetMenuItem extends Component {
const {
name,
implementation,
implementationName,
...otherProps
} = this.props;
@@ -43,6 +46,7 @@ class AddIndexerPresetMenuItem extends Component {
AddIndexerPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};

View File

@@ -17,10 +17,11 @@ class SelectIndexerRow extends Component {
onPress = () => {
const {
implementation,
implementationName,
name
} = this.props;
this.props.onIndexerSelect({ implementation, name });
this.props.onIndexerSelect({ implementation, implementationName, name });
};
//
@@ -81,6 +82,7 @@ SelectIndexerRow.propTypes = {
language: PropTypes.string.isRequired,
description: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onIndexerSelect: PropTypes.func.isRequired,
isExistingIndexer: PropTypes.bool.isRequired
};

View File

@@ -37,7 +37,7 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
</ModalHeader>
<ModalBody>
{translate('AreYouSureYouWantToDeleteIndexer', [name])}
{translate('AreYouSureYouWantToDeleteIndexer', { name })}
</ModalBody>
<ModalFooter>

View File

@@ -61,7 +61,7 @@ function EditIndexerModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? translate('EditIndexer') : translate('AddIndexer')} - ${indexerDisplayName}`}
{id ? translate('EditIndexerImplementation', { implementationName: indexerDisplayName }) : translate('AddIndexerImplementation', { implementationName: indexerDisplayName })}
</ModalHeader>
<ModalBody>

View File

@@ -22,7 +22,7 @@ import AddIndexerModal from 'Indexer/Add/AddIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import NoIndexer from 'Indexer/NoIndexer';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllIndexers } from 'Store/Actions/indexerActions';
import { cloneIndexer, testAllIndexers } from 'Store/Actions/indexerActions';
import {
setIndexerFilter,
setIndexerSort,
@@ -98,6 +98,15 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onCloneIndexerPress = useCallback(
(id: number) => {
dispatch(cloneIndexer({ id }));
setIsEditIndexerModalOpen(true);
},
[dispatch, setIsEditIndexerModalOpen]
);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
@@ -303,6 +312,7 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
jumpToCharacter={jumpToCharacter}
isSelectMode={isSelectMode}
isSmallScreen={isSmallScreen}
onCloneIndexerPress={onCloneIndexerPress}
/>
<IndexerIndexFooter />

View File

@@ -48,7 +48,9 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
<ModalBody>
<div className={styles.message}>
{translate('DeleteSelectedIndexersMessageText', [indexers.length])}
{translate('DeleteSelectedIndexersMessageText', {
count: indexers.length,
})}
</div>
<ul>

View File

@@ -257,7 +257,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])}
{translate('CountIndexersSelected', { count: selectedCount })}
</div>
<div>

View File

@@ -165,7 +165,7 @@ function IndexerIndexSelectFooter() {
</div>
<div className={styles.selected}>
{translate('CountIndexersSelected', [selectedCount])}
{translate('CountIndexersSelected', { count: selectedCount })}
</div>
<EditIndexerModal

View File

@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import Label from 'Components/Label';
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 VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
@@ -27,10 +27,11 @@ interface IndexerIndexRowProps {
sortKey: string;
columns: Column[];
isSelectMode: boolean;
onCloneIndexerPress(id: number): void;
}
function IndexerIndexRow(props: IndexerIndexRowProps) {
const { indexerId, columns, isSelectMode } = props;
const { indexerId, columns, isSelectMode, onCloneIndexerPress } = props;
const { indexer, appProfile, status, longDateFormat, timeFormat } =
useSelector(createIndexerIndexItemSelector(props.indexerId));
@@ -153,6 +154,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
<IndexerTitleLink
indexerId={indexerId}
indexerName={indexerName}
onCloneIndexerPress={onCloneIndexerPress}
/>
</VirtualTableRowCell>
);
@@ -202,7 +204,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
<RelativeDateCell
key={name}
className={styles[name]}
date={added.toString()}
@@ -215,7 +217,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
<RelativeDateCell
key={name}
className={styles[name]}
date={vipExpiration}

View File

@@ -26,6 +26,7 @@ interface RowItemData {
sortKey: string;
columns: Column[];
isSelectMode: boolean;
onCloneIndexerPress(id: number): void;
}
interface IndexerIndexTableProps {
@@ -37,6 +38,7 @@ interface IndexerIndexTableProps {
scrollerRef: RefObject<HTMLElement>;
isSelectMode: boolean;
isSmallScreen: boolean;
onCloneIndexerPress(id: number): void;
}
const columnsSelector = createSelector(
@@ -49,7 +51,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
style,
data,
}) => {
const { items, sortKey, columns, isSelectMode } = data;
const { items, sortKey, columns, isSelectMode, onCloneIndexerPress } = data;
if (index >= items.length) {
return null;
@@ -71,6 +73,7 @@ const Row: React.FC<ListChildComponentProps<RowItemData>> = ({
sortKey={sortKey}
columns={columns}
isSelectMode={isSelectMode}
onCloneIndexerPress={onCloneIndexerPress}
/>
</div>
);
@@ -89,6 +92,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
isSelectMode,
isSmallScreen,
scrollerRef,
onCloneIndexerPress,
} = props;
const columns = useSelector(columnsSelector);
@@ -198,6 +202,7 @@ function IndexerIndexTable(props: IndexerIndexTableProps) {
sortKey,
columns,
isSelectMode,
onCloneIndexerPress,
}}
>
{Row}

View File

@@ -7,10 +7,11 @@ import styles from './IndexerTitleLink.css';
interface IndexerTitleLinkProps {
indexerName: string;
indexerId: number;
onCloneIndexerPress(id: number): void;
}
function IndexerTitleLink(props: IndexerTitleLinkProps) {
const { indexerName, indexerId } = props;
const { indexerName, indexerId, onCloneIndexerPress } = props;
const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false);
@@ -32,6 +33,7 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) {
indexerId={indexerId}
isOpen={isIndexerInfoModalOpen}
onModalClose={onIndexerInfoModalClose}
onCloneIndexerPress={onCloneIndexerPress}
/>
</div>
);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -1,7 +1,11 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'markAsFailedButton': string;
'details': string;
'elapsedTime': string;
'parametersContent': string;
'query': string;
'source': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -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;

View File

@@ -7,16 +7,18 @@ interface IndexerInfoModalProps {
isOpen: boolean;
indexerId: number;
onModalClose(): void;
onCloneIndexerPress(id: number): void;
}
function IndexerInfoModal(props: IndexerInfoModalProps) {
const { isOpen, onModalClose, indexerId } = props;
const { isOpen, indexerId, onModalClose, onCloneIndexerPress } = props;
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
<Modal size={sizes.LARGE} isOpen={isOpen} onModalClose={onModalClose}>
<IndexerInfoModalContent
indexerId={indexerId}
onModalClose={onModalClose}
onCloneIndexerPress={onCloneIndexerPress}
/>
</Modal>
);

View File

@@ -9,3 +9,47 @@
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;
}
}

View File

@@ -3,6 +3,12 @@
interface CssExports {
'deleteButton': string;
'description': string;
'modalFooter': string;
'selectedTab': string;
'tab': string;
'tabContent': string;
'tabList': string;
'tabs': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,5 +1,7 @@
import { uniqBy } from 'lodash';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { createSelector } from 'reselect';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@@ -24,6 +26,7 @@ import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import Indexer from 'Indexer/Indexer';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
import IndexerHistory from './History/IndexerHistory';
import styles from './IndexerInfoModalContent.css';
function createIndexerInfoItemSelector(indexerId: number) {
@@ -37,15 +40,18 @@ function createIndexerInfoItemSelector(indexerId: number) {
);
}
const tabs = ['details', 'categories', 'history', 'stats'];
interface IndexerInfoModalContentProps {
indexerId: number;
onModalClose(): void;
onCloneIndexerPress(id: number): void;
}
function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
const { indexer } = useSelector(
createIndexerInfoItemSelector(props.indexerId)
);
const { indexerId, onCloneIndexerPress } = props;
const { indexer } = useSelector(createIndexerInfoItemSelector(indexerId));
const {
id,
@@ -69,10 +75,19 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
const vipExpiration =
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
const [selectedTab, setSelectedTab] = useState(tabs[0]);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const onTabSelect = useCallback(
(index: number) => {
const selectedTab = tabs[index];
setSelectedTab(selectedTab);
},
[setSelectedTab]
);
const onEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]);
@@ -91,222 +106,265 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
onModalClose();
}, [setIsDeleteIndexerModalOpen, onModalClose]);
const onCloneIndexerPressWrapper = useCallback(() => {
onCloneIndexerPress(id);
onModalClose();
}, [id, onCloneIndexerPress, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>{`${name}`}</ModalHeader>
<ModalBody>
<FieldSet legend={translate('IndexerDetails')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Id')}
data={id}
/>
<DescriptionListItem
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>
<Tabs
className={styles.tabs}
selectedIndex={tabs.indexOf(selectedTab)}
onSelect={onTabSelect}
>
<TabList className={styles.tabList}>
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
{translate('Details')}
</Tab>
<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 (
<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>
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
{translate('Categories')}
</Tab>
{capabilities.categories !== null &&
capabilities.categories.length > 0 ? (
<FieldSet legend={translate('IndexerCategories')}>
<Table
columns={[
{
name: 'id',
label: translate('Id'),
isVisible: true,
},
{
name: 'name',
label: translate('Name'),
isVisible: true,
},
]}
>
{capabilities.categories
.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 !== null &&
category.subCategories.length > 0
? category.subCategories
.sort((a, b) => a.id - b.id)
.map((subCategory) => {
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
{translate('History')}
</Tab>
</TabList>
<TabPanel>
<div className={styles.tabContent}>
<FieldSet legend={translate('IndexerDetails')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Id')}
data={id}
/>
<DescriptionListItem
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')}>
<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 (
<TableRow key={subCategory.id}>
<TableRowCell>{subCategory.id}</TableRowCell>
<TableRowCell>
{subCategory.name}
</TableRowCell>
</TableRow>
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
: null}
</TableBody>
);
})}
</Table>
</FieldSet>
) : null}
}
/>
<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>
</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>
<ModalFooter>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<ModalFooter className={styles.modalFooter}>
<div>
<Button
className={styles.deleteButton}
kind={kinds.DANGER}
onPress={onDeleteIndexerPress}
>
{translate('Delete')}
</Button>
<Button onPress={onCloneIndexerPressWrapper}>
{translate('Clone')}
</Button>
</div>
<div>
<Button onPress={onEditIndexerPress}>{translate('Edit')}</Button>
<Button onPress={onModalClose}>{translate('Close')}</Button>
</div>
</ModalFooter>
<EditIndexerModalConnector

View File

@@ -1,22 +1,52 @@
.fullWidthChart {
display: inline-block;
padding: 15px 25px;
width: 100%;
height: 300px;
}
.halfWidthChart {
display: inline-block;
padding: 15px 25px;
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;
padding: 15px 25px;
width: 100%;
height: 300px;
}
.quarterWidthChart {
display: inline-block;
width: 100%;
}
}

View File

@@ -1,8 +1,13 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'chartContainer': string;
'fullWidthChart': string;
'halfWidthChart': string;
'quarterWidthChart': string;
'stat': string;
'statContainer': string;
'statTitle': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -8,6 +8,7 @@ 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';
@@ -17,14 +18,16 @@ 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 IndexerStatsFilterMenu from './IndexerStatsFilterMenu';
import IndexerStatsFilterModal from './IndexerStatsFilterModal';
import styles from './IndexerStats.css';
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
@@ -165,15 +168,26 @@ function getHostQueryData(indexerStats: IndexerStatsHost[]) {
const indexerStatsSelector = () => {
return createSelector(
(state: AppState) => state.indexerStats,
(indexerStats: IndexerStatsAppState) => {
return indexerStats;
createCustomFiltersSelector('indexerStats'),
(indexerStats: IndexerStatsAppState, customFilters) => {
return {
...indexerStats,
customFilters,
};
}
);
};
function IndexerStats() {
const { isFetching, isPopulated, item, error, filters, selectedFilterKey } =
useSelector(indexerStatsSelector());
const {
isFetching,
isPopulated,
item,
error,
filters,
customFilters,
selectedFilterKey,
} = useSelector(indexerStatsSelector());
const dispatch = useDispatch();
useEffect(() => {
@@ -188,15 +202,33 @@ function IndexerStats() {
);
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}>
<IndexerStatsFilterMenu
<FilterMenu
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={customFilters}
onFilterSelect={onFilterSelect}
filterModalConnectorComponent={IndexerStatsFilterModal}
isDisabled={false}
/>
</PageToolbarSection>
@@ -212,58 +244,110 @@ function IndexerStats() {
{isLoaded && (
<div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
/>
<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}>
<BarChart
data={getFailureRateData(item.indexers)}
title={translate('IndexerFailureRate')}
kind={kinds.WARNING}
/>
<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}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
<div className={styles.chartContainer}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
<div className={styles.chartContainer}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
<div className={styles.chartContainer}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
<div className={styles.chartContainer}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
<div className={styles.chartContainer}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
<div className={styles.chartContainer}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>
</div>
</div>
)}

View File

@@ -1,27 +0,0 @@
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
interface IndexerStatsFilterMenuProps {
selectedFilterKey: string | number;
filters: object[];
isDisabled: boolean;
onFilterSelect(filterName: string): unknown;
}
function IndexerStatsFilterMenu(props: IndexerStatsFilterMenuProps) {
const { selectedFilterKey, filters, isDisabled, onFilterSelect } = props;
return (
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
);
}
export default IndexerStatsFilterMenu;

View File

@@ -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}
/>
);
}

View File

@@ -24,6 +24,7 @@ class SearchFooter extends Component {
super(props, context);
const {
defaultSearchQueryParams,
defaultIndexerIds,
defaultCategories,
defaultSearchQuery,
@@ -33,16 +34,16 @@ class SearchFooter extends Component {
} = props;
this.state = {
isQueryParameterModalOpen: false,
queryModalOptions: null,
searchType: defaultSearchType,
searchIndexerIds: defaultSearchQueryParams.searchIndexerIds ?? defaultIndexerIds,
searchCategories: defaultSearchQueryParams.searchCategories ?? defaultCategories,
searchQuery: (defaultSearchQueryParams.searchQuery ?? defaultSearchQuery) || '',
searchType: defaultSearchQueryParams.searchType ?? defaultSearchType,
searchLimit: defaultSearchQueryParams.searchLimit ?? defaultSearchLimit,
searchOffset: defaultSearchQueryParams.searchOffset ?? defaultSearchOffset,
newSearch: true,
searchingReleases: false,
searchQuery: defaultSearchQuery || '',
searchIndexerIds: defaultIndexerIds,
searchCategories: defaultCategories,
searchLimit: defaultSearchLimit,
searchOffset: defaultSearchOffset,
newSearch: true
isQueryParameterModalOpen: false,
queryModalOptions: null
};
}
@@ -189,12 +190,13 @@ class SearchFooter extends Component {
break;
default:
icon = icons.SEARCH;
break;
}
let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', [searchIndexerIds.length]);
let footerLabel = searchIndexerIds.length === 0 ? translate('SearchAllIndexers') : translate('SearchCountIndexers', { count: searchIndexerIds.length });
if (isPopulated) {
footerLabel = selectedCount === 0 ? translate('FoundCountReleases', [itemCount]) : translate('SelectedCountOfCountReleases', [selectedCount, itemCount]);
footerLabel = selectedCount === 0 ? translate('FoundCountReleases', { itemCount }) : translate('SelectedCountOfCountReleases', { selectedCount, itemCount });
}
return (
@@ -300,6 +302,7 @@ class SearchFooter extends Component {
}
SearchFooter.propTypes = {
defaultSearchQueryParams: PropTypes.object.isRequired,
defaultIndexerIds: PropTypes.arrayOf(PropTypes.number).isRequired,
defaultCategories: PropTypes.arrayOf(PropTypes.number).isRequired,
defaultSearchQuery: PropTypes.string.isRequired,

View File

@@ -49,12 +49,12 @@ function createMapStateToProps() {
return {
defaultSearchQueryParams,
defaultSearchQuery: defaultSearchQueryParams.searchQuery ?? defaultSearchQuery,
defaultIndexerIds: defaultSearchQueryParams.searchIndexerIds ?? defaultIndexerIds,
defaultCategories: defaultSearchQueryParams.searchCategories ?? defaultCategories,
defaultSearchType: defaultSearchQueryParams.searchType ?? defaultSearchType,
defaultSearchLimit: defaultSearchQueryParams.searchLimit ?? defaultSearchLimit,
defaultSearchOffset: defaultSearchQueryParams.searchOffset ?? defaultSearchOffset
defaultSearchQuery,
defaultIndexerIds,
defaultCategories,
defaultSearchType,
defaultSearchLimit,
defaultSearchOffset
};
}
);
@@ -87,14 +87,9 @@ class SearchFooterConnector extends Component {
// Render
render() {
const {
defaultSearchQueryParams,
...otherProps
} = this.props;
return (
<SearchFooter
{...otherProps}
{...this.props}
onInputChange={this.onInputChange}
/>
);

View File

@@ -16,10 +16,11 @@ class AddApplicationItem extends Component {
onApplicationSelect = () => {
const {
implementation
implementation,
implementationName
} = this.props;
this.props.onApplicationSelect({ implementation });
this.props.onApplicationSelect({ implementation, implementationName });
};
//
@@ -77,6 +78,7 @@ class AddApplicationItem extends Component {
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onApplicationSelect}
/>
);

View File

@@ -10,12 +10,14 @@ class AddApplicationPresetMenuItem extends Component {
onPress = () => {
const {
name,
implementation
implementation,
implementationName
} = this.props;
this.props.onPress({
name,
implementation
implementation,
implementationName
});
};
@@ -26,6 +28,7 @@ class AddApplicationPresetMenuItem extends Component {
const {
name,
implementation,
implementationName,
...otherProps
} = this.props;
@@ -43,6 +46,7 @@ class AddApplicationPresetMenuItem extends Component {
AddApplicationPresetMenuItem.propTypes = {
name: PropTypes.string.isRequired,
implementation: PropTypes.string.isRequired,
implementationName: PropTypes.string.isRequired,
onPress: PropTypes.func.isRequired
};

View File

@@ -127,7 +127,7 @@ class Application extends Component {
isOpen={this.state.isDeleteApplicationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteApplication')}
message={translate('DeleteApplicationMessageText', [name])}
message={translate('DeleteApplicationMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteApplication}
onCancel={this.onDeleteApplicationModalClose}

View File

@@ -71,7 +71,7 @@ function EditApplicationModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? translate('Edit') : translate('Add')} ${translate('Application')} - ${implementationName}`}
{id ? translate('EditApplicationImplementation', { implementationName }) : translate('AddApplicationImplementation', { implementationName })}
</ModalHeader>
<ModalBody>

View File

@@ -114,7 +114,7 @@ function ManageApplicationsEditModalContent(
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountApplicationsSelected', [selectedCount])}
{translate('CountApplicationsSelected', { count: selectedCount })}
</div>
<div>

View File

@@ -268,9 +268,9 @@ function ManageApplicationsModalContent(
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedApplications')}
message={translate('DeleteSelectedApplicationsMessageText', [
selectedIds.length,
])}
message={translate('DeleteSelectedApplicationsMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}

View File

@@ -35,7 +35,7 @@ function AddCategoryModalContent(props) {
return (
<ModalContent onModalClose={onCancelPress}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Category`}
{id ? translate('EditCategory') : translate('AddCategory')}
</ModalHeader>
<ModalBody>

View File

@@ -88,7 +88,7 @@ class Category extends Component {
message={
<div>
<div>
{translate('AreYouSureYouWantToDeleteCategory', [name])}
{translate('AreYouSureYouWantToDeleteCategory')}
</div>
</div>
}

View File

@@ -105,7 +105,7 @@ class DownloadClient extends Component {
isOpen={this.state.isDeleteDownloadClientModalOpen}
kind={kinds.DANGER}
title={translate('DeleteDownloadClient')}
message={translate('DeleteDownloadClientMessageText', [name])}
message={translate('DeleteDownloadClientMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteDownloadClient}
onCancel={this.onDeleteDownloadClientModalClose}

View File

@@ -84,7 +84,7 @@ class EditDownloadClientModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? translate('Edit') : translate('Add')} ${translate('DownloadClient')} - ${implementationName}`}
{id ? translate('EditDownloadClientImplementation', { implementationName }) : translate('AddDownloadClientImplementation', { implementationName })}
</ModalHeader>
<ModalBody>

View File

@@ -129,7 +129,7 @@ function ManageDownloadClientsEditModalContent(
<ModalFooter className={styles.modalFooter}>
<div className={styles.selected}>
{translate('CountDownloadClientsSelected', [selectedCount])}
{translate('CountDownloadClientsSelected', { count: selectedCount })}
</div>
<div>

View File

@@ -226,9 +226,9 @@ function ManageDownloadClientsModalContent(
isOpen={isDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteSelectedDownloadClients')}
message={translate('DeleteSelectedDownloadClientsMessageText', [
selectedIds.length,
])}
message={translate('DeleteSelectedDownloadClientsMessageText', {
count: selectedIds.length,
})}
confirmLabel={translate('Delete')}
onConfirm={onConfirmDelete}
onCancel={onDeleteModalClose}

View File

@@ -11,24 +11,69 @@ import ConfirmModal from 'Components/Modal/ConfirmModal';
import { icons, inputTypes, kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
export const authenticationRequiredWarning = translate('AuthenticationRequiredWarning');
export const authenticationMethodOptions = [
{ key: 'none', value: 'None', isDisabled: true },
{ key: 'external', value: 'External', isHidden: true },
{ key: 'basic', value: 'Basic (Browser Popup)' },
{ key: 'forms', value: 'Forms (Login Page)' }
{
key: 'none',
get value() {
return translate('None');
},
isDisabled: true
},
{
key: 'external',
get value() {
return translate('External');
},
isHidden: true
},
{
key: 'basic',
get value() {
return translate('AuthBasic');
}
},
{
key: 'forms',
get value() {
return translate('AuthForm');
}
}
];
export const authenticationRequiredOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' }
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
}
];
const certificateValidationOptions = [
{ key: 'enabled', value: 'Enabled' },
{ key: 'disabledForLocalAddresses', value: 'Disabled for Local Addresses' },
{ key: 'disabled', value: 'Disabled' }
{
key: 'enabled',
get value() {
return translate('Enabled');
}
},
{
key: 'disabledForLocalAddresses',
get value() {
return translate('DisabledForLocalAddresses');
}
},
{
key: 'disabled',
get value() {
return translate('Disabled');
}
}
];
class SecuritySettings extends Component {
@@ -94,8 +139,8 @@ class SecuritySettings extends Component {
type={inputTypes.SELECT}
name="authenticationMethod"
values={authenticationMethodOptions}
helpText={translate('AuthenticationMethodHelpText')}
helpTextWarning={authenticationRequiredWarning}
helpText={translate('AuthenticationMethodHelpText', { appName: 'Prowlarr' })}
helpTextWarning={translate('AuthenticationRequiredWarning', { appName: 'Prowlarr' })}
onChange={onInputChange}
{...authenticationMethod}
/>
@@ -155,6 +200,7 @@ class SecuritySettings extends Component {
type={inputTypes.TEXT}
name="apiKey"
readOnly={true}
helpTextWarning={translate('RestartRequiredHelpTextWarning')}
buttons={[
<ClipboardButton
key="copy"
@@ -196,7 +242,7 @@ class SecuritySettings extends Component {
isOpen={this.state.isConfirmApiKeyResetModalOpen}
kind={kinds.DANGER}
title={translate('ResetAPIKey')}
message={translate('AreYouSureYouWantToResetYourAPIKey')}
message={translate('ResetAPIKeyMessageText')}
confirmLabel={translate('Reset')}
onConfirm={this.onConfirmResetApiKey}
onCancel={this.onCloseResetApiKeyModal}

View File

@@ -16,10 +16,11 @@ class AddIndexerProxyItem extends Component {
onIndexerProxySelect = () => {
const {
implementation
implementation,
implementationName
} = this.props;
this.props.onIndexerProxySelect({ implementation });
this.props.onIndexerProxySelect({ implementation, implementationName });
};
//
@@ -77,6 +78,7 @@ class AddIndexerProxyItem extends Component {
key={preset.name}
name={preset.name}
implementation={implementation}
implementationName={implementationName}
onPress={onIndexerProxySelect}
/>
);

View File

@@ -47,7 +47,7 @@ function EditIndexerProxyModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Proxy - ${implementationName}`}
{id ? translate('EditIndexerProxyImplementation', { implementationName }) : translate('AddIndexerProxyImplementation', { implementationName })}
</ModalHeader>
<ModalBody>

View File

@@ -122,7 +122,7 @@ class IndexerProxy extends Component {
isOpen={this.state.isDeleteIndexerProxyModalOpen}
kind={kinds.DANGER}
title={translate('DeleteIndexerProxy')}
message={translate('DeleteIndexerProxyMessageText', [name])}
message={translate('DeleteIndexerProxyMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteIndexerProxy}
onCancel={this.onDeleteIndexerProxyModalClose}

View File

@@ -28,7 +28,7 @@ class AddNotificationModalContent extends Component {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
Add Notification
{translate('AddConnection')}
</ModalHeader>
<ModalBody>

View File

@@ -48,7 +48,7 @@ function EditNotificationModalContent(props) {
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{`${id ? 'Edit' : 'Add'} Connection - ${implementationName}`}
{id ? translate('EditConnectionImplementation', { implementationName }) : translate('AddConnectionImplementation', { implementationName })}
</ModalHeader>
<ModalBody>

View File

@@ -137,7 +137,7 @@ class Notification extends Component {
isOpen={this.state.isDeleteNotificationModalOpen}
kind={kinds.DANGER}
title={translate('DeleteNotification')}
message={translate('DeleteNotificationMessageText', [name])}
message={translate('DeleteNotificationMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteNotification}
onCancel={this.onDeleteNotificationModalClose}

View File

@@ -130,7 +130,7 @@ class AppProfile extends Component {
isOpen={this.state.isDeleteAppProfileModalOpen}
kind={kinds.DANGER}
title={translate('DeleteAppProfile')}
message={translate('AppProfileDeleteConfirm', [name])}
message={translate('DeleteAppProfileMessageText', { name })}
confirmLabel={translate('Delete')}
isSpinning={isDeleting}
onConfirm={this.onConfirmDeleteAppProfile}

View File

@@ -137,7 +137,7 @@ class Tag extends Component {
isOpen={isDeleteTagModalOpen}
kind={kinds.DANGER}
title={translate('DeleteTag')}
message={translate('DeleteTagMessageText', [label])}
message={translate('DeleteTagMessageText', { label })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeleteTag}
onCancel={this.onDeleteTagModalClose}

View File

@@ -147,7 +147,7 @@ class UISettings extends Component {
<FormInputGroup
type={inputTypes.SELECT}
name="theme"
helpText={translate('ThemeHelpText', ['Theme.Park'])}
helpText={translate('ThemeHelpText', { inspiredBy: 'Theme.Park' })}
values={themeOptions}
onChange={onInputChange}
{...settings.theme}

View File

@@ -7,6 +7,7 @@ import createSetSettingValueReducer from 'Store/Actions/Creators/Reducers/create
import { createThunk } from 'Store/thunks';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import translate from 'Utilities/String/translate';
//
// Variables
@@ -87,7 +88,7 @@ export default {
const pendingChanges = { ...item, id: 0 };
delete pendingChanges.id;
pendingChanges.name = `${pendingChanges.name} - Copy`;
pendingChanges.name = translate('DefaultNameCopiedProfile', { name: pendingChanges.name });
newState.pendingChanges = pendingChanges;
return updateSectionState(state, section, newState);

View File

@@ -4,6 +4,7 @@ import * as commands from './commandActions';
import * as customFilters from './customFilterActions';
import * as history from './historyActions';
import * as indexers from './indexerActions';
import * as indexerHistory from './indexerHistoryActions';
import * as indexerIndex from './indexerIndexActions';
import * as indexerStats from './indexerStatsActions';
import * as indexerStatus from './indexerStatusActions';
@@ -28,6 +29,7 @@ export default [
releases,
localization,
indexers,
indexerHistory,
indexerIndex,
indexerStats,
indexerStatus,

View File

@@ -184,6 +184,8 @@ export const reducers = createHandleActions({
[SELECT_INDEXER_SCHEMA]: (state, { payload }) => {
return selectSchema(state, payload, (selectedSchema) => {
selectedSchema.name = payload.name ?? payload.implementationName;
selectedSchema.implementationName = payload.implementationName;
selectedSchema.enable = selectedSchema.supportsRss;
return selectedSchema;
@@ -208,7 +210,7 @@ export const reducers = createHandleActions({
// Set the name in pendingChanges
newState.pendingChanges = {
name: `${item.name} - Copy`
name: translate('DefaultNameCopiedProfile', { name: item.name })
};
return updateSectionState(state, section, newState);

View File

@@ -0,0 +1,81 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'indexerHistory';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_INDEXER_HISTORY = 'indexerHistory/fetchIndexerHistory';
export const CLEAR_INDEXER_HISTORY = 'indexerHistory/clearIndexerHistory';
//
// Action Creators
export const fetchIndexerHistory = createThunk(FETCH_INDEXER_HISTORY);
export const clearIndexerHistory = createAction(CLEAR_INDEXER_HISTORY);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_INDEXER_HISTORY]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/history/indexer',
data: payload
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_INDEXER_HISTORY]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);

View File

@@ -3,6 +3,7 @@ import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
import translate from 'Utilities/String/translate';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
@@ -55,19 +56,20 @@ export const defaultState = {
filterBuilderProps: [
{
name: 'startDate',
label: 'Start Date',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.DATE
name: 'indexers',
label: () => translate('Indexers'),
type: filterBuilderTypes.CONTAINS,
valueType: filterBuilderValueTypes.INDEXER
},
{
name: 'endDate',
label: 'End Date',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.DATE
name: 'tags',
label: () => translate('Tags'),
type: filterBuilderTypes.CONTAINS,
valueType: filterBuilderValueTypes.TAG
}
],
selectedFilterKey: 'all'
selectedFilterKey: 'all',
customFilters: []
};
export const persistState = [
@@ -81,6 +83,10 @@ export const persistState = [
export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats';
export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter';
function getCustomFilters(state, type) {
return state.customFilters.items.filter((customFilter) => customFilter.type === type);
}
//
// Action Creators
@@ -94,23 +100,35 @@ export const actionHandlers = handleThunks({
[FETCH_INDEXER_STATS]: function(getState, payload, dispatch) {
const state = getState();
const indexerStats = state.indexerStats;
const customFilters = getCustomFilters(state, section);
const selectedFilters = findSelectedFilters(indexerStats.selectedFilterKey, indexerStats.filters, customFilters);
const requestParams = {
endDate: moment().toISOString()
};
selectedFilters.forEach((selectedFilter) => {
if (selectedFilter.key === 'indexers') {
requestParams.indexers = selectedFilter.value.join(',');
}
if (selectedFilter.key === 'tags') {
requestParams.tags = selectedFilter.value.join(',');
}
});
if (indexerStats.selectedFilterKey !== 'all') {
let dayCount = 7;
if (indexerStats.selectedFilterKey === 'lastSeven') {
requestParams.startDate = moment().add(-7, 'days').endOf('day').toISOString();
}
if (indexerStats.selectedFilterKey === 'lastThirty') {
dayCount = 30;
requestParams.startDate = moment().add(-30, 'days').endOf('day').toISOString();
}
if (indexerStats.selectedFilterKey === 'lastNinety') {
dayCount = 90;
requestParams.startDate = moment().add(-90, 'days').endOf('day').toISOString();
}
requestParams.startDate = moment().add(-dayCount, 'days').endOf('day').toISOString();
}
const basesAttrs = {

View File

@@ -1,7 +1,9 @@
import $ from 'jquery';
import React from 'react';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import Icon from 'Components/Icon';
import { filterBuilderTypes, filterBuilderValueTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState';
@@ -110,7 +112,11 @@ export const defaultState = {
},
{
name: 'indexerFlags',
columnLabel: 'Indexer Flags',
columnLabel: () => translate('IndexerFlags'),
label: React.createElement(Icon, {
name: icons.FLAG,
title: () => translate('IndexerFlags')
}),
isSortable: true,
isVisible: true
},

View File

@@ -187,6 +187,7 @@ module.exports = {
//
// Charts
chartBackgroundColor: '#262626',
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']

View File

@@ -187,6 +187,7 @@ module.exports = {
//
// Charts
chartBackgroundColor: '#fff',
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']

View File

@@ -4,7 +4,7 @@ import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
@@ -110,7 +110,7 @@ class BackupRow extends Component {
{formatBytes(size)}
</TableRowCell>
<RelativeDateCellConnector
<RelativeDateCell
date={time}
/>
@@ -138,7 +138,7 @@ class BackupRow extends Component {
isOpen={isConfirmDeleteModalOpen}
kind={kinds.DANGER}
title={translate('DeleteBackup')}
message={translate('DeleteBackupMessageText', [name])}
message={translate('DeleteBackupMessageText', { name })}
confirmLabel={translate('Delete')}
onConfirm={this.onConfirmDeletePress}
onCancel={this.onConfirmDeleteModalClose}

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
@@ -98,7 +98,7 @@ class LogsTableRow extends Component {
if (name === 'time') {
return (
<RelativeDateCellConnector
<RelativeDateCell
key={name}
date={time}
/>

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import styles from './LogFilesTableRow.css';
@@ -22,7 +22,7 @@ class LogFilesTableRow extends Component {
<TableRow>
<TableRowCell>{filename}</TableRowCell>
<RelativeDateCellConnector
<RelativeDateCell
date={lastWriteTime}
/>

View File

@@ -33,6 +33,14 @@ function getInternalLink(source) {
to="/settings/downloadclients"
/>
);
case 'NotificationStatusCheck':
return (
<IconButton
name={icons.SETTINGS}
title={translate('Settings')}
to="/settings/connect"
/>
);
case 'IndexerRssCheck':
case 'IndexerSearchCheck':
case 'IndexerStatusCheck':

View File

@@ -114,7 +114,7 @@ class Updates extends Component {
/>
<div className={styles.message}>
{translate('TheLatestVersionIsAlreadyInstalled', ['Prowlarr'])}
{translate('TheLatestVersionIsAlreadyInstalled', { appName: 'Prowlarr' })}
</div>
{

View File

@@ -0,0 +1,19 @@
export default function abbreviateNumber(num, decimalPlaces) {
if (num === null) {
return null;
}
if (num === 0) {
return '0';
}
decimalPlaces = (!decimalPlaces || decimalPlaces < 0) ? 0 : decimalPlaces;
const b = (num).toPrecision(2).split('e');
const k = b.length === 1 ? 0 : Math.floor(Math.min(b[1].slice(1), 14) / 3);
const c = k < 1 ? num.toFixed(0 + decimalPlaces) : (num / Math.pow(10, k * 3) ).toFixed(1 + decimalPlaces);
const d = c < 0 ? c : Math.abs(c);
const e = d + ['', 'K', 'M', 'B', 'T'][k];
return e;
}

View File

@@ -25,14 +25,19 @@ export async function fetchTranslations(): Promise<boolean> {
export default function translate(
key: string,
args?: (string | number | boolean)[]
tokens?: Record<string, string | number | boolean>
) {
const translation = translations[key] || key;
if (args) {
return translation.replace(/\{(\d+)\}/g, (match, index) => {
return String(args[index]) ?? match;
if (tokens) {
// Fallback to the old behaviour for translations not yet updated to use named tokens
Object.values(tokens).forEach((value, index) => {
tokens[index] = value;
});
return translation.replace(/\{([a-z0-9]+?)\}/gi, (match, tokenMatch) =>
String(tokens[tokenMatch] ?? match)
);
}
return translation;

View File

@@ -0,0 +1,21 @@
import ModelBase from 'App/ModelBase';
export interface HistoryData {
source: string;
host: string;
limit: number;
offset: number;
elapsedTime: number;
query: string;
queryType: string;
}
interface History extends ModelBase {
indexerId: number;
date: string;
successful: boolean;
eventType: string;
data: HistoryData;
}
export default History;

View File

@@ -29,7 +29,7 @@
"@fortawesome/free-solid-svg-icons": "6.4.0",
"@fortawesome/react-fontawesome": "0.2.0",
"@juggle/resize-observer": "3.4.0",
"@microsoft/signalr": "6.0.16",
"@microsoft/signalr": "6.0.21",
"@sentry/browser": "7.51.2",
"@sentry/integrations": "7.51.2",
"@types/node": "18.15.11",
@@ -71,6 +71,7 @@
"react-redux": "7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-tabs": "4.3.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.1",
"react-virtualized": "9.21.1",
@@ -85,17 +86,13 @@
"typescript": "5.0.4"
},
"devDependencies": {
"@babel/core": "7.22.9",
"@babel/eslint-parser": "7.22.9",
"@babel/plugin-proposal-class-properties": "7.18.6",
"@babel/core": "7.22.11",
"@babel/eslint-parser": "7.22.11",
"@babel/plugin-proposal-export-default-from": "7.22.5",
"@babel/plugin-proposal-export-namespace-from": "7.18.9",
"@babel/plugin-proposal-nullish-coalescing-operator": "7.18.6",
"@babel/plugin-proposal-optional-chaining": "7.21.0",
"@babel/plugin-syntax-dynamic-import": "7.8.3",
"@babel/preset-env": "7.22.9",
"@babel/preset-env": "7.22.14",
"@babel/preset-react": "7.22.5",
"@babel/preset-typescript": "7.22.5",
"@babel/preset-typescript": "7.22.11",
"@types/lodash": "4.14.194",
"@types/react-router-dom": "5.3.3",
"@types/react-text-truncate": "0.14.1",
@@ -108,7 +105,7 @@
"babel-loader": "9.1.3",
"babel-plugin-inline-classnames": "2.0.1",
"babel-plugin-transform-react-remove-prop-types": "0.4.24",
"core-js": "3.31.1",
"core-js": "3.32.1",
"css-loader": "6.7.3",
"css-modules-typescript-loader": "4.0.1",
"eslint": "8.45.0",

View File

@@ -39,6 +39,10 @@ namespace NzbDrone.Automation.Test.PageModel
var element = d.FindElement(By.ClassName("followingBalls"));
return !element.Displayed;
}
catch (StaleElementReferenceException)
{
return true;
}
catch (NoSuchElementException)
{
return true;

View File

@@ -104,6 +104,9 @@ namespace NzbDrone.Common.Test.InstrumentationTests
// RSS
[TestCase(@"<atom:link href = ""https://api.nzb.su/api?t=search&amp;extended=1&amp;cat=3030&apikey=mySecret&amp;q=Diggers"" rel=""self"" type=""application/rss+xml"" />")]
// Applications
[TestCase(@"""name"":""apiKey"",""value"":""mySecret""")]
// Internal
[TestCase(@"[Info] MigrationController: *** Migrating Database=prowlarr-main;Host=postgres14;Username=mySecret;Password=mySecret;Port=5432;Enlist=False ***")]
[TestCase("/readarr/signalr/messages/negotiate?access_token=1234530f422f4aacb6b301233210aaaa&negotiateVersion=1")]

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