mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Custom Filters for Stats
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import { AppSectionItemState } from 'App/State/AppSectionState';
|
import { AppSectionItemState } from 'App/State/AppSectionState';
|
||||||
import { Filter } from 'App/State/AppState';
|
import { Filter, FilterBuilderProp } from 'App/State/AppState';
|
||||||
|
import Indexer from 'Indexer/Indexer';
|
||||||
import { IndexerStats } from 'typings/IndexerStats';
|
import { IndexerStats } from 'typings/IndexerStats';
|
||||||
|
|
||||||
export interface IndexerStatsAppState
|
export interface IndexerStatsAppState
|
||||||
extends AppSectionItemState<IndexerStats> {
|
extends AppSectionItemState<IndexerStats> {
|
||||||
|
filterBuilderProps: FilterBuilderProp<Indexer>[];
|
||||||
selectedFilterKey: string;
|
selectedFilterKey: string;
|
||||||
filters: Filter[];
|
filters: Filter[];
|
||||||
}
|
}
|
||||||
|
@@ -1,20 +1,25 @@
|
|||||||
.fullWidthChart {
|
.fullWidthChart {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 15px 25px;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.halfWidthChart {
|
.halfWidthChart {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
padding: 15px 25px;
|
|
||||||
width: 50%;
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartContainer {
|
||||||
|
margin: 5px;
|
||||||
|
padding: 15px 25px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: var(--chartBackgroundColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: $breakpointSmall) {
|
@media only screen and (max-width: $breakpointSmall) {
|
||||||
.halfWidthChart {
|
.halfWidthChart {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
margin: 5px;
|
||||||
padding: 15px 25px;
|
padding: 15px 25px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
// This file is automatically generated.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
|
'chartContainer': string;
|
||||||
'fullWidthChart': string;
|
'fullWidthChart': string;
|
||||||
'halfWidthChart': string;
|
'halfWidthChart': string;
|
||||||
}
|
}
|
||||||
|
@@ -8,6 +8,7 @@ import BarChart from 'Components/Chart/BarChart';
|
|||||||
import DoughnutChart from 'Components/Chart/DoughnutChart';
|
import DoughnutChart from 'Components/Chart/DoughnutChart';
|
||||||
import StackedBarChart from 'Components/Chart/StackedBarChart';
|
import StackedBarChart from 'Components/Chart/StackedBarChart';
|
||||||
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
|
||||||
|
import FilterMenu from 'Components/Menu/FilterMenu';
|
||||||
import PageContent from 'Components/Page/PageContent';
|
import PageContent from 'Components/Page/PageContent';
|
||||||
import PageContentBody from 'Components/Page/PageContentBody';
|
import PageContentBody from 'Components/Page/PageContentBody';
|
||||||
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
import PageToolbar from 'Components/Page/Toolbar/PageToolbar';
|
||||||
@@ -17,6 +18,7 @@ import {
|
|||||||
fetchIndexerStats,
|
fetchIndexerStats,
|
||||||
setIndexerStatsFilter,
|
setIndexerStatsFilter,
|
||||||
} from 'Store/Actions/indexerStatsActions';
|
} from 'Store/Actions/indexerStatsActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import {
|
import {
|
||||||
IndexerStatsHost,
|
IndexerStatsHost,
|
||||||
IndexerStatsIndexer,
|
IndexerStatsIndexer,
|
||||||
@@ -24,7 +26,7 @@ import {
|
|||||||
} from 'typings/IndexerStats';
|
} from 'typings/IndexerStats';
|
||||||
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
import getErrorMessage from 'Utilities/Object/getErrorMessage';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import IndexerStatsFilterMenu from './IndexerStatsFilterMenu';
|
import IndexerStatsFilterModal from './IndexerStatsFilterModal';
|
||||||
import styles from './IndexerStats.css';
|
import styles from './IndexerStats.css';
|
||||||
|
|
||||||
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
function getAverageResponseTimeData(indexerStats: IndexerStatsIndexer[]) {
|
||||||
@@ -165,15 +167,26 @@ function getHostQueryData(indexerStats: IndexerStatsHost[]) {
|
|||||||
const indexerStatsSelector = () => {
|
const indexerStatsSelector = () => {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state: AppState) => state.indexerStats,
|
(state: AppState) => state.indexerStats,
|
||||||
(indexerStats: IndexerStatsAppState) => {
|
createCustomFiltersSelector('indexerStats'),
|
||||||
return indexerStats;
|
(indexerStats: IndexerStatsAppState, customFilters) => {
|
||||||
|
return {
|
||||||
|
...indexerStats,
|
||||||
|
customFilters,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function IndexerStats() {
|
function IndexerStats() {
|
||||||
const { isFetching, isPopulated, item, error, filters, selectedFilterKey } =
|
const {
|
||||||
useSelector(indexerStatsSelector());
|
isFetching,
|
||||||
|
isPopulated,
|
||||||
|
item,
|
||||||
|
error,
|
||||||
|
filters,
|
||||||
|
customFilters,
|
||||||
|
selectedFilterKey,
|
||||||
|
} = useSelector(indexerStatsSelector());
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -193,10 +206,13 @@ function IndexerStats() {
|
|||||||
<PageContent>
|
<PageContent>
|
||||||
<PageToolbar>
|
<PageToolbar>
|
||||||
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
|
<PageToolbarSection alignContent={align.RIGHT} collapseButtons={false}>
|
||||||
<IndexerStatsFilterMenu
|
<FilterMenu
|
||||||
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
|
customFilters={customFilters}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
|
filterModalConnectorComponent={IndexerStatsFilterModal}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
@@ -213,57 +229,73 @@ function IndexerStats() {
|
|||||||
{isLoaded && (
|
{isLoaded && (
|
||||||
<div>
|
<div>
|
||||||
<div className={styles.fullWidthChart}>
|
<div className={styles.fullWidthChart}>
|
||||||
<BarChart
|
<div className={styles.chartContainer}>
|
||||||
data={getAverageResponseTimeData(item.indexers)}
|
<BarChart
|
||||||
title={translate('AverageResponseTimesMs')}
|
data={getAverageResponseTimeData(item.indexers)}
|
||||||
/>
|
title={translate('AverageResponseTimesMs')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.fullWidthChart}>
|
<div className={styles.fullWidthChart}>
|
||||||
<BarChart
|
<div className={styles.chartContainer}>
|
||||||
data={getFailureRateData(item.indexers)}
|
<BarChart
|
||||||
title={translate('IndexerFailureRate')}
|
data={getFailureRateData(item.indexers)}
|
||||||
kind={kinds.WARNING}
|
title={translate('IndexerFailureRate')}
|
||||||
/>
|
kind={kinds.WARNING}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
<div className={styles.halfWidthChart}>
|
||||||
<StackedBarChart
|
<div className={styles.chartContainer}>
|
||||||
data={getTotalRequestsData(item.indexers)}
|
<StackedBarChart
|
||||||
title={translate('TotalIndexerQueries')}
|
data={getTotalRequestsData(item.indexers)}
|
||||||
/>
|
title={translate('TotalIndexerQueries')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
<div className={styles.halfWidthChart}>
|
||||||
<BarChart
|
<div className={styles.chartContainer}>
|
||||||
data={getNumberGrabsData(item.indexers)}
|
<BarChart
|
||||||
title={translate('TotalIndexerSuccessfulGrabs')}
|
data={getNumberGrabsData(item.indexers)}
|
||||||
/>
|
title={translate('TotalIndexerSuccessfulGrabs')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
<div className={styles.halfWidthChart}>
|
||||||
<BarChart
|
<div className={styles.chartContainer}>
|
||||||
data={getUserAgentQueryData(item.userAgents)}
|
<BarChart
|
||||||
title={translate('TotalUserAgentQueries')}
|
data={getUserAgentQueryData(item.userAgents)}
|
||||||
horizontal={true}
|
title={translate('TotalUserAgentQueries')}
|
||||||
/>
|
horizontal={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
<div className={styles.halfWidthChart}>
|
||||||
<BarChart
|
<div className={styles.chartContainer}>
|
||||||
data={getUserAgentGrabsData(item.userAgents)}
|
<BarChart
|
||||||
title={translate('TotalUserAgentGrabs')}
|
data={getUserAgentGrabsData(item.userAgents)}
|
||||||
horizontal={true}
|
title={translate('TotalUserAgentGrabs')}
|
||||||
/>
|
horizontal={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
<div className={styles.halfWidthChart}>
|
||||||
<DoughnutChart
|
<div className={styles.chartContainer}>
|
||||||
data={getHostQueryData(item.hosts)}
|
<DoughnutChart
|
||||||
title={translate('TotalHostQueries')}
|
data={getHostQueryData(item.hosts)}
|
||||||
horizontal={true}
|
title={translate('TotalHostQueries')}
|
||||||
/>
|
horizontal={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.halfWidthChart}>
|
<div className={styles.halfWidthChart}>
|
||||||
<DoughnutChart
|
<div className={styles.chartContainer}>
|
||||||
data={getHostGrabsData(item.hosts)}
|
<DoughnutChart
|
||||||
title={translate('TotalHostGrabs')}
|
data={getHostGrabsData(item.hosts)}
|
||||||
horizontal={true}
|
title={translate('TotalHostGrabs')}
|
||||||
/>
|
horizontal={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@@ -1,27 +0,0 @@
|
|||||||
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;
|
|
56
frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx
Normal file
56
frontend/src/Indexer/Stats/IndexerStatsFilterModal.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import React, { useCallback } from 'react';
|
||||||
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
|
import { createSelector } from 'reselect';
|
||||||
|
import AppState from 'App/State/AppState';
|
||||||
|
import FilterModal from 'Components/Filter/FilterModal';
|
||||||
|
import { setIndexerStatsFilter } from 'Store/Actions/indexerStatsActions';
|
||||||
|
|
||||||
|
function createIndexerStatsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.indexerStats.item,
|
||||||
|
(indexerStats) => {
|
||||||
|
return indexerStats;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.indexerStats.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IndexerStatsFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IndexerStatsFilterModal(
|
||||||
|
props: IndexerStatsFilterModalProps
|
||||||
|
) {
|
||||||
|
const sectionItems = [useSelector(createIndexerStatsSelector())];
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'indexerStats';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setIndexerStatsFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -3,6 +3,7 @@ import { batchActions } from 'redux-batched-actions';
|
|||||||
import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes } from 'Helpers/Props';
|
||||||
import { createThunk, handleThunks } from 'Store/thunks';
|
import { createThunk, handleThunks } from 'Store/thunks';
|
||||||
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
import createAjaxRequest from 'Utilities/createAjaxRequest';
|
||||||
|
import findSelectedFilters from 'Utilities/Filter/findSelectedFilters';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import { set, update } from './baseActions';
|
import { set, update } from './baseActions';
|
||||||
import createHandleActions from './Creators/createHandleActions';
|
import createHandleActions from './Creators/createHandleActions';
|
||||||
@@ -55,19 +56,20 @@ export const defaultState = {
|
|||||||
|
|
||||||
filterBuilderProps: [
|
filterBuilderProps: [
|
||||||
{
|
{
|
||||||
name: 'startDate',
|
name: 'indexers',
|
||||||
label: 'Start Date',
|
label: () => translate('Indexers'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.CONTAINS,
|
||||||
valueType: filterBuilderValueTypes.DATE
|
valueType: filterBuilderValueTypes.INDEXER
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'endDate',
|
name: 'tags',
|
||||||
label: 'End Date',
|
label: () => translate('Tags'),
|
||||||
type: filterBuilderTypes.EXACT,
|
type: filterBuilderTypes.CONTAINS,
|
||||||
valueType: filterBuilderValueTypes.DATE
|
valueType: filterBuilderValueTypes.TAG
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectedFilterKey: 'all'
|
selectedFilterKey: 'all',
|
||||||
|
customFilters: []
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
@@ -81,6 +83,10 @@ export const persistState = [
|
|||||||
export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats';
|
export const FETCH_INDEXER_STATS = 'indexerStats/fetchIndexerStats';
|
||||||
export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter';
|
export const SET_INDEXER_STATS_FILTER = 'indexerStats/setIndexerStatsFilter';
|
||||||
|
|
||||||
|
function getCustomFilters(state, type) {
|
||||||
|
return state.customFilters.items.filter((customFilter) => customFilter.type === type);
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// Action Creators
|
// Action Creators
|
||||||
|
|
||||||
@@ -94,23 +100,35 @@ export const actionHandlers = handleThunks({
|
|||||||
[FETCH_INDEXER_STATS]: function(getState, payload, dispatch) {
|
[FETCH_INDEXER_STATS]: function(getState, payload, dispatch) {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const indexerStats = state.indexerStats;
|
const indexerStats = state.indexerStats;
|
||||||
|
const customFilters = getCustomFilters(state, section);
|
||||||
|
const selectedFilters = findSelectedFilters(indexerStats.selectedFilterKey, indexerStats.filters, customFilters);
|
||||||
|
|
||||||
const requestParams = {
|
const requestParams = {
|
||||||
endDate: moment().toISOString()
|
endDate: moment().toISOString()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectedFilters.forEach((selectedFilter) => {
|
||||||
|
if (selectedFilter.key === 'indexers') {
|
||||||
|
requestParams.indexers = selectedFilter.value.join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedFilter.key === 'tags') {
|
||||||
|
requestParams.tags = selectedFilter.value.join(',');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (indexerStats.selectedFilterKey !== 'all') {
|
if (indexerStats.selectedFilterKey !== 'all') {
|
||||||
let dayCount = 7;
|
if (indexerStats.selectedFilterKey === 'lastSeven') {
|
||||||
|
requestParams.startDate = moment().add(-7, 'days').endOf('day').toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
if (indexerStats.selectedFilterKey === 'lastThirty') {
|
if (indexerStats.selectedFilterKey === 'lastThirty') {
|
||||||
dayCount = 30;
|
requestParams.startDate = moment().add(-30, 'days').endOf('day').toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (indexerStats.selectedFilterKey === 'lastNinety') {
|
if (indexerStats.selectedFilterKey === 'lastNinety') {
|
||||||
dayCount = 90;
|
requestParams.startDate = moment().add(-90, 'days').endOf('day').toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
requestParams.startDate = moment().add(-dayCount, 'days').endOf('day').toISOString();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const basesAttrs = {
|
const basesAttrs = {
|
||||||
|
@@ -187,6 +187,7 @@ module.exports = {
|
|||||||
//
|
//
|
||||||
// Charts
|
// Charts
|
||||||
|
|
||||||
|
chartBackgroundColor: '#262626',
|
||||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
||||||
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
|
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
|
||||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
||||||
|
@@ -187,6 +187,7 @@ module.exports = {
|
|||||||
//
|
//
|
||||||
// Charts
|
// Charts
|
||||||
|
|
||||||
|
chartBackgroundColor: '#fff',
|
||||||
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
failedColors: ['#ffbeb2', '#feb4a6', '#fdab9b', '#fca290', '#fb9984', '#fa8f79', '#f9856e', '#f77b66', '#f5715d', '#f36754', '#f05c4d', '#ec5049', '#e74545', '#e13b42', '#da323f', '#d3293d', '#ca223c', '#c11a3b', '#b8163a', '#ae123a'],
|
||||||
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
|
chartColorsDiversified: ['#90caf9', '#f4d166', '#ff8a65', '#ce93d8', '#80cba9', '#ffab91', '#8097ea', '#bcaaa4', '#a57583', '#e4e498', '#9e96af', '#c6ab81', '#6972c6', '#619fc6', '#81ad81', '#f48fb1', '#82afca', '#b5b071', '#8b959b', '#7ec0b4'],
|
||||||
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
chartColors: ['#f4d166', '#f6c760', '#f8bc58', '#f8b252', '#f7a84a', '#f69e41', '#f49538', '#f38b2f', '#f28026', '#f0751e', '#eb6c1c', '#e4641e', '#de5d1f', '#d75521', '#cf4f22', '#c64a22', '#bc4623', '#b24223', '#a83e24', '#9e3a26']
|
||||||
|
@@ -46,7 +46,7 @@ namespace NzbDrone.Core.Test.IndexerStatsTests
|
|||||||
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
|
.Setup(o => o.Between(It.IsAny<DateTime>(), It.IsAny<DateTime>()))
|
||||||
.Returns<DateTime, DateTime>((s, f) => history);
|
.Returns<DateTime, DateTime>((s, f) => history);
|
||||||
|
|
||||||
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow);
|
var statistics = Subject.IndexerStatistics(DateTime.UtcNow.AddMonths(-1), DateTime.UtcNow, new List<int> { 5 });
|
||||||
|
|
||||||
statistics.IndexerStatistics.Count.Should().Be(1);
|
statistics.IndexerStatistics.Count.Should().Be(1);
|
||||||
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
|
statistics.IndexerStatistics.First().AverageResponseTime.Should().Be(0);
|
||||||
|
@@ -8,7 +8,7 @@ namespace NzbDrone.Core.IndexerStats
|
|||||||
{
|
{
|
||||||
public interface IIndexerStatisticsService
|
public interface IIndexerStatisticsService
|
||||||
{
|
{
|
||||||
CombinedStatistics IndexerStatistics(DateTime start, DateTime end);
|
CombinedStatistics IndexerStatistics(DateTime start, DateTime end, List<int> indexerIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
public class IndexerStatisticsService : IIndexerStatisticsService
|
public class IndexerStatisticsService : IIndexerStatisticsService
|
||||||
@@ -22,13 +22,15 @@ namespace NzbDrone.Core.IndexerStats
|
|||||||
_indexerFactory = indexerFactory;
|
_indexerFactory = indexerFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public CombinedStatistics IndexerStatistics(DateTime start, DateTime end)
|
public CombinedStatistics IndexerStatistics(DateTime start, DateTime end, List<int> indexerIds)
|
||||||
{
|
{
|
||||||
var history = _historyService.Between(start, end);
|
var history = _historyService.Between(start, end);
|
||||||
|
|
||||||
var groupedByIndexer = history.GroupBy(h => h.IndexerId);
|
var filteredHistory = history.Where(h => indexerIds.Contains(h.IndexerId));
|
||||||
var groupedByUserAgent = history.GroupBy(h => h.Data.GetValueOrDefault("source") ?? "");
|
|
||||||
var groupedByHost = history.GroupBy(h => h.Data.GetValueOrDefault("host") ?? "");
|
var groupedByIndexer = filteredHistory.GroupBy(h => h.IndexerId);
|
||||||
|
var groupedByUserAgent = filteredHistory.GroupBy(h => h.Data.GetValueOrDefault("source") ?? "");
|
||||||
|
var groupedByHost = filteredHistory.GroupBy(h => h.Data.GetValueOrDefault("host") ?? "");
|
||||||
|
|
||||||
var indexerStatsList = new List<IndexerStatistics>();
|
var indexerStatsList = new List<IndexerStatistics>();
|
||||||
var userAgentStatsList = new List<UserAgentStatistics>();
|
var userAgentStatsList = new List<UserAgentStatistics>();
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using NzbDrone.Common.Extensions;
|
||||||
|
using NzbDrone.Core.Indexers;
|
||||||
using NzbDrone.Core.IndexerStats;
|
using NzbDrone.Core.IndexerStats;
|
||||||
|
using NzbDrone.Core.Tags;
|
||||||
using Prowlarr.Http;
|
using Prowlarr.Http;
|
||||||
|
|
||||||
namespace Prowlarr.Api.V1.Indexers
|
namespace Prowlarr.Api.V1.Indexers
|
||||||
@@ -9,20 +14,41 @@ namespace Prowlarr.Api.V1.Indexers
|
|||||||
public class IndexerStatsController : Controller
|
public class IndexerStatsController : Controller
|
||||||
{
|
{
|
||||||
private readonly IIndexerStatisticsService _indexerStatisticsService;
|
private readonly IIndexerStatisticsService _indexerStatisticsService;
|
||||||
|
private readonly IIndexerFactory _indexerFactory;
|
||||||
|
private readonly ITagService _tagService;
|
||||||
|
|
||||||
public IndexerStatsController(IIndexerStatisticsService indexerStatisticsService)
|
public IndexerStatsController(IIndexerStatisticsService indexerStatisticsService, IIndexerFactory indexerFactory, ITagService tagService)
|
||||||
{
|
{
|
||||||
_indexerStatisticsService = indexerStatisticsService;
|
_indexerStatisticsService = indexerStatisticsService;
|
||||||
|
_indexerFactory = indexerFactory;
|
||||||
|
_tagService = tagService;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public IndexerStatsResource GetAll(DateTime? startDate, DateTime? endDate)
|
public IndexerStatsResource GetAll(DateTime? startDate, DateTime? endDate, string indexers, string tags)
|
||||||
{
|
{
|
||||||
var statsStartDate = startDate ?? DateTime.MinValue;
|
var statsStartDate = startDate ?? DateTime.MinValue;
|
||||||
var statsEndDate = endDate ?? DateTime.Now;
|
var statsEndDate = endDate ?? DateTime.Now;
|
||||||
|
var parsedIndexers = new List<int>();
|
||||||
|
var parsedTags = new List<int>();
|
||||||
|
var indexerIds = _indexerFactory.All().Select(i => i.Id).ToList();
|
||||||
|
|
||||||
var indexerStats = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate);
|
if (tags.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
parsedTags.AddRange(tags.Split(',').Select(_tagService.GetTag).Select(t => t.Id));
|
||||||
|
|
||||||
|
indexerIds = indexerIds.Intersect(parsedTags.SelectMany(t => _indexerFactory.AllForTag(t).Select(i => i.Id))).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indexers.IsNotNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
parsedIndexers.AddRange(indexers.Split(',').Select(x => Convert.ToInt32(x)));
|
||||||
|
|
||||||
|
indexerIds = indexerIds.Intersect(parsedIndexers).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
var indexerStats = _indexerStatisticsService.IndexerStatistics(statsStartDate, statsEndDate, indexerIds);
|
||||||
|
|
||||||
var indexerResource = new IndexerStatsResource
|
var indexerResource = new IndexerStatsResource
|
||||||
{
|
{
|
||||||
|
Reference in New Issue
Block a user