mirror of
https://github.com/Sonarr/Sonarr.git
synced 2026-03-07 13:39:58 -05:00
Compare commits
13 Commits
phantom-di
...
phantom-x6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
473b146ae2 | ||
|
|
bdc0bb4441 | ||
|
|
96c5a37ca8 | ||
|
|
ce0b7e1077 | ||
|
|
29cde083f9 | ||
|
|
8faebc01ee | ||
|
|
2ea154b863 | ||
|
|
d02dfc9ff1 | ||
|
|
7bcfa3d7b2 | ||
|
|
1c1f9cddff | ||
|
|
8308d65375 | ||
|
|
d64d59ff27 | ||
|
|
8da6f7d7f4 |
85
README.md
85
README.md
@@ -1,73 +1,58 @@
|
||||
# <img width="24px" src="./Logo/256.png" alt="Sonarr"></img> Sonarr
|
||||
# Sonarr
|
||||
|
||||
Sonarr is a PVR for Usenet and BitTorrent users. It can monitor multiple RSS feeds for new episodes of your favorite shows and will grab, sort and rename them. It can also be configured to automatically upgrade the quality of files already downloaded when a better quality format becomes available.
|
||||
|
||||
## Getting Started
|
||||
## Major Features Include:
|
||||
|
||||
- [Download](https://sonarr.tv/#download) (Linux, MacOS, Windows, Docker, etc.)
|
||||
- [Installation](https://github.com/Sonarr/Sonarr/wiki/Installation)
|
||||
- [FAQ](https://github.com/Sonarr/Sonarr/wiki/FAQ)
|
||||
- [Wiki](https://github.com/Sonarr/Sonarr/wiki)
|
||||
- [API Documentation](https://github.com/Sonarr/Sonarr/wiki/API)
|
||||
|
||||
## Support
|
||||
|
||||
- [Donate](https://sonarr.tv/donate)
|
||||
- [Discord](https://discord.gg/M6BvZn5)
|
||||
- [Reddit](https://www.reddit.com/r/sonarr)
|
||||
|
||||
## Features
|
||||
|
||||
### Current Features
|
||||
|
||||
- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
- Automatically detects new episodes
|
||||
- Can scan your existing library and download any missing episodes
|
||||
- Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||
- Automatic failed download handling will try another release if one fails
|
||||
- Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
- Fully configurable episode renaming
|
||||
- Full integration with SABnzbd and NZBGet
|
||||
- Full integration with Kodi, Plex (notification, library update, metadata)
|
||||
- Full support for specials and multi-episode releases
|
||||
- And a beautiful UI
|
||||
* Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc.
|
||||
* Automatically detects new episodes
|
||||
* Can scan your existing library and download any missing episodes
|
||||
* Can watch for better quality of the episodes you already have and do an automatic upgrade. *eg. from DVD to Blu-Ray*
|
||||
* Automatic failed download handling will try another release if one fails
|
||||
* Manual search so you can pick any release or to see why a release was not downloaded automatically
|
||||
* Fully configurable episode renaming
|
||||
* Full integration with SABnzbd and NZBGet
|
||||
* Full integration with Kodi, Plex (notification, library update, metadata)
|
||||
* Full support for specials and multi-episode releases
|
||||
* And a beautiful UI
|
||||
|
||||
## Configuring Development Environment:
|
||||
|
||||
### Requirements
|
||||
|
||||
- [Visual Studio 2017](https://www.visualstudio.com/vs)
|
||||
- [Git](https://git-scm.com/downloads)
|
||||
- [NodeJS](https://nodejs.org/en/download)
|
||||
- [Yarn](https://yarnpkg.com)
|
||||
* [Visual Studio 2017](https://www.visualstudio.com/vs/)
|
||||
* [Git](https://git-scm.com/downloads)
|
||||
* [NodeJS](https://nodejs.org/en/download/)
|
||||
* [Yarn](https://yarnpkg.com/)
|
||||
|
||||
### Setup
|
||||
|
||||
- Make sure all the required software mentioned above are installed
|
||||
- Clone the repository recursively to get Sonarr and it's submodules
|
||||
- You can do this by running `git clone --recursive https://github.com/Sonarr/Sonarr.git`
|
||||
- Install the required Node Packages using `yarn`
|
||||
* Make sure all the required software mentioned above are installed
|
||||
* Clone the repository into your development machine. [*info*](https://help.github.com/en/articles/working-with-forks)
|
||||
* Grab the submodules `git submodule init && git submodule update`
|
||||
* Install the required Node Packages `yarn`
|
||||
|
||||
### Backend Development
|
||||
|
||||
- Run `yarn build` to build the UI
|
||||
- Open `Sonarr.sln` in Visual Studio
|
||||
- Make sure `Sonarr.Console` is set as the startup project
|
||||
- Build `Sonarr.Windows` and `Sonarr.Mono` projects
|
||||
- Build Solution
|
||||
* Run `yarn build` to build the UI
|
||||
* Open `Sonarr.sln` in Visual Studio
|
||||
* Make sure `NzbDrone.Console` is set as the startup project
|
||||
* Build `NzbDrone.Windows` and `NzbDrone.Mono` projects
|
||||
* Build Solution
|
||||
|
||||
### UI Development
|
||||
|
||||
- Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
||||
- Run Sonarr.Console.exe (or debug in Visual Studio)
|
||||
* Run `yarn watch` to build UI and rebuild automatically when changes are detected
|
||||
* Run Sonarr.Console.exe (or debug in Visual Studio)
|
||||
|
||||
### Licenses
|
||||
### License
|
||||
|
||||
- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
- Copyright 2010-2020
|
||||
|
||||
* [GNU GPL v3](http://www.gnu.org/licenses/gpl.html)
|
||||
* Copyright 2010-2019
|
||||
|
||||
### Sponsors
|
||||
|
||||
- [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||
- [ReSharper](http://www.jetbrains.com/resharper/)
|
||||
- [TeamCity](http://www.jetbrains.com/teamcity/)
|
||||
* [JetBrains](http://www.jetbrains.com/) for providing us with free licenses to their great tools
|
||||
* [ReSharper](http://www.jetbrains.com/resharper/)
|
||||
* [TeamCity](http://www.jetbrains.com/teamcity/)
|
||||
|
||||
51
build.sh
51
build.sh
@@ -12,6 +12,8 @@ sourceFolder='./src'
|
||||
slnFile=$sourceFolder/Sonarr.sln
|
||||
updateSubFolder=Sonarr.Update
|
||||
|
||||
sqlitePackageDir="$HOME/.nuget/packages/system.data.sqlite.core.lidarr/1.0.111-5"
|
||||
|
||||
nuget='tools/nuget/nuget.exe';
|
||||
vswhere='tools/vswhere/vswhere.exe';
|
||||
|
||||
@@ -96,17 +98,17 @@ BuildWithMSBuild()
|
||||
echo $msBuildDir
|
||||
|
||||
export PATH=$msBuildDir:$PATH
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Clean //m
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x64 //t:Clean //m
|
||||
$nuget restore $slnFile
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x86 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
||||
CheckExitCode MSBuild.exe $slnFile //p:Configuration=Release //p:Platform=x64 //t:Build //m //p:AllowedReferenceRelatedFileExtensions=.pdb
|
||||
}
|
||||
|
||||
BuildWithXbuild()
|
||||
{
|
||||
export MONO_IOMAP=case
|
||||
CheckExitCode msbuild /t:Clean $slnFile
|
||||
CheckExitCode xbuild /t:Clean $slnFile
|
||||
mono $nuget restore $slnFile
|
||||
CheckExitCode msbuild /p:Configuration=Release /p:Platform=x86 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
CheckExitCode xbuild /p:Configuration=Release /p:Platform=x64 /t:Build /p:AllowedReferenceRelatedFileExtensions=.pdb $slnFile
|
||||
}
|
||||
|
||||
LintUI()
|
||||
@@ -135,9 +137,6 @@ Build()
|
||||
|
||||
CleanFolder $outputFolder false
|
||||
|
||||
echo "Removing Mono.Posix.dll"
|
||||
rm $outputFolder/Mono.Posix.dll
|
||||
|
||||
ProgressEnd 'Build'
|
||||
}
|
||||
|
||||
@@ -175,23 +174,11 @@ PatchMono()
|
||||
{
|
||||
local path=$1
|
||||
|
||||
# Below we deal with some mono incompatibilities with windows-only dotnet core/standard libs
|
||||
# See: https://github.com/mono/mono/blob/master/tools/nuget-hash-extractor/download.sh
|
||||
# That list defines assemblies that are prohibited from being loaded from the appdir, instead loading from mono GAC.
|
||||
|
||||
# We have debian dependencies to get these installed or facades from mono 5.10+
|
||||
for assembly in System.IO.Compression System.Runtime.InteropServices.RuntimeInformation System.Net.Http System.Globalization.Extensions System.Text.Encoding.CodePages System.Threading.Overlapped
|
||||
# Copy over the netstandard.dll facade since mono has no separate package for it and includes it in mono-devel
|
||||
for assembly in netstandard System.Runtime
|
||||
do
|
||||
if [ -e $path/$assembly.dll ]; then
|
||||
if [ -e $sourceFolder/Libraries/Mono/$assembly.dll ]; then
|
||||
echo "Copy Mono-specific facade $assembly.dll (uses win32 interop)"
|
||||
echo "Copy Mono-specific facade $assembly.dll"
|
||||
cp $sourceFolder/Libraries/Mono/$assembly.dll $path/$assembly.dll
|
||||
else
|
||||
echo "Remove $assembly.dll (uses win32 interop)"
|
||||
rm $path/$assembly.dll
|
||||
fi
|
||||
|
||||
fi
|
||||
done
|
||||
|
||||
# Copy more stable version of Vectors for mono <5.12
|
||||
@@ -241,10 +228,6 @@ PackageMono()
|
||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $outputFolderLinux
|
||||
|
||||
# Remove Http binding redirect by renaming it
|
||||
# We don't need this anymore once our minimum mono version is 5.10
|
||||
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $outputFolderLinux/Sonarr.Console.exe.config
|
||||
|
||||
echo "Renaming Sonarr.Console.exe to Sonarr.exe"
|
||||
rm $outputFolderLinux/Sonarr.exe*
|
||||
for file in $outputFolderLinux/Sonarr.Console.exe*; do
|
||||
@@ -274,11 +257,11 @@ PackageMacOS()
|
||||
echo "Copying Binaries"
|
||||
cp -r $outputFolderLinux/* $outputFolderMacOS
|
||||
|
||||
echo "Adding sqlite dylibs"
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOS
|
||||
echo "Adding sqlite dylib"
|
||||
cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/* $outputFolderMacOS
|
||||
|
||||
echo "Adding MediaInfo dylib"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOS
|
||||
cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS
|
||||
|
||||
ProgressEnd 'Creating MacOS Package'
|
||||
}
|
||||
@@ -299,11 +282,11 @@ PackageMacOSApp()
|
||||
echo "Copying Binaries"
|
||||
cp -r $outputFolderLinux/* $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
||||
|
||||
echo "Adding sqlite dylibs"
|
||||
cp $sourceFolder/Libraries/Sqlite/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
||||
echo "Adding sqlite dylib"
|
||||
cp "$sqlitePackageDir/runtimes/osx-x64/native/net46"/* $outputFolderMacOS
|
||||
|
||||
echo "Adding MediaInfo dylib"
|
||||
cp $sourceFolder/Libraries/MediaInfo/*.dylib $outputFolderMacOSApp/Sonarr.app/Contents/MacOS
|
||||
cp $sourceFolder/Libraries/MediaInfo/x64/*.dylib $outputFolderMacOS
|
||||
|
||||
echo "Removing Update Folder"
|
||||
rm -r $outputFolderMacOSApp/Sonarr.app/Contents/MacOS/Sonarr.Update
|
||||
@@ -337,10 +320,6 @@ PackageTestsMono()
|
||||
echo "Adding Sonarr.Core.dll.config (for dllmap)"
|
||||
cp $sourceFolder/NzbDrone.Core/Sonarr.Core.dll.config $testPackageFolderLinux
|
||||
|
||||
# Remove Http binding redirect by renaming it
|
||||
# We don't need this anymore once our minimum mono version is 5.10
|
||||
sed -i "s/System.Net.Http/System.Net.Http.Mono/g" $testPackageFolderLinux/Sonarr.Common.Test.dll.config
|
||||
|
||||
cp ./test.sh $testPackageFolderLinux/
|
||||
dos2unix $testPackageFolderLinux/test.sh
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ chown -R $USER:$GROUP /usr/lib/sonarr
|
||||
sed -i "s:User=sonarr:User=$USER:g; s:Group=sonarr:Group=$GROUP:g; s:-data=/var/lib/sonarr:-data=$CONFDIR:g" /lib/systemd/system/sonarr.service
|
||||
|
||||
#BEGIN BUILTIN UPDATER
|
||||
if [ "$UPDATER" = "BuiltIn" ]; then
|
||||
if [ $1 = "upgrade" ] && [ "$UPDATER" = "BuiltIn" ]; then
|
||||
# If we upgraded, signal Sonarr to do an update check on startup instead of scheduled.
|
||||
touch $CONFDIR/update_required
|
||||
chown $USER:$GROUP $CONFDIR/update_required
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
# Uncomment this to turn on verbose mode.
|
||||
#export DH_VERBOSE=1
|
||||
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal ole32.dll libmonosgen-2.0 clr mscorlib mscoree.dll Microsoft.DiaSymReader.Native.x86.dll Microsoft.DiaSymReader.Native.amd64.dll
|
||||
EXCLUDE_MODULEREFS = crypt32 httpapi __Internal
|
||||
|
||||
%:
|
||||
dh $@ --with=systemd --with=cli
|
||||
|
||||
@@ -25,10 +25,10 @@ done
|
||||
MONO_VERSIONS=""
|
||||
|
||||
# Future versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.10=preview-xenial"
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.8=preview-xenial"
|
||||
|
||||
# Semi-Supported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.8 6.6 6.4 6.0"
|
||||
MONO_VERSIONS="$MONO_VERSIONS 6.6 6.4 6.0"
|
||||
|
||||
# Supported versions
|
||||
MONO_VERSIONS="$MONO_VERSIONS 5.20 5.18"
|
||||
|
||||
@@ -10,7 +10,8 @@ gulp.task('build',
|
||||
'webpack',
|
||||
'copyHtml',
|
||||
'copyFonts',
|
||||
'copyImages'
|
||||
'copyImages',
|
||||
'copyJs'
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@@ -5,6 +5,17 @@ const cache = require('gulp-cached');
|
||||
const livereload = require('gulp-livereload');
|
||||
const paths = require('./helpers/paths.js');
|
||||
|
||||
gulp.task('copyJs', () => {
|
||||
return gulp.src(
|
||||
[
|
||||
path.join(paths.src.root, 'polyfills.js')
|
||||
], { base: paths.src.root })
|
||||
.pipe(cache('copyJs'))
|
||||
.pipe(print())
|
||||
.pipe(gulp.dest(paths.dest.root))
|
||||
.pipe(livereload());
|
||||
});
|
||||
|
||||
gulp.task('copyHtml', () => {
|
||||
return gulp.src(paths.src.html, { base: paths.src.root })
|
||||
.pipe(cache('copyHtml'))
|
||||
|
||||
@@ -6,21 +6,17 @@ const webpack = require('webpack');
|
||||
const errorHandler = require('./helpers/errorHandler');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
|
||||
const uiFolder = 'UI';
|
||||
const frontendFolder = path.join(__dirname, '..');
|
||||
const srcFolder = path.join(frontendFolder, 'src');
|
||||
const isProduction = process.argv.indexOf('--production') > -1;
|
||||
const isProfiling = isProduction && process.argv.indexOf('--profile') > -1;
|
||||
const inlineWebWorkers = true;
|
||||
|
||||
const distFolder = path.resolve(frontendFolder, '..', '_output', uiFolder);
|
||||
|
||||
console.log('Source Folder:', srcFolder);
|
||||
console.log('Output Folder:', distFolder);
|
||||
console.log('isProduction:', isProduction);
|
||||
console.log('isProfiling:', isProfiling);
|
||||
|
||||
const cssVarsFiles = [
|
||||
'../src/Styles/Variables/colors',
|
||||
@@ -117,22 +113,6 @@ const config = {
|
||||
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.worker\.js$/,
|
||||
issuer: {
|
||||
// monaco-editor includes the editor.worker.js in other language workers,
|
||||
// don't use worker-loader in that case
|
||||
exclude: /monaco-editor/
|
||||
},
|
||||
use: {
|
||||
loader: 'worker-loader',
|
||||
options: {
|
||||
name: '[name].js',
|
||||
inline: inlineWebWorkers,
|
||||
fallback: !inlineWebWorkers
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js?$/,
|
||||
exclude: /(node_modules|JsLibraries)/,
|
||||
@@ -233,24 +213,6 @@ const config = {
|
||||
}
|
||||
};
|
||||
|
||||
if (isProfiling) {
|
||||
config.resolve.alias['react-dom$'] = 'react-dom/profiling';
|
||||
config.resolve.alias['scheduler/tracing'] = 'scheduler/tracing-profiling';
|
||||
|
||||
config.optimization.minimizer = [
|
||||
new TerserPlugin({
|
||||
cache: true,
|
||||
parallel: true,
|
||||
sourceMap: true, // Must be set to true if using source-maps in production
|
||||
terserOptions: {
|
||||
mangle: false,
|
||||
keep_classnames: true,
|
||||
keep_fnames: true
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
|
||||
gulp.task('webpack', () => {
|
||||
return webpackStream(config)
|
||||
.pipe(gulp.dest('_output/UI'));
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es6",
|
||||
"checkJs": false,
|
||||
"baseUrl": "src",
|
||||
"jsx": "react",
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"paths": {
|
||||
"*": [
|
||||
"*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -56,7 +56,7 @@ class Blacklist extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -103,7 +103,7 @@ class Blacklist extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -232,30 +232,6 @@ function HistoryDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (eventType === 'downloadIgnored') {
|
||||
const {
|
||||
message
|
||||
} = data;
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
descriptionClassName={styles.description}
|
||||
title="Name"
|
||||
data={sourceTitle}
|
||||
/>
|
||||
|
||||
{
|
||||
!!message &&
|
||||
<DescriptionListItem
|
||||
title="Message"
|
||||
data={message}
|
||||
/>
|
||||
}
|
||||
</DescriptionList>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DescriptionList>
|
||||
<DescriptionListItem
|
||||
|
||||
@@ -23,8 +23,6 @@ function getHeaderTitle(eventType) {
|
||||
return 'Episode File Deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode File Renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Download Ignored';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import TableBody from 'Components/Table/TableBody';
|
||||
import TableOptionsModalWrapper from 'Components/Table/TableOptions/TableOptionsModalWrapper';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -96,7 +96,7 @@ class History extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetchingAny && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -147,7 +147,7 @@ class History extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ function getIconName(eventType) {
|
||||
return icons.DELETE;
|
||||
case 'episodeFileRenamed':
|
||||
return icons.ORGANIZE;
|
||||
case 'downloadIgnored':
|
||||
return icons.IGNORE;
|
||||
default:
|
||||
return icons.UNKNOWN;
|
||||
}
|
||||
@@ -49,8 +47,6 @@ function getTooltip(eventType, data) {
|
||||
return 'Episode file deleted';
|
||||
case 'episodeFileRenamed':
|
||||
return 'Episode file renamed';
|
||||
case 'downloadIgnored':
|
||||
return 'Episode Download Ignored';
|
||||
default:
|
||||
return 'Unknown event';
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import getRemovedItems from 'Utilities/Object/getRemovedItems';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import getSelectedIds from 'Utilities/Table/getSelectedIds';
|
||||
import removeOldSelectedState from 'Utilities/Table/removeOldSelectedState';
|
||||
@@ -13,7 +12,7 @@ import Table from 'Components/Table/Table';
|
||||
import TableBody from 'Components/Table/TableBody';
|
||||
import TablePager from 'Components/Table/TablePager';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
@@ -37,26 +36,34 @@ class Queue extends Component {
|
||||
lastToggled: null,
|
||||
selectedState: {},
|
||||
isPendingSelected: false,
|
||||
isConfirmRemoveModalOpen: false,
|
||||
items: props.items
|
||||
isConfirmRemoveModalOpen: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
items,
|
||||
isEpisodesFetching
|
||||
} = this.props;
|
||||
shouldComponentUpdate(nextProps) {
|
||||
// Don't update when fetching has completed if items have changed,
|
||||
// before episodes start fetching or when episodes start fetching.
|
||||
|
||||
if (
|
||||
(!isEpisodesFetching && prevProps.isEpisodesFetching) ||
|
||||
(hasDifferentItems(prevProps.items, items) && !items.some((e) => e.episodeId))
|
||||
this.props.isFetching &&
|
||||
nextProps.isPopulated &&
|
||||
hasDifferentItems(this.props.items, nextProps.items) &&
|
||||
nextProps.items.some((e) => e.episodeId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.props.isEpisodesFetching && nextProps.isEpisodesFetching) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (hasDifferentItems(prevProps.items, this.props.items)) {
|
||||
this.setState((state) => {
|
||||
return {
|
||||
...removeOldSelectedState(state, getRemovedItems(prevProps.items, items)),
|
||||
items
|
||||
};
|
||||
return removeOldSelectedState(state, prevProps.items);
|
||||
});
|
||||
|
||||
return;
|
||||
@@ -64,7 +71,7 @@ class Queue extends Component {
|
||||
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const isPendingSelected = _.some(this.props.items, (item) => {
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'delay';
|
||||
return selectedIds.indexOf(item.id) > -1 && item.status === 'Delay';
|
||||
});
|
||||
|
||||
if (isPendingSelected !== this.state.isPendingSelected) {
|
||||
@@ -100,8 +107,8 @@ class Queue extends Component {
|
||||
this.setState({ isConfirmRemoveModalOpen: true });
|
||||
}
|
||||
|
||||
onRemoveSelectedConfirmed = (payload) => {
|
||||
this.props.onRemoveSelectedPress({ ids: this.getSelectedIds(), ...payload });
|
||||
onRemoveSelectedConfirmed = (blacklist) => {
|
||||
this.props.onRemoveSelectedPress(this.getSelectedIds(), blacklist);
|
||||
this.setState({ isConfirmRemoveModalOpen: false });
|
||||
}
|
||||
|
||||
@@ -117,6 +124,7 @@ class Queue extends Component {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items,
|
||||
isEpisodesFetching,
|
||||
isEpisodesPopulated,
|
||||
episodesError,
|
||||
@@ -124,7 +132,7 @@ class Queue extends Component {
|
||||
totalRecords,
|
||||
isGrabbing,
|
||||
isRemoving,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
onRefreshPress,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
@@ -134,15 +142,13 @@ class Queue extends Component {
|
||||
allUnselected,
|
||||
selectedState,
|
||||
isConfirmRemoveModalOpen,
|
||||
isPendingSelected,
|
||||
items
|
||||
isPendingSelected
|
||||
} = this.state;
|
||||
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isRefreshMonitoredDownloadsExecuting;
|
||||
const isRefreshing = isFetching || isEpisodesFetching || isCheckForFinishedDownloadExecuting;
|
||||
const isAllPopulated = isPopulated && (isEpisodesPopulated || !items.length || items.every((e) => !e.episodeId));
|
||||
const hasError = error || episodesError;
|
||||
const selectedIds = this.getSelectedIds();
|
||||
const selectedCount = selectedIds.length;
|
||||
const selectedCount = this.getSelectedIds().length;
|
||||
const disableSelectedActions = selectedCount === 0;
|
||||
|
||||
return (
|
||||
@@ -191,7 +197,7 @@ class Queue extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody>
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isRefreshing && !isAllPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -248,18 +254,11 @@ class Queue extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<RemoveQueueItemsModal
|
||||
isOpen={isConfirmRemoveModalOpen}
|
||||
selectedCount={selectedCount}
|
||||
canIgnore={isConfirmRemoveModalOpen && (
|
||||
selectedIds.every((id) => {
|
||||
const item = items.find((i) => i.id === id);
|
||||
|
||||
return !!(item && item.seriesId && item.episodeId);
|
||||
})
|
||||
)}
|
||||
onRemovePress={this.onRemoveSelectedConfirmed}
|
||||
onModalClose={this.onConfirmRemoveModalClose}
|
||||
/>
|
||||
@@ -280,7 +279,7 @@ Queue.propTypes = {
|
||||
totalRecords: PropTypes.number,
|
||||
isGrabbing: PropTypes.bool.isRequired,
|
||||
isRemoving: PropTypes.bool.isRequired,
|
||||
isRefreshMonitoredDownloadsExecuting: PropTypes.bool.isRequired,
|
||||
isCheckForFinishedDownloadExecuting: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired,
|
||||
onGrabSelectedPress: PropTypes.func.isRequired,
|
||||
onRemoveSelectedPress: PropTypes.func.isRequired
|
||||
|
||||
@@ -18,13 +18,13 @@ function createMapStateToProps() {
|
||||
(state) => state.episodes,
|
||||
(state) => state.queue.options,
|
||||
(state) => state.queue.paged,
|
||||
createCommandExecutingSelector(commandNames.REFRESH_MONITORED_DOWNLOADS),
|
||||
(episodes, options, queue, isRefreshMonitoredDownloadsExecuting) => {
|
||||
createCommandExecutingSelector(commandNames.CHECK_FOR_FINISHED_DOWNLOAD),
|
||||
(episodes, options, queue, isCheckForFinishedDownloadExecuting) => {
|
||||
return {
|
||||
isEpisodesFetching: episodes.isFetching,
|
||||
isEpisodesPopulated: episodes.isPopulated,
|
||||
episodesError: episodes.error,
|
||||
isRefreshMonitoredDownloadsExecuting,
|
||||
isCheckForFinishedDownloadExecuting,
|
||||
...options,
|
||||
...queue
|
||||
};
|
||||
@@ -129,7 +129,7 @@ class QueueConnector extends Component {
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.executeCommand({
|
||||
name: commandNames.REFRESH_MONITORED_DOWNLOADS
|
||||
name: commandNames.CHECK_FOR_FINISHED_DOWNLOAD
|
||||
});
|
||||
}
|
||||
|
||||
@@ -137,8 +137,8 @@ class QueueConnector extends Component {
|
||||
this.props.grabQueueItems({ ids });
|
||||
}
|
||||
|
||||
onRemoveSelectedPress = (payload) => {
|
||||
this.props.removeQueueItems(payload);
|
||||
onRemoveSelectedPress = (ids, blacklist) => {
|
||||
this.props.removeQueueItems({ ids, blacklist });
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -10,13 +10,13 @@ function QueueDetails(props) {
|
||||
size,
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
status: queueStatus,
|
||||
errorMessage,
|
||||
progressBar
|
||||
} = props;
|
||||
|
||||
const status = queueStatus.toLowerCase();
|
||||
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
if (status === 'pending') {
|
||||
@@ -39,35 +39,7 @@ function QueueDetails(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadStatus === 'warning') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.WARNING}
|
||||
title={'Downloaded - Unable to Import: check logs for details'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={'Downloaded - Waiting to Import'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
return (
|
||||
<Icon
|
||||
name={icons.DOWNLOAD}
|
||||
kind={kinds.PURPLE}
|
||||
title={'Downloaded - Importing'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
// TODO: show an icon when download is complete, but not imported yet?
|
||||
}
|
||||
|
||||
if (errorMessage) {
|
||||
@@ -118,8 +90,6 @@ QueueDetails.propTypes = {
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
progressBar: PropTypes.node.isRequired
|
||||
};
|
||||
|
||||
@@ -68,7 +68,6 @@ class QueueRow extends Component {
|
||||
title,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
statusMessages,
|
||||
errorMessage,
|
||||
series,
|
||||
@@ -101,8 +100,8 @@ class QueueRow extends Component {
|
||||
} = this.state;
|
||||
|
||||
const progress = 100 - (sizeleft / size * 100);
|
||||
const showInteractiveImport = status === 'completed' && trackedDownloadStatus === 'warning';
|
||||
const isPending = status === 'delay' || status === 'downloadClientUnavailable';
|
||||
const showInteractiveImport = status === 'Completed' && trackedDownloadStatus === 'Warning';
|
||||
const isPending = status === 'Delay' || status === 'DownloadClientUnavailable';
|
||||
|
||||
return (
|
||||
<TableRow>
|
||||
@@ -130,7 +129,6 @@ class QueueRow extends Component {
|
||||
sourceTitle={title}
|
||||
status={status}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
statusMessages={statusMessages}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
@@ -222,13 +220,9 @@ class QueueRow extends Component {
|
||||
if (name === 'quality') {
|
||||
return (
|
||||
<TableRowCell key={name}>
|
||||
{
|
||||
quality ?
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/> :
|
||||
null
|
||||
}
|
||||
<EpisodeQuality
|
||||
quality={quality}
|
||||
/>
|
||||
</TableRowCell>
|
||||
);
|
||||
}
|
||||
@@ -356,7 +350,6 @@ class QueueRow extends Component {
|
||||
<RemoveQueueItemModal
|
||||
isOpen={isRemoveQueueItemModalOpen}
|
||||
sourceTitle={title}
|
||||
canIgnore={!!series}
|
||||
onRemovePress={this.onRemoveQueueItemModalConfirmed}
|
||||
onModalClose={this.onRemoveQueueItemModalClose}
|
||||
/>
|
||||
@@ -372,7 +365,6 @@ QueueRow.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
trackedDownloadState: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string,
|
||||
series: PropTypes.object,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -14,11 +15,11 @@ function createMapStateToProps() {
|
||||
createEpisodeSelector(),
|
||||
createUISettingsSelector(),
|
||||
(series, episode, uiSettings) => {
|
||||
const result = {
|
||||
showRelativeDates: uiSettings.showRelativeDates,
|
||||
shortDateFormat: uiSettings.shortDateFormat,
|
||||
timeFormat: uiSettings.timeFormat
|
||||
};
|
||||
const result = _.pick(uiSettings, [
|
||||
'showRelativeDates',
|
||||
'shortDateFormat',
|
||||
'timeFormat'
|
||||
]);
|
||||
|
||||
result.series = series;
|
||||
result.episode = episode;
|
||||
@@ -42,8 +43,8 @@ class QueueRowConnector extends Component {
|
||||
this.props.grabQueueItem({ id: this.props.id });
|
||||
}
|
||||
|
||||
onRemoveQueueItemPress = (payload) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, ...payload });
|
||||
onRemoveQueueItemPress = (blacklist) => {
|
||||
this.props.removeQueueItem({ id: this.props.id, blacklist });
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -37,79 +37,63 @@ function QueueStatusCell(props) {
|
||||
const {
|
||||
sourceTitle,
|
||||
status,
|
||||
trackedDownloadStatus,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus = 'Ok',
|
||||
statusMessages,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const hasWarning = trackedDownloadStatus === 'warning';
|
||||
const hasError = trackedDownloadStatus === 'error';
|
||||
const hasWarning = trackedDownloadStatus === 'Warning';
|
||||
const hasError = trackedDownloadStatus === 'Error';
|
||||
|
||||
// status === 'downloading'
|
||||
let iconName = icons.DOWNLOADING;
|
||||
let iconKind = kinds.DEFAULT;
|
||||
let title = 'Downloading';
|
||||
|
||||
if (status === 'paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
|
||||
if (trackedDownloadState === 'importPending') {
|
||||
title += ' - Waiting to Import';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'importing') {
|
||||
title += ' - Importing';
|
||||
iconKind = kinds.PURPLE;
|
||||
}
|
||||
|
||||
if (trackedDownloadState === 'failedPending') {
|
||||
title += ' - Waiting to Process';
|
||||
iconKind = kinds.DANGER;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasWarning) {
|
||||
iconKind = kinds.WARNING;
|
||||
}
|
||||
|
||||
if (status === 'delay') {
|
||||
if (status === 'Paused') {
|
||||
iconName = icons.PAUSED;
|
||||
title = 'Paused';
|
||||
}
|
||||
|
||||
if (status === 'Queued') {
|
||||
iconName = icons.QUEUED;
|
||||
title = 'Queued';
|
||||
}
|
||||
|
||||
if (status === 'Completed') {
|
||||
iconName = icons.DOWNLOADED;
|
||||
title = 'Downloaded';
|
||||
}
|
||||
|
||||
if (status === 'Delay') {
|
||||
iconName = icons.PENDING;
|
||||
title = 'Pending';
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
iconName = icons.PENDING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = 'Pending - Download client is unavailable';
|
||||
}
|
||||
|
||||
if (status === 'failed') {
|
||||
if (status === 'Failed') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.DANGER;
|
||||
title = 'Download failed';
|
||||
}
|
||||
|
||||
if (status === 'warning') {
|
||||
if (status === 'Warning') {
|
||||
iconName = icons.DOWNLOADING;
|
||||
iconKind = kinds.WARNING;
|
||||
title = `Download warning: ${errorMessage || 'check download client for more details'}`;
|
||||
}
|
||||
|
||||
if (hasError) {
|
||||
if (status === 'completed') {
|
||||
if (status === 'Completed') {
|
||||
iconName = icons.DOWNLOAD;
|
||||
iconKind = kinds.DANGER;
|
||||
title = `Import failed: ${sourceTitle}`;
|
||||
@@ -141,15 +125,9 @@ function QueueStatusCell(props) {
|
||||
QueueStatusCell.propTypes = {
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string,
|
||||
statusMessages: PropTypes.arrayOf(PropTypes.object),
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
QueueStatusCell.defaultProps = {
|
||||
trackedDownloadStatus: 'Ok',
|
||||
trackedDownloadState: 'Downloading'
|
||||
};
|
||||
|
||||
export default QueueStatusCell;
|
||||
|
||||
4
frontend/src/Activity/Queue/RemoveQueueItemModal.css
Normal file
4
frontend/src/Activity/Queue/RemoveQueueItemModal.css
Normal file
@@ -0,0 +1,4 @@
|
||||
.messageRemove {
|
||||
margin-bottom: 30px;
|
||||
color: $dangerColor;
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './RemoveQueueItemModal.css';
|
||||
|
||||
class RemoveQueueItemModal extends Component {
|
||||
|
||||
@@ -20,41 +21,26 @@ class RemoveQueueItemModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@@ -64,11 +50,10 @@ class RemoveQueueItemModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
sourceTitle,
|
||||
canIgnore
|
||||
sourceTitle
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist } = this.state;
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -88,27 +73,17 @@ class RemoveQueueItemModal extends Component {
|
||||
Are you sure you want to remove '{sourceTitle}' from the queue?
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
<div className={styles.messageRemove}>
|
||||
Removing will remove the download and the file(s) from the download client.
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
value={blacklist}
|
||||
helpText="Starts a search for this episode again and prevents this release from being grabbed again"
|
||||
helpText="Prevents Sonarr from automatically grabbing this episode again"
|
||||
onChange={this.onBlacklistChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
@@ -122,7 +97,7 @@ class RemoveQueueItemModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -136,7 +111,6 @@ class RemoveQueueItemModal extends Component {
|
||||
RemoveQueueItemModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
sourceTitle: PropTypes.string.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -21,41 +21,26 @@ class RemoveQueueItemsModal extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
remove: true,
|
||||
blacklist: false
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
resetState = function() {
|
||||
this.setState({
|
||||
remove: true,
|
||||
blacklist: false
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRemoveChange = ({ value }) => {
|
||||
this.setState({ remove: value });
|
||||
}
|
||||
// Listeners
|
||||
|
||||
onBlacklistChange = ({ value }) => {
|
||||
this.setState({ blacklist: value });
|
||||
}
|
||||
|
||||
onRemoveConfirmed = () => {
|
||||
const state = this.state;
|
||||
onRemoveQueueItemConfirmed = () => {
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
this.resetState();
|
||||
this.props.onRemovePress(state);
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onRemovePress(blacklist);
|
||||
}
|
||||
|
||||
onModalClose = () => {
|
||||
this.resetState();
|
||||
this.setState({ blacklist: false });
|
||||
this.props.onModalClose();
|
||||
}
|
||||
|
||||
@@ -65,11 +50,10 @@ class RemoveQueueItemsModal extends Component {
|
||||
render() {
|
||||
const {
|
||||
isOpen,
|
||||
selectedCount,
|
||||
canIgnore
|
||||
selectedCount
|
||||
} = this.props;
|
||||
|
||||
const { remove, blacklist } = this.state;
|
||||
const blacklist = this.state.blacklist;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -90,23 +74,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
</div>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>Remove From Download Client</FormLabel>
|
||||
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="remove"
|
||||
value={remove}
|
||||
helpTextWarning="Removing will remove the download and the file(s) from the download client."
|
||||
isDisabled={!canIgnore}
|
||||
onChange={this.onRemoveChange}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup>
|
||||
<FormLabel>
|
||||
Blacklist Release{selectedCount > 1 ? 's' : ''}
|
||||
</FormLabel>
|
||||
|
||||
<FormLabel>Blacklist Release</FormLabel>
|
||||
<FormInputGroup
|
||||
type={inputTypes.CHECK}
|
||||
name="blacklist"
|
||||
@@ -125,7 +93,7 @@ class RemoveQueueItemsModal extends Component {
|
||||
|
||||
<Button
|
||||
kind={kinds.DANGER}
|
||||
onPress={this.onRemoveConfirmed}
|
||||
onPress={this.onRemoveQueueItemConfirmed}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
@@ -139,7 +107,6 @@ class RemoveQueueItemsModal extends Component {
|
||||
RemoveQueueItemsModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
selectedCount: PropTypes.number.isRequired,
|
||||
canIgnore: PropTypes.bool.isRequired,
|
||||
onRemovePress: PropTypes.func.isRequired,
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@ function TimeleftCell(props) {
|
||||
timeFormat
|
||||
} = props;
|
||||
|
||||
if (status === 'delay') {
|
||||
if (status === 'Delay') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
@@ -33,7 +33,7 @@ function TimeleftCell(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'downloadClientUnavailable') {
|
||||
if (status === 'DownloadClientUnavailable') {
|
||||
const date = getRelativeDate(estimatedCompletionTime, shortDateFormat, showRelativeDates);
|
||||
const time = formatTime(estimatedCompletionTime, timeFormat, { includeMinuteZero: true });
|
||||
|
||||
@@ -47,7 +47,7 @@ function TimeleftCell(props) {
|
||||
);
|
||||
}
|
||||
|
||||
if (!timeleft || status === 'completed' || status === 'failed') {
|
||||
if (!timeleft) {
|
||||
return (
|
||||
<TableRowCell className={styles.timeleft}>
|
||||
-
|
||||
|
||||
@@ -8,7 +8,7 @@ import Icon from 'Components/Icon';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import TextInput from 'Components/Form/TextInput';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import AddNewSeriesSearchResultConnector from './AddNewSeriesSearchResultConnector';
|
||||
import styles from './AddNewSeries.css';
|
||||
|
||||
@@ -88,7 +88,7 @@ class AddNewSeries extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Add New Series">
|
||||
<PageContentBody>
|
||||
<PageContentBodyConnector>
|
||||
<div className={styles.searchContainer}>
|
||||
<div className={styles.searchIconContainer}>
|
||||
<Icon
|
||||
@@ -191,7 +191,7 @@ class AddNewSeries extends Component {
|
||||
}
|
||||
|
||||
<div />
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import Popover from 'Components/Tooltip/Popover';
|
||||
import SeriesPoster from 'Series/SeriesPoster';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import SeriesMonitoringOptionsPopoverContent from 'AddSeries/SeriesMonitoringOptionsPopoverContent';
|
||||
import SeriesTypePopoverContent from 'AddSeries/SeriesTypePopoverContent';
|
||||
import styles from './AddNewSeriesModalContent.css';
|
||||
@@ -28,19 +27,10 @@ class AddNewSeriesModalContent extends Component {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
seriesType: props.initialSeriesType === seriesTypes.STANDARD ?
|
||||
props.seriesType.value :
|
||||
props.initialSeriesType,
|
||||
searchForMissingEpisodes: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (this.props.seriesType.value !== prevProps.seriesType.value) {
|
||||
this.setState({ seriesType: this.props.seriesType.value });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -57,12 +47,7 @@ class AddNewSeriesModalContent extends Component {
|
||||
}
|
||||
|
||||
onAddSeriesPress = () => {
|
||||
const {
|
||||
searchForMissingEpisodes,
|
||||
seriesType
|
||||
} = this.state;
|
||||
|
||||
this.props.onAddSeriesPress(searchForMissingEpisodes, seriesType);
|
||||
this.props.onAddSeriesPress(this.state.searchForMissingEpisodes);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -215,7 +200,6 @@ class AddNewSeriesModalContent extends Component {
|
||||
name="seriesType"
|
||||
onChange={onInputChange}
|
||||
{...seriesType}
|
||||
value={this.state.seriesType}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
@@ -278,7 +262,6 @@ AddNewSeriesModalContent.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
year: PropTypes.number.isRequired,
|
||||
overview: PropTypes.string,
|
||||
initialSeriesType: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isAdding: PropTypes.bool.isRequired,
|
||||
addError: PropTypes.object,
|
||||
|
||||
@@ -55,13 +55,14 @@ class AddNewSeriesModalContentConnector extends Component {
|
||||
this.props.setAddSeriesDefault({ [name]: value });
|
||||
}
|
||||
|
||||
onAddSeriesPress = (searchForMissingEpisodes, seriesType) => {
|
||||
onAddSeriesPress = (searchForMissingEpisodes) => {
|
||||
const {
|
||||
tvdbId,
|
||||
rootFolderPath,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
languageProfileId,
|
||||
seriesType,
|
||||
seasonFolder,
|
||||
tags
|
||||
} = this.props;
|
||||
@@ -72,7 +73,7 @@ class AddNewSeriesModalContentConnector extends Component {
|
||||
monitor: monitor.value,
|
||||
qualityProfileId: qualityProfileId.value,
|
||||
languageProfileId: languageProfileId.value,
|
||||
seriesType,
|
||||
seriesType: seriesType.value,
|
||||
seasonFolder: seasonFolder.value,
|
||||
tags: tags.value,
|
||||
searchForMissingEpisodes
|
||||
|
||||
@@ -62,7 +62,6 @@
|
||||
.alreadyExistsIcon {
|
||||
margin-left: 10px;
|
||||
color: #37bc9b;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.overview {
|
||||
|
||||
@@ -58,7 +58,6 @@ class AddNewSeriesSearchResult extends Component {
|
||||
statistics,
|
||||
ratings,
|
||||
folder,
|
||||
seriesType,
|
||||
images,
|
||||
isExistingSeries,
|
||||
isSmallScreen
|
||||
@@ -166,17 +165,6 @@ class AddNewSeriesSearchResult extends Component {
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
|
||||
{
|
||||
status === 'upcoming' ?
|
||||
<Label
|
||||
kind={kinds.INFO}
|
||||
size={sizes.LARGE}
|
||||
>
|
||||
Upcoming
|
||||
</Label> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className={styles.overview}>
|
||||
@@ -192,7 +180,6 @@ class AddNewSeriesSearchResult extends Component {
|
||||
year={year}
|
||||
overview={overview}
|
||||
folder={folder}
|
||||
initialSeriesType={seriesType}
|
||||
images={images}
|
||||
onModalClose={this.onAddSeriesModalClose}
|
||||
/>
|
||||
@@ -212,7 +199,6 @@ AddNewSeriesSearchResult.propTypes = {
|
||||
statistics: PropTypes.object.isRequired,
|
||||
ratings: PropTypes.object.isRequired,
|
||||
folder: PropTypes.string.isRequired,
|
||||
seriesType: PropTypes.string.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
isExistingSeries: PropTypes.bool.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired
|
||||
|
||||
@@ -5,7 +5,7 @@ import selectAll from 'Utilities/Table/selectAll';
|
||||
import toggleSelected from 'Utilities/Table/toggleSelected';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import ImportSeriesTableConnector from './ImportSeriesTableConnector';
|
||||
import ImportSeriesFooterConnector from './ImportSeriesFooterConnector';
|
||||
|
||||
@@ -21,15 +21,17 @@ class ImportSeries extends Component {
|
||||
allSelected: false,
|
||||
allUnselected: false,
|
||||
lastToggled: null,
|
||||
selectedState: {}
|
||||
selectedState: {},
|
||||
contentBody: null,
|
||||
scrollTop: 0
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setScrollerRef = (ref) => {
|
||||
this.setState({ scroller: ref });
|
||||
setContentBodyRef = (ref) => {
|
||||
this.setState({ contentBody: ref });
|
||||
}
|
||||
|
||||
//
|
||||
@@ -92,13 +94,13 @@ class ImportSeries extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
selectedState,
|
||||
scroller
|
||||
contentBody
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBody
|
||||
registerScroller={this.setScrollerRef}
|
||||
<PageContentBodyConnector
|
||||
ref={this.setContentBodyRef}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{
|
||||
@@ -119,21 +121,23 @@ class ImportSeries extends Component {
|
||||
}
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && scroller &&
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length && contentBody &&
|
||||
<ImportSeriesTableConnector
|
||||
rootFolderId={rootFolderId}
|
||||
unmappedFolders={unmappedFolders}
|
||||
allSelected={allSelected}
|
||||
allUnselected={allUnselected}
|
||||
selectedState={selectedState}
|
||||
scroller={scroller}
|
||||
contentBody={contentBody}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
scrollTop={this.state.scrollTop}
|
||||
onSelectAllChange={this.onSelectAllChange}
|
||||
onSelectedChange={this.onSelectedChange}
|
||||
onRemoveSelectedStateItem={this.onRemoveSelectedStateItem}
|
||||
onScroll={this.onScroll}
|
||||
/>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
|
||||
{
|
||||
!rootFoldersError && rootFoldersPopulated && !!unmappedFolders.length &&
|
||||
|
||||
@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { inputTypes } from 'Helpers/Props';
|
||||
import FormInputGroup from 'Components/Form/FormInputGroup';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||
import ImportSeriesSelectSeriesConnector from './SelectSeries/ImportSeriesSelectSeriesConnector';
|
||||
@@ -9,6 +10,7 @@ import styles from './ImportSeriesRow.css';
|
||||
|
||||
function ImportSeriesRow(props) {
|
||||
const {
|
||||
style,
|
||||
id,
|
||||
monitor,
|
||||
qualityProfileId,
|
||||
@@ -24,7 +26,7 @@ function ImportSeriesRow(props) {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<>
|
||||
<VirtualTableRow style={style}>
|
||||
<VirtualTableSelectCell
|
||||
inputClassName={styles.selectInput}
|
||||
id={id}
|
||||
@@ -88,14 +90,14 @@ function ImportSeriesRow(props) {
|
||||
<ImportSeriesSelectSeriesConnector
|
||||
id={id}
|
||||
isExistingSeries={isExistingSeries}
|
||||
onInputChange={onInputChange}
|
||||
/>
|
||||
</VirtualTableRowCell>
|
||||
</>
|
||||
</VirtualTableRow>
|
||||
);
|
||||
}
|
||||
|
||||
ImportSeriesRow.propTypes = {
|
||||
style: PropTypes.object.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
monitor: PropTypes.string.isRequired,
|
||||
qualityProfileId: PropTypes.number.isRequired,
|
||||
|
||||
@@ -2,7 +2,6 @@ import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import VirtualTable from 'Components/Table/VirtualTable';
|
||||
import VirtualTableRow from 'Components/Table/VirtualTableRow';
|
||||
import ImportSeriesHeader from './ImportSeriesHeader';
|
||||
import ImportSeriesRowConnector from './ImportSeriesRowConnector';
|
||||
|
||||
@@ -113,19 +112,15 @@ class ImportSeriesTable extends Component {
|
||||
const item = items[rowIndex];
|
||||
|
||||
return (
|
||||
<VirtualTableRow
|
||||
<ImportSeriesRowConnector
|
||||
key={key}
|
||||
style={style}
|
||||
>
|
||||
<ImportSeriesRowConnector
|
||||
key={item.id}
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
</VirtualTableRow>
|
||||
rootFolderId={rootFolderId}
|
||||
showLanguageProfile={showLanguageProfile}
|
||||
isSelected={selectedState[item.id]}
|
||||
onSelectedChange={onSelectedChange}
|
||||
id={item.id}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,10 +133,12 @@ class ImportSeriesTable extends Component {
|
||||
allSelected,
|
||||
allUnselected,
|
||||
isSmallScreen,
|
||||
scroller,
|
||||
contentBody,
|
||||
showLanguageProfile,
|
||||
scrollTop,
|
||||
selectedState,
|
||||
onSelectAllChange
|
||||
onSelectAllChange,
|
||||
onScroll
|
||||
} = this.props;
|
||||
|
||||
if (!items.length) {
|
||||
@@ -151,9 +148,10 @@ class ImportSeriesTable extends Component {
|
||||
return (
|
||||
<VirtualTable
|
||||
items={items}
|
||||
scroller={scroller}
|
||||
contentBody={contentBody}
|
||||
isSmallScreen={isSmallScreen}
|
||||
rowHeight={52}
|
||||
scrollTop={scrollTop}
|
||||
overscanRowCount={2}
|
||||
rowRenderer={this.rowRenderer}
|
||||
header={
|
||||
@@ -165,6 +163,7 @@ class ImportSeriesTable extends Component {
|
||||
/>
|
||||
}
|
||||
selectedState={selectedState}
|
||||
onScroll={onScroll}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -184,13 +183,15 @@ ImportSeriesTable.propTypes = {
|
||||
selectedState: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
allSeries: PropTypes.arrayOf(PropTypes.object),
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
showLanguageProfile: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
onSelectAllChange: PropTypes.func.isRequired,
|
||||
onSelectedChange: PropTypes.func.isRequired,
|
||||
onRemoveSelectedStateItem: PropTypes.func.isRequired,
|
||||
onSeriesLookup: PropTypes.func.isRequired,
|
||||
onSetImportSeriesValue: PropTypes.func.isRequired
|
||||
onSetImportSeriesValue: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ImportSeriesTable;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { queueLookupSeries, setImportSeriesValue } from 'Store/Actions/importSeriesActions';
|
||||
import createImportSeriesItemSelector from 'Store/Selectors/createImportSeriesItemSelector';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import ImportSeriesSelectSeries from './ImportSeriesSelectSeries';
|
||||
|
||||
function createMapStateToProps() {
|
||||
@@ -41,23 +41,13 @@ class ImportSeriesSelectSeriesConnector extends Component {
|
||||
onSeriesSelect = (tvdbId) => {
|
||||
const {
|
||||
id,
|
||||
items,
|
||||
onInputChange
|
||||
items
|
||||
} = this.props;
|
||||
|
||||
const selectedSeries = items.find((item) => item.tvdbId === tvdbId);
|
||||
|
||||
this.props.setImportSeriesValue({
|
||||
id,
|
||||
selectedSeries
|
||||
selectedSeries: _.find(items, { tvdbId })
|
||||
});
|
||||
|
||||
if (selectedSeries.seriesType !== seriesTypes.STANDARD) {
|
||||
onInputChange({
|
||||
name: 'seriesType',
|
||||
value: selectedSeries.seriesType
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -79,7 +69,6 @@ ImportSeriesSelectSeriesConnector.propTypes = {
|
||||
items: PropTypes.arrayOf(PropTypes.object),
|
||||
selectedSeries: PropTypes.object,
|
||||
isSelected: PropTypes.bool,
|
||||
onInputChange: PropTypes.func.isRequired,
|
||||
queueLookupSeries: PropTypes.func.isRequired,
|
||||
setImportSeriesValue: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import FieldSet from 'Components/FieldSet';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FileBrowserModal from 'Components/FileBrowser/FileBrowserModal';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import RootFolders from 'RootFolder/RootFolders';
|
||||
import styles from './ImportSeriesSelectFolder.css';
|
||||
|
||||
@@ -53,7 +53,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
|
||||
return (
|
||||
<PageContent title="Import Series">
|
||||
<PageContentBody>
|
||||
<PageContentBodyConnector>
|
||||
{
|
||||
isFetching && !isPopulated &&
|
||||
<LoadingIndicator />
|
||||
@@ -132,7 +132,7 @@ class ImportSeriesSelectFolder extends Component {
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import BackupsConnector from 'System/Backup/BackupsConnector';
|
||||
import UpdatesConnector from 'System/Updates/UpdatesConnector';
|
||||
import LogsTableConnector from 'System/Events/LogsTableConnector';
|
||||
import Logs from 'System/Logs/Logs';
|
||||
import Diagnostic from 'Diagnostic/Diagnostic';
|
||||
|
||||
function AppRoutes(props) {
|
||||
const {
|
||||
@@ -230,15 +229,6 @@ function AppRoutes(props) {
|
||||
component={Logs}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Diagnostics
|
||||
*/}
|
||||
|
||||
<Route
|
||||
path="/diag"
|
||||
component={Diagnostic}
|
||||
/>
|
||||
|
||||
{/*
|
||||
Not Found
|
||||
*/}
|
||||
|
||||
@@ -3,10 +3,9 @@ import React, { Component } from 'react';
|
||||
import { align, icons } from 'Helpers/Props';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import Measure from 'Components/Measure';
|
||||
import PageContentBody from 'Components/Page/PageContentBody';
|
||||
import PageContentBodyConnector from 'Components/Page/PageContentBodyConnector';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||
import NoSeries from 'Series/NoSeries';
|
||||
@@ -77,10 +76,8 @@ class CalendarPage extends Component {
|
||||
filters,
|
||||
hasSeries,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing,
|
||||
useCurrentPage,
|
||||
onRssSyncPress,
|
||||
onFilterSelect
|
||||
} = this.props;
|
||||
|
||||
@@ -102,15 +99,6 @@ class CalendarPage extends Component {
|
||||
onPress={this.onGetCalendarLinkPress}
|
||||
/>
|
||||
|
||||
<PageToolbarSeparator />
|
||||
|
||||
<PageToolbarButton
|
||||
label="RSS Sync"
|
||||
iconName={icons.RSS}
|
||||
isSpinning={isRssSyncExecuting}
|
||||
onPress={onRssSyncPress}
|
||||
/>
|
||||
|
||||
<PageToolbarButton
|
||||
label="Search for Missing"
|
||||
iconName={icons.SEARCH}
|
||||
@@ -138,7 +126,7 @@ class CalendarPage extends Component {
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
|
||||
<PageContentBody
|
||||
<PageContentBodyConnector
|
||||
className={styles.calendarPageBody}
|
||||
innerClassName={styles.calendarInnerPageBody}
|
||||
>
|
||||
@@ -159,7 +147,7 @@ class CalendarPage extends Component {
|
||||
hasSeries &&
|
||||
<LegendConnector />
|
||||
}
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
|
||||
<CalendarLinkModal
|
||||
isOpen={isCalendarLinkModalOpen}
|
||||
@@ -180,12 +168,10 @@ CalendarPage.propTypes = {
|
||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
hasSeries: PropTypes.bool.isRequired,
|
||||
missingEpisodeIds: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
isRssSyncExecuting: PropTypes.bool.isRequired,
|
||||
isSearchingForMissing: PropTypes.bool.isRequired,
|
||||
useCurrentPage: PropTypes.bool.isRequired,
|
||||
onSearchMissingPress: PropTypes.func.isRequired,
|
||||
onDaysCountChange: PropTypes.func.isRequired,
|
||||
onRssSyncPress: PropTypes.func.isRequired,
|
||||
onFilterSelect: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
|
||||
@@ -3,14 +3,11 @@ import { createSelector } from 'reselect';
|
||||
import moment from 'moment';
|
||||
import { isCommandExecuting } from 'Utilities/Command';
|
||||
import isBefore from 'Utilities/Date/isBefore';
|
||||
import * as commandNames from 'Commands/commandNames';
|
||||
import withCurrentPage from 'Components/withCurrentPage';
|
||||
import { executeCommand } from 'Store/Actions/commandActions';
|
||||
import { searchMissing, setCalendarDaysCount, setCalendarFilter } from 'Store/Actions/calendarActions';
|
||||
import createSeriesCountSelector from 'Store/Selectors/createSeriesCountSelector';
|
||||
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
|
||||
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
|
||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||
import CalendarPage from './CalendarPage';
|
||||
|
||||
function createMissingEpisodeIdsSelector() {
|
||||
@@ -62,7 +59,6 @@ function createMapStateToProps() {
|
||||
createSeriesCountSelector(),
|
||||
createUISettingsSelector(),
|
||||
createMissingEpisodeIdsSelector(),
|
||||
createCommandExecutingSelector(commandNames.RSS_SYNC),
|
||||
createIsSearchingSelector(),
|
||||
(
|
||||
selectedFilterKey,
|
||||
@@ -70,7 +66,6 @@ function createMapStateToProps() {
|
||||
seriesCount,
|
||||
uiSettings,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing
|
||||
) => {
|
||||
return {
|
||||
@@ -79,7 +74,6 @@ function createMapStateToProps() {
|
||||
colorImpairedMode: uiSettings.enableColorImpairedMode,
|
||||
hasSeries: !!seriesCount,
|
||||
missingEpisodeIds,
|
||||
isRssSyncExecuting,
|
||||
isSearchingForMissing
|
||||
};
|
||||
}
|
||||
@@ -88,12 +82,6 @@ function createMapStateToProps() {
|
||||
|
||||
function createMapDispatchToProps(dispatch, props) {
|
||||
return {
|
||||
onRssSyncPress() {
|
||||
dispatch(executeCommand({
|
||||
name: commandNames.RSS_SYNC
|
||||
}));
|
||||
},
|
||||
|
||||
onSearchMissingPress(episodeIds) {
|
||||
dispatch(searchMissing({ episodeIds }));
|
||||
},
|
||||
|
||||
@@ -11,12 +11,10 @@ function CalendarEventQueueDetails(props) {
|
||||
sizeleft,
|
||||
estimatedCompletionTime,
|
||||
status,
|
||||
trackedDownloadState,
|
||||
trackedDownloadStatus,
|
||||
errorMessage
|
||||
} = props;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
return (
|
||||
<QueueDetails
|
||||
@@ -25,8 +23,6 @@ function CalendarEventQueueDetails(props) {
|
||||
sizeleft={sizeleft}
|
||||
estimatedCompletionTime={estimatedCompletionTime}
|
||||
status={status}
|
||||
trackedDownloadState={trackedDownloadState}
|
||||
trackedDownloadStatus={trackedDownloadStatus}
|
||||
errorMessage={errorMessage}
|
||||
progressBar={
|
||||
<div title={`Episode is downloading - ${progress.toFixed(1)}% ${title}`}>
|
||||
@@ -48,8 +44,6 @@ CalendarEventQueueDetails.propTypes = {
|
||||
sizeleft: PropTypes.number.isRequired,
|
||||
estimatedCompletionTime: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
trackedDownloadState: PropTypes.string.isRequired,
|
||||
trackedDownloadStatus: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export const APPLICATION_UPDATE = 'ApplicationUpdate';
|
||||
export const BACKUP = 'Backup';
|
||||
export const REFRESH_MONITORED_DOWNLOADS = 'RefreshMonitoredDownloads';
|
||||
export const CHECK_FOR_FINISHED_DOWNLOAD = 'CheckForFinishedDownload';
|
||||
export const CLEAR_BLACKLIST = 'ClearBlacklist';
|
||||
export const CLEAR_LOGS = 'ClearLog';
|
||||
export const CUTOFF_UNMET_EPISODE_SEARCH = 'CutoffUnmetEpisodeSearch';
|
||||
|
||||
@@ -172,7 +172,7 @@ class FilterBuilderModalContent extends Component {
|
||||
filters.map((filter, index) => {
|
||||
return (
|
||||
<FilterBuilderRow
|
||||
key={`${filter.key}-${index}`}
|
||||
key={index}
|
||||
index={index}
|
||||
sectionItems={sectionItems}
|
||||
filterBuilderProps={filterBuilderProps}
|
||||
|
||||
@@ -135,7 +135,7 @@ class FilterBuilderRowValue extends Component {
|
||||
tagList={tagList}
|
||||
allowNew={!tagList.length}
|
||||
kind={kinds.DEFAULT}
|
||||
delimiters={['Tab', 'Enter']}
|
||||
delimiters={[9, 13]}
|
||||
maxSuggestionsLength={100}
|
||||
minQueryLength={0}
|
||||
tagComponent={FilterBuilderRowValueTag}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
|
||||
const seriesStatusList = [
|
||||
const protocols = [
|
||||
{ id: 'continuing', name: 'Continuing' },
|
||||
{ id: 'upcoming', name: 'Upcoming' },
|
||||
{ id: 'ended', name: 'Ended' }
|
||||
];
|
||||
|
||||
function SeriesStatusFilterBuilderRowValue(props) {
|
||||
return (
|
||||
<FilterBuilderRowValue
|
||||
tagList={seriesStatusList}
|
||||
tagList={protocols}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,19 @@
|
||||
.enhancedSelect {
|
||||
composes: input from '~Components/Form/Input.css';
|
||||
composes: link from '~Components/Link/Link.css';
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 16px;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
border: 1px solid $inputBorderColor;
|
||||
border-radius: 4px;
|
||||
background-color: $white;
|
||||
box-shadow: inset 0 1px 1px $inputBoxShadowColor;
|
||||
color: $black;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.hasError {
|
||||
@@ -45,7 +56,6 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 90%;
|
||||
max-height: 100%;
|
||||
width: 350px !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ class EnhancedSelectInputOption extends Component {
|
||||
|
||||
EnhancedSelectInputOption.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
isSelected: PropTypes.bool.isRequired,
|
||||
isDisabled: PropTypes.bool.isRequired,
|
||||
isHidden: PropTypes.bool.isRequired,
|
||||
|
||||
@@ -14,7 +14,6 @@ import PasswordInput from './PasswordInput';
|
||||
import PathInputConnector from './PathInputConnector';
|
||||
import QualityProfileSelectInputConnector from './QualityProfileSelectInputConnector';
|
||||
import LanguageProfileSelectInputConnector from './LanguageProfileSelectInputConnector';
|
||||
import IndexerSelectInputConnector from './IndexerSelectInputConnector';
|
||||
import RootFolderSelectInputConnector from './RootFolderSelectInputConnector';
|
||||
import SeriesTypeSelectInput from './SeriesTypeSelectInput';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
@@ -62,9 +61,6 @@ function getComponent(type) {
|
||||
case inputTypes.LANGUAGE_PROFILE_SELECT:
|
||||
return LanguageProfileSelectInputConnector;
|
||||
|
||||
case inputTypes.INDEXER_SELECT:
|
||||
return IndexerSelectInputConnector;
|
||||
|
||||
case inputTypes.ROOT_FOLDER_SELECT:
|
||||
return RootFolderSelectInputConnector;
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import { fetchIndexers } from 'Store/Actions/settingsActions';
|
||||
import EnhancedSelectInput from './EnhancedSelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.settings.indexers,
|
||||
(state, { includeAny }) => includeAny,
|
||||
(indexers, includeAny) => {
|
||||
const {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
items
|
||||
} = indexers;
|
||||
|
||||
const values = _.map(items.sort(sortByName), (indexer) => {
|
||||
return {
|
||||
key: indexer.id,
|
||||
value: indexer.name
|
||||
};
|
||||
});
|
||||
|
||||
if (includeAny) {
|
||||
values.unshift({
|
||||
key: 0,
|
||||
value: '(Any)'
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
isFetching,
|
||||
isPopulated,
|
||||
error,
|
||||
values
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchFetchIndexers: fetchIndexers
|
||||
};
|
||||
|
||||
class IndexerSelectInputConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isPopulated) {
|
||||
this.props.dispatchFetchIndexers();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onChange = ({ name, value }) => {
|
||||
this.props.onChange({ name, value: parseInt(value) });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<EnhancedSelectInput
|
||||
{...this.props}
|
||||
onChange={this.onChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IndexerSelectInputConnector.propTypes = {
|
||||
isFetching: PropTypes.bool.isRequired,
|
||||
isPopulated: PropTypes.bool.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired,
|
||||
values: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
includeAny: PropTypes.bool.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
dispatchFetchIndexers: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
IndexerSelectInputConnector.defaultProps = {
|
||||
includeAny: false
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(IndexerSelectInputConnector);
|
||||
@@ -98,9 +98,7 @@ class KeyValueListInput extends Component {
|
||||
className,
|
||||
value,
|
||||
keyPlaceholder,
|
||||
valuePlaceholder,
|
||||
hasError,
|
||||
hasWarning
|
||||
valuePlaceholder
|
||||
} = this.props;
|
||||
|
||||
const { isFocused } = this.state;
|
||||
@@ -108,9 +106,7 @@ class KeyValueListInput extends Component {
|
||||
return (
|
||||
<div className={classNames(
|
||||
className,
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
isFocused && styles.isFocused
|
||||
)}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -8,16 +8,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.inputWrapper {
|
||||
flex: 1 0 0;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
flex: 0 0 22px;
|
||||
}
|
||||
|
||||
.keyInput,
|
||||
.valueInput {
|
||||
width: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -63,41 +63,34 @@ class KeyValueListInputItem extends Component {
|
||||
|
||||
return (
|
||||
<div className={styles.itemContainer}>
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={this.onKeyChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
className={styles.keyInput}
|
||||
name="key"
|
||||
value={keyValue}
|
||||
placeholder={keyPlaceholder}
|
||||
onChange={this.onKeyChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
||||
<div className={styles.inputWrapper}>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={this.onValueChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
</div>
|
||||
<TextInput
|
||||
className={styles.valueInput}
|
||||
name="value"
|
||||
value={value}
|
||||
placeholder={valuePlaceholder}
|
||||
onChange={this.onValueChange}
|
||||
onFocus={this.onFocus}
|
||||
onBlur={this.onBlur}
|
||||
/>
|
||||
|
||||
<div className={styles.buttonWrapper}>
|
||||
{
|
||||
isNew ?
|
||||
null :
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!isNew &&
|
||||
<IconButton
|
||||
name={icons.REMOVE}
|
||||
tabIndex={-1}
|
||||
onPress={this.onRemovePress}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,16 +4,15 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.languageProfiles', sortByName),
|
||||
(state) => state.settings.languageProfiles,
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(languageProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(languageProfiles.items, (languageProfile) => {
|
||||
const values = _.map(languageProfiles.items.sort(sortByName), (languageProfile) => {
|
||||
return {
|
||||
key: languageProfile.id,
|
||||
value: languageProfile.name
|
||||
|
||||
@@ -4,16 +4,15 @@ import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import sortByName from 'Utilities/Array/sortByName';
|
||||
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createSortedSectionSelector('settings.qualityProfiles', sortByName),
|
||||
(state) => state.settings.qualityProfiles,
|
||||
(state, { includeNoChange }) => includeNoChange,
|
||||
(state, { includeMixed }) => includeMixed,
|
||||
(qualityProfiles, includeNoChange, includeMixed) => {
|
||||
const values = _.map(qualityProfiles.items, (qualityProfile) => {
|
||||
const values = _.map(qualityProfiles.items.sort(sortByName), (qualityProfile) => {
|
||||
return {
|
||||
key: qualityProfile.id,
|
||||
value: qualityProfile.name
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import * as seriesTypes from 'Utilities/Series/seriesTypes';
|
||||
import SelectInput from './SelectInput';
|
||||
|
||||
const seriesTypeOptions = [
|
||||
{ key: seriesTypes.STANDARD, value: 'Standard' },
|
||||
{ key: seriesTypes.DAILY, value: 'Daily' },
|
||||
{ key: seriesTypes.ANIME, value: 'Anime' }
|
||||
{ key: 'standard', value: 'Standard' },
|
||||
{ key: 'daily', value: 'Daily' },
|
||||
{ key: 'anime', value: 'Anime' }
|
||||
];
|
||||
|
||||
function SeriesTypeSelectInput(props) {
|
||||
|
||||
@@ -12,14 +12,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.hasError {
|
||||
composes: hasError from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.hasWarning {
|
||||
composes: hasWarning from '~Components/Form/Input.css';
|
||||
}
|
||||
|
||||
.internalInput {
|
||||
flex: 1 1 0%;
|
||||
margin-left: 3px;
|
||||
|
||||
@@ -100,9 +100,9 @@ class TagInput extends Component {
|
||||
suggestions
|
||||
} = this.state;
|
||||
|
||||
const key = event.key;
|
||||
const keyCode = event.keyCode;
|
||||
|
||||
if (key === 'Backspace' && !value.length) {
|
||||
if (keyCode === 8 && !value.length) {
|
||||
const index = tags.length - 1;
|
||||
|
||||
if (index >= 0) {
|
||||
@@ -116,7 +116,7 @@ class TagInput extends Component {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (delimiters.includes(key)) {
|
||||
if (delimiters.includes(keyCode)) {
|
||||
const selectedIndex = this._autosuggestRef.highlightedSuggestionIndex;
|
||||
const tag = getTag(value, selectedIndex, suggestions, allowNew);
|
||||
|
||||
@@ -210,8 +210,6 @@ class TagInput extends Component {
|
||||
const {
|
||||
className,
|
||||
inputContainerClassName,
|
||||
hasError,
|
||||
hasWarning,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -228,9 +226,7 @@ class TagInput extends Component {
|
||||
className={styles.internalInput}
|
||||
inputContainerClassName={classNames(
|
||||
inputContainerClassName,
|
||||
isFocused && styles.isFocused,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning
|
||||
isFocused && styles.isFocused
|
||||
)}
|
||||
value={value}
|
||||
suggestions={suggestions}
|
||||
@@ -260,7 +256,7 @@ TagInput.propTypes = {
|
||||
allowNew: PropTypes.bool.isRequired,
|
||||
kind: PropTypes.oneOf(kinds.all).isRequired,
|
||||
placeholder: PropTypes.string.isRequired,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
delimiters: PropTypes.arrayOf(PropTypes.number).isRequired,
|
||||
minQueryLength: PropTypes.number.isRequired,
|
||||
hasError: PropTypes.bool,
|
||||
hasWarning: PropTypes.bool,
|
||||
@@ -275,7 +271,8 @@ TagInput.defaultProps = {
|
||||
allowNew: true,
|
||||
kind: kinds.INFO,
|
||||
placeholder: '',
|
||||
delimiters: ['Tab', 'Enter', ' ', ','],
|
||||
// Tab, enter, space and comma
|
||||
delimiters: [9, 13, 32, 188],
|
||||
minQueryLength: 1,
|
||||
tagComponent: TagInputTag
|
||||
};
|
||||
|
||||
@@ -25,7 +25,3 @@
|
||||
.warning {
|
||||
color: $warningColor;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: $purple;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './LoadingIndicator.css';
|
||||
|
||||
function LoadingIndicator({ className, rippleClassName, size }) {
|
||||
function LoadingIndicator({ className, size }) {
|
||||
const sizeInPx = `${size}px`;
|
||||
const width = sizeInPx;
|
||||
const height = sizeInPx;
|
||||
@@ -17,17 +17,17 @@ function LoadingIndicator({ className, rippleClassName, size }) {
|
||||
style={{ width, height }}
|
||||
>
|
||||
<div
|
||||
className={rippleClassName}
|
||||
className={styles.ripple}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={rippleClassName}
|
||||
className={styles.ripple}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={rippleClassName}
|
||||
className={styles.ripple}
|
||||
style={{ width, height }}
|
||||
/>
|
||||
</div>
|
||||
@@ -37,13 +37,11 @@ function LoadingIndicator({ className, rippleClassName, size }) {
|
||||
|
||||
LoadingIndicator.propTypes = {
|
||||
className: PropTypes.string,
|
||||
rippleClassName: PropTypes.string,
|
||||
size: PropTypes.number
|
||||
};
|
||||
|
||||
LoadingIndicator.defaultProps = {
|
||||
className: styles.loading,
|
||||
rippleClassName: styles.ripple,
|
||||
size: 50
|
||||
};
|
||||
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Link from 'Components/Link/Link';
|
||||
|
||||
class InlineMarkdown extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
className,
|
||||
data
|
||||
} = this.props;
|
||||
|
||||
// For now only replace links
|
||||
const markdownBlocks = [];
|
||||
if (data) {
|
||||
const regex = RegExp(/\[(.+?)\]\((.+?)\)/g);
|
||||
|
||||
let endIndex = 0;
|
||||
let match = null;
|
||||
while ((match = regex.exec(data)) !== null) {
|
||||
if (match.index > endIndex) {
|
||||
markdownBlocks.push(data.substr(endIndex, match.index - endIndex));
|
||||
}
|
||||
markdownBlocks.push(<Link key={match.index} to={match[2]}>{match[1]}</Link>);
|
||||
endIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
if (endIndex !== data.length) {
|
||||
markdownBlocks.push(data.substr(endIndex, data.length - endIndex));
|
||||
}
|
||||
}
|
||||
|
||||
return <span className={className}>{markdownBlocks}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
InlineMarkdown.propTypes = {
|
||||
className: PropTypes.string,
|
||||
data: PropTypes.string
|
||||
};
|
||||
|
||||
export default InlineMarkdown;
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { kinds, sizes } from 'Helpers/Props';
|
||||
import keyboardShortcuts from 'Components/keyboardShortcuts';
|
||||
import Button from 'Components/Link/Button';
|
||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
||||
import Modal from 'Components/Modal/Modal';
|
||||
@@ -22,19 +21,9 @@ function ConfirmModal(props) {
|
||||
hideCancelButton,
|
||||
isSpinning,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
bindShortcut,
|
||||
unbindShortcut
|
||||
onCancel
|
||||
} = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
bindShortcut('enter', onConfirm);
|
||||
} else {
|
||||
unbindShortcut('enter', onConfirm);
|
||||
}
|
||||
}, [onConfirm]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -60,7 +49,7 @@ function ConfirmModal(props) {
|
||||
}
|
||||
|
||||
<SpinnerButton
|
||||
autoFocus={true}
|
||||
data-autofocus={true}
|
||||
kind={kind}
|
||||
isSpinning={isSpinning}
|
||||
onPress={onConfirm}
|
||||
@@ -85,9 +74,7 @@ ConfirmModal.propTypes = {
|
||||
hideCancelButton: PropTypes.bool,
|
||||
isSpinning: PropTypes.bool.isRequired,
|
||||
onConfirm: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
bindShortcut: PropTypes.func.isRequired,
|
||||
unbindShortcut: PropTypes.func.isRequired
|
||||
onCancel: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
ConfirmModal.defaultProps = {
|
||||
@@ -98,4 +85,4 @@ ConfirmModal.defaultProps = {
|
||||
isSpinning: false
|
||||
};
|
||||
|
||||
export default keyboardShortcuts(ConfirmModal);
|
||||
export default ConfirmModal;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import FocusLock from 'react-focus-lock';
|
||||
import classNames from 'classnames';
|
||||
import elementClass from 'element-class';
|
||||
import getUniqueElememtId from 'Utilities/getUniqueElementId';
|
||||
@@ -182,33 +181,31 @@ class Modal extends Component {
|
||||
}
|
||||
|
||||
return ReactDOM.createPortal(
|
||||
<FocusLock disabled={false}>
|
||||
<div
|
||||
className={styles.modalContainer}
|
||||
>
|
||||
<div
|
||||
className={styles.modalContainer}
|
||||
ref={this._setBackgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={this.onBackdropBeginPress}
|
||||
onMouseUp={this.onBackdropEndPress}
|
||||
>
|
||||
<div
|
||||
ref={this._setBackgroundRef}
|
||||
className={backdropClassName}
|
||||
onMouseDown={this.onBackdropBeginPress}
|
||||
onMouseUp={this.onBackdropEndPress}
|
||||
className={classNames(
|
||||
className,
|
||||
styles[size]
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
styles[size]
|
||||
)}
|
||||
style={style}
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
<ErrorBoundary
|
||||
errorComponent={ModalError}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
</FocusLock>,
|
||||
</div>,
|
||||
this._node
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import styles from './MonitorToggleButton.css';
|
||||
|
||||
function getTooltip(monitored, isDisabled) {
|
||||
if (isDisabled) {
|
||||
return 'Cannot toggle monitored state when series is unmonitored';
|
||||
return 'Cannot toogle monitored state when series is unmonitored';
|
||||
}
|
||||
|
||||
if (monitored) {
|
||||
|
||||
@@ -3,18 +3,6 @@
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
margin-top: 18px;
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ripple {
|
||||
composes: ripple from '~Components/Loading/LoadingIndicator.css';
|
||||
|
||||
border: 2px solid $toolbarColor;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-left: 8px;
|
||||
width: 200px;
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import Autosuggest from 'react-autosuggest';
|
||||
import Fuse from 'fuse.js';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import Icon from 'Components/Icon';
|
||||
import keyboardShortcuts, { shortcuts } from 'Components/keyboardShortcuts';
|
||||
import SeriesSearchResult from './SeriesSearchResult';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import FuseWorker from './fuse.worker';
|
||||
import styles from './SeriesSearchInput.css';
|
||||
|
||||
const LOADING_TYPE = 'suggestionsLoading';
|
||||
const ADD_NEW_TYPE = 'addNew';
|
||||
const workerInstance = new FuseWorker();
|
||||
|
||||
const fuseOptions = {
|
||||
shouldSort: true,
|
||||
includeMatches: true,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [
|
||||
'title',
|
||||
'alternateTitles.title',
|
||||
'tags.label'
|
||||
]
|
||||
};
|
||||
|
||||
class SeriesSearchInput extends Component {
|
||||
|
||||
@@ -32,7 +43,6 @@ class SeriesSearchInput extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.bindShortcut(shortcuts.SERIES_SEARCH_INPUT.key, this.focusInput);
|
||||
workerInstance.addEventListener('message', this.onSuggestionsReceived, false);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -72,16 +82,6 @@ class SeriesSearchInput extends Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (item.type === LOADING_TYPE) {
|
||||
return (
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
rippleClassName={styles.ripple}
|
||||
size={30}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SeriesSearchResult
|
||||
{...item.item}
|
||||
@@ -154,30 +154,35 @@ class SeriesSearchInput extends Component {
|
||||
}
|
||||
|
||||
onSuggestionsFetchRequested = ({ value }) => {
|
||||
this.setState({
|
||||
suggestions: [
|
||||
{
|
||||
type: LOADING_TYPE,
|
||||
title: value
|
||||
const { series } = this.props;
|
||||
let suggestions = [];
|
||||
|
||||
if (value.length === 1) {
|
||||
suggestions = series.reduce((acc, s) => {
|
||||
if (s.firstCharacter === value.toLowerCase()) {
|
||||
acc.push({
|
||||
item: s,
|
||||
indices: [
|
||||
[0, 0]
|
||||
],
|
||||
matches: [
|
||||
{
|
||||
value: s.title,
|
||||
key: 'title'
|
||||
}
|
||||
],
|
||||
arrayIndex: 0
|
||||
});
|
||||
}
|
||||
]
|
||||
});
|
||||
this.requestSuggestions(value);
|
||||
};
|
||||
|
||||
requestSuggestions = _.debounce((value) => {
|
||||
const payload = {
|
||||
value,
|
||||
series: this.props.series
|
||||
};
|
||||
return acc;
|
||||
}, []);
|
||||
} else {
|
||||
const fuse = new Fuse(series, fuseOptions);
|
||||
suggestions = fuse.search(value);
|
||||
}
|
||||
|
||||
workerInstance.postMessage(payload);
|
||||
}, 250);
|
||||
|
||||
onSuggestionsReceived = (message) => {
|
||||
this.setState({
|
||||
suggestions: message.data
|
||||
});
|
||||
this.setState({ suggestions });
|
||||
}
|
||||
|
||||
onSuggestionsClearRequested = () => {
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const fuseOptions = {
|
||||
shouldSort: true,
|
||||
includeMatches: true,
|
||||
threshold: 0.3,
|
||||
location: 0,
|
||||
distance: 100,
|
||||
maxPatternLength: 32,
|
||||
minMatchCharLength: 1,
|
||||
keys: [
|
||||
'title',
|
||||
'alternateTitles.title',
|
||||
'tags.label'
|
||||
]
|
||||
};
|
||||
|
||||
function getSuggestions(series, value) {
|
||||
const limit = 10;
|
||||
let suggestions = [];
|
||||
|
||||
if (value.length === 1) {
|
||||
for (let i = 0; i < series.length; i++) {
|
||||
const s = series[i];
|
||||
if (s.firstCharacter === value.toLowerCase()) {
|
||||
suggestions.push({
|
||||
item: series[i],
|
||||
indices: [
|
||||
[0, 0]
|
||||
],
|
||||
matches: [
|
||||
{
|
||||
value: s.title,
|
||||
key: 'title'
|
||||
}
|
||||
],
|
||||
arrayIndex: 0
|
||||
});
|
||||
if (suggestions.length > limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const fuse = new Fuse(series, fuseOptions);
|
||||
suggestions = fuse.search(value, { limit });
|
||||
}
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
|
||||
self.addEventListener('message', (e) => {
|
||||
if (!e) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
series,
|
||||
value
|
||||
} = e.data;
|
||||
|
||||
self.postMessage(getSuggestions(series, value));
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { isMobile as isMobileUtil } from 'Utilities/mobile';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import OverlayScroller from 'Components/Scroller/OverlayScroller';
|
||||
@@ -9,15 +8,6 @@ import styles from './PageContentBody.css';
|
||||
|
||||
class PageContentBody extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this._isMobile = isMobileUtil();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
@@ -36,12 +26,13 @@ class PageContentBody extends Component {
|
||||
const {
|
||||
className,
|
||||
innerClassName,
|
||||
isSmallScreen,
|
||||
children,
|
||||
dispatch,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const ScrollerComponent = this._isMobile ? Scroller : OverlayScroller;
|
||||
const ScrollerComponent = isSmallScreen ? Scroller : OverlayScroller;
|
||||
|
||||
return (
|
||||
<ScrollerComponent
|
||||
@@ -61,6 +52,7 @@ class PageContentBody extends Component {
|
||||
PageContentBody.propTypes = {
|
||||
className: PropTypes.string,
|
||||
innerClassName: PropTypes.string,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
onScroll: PropTypes.func,
|
||||
dispatch: PropTypes.func
|
||||
|
||||
17
frontend/src/Components/Page/PageContentBodyConnector.js
Normal file
17
frontend/src/Components/Page/PageContentBodyConnector.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
|
||||
import PageContentBody from './PageContentBody';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
createDimensionsSelector(),
|
||||
(dimensions) => {
|
||||
return {
|
||||
isSmallScreen: dimensions.isSmallScreen
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps, null, null, { forwardRef: true })(PageContentBody);
|
||||
@@ -1,17 +1,17 @@
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import PageContentBody from './PageContentBody';
|
||||
import PageContentBodyConnector from './PageContentBodyConnector';
|
||||
import styles from './PageContentError.css';
|
||||
|
||||
function PageContentError(props) {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<PageContentBody>
|
||||
<PageContentBodyConnector>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message='There was an error loading this page'
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContentBodyConnector>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import dimensions from 'Styles/Variables/dimensions';
|
||||
@@ -17,7 +18,7 @@ class PageJumpBar extends Component {
|
||||
|
||||
this.state = {
|
||||
height: 0,
|
||||
visibleItems: props.items.order
|
||||
visibleItems: props.items
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,47 +52,29 @@ class PageJumpBar extends Component {
|
||||
minimumItems
|
||||
} = this.props;
|
||||
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
characters,
|
||||
order
|
||||
} = items;
|
||||
|
||||
const height = this.state.height;
|
||||
const maximumItems = Math.floor(height / ITEM_HEIGHT);
|
||||
const diff = order.length - maximumItems;
|
||||
const diff = items.length - maximumItems;
|
||||
|
||||
if (diff < 0) {
|
||||
this.setState({ visibleItems: order });
|
||||
this.setState({ visibleItems: items });
|
||||
return;
|
||||
}
|
||||
|
||||
if (order.length < minimumItems) {
|
||||
this.setState({ visibleItems: order });
|
||||
if (items.length < minimumItems) {
|
||||
this.setState({ visibleItems: items });
|
||||
return;
|
||||
}
|
||||
|
||||
// get first, last, and most common in between to make up numbers
|
||||
const visibleItems = [order[0]];
|
||||
const removeDiff = Math.ceil(items.length / maximumItems);
|
||||
|
||||
const sorted = order.slice(1, -1).map((x) => characters[x]).sort((a, b) => b - a);
|
||||
const minCount = sorted[maximumItems - 3];
|
||||
const greater = sorted.reduce((acc, value) => acc + (value > minCount ? 1 : 0), 0);
|
||||
let minAllowed = maximumItems - 2 - greater;
|
||||
|
||||
for (let i = 1; i < order.length - 1; i++) {
|
||||
if (characters[order[i]] > minCount) {
|
||||
visibleItems.push(order[i]);
|
||||
} else if (characters[order[i]] === minCount && minAllowed > 0) {
|
||||
visibleItems.push(order[i]);
|
||||
minAllowed--;
|
||||
const visibleItems = _.reduce(items, (acc, item, index) => {
|
||||
if (index % removeDiff === 0) {
|
||||
acc.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
visibleItems.push(order[order.length - 1]);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
this.setState({ visibleItems });
|
||||
}
|
||||
@@ -146,7 +129,7 @@ class PageJumpBar extends Component {
|
||||
}
|
||||
|
||||
PageJumpBar.propTypes = {
|
||||
items: PropTypes.object.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
minimumItems: PropTypes.number.isRequired,
|
||||
onItemPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
.jumpBarItem {
|
||||
flex: 1 1 $jumpBarItemHeight;
|
||||
flex: 1 0 $jumpBarItemHeight;
|
||||
border-bottom: 1px solid $borderColor;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
|
||||
@@ -165,23 +165,6 @@ const links = [
|
||||
to: '/system/logs/files'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
iconName: icons.DEBUG,
|
||||
hidden: true,
|
||||
title: 'Diagnostics',
|
||||
to: '/diag/status',
|
||||
children: [
|
||||
{
|
||||
title: 'Status',
|
||||
to: '/diag/status'
|
||||
},
|
||||
{
|
||||
title: 'Script Console',
|
||||
to: '/diag/script'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -490,10 +473,6 @@ class PageSidebar extends Component {
|
||||
const isActiveParent = activeParent === link.to;
|
||||
const hasActiveChild = hasActiveChildLink(link, pathname);
|
||||
|
||||
if (link.hidden && !isActiveParent && !hasActiveChild) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<PageSidebarItem
|
||||
key={link.to}
|
||||
|
||||
@@ -41,7 +41,6 @@ function PageToolbarButton(props) {
|
||||
}
|
||||
|
||||
PageToolbarButton.propTypes = {
|
||||
...Link.propTypes,
|
||||
label: PropTypes.string.isRequired,
|
||||
iconName: PropTypes.object.isRequired,
|
||||
spinningName: PropTypes.object,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.sectionContainer {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex: 1 1 10%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import styles from './PageToolbarSection.css';
|
||||
const BUTTON_WIDTH = parseInt(dimensions.toolbarButtonWidth);
|
||||
const SEPARATOR_MARGIN = parseInt(dimensions.toolbarSeparatorMargin);
|
||||
const SEPARATOR_WIDTH = 2 * SEPARATOR_MARGIN + 1;
|
||||
const SEPARATOR_NAME = 'PageToolbarSeparator';
|
||||
|
||||
function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
|
||||
let buttonCount = 0;
|
||||
@@ -22,7 +23,9 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
|
||||
const validChildren = [];
|
||||
|
||||
forEach(children, (child) => {
|
||||
if (Object.keys(child.props).length === 0) {
|
||||
const name = child.type.name;
|
||||
|
||||
if (name === SEPARATOR_NAME) {
|
||||
separatorCount++;
|
||||
} else {
|
||||
buttonCount++;
|
||||
@@ -65,14 +68,12 @@ function calculateOverflowItems(children, isMeasured, width, collapseButtons) {
|
||||
}
|
||||
|
||||
validChildren.forEach((child, index) => {
|
||||
const isSeparator = Object.keys(child.props).length === 0;
|
||||
|
||||
if (actualButtons < maxButtons) {
|
||||
if (!isSeparator) {
|
||||
if (child.type.name !== SEPARATOR_NAME) {
|
||||
buttons.push(child);
|
||||
actualButtons++;
|
||||
}
|
||||
} else if (!isSeparator) {
|
||||
} else if (child.type.name !== SEPARATOR_NAME) {
|
||||
overflowItems.push(child.props);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,10 +2,6 @@
|
||||
/* Placeholder */
|
||||
}
|
||||
|
||||
.track {
|
||||
/* Placeholder */
|
||||
}
|
||||
|
||||
.thumb {
|
||||
min-height: 100px;
|
||||
border: 1px solid transparent;
|
||||
|
||||
@@ -37,10 +37,6 @@ class OverlayScroller extends Component {
|
||||
|
||||
_setScrollRef = (ref) => {
|
||||
this._scroller = ref;
|
||||
|
||||
if (ref) {
|
||||
this.props.registerScroller(ref.view);
|
||||
}
|
||||
}
|
||||
|
||||
_renderThumb = (props) => {
|
||||
@@ -64,7 +60,6 @@ class OverlayScroller extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.track}
|
||||
style={finalStyle}
|
||||
{...props}
|
||||
/>
|
||||
@@ -83,7 +78,6 @@ class OverlayScroller extends Component {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.track}
|
||||
style={finalStyle}
|
||||
{...props}
|
||||
/>
|
||||
@@ -163,8 +157,7 @@ OverlayScroller.propTypes = {
|
||||
autoHide: PropTypes.bool.isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func,
|
||||
registerScroller: PropTypes.func
|
||||
onScroll: PropTypes.func
|
||||
};
|
||||
|
||||
OverlayScroller.defaultProps = {
|
||||
@@ -172,8 +165,7 @@ OverlayScroller.defaultProps = {
|
||||
trackClassName: styles.thumb,
|
||||
scrollDirection: scrollDirections.VERTICAL,
|
||||
autoHide: false,
|
||||
autoScroll: true,
|
||||
registerScroller: () => {}
|
||||
autoScroll: true
|
||||
};
|
||||
|
||||
export default OverlayScroller;
|
||||
|
||||
@@ -17,18 +17,12 @@ class Scroller extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
const {
|
||||
scrollDirection,
|
||||
autoFocus,
|
||||
scrollTop
|
||||
} = this.props;
|
||||
|
||||
if (this.props.scrollTop != null) {
|
||||
this._scroller.scrollTop = scrollTop;
|
||||
}
|
||||
|
||||
if (autoFocus && scrollDirection !== scrollDirections.NONE) {
|
||||
this._scroller.focus({ preventScroll: true });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
@@ -36,8 +30,6 @@ class Scroller extends Component {
|
||||
|
||||
_setScrollerRef = (ref) => {
|
||||
this._scroller = ref;
|
||||
|
||||
this.props.registerScroller(ref);
|
||||
}
|
||||
|
||||
//
|
||||
@@ -51,7 +43,6 @@ class Scroller extends Component {
|
||||
children,
|
||||
scrollTop,
|
||||
onScroll,
|
||||
registerScroller,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -64,7 +55,6 @@ class Scroller extends Component {
|
||||
styles[scrollDirection],
|
||||
autoScroll && styles.autoScroll
|
||||
)}
|
||||
tabIndex={-1}
|
||||
{...otherProps}
|
||||
>
|
||||
{children}
|
||||
@@ -77,19 +67,15 @@ class Scroller extends Component {
|
||||
Scroller.propTypes = {
|
||||
className: PropTypes.string,
|
||||
scrollDirection: PropTypes.oneOf(scrollDirections.all).isRequired,
|
||||
autoFocus: PropTypes.bool.isRequired,
|
||||
autoScroll: PropTypes.bool.isRequired,
|
||||
scrollTop: PropTypes.number,
|
||||
children: PropTypes.node,
|
||||
onScroll: PropTypes.func,
|
||||
registerScroller: PropTypes.func
|
||||
onScroll: PropTypes.func
|
||||
};
|
||||
|
||||
Scroller.defaultProps = {
|
||||
scrollDirection: scrollDirections.VERTICAL,
|
||||
autoFocus: true,
|
||||
autoScroll: true,
|
||||
registerScroller: () => {}
|
||||
autoScroll: true
|
||||
};
|
||||
|
||||
export default Scroller;
|
||||
|
||||
@@ -262,7 +262,7 @@ class SignalRConnector extends Component {
|
||||
}
|
||||
|
||||
handleSystemTask = () => {
|
||||
this.props.dispatchFetchCommands();
|
||||
// No-op for now, we may want this later
|
||||
}
|
||||
|
||||
handleRootfolder = () => {
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
.tableContainer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tableBodyContainer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import { WindowScroller } from 'react-virtualized';
|
||||
import { isLocked } from 'Utilities/scrollLock';
|
||||
import { scrollDirections } from 'Helpers/Props';
|
||||
import Measure from 'Components/Measure';
|
||||
import Scroller from 'Components/Scroller/Scroller';
|
||||
import { WindowScroller, Grid } from 'react-virtualized';
|
||||
import hasDifferentItemsOrOrder from 'Utilities/Object/hasDifferentItemsOrOrder';
|
||||
import VirtualTableBody from './VirtualTableBody';
|
||||
import styles from './VirtualTable.css';
|
||||
|
||||
const ROW_HEIGHT = 38;
|
||||
@@ -42,37 +44,28 @@ class VirtualTable extends Component {
|
||||
width: 0
|
||||
};
|
||||
|
||||
this._grid = null;
|
||||
this._isInitialized = false;
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
const {
|
||||
items,
|
||||
scrollIndex
|
||||
} = this.props;
|
||||
componentDidMount() {
|
||||
this._contentBodyNode = ReactDOM.findDOMNode(this.props.contentBody);
|
||||
}
|
||||
|
||||
const {
|
||||
width
|
||||
} = this.state;
|
||||
|
||||
if (this._grid && (prevState.width !== width || hasDifferentItemsOrOrder(prevProps.items, items))) {
|
||||
// recomputeGridSize also forces Grid to discard its cache of rendered cells
|
||||
this._grid.recomputeGridSize();
|
||||
}
|
||||
componentDidUpdate(prevProps, preState) {
|
||||
const { scrollIndex, rowHeight } = this.props;
|
||||
|
||||
if (scrollIndex != null && scrollIndex !== prevProps.scrollIndex) {
|
||||
this._grid.scrollToCell({
|
||||
rowIndex: scrollIndex,
|
||||
columnIndex: 0
|
||||
});
|
||||
const scrollTop = (scrollIndex + 1) * rowHeight + 20;
|
||||
|
||||
this.props.onScroll({ scrollTop });
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Control
|
||||
|
||||
setGridRef = (ref) => {
|
||||
this._grid = ref;
|
||||
rowGetter = ({ index }) => {
|
||||
return this.props.items[index];
|
||||
}
|
||||
|
||||
//
|
||||
@@ -84,18 +77,36 @@ class VirtualTable extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
onSectionRendered = () => {
|
||||
if (!this._isInitialized && this._contentBodyNode) {
|
||||
this.props.onRender();
|
||||
this._isInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
onScroll = (props) => {
|
||||
if (isLocked()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { onScroll } = this.props;
|
||||
|
||||
onScroll(props);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
isSmallScreen,
|
||||
className,
|
||||
items,
|
||||
scroller,
|
||||
isSmallScreen,
|
||||
header,
|
||||
headerHeight,
|
||||
scrollTop,
|
||||
rowRenderer,
|
||||
onScroll,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
@@ -103,89 +114,66 @@ class VirtualTable extends Component {
|
||||
width
|
||||
} = this.state;
|
||||
|
||||
const gridStyle = {
|
||||
boxSizing: undefined,
|
||||
direction: undefined,
|
||||
height: undefined,
|
||||
position: undefined,
|
||||
willChange: undefined,
|
||||
overflow: undefined,
|
||||
width: undefined
|
||||
};
|
||||
|
||||
const containerStyle = {
|
||||
position: undefined
|
||||
};
|
||||
|
||||
return (
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : scroller}
|
||||
>
|
||||
{({ height, registerChild, onChildScroll, scrollTop }) => {
|
||||
if (!height) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Measure
|
||||
whitelist={['width']}
|
||||
onMeasure={this.onMeasure}
|
||||
>
|
||||
<Measure onMeasure={this.onMeasure}>
|
||||
<WindowScroller
|
||||
scrollElement={isSmallScreen ? undefined : this._contentBodyNode}
|
||||
onScroll={this.onScroll}
|
||||
>
|
||||
{({ height, isScrolling }) => {
|
||||
return (
|
||||
<Scroller
|
||||
className={className}
|
||||
scrollDirection={scrollDirections.HORIZONTAL}
|
||||
>
|
||||
{header}
|
||||
<div ref={registerChild}>
|
||||
<Grid
|
||||
ref={this.setGridRef}
|
||||
autoContainerWidth={true}
|
||||
autoHeight={true}
|
||||
autoWidth={true}
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
columnWidth={width}
|
||||
scrollTop={scrollTop}
|
||||
onScroll={onChildScroll}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={rowRenderer}
|
||||
overscanIndicesGetter={overscanIndicesGetter}
|
||||
scrollToAlignment={'start'}
|
||||
isScrollingOptout={true}
|
||||
className={styles.tableBodyContainer}
|
||||
style={gridStyle}
|
||||
containerStyle={containerStyle}
|
||||
{...otherProps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<VirtualTableBody
|
||||
autoContainerWidth={true}
|
||||
width={width}
|
||||
height={height}
|
||||
headerHeight={height - headerHeight}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowCount={items.length}
|
||||
columnCount={1}
|
||||
scrollTop={scrollTop}
|
||||
autoHeight={true}
|
||||
overscanRowCount={2}
|
||||
cellRenderer={rowRenderer}
|
||||
columnWidth={width}
|
||||
overscanIndicesGetter={overscanIndicesGetter}
|
||||
onSectionRendered={this.onSectionRendered}
|
||||
{...otherProps}
|
||||
/>
|
||||
</Scroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
);
|
||||
}
|
||||
}
|
||||
</WindowScroller>
|
||||
</Measure>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTable.propTypes = {
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
className: PropTypes.string.isRequired,
|
||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
scrollTop: PropTypes.number.isRequired,
|
||||
scrollIndex: PropTypes.number,
|
||||
scroller: PropTypes.instanceOf(Element).isRequired,
|
||||
contentBody: PropTypes.object.isRequired,
|
||||
isSmallScreen: PropTypes.bool.isRequired,
|
||||
header: PropTypes.node.isRequired,
|
||||
headerHeight: PropTypes.number.isRequired,
|
||||
rowRenderer: PropTypes.func.isRequired,
|
||||
rowHeight: PropTypes.number.isRequired
|
||||
rowHeight: PropTypes.number.isRequired,
|
||||
onRender: PropTypes.func.isRequired,
|
||||
onScroll: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
VirtualTable.defaultProps = {
|
||||
className: styles.tableContainer,
|
||||
headerHeight: 38
|
||||
headerHeight: 38,
|
||||
onRender: () => {}
|
||||
};
|
||||
|
||||
export default VirtualTable;
|
||||
|
||||
3
frontend/src/Components/Table/VirtualTableBody.css
Normal file
3
frontend/src/Components/Table/VirtualTableBody.css
Normal file
@@ -0,0 +1,3 @@
|
||||
.tableBodyContainer {
|
||||
position: relative;
|
||||
}
|
||||
40
frontend/src/Components/Table/VirtualTableBody.js
Normal file
40
frontend/src/Components/Table/VirtualTableBody.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { Grid } from 'react-virtualized';
|
||||
import styles from './VirtualTableBody.css';
|
||||
|
||||
class VirtualTableBody extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Grid
|
||||
{...this.props}
|
||||
style={{
|
||||
boxSizing: undefined,
|
||||
direction: undefined,
|
||||
height: undefined,
|
||||
position: undefined,
|
||||
willChange: undefined,
|
||||
overflow: undefined,
|
||||
width: undefined
|
||||
}}
|
||||
containerStyle={{
|
||||
position: undefined
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
VirtualTableBody.propTypes = {
|
||||
className: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
VirtualTableBody.defaultProps = {
|
||||
className: styles.tableBodyContainer
|
||||
};
|
||||
|
||||
export default VirtualTableBody;
|
||||
@@ -54,9 +54,9 @@ class Tooltip extends Component {
|
||||
} else if ((/^bottom/).test(data.placement)) {
|
||||
data.styles.maxHeight = windowHeight - bottom - 20;
|
||||
} else if ((/^right/).test(data.placement)) {
|
||||
data.styles.maxWidth = windowWidth - right - 35;
|
||||
data.styles.maxWidth = windowWidth - right - 30;
|
||||
} else {
|
||||
data.styles.maxWidth = left - 35;
|
||||
data.styles.maxWidth = left - 30;
|
||||
}
|
||||
|
||||
return data;
|
||||
|
||||
@@ -8,16 +8,6 @@ export const shortcuts = {
|
||||
name: 'Open This Modal'
|
||||
},
|
||||
|
||||
CLOSE_MODAL: {
|
||||
key: 'Esc',
|
||||
name: 'Close Current Modal'
|
||||
},
|
||||
|
||||
ACCEPT_CONFIRM_MODAL: {
|
||||
key: 'Enter',
|
||||
name: 'Accept Confirmation Modal'
|
||||
},
|
||||
|
||||
SERIES_SEARCH_INPUT: {
|
||||
key: 's',
|
||||
name: 'Focus Search Box'
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,44 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
import getPathWithUrlBase from 'Utilities/getPathWithUrlBase';
|
||||
import Switch from 'Components/Router/Switch';
|
||||
import StatusConnector from './Status/StatusConnector';
|
||||
import ScriptConnector from './Script/ScriptConnector';
|
||||
|
||||
class Diagnostic extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Switch>
|
||||
<Route
|
||||
exact={true}
|
||||
path="/diag/status"
|
||||
component={StatusConnector}
|
||||
/>
|
||||
<Route
|
||||
exact={true}
|
||||
path="/diag/script"
|
||||
component={ScriptConnector}
|
||||
/>
|
||||
|
||||
{/* Redirect root to status */}
|
||||
<Route
|
||||
exact={true}
|
||||
path="/diag"
|
||||
render={() => {
|
||||
return (
|
||||
<Redirect
|
||||
to={getPathWithUrlBase('/diag/status')}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Switch>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Diagnostic;
|
||||
@@ -1,82 +0,0 @@
|
||||
import ReactMonacoEditor from 'react-monaco-editor';
|
||||
import shallowEqual from 'shallowequal';
|
||||
|
||||
// All editor features -> 7.56 MiB
|
||||
// import 'monaco-editor/esm/vs/editor/editor.all';
|
||||
|
||||
// Only the needed editor features -> 6.88 MiB
|
||||
import 'monaco-editor/esm/vs/editor/browser/controller/coreCommands';
|
||||
import 'monaco-editor/esm/vs/editor/browser/widget/codeEditorWidget';
|
||||
// import 'monaco-editor/esm/vs/editor/browser/widget/diffEditorWidget';
|
||||
// import 'monaco-editor/esm/vs/editor/browser/widget/diffNavigator';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/bracketMatching/bracketMatching';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/caretOperations';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/caretOperations/transpose';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/clipboard/clipboard';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/codeAction/codeActionContributions';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/codelens/codelensController';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/colorPicker/colorDetector';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/comment/comment';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/contextmenu/contextmenu';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/cursorUndo/cursorUndo';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/dnd/dnd';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/find/findController';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/folding/folding';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/fontZoom/fontZoom';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/format/formatActions';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionCommands';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/goToDefinition/goToDefinitionMouse';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/gotoError/gotoError';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/hover/hover';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/inPlaceReplace/inPlaceReplace';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/linesOperations/linesOperations';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/links/links';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/multicursor/multicursor';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/parameterHints/parameterHints';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/referenceSearch/referenceSearch';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/rename/rename';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/smartSelect/smartSelect';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/snippet/snippetController2';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/suggest/suggestController';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/tokenization/tokenization';
|
||||
// import 'monaco-editor/esm/vs/editor/contrib/toggleTabFocusMode/toggleTabFocusMode';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/wordHighlighter/wordHighlighter';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/wordOperations/wordOperations';
|
||||
import 'monaco-editor/esm/vs/editor/contrib/wordPartOperations/wordPartOperations';
|
||||
|
||||
// csharp&json language
|
||||
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp';
|
||||
import 'monaco-editor/esm/vs/basic-languages/csharp/csharp.contribution';
|
||||
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
|
||||
import 'monaco-editor/esm/vs/language/json/jsonWorker';
|
||||
import 'monaco-editor/esm/vs/language/json/jsonMode';
|
||||
|
||||
// Create a WebWorker from a blob rather than an url
|
||||
import * as EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker';
|
||||
import * as JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker';
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker: (moduleId, label) => {
|
||||
if (label === 'editorWorkerService') {
|
||||
return new EditorWorker();
|
||||
}
|
||||
if (label === 'json') {
|
||||
return new JsonWorker();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
class MonacoEditor extends ReactMonacoEditor {
|
||||
|
||||
// ReactMonacoEditor should've been PureComponent
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
if (!shallowEqual(this.props, nextProps)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default MonacoEditor;
|
||||
@@ -1,93 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus, updateScript, validateScript, executeScript } from 'Store/Actions/diagnosticActions';
|
||||
import ScriptConsole from './ScriptConsole';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import Alert from 'Components/Alert';
|
||||
import { kinds } from 'Helpers/Props';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.diagnostic,
|
||||
(diag) => {
|
||||
return {
|
||||
isStatusPopulated: diag.status.isPopulated,
|
||||
isScriptConsoleEnabled: diag.status.item.scriptConsoleEnabled,
|
||||
isExecuting: diag.script.isExecuting || false,
|
||||
isDebugging: diag.script.isDebugging || false,
|
||||
isValidating: diag.script.isValidating,
|
||||
code: diag.script.code,
|
||||
result: diag.script.result,
|
||||
validation: diag.script.validation,
|
||||
error: diag.script.error
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchStatus,
|
||||
updateScript,
|
||||
validateScript,
|
||||
executeScript
|
||||
};
|
||||
|
||||
class ScriptConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
if (!this.props.isStatusPopulated) {
|
||||
this.props.fetchStatus();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
if (!this.props.isStatusPopulated) {
|
||||
return (
|
||||
<PageContent>
|
||||
<LoadingIndicator />
|
||||
</PageContent>
|
||||
);
|
||||
} else if (!this.props.isScriptConsoleEnabled) {
|
||||
return (
|
||||
<PageContent>
|
||||
<Alert kind={kinds.WARNING}>
|
||||
Diagnostic Scripting is disabled
|
||||
</Alert>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ScriptConsole
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScriptConnector.propTypes = {
|
||||
isStatusPopulated: PropTypes.bool.isRequired,
|
||||
isScriptConsoleEnabled: PropTypes.bool,
|
||||
isExecuting: PropTypes.bool.isRequired,
|
||||
isDebugging: PropTypes.bool.isRequired,
|
||||
isValidating: PropTypes.bool.isRequired,
|
||||
code: PropTypes.string,
|
||||
result: PropTypes.object,
|
||||
error: PropTypes.object,
|
||||
validation: PropTypes.object,
|
||||
fetchStatus: PropTypes.func.isRequired,
|
||||
updateScript: PropTypes.func.isRequired,
|
||||
validateScript: PropTypes.func.isRequired,
|
||||
executeScript: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(ScriptConnector);
|
||||
@@ -1,6 +0,0 @@
|
||||
.split {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component, lazy, Suspense } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||
import PageContent from 'Components/Page/PageContent';
|
||||
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
|
||||
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import styles from './ScriptConsole.css';
|
||||
|
||||
// Lazy load the Monaco Editor since it's a big bundle
|
||||
const MonacoEditor = lazy(() => import(/* webpackChunkName: "monaco-editor" */ './MonacoEditor'));
|
||||
|
||||
const DefaultOptions = {
|
||||
selectOnLineNumbers: true,
|
||||
scrollBeyondLastLine: false
|
||||
};
|
||||
const DefaultResultOptions = {
|
||||
...DefaultOptions,
|
||||
readOnly: true
|
||||
};
|
||||
|
||||
class ScriptConsole extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
editorDidMount = (editor, monaco) => {
|
||||
console.log('editorDidMount', editor);
|
||||
editor.focus();
|
||||
this.monaco = monaco;
|
||||
this.editor = editor;
|
||||
|
||||
this.updateValidation(this.props.validation);
|
||||
}
|
||||
|
||||
updateValidation(validation) {
|
||||
if (!this.monaco) {
|
||||
return;
|
||||
}
|
||||
|
||||
let diagnostics = [];
|
||||
|
||||
if (validation && validation.errorDiagnostics) {
|
||||
diagnostics = validation.errorDiagnostics;
|
||||
}
|
||||
|
||||
const model = this.editor.getModel();
|
||||
|
||||
this.monaco.editor.setModelMarkers(model, 'editor', diagnostics);
|
||||
}
|
||||
|
||||
onChange = (newValue, e) => {
|
||||
this.props.updateScript({ code: newValue });
|
||||
|
||||
this.validateCode();
|
||||
}
|
||||
|
||||
validateCode = _.debounce(() => {
|
||||
const code = this.props.code;
|
||||
this.props.validateScript({ code });
|
||||
}, 250, { leading: false, trailing: true })
|
||||
|
||||
onExecuteScriptPress = () => {
|
||||
const code = this.props.code;
|
||||
this.props.executeScript({ code });
|
||||
}
|
||||
|
||||
onDebugScriptPress = () => {
|
||||
const code = this.props.code;
|
||||
this.props.executeScript({ code, debug: true });
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
render() {
|
||||
const code = this.props.code;
|
||||
const result = JSON.stringify(this.props.result, null, 2);
|
||||
|
||||
this.updateValidation(this.props.validation);
|
||||
|
||||
return (
|
||||
<PageContent>
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Run"
|
||||
iconName={this.props.isExecuting ? icons.REFRESH : icons.SCRIPT_RUN}
|
||||
isSpinning={this.props.isExecuting}
|
||||
onPress={this.onExecuteScriptPress}
|
||||
/>
|
||||
<PageToolbarButton
|
||||
label="Debug"
|
||||
iconName={this.props.isDebugging ? icons.REFRESH : icons.SCRIPT_DEBUG}
|
||||
isSpinning={this.props.isDebugging}
|
||||
onPress={this.onDebugScriptPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<Suspense fallback={<LoadingIndicator />}>
|
||||
<div className={styles.split}>
|
||||
<MonacoEditor
|
||||
language="csharp"
|
||||
theme="vs-light"
|
||||
width="50%"
|
||||
value={code}
|
||||
options={DefaultOptions}
|
||||
onChange={this.onChange}
|
||||
editorDidMount={this.editorDidMount}
|
||||
/>
|
||||
<MonacoEditor
|
||||
language="json"
|
||||
theme="vs-light"
|
||||
width="50%"
|
||||
value={result}
|
||||
options={DefaultResultOptions}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ScriptConsole.propTypes = {
|
||||
isExecuting: PropTypes.bool.isRequired,
|
||||
isDebugging: PropTypes.bool.isRequired,
|
||||
isValidating: PropTypes.bool.isRequired,
|
||||
code: PropTypes.string,
|
||||
result: PropTypes.object,
|
||||
error: PropTypes.object,
|
||||
validation: PropTypes.object,
|
||||
updateScript: PropTypes.func.isRequired,
|
||||
validateScript: PropTypes.func.isRequired,
|
||||
executeScript: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ScriptConsole;
|
||||
@@ -1,5 +0,0 @@
|
||||
.descriptionList {
|
||||
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
|
||||
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import FieldSet from 'Components/FieldSet';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import styles from './Statistics.css';
|
||||
import formatBytes from 'Utilities/Number/formatBytes';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
import moment from 'moment';
|
||||
|
||||
function formatValue(val, formatter) {
|
||||
if (val === undefined) {
|
||||
return 'n/a';
|
||||
}
|
||||
|
||||
if (formatter) {
|
||||
return formatter(val);
|
||||
}
|
||||
|
||||
return val;
|
||||
}
|
||||
|
||||
class Statistics extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
process,
|
||||
databaseMain,
|
||||
databaseLog,
|
||||
commandsExecuted
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<FieldSet legend="Statistics">
|
||||
<DescriptionList className={styles.descriptionList}>
|
||||
<DescriptionListItem
|
||||
title="Up Time"
|
||||
data={formatValue(process.startTime, (startTime) => formatTimeSpan(moment().diff(startTime)))}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Processor Time"
|
||||
data={formatValue(process.totalProcessorTime, formatTimeSpan)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Memory Working Set"
|
||||
data={formatValue(process.workingSet, formatBytes)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Memory Virtual Size"
|
||||
data={formatValue(process.virtualMemorySize, formatBytes)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Main Database Size"
|
||||
data={formatValue(databaseMain.size, formatBytes)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Logs Database Size"
|
||||
data={formatValue(databaseLog.size, formatBytes)}
|
||||
/>
|
||||
|
||||
<DescriptionListItem
|
||||
title="Commands Executed"
|
||||
data={formatValue(commandsExecuted, formatBytes)}
|
||||
/>
|
||||
</DescriptionList>
|
||||
</FieldSet>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Statistics.propTypes = {
|
||||
process: PropTypes.object,
|
||||
databaseMain: PropTypes.object,
|
||||
databaseLog: PropTypes.object,
|
||||
commandsExecuted: PropTypes.number
|
||||
};
|
||||
|
||||
Statistics.defaultProps = {
|
||||
process: {},
|
||||
databaseMain: {},
|
||||
databaseLog: {}
|
||||
};
|
||||
|
||||
export default Statistics;
|
||||
@@ -1,46 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { icons } from 'Helpers/Props';
|
||||
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 PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
|
||||
import Statistics from './Statistics/Statistics';
|
||||
|
||||
class Status extends Component {
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<PageContent title="Diagnostic Status">
|
||||
<PageToolbar>
|
||||
<PageToolbarSection>
|
||||
<PageToolbarButton
|
||||
label="Refresh"
|
||||
iconName={icons.REFRESH}
|
||||
isSpinning={this.props.isStatusFetching}
|
||||
onPress={this.props.onRefreshPress}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
</PageToolbar>
|
||||
<PageContentBody>
|
||||
<Statistics
|
||||
{...this.props.status}
|
||||
/>
|
||||
</PageContentBody>
|
||||
</PageContent>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Status.propTypes = {
|
||||
status: PropTypes.object.isRequired,
|
||||
isStatusFetching: PropTypes.bool.isRequired,
|
||||
onRefreshPress: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Status;
|
||||
@@ -1,59 +0,0 @@
|
||||
// @ts-check
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { fetchStatus } from 'Store/Actions/diagnosticActions';
|
||||
import Status from './Status';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.diagnostic.status,
|
||||
(status) => {
|
||||
return {
|
||||
isStatusFetching: status.isFetching,
|
||||
status: status.item
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
fetchStatus
|
||||
};
|
||||
|
||||
class DiagnosticConnector extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentDidMount() {
|
||||
this.props.fetchStatus();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onRefreshPress = () => {
|
||||
this.props.fetchStatus();
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Status
|
||||
onRefreshPress={this.onRefreshPress}
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DiagnosticConnector.propTypes = {
|
||||
status: PropTypes.object.isRequired,
|
||||
fetchStatus: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(DiagnosticConnector);
|
||||
@@ -35,10 +35,6 @@ function EpisodeQuality(props) {
|
||||
isCutoffNotMet
|
||||
} = props;
|
||||
|
||||
if (!quality) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Label
|
||||
className={className}
|
||||
|
||||
@@ -27,7 +27,7 @@ function EpisodeStatus(props) {
|
||||
size
|
||||
} = queueItem;
|
||||
|
||||
const progress = size ? (100 - sizeleft / size * 100) : 0;
|
||||
const progress = (100 - sizeleft / size * 100);
|
||||
|
||||
return (
|
||||
<div className={styles.center}>
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||
import padNumber from 'Utilities/Number/padNumber';
|
||||
import styles from './SceneInfo.css';
|
||||
|
||||
function SceneInfo(props) {
|
||||
@@ -61,10 +60,6 @@ function SceneInfo(props) {
|
||||
key={alternateTitle.title}
|
||||
>
|
||||
{alternateTitle.title}
|
||||
{
|
||||
alternateTitle.sceneSeasonNumber !== -1 &&
|
||||
<span> (S{padNumber(alternateTitle.sceneSeasonNumber, 2)})</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -24,7 +24,6 @@ import {
|
||||
import {
|
||||
faArrowCircleLeft as fasArrowCircleLeft,
|
||||
faArrowCircleRight as fasArrowCircleRight,
|
||||
faAsterisk as fasAsterisk,
|
||||
faBackward as fasBackward,
|
||||
faBars as fasBars,
|
||||
faBolt as fasBolt,
|
||||
@@ -81,7 +80,6 @@ import {
|
||||
faSignOutAlt as fasSignOutAlt,
|
||||
faSitemap as fasSitemap,
|
||||
faSpinner as fasSpinner,
|
||||
faStepForward as fasStepForward,
|
||||
faSort as fasSort,
|
||||
faSortDown as fasSortDown,
|
||||
faSortUp as fasSortUp,
|
||||
@@ -127,7 +125,6 @@ export const CLONE = farClone;
|
||||
export const COLLAPSE = fasChevronCircleUp;
|
||||
export const COMPUTER = fasDesktop;
|
||||
export const DANGER = fasExclamationCircle;
|
||||
export const DEBUG = fasBug;
|
||||
export const DELETE = fasTrashAlt;
|
||||
export const DOWNLOAD = fasDownload;
|
||||
export const DOWNLOADED = fasDownload;
|
||||
@@ -141,7 +138,6 @@ export const EXTERNAL_LINK = fasExternalLinkAlt;
|
||||
export const FATAL = fasTimesCircle;
|
||||
export const FILE = farFile;
|
||||
export const FILTER = fasFilter;
|
||||
export const FOOTNOTE = fasAsterisk;
|
||||
export const FOLDER = farFolder;
|
||||
export const FOLDER_OPEN = fasFolderOpen;
|
||||
export const GROUP = farObjectGroup;
|
||||
@@ -149,7 +145,6 @@ export const HEALTH = fasMedkit;
|
||||
export const HEART = fasHeart;
|
||||
export const HISTORY = fasHistory;
|
||||
export const HOUSEKEEPING = fasHome;
|
||||
export const IGNORE = fasTimesCircle;
|
||||
export const INFO = fasInfoCircle;
|
||||
export const INTERACTIVE = fasUser;
|
||||
export const KEYBOARD = farKeyboard;
|
||||
@@ -182,8 +177,6 @@ export const REORDER = fasBars;
|
||||
export const RSS = fasRss;
|
||||
export const SAVE = fasSave;
|
||||
export const SCHEDULED = farClock;
|
||||
export const SCRIPT_DEBUG = fasStepForward;
|
||||
export const SCRIPT_RUN = fasPlay;
|
||||
export const SCORE = fasUserPlus;
|
||||
export const SEARCH = fasSearch;
|
||||
export const SERIES_CONTINUING = fasPlay;
|
||||
|
||||
@@ -10,7 +10,6 @@ export const PASSWORD = 'password';
|
||||
export const PATH = 'path';
|
||||
export const QUALITY_PROFILE_SELECT = 'qualityProfileSelect';
|
||||
export const LANGUAGE_PROFILE_SELECT = 'languageProfileSelect';
|
||||
export const INDEXER_SELECT = 'indexerSelect';
|
||||
export const ROOT_FOLDER_SELECT = 'rootFolderSelect';
|
||||
export const SELECT = 'select';
|
||||
export const SERIES_TYPE_SELECT = 'seriesTypeSelect';
|
||||
@@ -31,7 +30,6 @@ export const all = [
|
||||
PATH,
|
||||
QUALITY_PROFILE_SELECT,
|
||||
LANGUAGE_PROFILE_SELECT,
|
||||
INDEXER_SELECT,
|
||||
ROOT_FOLDER_SELECT,
|
||||
SELECT,
|
||||
SERIES_TYPE_SELECT,
|
||||
|
||||
@@ -93,7 +93,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
|
||||
>
|
||||
<TableBody>
|
||||
{
|
||||
recentFolders.slice(0).reverse().map((recentFolder) => {
|
||||
recentFolders.map((recentFolder) => {
|
||||
return (
|
||||
<RecentFolderRow
|
||||
key={recentFolder.folder}
|
||||
|
||||
@@ -66,7 +66,6 @@ const columns = [
|
||||
{
|
||||
name: 'size',
|
||||
label: 'Size',
|
||||
isSortable: true,
|
||||
isVisible: true
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,6 @@ class InteractiveImportModal extends Component {
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
closeOnBackgroundClick={false}
|
||||
onModalClose={onModalClose}
|
||||
>
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user