Compare commits

...

60 Commits

Author SHA1 Message Date
Bogdan
9f4c9d3344 Show successful grabs in Search with green icon 2023-08-12 12:12:51 +03:00
Bogdan
dfb00d9bb1 Fixed: Ensure grab notifications are sent according to tags requirements 2023-08-12 12:07:17 +03:00
Bogdan
f7727855b5 Rework adding one minute back-off level for all providers
(cherry picked from commit d8f314ff0ef64e8d90b21b7865e46be74db5e570)
2023-08-12 09:32:07 +03:00
Stepan Goremykin
1e4c67dcdb Update FluentAssertions
(cherry picked from commit 951a9ade00d7c9105f03608cb598450d706b826f)
2023-08-12 09:28:42 +03:00
Robin Dadswell
26afcb0071 Fixed: PostgreSQL timezone issues
(cherry picked from commit d55864f86914199aa0c4ee37df1e42e6ad71ef4f)
2023-08-10 23:03:27 +01:00
Bogdan
7a937e85a4 Fixed: Retain user settings not-affiliated with Prowlarr 2023-08-10 18:05:00 +03:00
Bogdan
7cd82321b4 Bump Npgsql version to 6.0.9
Fixes #1819
2023-08-09 23:41:09 +03:00
Bogdan
8c9adba516 Fixed color for links 2023-08-09 19:30:25 +03:00
Bogdan
03fa9254e3 Prevent NullRef in IsPathValid for null paths 2023-08-09 13:35:13 +03:00
Weblate
e66ecf5c95 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: byakurau <byakurau1@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-08-08 18:49:41 +03:00
Bogdan
e0dddfa215 Remove Order and Help columns from Apps Fields 2023-08-08 18:30:25 +03:00
Bogdan
bcb8afadf8 New: Add Content Summary for requests to apps 2023-08-08 18:26:09 +03:00
Bogdan
fc4a0979c3 Fixed: Detect Docker when using control group v2 2023-08-07 19:15:21 +03:00
Bogdan
5f643b2ced Fixed: (Indexers) Don't fetch releases when using unsupported capabilities 2023-08-06 20:30:59 +03:00
Bogdan
6f09b0f4f5 Bump version to 1.8.2 2023-08-06 08:42:24 +03:00
Qstick
95c2531107 Filter user issues from Sentry
(cherry picked from commit 03d361f5537bfc0caba1b86085f974570942fdbc)
2023-08-05 21:38:44 +03:00
Bogdan
f83828cc22 Fixed border for actions in health status 2023-08-05 17:56:13 +03:00
Bogdan
cdea548ce2 New: Add internal links for apps and download clients health checks 2023-08-05 14:26:53 +03:00
Bogdan
cae1da0ce2 Fixed: (Apps) Lower the severity for testing messages 2023-08-05 14:12:06 +03:00
Bogdan
765f354c51 New: Add test all action for apps and download clients to status health 2023-08-05 14:11:50 +03:00
Bogdan
5cbbffb018 Fix translation typo in sync level options 2023-08-05 14:11:50 +03:00
Bogdan
b2c5448cbf Fixed: Run health checks for applications and download clients on bulk events 2023-08-05 11:40:15 +03:00
Bogdan
3dae84705c Fixed: Ensure failing providers are marked as failed when testing all 2023-08-05 10:02:55 +03:00
Weblate
2321d278d6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Albert <zuozl1992@foxmail.com>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Ivan Mazzoli <dreadtank27@gmail.com>
Co-authored-by: Magnus <magnus.fladvad@gmail.com>
Co-authored-by: Stjepan <stjepstjepanovic@gmail.com>
Co-authored-by: Thirrian <matthiaslantermann@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: stormaac <yxc.frank@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/hr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/it/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nb_NO/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/nl/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-08-04 21:07:31 +03:00
TwentyNine78
ea73466f6a Fixed: Compatibility with the new Download Station API
(cherry picked from commit 49e90463e57929e7b9885f1b7b0eb05bd7cc3ebe)
2023-08-04 21:06:45 +03:00
Bogdan
6961c5a1c6 Fixed: (AlphaRatio) Use FL tokens only if canUseToken is true
Fixes #1811
2023-08-04 17:20:28 +03:00
Mark McDowall
141f1597dc New: Ignore inaccessible files with getting files
(cherry picked from commit e5aa8584100d96a2077c57f74ae5b2ceab63de19)
2023-08-04 13:17:37 +03:00
Bogdan
1100f350ae Fix translations for option values 2023-08-04 07:05:31 +03:00
Bogdan
3c5eefc349 New: Health check for indexers with invalid download client
(cherry picked from commit 377fce6fe15c0875c4bd33f1371a31af79c9310c)
2023-08-04 06:50:48 +03:00
Mark McDowall
0bfb557470 Prevent NullRef in ContainsInvalidPathChars
(cherry picked from commit 5f7217844533907d7fc6287a48efb31987736c4c)
2023-08-04 06:43:40 +03:00
Servarr
c93d6cff63 Automated API Docs update [skip ci] 2023-08-03 17:40:31 +03:00
Bogdan
7e4980b855 New: Add translations for columns 2023-08-03 17:20:36 +03:00
Bogdan
419ef4b3bf New: More translations for columns
(cherry picked from commit aee8579d1823b7dfb94c0055fe33b5fb5a7fbf17)
2023-08-03 16:10:13 +03:00
Mark McDowall
c56d49ab60 Fixed: Translations for columns
(cherry picked from commit 6d53d2a153a98070c42d0619c15902b6bd5dfab4)
2023-08-03 16:07:00 +03:00
Mark McDowall
1a40924db3 Fixed: Improve translation loading
(cherry picked from commit 73c5ec1da4dd00301e1b0dddbcea37590a99b045)
2023-08-03 16:05:39 +03:00
Mark McDowall
d55906d49a UI loading improvements
Fixed: Caching for dynamically loaded JS files
Fixed: Incorrect caching of initialize.js
(cherry picked from commit f0cb5b81f140c67fa84162e094cc4e0f3476f5da)
2023-08-03 15:57:52 +03:00
Bogdan
bc53fab966 Fixed: Don't fetch capabilities for disabled Newznab/Torznab indexers on create
Also prevent NullRef in GetProxy since definition is null when using FetchCapabilities on add
2023-08-02 14:54:01 +03:00
Bogdan
d897b50f80 New: (UI) Show Magnet Link in search results if any 2023-08-01 13:29:12 +03:00
Bogdan
cc66cee71c Fixed: (Apps) Avoid force saving remote indexers when it's not necessary 2023-07-31 10:33:57 +03:00
Bogdan
f5e96f3f51 Ensure yarn packages are installed when running only LintUI 2023-07-31 08:18:45 +03:00
Mark McDowall
d52e1259a1 Re-order frontend build steps
(cherry picked from commit 97ad6682f7d54af8886144bc5a179fa7242f1f1f)
2023-07-31 07:56:20 +03:00
Bogdan
72e6d66269 New: (Apps) Add force sync indexers for applications 2023-07-31 07:22:08 +03:00
Bogdan
e51b85449d Convert store selectors to Typescript 2023-07-30 21:06:44 +03:00
Bogdan
efd5e92ca5 Support categories with Transmission 2023-07-30 12:38:33 +03:00
Bogdan
d153746a98 Bump version to 1.8.1 2023-07-30 10:45:08 +03:00
Bogdan
a1927e1e0f Sort indexers by name in search footer dropdown 2023-07-29 13:48:10 +03:00
Bogdan
630a4ce800 Fixed: Ensure failing indexers are marked as failed when testing all
(cherry picked from commit b407eba61284d5fb855df6a2868805853aa6f448)
2023-07-29 12:14:58 +03:00
Bogdan
8b1dd78300 Fixed: (Apps) Ensure populated capabilities for Torznab/Newznab definitions 2023-07-29 12:08:48 +03:00
Bogdan
cab50b35aa Convert some selectors to Typescript 2023-07-29 03:14:47 +03:00
Weblate
eee1be784b Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Fixer <ygj59783@zslsz.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-28 12:55:09 +03:00
Bogdan
269dc5688b New: (IPTorrents) Add new base url 2023-07-27 12:18:49 +03:00
Weblate
9bed795c89 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translation: Servarr/Prowlarr
2023-07-27 12:14:26 +03:00
Bogdan
3b5f151252 New: Set default names for providers in Add Modals 2023-07-27 02:58:07 +03:00
Weblate
b3a541c9ff Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: Dortlix <jeremy.boely@hotmail.fr>
Co-authored-by: Fixer <ygj59783@zslsz.com>
Co-authored-by: Havok Dan <havokdan@yahoo.com.br>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: aguillement <adrien.guillement@gmail.com>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/fr/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/pt_BR/
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/ro/
Translation: Servarr/Prowlarr
2023-07-26 07:18:25 +03:00
Bogdan
bc90fa2d3f Add unit to history cleanup days option 2023-07-26 07:17:06 +03:00
Bogdan
4b0a896434 Fixed: (Cardigann) Improvements to automatic logins with captcha 2023-07-26 05:35:42 +03:00
Bogdan
6be0e08635 Convert Delete Indexer to Typescript 2023-07-25 05:50:45 +03:00
Bogdan
f618901048 Convert Indexer Stats to Typescript 2023-07-25 05:50:45 +03:00
Weblate
809ed940e6 Multiple Translations updated by Weblate
ignore-downstream

Co-authored-by: SHUAI.W <x@ousui.org>
Translate-URL: https://translate.servarr.com/projects/servarr/prowlarr/zh_CN/
Translation: Servarr/Prowlarr
2023-07-25 01:05:11 +03:00
Qstick
7b14c2ee66 Bump version to 1.8.0 2023-07-23 23:25:30 -05:00
254 changed files with 2727 additions and 1698 deletions

View File

@@ -9,7 +9,7 @@ variables:
testsFolder: './_tests'
yarnCacheFolder: $(Pipeline.Workspace)/.yarn
nugetCacheFolder: $(Pipeline.Workspace)/.nuget/packages
majorVersion: '1.7.4'
majorVersion: '1.8.2'
minorVersion: $[counter('minorVersion', 1)]
prowlarrVersion: '$(majorVersion).$(minorVersion)'
buildName: '$(Build.SourceBranchName).$(prowlarrVersion)'
@@ -565,6 +565,7 @@ stages:
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |
@@ -710,6 +711,7 @@ stages:
-e POSTGRES_PASSWORD=prowlarr \
-e POSTGRES_USER=prowlarr \
-p 5432:5432/tcp \
-v /usr/share/zoneinfo/America/Chicago:/etc/localtime:ro \
postgres:14
displayName: Start postgres
- bash: |

View File

@@ -392,22 +392,21 @@ then
fi
fi
if [ "$FRONTEND" = "YES" ];
if [[ "$LINT" = "YES" || "$FRONTEND" = "YES" ]];
then
YarnInstall
RunWebpack
fi
if [ "$LINT" = "YES" ];
then
if [ -z "$FRONTEND" ];
then
YarnInstall
fi
LintUI
fi
if [ "$FRONTEND" = "YES" ];
then
RunWebpack
fi
if [ "$PACKAGES" = "YES" ];
then
UpdateVersionNumber

View File

@@ -26,7 +26,8 @@ module.exports = {
globals: {
expect: false,
chai: false,
sinon: false
sinon: false,
JSX: true
},
parserOptions: {

View File

@@ -35,7 +35,7 @@ module.exports = (env) => {
},
entry: {
index: 'index.js'
index: 'index.ts'
},
resolve: {
@@ -96,7 +96,8 @@ module.exports = (env) => {
new HtmlWebpackPlugin({
template: 'frontend/src/index.ejs',
filename: 'index.html',
publicPath: '/'
publicPath: '/',
inject: false
}),
new FileManagerPlugin({

View File

@@ -7,13 +7,13 @@ import PageConnector from 'Components/Page/PageConnector';
import ApplyTheme from './ApplyTheme';
import AppRoutes from './AppRoutes';
function App({ store, history, hasTranslationsError }) {
function App({ store, history }) {
return (
<DocumentTitle title={window.Prowlarr.instanceName}>
<Provider store={store}>
<ConnectedRouter history={history}>
<ApplyTheme>
<PageConnector hasTranslationsError={hasTranslationsError}>
<PageConnector>
<AppRoutes app={App} />
</PageConnector>
</ApplyTheme>
@@ -25,8 +25,7 @@ function App({ store, history, hasTranslationsError }) {
App.propTypes = {
store: PropTypes.object.isRequired,
history: PropTypes.object.isRequired,
hasTranslationsError: PropTypes.bool.isRequired
history: PropTypes.object.isRequired
};
export default App;

View File

@@ -5,9 +5,9 @@ import NotFound from 'Components/NotFound';
import Switch from 'Components/Router/Switch';
import HistoryConnector from 'History/HistoryConnector';
import IndexerIndex from 'Indexer/Index/IndexerIndex';
import StatsConnector from 'Indexer/Stats/StatsConnector';
import IndexerStats from 'Indexer/Stats/IndexerStats';
import SearchIndexConnector from 'Search/SearchIndexConnector';
import ApplicationSettingsConnector from 'Settings/Applications/ApplicationSettingsConnector';
import ApplicationSettings from 'Settings/Applications/ApplicationSettings';
import DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
import DownloadClientSettingsConnector from 'Settings/DownloadClients/DownloadClientSettingsConnector';
import GeneralSettingsConnector from 'Settings/General/GeneralSettingsConnector';
@@ -60,7 +60,7 @@ function AppRoutes(props) {
<Route
path="/indexers/stats"
component={StatsConnector}
component={IndexerStats}
/>
{/*
@@ -98,7 +98,7 @@ function AppRoutes(props) {
<Route
path="/settings/applications"
component={ApplicationSettingsConnector}
component={ApplicationSettings}
/>
<Route

View File

@@ -1,5 +1,11 @@
import IndexerAppState, { IndexerIndexAppState } from './IndexerAppState';
import CommandAppState from './CommandAppState';
import IndexerAppState, {
IndexerIndexAppState,
IndexerStatusAppState,
} from './IndexerAppState';
import IndexerStatsAppState from './IndexerStatsAppState';
import SettingsAppState from './SettingsAppState';
import SystemAppState from './SystemAppState';
import TagsAppState from './TagsAppState';
interface FilterBuilderPropOption {
@@ -35,9 +41,13 @@ export interface CustomFilter {
}
interface AppState {
commands: CommandAppState;
indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexerStatus: IndexerStatusAppState;
indexers: IndexerAppState;
settings: SettingsAppState;
system: SystemAppState;
tags: TagsAppState;
}

View File

@@ -0,0 +1,6 @@
import AppSectionState from 'App/State/AppSectionState';
import Command from 'Commands/Command';
export type CommandAppState = AppSectionState<Command>;
export default CommandAppState;

View File

@@ -1,6 +1,6 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer from 'Indexer/Indexer';
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
@@ -28,6 +28,10 @@ export interface IndexerIndexAppState {
interface IndexerAppState
extends AppSectionState<Indexer>,
AppSectionDeleteState,
AppSectionSaveState {}
AppSectionSaveState {
itemMap: Record<number, number>;
}
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
export default IndexerAppState;

View File

@@ -0,0 +1,11 @@
import { AppSectionItemState } from 'App/State/AppSectionState';
import { Filter } from 'App/State/AppState';
import { IndexerStats } from 'typings/IndexerStats';
export interface IndexerStatsAppState
extends AppSectionItemState<IndexerStats> {
selectedFilterKey: string;
filters: Filter[];
}
export default IndexerStatsAppState;

View File

@@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionDeleteState,
} from 'App/State/AppSectionState';
import Release from 'typings/Release';
interface ReleaseAppState
extends AppSectionState<Release>,
AppSectionDeleteState {}
export default ReleaseAppState;

View File

@@ -1,5 +1,6 @@
import AppSectionState, {
AppSectionDeleteState,
AppSectionItemState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
import Application from 'typings/Application';
@@ -7,11 +8,18 @@ import DownloadClient from 'typings/DownloadClient';
import Notification from 'typings/Notification';
import { UiSettings } from 'typings/UiSettings';
export interface ApplicationAppState
export interface AppProfileAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
AppSectionSaveState {}
export interface ApplicationAppState
extends AppSectionState<Application>,
AppSectionDeleteState,
AppSectionSaveState {
isTestingAll: boolean;
}
export interface DownloadClientAppState
extends AppSectionState<DownloadClient>,
AppSectionDeleteState,
@@ -21,13 +29,14 @@ export interface NotificationAppState
extends AppSectionState<Notification>,
AppSectionDeleteState {}
export type UiSettingsAppState = AppSectionState<UiSettings>;
export type UiSettingsAppState = AppSectionItemState<UiSettings>;
interface SettingsAppState {
appProfiles: AppProfileAppState;
applications: ApplicationAppState;
downloadClients: DownloadClientAppState;
notifications: NotificationAppState;
uiSettings: UiSettingsAppState;
ui: UiSettingsAppState;
}
export default SettingsAppState;

View File

@@ -0,0 +1,10 @@
import SystemStatus from 'typings/SystemStatus';
import { AppSectionItemState } from './AppSectionState';
export type SystemStatusAppState = AppSectionItemState<SystemStatus>;
interface SystemAppState {
status: SystemStatusAppState;
}
export default SystemAppState;

View File

@@ -1,12 +1,28 @@
import ModelBase from 'App/ModelBase';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
} from 'App/State/AppSectionState';
export interface Tag extends ModelBase {
label: string;
}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {}
export interface TagDetail extends ModelBase {
label: string;
applicationIds: number[];
indexerIds: number[];
indexerProxyIds: number[];
notificationIds: number[];
}
export interface TagDetailAppState
extends AppSectionState<TagDetail>,
AppSectionDeleteState,
AppSectionSaveState {}
interface TagsAppState extends AppSectionState<Tag>, AppSectionDeleteState {
details: TagDetailAppState;
}
export default TagsAppState;

View File

@@ -0,0 +1,37 @@
import ModelBase from 'App/ModelBase';
export interface CommandBody {
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
completionMessage: string;
requiresDiskAccess: boolean;
isExclusive: boolean;
isLongRunning: boolean;
name: string;
lastExecutionTime: string;
lastStartTime: string;
trigger: string;
suppressMessages: boolean;
seriesId?: number;
}
interface Command extends ModelBase {
name: string;
commandName: string;
message: string;
body: CommandBody;
priority: string;
status: string;
result: string;
queued: string;
started: string;
ended: string;
duration: string;
trigger: string;
stateChangeTime: string;
sendUpdatesToClient: boolean;
updateScheduledTask: boolean;
lastExecutionTime: string;
}
export default Command;

View File

@@ -20,12 +20,12 @@ import styles from './FileBrowserModalContent.css';
const columns = [
{
name: 'type',
label: translate('Type'),
label: () => translate('Type'),
isVisible: true
},
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isVisible: true
}
];

View File

@@ -198,9 +198,11 @@ class FilterBuilderRow extends Component {
const selectedFilterBuilderProp = this.selectedFilterBuilderProp;
const keyOptions = filterBuilderProps.map((availablePropFilter) => {
const { name, label } = availablePropFilter;
return {
key: availablePropFilter.name,
value: availablePropFilter.label
key: name,
value: typeof label === 'function' ? label() : label
};
}).sort((a, b) => a.value.localeCompare(b.value));

View File

@@ -270,6 +270,7 @@ FormInputGroup.propTypes = {
helpTexts: PropTypes.arrayOf(PropTypes.string),
helpTextWarning: PropTypes.string,
helpLink: PropTypes.string,
autoFocus: PropTypes.bool,
includeNoChange: PropTypes.bool,
includeNoChangeDisabled: PropTypes.bool,
selectedValueOptions: PropTypes.object,

View File

@@ -33,7 +33,7 @@ function HintedSelectInputOption(props) {
isMobile && styles.isMobile
)}
>
<div>{value}</div>
<div>{typeof value === 'function' ? value() : value}</div>
{
hint != null &&
@@ -48,7 +48,7 @@ function HintedSelectInputOption(props) {
HintedSelectInputOption.propTypes = {
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
hint: PropTypes.node,
depth: PropTypes.number,
isSelected: PropTypes.bool.isRequired,

View File

@@ -3,13 +3,15 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createSortedSectionSelector from 'Store/Selectors/createSortedSectionSelector';
import sortByName from 'Utilities/Array/sortByName';
import titleCase from 'Utilities/String/titleCase';
import EnhancedSelectInput from './EnhancedSelectInput';
function createMapStateToProps() {
return createSelector(
(state, { value }) => value,
(state) => state.indexers,
createSortedSectionSelector('indexers', sortByName),
(value, indexers) => {
const values = [];
const groupedIndexers = map(groupBy(indexers.items, 'protocol'), (val, key) => ({ protocol: key, indexers: val }));

View File

@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
}
PathInputConnector.propTypes = {
...PathInput.props,
includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired

View File

@@ -61,7 +61,7 @@ class SelectInput extends Component {
value={key}
{...otherOptionProps}
>
{optionValue}
{typeof optionValue === 'function' ? optionValue() : optionValue}
</option>
);
})
@@ -75,7 +75,7 @@ SelectInput.propTypes = {
className: PropTypes.string,
disabledClassName: PropTypes.string,
name: PropTypes.string.isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.func]).isRequired,
values: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool,
hasError: PropTypes.bool,

View File

@@ -41,7 +41,7 @@ class Icon extends PureComponent {
return (
<span
className={containerClassName}
title={title}
title={typeof title === 'function' ? title() : title}
>
{icon}
</span>
@@ -58,7 +58,7 @@ Icon.propTypes = {
name: PropTypes.object.isRequired,
kind: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,
title: PropTypes.string,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSpinning: PropTypes.bool.isRequired,
fixedWidth: PropTypes.bool.isRequired
};

View File

@@ -33,7 +33,7 @@ class FilterMenuContent extends Component {
selectedFilterKey={selectedFilterKey}
onPress={onFilterSelect}
>
{filter.label}
{typeof filter.label === 'function' ? filter.label() : filter.label}
</FilterMenuItem>
);
})

View File

@@ -7,7 +7,7 @@ function ErrorPage(props) {
const {
version,
isLocalStorageSupported,
hasTranslationsError,
translationsError,
indexersError,
indexerStatusError,
indexerCategoriesError,
@@ -22,8 +22,8 @@ function ErrorPage(props) {
if (!isLocalStorageSupported) {
errorMessage = 'Local Storage is not supported or disabled. A plugin or private browsing may have disabled it.';
} else if (hasTranslationsError) {
errorMessage = 'Failed to load translations from API';
} else if (translationsError) {
errorMessage = getErrorMessage(translationsError, 'Failed to load translations from API');
} else if (indexersError) {
errorMessage = getErrorMessage(indexersError, 'Failed to load indexers from API');
} else if (indexerStatusError) {
@@ -58,7 +58,7 @@ function ErrorPage(props) {
ErrorPage.propTypes = {
version: PropTypes.string.isRequired,
isLocalStorageSupported: PropTypes.bool.isRequired,
hasTranslationsError: PropTypes.bool.isRequired,
translationsError: PropTypes.object,
indexersError: PropTypes.object,
indexerStatusError: PropTypes.object,
indexerCategoriesError: PropTypes.object,

View File

@@ -3,7 +3,7 @@ import React, { Component } from 'react';
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import { createSelector } from 'reselect';
import { saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchTranslations, saveDimensions, setIsSidebarVisible } from 'Store/Actions/appActions';
import { fetchCustomFilters } from 'Store/Actions/customFilterActions';
import { fetchIndexers } from 'Store/Actions/indexerActions';
import { fetchIndexerStatus } from 'Store/Actions/indexerStatusActions';
@@ -54,6 +54,7 @@ const selectIsPopulated = createSelector(
(state) => state.indexerStatus.isPopulated,
(state) => state.settings.indexerCategories.isPopulated,
(state) => state.system.status.isPopulated,
(state) => state.app.translations.isPopulated,
(
customFiltersIsPopulated,
tagsIsPopulated,
@@ -63,7 +64,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated,
indexerStatusIsPopulated,
indexerCategoriesIsPopulated,
systemStatusIsPopulated
systemStatusIsPopulated,
translationsIsPopulated
) => {
return (
customFiltersIsPopulated &&
@@ -74,7 +76,8 @@ const selectIsPopulated = createSelector(
indexersIsPopulated &&
indexerStatusIsPopulated &&
indexerCategoriesIsPopulated &&
systemStatusIsPopulated
systemStatusIsPopulated &&
translationsIsPopulated
);
}
);
@@ -89,6 +92,7 @@ const selectErrors = createSelector(
(state) => state.indexerStatus.error,
(state) => state.settings.indexerCategories.error,
(state) => state.system.status.error,
(state) => state.app.translations.error,
(
customFiltersError,
tagsError,
@@ -98,7 +102,8 @@ const selectErrors = createSelector(
indexersError,
indexerStatusError,
indexerCategoriesError,
systemStatusError
systemStatusError,
translationsError
) => {
const hasError = !!(
customFiltersError ||
@@ -109,7 +114,8 @@ const selectErrors = createSelector(
indexersError ||
indexerStatusError ||
indexerCategoriesError ||
systemStatusError
systemStatusError ||
translationsError
);
return {
@@ -122,7 +128,8 @@ const selectErrors = createSelector(
indexersError,
indexerStatusError,
indexerCategoriesError,
systemStatusError
systemStatusError,
translationsError
};
}
);
@@ -184,6 +191,9 @@ function createMapDispatchToProps(dispatch, props) {
dispatchFetchStatus() {
dispatch(fetchStatus());
},
dispatchFetchTranslations() {
dispatch(fetchTranslations());
},
onResize(dimensions) {
dispatch(saveDimensions(dimensions));
},
@@ -217,6 +227,7 @@ class PageConnector extends Component {
this.props.dispatchFetchUISettings();
this.props.dispatchFetchGeneralSettings();
this.props.dispatchFetchStatus();
this.props.dispatchFetchTranslations();
}
}
@@ -232,7 +243,6 @@ class PageConnector extends Component {
render() {
const {
hasTranslationsError,
isPopulated,
hasError,
dispatchFetchTags,
@@ -243,15 +253,15 @@ class PageConnector extends Component {
dispatchFetchUISettings,
dispatchFetchGeneralSettings,
dispatchFetchStatus,
dispatchFetchTranslations,
...otherProps
} = this.props;
if (hasTranslationsError || hasError || !this.state.isLocalStorageSupported) {
if (hasError || !this.state.isLocalStorageSupported) {
return (
<ErrorPage
{...this.state}
{...otherProps}
hasTranslationsError={hasTranslationsError}
/>
);
}
@@ -272,7 +282,6 @@ class PageConnector extends Component {
}
PageConnector.propTypes = {
hasTranslationsError: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
hasError: PropTypes.bool.isRequired,
isSidebarVisible: PropTypes.bool.isRequired,
@@ -285,6 +294,7 @@ PageConnector.propTypes = {
dispatchFetchUISettings: PropTypes.func.isRequired,
dispatchFetchGeneralSettings: PropTypes.func.isRequired,
dispatchFetchStatus: PropTypes.func.isRequired,
dispatchFetchTranslations: PropTypes.func.isRequired,
onSidebarVisibleChange: PropTypes.func.isRequired
};

View File

@@ -20,12 +20,12 @@ const SIDEBAR_WIDTH = parseInt(dimensions.sidebarWidth);
const links = [
{
iconName: icons.MOVIE_CONTINUING,
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/',
alias: '/indexers',
children: [
{
title: translate('Stats'),
title: () => translate('Stats'),
to: '/indexers/stats'
}
]
@@ -33,47 +33,47 @@ const links = [
{
iconName: icons.SEARCH,
title: translate('Search'),
title: () => translate('Search'),
to: '/search'
},
{
iconName: icons.ACTIVITY,
title: translate('History'),
title: () => translate('History'),
to: '/history'
},
{
iconName: icons.SETTINGS,
title: translate('Settings'),
title: () => translate('Settings'),
to: '/settings',
children: [
{
title: translate('Indexers'),
title: () => translate('Indexers'),
to: '/settings/indexers'
},
{
title: translate('Apps'),
title: () => translate('Apps'),
to: '/settings/applications'
},
{
title: translate('DownloadClients'),
title: () => translate('DownloadClients'),
to: '/settings/downloadclients'
},
{
title: translate('Connect'),
title: () => translate('Connect'),
to: '/settings/connect'
},
{
title: translate('Tags'),
title: () => translate('Tags'),
to: '/settings/tags'
},
{
title: translate('General'),
title: () => translate('General'),
to: '/settings/general'
},
{
title: translate('UI'),
title: () => translate('UI'),
to: '/settings/ui'
}
]
@@ -81,32 +81,32 @@ const links = [
{
iconName: icons.SYSTEM,
title: translate('System'),
title: () => translate('System'),
to: '/system/status',
children: [
{
title: translate('Status'),
title: () => translate('Status'),
to: '/system/status',
statusComponent: HealthStatusConnector
},
{
title: translate('Tasks'),
title: () => translate('Tasks'),
to: '/system/tasks'
},
{
title: translate('Backup'),
title: () => translate('Backup'),
to: '/system/backup'
},
{
title: translate('Updates'),
title: () => translate('Updates'),
to: '/system/updates'
},
{
title: translate('Events'),
title: () => translate('Events'),
to: '/system/events'
},
{
title: translate('LogFiles'),
title: () => translate('LogFiles'),
to: '/system/logs/files'
}
]

View File

@@ -64,7 +64,7 @@ class PageSidebarItem extends Component {
}
<span className={isChildItem ? styles.noIcon : null}>
{title}
{typeof title === 'function' ? title() : title}
</span>
{
@@ -88,7 +88,7 @@ class PageSidebarItem extends Component {
PageSidebarItem.propTypes = {
iconName: PropTypes.object,
title: PropTypes.string.isRequired,
title: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
to: PropTypes.string.isRequired,
isActive: PropTypes.bool,
isActiveParent: PropTypes.bool,

View File

@@ -0,0 +1,7 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'cell': string;
}
export const cssExports: CssExports;
export default cssExports;

View File

@@ -1,25 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Link from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
function TableRowCellButton({ className, ...otherProps }) {
return (
<Link
className={className}
component={TableRowCell}
{...otherProps}
/>
);
}
TableRowCellButton.propTypes = {
className: PropTypes.string.isRequired
};
TableRowCellButton.defaultProps = {
className: styles.cell
};
export default TableRowCellButton;

View File

@@ -0,0 +1,19 @@
import React, { ReactNode } from 'react';
import Link, { LinkProps } from 'Components/Link/Link';
import TableRowCell from './TableRowCell';
import styles from './TableRowCellButton.css';
interface TableRowCellButtonProps extends LinkProps {
className?: string;
children: ReactNode;
}
function TableRowCellButton(props: TableRowCellButtonProps) {
const { className = styles.cell, ...otherProps } = props;
return (
<Link className={className} component={TableRowCell} {...otherProps} />
);
}
export default TableRowCellButton;

View File

@@ -1,8 +1,10 @@
import React from 'react';
type PropertyFunction<T> = () => T;
interface Column {
name: string;
label: string | React.ReactNode;
label: string | PropertyFunction<string> | React.ReactNode;
columnLabel?: string;
isSortable?: boolean;
isVisible: boolean;

View File

@@ -107,7 +107,7 @@ function Table(props) {
{...getTableHeaderCellProps(otherProps)}
{...column}
>
{column.label}
{typeof column.label === 'function' ? column.label() : column.label}
</TableHeaderCell>
);
})

View File

@@ -30,6 +30,7 @@ class TableHeaderCell extends Component {
const {
className,
name,
label,
columnLabel,
isSortable,
isVisible,
@@ -53,7 +54,8 @@ class TableHeaderCell extends Component {
{...otherProps}
component="th"
className={className}
title={columnLabel}
label={typeof label === 'function' ? label() : label}
title={typeof columnLabel === 'function' ? columnLabel() : columnLabel}
onPress={this.onPress}
>
{children}
@@ -77,7 +79,8 @@ class TableHeaderCell extends Component {
TableHeaderCell.propTypes = {
className: PropTypes.string,
name: PropTypes.string.isRequired,
columnLabel: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func, PropTypes.node]),
columnLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
isSortable: PropTypes.bool,
isVisible: PropTypes.bool,
isModifiable: PropTypes.bool,

View File

@@ -35,7 +35,7 @@ function TableOptionsColumn(props) {
isDisabled={isModifiable === false}
onChange={onVisibleChange}
/>
{label}
{typeof label === 'function' ? label() : label}
</label>
{
@@ -56,7 +56,7 @@ function TableOptionsColumn(props) {
TableOptionsColumn.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,

View File

@@ -112,7 +112,7 @@ class TableOptionsColumnDragSource extends Component {
<TableOptionsColumn
name={name}
label={label}
label={typeof label === 'function' ? label() : label}
isVisible={isVisible}
isModifiable={isModifiable}
index={index}
@@ -138,7 +138,7 @@ class TableOptionsColumnDragSource extends Component {
TableOptionsColumnDragSource.propTypes = {
name: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired,
isVisible: PropTypes.bool.isRequired,
isModifiable: PropTypes.bool.isRequired,
index: PropTypes.number.isRequired,

View File

@@ -75,6 +75,7 @@ import {
faListCheck as fasListCheck,
faLocationArrow as fasLocationArrow,
faLock as fasLock,
faMagnet as fasMagnet,
faMedkit as fasMedkit,
faMinus as fasMinus,
faMusic as fasMusic,
@@ -181,6 +182,7 @@ export const INTERACTIVE = fasUser;
export const KEYBOARD = farKeyboard;
export const LOCK = fasLock;
export const LOGOUT = fasSignOutAlt;
export const MAGNET = fasMagnet;
export const MANAGE = fasListCheck;
export const MEDIA_INFO = farFileInvoice;
export const MISSING = fasExclamationTriangle;

View File

@@ -62,6 +62,7 @@ class HistoryOptions extends Component {
<FormInputGroup
type={inputTypes.NUMBER}
name="historyCleanupDays"
unit={translate('days')}
value={historyCleanupDays}
helpText={translate('HistoryCleanupDaysHelpText')}
helpTextWarning={translate('HistoryCleanupDaysHelpTextWarning')}

View File

@@ -22,31 +22,31 @@ import styles from './AddIndexerModalContent.css';
const columns = [
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
isSortable: true,
isVisible: true
},
{
name: 'sortName',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true
},
{
name: 'language',
label: translate('Language'),
label: () => translate('Language'),
isSortable: true,
isVisible: true
},
{
name: 'description',
label: translate('Description'),
label: () => translate('Description'),
isSortable: false,
isVisible: true
},
{
name: 'privacy',
label: translate('Privacy'),
label: () => translate('Privacy'),
isSortable: true,
isVisible: true
}
@@ -66,15 +66,21 @@ const protocols = [
const privacyLevels = [
{
key: 'private',
value: translate('Private')
get value() {
return translate('Private');
}
},
{
key: 'semiPrivate',
value: translate('SemiPrivate')
get value() {
return translate('SemiPrivate');
}
},
{
key: 'public',
value: translate('Public')
get value() {
return translate('Public');
}
}
];

View File

@@ -1,34 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContentConnector from './DeleteIndexerModalContentConnector';
function DeleteIndexerModal(props) {
const {
isOpen,
onModalClose,
...otherProps
} = props;
return (
<Modal
isOpen={isOpen}
size={sizes.MEDIUM}
onModalClose={onModalClose}
>
<DeleteIndexerModalContentConnector
{...otherProps}
onModalClose={onModalClose}
/>
</Modal>
);
}
DeleteIndexerModal.propTypes = {
...DeleteIndexerModalContentConnector.propTypes,
isOpen: PropTypes.bool.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModal;

View File

@@ -0,0 +1,25 @@
import React from 'react';
import Modal from 'Components/Modal/Modal';
import { sizes } from 'Helpers/Props';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
interface DeleteIndexerModalProps {
isOpen: boolean;
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModal(props: DeleteIndexerModalProps) {
const { isOpen, indexerId, onModalClose } = props;
return (
<Modal isOpen={isOpen} size={sizes.MEDIUM} onModalClose={onModalClose}>
<DeleteIndexerModalContent
indexerId={indexerId}
onModalClose={onModalClose}
/>
</Modal>
);
}
export default DeleteIndexerModal;

View File

@@ -1,88 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
class DeleteIndexerModalContent extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
deleteFiles: false,
addImportExclusion: false
};
}
//
// Listeners
onDeleteFilesChange = ({ value }) => {
this.setState({ deleteFiles: value });
};
onAddImportExclusionChange = ({ value }) => {
this.setState({ addImportExclusion: value });
};
onDeleteMovieConfirmed = () => {
const deleteFiles = this.state.deleteFiles;
const addImportExclusion = this.state.addImportExclusion;
this.setState({ deleteFiles: false, addImportExclusion: false });
this.props.onDeletePress(deleteFiles, addImportExclusion);
};
//
// Render
render() {
const {
name,
onModalClose
} = this.props;
return (
<ModalContent
onModalClose={onModalClose}
>
<ModalHeader>
Delete - {name}
</ModalHeader>
<ModalBody>
{`Are you sure you want to delete ${name} from Prowlarr`}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>
{translate('Close')}
</Button>
<Button
kind={kinds.DANGER}
onPress={this.onDeleteMovieConfirmed}
>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
}
DeleteIndexerModalContent.propTypes = {
name: PropTypes.string.isRequired,
onDeletePress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};
export default DeleteIndexerModalContent;

View File

@@ -0,0 +1,54 @@
import React, { useCallback } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Button from 'Components/Link/Button';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import Indexer from 'Indexer/Indexer';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
interface DeleteIndexerModalContentProps {
indexerId: number;
onModalClose(): void;
}
function DeleteIndexerModalContent(props: DeleteIndexerModalContentProps) {
const { indexerId, onModalClose } = props;
const { name } = useSelector(
createIndexerSelectorForHook(indexerId)
) as Indexer;
const dispatch = useDispatch();
const onConfirmDelete = useCallback(() => {
dispatch(deleteIndexer({ id: indexerId }));
onModalClose();
}, [indexerId, dispatch, onModalClose]);
return (
<ModalContent onModalClose={onModalClose}>
<ModalHeader>
{translate('Delete')} - {name}
</ModalHeader>
<ModalBody>
{translate('AreYouSureYouWantToDeleteIndexer', [name])}
</ModalBody>
<ModalFooter>
<Button onPress={onModalClose}>{translate('Close')}</Button>
<Button kind={kinds.DANGER} onPress={onConfirmDelete}>
{translate('Delete')}
</Button>
</ModalFooter>
</ModalContent>
);
}
export default DeleteIndexerModalContent;

View File

@@ -1,57 +0,0 @@
import { push } from 'connected-react-router';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { deleteIndexer } from 'Store/Actions/indexerActions';
import createIndexerSelector from 'Store/Selectors/createIndexerSelector';
import DeleteIndexerModalContent from './DeleteIndexerModalContent';
function createMapStateToProps() {
return createSelector(
createIndexerSelector(),
(indexer) => {
return indexer;
}
);
}
const mapDispatchToProps = {
deleteIndexer,
push
};
class DeleteIndexerModalContentConnector extends Component {
//
// Listeners
onDeletePress = () => {
this.props.deleteIndexer({
id: this.props.indexerId
});
this.props.onModalClose(true);
};
//
// Render
render() {
return (
<DeleteIndexerModalContent
{...this.props}
onDeletePress={this.onDeletePress}
/>
);
}
}
DeleteIndexerModalContentConnector.propTypes = {
indexerId: PropTypes.number.isRequired,
onModalClose: PropTypes.func.isRequired,
deleteIndexer: PropTypes.func.isRequired,
push: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, mapDispatchToProps)(DeleteIndexerModalContentConnector);

View File

@@ -45,9 +45,7 @@ import IndexerIndexTable from './Table/IndexerIndexTable';
import IndexerIndexTableOptions from './Table/IndexerIndexTableOptions';
import styles from './IndexerIndex.css';
function getViewComponent() {
return IndexerIndexTable;
}
const getViewComponent = () => IndexerIndexTable;
interface IndexerIndexProps {
initialScrollTop?: number;
@@ -84,14 +82,6 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
);
const [isSelectMode, setIsSelectMode] = useState(false);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
})
);
}, [dispatch]);
const onAddIndexerPress = useCallback(() => {
setIsAddIndexerModalOpen(true);
}, [setIsAddIndexerModalOpen]);
@@ -108,6 +98,15 @@ const IndexerIndex = withScrollPosition((props: IndexerIndexProps) => {
setIsEditIndexerModalOpen(false);
}, [setIsEditIndexerModalOpen]);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
forceSync: true,
})
);
}, [dispatch]);
const onTestAllPress = useCallback(() => {
dispatch(testAllIndexers());
}, [dispatch]);

View File

@@ -30,9 +30,25 @@ interface EditIndexerModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'true', value: translate('Enabled') },
{ key: 'false', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'true',
get value() {
return translate('Enabled');
},
},
{
key: 'false',
get value() {
return translate('Disabled');
},
},
];
function EditIndexerModalContent(props: EditIndexerModalContentProps) {

View File

@@ -12,6 +12,7 @@ import { icons } from 'Helpers/Props';
import DeleteIndexerModal from 'Indexer/Delete/DeleteIndexerModal';
import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import createIndexerIndexItemSelector from 'Indexer/Index/createIndexerIndexItemSelector';
import Indexer from 'Indexer/Indexer';
import IndexerTitleLink from 'Indexer/IndexerTitleLink';
import { SelectStateInputProps } from 'typings/props';
import firstCharToUpper from 'Utilities/String/firstCharToUpper';
@@ -47,7 +48,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
fields,
added,
capabilities,
} = indexer;
} = indexer as Indexer;
const baseUrl =
fields.find((field) => field.name === 'baseUrl')?.value ??

View File

@@ -103,7 +103,7 @@ function IndexerIndexTableHeader(props: IndexerIndexTableHeaderProps) {
isSortable={isSortable}
onSortPress={onSortPress}
>
{label}
{typeof label === 'function' ? label() : label}
</VirtualTableHeaderCell>
);
})}

View File

@@ -12,7 +12,7 @@ interface IndexerStatusCellProps {
className: string;
enabled: boolean;
redirect: boolean;
status: IndexerStatus;
status?: IndexerStatus;
longDateFormat: string;
timeFormat: string;
component?: React.ElementType;

View File

@@ -1,5 +1,4 @@
import { createSelector } from 'reselect';
import Indexer from 'Indexer/Indexer';
import createIndexerAppProfileSelector from 'Store/Selectors/createIndexerAppProfileSelector';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import createIndexerStatusSelector from 'Store/Selectors/createIndexerStatusSelector';
@@ -11,7 +10,7 @@ function createIndexerIndexItemSelector(indexerId: number) {
createIndexerAppProfileSelector(indexerId),
createIndexerStatusSelector(indexerId),
createUISettingsSelector(),
(indexer: Indexer, appProfile, status, uiSettings) => {
(indexer, appProfile, status, uiSettings) => {
return {
indexer,
appProfile,

View File

@@ -25,11 +25,13 @@ export interface IndexerCapabilities extends ModelBase {
}
export interface IndexerField extends ModelBase {
order: number;
name: string;
label: string;
advanced: boolean;
type: string;
value: string;
privacy: string;
}
interface Indexer extends ModelBase {
@@ -40,6 +42,10 @@ interface Indexer extends ModelBase {
added: Date;
enable: boolean;
redirect: boolean;
supportsRss: boolean;
supportsSearch: boolean;
supportsRedirect: boolean;
supportsPagination: boolean;
protocol: string;
privacy: string;
priority: number;
@@ -49,6 +55,12 @@ interface Indexer extends ModelBase {
status: IndexerStatus;
capabilities: IndexerCapabilities;
indexerUrls: string[];
legacyUrls: string[];
appProfileId: number;
implementationName: string;
implementation: string;
configContract: string;
infoLink: string;
}
export default Indexer;

View File

@@ -29,7 +29,7 @@ import styles from './IndexerInfoModalContent.css';
function createIndexerInfoItemSelector(indexerId: number) {
return createSelector(
createIndexerSelectorForHook(indexerId),
(indexer: Indexer) => {
(indexer?: Indexer) => {
return {
indexer,
};
@@ -58,7 +58,7 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
tags,
protocol,
capabilities,
} = indexer;
} = indexer as Indexer;
const { onModalClose } = props;

View File

@@ -1,15 +1,16 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import styles from './NoIndexer.css';
function NoIndexer(props) {
const {
totalItems,
onAddIndexerPress
} = props;
interface NoIndexerProps {
totalItems: number;
onAddIndexerPress(): void;
}
function NoIndexer(props: NoIndexerProps) {
const { totalItems, onAddIndexerPress } = props;
if (totalItems > 0) {
return (
@@ -28,10 +29,7 @@ function NoIndexer(props) {
</div>
<div className={styles.buttonContainer}>
<Button
onPress={onAddIndexerPress}
kind={kinds.PRIMARY}
>
<Button onPress={onAddIndexerPress} kind={kinds.PRIMARY}>
{translate('AddNewIndexer')}
</Button>
</div>
@@ -39,9 +37,4 @@ function NoIndexer(props) {
);
}
NoIndexer.propTypes = {
totalItems: PropTypes.number.isRequired,
onAddIndexerPress: PropTypes.func.isRequired
};
export default NoIndexer;

View File

@@ -0,0 +1,275 @@
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import IndexerStatsAppState from 'App/State/IndexerStatsAppState';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import {
fetchIndexerStats,
setIndexerStatsFilter,
} from 'Store/Actions/indexerStatsActions';
import {
IndexerStatsHost,
IndexerStatsIndexer,
IndexerStatsUserAgent,
} from 'typings/IndexerStats';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import IndexerStatsFilterMenu from './IndexerStatsFilterMenu';
import styles from './IndexerStats.css';
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.averageResponseTime,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getFailureRateData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value:
(indexer.numberOfFailedQueries +
indexer.numberOfFailedRssQueries +
indexer.numberOfFailedAuthQueries +
indexer.numberOfFailedGrabs) /
(indexer.numberOfQueries +
indexer.numberOfRssQueries +
indexer.numberOfAuthQueries +
indexer.numberOfGrabs),
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getTotalRequestsData(indexerStats: IndexerStatsIndexer[]) {
const data = {
labels: indexerStats.map((indexer) => indexer.indexerName),
datasets: [
{
label: translate('SearchQueries'),
data: indexerStats.map((indexer) => indexer.numberOfQueries),
},
{
label: translate('RssQueries'),
data: indexerStats.map((indexer) => indexer.numberOfRssQueries),
},
{
label: translate('AuthQueries'),
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries),
},
],
};
return data;
}
function getNumberGrabsData(indexerStats: IndexerStatsIndexer[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentGrabsData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentQueryData(indexerStats: IndexerStatsUserAgent[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostGrabsData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostQueryData(indexerStats: IndexerStatsHost[]) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries,
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
const indexerStatsSelector = () => {
return createSelector(
(state: AppState) => state.indexerStats,
(indexerStats: IndexerStatsAppState) => {
return indexerStats;
}
);
};
function IndexerStats() {
const { isFetching, isPopulated, item, error, filters, selectedFilterKey } =
useSelector(indexerStatsSelector());
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchIndexerStats());
}, [dispatch]);
const onFilterSelect = useCallback(
(value: string) => {
dispatch(setIndexerStatsFilter({ selectedFilterKey: value }));
},
[dispatch]
);
const isLoaded = !error && isPopulated;
return (
<PageContent>
<PageToolbar>
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
<IndexerStatsFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
onFilterSelect={onFilterSelect}
isDisabled={false}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{isFetching && !isPopulated && <LoadingIndicator />}
{!isFetching && !!error && (
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</Alert>
)}
{isLoaded && (
<div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
/>
</div>
<div className={styles.fullWidthChart}>
<BarChart
data={getFailureRateData(item.indexers)}
title={translate('IndexerFailureRate')}
kind={kinds.WARNING}
/>
</div>
<div className={styles.halfWidthChart}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>
</div>
)}
</PageContentBody>
</PageContent>
);
}
export default IndexerStats;

View File

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

View File

@@ -1,261 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import Alert from 'Components/Alert';
import BarChart from 'Components/Chart/BarChart';
import DoughnutChart from 'Components/Chart/DoughnutChart';
import StackedBarChart from 'Components/Chart/StackedBarChart';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
import PageToolbarSection from 'Components/Page/Toolbar/PageToolbarSection';
import { align, kinds } from 'Helpers/Props';
import getErrorMessage from 'Utilities/Object/getErrorMessage';
import translate from 'Utilities/String/translate';
import StatsFilterMenu from './StatsFilterMenu';
import styles from './Stats.css';
function getAverageResponseTimeData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.averageResponseTime
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getFailureRateData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: (indexer.numberOfFailedQueries + indexer.numberOfFailedRssQueries + indexer.numberOfFailedAuthQueries + indexer.numberOfFailedGrabs) /
(indexer.numberOfQueries + indexer.numberOfRssQueries + indexer.numberOfAuthQueries + indexer.numberOfGrabs)
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getTotalRequestsData(indexerStats) {
const data = {
labels: indexerStats.map((indexer) => indexer.indexerName),
datasets: [
{
label: 'Search Queries',
data: indexerStats.map((indexer) => indexer.numberOfQueries)
},
{
label: 'Rss Queries',
data: indexerStats.map((indexer) => indexer.numberOfRssQueries)
},
{
label: 'Auth Queries',
data: indexerStats.map((indexer) => indexer.numberOfAuthQueries)
}
]
};
return data;
}
function getNumberGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.indexerName,
value: indexer.numberOfGrabs - indexer.numberOfFailedGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getUserAgentQueryData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.userAgent ? indexer.userAgent : 'Other',
value: indexer.numberOfQueries
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostGrabsData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfGrabs
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function getHostQueryData(indexerStats) {
const data = indexerStats.map((indexer) => {
return {
label: indexer.host ? indexer.host : 'Other',
value: indexer.numberOfQueries
};
});
data.sort((a, b) => {
return b.value - a.value;
});
return data;
}
function Stats(props) {
const {
item,
isFetching,
isPopulated,
error,
filters,
selectedFilterKey,
onFilterSelect
} = props;
const isLoaded = !!(!error && isPopulated);
return (
<PageContent>
<PageToolbar>
<PageToolbarSection
alignContent={align.RIGHT}
collapseButtons={false}
>
<StatsFilterMenu
selectedFilterKey={selectedFilterKey}
filters={filters}
onFilterSelect={onFilterSelect}
isDisabled={false}
/>
</PageToolbarSection>
</PageToolbar>
<PageContentBody>
{
isFetching && !isPopulated &&
<LoadingIndicator />
}
{
!isFetching && !!error &&
<Alert kind={kinds.DANGER}>
{getErrorMessage(error, 'Failed to load indexer stats from API')}
</Alert>
}
{
isLoaded &&
<div>
<div className={styles.fullWidthChart}>
<BarChart
data={getAverageResponseTimeData(item.indexers)}
title={translate('AverageResponseTimesMs')}
/>
</div>
<div className={styles.fullWidthChart}>
<BarChart
data={getFailureRateData(item.indexers)}
title={translate('IndexerFailureRate')}
kind={kinds.WARNING}
/>
</div>
<div className={styles.halfWidthChart}>
<StackedBarChart
data={getTotalRequestsData(item.indexers)}
title={translate('TotalIndexerQueries')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getNumberGrabsData(item.indexers)}
title={translate('TotalIndexerSuccessfulGrabs')}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentQueryData(item.userAgents)}
title={translate('TotalUserAgentQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<BarChart
data={getUserAgentGrabsData(item.userAgents)}
title={translate('TotalUserAgentGrabs')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostQueryData(item.hosts)}
title={translate('TotalHostQueries')}
horizontal={true}
/>
</div>
<div className={styles.halfWidthChart}>
<DoughnutChart
data={getHostGrabsData(item.hosts)}
title={translate('TotalHostGrabs')}
horizontal={true}
/>
</div>
</div>
}
</PageContentBody>
</PageContent>
);
}
Stats.propTypes = {
item: PropTypes.object.isRequired,
isFetching: PropTypes.bool.isRequired,
isPopulated: PropTypes.bool.isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
selectedFilterKey: PropTypes.string.isRequired,
onFilterSelect: PropTypes.func.isRequired,
error: PropTypes.object,
data: PropTypes.object
};
export default Stats;

View File

@@ -1,51 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { fetchIndexerStats, setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
import Stats from './Stats';
function createMapStateToProps() {
return createSelector(
(state) => state.indexerStats,
(indexerStats) => indexerStats
);
}
function createMapDispatchToProps(dispatch, props) {
return {
onFilterSelect(selectedFilterKey) {
dispatch(setIndexerStatsFilter({ selectedFilterKey }));
},
dispatchFetchIndexerStats() {
dispatch(fetchIndexerStats());
}
};
}
class StatsConnector extends Component {
//
// Lifecycle
componentDidMount() {
this.props.dispatchFetchIndexerStats();
}
//
// Render
render() {
return (
<Stats
{...this.props}
/>
);
}
}
StatsConnector.propTypes = {
dispatchFetchIndexerStats: PropTypes.func.isRequired
};
export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector);

View File

@@ -1,37 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import FilterMenu from 'Components/Menu/FilterMenu';
import { align } from 'Helpers/Props';
function StatsFilterMenu(props) {
const {
selectedFilterKey,
filters,
isDisabled,
onFilterSelect
} = props;
return (
<FilterMenu
alignMenu={align.RIGHT}
isDisabled={isDisabled}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
onFilterSelect={onFilterSelect}
/>
);
}
StatsFilterMenu.propTypes = {
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
isDisabled: PropTypes.bool.isRequired,
onFilterSelect: PropTypes.func.isRequired
};
StatsFilterMenu.defaultProps = {
showCustomFilters: false
};
export default StatsFilterMenu;

View File

@@ -1,24 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import FilterModal from 'Components/Filter/FilterModal';
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
function createMapStateToProps() {
return createSelector(
(state) => state.indexerStats.items,
(state) => state.indexerStats.filterBuilderProps,
(sectionItems, filterBuilderProps) => {
return {
sectionItems,
filterBuilderProps,
customFilterType: 'indexerStats'
};
}
);
}
const mapDispatchToProps = {
dispatchSetFilter: setIndexerStatsFilter
};
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);

View File

@@ -37,6 +37,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed, grabError) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
@@ -76,6 +88,7 @@ class SearchIndexOverview extends Component {
infoUrl,
protocol,
downloadUrl,
magnetUrl,
categories,
seeders,
leechers,
@@ -114,19 +127,22 @@ class SearchIndexOverview extends Component {
<div className={styles.actions}>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl}
/>
{
downloadUrl || magnetUrl ?
<IconButton
name={icons.SAVE}
title={translate('Save')}
to={downloadUrl ?? magnetUrl}
/> :
null
}
</div>
</div>
<div className={styles.indexerRow}>
@@ -188,7 +204,8 @@ SearchIndexOverview.propTypes = {
publishDate: PropTypes.string.isRequired,
title: PropTypes.string.isRequired,
infoUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string.isRequired,
downloadUrl: PropTypes.string,
magnetUrl: PropTypes.string,
indexerId: PropTypes.number.isRequired,
indexer: PropTypes.string.isRequired,
size: PropTypes.number.isRequired,

View File

@@ -195,7 +195,7 @@ class SearchIndexOverviews extends Component {
SearchIndexOverviews.propTypes = {
items: PropTypes.arrayOf(PropTypes.object).isRequired,
sortKey: PropTypes.string,
scrollTop: PropTypes.number.isRequired,
scrollTop: PropTypes.number,
jumpToCharacter: PropTypes.string,
scroller: PropTypes.instanceOf(Element).isRequired,
showRelativeDates: PropTypes.bool.isRequired,

View File

@@ -14,11 +14,11 @@ import QueryParameterOption from './QueryParameterOption';
import styles from './QueryParameterModal.css';
const searchOptions = [
{ key: 'search', value: translate('BasicSearch') },
{ key: 'tvsearch', value: translate('TvSearch') },
{ key: 'movie', value: translate('MovieSearch') },
{ key: 'music', value: translate( 'AudioSearch') },
{ key: 'book', value: translate('BookSearch') }
{ key: 'search', value: () => translate('BasicSearch') },
{ key: 'tvsearch', value: () => translate('TvSearch') },
{ key: 'movie', value: () => translate('MovieSearch') },
{ key: 'music', value: () => translate( 'AudioSearch') },
{ key: 'book', value: () => translate('BookSearch') }
];
const seriesTokens = [

View File

@@ -96,7 +96,7 @@ class SearchIndexHeader extends Component {
isSortable={isSortable}
{...otherProps}
>
{label}
{typeof label === 'function' ? label() : label}
</VirtualTableHeaderCell>
);
})

View File

@@ -59,6 +59,7 @@
margin: 0 2px;
width: 22px;
color: var(--textColor);
text-align: center;
}
.externalLinks {

View File

@@ -30,6 +30,18 @@ function getDownloadIcon(isGrabbing, isGrabbed, grabError) {
return icons.DOWNLOAD;
}
function getDownloadKind(isGrabbed, grabError) {
if (isGrabbed) {
return kinds.SUCCESS;
}
if (grabError) {
return kinds.DANGER;
}
return kinds.DEFAULT;
}
function getDownloadTooltip(isGrabbing, isGrabbed, grabError) {
if (isGrabbing) {
return '';
@@ -91,6 +103,8 @@ class SearchIndexRow extends Component {
const {
guid,
protocol,
downloadUrl,
magnetUrl,
categories,
age,
ageHours,
@@ -301,19 +315,34 @@ class SearchIndexRow extends Component {
>
<SpinnerIconButton
name={getDownloadIcon(isGrabbing, isGrabbed, grabError)}
kind={grabError ? kinds.DANGER : kinds.DEFAULT}
kind={getDownloadKind(isGrabbed, grabError)}
title={getDownloadTooltip(isGrabbing, isGrabbed, grabError)}
isDisabled={isGrabbed}
isSpinning={isGrabbing}
onPress={this.onGrabPress}
/>
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
onPress={this.onSavePress}
/>
{
downloadUrl ?
<IconButton
className={styles.downloadLink}
name={icons.SAVE}
title={translate('Save')}
onPress={this.onSavePress}
/> :
null
}
{
magnetUrl ?
<IconButton
className={styles.downloadLink}
name={icons.MAGNET}
title={translate('Open')}
to={magnetUrl}
/> :
null
}
</VirtualTableRowCell>
);
}

View File

@@ -1,103 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, Fragment } from 'react';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import AppProfilesConnector from 'Settings/Profiles/App/AppProfilesConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import translate from 'Utilities/String/translate';
import ApplicationsConnector from './Applications/ApplicationsConnector';
import ManageApplicationsModal from './Applications/Manage/ManageApplicationsModal';
class ApplicationSettings extends Component {
//
// Lifecycle
constructor(props, context) {
super(props, context);
this.state = {
isManageApplicationsOpen: false
};
}
//
// Listeners
onManageApplicationsPress = () => {
this.setState({ isManageApplicationsOpen: true });
};
onManageApplicationsModalClose = () => {
this.setState({ isManageApplicationsOpen: false });
};
//
// Render
render() {
const {
isTestingAll,
isSyncingIndexers,
onTestAllPress,
onAppIndexerSyncPress
} = this.props;
const { isManageApplicationsOpen } = this.state;
return (
<PageContent title={translate('Applications')}>
<SettingsToolbarConnector
showSave={false}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
isSpinning={isSyncingIndexers}
onPress={onAppIndexerSyncPress}
/>
<PageToolbarButton
label={translate('TestAllApps')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={onTestAllPress}
/>
<PageToolbarButton
label={translate('ManageApplications')}
iconName={icons.MANAGE}
onPress={this.onManageApplicationsPress}
/>
</Fragment>
}
/>
<PageContentBody>
<ApplicationsConnector />
<AppProfilesConnector />
<ManageApplicationsModal
isOpen={isManageApplicationsOpen}
onModalClose={this.onManageApplicationsModalClose}
/>
</PageContentBody>
</PageContent>
);
}
}
ApplicationSettings.propTypes = {
isTestingAll: PropTypes.bool.isRequired,
isSyncingIndexers: PropTypes.bool.isRequired,
onTestAllPress: PropTypes.func.isRequired,
onAppIndexerSyncPress: PropTypes.func.isRequired
};
export default ApplicationSettings;

View File

@@ -0,0 +1,102 @@
import React, { Fragment, useCallback, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import AppState from 'App/State/AppState';
import { APP_INDEXER_SYNC } from 'Commands/commandNames';
import PageContent from 'Components/Page/PageContent';
import PageContentBody from 'Components/Page/PageContentBody';
import PageToolbarButton from 'Components/Page/Toolbar/PageToolbarButton';
import PageToolbarSeparator from 'Components/Page/Toolbar/PageToolbarSeparator';
import { icons } from 'Helpers/Props';
import AppProfilesConnector from 'Settings/Profiles/App/AppProfilesConnector';
import SettingsToolbarConnector from 'Settings/SettingsToolbarConnector';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllApplications } from 'Store/Actions/Settings/applications';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import ApplicationsConnector from './Applications/ApplicationsConnector';
import ManageApplicationsModal from './Applications/Manage/ManageApplicationsModal';
function ApplicationSettings() {
const isSyncingIndexers = useSelector(
createCommandExecutingSelector(APP_INDEXER_SYNC)
);
const isTestingAll = useSelector(
(state: AppState) => state.settings.applications.isTestingAll
);
const dispatch = useDispatch();
const [isManageApplicationsOpen, setIsManageApplicationsOpen] =
useState(false);
const onManageApplicationsPress = useCallback(() => {
setIsManageApplicationsOpen(true);
}, [setIsManageApplicationsOpen]);
const onManageApplicationsModalClose = useCallback(() => {
setIsManageApplicationsOpen(false);
}, [setIsManageApplicationsOpen]);
const onAppIndexerSyncPress = useCallback(() => {
dispatch(
executeCommand({
name: APP_INDEXER_SYNC,
forceSync: true,
})
);
}, [dispatch]);
const onTestAllPress = useCallback(() => {
dispatch(testAllApplications());
}, [dispatch]);
return (
<PageContent title={translate('Applications')}>
<SettingsToolbarConnector
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
showSave={false}
additionalButtons={
<Fragment>
<PageToolbarSeparator />
<PageToolbarButton
label={translate('SyncAppIndexers')}
iconName={icons.REFRESH}
isSpinning={isSyncingIndexers}
onPress={onAppIndexerSyncPress}
/>
<PageToolbarButton
label={translate('TestAllApps')}
iconName={icons.TEST}
isSpinning={isTestingAll}
onPress={onTestAllPress}
/>
<PageToolbarButton
label={translate('ManageApplications')}
iconName={icons.MANAGE}
onPress={onManageApplicationsPress}
/>
</Fragment>
}
/>
<PageContentBody>
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<ApplicationsConnector />
{/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */}
{/* @ts-ignore */}
<AppProfilesConnector />
<ManageApplicationsModal
isOpen={isManageApplicationsOpen}
onModalClose={onManageApplicationsModalClose}
/>
</PageContentBody>
</PageContent>
);
}
export default ApplicationSettings;

View File

@@ -1,35 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import * as commandNames from 'Commands/commandNames';
import { executeCommand } from 'Store/Actions/commandActions';
import { testAllApplications } from 'Store/Actions/settingsActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import ApplicationSettings from './ApplicationSettings';
function createMapStateToProps() {
return createSelector(
(state) => state.settings.applications.isTestingAll,
createCommandExecutingSelector(commandNames.APP_INDEXER_SYNC),
(isTestingAll, isSyncingIndexers) => {
return {
isTestingAll,
isSyncingIndexers
};
}
);
}
function mapDispatchToProps(dispatch, props) {
return {
onTestAllPress() {
dispatch(testAllApplications());
},
onAppIndexerSyncPress() {
dispatch(executeCommand({
name: commandNames.APP_INDEXER_SYNC
}));
}
};
}
export default connect(createMapStateToProps, mapDispatchToProps)(ApplicationSettings);

View File

@@ -19,9 +19,24 @@ import translate from 'Utilities/String/translate';
import styles from './EditApplicationModalContent.css';
const syncLevelOptions = [
{ key: 'disabled', value: translate('Disabled') },
{ key: 'addOnly', value: translate('AddRemoveOnly') },
{ key: 'fullSync', value: translate('FullSync') }
{
key: 'disabled',
get value() {
return translate('Disabled');
}
},
{
key: 'addOnly',
get value() {
return translate('AddRemoveOnly');
}
},
{
key: 'fullSync',
get value() {
return translate('FullSync');
}
}
];
function EditApplicationModalContent(props) {

View File

@@ -25,10 +25,31 @@ interface ManageApplicationsEditModalContentProps {
const NO_CHANGE = 'noChange';
const syncLevelOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: ApplicationSyncLevel.Disabled, value: translate('Disabled') },
{ key: ApplicationSyncLevel.AddOnly, value: translate('AddOnly') },
{ key: ApplicationSyncLevel.FullSync, value: translate('FullSync') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: ApplicationSyncLevel.Disabled,
get value() {
return translate('Disabled');
},
},
{
key: ApplicationSyncLevel.AddOnly,
get value() {
return translate('AddRemoveOnly');
},
},
{
key: ApplicationSyncLevel.FullSync,
get value() {
return translate('FullSync');
},
},
];
function ManageApplicationsEditModalContent(

View File

@@ -36,25 +36,25 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'syncLevel',
label: translate('SyncLevel'),
label: () => translate('SyncLevel'),
isSortable: true,
isVisible: true,
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
isSortable: true,
isVisible: true,
},

View File

@@ -25,9 +25,25 @@ interface ManageDownloadClientsEditModalContentProps {
const NO_CHANGE = 'noChange';
const enableOptions = [
{ key: NO_CHANGE, value: translate('NoChange'), disabled: true },
{ key: 'enabled', value: translate('Enabled') },
{ key: 'disabled', value: translate('Disabled') },
{
key: NO_CHANGE,
get value() {
return translate('NoChange');
},
disabled: true,
},
{
key: 'enabled',
get value() {
return translate('Enabled');
},
},
{
key: 'disabled',
get value() {
return translate('Disabled');
},
},
];
function ManageDownloadClientsEditModalContent(

View File

@@ -35,25 +35,25 @@ type OnSelectedChangeCallback = React.ComponentProps<
const COLUMNS = [
{
name: 'name',
label: translate('Name'),
label: () => translate('Name'),
isSortable: true,
isVisible: true,
},
{
name: 'implementation',
label: translate('Implementation'),
label: () => translate('Implementation'),
isSortable: true,
isVisible: true,
},
{
name: 'enable',
label: translate('Enabled'),
label: () => translate('Enabled'),
isSortable: true,
isVisible: true,
},
{
name: 'priority',
label: translate('ClientPriority'),
label: () => translate('ClientPriority'),
isSortable: true,
isVisible: true,
},

View File

@@ -117,10 +117,7 @@ export default {
[SELECT_APPLICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onDownload = selectedSchema.supportsOnDownload;
selectedSchema.onUpgrade = selectedSchema.supportsOnUpgrade;
selectedSchema.onRename = selectedSchema.supportsOnRename;
selectedSchema.name = selectedSchema.implementationName;
return selectedSchema;
});

View File

@@ -142,6 +142,7 @@ export default {
[SELECT_DOWNLOAD_CLIENT_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = selectedSchema.implementationName;
selectedSchema.enable = true;
return selectedSchema;

View File

@@ -104,6 +104,8 @@ export default {
[SELECT_INDEXER_PROXY_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = selectedSchema.implementationName;
return selectedSchema;
});
}

View File

@@ -104,6 +104,7 @@ export default {
[SELECT_NOTIFICATION_SCHEMA]: (state, { payload }) => {
return selectProviderSchema(state, section, payload, (selectedSchema) => {
selectedSchema.name = selectedSchema.implementationName;
selectedSchema.onGrab = selectedSchema.supportsOnGrab;
selectedSchema.onApplicationUpdate = selectedSchema.supportsOnApplicationUpdate;

View File

@@ -4,6 +4,7 @@ import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import getSectionState from 'Utilities/State/getSectionState';
import updateSectionState from 'Utilities/State/updateSectionState';
import { fetchTranslations as fetchAppTranslations } from 'Utilities/String/translate';
import createHandleActions from './Creators/createHandleActions';
function getDimensions(width, height) {
@@ -41,7 +42,12 @@ export const defaultState = {
isReconnecting: false,
isDisconnected: false,
isRestarting: false,
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen
isSidebarVisible: !getDimensions(window.innerWidth, window.innerHeight).isSmallScreen,
translations: {
isFetching: true,
isPopulated: false,
error: null
}
};
//
@@ -53,6 +59,7 @@ export const SAVE_DIMENSIONS = 'app/saveDimensions';
export const SET_VERSION = 'app/setVersion';
export const SET_APP_VALUE = 'app/setAppValue';
export const SET_IS_SIDEBAR_VISIBLE = 'app/setIsSidebarVisible';
export const FETCH_TRANSLATIONS = 'app/fetchTranslations';
export const PING_SERVER = 'app/pingServer';
@@ -66,6 +73,7 @@ export const setAppValue = createAction(SET_APP_VALUE);
export const showMessage = createAction(SHOW_MESSAGE);
export const hideMessage = createAction(HIDE_MESSAGE);
export const pingServer = createThunk(PING_SERVER);
export const fetchTranslations = createThunk(FETCH_TRANSLATIONS);
//
// Helpers
@@ -127,6 +135,17 @@ function pingServerAfterTimeout(getState, dispatch) {
export const actionHandlers = handleThunks({
[PING_SERVER]: function(getState, payload, dispatch) {
pingServerAfterTimeout(getState, dispatch);
},
[FETCH_TRANSLATIONS]: async function(getState, payload, dispatch) {
const isFetchingComplete = await fetchAppTranslations();
dispatch(setAppValue({
translations: {
isFetching: false,
isPopulated: isFetchingComplete,
error: isFetchingComplete ? null : 'Failed to load translations from API'
}
}));
}
});

View File

@@ -30,67 +30,67 @@ export const defaultState = {
columns: [
{
name: 'eventType',
columnLabel: translate('EventType'),
columnLabel: () => translate('EventType'),
isVisible: true,
isModifiable: false
},
{
name: 'indexer',
label: translate('Indexer'),
label: () => translate('Indexer'),
isSortable: false,
isVisible: true
},
{
name: 'query',
label: translate('Query'),
label: () => translate('Query'),
isSortable: false,
isVisible: true
},
{
name: 'parameters',
label: translate('Parameters'),
label: () => translate('Parameters'),
isSortable: false,
isVisible: true
},
{
name: 'grabTitle',
label: translate('GrabTitle'),
label: () => translate('GrabTitle'),
isSortable: false,
isVisible: false
},
{
name: 'queryType',
label: translate('QueryType'),
label: () => translate('QueryType'),
isSortable: false,
isVisible: false
},
{
name: 'categories',
label: translate('Categories'),
label: () => translate('Categories'),
isSortable: false,
isVisible: true
},
{
name: 'date',
label: translate('Date'),
label: () => translate('Date'),
isSortable: true,
isVisible: true
},
{
name: 'source',
label: translate('Source'),
label: () => translate('Source'),
isSortable: false,
isVisible: false
},
{
name: 'elapsedTime',
label: translate('ElapsedTime'),
label: () => translate('ElapsedTime'),
isSortable: false,
isVisible: true
},
{
name: 'details',
columnLabel: translate('Details'),
columnLabel: () => translate('Details'),
isVisible: true,
isModifiable: false
}
@@ -101,12 +101,12 @@ export const defaultState = {
filters: [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
},
{
key: 'releaseGrabbed',
label: translate('Grabbed'),
label: () => translate('Grabbed'),
filters: [
{
key: 'eventType',
@@ -117,7 +117,7 @@ export const defaultState = {
},
{
key: 'indexerRss',
label: translate('IndexerRss'),
label: () => translate('IndexerRss'),
filters: [
{
key: 'eventType',
@@ -128,7 +128,7 @@ export const defaultState = {
},
{
key: 'indexerQuery',
label: translate('IndexerQuery'),
label: () => translate('IndexerQuery'),
filters: [
{
key: 'eventType',
@@ -139,7 +139,7 @@ export const defaultState = {
},
{
key: 'indexerAuth',
label: translate('IndexerAuth'),
label: () => translate('IndexerAuth'),
filters: [
{
key: 'eventType',
@@ -150,7 +150,7 @@ export const defaultState = {
},
{
key: 'failed',
label: translate('Failed'),
label: () => translate('Failed'),
filters: [
{
key: 'successful',

View File

@@ -54,7 +54,7 @@ export const defaultState = {
export const filters = [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
}
];

View File

@@ -32,93 +32,93 @@ export const defaultState = {
columns: [
{
name: 'status',
columnLabel: translate('IndexerStatus'),
columnLabel: () => translate('IndexerStatus'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'sortName',
label: translate('IndexerName'),
label: () => translate('IndexerName'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
isSortable: true,
isVisible: true
},
{
name: 'privacy',
label: translate('Privacy'),
label: () => translate('Privacy'),
isSortable: true,
isVisible: true
},
{
name: 'priority',
label: translate('Priority'),
label: () => translate('Priority'),
isSortable: true,
isVisible: true
},
{
name: 'appProfileId',
label: translate('SyncProfile'),
label: () => translate('SyncProfile'),
isSortable: true,
isVisible: true
},
{
name: 'added',
label: translate('Added'),
label: () => translate('Added'),
isSortable: true,
isVisible: true
},
{
name: 'vipExpiration',
label: translate('VipExpiration'),
label: () => translate('VipExpiration'),
isSortable: true,
isVisible: false
},
{
name: 'capabilities',
label: translate('Categories'),
label: () => translate('Categories'),
isSortable: false,
isVisible: true
},
{
name: 'minimumSeeders',
label: translate('MinimumSeeders'),
label: () => translate('MinimumSeeders'),
isSortable: true,
isVisible: false
},
{
name: 'seedRatio',
label: translate('SeedRatio'),
label: () => translate('SeedRatio'),
isSortable: true,
isVisible: false
},
{
name: 'seedTime',
label: translate('SeedTime'),
label: () => translate('SeedTime'),
isSortable: true,
isVisible: false
},
{
name: 'packSeedTime',
label: translate('PackSeedTime'),
label: () => translate('PackSeedTime'),
isSortable: true,
isVisible: false
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
isSortable: false,
isVisible: false
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isVisible: true,
isModifiable: false
}
@@ -136,53 +136,53 @@ export const defaultState = {
filterBuilderProps: [
{
name: 'name',
label: translate('IndexerName'),
label: () => translate('IndexerName'),
type: filterBuilderTypes.STRING
},
{
name: 'enable',
label: translate('Enabled'),
label: () => translate('Enabled'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'added',
label: translate('Added'),
label: () => translate('Added'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'vipExpiration',
label: translate('VipExpiration'),
label: () => translate('VipExpiration'),
type: filterBuilderTypes.DATE,
valueType: filterBuilderValueTypes.DATE
},
{
name: 'priority',
label: translate('Priority'),
label: () => translate('Priority'),
type: filterBuilderTypes.NUMBER
},
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.PROTOCOL
},
{
name: 'privacy',
label: translate('Privacy'),
label: () => translate('Privacy'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.PRIVACY
},
{
name: 'appProfileId',
label: translate('SyncProfile'),
label: () => translate('SyncProfile'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.APP_PROFILE
},
{
name: 'tags',
label: translate('Tags'),
label: () => translate('Tags'),
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG
}

View File

@@ -33,7 +33,7 @@ export const defaultState = {
filters: [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
},
{

View File

@@ -60,7 +60,7 @@ function showOAuthWindow(url, payload) {
responseJSON: [
{
propertyName: payload.name,
errorMessage: translate('OAuthPopupMessage')
errorMessage: () => translate('OAuthPopupMessage')
}
]
};

View File

@@ -56,55 +56,55 @@ export const defaultState = {
},
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
isSortable: true,
isVisible: true
},
{
name: 'age',
label: translate('Age'),
label: () => translate('Age'),
isSortable: true,
isVisible: true
},
{
name: 'sortTitle',
label: translate('Title'),
label: () => translate('Title'),
isSortable: true,
isVisible: true
},
{
name: 'indexer',
label: translate('Indexer'),
label: () => translate('Indexer'),
isSortable: true,
isVisible: true
},
{
name: 'size',
label: translate('Size'),
label: () => translate('Size'),
isSortable: true,
isVisible: true
},
{
name: 'files',
label: translate('Files'),
label: () => translate('Files'),
isSortable: true,
isVisible: false
},
{
name: 'grabs',
label: translate('Grabs'),
label: () => translate('Grabs'),
isSortable: true,
isVisible: true
},
{
name: 'peers',
label: translate('Peers'),
label: () => translate('Peers'),
isSortable: true,
isVisible: true
},
{
name: 'category',
label: translate('Category'),
label: () => translate('Category'),
isSortable: true,
isVisible: true
},
@@ -116,7 +116,7 @@ export const defaultState = {
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isVisible: true,
isModifiable: false
}
@@ -158,7 +158,7 @@ export const defaultState = {
filters: [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
}
],
@@ -166,50 +166,50 @@ export const defaultState = {
filterBuilderProps: [
{
name: 'title',
label: translate('Title'),
label: () => translate('Title'),
type: filterBuilderTypes.STRING
},
{
name: 'age',
label: translate('Age'),
label: () => translate('Age'),
type: filterBuilderTypes.NUMBER
},
{
name: 'protocol',
label: translate('Protocol'),
label: () => translate('Protocol'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.PROTOCOL
},
{
name: 'indexerId',
label: translate('Indexer'),
label: () => translate('Indexer'),
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.INDEXER
},
{
name: 'size',
label: translate('Size'),
label: () => translate('Size'),
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{
name: 'files',
label: translate('Files'),
label: () => translate('Files'),
type: filterBuilderTypes.NUMBER
},
{
name: 'grabs',
label: translate('Grabs'),
label: () => translate('Grabs'),
type: filterBuilderTypes.NUMBER
},
{
name: 'seeders',
label: translate('Seeders'),
label: () => translate('Seeders'),
type: filterBuilderTypes.NUMBER
},
{
name: 'peers',
label: translate('Peers'),
label: () => translate('Peers'),
type: filterBuilderTypes.NUMBER
}
],

View File

@@ -82,34 +82,34 @@ export const defaultState = {
columns: [
{
name: 'level',
columnLabel: translate('Level'),
columnLabel: () => translate('Level'),
isSortable: false,
isVisible: true,
isModifiable: false
},
{
name: 'time',
label: translate('Time'),
label: () => translate('Time'),
isSortable: true,
isVisible: true,
isModifiable: false
},
{
name: 'logger',
label: translate('Component'),
label: () => translate('Component'),
isSortable: false,
isVisible: true,
isModifiable: false
},
{
name: 'message',
label: translate('Message'),
label: () => translate('Message'),
isVisible: true,
isModifiable: false
},
{
name: 'actions',
columnLabel: translate('Actions'),
columnLabel: () => translate('Actions'),
isSortable: true,
isVisible: true,
isModifiable: false
@@ -121,12 +121,12 @@ export const defaultState = {
filters: [
{
key: 'all',
label: translate('All'),
label: () => translate('All'),
filters: []
},
{
key: 'info',
label: translate('Info'),
label: () => translate('Info'),
filters: [
{
key: 'level',
@@ -137,7 +137,7 @@ export const defaultState = {
},
{
key: 'warn',
label: translate('Warn'),
label: () => translate('Warn'),
filters: [
{
key: 'level',
@@ -148,7 +148,7 @@ export const defaultState = {
},
{
key: 'error',
label: translate('Error'),
label: () => translate('Error'),
filters: [
{
key: 'level',

View File

@@ -36,10 +36,17 @@ function mergeColumns(path, initialState, persistedState, computedState) {
const column = initialColumns.find((i) => i.name === persistedColumn.name);
if (column) {
columns.push({
...column,
isVisible: persistedColumn.isVisible
});
const newColumn = {};
// We can't use a spread operator or Object.assign to clone the column
// or any accessors are lost and can break translations.
for (const prop of Object.keys(column)) {
Object.defineProperty(newColumn, prop, Object.getOwnPropertyDescriptor(column, prop));
}
newColumn.isVisible = persistedColumn.isVisible;
columns.push(newColumn);
}
});

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createAllIndexersSelector() {
return createSelector(
(state) => state.indexers,
(state: AppState) => state.indexers,
(indexers) => {
return indexers.items;
}

View File

@@ -1,15 +0,0 @@
import { createSelector } from 'reselect';
function createAppProfileSelector() {
return createSelector(
(state, { appProfileId }) => appProfileId,
(state) => state.settings.appProfiles.items,
(appProfileId, appProfiles) => {
return appProfiles.find((profile) => {
return profile.id === appProfileId;
});
}
);
}
export default createAppProfileSelector;

View File

@@ -0,0 +1,14 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createAppProfileSelector() {
return createSelector(
(_: AppState, { appProfileId }: { appProfileId: number }) => appProfileId,
(state: AppState) => state.settings.appProfiles.items,
(appProfileId, appProfiles) => {
return appProfiles.find((profile) => profile.id === appProfileId);
}
);
}
export default createAppProfileSelector;

View File

@@ -2,13 +2,10 @@ import { createSelector } from 'reselect';
import { isCommandExecuting } from 'Utilities/Command';
import createCommandSelector from './createCommandSelector';
function createCommandExecutingSelector(name, contraints = {}) {
return createSelector(
createCommandSelector(name, contraints),
(command) => {
return isCommandExecuting(command);
}
);
function createCommandExecutingSelector(name: string, contraints = {}) {
return createSelector(createCommandSelector(name, contraints), (command) => {
return isCommandExecuting(command);
});
}
export default createCommandExecutingSelector;

View File

@@ -1,14 +0,0 @@
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name, contraints = {}) {
return createSelector(
createCommandsSelector(),
(commands) => {
return findCommand(commands, { name, ...contraints });
}
);
}
export default createCommandSelector;

View File

@@ -0,0 +1,11 @@
import { createSelector } from 'reselect';
import { findCommand } from 'Utilities/Command';
import createCommandsSelector from './createCommandsSelector';
function createCommandSelector(name: string, contraints = {}) {
return createSelector(createCommandsSelector(), (commands) => {
return findCommand(commands, { name, ...contraints });
});
}
export default createCommandSelector;

View File

@@ -1,8 +1,9 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
function createCommandsSelector() {
return createSelector(
(state) => state.commands,
(state: AppState) => state.commands,
(commands) => {
return commands.items;
}

View File

@@ -1,9 +0,0 @@
import _ from 'lodash';
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(
defaultMemoize,
_.isEqual
);
export default createDeepEqualSelector;

View File

@@ -0,0 +1,6 @@
import { isEqual } from 'lodash';
import { createSelectorCreator, defaultMemoize } from 'reselect';
const createDeepEqualSelector = createSelectorCreator(defaultMemoize, isEqual);
export default createDeepEqualSelector;

View File

@@ -1,9 +1,10 @@
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { isCommandExecuting } from 'Utilities/Command';
function createExecutingCommandsSelector() {
return createSelector(
(state) => state.commands.items,
(state: AppState) => state.commands.items,
(commands) => {
return commands.filter((command) => isCommandExecuting(command));
}

View File

@@ -1,13 +1,15 @@
import _ from 'lodash';
import { some } from 'lodash';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import createAllIndexersSelector from './createAllIndexersSelector';
function createExistingIndexerSelector() {
return createSelector(
(state, { definitionName }) => definitionName,
(_: AppState, { definitionName }: { definitionName: string }) =>
definitionName,
createAllIndexersSelector(),
(definitionName, indexers) => {
return _.some(indexers, { definitionName });
return some(indexers, { definitionName });
}
);
}

View File

@@ -1,16 +0,0 @@
import { createSelector } from 'reselect';
import { createIndexerSelectorForHook } from './createIndexerSelector';
function createIndexerAppProfileSelector(indexerId) {
return createSelector(
(state) => state.settings.appProfiles.items,
createIndexerSelectorForHook(indexerId),
(appProfiles, indexer = {}) => {
return appProfiles.find((profile) => {
return profile.id === indexer.appProfileId;
});
}
);
}
export default createIndexerAppProfileSelector;

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