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