mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2026-03-05 13:40:08 -05:00
Compare commits
403 Commits
v1.8.1.383
...
v1.14.3.43
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f0a8d22e84 | ||
|
|
50c6f15e12 | ||
|
|
2e3a95f389 | ||
|
|
3d52096eb4 | ||
|
|
e981cacbda | ||
|
|
218371a318 | ||
|
|
30fd7c8c2a | ||
|
|
96d2d61fa0 | ||
|
|
17ff86aaea | ||
|
|
7f8c1ace14 | ||
|
|
dc0edb7bc1 | ||
|
|
2ac996c9f9 | ||
|
|
2ebabd69b5 | ||
|
|
b3738f1602 | ||
|
|
882152b911 | ||
|
|
a25e79031f | ||
|
|
cc85060b1b | ||
|
|
00bd9c241a | ||
|
|
1283e06f95 | ||
|
|
ab0108778a | ||
|
|
099b04f718 | ||
|
|
ecdc0a51a9 | ||
|
|
6c7c37affe | ||
|
|
45d378a2d9 | ||
|
|
007601cb19 | ||
|
|
5f0d6e2fdd | ||
|
|
ede9879c99 | ||
|
|
7287abc77c | ||
|
|
8c653b5c09 | ||
|
|
15c6b3c308 | ||
|
|
9676447c74 | ||
|
|
5d35f1dcc7 | ||
|
|
858f16195e | ||
|
|
a1a5dd574e | ||
|
|
a5ecc2dc9f | ||
|
|
7d46660583 | ||
|
|
22cbf40e3c | ||
|
|
25821c758f | ||
|
|
6153737a78 | ||
|
|
07adb45d63 | ||
|
|
02bc40b9b6 | ||
|
|
83e7e30e4f | ||
|
|
ae870fd46a | ||
|
|
33b7ba8725 | ||
|
|
dd2567a85f | ||
|
|
264ff8f885 | ||
|
|
629c6a8891 | ||
|
|
0ce2f96789 | ||
|
|
cd7d1571db | ||
|
|
4558f55282 | ||
|
|
21589fda57 | ||
|
|
3496263cd2 | ||
|
|
1bb1ec6106 | ||
|
|
2bfb838933 | ||
|
|
9eb291f578 | ||
|
|
8cf892124c | ||
|
|
47fb886930 | ||
|
|
5034a211cb | ||
|
|
ed1364b6ff | ||
|
|
71e18b616d | ||
|
|
f7bf21df68 | ||
|
|
d764e3405d | ||
|
|
16baceb784 | ||
|
|
5d2b80d15a | ||
|
|
a20a81f424 | ||
|
|
ebb66e9086 | ||
|
|
cb8797693e | ||
|
|
255c6335ae | ||
|
|
155cd53dcd | ||
|
|
ae70a96c10 | ||
|
|
16c0daf090 | ||
|
|
34c78c5a9d | ||
|
|
dd5b108ffd | ||
|
|
0b83986255 | ||
|
|
2bd25fb6f3 | ||
|
|
0f5eb5d3a3 | ||
|
|
c9434c61e3 | ||
|
|
ee969b7a06 | ||
|
|
cfdf88a1e2 | ||
|
|
70a5dae293 | ||
|
|
b53f8d4552 | ||
|
|
9668e91b21 | ||
|
|
49857693c0 | ||
|
|
850315ad1c | ||
|
|
86124d4319 | ||
|
|
4f28d583d7 | ||
|
|
01f3930211 | ||
|
|
7c7114c87a | ||
|
|
ef8e6d774b | ||
|
|
2960fc37d9 | ||
|
|
8bddf753bb | ||
|
|
cff24b3fd4 | ||
|
|
031d81330d | ||
|
|
6201b42fbd | ||
|
|
7022054dd7 | ||
|
|
c9b663247c | ||
|
|
0b0a0cfa5b | ||
|
|
3c0fea8b7c | ||
|
|
ac97952fd7 | ||
|
|
c3e40c0564 | ||
|
|
ce615a77c2 | ||
|
|
0f6dfe389c | ||
|
|
25d94a9286 | ||
|
|
52a690b41a | ||
|
|
56c8c3d6c6 | ||
|
|
5cee8990b3 | ||
|
|
9679e88717 | ||
|
|
f4203993ba | ||
|
|
4be0715fe3 | ||
|
|
f9c9d4a0e0 | ||
|
|
bf47380f7b | ||
|
|
e27a46f578 | ||
|
|
af4e69f8fb | ||
|
|
1b2106d4f0 | ||
|
|
bdfbda3805 | ||
|
|
cb98b10468 | ||
|
|
ae1bc8366c | ||
|
|
67eeb4373c | ||
|
|
9d40a64be4 | ||
|
|
20cc146d82 | ||
|
|
8beff32117 | ||
|
|
8b7c488173 | ||
|
|
1dabbc94f8 | ||
|
|
52667b979b | ||
|
|
14f9a75a73 | ||
|
|
ed17d91a7b | ||
|
|
f54280b888 | ||
|
|
7890ef6f9d | ||
|
|
df8e4e5acb | ||
|
|
1b36951879 | ||
|
|
d8d5170ab8 | ||
|
|
a443a87603 | ||
|
|
df18ee77e7 | ||
|
|
426159b452 | ||
|
|
8704bef69a | ||
|
|
0f1b01adab | ||
|
|
4dbf5aa9f4 | ||
|
|
e1264d7cda | ||
|
|
d4bbb2e14a | ||
|
|
a2395dccb5 | ||
|
|
df89450428 | ||
|
|
7b5e1f40ba | ||
|
|
fe142b8a1c | ||
|
|
8c7f73ed16 | ||
|
|
a4ae800603 | ||
|
|
e5d7a21714 | ||
|
|
67355ec07b | ||
|
|
43d56f6817 | ||
|
|
515f4dff8b | ||
|
|
c3aa377ed5 | ||
|
|
fbc3c09094 | ||
|
|
b88f8e5fde | ||
|
|
aaf4c73c83 | ||
|
|
7c38ec2baa | ||
|
|
dcfdd49119 | ||
|
|
badb9dee61 | ||
|
|
12ca705556 | ||
|
|
a0d0e3e232 | ||
|
|
e12cd68772 | ||
|
|
9dc117191e | ||
|
|
fdaca64d60 | ||
|
|
6d134750ff | ||
|
|
bbf9945b49 | ||
|
|
b66a85269f | ||
|
|
81e9c29d54 | ||
|
|
3df33e1a86 | ||
|
|
c19802c471 | ||
|
|
36c8806f88 | ||
|
|
0d69a42ae0 | ||
|
|
fc482d4808 | ||
|
|
fab4bd5ead | ||
|
|
9c243c7e0d | ||
|
|
c107011659 | ||
|
|
c690e9a50a | ||
|
|
c4b0ecdafe | ||
|
|
f2a709f725 | ||
|
|
01a9799ce8 | ||
|
|
8ac68e8946 | ||
|
|
6cb787c17b | ||
|
|
51259ceb30 | ||
|
|
92cf329174 | ||
|
|
257c9ab248 | ||
|
|
cad42fd005 | ||
|
|
d7927f60fe | ||
|
|
26a657fa77 | ||
|
|
7fa4daae9b | ||
|
|
e1b063eaa5 | ||
|
|
75f00436ec | ||
|
|
343d7088c9 | ||
|
|
709dfe453b | ||
|
|
3130fac106 | ||
|
|
28004dfae1 | ||
|
|
9b34c89bc8 | ||
|
|
28e90acd0d | ||
|
|
9d11d7e17f | ||
|
|
2cbdb5bcba | ||
|
|
118bfb8c28 | ||
|
|
942477ecf9 | ||
|
|
4b4589ed27 | ||
|
|
bd0609639e | ||
|
|
ccdad3a44c | ||
|
|
d99da0481b | ||
|
|
da1965b18e | ||
|
|
493114f4e8 | ||
|
|
6969326092 | ||
|
|
95f899131d | ||
|
|
0ba4f3e692 | ||
|
|
a7c00a0fd7 | ||
|
|
c84ff60ec9 | ||
|
|
b3f6f54e6e | ||
|
|
ed272aaf74 | ||
|
|
c0b10f889b | ||
|
|
bbfb92bbd8 | ||
|
|
793de05e3d | ||
|
|
1b1f9d16be | ||
|
|
051dea30c2 | ||
|
|
75d8a3d1d0 | ||
|
|
edf41e2ead | ||
|
|
c15c71386d | ||
|
|
71a19efd9a | ||
|
|
2c6c0fcc81 | ||
|
|
203e2dbb10 | ||
|
|
6169fc2fa3 | ||
|
|
768ce14afb | ||
|
|
31d32e8c30 | ||
|
|
7a61761b2b | ||
|
|
3963807c96 | ||
|
|
e0f6726a3d | ||
|
|
dd25bff3d6 | ||
|
|
d834c4292e | ||
|
|
7e8272ec2b | ||
|
|
62548f32fe | ||
|
|
db9f061564 | ||
|
|
b37d8799a0 | ||
|
|
4366530409 | ||
|
|
c7959f735e | ||
|
|
be3ee00e1f | ||
|
|
dace1982d6 | ||
|
|
980bd35f95 | ||
|
|
4b2f81bee8 | ||
|
|
30eb481c65 | ||
|
|
29f1c36f54 | ||
|
|
f1c01343bf | ||
|
|
bae79b22ad | ||
|
|
229d879f86 | ||
|
|
d1cee950a4 | ||
|
|
7e32b54547 | ||
|
|
b1f7d30021 | ||
|
|
c41a7e0ccc | ||
|
|
42c533386b | ||
|
|
bdae7a2cdc | ||
|
|
5e8d3542f4 | ||
|
|
d9d2aa8493 | ||
|
|
09bf1500d6 | ||
|
|
34464160cb | ||
|
|
bada5fe309 | ||
|
|
b088febbc4 | ||
|
|
1a307b8e21 | ||
|
|
32db2af0ea | ||
|
|
e602862102 | ||
|
|
bd5336e4c4 | ||
|
|
c664eaa9b5 | ||
|
|
b7e57f0c08 | ||
|
|
c06bf0e4ea | ||
|
|
c6db30c35a | ||
|
|
75c30dd318 | ||
|
|
6e7bf55dbd | ||
|
|
eb642dd2f9 | ||
|
|
19a196e2c7 | ||
|
|
93ec6cf89b | ||
|
|
52c6b56a4c | ||
|
|
82688d8a55 | ||
|
|
c81cbc801a | ||
|
|
993d189c61 | ||
|
|
1901af5a51 | ||
|
|
c1b399be39 | ||
|
|
2100e96570 | ||
|
|
3ff144421d | ||
|
|
f37ccba3f9 | ||
|
|
181cb2e0fe | ||
|
|
93c81bb7d3 | ||
|
|
7dd289b5f9 | ||
|
|
09cef8cf94 | ||
|
|
ca08c818e6 | ||
|
|
3e95bc4056 | ||
|
|
e241112915 | ||
|
|
0d98c12fa2 | ||
|
|
a0bcf5c9ae | ||
|
|
e318a47b3a | ||
|
|
b8df720c6c | ||
|
|
9625be723d | ||
|
|
d4b037db78 | ||
|
|
add2988789 | ||
|
|
9869c2272a | ||
|
|
4c8b0c9eec | ||
|
|
43cb22ff2b | ||
|
|
3cabc0589a | ||
|
|
cdb3ed36f6 | ||
|
|
840f2ae3e6 | ||
|
|
3ed6ef0336 | ||
|
|
c2ae0cce03 | ||
|
|
934b908b37 | ||
|
|
6c831f11a6 | ||
|
|
9adbfd2391 | ||
|
|
4a7cc82f0d | ||
|
|
c061c309bd | ||
|
|
0f3a77c336 | ||
|
|
478d5a624f | ||
|
|
3283d144f5 | ||
|
|
1a9ec4febd | ||
|
|
0598211319 | ||
|
|
0b0d6b7590 | ||
|
|
86cec51ebe | ||
|
|
80e5ac4aa9 | ||
|
|
ee5ed0c91b | ||
|
|
ba278930ed | ||
|
|
6449b89eb6 | ||
|
|
73b85e240e | ||
|
|
6338460ff4 | ||
|
|
0463e66881 | ||
|
|
bd75621437 | ||
|
|
9615c1183d | ||
|
|
bbf042ed55 | ||
|
|
98e948dbb2 | ||
|
|
2af9f7eb8d | ||
|
|
96413f99c7 | ||
|
|
d44b946d30 | ||
|
|
fe9cad5697 | ||
|
|
098be3cff6 | ||
|
|
8f2fea0be8 | ||
|
|
8d035c6c1f | ||
|
|
7dbfa74c40 | ||
|
|
caaf50ed9c | ||
|
|
b472a022a6 | ||
|
|
0a439a4a96 | ||
|
|
4410636b97 | ||
|
|
ba3ebc7574 | ||
|
|
2ce49a0785 | ||
|
|
d7df946c2b | ||
|
|
3dd3c80b54 | ||
|
|
0f160707d3 | ||
|
|
b608e38454 | ||
|
|
c873b3ffac | ||
|
|
07b98f4137 | ||
|
|
09606af351 | ||
|
|
1d79b92fca | ||
|
|
fbcf1b03c5 | ||
|
|
dee98ac46f | ||
|
|
4267b8a244 | ||
|
|
00dc55996c | ||
|
|
b912cc6110 | ||
|
|
56f0c137f8 | ||
|
|
1b8ff9b989 | ||
|
|
bfecf35a8b | ||
|
|
80da5ce165 | ||
|
|
60ca0db26f | ||
|
|
288a3d1495 | ||
|
|
4c42907eb2 | ||
|
|
6300eb1442 | ||
|
|
e4c0edf24c | ||
|
|
74a9fa784a | ||
|
|
1b0c9adf24 | ||
|
|
0eaa538e8a | ||
|
|
39a54eb8f6 | ||
|
|
5ad6237785 | ||
|
|
9fee4f914f | ||
|
|
ba2aab6bb3 | ||
|
|
5c8ae82f11 | ||
|
|
bcbeac1e83 | ||
|
|
b36d793d85 | ||
|
|
b0162ccc5b | ||
|
|
f0892eb4b8 | ||
|
|
e456979467 | ||
|
|
66ca47b615 | ||
|
|
2b7771bfe0 | ||
|
|
955bc472a1 | ||
|
|
e024bba6b6 | ||
|
|
aeb3b7d8b5 | ||
|
|
a7b25b8b93 | ||
|
|
130257fdd4 | ||
|
|
b618f23bc0 | ||
|
|
a758161e31 | ||
|
|
27928103c5 | ||
|
|
d5b3961e8a | ||
|
|
307adf053e | ||
|
|
31261f66ad | ||
|
|
5dbb59dfaa | ||
|
|
25c1803d0e | ||
|
|
9f4c9d3344 | ||
|
|
dfb00d9bb1 | ||
|
|
f7727855b5 | ||
|
|
1e4c67dcdb | ||
|
|
26afcb0071 | ||
|
|
7a937e85a4 | ||
|
|
7cd82321b4 | ||
|
|
8c9adba516 | ||
|
|
03fa9254e3 | ||
|
|
e66ecf5c95 | ||
|
|
e0dddfa215 | ||
|
|
bcb8afadf8 | ||
|
|
fc4a0979c3 | ||
|
|
5f643b2ced | ||
|
|
6f09b0f4f5 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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
|
||||
|
||||
3
.github/ISSUE_TEMPLATE/config.yml
vendored
3
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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.
|
||||
|
||||
3
.github/label-actions.yml
vendored
3
.github/label-actions.yml
vendored
@@ -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'
|
||||
|
||||
|
||||
2
.github/workflows/label-actions.yml
vendored
2
.github/workflows/label-actions.yml
vendored
@@ -18,6 +18,6 @@ jobs:
|
||||
action:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/label-actions@v3
|
||||
- uses: dessant/label-actions@v4
|
||||
with:
|
||||
process-only: 'issues, prs'
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://dev.azure.com/Prowlarr/Prowlarr/_build/latest?definitionId=1&branchName=develop)
|
||||
[](https://translate.servarr.com/engage/prowlarr/?utm_source=widget)
|
||||
[](https://wiki.servarr.com/prowlarr/installation#docker)
|
||||
[](https://wiki.servarr.com/prowlarr/installation/docker)
|
||||

|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
@@ -29,7 +29,6 @@ Prowlarr is an indexer manager/proxy built on the popular \*arr .net/reactjs bas
|
||||
|
||||
[](https://wiki.servarr.com/prowlarr)
|
||||
[](https://prowlarr.com/discord)
|
||||
[](https://www.reddit.com/r/Prowlarr)
|
||||
|
||||
Note: GitHub Issues are for Bugs and Feature Requests Only
|
||||
|
||||
|
||||
@@ -9,15 +9,15 @@ variables:
|
||||
testsFolder: './_tests'
|
||||
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
|
||||
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
|
||||
majorVersion: '1.8.1'
|
||||
majorVersion: '1.14.3'
|
||||
minorVersion: $[counter('minorVersion', 1)]
|
||||
prowlarrVersion: '$(majorVersion).$(minorVersion)'
|
||||
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
|
||||
sentryOrg: 'servarr'
|
||||
sentryUrl: 'https://sentry.servarr.com'
|
||||
dotnetVersion: '6.0.408'
|
||||
innoVersion: '6.2.0'
|
||||
nodeVersion: '16.x'
|
||||
dotnetVersion: '6.0.417'
|
||||
nodeVersion: '20.X'
|
||||
innoVersion: '6.2.2'
|
||||
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:
|
||||
@@ -565,6 +570,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=prowlarr \
|
||||
-e POSTGRES_USER=prowlarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -577,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
|
||||
@@ -663,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:
|
||||
@@ -710,6 +769,7 @@ stages:
|
||||
-e POSTGRES_PASSWORD=prowlarr \
|
||||
-e POSTGRES_USER=prowlarr \
|
||||
-p 5432:5432/tcp \
|
||||
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
|
||||
postgres:14
|
||||
displayName: Start postgres
|
||||
- bash: |
|
||||
@@ -720,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
|
||||
|
||||
@@ -1003,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
|
||||
@@ -1083,6 +1206,7 @@ stages:
|
||||
- stage: Report_Out
|
||||
dependsOn:
|
||||
- Analyze
|
||||
- Installer
|
||||
- Unit_Test
|
||||
- Integration
|
||||
- Automation
|
||||
|
||||
2
build.sh
2
build.sh
@@ -254,7 +254,7 @@ InstallInno()
|
||||
ProgressStart "Installing portable Inno Setup"
|
||||
|
||||
rm -rf _inno
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.0}.exe"
|
||||
curl -s --output innosetup.exe "https://files.jrsoftware.org/is/6/innosetup-${INNOVERSION:-6.2.2}.exe"
|
||||
mkdir _inno
|
||||
./innosetup.exe //portable=1 //silent //currentuser //dir=.\\_inno
|
||||
rm innosetup.exe
|
||||
|
||||
@@ -2,16 +2,18 @@ const loose = true;
|
||||
|
||||
module.exports = {
|
||||
plugins: [
|
||||
'@babel/plugin-transform-logical-assignment-operators',
|
||||
|
||||
// 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: {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.version {
|
||||
margin: 0 3px;
|
||||
font-weight: bold;
|
||||
font-family: var(--defaultFontFamily);
|
||||
}
|
||||
|
||||
.maintenance {
|
||||
|
||||
@@ -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')}
|
||||
</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', { 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>
|
||||
|
||||
@@ -28,11 +28,11 @@ function ConnectionLostModal(props) {
|
||||
|
||||
<ModalBody>
|
||||
<div>
|
||||
{translate('ConnectionLostMessage')}
|
||||
{translate('ConnectionLostToBackend')}
|
||||
</div>
|
||||
|
||||
<div className={styles.automatic}>
|
||||
{translate('ConnectionLostAutomaticMessage')}
|
||||
{translate('ConnectionLostReconnect')}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import SortDirection from 'Helpers/Props/SortDirection';
|
||||
import { FilterBuilderProp } from './AppState';
|
||||
|
||||
export interface Error {
|
||||
responseJSON: {
|
||||
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
|
||||
pageSize: number;
|
||||
}
|
||||
|
||||
export interface AppSectionFilterState<T> {
|
||||
filterBuilderProps: FilterBuilderProp<T>[];
|
||||
}
|
||||
|
||||
export interface AppSectionSchemaState<T> {
|
||||
isSchemaFetching: boolean;
|
||||
isSchemaPopulated: boolean;
|
||||
|
||||
@@ -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;
|
||||
|
||||
14
frontend/src/App/State/HistoryAppState.ts
Normal file
14
frontend/src/App/State/HistoryAppState.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import Column from 'Components/Table/Column';
|
||||
import History from 'typings/History';
|
||||
|
||||
interface HistoryAppState
|
||||
extends AppSectionState<History>,
|
||||
AppSectionFilterState<History> {
|
||||
pageSize: number;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export default HistoryAppState;
|
||||
@@ -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;
|
||||
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import AppSectionState, {
|
||||
AppSectionItemState,
|
||||
AppSectionSaveState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import Application from 'typings/Application';
|
||||
import DownloadClient from 'typings/DownloadClient';
|
||||
import Notification from 'typings/Notification';
|
||||
@@ -25,6 +26,11 @@ export interface DownloadClientAppState
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface IndexerCategoryAppState
|
||||
extends AppSectionState<IndexerCategory>,
|
||||
AppSectionDeleteState,
|
||||
AppSectionSaveState {}
|
||||
|
||||
export interface NotificationAppState
|
||||
extends AppSectionState<Notification>,
|
||||
AppSectionDeleteState {}
|
||||
@@ -35,6 +41,7 @@ interface SettingsAppState {
|
||||
appProfiles: AppProfileAppState;
|
||||
applications: ApplicationAppState;
|
||||
downloadClients: DownloadClientAppState;
|
||||
indexerCategories: IndexerCategoryAppState;
|
||||
notifications: NotificationAppState;
|
||||
ui: UiSettingsAppState;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface CommandBody {
|
||||
lastStartTime: string;
|
||||
trigger: string;
|
||||
suppressMessages: boolean;
|
||||
seriesId?: number;
|
||||
}
|
||||
|
||||
interface Command extends ModelBase {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import AppState from 'App/State/AppState';
|
||||
import { IndexerCategory } from 'Indexer/Indexer';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const indexerCategoriesSelector = createSelector(
|
||||
(state: AppState) => state.settings.indexerCategories,
|
||||
(categories) => categories.items
|
||||
);
|
||||
|
||||
function CategoryFilterBuilderRowValue(props: FilterBuilderRowValueProps) {
|
||||
const categories: IndexerCategory[] = useSelector(indexerCategoriesSelector);
|
||||
|
||||
const tagList = categories.reduce(
|
||||
(acc: { id: number; name: string }[], element) => {
|
||||
acc.push({
|
||||
id: element.id,
|
||||
name: `${element.name} (${element.id})`,
|
||||
});
|
||||
|
||||
if (element.subCategories && element.subCategories.length > 0) {
|
||||
element.subCategories.forEach((subCat) => {
|
||||
acc.push({
|
||||
id: subCat.id,
|
||||
name: `${subCat.name} (${subCat.id})`,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
return <FilterBuilderRowValue {...props} tagList={tagList} />;
|
||||
}
|
||||
|
||||
export default CategoryFilterBuilderRowValue;
|
||||
@@ -1,3 +1,4 @@
|
||||
import { maxBy } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
@@ -50,7 +51,7 @@ class FilterBuilderModalContent extends Component {
|
||||
if (id) {
|
||||
dispatchSetFilter({ selectedFilterKey: id });
|
||||
} else {
|
||||
const last = customFilters[customFilters.length -1];
|
||||
const last = maxBy(customFilters, 'id');
|
||||
dispatchSetFilter({ selectedFilterKey: last.id });
|
||||
}
|
||||
|
||||
@@ -108,7 +109,7 @@ class FilterBuilderModalContent extends Component {
|
||||
this.setState({
|
||||
labelErrors: [
|
||||
{
|
||||
message: 'Label is required'
|
||||
message: translate('LabelIsRequired')
|
||||
}
|
||||
]
|
||||
});
|
||||
@@ -146,13 +147,13 @@ class FilterBuilderModalContent extends Component {
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Custom Filter
|
||||
{translate('CustomFilter')}
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.labelContainer}>
|
||||
<div className={styles.label}>
|
||||
Label
|
||||
{translate('Label')}
|
||||
</div>
|
||||
|
||||
<div className={styles.labelInputContainer}>
|
||||
|
||||
@@ -5,8 +5,10 @@ import IconButton from 'Components/Link/IconButton';
|
||||
import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Props';
|
||||
import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderRowValueConnector';
|
||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import CategoryFilterBuilderRowValue from './CategoryFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
@@ -55,9 +57,15 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
||||
case filterBuilderValueTypes.BOOL:
|
||||
return BoolFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.CATEGORY:
|
||||
return CategoryFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { FilterBuilderProp } from 'App/State/AppState';
|
||||
|
||||
interface FilterBuilderRowOnChangeProps {
|
||||
name: string;
|
||||
value: unknown[];
|
||||
}
|
||||
|
||||
interface FilterBuilderRowValueProps {
|
||||
filterType?: string;
|
||||
filterValue: string | number | object | string[] | number[] | object[];
|
||||
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||
sectionItem: unknown[];
|
||||
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||
}
|
||||
|
||||
export default FilterBuilderRowValueProps;
|
||||
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: 1,
|
||||
get name() {
|
||||
return translate('Grabbed');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
get name() {
|
||||
return translate('IndexerRss');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
get name() {
|
||||
return translate('IndexerQuery');
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
get name() {
|
||||
return translate('IndexerAuth');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
function HistoryEventTypeFilterBuilderRowValue(
|
||||
props: FilterBuilderRowValueProps
|
||||
) {
|
||||
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||
}
|
||||
|
||||
export default HistoryEventTypeFilterBuilderRowValue;
|
||||
@@ -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) {
|
||||
|
||||
@@ -37,8 +37,8 @@ class CustomFilter extends Component {
|
||||
dispatchSetFilter
|
||||
} = this.props;
|
||||
|
||||
// Assume that delete and then unmounting means the delete was successful.
|
||||
// Moving this check to a ancestor would be more accurate, but would have
|
||||
// Assume that delete and then unmounting means the deletion was successful.
|
||||
// Moving this check to an ancestor would be more accurate, but would have
|
||||
// more boilerplate.
|
||||
if (this.state.isDeleting && id === selectedFilterKey) {
|
||||
dispatchSetFilter({ selectedFilterKey: 'all' });
|
||||
|
||||
@@ -30,22 +30,24 @@ function CustomFiltersModalContent(props) {
|
||||
|
||||
<ModalBody>
|
||||
{
|
||||
customFilters.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((customFilter) => {
|
||||
return (
|
||||
<CustomFilter
|
||||
key={customFilter.id}
|
||||
id={customFilter.id}
|
||||
label={customFilter.label}
|
||||
filters={customFilter.filters}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
isDeleting={isDeleting}
|
||||
deleteError={deleteError}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
dispatchDeleteCustomFilter={dispatchDeleteCustomFilter}
|
||||
onEditPress={onEditCustomFilter}
|
||||
/>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
<div className={styles.addButtonContainer}>
|
||||
|
||||
@@ -24,16 +24,20 @@ function createMapStateToProps() {
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: translate('NoChange'),
|
||||
disabled: true
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
get value() {
|
||||
return `(${translate('Mixed')})`;
|
||||
},
|
||||
isDisabled: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const availabilityOptions = [
|
||||
{ key: 'announced', value: 'Announced' },
|
||||
{ key: 'inCinemas', value: 'In Cinemas' },
|
||||
{ key: 'released', value: 'Released' },
|
||||
{ key: 'preDB', value: 'PreDB' }
|
||||
];
|
||||
|
||||
function AvailabilitySelectInput(props) {
|
||||
const values = [...availabilityOptions];
|
||||
|
||||
const {
|
||||
includeNoChange,
|
||||
includeMixed
|
||||
} = props;
|
||||
|
||||
if (includeNoChange) {
|
||||
values.unshift({
|
||||
key: 'noChange',
|
||||
value: 'No Change',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
if (includeMixed) {
|
||||
values.unshift({
|
||||
key: 'mixed',
|
||||
value: '(Mixed)',
|
||||
disabled: true
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectInput
|
||||
{...props}
|
||||
values={values}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
AvailabilitySelectInput.propTypes = {
|
||||
includeNoChange: PropTypes.bool.isRequired,
|
||||
includeMixed: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
AvailabilitySelectInput.defaultProps = {
|
||||
includeNoChange: false,
|
||||
includeMixed: false
|
||||
};
|
||||
|
||||
export default AvailabilitySelectInput;
|
||||
@@ -24,7 +24,8 @@ function createMapStateToProps() {
|
||||
.sort(sortByName)
|
||||
.map((downloadClient) => ({
|
||||
key: downloadClient.id,
|
||||
value: downloadClient.name
|
||||
value: downloadClient.name,
|
||||
hint: `(${downloadClient.id})`
|
||||
}));
|
||||
|
||||
if (includeAny) {
|
||||
|
||||
@@ -147,7 +147,7 @@ EnhancedSelectInputConnector.propTypes = {
|
||||
provider: PropTypes.string.isRequired,
|
||||
providerData: PropTypes.object.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.number, PropTypes.string])).isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.arrayOf(PropTypes.string), PropTypes.arrayOf(PropTypes.number)]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectOptionsProviderAction: PropTypes.string,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
&:hover {
|
||||
background-color: var(--inputHoverBackgroundColor);
|
||||
}
|
||||
|
||||
&.isDisabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.optionCheck {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { inputTypes, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import AppProfileSelectInputConnector from './AppProfileSelectInputConnector';
|
||||
import AutoCompleteInput from './AutoCompleteInput';
|
||||
import AvailabilitySelectInput from './AvailabilitySelectInput';
|
||||
import CaptchaInputConnector from './CaptchaInputConnector';
|
||||
import CardigannCaptchaInputConnector from './CardigannCaptchaInputConnector';
|
||||
import CheckInput from './CheckInput';
|
||||
@@ -37,9 +36,6 @@ function getComponent(type) {
|
||||
case inputTypes.AUTO_COMPLETE:
|
||||
return AutoCompleteInput;
|
||||
|
||||
case inputTypes.AVAILABILITY_SELECT:
|
||||
return AvailabilitySelectInput;
|
||||
|
||||
case inputTypes.CAPTCHA:
|
||||
return CaptchaInputConnector;
|
||||
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-right: $formLabelRightMarginWidth;
|
||||
padding-top: 8px;
|
||||
min-height: 35px;
|
||||
text-align: end;
|
||||
font-weight: bold;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
|
||||
13
frontend/src/Components/Form/InfoInput.css
Normal file
13
frontend/src/Components/Form/InfoInput.css
Normal file
@@ -0,0 +1,13 @@
|
||||
.message {
|
||||
composes: alert from '~Components/Alert.css';
|
||||
|
||||
a {
|
||||
color: var(--linkColor);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
color: var(--linkHoverColor);
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'markAsFailedButton': string;
|
||||
'message': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -1,5 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Alert from 'Components/Alert';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import styles from './InfoInput.css';
|
||||
|
||||
class InfoInput extends Component {
|
||||
|
||||
@@ -7,12 +10,15 @@ class InfoInput extends Component {
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
value
|
||||
} = this.props;
|
||||
const { value } = this.props;
|
||||
|
||||
return (
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
<Alert
|
||||
kind={kinds.INFO}
|
||||
className={styles.message}
|
||||
>
|
||||
<span dangerouslySetInnerHTML={{ __html: value }} />
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,18 +40,26 @@ class FilterMenuContent extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
customFilters.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
customFilters.length > 0 ?
|
||||
<MenuItemSeparator /> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
customFilters
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
.map((filter) => {
|
||||
return (
|
||||
<FilterMenuItem
|
||||
key={filter.id}
|
||||
filterKey={filter.id}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
onPress={onFilterSelect}
|
||||
>
|
||||
{filter.label}
|
||||
</FilterMenuItem>
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import Link from 'Components/Link/Link';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './ModalContent.css';
|
||||
|
||||
function ModalContent(props) {
|
||||
@@ -28,6 +29,7 @@ function ModalContent(props) {
|
||||
<Icon
|
||||
name={icons.CLOSE}
|
||||
size={18}
|
||||
title={translate('Close')}
|
||||
/>
|
||||
</Link>
|
||||
}
|
||||
|
||||
@@ -78,6 +78,7 @@ class PageHeader extends Component {
|
||||
aria-label="Donate"
|
||||
to="https://prowlarr.com/donate"
|
||||
size={14}
|
||||
title={translate('Donate')}
|
||||
/>
|
||||
<IconButton
|
||||
className={styles.translate}
|
||||
|
||||
@@ -24,6 +24,7 @@ function PageHeaderActionsMenu(props) {
|
||||
<MenuButton className={styles.menuButton} aria-label="Menu Button">
|
||||
<Icon
|
||||
name={icons.INTERACTIVE}
|
||||
title={translate('Menu')}
|
||||
/>
|
||||
</MenuButton>
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -34,7 +34,8 @@ function AuthenticationRequiredModalContent(props) {
|
||||
authenticationMethod,
|
||||
authenticationRequired,
|
||||
username,
|
||||
password
|
||||
password,
|
||||
passwordConfirmation
|
||||
} = settings;
|
||||
|
||||
const authenticationEnabled = authenticationMethod && authenticationMethod.value !== 'none';
|
||||
@@ -63,71 +64,75 @@ function AuthenticationRequiredModalContent(props) {
|
||||
className={styles.authRequiredAlert}
|
||||
kind={kinds.WARNING}
|
||||
>
|
||||
{authenticationRequiredWarning}
|
||||
{translate('AuthenticationRequiredWarning')}
|
||||
</Alert>
|
||||
|
||||
{
|
||||
isPopulated && !error ?
|
||||
<div>
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('Authentication')}</FormLabel>
|
||||
<FormLabel>{translate('AuthenticationMethod')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="authenticationMethod"
|
||||
values={authenticationMethodOptions}
|
||||
helpText={translate('AuthenticationMethodHelpText')}
|
||||
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>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>{translate('PasswordConfirmation')}</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.PASSWORD}
|
||||
name="passwordConfirmation"
|
||||
onChange={onInputChange}
|
||||
helpTextWarning={passwordConfirmation?.value ? undefined : translate('AuthenticationRequiredPasswordConfirmationHelpTextWarning')}
|
||||
{...passwordConfirmation}
|
||||
/>
|
||||
</FormGroup>
|
||||
</div> :
|
||||
null
|
||||
}
|
||||
|
||||
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
17
frontend/src/Helpers/Hooks/useModalOpenState.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
export default function useModalOpenState(
|
||||
initialState: boolean
|
||||
): [boolean, () => void, () => void] {
|
||||
const [isOpen, setOpen] = useState(initialState);
|
||||
|
||||
const setModalOpen = useCallback(() => {
|
||||
setOpen(true);
|
||||
}, [setOpen]);
|
||||
|
||||
const setModalClosed = useCallback(() => {
|
||||
setOpen(false);
|
||||
}, [setOpen]);
|
||||
|
||||
return [isOpen, setModalOpen, setModalClosed];
|
||||
}
|
||||
@@ -2,9 +2,10 @@ export const BOOL = 'bool';
|
||||
export const BYTES = 'bytes';
|
||||
export const DATE = 'date';
|
||||
export const DEFAULT = 'default';
|
||||
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||
export const INDEXER = 'indexer';
|
||||
export const PROTOCOL = 'protocol';
|
||||
export const PRIVACY = 'privacy';
|
||||
export const APP_PROFILE = 'appProfile';
|
||||
export const MOVIE_STATUS = 'movieStatus';
|
||||
export const CATEGORY = 'category';
|
||||
export const TAG = 'tag';
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export const AUTO_COMPLETE = 'autoComplete';
|
||||
export const APP_PROFILE_SELECT = 'appProfileSelect';
|
||||
export const AVAILABILITY_SELECT = 'availabilitySelect';
|
||||
export const CAPTCHA = 'captcha';
|
||||
export const CARDIGANNCAPTCHA = 'cardigannCaptcha';
|
||||
export const CHECK = 'check';
|
||||
@@ -10,6 +9,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';
|
||||
@@ -26,7 +26,6 @@ export const TAG_SELECT = 'tagSelect';
|
||||
export const all = [
|
||||
AUTO_COMPLETE,
|
||||
APP_PROFILE_SELECT,
|
||||
AVAILABILITY_SELECT,
|
||||
CAPTCHA,
|
||||
CARDIGANNCAPTCHA,
|
||||
CHECK,
|
||||
@@ -35,6 +34,7 @@ export const all = [
|
||||
INFO,
|
||||
MOVIE_MONITORED_SELECT,
|
||||
CATEGORY_SELECT,
|
||||
FLOAT,
|
||||
NUMBER,
|
||||
OAUTH,
|
||||
PASSWORD,
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.markAsFailedButton {
|
||||
composes: button from '~Components/Link/Button.css';
|
||||
|
||||
margin-right: auto;
|
||||
}
|
||||
@@ -1,16 +1,13 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import 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
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptions
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import HistoryFilterModal from './HistoryFilterModal';
|
||||
import HistoryOptionsConnector from './HistoryOptionsConnector';
|
||||
import HistoryRowConnector from './HistoryRowConnector';
|
||||
|
||||
@@ -63,6 +64,7 @@ class History extends Component {
|
||||
columns,
|
||||
selectedFilterKey,
|
||||
filters,
|
||||
customFilters,
|
||||
totalRecords,
|
||||
onFilterSelect,
|
||||
onFirstPagePress,
|
||||
@@ -108,7 +110,8 @@ class History extends Component {
|
||||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={HistoryFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
@@ -193,8 +196,9 @@ History.propTypes = {
|
||||
indexersError: PropTypes.object,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
selectedFilterKey: PropTypes.string.isRequired,
|
||||
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
totalRecords: PropTypes.number,
|
||||
onFilterSelect: PropTypes.func.isRequired,
|
||||
onFirstPagePress: PropTypes.func.isRequired,
|
||||
|
||||
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
import History from './History';
|
||||
@@ -14,13 +15,15 @@ function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.indexers,
|
||||
createCustomFiltersSelector('history'),
|
||||
createCommandExecutingSelector(commandNames.CLEAR_HISTORY),
|
||||
(history, indexers, isHistoryClearing) => {
|
||||
(history, indexers, customFilters, isHistoryClearing) => {
|
||||
return {
|
||||
isIndexersFetching: indexers.isFetching,
|
||||
isIndexersPopulated: indexers.isPopulated,
|
||||
indexersError: indexers.error,
|
||||
isHistoryClearing,
|
||||
customFilters,
|
||||
...history
|
||||
};
|
||||
}
|
||||
|
||||
54
frontend/src/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/History/HistoryFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
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 { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||
|
||||
function createHistorySelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.items,
|
||||
(queueItems) => {
|
||||
return queueItems;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createFilterBuilderPropsSelector() {
|
||||
return createSelector(
|
||||
(state: AppState) => state.history.filterBuilderProps,
|
||||
(filterBuilderProps) => {
|
||||
return filterBuilderProps;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface HistoryFilterModalProps {
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||
const sectionItems = useSelector(createHistorySelector());
|
||||
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||
const customFilterType = 'history';
|
||||
|
||||
const dispatch = useDispatch();
|
||||
|
||||
const dispatchSetFilter = useCallback(
|
||||
(payload: unknown) => {
|
||||
dispatch(setHistoryFilter(payload));
|
||||
},
|
||||
[dispatch]
|
||||
);
|
||||
|
||||
return (
|
||||
<FilterModal
|
||||
// TODO: Don't spread all the props
|
||||
{...props}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
customFilterType={customFilterType}
|
||||
dispatchSetFilter={dispatchSetFilter}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
44
frontend/src/History/HistoryRowParameter.tsx
Normal file
44
frontend/src/History/HistoryRowParameter.tsx
Normal 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;
|
||||
@@ -1,6 +1,7 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
import { sizes } from 'Helpers/Props';
|
||||
import AddIndexerModalContentConnector from './AddIndexerModalContentConnector';
|
||||
import styles from './AddIndexerModal.css';
|
||||
|
||||
@@ -8,6 +9,7 @@ function AddIndexerModal({ isOpen, onModalClose, onSelectIndexer, ...otherProps
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
size={sizes.EXTRA_LARGE}
|
||||
onModalClose={onModalClose}
|
||||
className={styles.modal}
|
||||
>
|
||||
|
||||
@@ -16,7 +16,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import { kinds, scrollDirections } from 'Helpers/Props';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import SelectIndexerRowConnector from './SelectIndexerRowConnector';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
import styles from './AddIndexerModalContent.css';
|
||||
|
||||
const columns = [
|
||||
@@ -49,6 +49,12 @@ const columns = [
|
||||
label: () => translate('Privacy'),
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
name: 'categories',
|
||||
label: () => translate('Categories'),
|
||||
isSortable: false,
|
||||
isVisible: true
|
||||
}
|
||||
];
|
||||
|
||||
@@ -260,9 +266,10 @@ class AddIndexerModalContent extends Component {
|
||||
<TableBody>
|
||||
{
|
||||
filteredIndexers.map((indexer) => (
|
||||
<SelectIndexerRowConnector
|
||||
<SelectIndexerRow
|
||||
key={`${indexer.implementation}-${indexer.name}`}
|
||||
implementation={indexer.implementation}
|
||||
implementationName={indexer.implementationName}
|
||||
{...indexer}
|
||||
onIndexerSelect={onIndexerSelect}
|
||||
/>
|
||||
@@ -288,7 +295,7 @@ class AddIndexerModalContent extends Component {
|
||||
<div className={styles.available}>
|
||||
{
|
||||
isPopulated ?
|
||||
translate('CountIndexersAvailable', [filteredIndexers.length]) :
|
||||
translate('CountIndexersAvailable', { count: filteredIndexers.length }) :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
import { some } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchIndexerSchema, selectIndexerSchema, setIndexerSchemaSort } from 'Store/Actions/indexerActions';
|
||||
import createAllIndexersSelector from 'Store/Selectors/createAllIndexersSelector';
|
||||
import createClientSideCollectionSelector from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import AddIndexerModalContent from './AddIndexerModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createClientSideCollectionSelector('indexers.schema'),
|
||||
(indexers) => {
|
||||
createAllIndexersSelector(),
|
||||
(indexers, allIndexers) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
@@ -19,11 +22,19 @@ function createMapStateToProps() {
|
||||
sortKey
|
||||
} = indexers;
|
||||
|
||||
const indexerList = items.map((item) => {
|
||||
const { definitionName } = item;
|
||||
return {
|
||||
...item,
|
||||
isExistingIndexer: some(allIndexers, { definitionName })
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
indexers: items,
|
||||
indexers: indexerList,
|
||||
sortKey,
|
||||
sortDirection
|
||||
};
|
||||
@@ -49,8 +60,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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
class SelectIndexerRow extends Component {
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onPress = () => {
|
||||
const {
|
||||
implementation,
|
||||
name
|
||||
} = this.props;
|
||||
|
||||
this.props.onIndexerSelect({ implementation, name });
|
||||
};
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
protocol,
|
||||
privacy,
|
||||
name,
|
||||
language,
|
||||
description,
|
||||
isExistingIndexer
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={this.onPress}>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel
|
||||
protocol={protocol}
|
||||
/>
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{name}
|
||||
{
|
||||
isExistingIndexer ?
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title={translate('IndexerAlreadySetup')}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{language}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{description}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{translate(firstCharToUpper(privacy))}
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SelectIndexerRow.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
protocol: PropTypes.string.isRequired,
|
||||
privacy: PropTypes.string.isRequired,
|
||||
language: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
implementation: PropTypes.string.isRequired,
|
||||
onIndexerSelect: PropTypes.func.isRequired,
|
||||
isExistingIndexer: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
export default SelectIndexerRow;
|
||||
75
frontend/src/Indexer/Add/SelectIndexerRow.tsx
Normal file
75
frontend/src/Indexer/Add/SelectIndexerRow.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import Icon from 'Components/Icon';
|
||||
import TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||
import TableRowButton from 'Components/Table/TableRowButton';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import CapabilitiesLabel from 'Indexer/Index/Table/CapabilitiesLabel';
|
||||
import ProtocolLabel from 'Indexer/Index/Table/ProtocolLabel';
|
||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import styles from './SelectIndexerRow.css';
|
||||
|
||||
interface SelectIndexerRowProps {
|
||||
name: string;
|
||||
protocol: string;
|
||||
privacy: string;
|
||||
language: string;
|
||||
description: string;
|
||||
capabilities: IndexerCapabilities;
|
||||
implementation: string;
|
||||
implementationName: string;
|
||||
isExistingIndexer: boolean;
|
||||
onIndexerSelect(...args: unknown[]): void;
|
||||
}
|
||||
|
||||
function SelectIndexerRow(props: SelectIndexerRowProps) {
|
||||
const {
|
||||
name,
|
||||
protocol,
|
||||
privacy,
|
||||
language,
|
||||
description,
|
||||
capabilities,
|
||||
implementation,
|
||||
implementationName,
|
||||
isExistingIndexer,
|
||||
onIndexerSelect,
|
||||
} = props;
|
||||
|
||||
const onPress = useCallback(() => {
|
||||
onIndexerSelect({ implementation, implementationName, name });
|
||||
}, [implementation, implementationName, name, onIndexerSelect]);
|
||||
|
||||
return (
|
||||
<TableRowButton onPress={onPress}>
|
||||
<TableRowCell className={styles.protocol}>
|
||||
<ProtocolLabel protocol={protocol} />
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
{name}
|
||||
{isExistingIndexer ? (
|
||||
<Icon
|
||||
className={styles.alreadyExistsIcon}
|
||||
name={icons.CHECK_CIRCLE}
|
||||
size={15}
|
||||
title={translate('IndexerAlreadySetup')}
|
||||
/>
|
||||
) : null}
|
||||
</TableRowCell>
|
||||
|
||||
<TableRowCell>{language}</TableRowCell>
|
||||
|
||||
<TableRowCell>{description}</TableRowCell>
|
||||
|
||||
<TableRowCell>{translate(firstCharToUpper(privacy))}</TableRowCell>
|
||||
|
||||
<TableRowCell>
|
||||
<CapabilitiesLabel capabilities={capabilities} />
|
||||
</TableRowCell>
|
||||
</TableRowButton>
|
||||
);
|
||||
}
|
||||
|
||||
export default SelectIndexerRow;
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createExistingIndexerSelector from 'Store/Selectors/createExistingIndexerSelector';
|
||||
import SelectIndexerRow from './SelectIndexerRow';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createExistingIndexerSelector(),
|
||||
(isExistingIndexer, dimensions) => {
|
||||
return {
|
||||
isExistingIndexer
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(SelectIndexerRow);
|
||||
@@ -37,7 +37,7 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
{translate('AreYouSureYouWantToDeleteIndexer', [name])}
|
||||
{translate('AreYouSureYouWantToDeleteIndexer', { name })}
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
|
||||
@@ -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>
|
||||
@@ -97,7 +97,7 @@ function EditIndexerModalContent(props) {
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="enable"
|
||||
helpTextWarning={supportsRss.value ? undefined : translate('RSSIsNotSupportedWithThisIndexer')}
|
||||
helpTextWarning={supportsRss.value ? undefined : translate('RssIsNotSupportedWithThisIndexer')}
|
||||
{...enable}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
@@ -144,6 +144,7 @@ function EditIndexerModalContent(props) {
|
||||
}) :
|
||||
null
|
||||
}
|
||||
|
||||
<FormGroup
|
||||
advancedSettings={advancedSettings}
|
||||
isAdvanced={true}
|
||||
@@ -187,6 +188,7 @@ function EditIndexerModalContent(props) {
|
||||
type={inputTypes.TAG}
|
||||
name="tags"
|
||||
helpText={translate('IndexerTagsHelpText')}
|
||||
helpTextWarning={translate('IndexerTagsHelpTextWarning')}
|
||||
{...tags}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -48,7 +48,9 @@ function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
|
||||
|
||||
<ModalBody>
|
||||
<div className={styles.message}>
|
||||
{translate('DeleteSelectedIndexersMessageText', [indexers.length])}
|
||||
{translate('DeleteSelectedIndexersMessageText', {
|
||||
count: indexers.length,
|
||||
})}
|
||||
</div>
|
||||
|
||||
<ul>
|
||||
|
||||
@@ -19,6 +19,7 @@ interface SavePayload {
|
||||
seedRatio?: number;
|
||||
seedTime?: number;
|
||||
packSeedTime?: number;
|
||||
rejectBlocklistedTorrentHashesWhileGrabbing?: boolean;
|
||||
}
|
||||
|
||||
interface EditIndexerModalContentProps {
|
||||
@@ -35,7 +36,7 @@ const enableOptions = [
|
||||
get value() {
|
||||
return translate('NoChange');
|
||||
},
|
||||
disabled: true,
|
||||
isDisabled: true,
|
||||
},
|
||||
{
|
||||
key: 'true',
|
||||
@@ -65,6 +66,10 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
const [packSeedTime, setPackSeedTime] = useState<null | string | number>(
|
||||
null
|
||||
);
|
||||
const [
|
||||
rejectBlocklistedTorrentHashesWhileGrabbing,
|
||||
setRejectBlocklistedTorrentHashesWhileGrabbing,
|
||||
] = useState(NO_CHANGE);
|
||||
|
||||
const save = useCallback(() => {
|
||||
let hasChanges = false;
|
||||
@@ -105,6 +110,12 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
payload.packSeedTime = packSeedTime as number;
|
||||
}
|
||||
|
||||
if (rejectBlocklistedTorrentHashesWhileGrabbing !== NO_CHANGE) {
|
||||
hasChanges = true;
|
||||
payload.rejectBlocklistedTorrentHashesWhileGrabbing =
|
||||
rejectBlocklistedTorrentHashesWhileGrabbing === 'true';
|
||||
}
|
||||
|
||||
if (hasChanges) {
|
||||
onSavePress(payload);
|
||||
}
|
||||
@@ -118,6 +129,7 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
seedRatio,
|
||||
seedTime,
|
||||
packSeedTime,
|
||||
rejectBlocklistedTorrentHashesWhileGrabbing,
|
||||
onSavePress,
|
||||
onModalClose,
|
||||
]);
|
||||
@@ -146,6 +158,9 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
case 'packSeedTime':
|
||||
setPackSeedTime(value);
|
||||
break;
|
||||
case 'rejectBlocklistedTorrentHashesWhileGrabbing':
|
||||
setRejectBlocklistedTorrentHashesWhileGrabbing(value);
|
||||
break;
|
||||
default:
|
||||
console.warn(`EditIndexersModalContent Unknown Input: '${name}'`);
|
||||
}
|
||||
@@ -253,11 +268,28 @@ function EditIndexerModalContent(props: EditIndexerModalContentProps) {
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup size={sizes.MEDIUM}>
|
||||
<FormLabel>
|
||||
{translate('IndexerSettingsRejectBlocklistedTorrentHashes')}
|
||||
</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.SELECT}
|
||||
name="rejectBlocklistedTorrentHashesWhileGrabbing"
|
||||
value={rejectBlocklistedTorrentHashesWhileGrabbing}
|
||||
values={enableOptions}
|
||||
helpText={translate(
|
||||
'IndexerSettingsRejectBlocklistedTorrentHashesHelpText'
|
||||
)}
|
||||
onChange={onInputChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter className={styles.modalFooter}>
|
||||
<div className={styles.selected}>
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
{translate('CountIndexersSelected', { count: selectedCount })}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@@ -165,7 +165,7 @@ function IndexerIndexSelectFooter() {
|
||||
</div>
|
||||
|
||||
<div className={styles.selected}>
|
||||
{translate('CountIndexersSelected', [selectedCount])}
|
||||
{translate('CountIndexersSelected', { count: selectedCount })}
|
||||
</div>
|
||||
|
||||
<EditIndexerModal
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { uniqBy } from 'lodash';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import { IndexerCapabilities } from 'Indexer/Indexer';
|
||||
@@ -23,14 +24,18 @@ function CapabilitiesLabel(props: CapabilitiesLabelProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const nameList = Array.from(
|
||||
new Set(filteredList.map((item) => item.name).sort())
|
||||
const indexerCategories = uniqBy(filteredList, 'id').sort(
|
||||
(a, b) => a.id - b.id
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
{nameList.map((category) => {
|
||||
return <Label key={category}>{category}</Label>;
|
||||
{indexerCategories.map((category) => {
|
||||
return (
|
||||
<Label key={category.id} title={`${category.id}`}>
|
||||
{category.name}
|
||||
</Label>
|
||||
);
|
||||
})}
|
||||
|
||||
{filteredList.length === 0 ? <Label>{'None'}</Label> : null}
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
|
||||
.id {
|
||||
composes: cell;
|
||||
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
|
||||
.sortName {
|
||||
composes: cell;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ interface CssExports {
|
||||
'cell': string;
|
||||
'checkInput': string;
|
||||
'externalLink': string;
|
||||
'id': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'priority': string;
|
||||
|
||||
@@ -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,13 +27,14 @@ 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));
|
||||
useSelector(createIndexerIndexItemSelector(indexerId));
|
||||
|
||||
const {
|
||||
id,
|
||||
@@ -147,12 +148,25 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'id') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<IndexerTitleLink
|
||||
indexerId={indexerId}
|
||||
title={`${indexerId}`}
|
||||
onCloneIndexerPress={onCloneIndexerPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
}
|
||||
|
||||
if (name === 'sortName') {
|
||||
return (
|
||||
<VirtualTableRowCell key={name} className={styles[name]}>
|
||||
<IndexerTitleLink
|
||||
indexerId={indexerId}
|
||||
indexerName={indexerName}
|
||||
title={indexerName}
|
||||
onCloneIndexerPress={onCloneIndexerPress}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
);
|
||||
@@ -202,7 +216,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 +229,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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -4,6 +4,12 @@
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
|
||||
.id {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
|
||||
.sortName {
|
||||
composes: headerCell from '~Components/Table/VirtualTableHeaderCell.css';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ interface CssExports {
|
||||
'added': string;
|
||||
'appProfileId': string;
|
||||
'capabilities': string;
|
||||
'id': string;
|
||||
'minimumSeeders': string;
|
||||
'packSeedTime': string;
|
||||
'priority': string;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
import IndexerInfoModal from './Info/IndexerInfoModal';
|
||||
import styles from './IndexerTitleLink.css';
|
||||
|
||||
interface IndexerTitleLinkProps {
|
||||
indexerName: string;
|
||||
indexerId: number;
|
||||
title: string;
|
||||
onCloneIndexerPress(id: number): void;
|
||||
}
|
||||
|
||||
function IndexerTitleLink(props: IndexerTitleLinkProps) {
|
||||
const { indexerName, indexerId } = props;
|
||||
const { title, indexerId, onCloneIndexerPress } = props;
|
||||
|
||||
const [isIndexerInfoModalOpen, setIsIndexerInfoModalOpen] = useState(false);
|
||||
|
||||
@@ -25,20 +25,17 @@ function IndexerTitleLink(props: IndexerTitleLinkProps) {
|
||||
return (
|
||||
<div>
|
||||
<Link className={styles.link} onPress={onIndexerInfoPress}>
|
||||
{indexerName}
|
||||
{title}
|
||||
</Link>
|
||||
|
||||
<IndexerInfoModal
|
||||
indexerId={indexerId}
|
||||
isOpen={isIndexerInfoModalOpen}
|
||||
onModalClose={onIndexerInfoModalClose}
|
||||
onCloneIndexerPress={onCloneIndexerPress}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
IndexerTitleLink.propTypes = {
|
||||
indexerName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default IndexerTitleLink;
|
||||
|
||||
139
frontend/src/Indexer/Info/History/IndexerHistory.tsx
Normal file
139
frontend/src/Indexer/Info/History/IndexerHistory.tsx
Normal 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;
|
||||
23
frontend/src/Indexer/Info/History/IndexerHistoryRow.css
Normal file
23
frontend/src/Indexer/Info/History/IndexerHistoryRow.css
Normal 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;
|
||||
}
|
||||
11
frontend/src/Indexer/Info/History/IndexerHistoryRow.css.d.ts
vendored
Normal file
11
frontend/src/Indexer/Info/History/IndexerHistoryRow.css.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'details': string;
|
||||
'elapsedTime': string;
|
||||
'parametersContent': string;
|
||||
'query': string;
|
||||
'source': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
104
frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx
Normal file
104
frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
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 Alert from 'Components/Alert';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import DescriptionListItemDescription from 'Components/DescriptionList/DescriptionListItemDescription';
|
||||
@@ -21,9 +24,10 @@ import TagListConnector from 'Components/TagListConnector';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
|
||||
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
||||
import Indexer from 'Indexer/Indexer';
|
||||
import Indexer, { IndexerCapabilities } 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 +41,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,
|
||||
@@ -57,7 +64,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
||||
fields,
|
||||
tags,
|
||||
protocol,
|
||||
capabilities,
|
||||
capabilities = {} as IndexerCapabilities,
|
||||
} = indexer as Indexer;
|
||||
|
||||
const { onModalClose } = props;
|
||||
@@ -69,10 +76,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 +107,269 @@ 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 ? (
|
||||
<Label kind={kinds.PRIMARY}>
|
||||
{capabilities.searchParams[0]}
|
||||
</Label>
|
||||
) : (
|
||||
translate('NotSupported')
|
||||
)
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('TVSearchTypes')}
|
||||
data={
|
||||
capabilities?.tvSearchParams?.length > 0
|
||||
? 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}
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MovieSearchTypes')}
|
||||
data={
|
||||
capabilities?.movieSearchParams?.length > 0
|
||||
? capabilities.movieSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('BookSearchTypes')}
|
||||
data={
|
||||
capabilities?.bookSearchParams?.length > 0
|
||||
? capabilities.bookSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title={translate('MusicSearchTypes')}
|
||||
data={
|
||||
capabilities?.musicSearchParams?.length > 0
|
||||
? capabilities.musicSearchParams.map((p) => {
|
||||
return (
|
||||
<Label key={p} kind={kinds.PRIMARY}>
|
||||
{p}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
: translate('NotSupported')
|
||||
}
|
||||
/>
|
||||
</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>
|
||||
) : (
|
||||
<Alert kind={kinds.INFO}>
|
||||
{translate('NoIndexerCategories')}
|
||||
</Alert>
|
||||
)}
|
||||
</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
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -8,23 +8,27 @@ import BarChart from 'Components/Chart/BarChart';
|
||||
import DoughnutChart from 'Components/Chart/DoughnutChart';
|
||||
import StackedBarChart from 'Components/Chart/StackedBarChart';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import { align, kinds } from 'Helpers/Props';
|
||||
import { align, icons, kinds } from 'Helpers/Props';
|
||||
import {
|
||||
fetchIndexerStats,
|
||||
setIndexerStatsFilter,
|
||||
} from 'Store/Actions/indexerStatsActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import {
|
||||
IndexerStatsHost,
|
||||
IndexerStatsIndexer,
|
||||
IndexerStatsUserAgent,
|
||||
} from 'typings/IndexerStats';
|
||||
import abbreviateNumber from 'Utilities/Number/abbreviateNumber';
|
||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import IndexerStatsFilterMenu from './IndexerStatsFilterMenu';
|
||||
import IndexerStatsFilterModal from './IndexerStatsFilterModal';
|
||||
import styles from './IndexerStats.css';
|
||||
|
||||
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
||||
@@ -165,21 +169,36 @@ 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(() => {
|
||||
dispatch(fetchIndexerStats());
|
||||
}, [dispatch]);
|
||||
|
||||
const onRefreshPress = useCallback(() => {
|
||||
dispatch(fetchIndexerStats());
|
||||
}, [dispatch]);
|
||||
|
||||
const onFilterSelect = useCallback(
|
||||
(value: string) => {
|
||||
dispatch(setIndexerStatsFilter({ selectedFilterKey: value }));
|
||||
@@ -188,15 +207,42 @@ 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>
|
||||
<PageContent title={translate('Stats')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label={translate('Refresh')}
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={isFetching}
|
||||
onPress={onRefreshPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
||||
<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 +258,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>
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
56
frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx
Normal file
56
frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx
Normal 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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -37,6 +37,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
|
||||
return icons.DOWNLOAD;
|
||||
}
|
||||
|
||||
function getDownloadKind(isGrabbed, grabError) {
|
||||
if (isGrabbed) {
|
||||
return kinds.SUCCESS;
|
||||
}
|
||||
|
||||
if (grabError) {
|
||||
return kinds.DANGER;
|
||||
}
|
||||
|
||||
return kinds.DEFAULT;
|
||||
}
|
||||
|
||||
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
|
||||
if (isGrabbing) {
|
||||
return '';
|
||||
@@ -115,7 +127,7 @@ class SearchIndexOverview extends Component {
|
||||
<div className={styles.actions}>
|
||||
<SpinnerIconButton
|
||||
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
|
||||
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
|
||||
kind={getDownloadKind(isGrabbed, grabError)}
|
||||
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
|
||||
isDisabled={isGrabbed}
|
||||
isSpinning={isGrabbing}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -285,7 +285,7 @@ class SearchIndex extends Component {
|
||||
const hasNoIndexer = !totalItems;
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageContent title={translate('Search')}>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection
|
||||
alignContent={align.RIGHT}
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import Label from 'Components/Label';
|
||||
import Tooltip from 'Components/Tooltip/Tooltip';
|
||||
import { kinds, tooltipPositions } from 'Helpers/Props';
|
||||
|
||||
function CategoryLabel({ categories }) {
|
||||
const sortedCategories = categories.filter((cat) => cat.name !== undefined).sort((c) => c.id);
|
||||
|
||||
if (categories?.length === 0) {
|
||||
return (
|
||||
<Tooltip
|
||||
anchor={<Label kind={kinds.DANGER}>Unknown</Label>}
|
||||
tooltip="Please report this issue to the GitHub as this shouldn't be happening"
|
||||
position={tooltipPositions.LEFT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
{
|
||||
sortedCategories.map((category) => {
|
||||
return (
|
||||
<Label key={category.name}>
|
||||
{category.name}
|
||||
</Label>
|
||||
);
|
||||
})
|
||||
}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
CategoryLabel.defaultProps = {
|
||||
categories: []
|
||||
};
|
||||
|
||||
CategoryLabel.propTypes = {
|
||||
categories: PropTypes.arrayOf(PropTypes.object).isRequired
|
||||
};
|
||||
|
||||
export default CategoryLabel;
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user