1
0
mirror of https://github.com/Sonarr/Sonarr.git synced 2026-03-07 13:39:58 -05:00

Compare commits

..

13 Commits

Author SHA1 Message Date
Taloth Saldono
473b146ae2 Include ReferenceAssemblies for build agent 2019-12-29 00:43:05 +01:00
Taloth Saldono
bdc0bb4441 Added Lidarr NuGet package url to get the sqlite package 2019-12-29 00:31:51 +01:00
Taloth Saldono
96c5a37ca8 Added netstandard and System.Runtime Facades from mono 5.18 2019-12-29 00:31:51 +01:00
Taloth Saldono
ce0b7e1077 Increased mono dependency from 5.4 to 5.18 for debian 2019-12-29 00:31:51 +01:00
Taloth Saldono
29cde083f9 Added .NET Framework 4.7.2 requirement check to windows installer 2019-12-28 18:01:46 +01:00
Taloth Saldono
8faebc01ee Updated build scripts and added support for Visual Studio 2019 2019-12-28 18:01:46 +01:00
Taloth Saldono
2ea154b863 Updated Sqlite to x64 and upgraded to 1.0.111 2019-12-28 18:01:46 +01:00
Taloth Saldono
d02dfc9ff1 Updated MediaInfo to x64 2019-12-25 11:45:29 +01:00
Taloth Saldono
7bcfa3d7b2 Updated Interop.NetFwTypeLib to x64 2019-12-25 11:44:44 +01:00
Taloth Saldono
1c1f9cddff Switched projects to x64 2019-12-25 00:40:18 +01:00
Taloth Saldono
8308d65375 Use Newtonsoft in TinyTwitter 2019-12-24 17:37:04 +01:00
Taloth Saldono
d64d59ff27 Moved Windows-only Permission function to Sonarr.Windows 2019-12-24 17:18:07 +01:00
Taloth Saldono
8da6f7d7f4 Removed unused dialects from Marr so it compiles with less dependencies. 2019-12-24 13:47:04 +01:00
607 changed files with 3605 additions and 24565 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,8 @@ gulp.task('build',
'webpack',
'copyHtml',
'copyFonts',
'copyImages'
'copyImages',
'copyJs'
)
)
);

View File

@@ -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'))

View File

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

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "es6",
"checkJs": false,
"baseUrl": "src",
"jsx": "react",
"module": "commonjs",
"moduleResolution": "node",
"paths": {
"*": [
"*"
]
}
},
"include": [
"./src/**/*"
],
"exclude": [
]
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,4 @@
.messageRemove {
margin-bottom: 30px;
color: $dangerColor;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,6 @@
.alreadyExistsIcon {
margin-left: 10px;
color: #37bc9b;
pointer-events: all;
}
.overview {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
*/}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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
)}
>
{

View File

@@ -8,16 +8,7 @@
}
}
.inputWrapper {
flex: 1 0 0;
}
.buttonWrapper {
flex: 0 0 22px;
}
.keyInput,
.valueInput {
width: 100%;
border: none;
}

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

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

View File

@@ -25,7 +25,3 @@
.warning {
color: $warningColor;
}
.purple {
color: $purple;
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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) {

View File

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

View File

@@ -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 = () => {

View File

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

View File

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

View 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);

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
.jumpBarItem {
flex: 1 1 $jumpBarItemHeight;
flex: 1 0 $jumpBarItemHeight;
border-bottom: 1px solid $borderColor;
text-align: center;
font-weight: bold;

View File

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

View File

@@ -41,7 +41,6 @@ function PageToolbarButton(props) {
}
PageToolbarButton.propTypes = {
...Link.propTypes,
label: PropTypes.string.isRequired,
iconName: PropTypes.object.isRequired,
spinningName: PropTypes.object,

View File

@@ -1,6 +1,6 @@
.sectionContainer {
display: flex;
flex: 1 1 auto;
flex: 1 1 10%;
overflow: hidden;
}

View File

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

View File

@@ -2,10 +2,6 @@
/* Placeholder */
}
.track {
/* Placeholder */
}
.thumb {
min-height: 100px;
border: 1px solid transparent;

View File

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

View File

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

View File

@@ -262,7 +262,7 @@ class SignalRConnector extends Component {
}
handleSystemTask = () => {
this.props.dispatchFetchCommands();
// No-op for now, we may want this later
}
handleRootfolder = () => {

View File

@@ -1,7 +1,3 @@
.tableContainer {
width: 100%;
}
.tableBodyContainer {
position: relative;
}

View File

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

View File

@@ -0,0 +1,3 @@
.tableBodyContainer {
position: relative;
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +0,0 @@
.split {
display: flex;
justify-content: space-between;
overflow: hidden;
height: 100%;
}

View File

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

View File

@@ -1,5 +0,0 @@
.descriptionList {
composes: descriptionList from '~Components/DescriptionList/DescriptionList.css';
margin-bottom: 10px;
}

View File

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

View File

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

View File

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

View File

@@ -35,10 +35,6 @@ function EpisodeQuality(props) {
isCutoffNotMet
} = props;
if (!quality) {
return null;
}
return (
<Label
className={className}

View File

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

View File

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

View File

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

View File

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

View File

@@ -93,7 +93,7 @@ class InteractiveImportSelectFolderModalContent extends Component {
>
<TableBody>
{
recentFolders.slice(0).reverse().map((recentFolder) => {
recentFolders.map((recentFolder) => {
return (
<RecentFolderRow
key={recentFolder.folder}

View File

@@ -66,7 +66,6 @@ const columns = [
{
name: 'size',
label: 'Size',
isSortable: true,
isVisible: true
},
{

View File

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