Convert Indexer Stats to Typescript

pull/1755/head
Bogdan 10 months ago
parent 809ed940e6
commit f618901048

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

@ -5,7 +5,7 @@ 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 DevelopmentSettingsConnector from 'Settings/Development/DevelopmentSettingsConnector';
@ -60,7 +60,7 @@ function AppRoutes(props) {
<Route
path="/indexers/stats"
component={StatsConnector}
component={IndexerStats}
/>
{/*

@ -1,4 +1,5 @@
import IndexerAppState, { IndexerIndexAppState } from './IndexerAppState';
import IndexerStatsAppState from './IndexerStatsAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@ -36,6 +37,7 @@ export interface CustomFilter {
interface AppState {
indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexers: IndexerAppState;
settings: SettingsAppState;
tags: TagsAppState;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save