{
+ selectedFilterKey: string;
+ filters: Filter[];
+}
+
+export default IndexerStatsAppState;
diff --git a/frontend/src/Components/Form/FormInputGroup.js b/frontend/src/Components/Form/FormInputGroup.js
index bd0e9184d..a00f6f8de 100644
--- a/frontend/src/Components/Form/FormInputGroup.js
+++ b/frontend/src/Components/Form/FormInputGroup.js
@@ -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,
diff --git a/frontend/src/Components/Form/PathInputConnector.js b/frontend/src/Components/Form/PathInputConnector.js
index 3917a8d3f..563437f9a 100644
--- a/frontend/src/Components/Form/PathInputConnector.js
+++ b/frontend/src/Components/Form/PathInputConnector.js
@@ -68,6 +68,7 @@ class PathInputConnector extends Component {
}
PathInputConnector.propTypes = {
+ ...PathInput.props,
includeFiles: PropTypes.bool.isRequired,
dispatchFetchPaths: PropTypes.func.isRequired,
dispatchClearPaths: PropTypes.func.isRequired
diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts b/frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
new file mode 100644
index 000000000..c748f6f97
--- /dev/null
+++ b/frontend/src/Components/Table/Cells/TableRowCellButton.css.d.ts
@@ -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;
diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.js b/frontend/src/Components/Table/Cells/TableRowCellButton.js
deleted file mode 100644
index ff50d3bc9..000000000
--- a/frontend/src/Components/Table/Cells/TableRowCellButton.js
+++ /dev/null
@@ -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 (
-
- );
-}
-
-TableRowCellButton.propTypes = {
- className: PropTypes.string.isRequired
-};
-
-TableRowCellButton.defaultProps = {
- className: styles.cell
-};
-
-export default TableRowCellButton;
diff --git a/frontend/src/Components/Table/Cells/TableRowCellButton.tsx b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx
new file mode 100644
index 000000000..c80a3d626
--- /dev/null
+++ b/frontend/src/Components/Table/Cells/TableRowCellButton.tsx
@@ -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 (
+
+ );
+}
+
+export default TableRowCellButton;
diff --git a/frontend/src/Indexer/NoIndexer.js b/frontend/src/Indexer/NoIndexer.tsx
similarity index 67%
rename from frontend/src/Indexer/NoIndexer.js
rename to frontend/src/Indexer/NoIndexer.tsx
index f94df7902..75650cad6 100644
--- a/frontend/src/Indexer/NoIndexer.js
+++ b/frontend/src/Indexer/NoIndexer.tsx
@@ -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) {
-
@@ -39,9 +37,4 @@ function NoIndexer(props) {
);
}
-NoIndexer.propTypes = {
- totalItems: PropTypes.number.isRequired,
- onAddIndexerPress: PropTypes.func.isRequired
-};
-
export default NoIndexer;
diff --git a/frontend/src/Indexer/Stats/Stats.css b/frontend/src/Indexer/Stats/IndexerStats.css
similarity index 100%
rename from frontend/src/Indexer/Stats/Stats.css
rename to frontend/src/Indexer/Stats/IndexerStats.css
diff --git a/frontend/src/Indexer/Stats/Stats.css.d.ts b/frontend/src/Indexer/Stats/IndexerStats.css.d.ts
similarity index 100%
rename from frontend/src/Indexer/Stats/Stats.css.d.ts
rename to frontend/src/Indexer/Stats/IndexerStats.css.d.ts
diff --git a/frontend/src/Indexer/Stats/IndexerStats.tsx b/frontend/src/Indexer/Stats/IndexerStats.tsx
new file mode 100644
index 000000000..0a61fc8fc
--- /dev/null
+++ b/frontend/src/Indexer/Stats/IndexerStats.tsx
@@ -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 (
+
+
+
+
+
+
+
+ {isFetching && !isPopulated && }
+
+ {!isFetching && !!error && (
+
+ {getErrorMessage(error, 'Failed to load indexer stats from API')}
+
+ )}
+
+ {isLoaded && (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+
+ );
+}
+
+export default IndexerStats;
diff --git a/frontend/src/Indexer/Stats/IndexerStatsFilterMenu.tsx b/frontend/src/Indexer/Stats/IndexerStatsFilterMenu.tsx
new file mode 100644
index 000000000..7b30be4c3
--- /dev/null
+++ b/frontend/src/Indexer/Stats/IndexerStatsFilterMenu.tsx
@@ -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 (
+
+ );
+}
+
+export default IndexerStatsFilterMenu;
diff --git a/frontend/src/Indexer/Stats/Stats.js b/frontend/src/Indexer/Stats/Stats.js
deleted file mode 100644
index 30bff5f84..000000000
--- a/frontend/src/Indexer/Stats/Stats.js
+++ /dev/null
@@ -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 (
-
-
-
-
-
-
-
- {
- isFetching && !isPopulated &&
-
- }
-
- {
- !isFetching && !!error &&
-
- {getErrorMessage(error, 'Failed to load indexer stats from API')}
-
- }
-
- {
- isLoaded &&
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
-
-
- );
-}
-
-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;
diff --git a/frontend/src/Indexer/Stats/StatsConnector.js b/frontend/src/Indexer/Stats/StatsConnector.js
deleted file mode 100644
index 006716953..000000000
--- a/frontend/src/Indexer/Stats/StatsConnector.js
+++ /dev/null
@@ -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 (
-
- );
- }
-}
-
-StatsConnector.propTypes = {
- dispatchFetchIndexerStats: PropTypes.func.isRequired
-};
-
-export default connect(createMapStateToProps, createMapDispatchToProps)(StatsConnector);
diff --git a/frontend/src/Indexer/Stats/StatsFilterMenu.js b/frontend/src/Indexer/Stats/StatsFilterMenu.js
deleted file mode 100644
index 283159b7e..000000000
--- a/frontend/src/Indexer/Stats/StatsFilterMenu.js
+++ /dev/null
@@ -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 (
-
- );
-}
-
-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;
diff --git a/frontend/src/Indexer/Stats/StatsFilterModalConnector.js b/frontend/src/Indexer/Stats/StatsFilterModalConnector.js
deleted file mode 100644
index 53bf2ed3c..000000000
--- a/frontend/src/Indexer/Stats/StatsFilterModalConnector.js
+++ /dev/null
@@ -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);
diff --git a/frontend/src/typings/IndexerStats.ts b/frontend/src/typings/IndexerStats.ts
new file mode 100644
index 000000000..74fa1862e
--- /dev/null
+++ b/frontend/src/typings/IndexerStats.ts
@@ -0,0 +1,31 @@
+export interface IndexerStatsIndexer {
+ indexerId: number;
+ indexerName: string;
+ averageResponseTime: number;
+ numberOfQueries: number;
+ numberOfGrabs: number;
+ numberOfRssQueries: number;
+ numberOfAuthQueries: number;
+ numberOfFailedQueries: number;
+ numberOfFailedGrabs: number;
+ numberOfFailedRssQueries: number;
+ numberOfFailedAuthQueries: number;
+}
+
+export interface IndexerStatsUserAgent {
+ userAgent: string;
+ numberOfQueries: number;
+ numberOfGrabs: number;
+}
+
+export interface IndexerStatsHost {
+ host: string;
+ numberOfQueries: number;
+ numberOfGrabs: number;
+}
+
+export interface IndexerStats {
+ indexers: IndexerStatsIndexer[];
+ userAgents: IndexerStatsUserAgent[];
+ hosts: IndexerStatsHost[];
+}
diff --git a/src/NzbDrone.Core/Localization/Core/en.json b/src/NzbDrone.Core/Localization/Core/en.json
index 9b89992d2..f10617902 100644
--- a/src/NzbDrone.Core/Localization/Core/en.json
+++ b/src/NzbDrone.Core/Localization/Core/en.json
@@ -56,6 +56,7 @@
"Artist": "Artist",
"AudioSearch": "Audio Search",
"Auth": "Auth",
+ "AuthQueries": "Auth Queries",
"Authentication": "Authentication",
"AuthenticationMethodHelpText": "Require Username and Password to access Prowlarr",
"AuthenticationRequired": "Authentication Required",
@@ -398,6 +399,7 @@
"Result": "Result",
"Retention": "Retention",
"RssFeed": "RSS Feed",
+ "RssQueries": "RSS Queries",
"SSLCertPassword": "SSL Cert Password",
"SSLCertPasswordHelpText": "Password for pfx file",
"SSLCertPath": "SSL Cert Path",
@@ -413,6 +415,7 @@
"SearchCapabilities": "Search Capabilities",
"SearchCountIndexers": "Search {0} indexers",
"SearchIndexers": "Search Indexers",
+ "SearchQueries": "Search Queries",
"SearchType": "Search Type",
"SearchTypes": "Search Types",
"Season": "Season",