mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: History custom filters
Co-authored-by: Mark McDowall <mark@mcdowall.ca>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
|
import { FilterBuilderProp } from './AppState';
|
||||||
|
|
||||||
export interface Error {
|
export interface Error {
|
||||||
responseJSON: {
|
responseJSON: {
|
||||||
@@ -20,6 +21,10 @@ export interface PagedAppSectionState {
|
|||||||
pageSize: number;
|
pageSize: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AppSectionFilterState<T> {
|
||||||
|
filterBuilderProps: FilterBuilderProp<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AppSectionSchemaState<T> {
|
export interface AppSectionSchemaState<T> {
|
||||||
isSchemaFetching: boolean;
|
isSchemaFetching: boolean;
|
||||||
isSchemaPopulated: boolean;
|
isSchemaPopulated: boolean;
|
||||||
|
@@ -1,8 +1,12 @@
|
|||||||
import AppSectionState from 'App/State/AppSectionState';
|
import AppSectionState, {
|
||||||
|
AppSectionFilterState,
|
||||||
|
} from 'App/State/AppSectionState';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
import History from 'typings/History';
|
import History from 'typings/History';
|
||||||
|
|
||||||
interface HistoryAppState extends AppSectionState<History> {
|
interface HistoryAppState
|
||||||
|
extends AppSectionState<History>,
|
||||||
|
AppSectionFilterState<History> {
|
||||||
pageSize: number;
|
pageSize: number;
|
||||||
columns: Column[];
|
columns: Column[];
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderR
|
|||||||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||||
|
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||||
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
|
import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue';
|
||||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||||
@@ -58,6 +59,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||||||
case filterBuilderValueTypes.DATE:
|
case filterBuilderValueTypes.DATE:
|
||||||
return DateFilterBuilderRowValue;
|
return DateFilterBuilderRowValue;
|
||||||
|
|
||||||
|
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||||
|
return HistoryEventTypeFilterBuilderRowValue;
|
||||||
|
|
||||||
case filterBuilderValueTypes.INDEXER:
|
case filterBuilderValueTypes.INDEXER:
|
||||||
return IndexerFilterBuilderRowValueConnector;
|
return IndexerFilterBuilderRowValueConnector;
|
||||||
|
|
||||||
|
@@ -0,0 +1,16 @@
|
|||||||
|
import { FilterBuilderProp } from 'App/State/AppState';
|
||||||
|
|
||||||
|
interface FilterBuilderRowOnChangeProps {
|
||||||
|
name: string;
|
||||||
|
value: unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FilterBuilderRowValueProps {
|
||||||
|
filterType?: string;
|
||||||
|
filterValue: string | number | object | string[] | number[] | object[];
|
||||||
|
selectedFilterBuilderProp: FilterBuilderProp<unknown>;
|
||||||
|
sectionItem: unknown[];
|
||||||
|
onChange: (payload: FilterBuilderRowOnChangeProps) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FilterBuilderRowValueProps;
|
@@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import translate from 'Utilities/String/translate';
|
||||||
|
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||||
|
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||||
|
|
||||||
|
const EVENT_TYPE_OPTIONS = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
get name() {
|
||||||
|
return translate('Grabbed');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
get name() {
|
||||||
|
return translate('IndexerRss');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
get name() {
|
||||||
|
return translate('IndexerQuery');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
get name() {
|
||||||
|
return translate('IndexerAuth');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function HistoryEventTypeFilterBuilderRowValue(
|
||||||
|
props: FilterBuilderRowValueProps
|
||||||
|
) {
|
||||||
|
return <FilterBuilderRowValue {...props} tagList={EVENT_TYPE_OPTIONS} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default HistoryEventTypeFilterBuilderRowValue;
|
@@ -2,6 +2,7 @@ export const BOOL = 'bool';
|
|||||||
export const BYTES = 'bytes';
|
export const BYTES = 'bytes';
|
||||||
export const DATE = 'date';
|
export const DATE = 'date';
|
||||||
export const DEFAULT = 'default';
|
export const DEFAULT = 'default';
|
||||||
|
export const HISTORY_EVENT_TYPE = 'historyEventType';
|
||||||
export const INDEXER = 'indexer';
|
export const INDEXER = 'indexer';
|
||||||
export const PROTOCOL = 'protocol';
|
export const PROTOCOL = 'protocol';
|
||||||
export const PRIVACY = 'privacy';
|
export const PRIVACY = 'privacy';
|
||||||
|
@@ -17,6 +17,7 @@ import { align, icons, kinds } from 'Helpers/Props';
|
|||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import HistoryOptionsConnector from './HistoryOptionsConnector';
|
import HistoryOptionsConnector from './HistoryOptionsConnector';
|
||||||
import HistoryRowConnector from './HistoryRowConnector';
|
import HistoryRowConnector from './HistoryRowConnector';
|
||||||
|
import HistoryFilterModal from './HistoryFilterModal';
|
||||||
|
|
||||||
class History extends Component {
|
class History extends Component {
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ class History extends Component {
|
|||||||
columns,
|
columns,
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters,
|
||||||
|
customFilters,
|
||||||
totalRecords,
|
totalRecords,
|
||||||
onFilterSelect,
|
onFilterSelect,
|
||||||
onFirstPagePress,
|
onFirstPagePress,
|
||||||
@@ -108,7 +110,8 @@ class History extends Component {
|
|||||||
alignMenu={align.RIGHT}
|
alignMenu={align.RIGHT}
|
||||||
selectedFilterKey={selectedFilterKey}
|
selectedFilterKey={selectedFilterKey}
|
||||||
filters={filters}
|
filters={filters}
|
||||||
customFilters={[]}
|
customFilters={customFilters}
|
||||||
|
filterModalConnectorComponent={HistoryFilterModal}
|
||||||
onFilterSelect={onFilterSelect}
|
onFilterSelect={onFilterSelect}
|
||||||
/>
|
/>
|
||||||
</PageToolbarSection>
|
</PageToolbarSection>
|
||||||
@@ -193,8 +196,9 @@ History.propTypes = {
|
|||||||
indexersError: PropTypes.object,
|
indexersError: PropTypes.object,
|
||||||
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
items: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
columns: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
selectedFilterKey: PropTypes.string.isRequired,
|
selectedFilterKey: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
|
||||||
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
filters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
|
customFilters: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||||
totalRecords: PropTypes.number,
|
totalRecords: PropTypes.number,
|
||||||
onFilterSelect: PropTypes.func.isRequired,
|
onFilterSelect: PropTypes.func.isRequired,
|
||||||
onFirstPagePress: PropTypes.func.isRequired,
|
onFirstPagePress: PropTypes.func.isRequired,
|
||||||
|
@@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames';
|
|||||||
import withCurrentPage from 'Components/withCurrentPage';
|
import withCurrentPage from 'Components/withCurrentPage';
|
||||||
import { executeCommand } from 'Store/Actions/commandActions';
|
import { executeCommand } from 'Store/Actions/commandActions';
|
||||||
import * as historyActions from 'Store/Actions/historyActions';
|
import * as historyActions from 'Store/Actions/historyActions';
|
||||||
|
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||||
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
|
||||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||||
import History from './History';
|
import History from './History';
|
||||||
@@ -14,13 +15,15 @@ function createMapStateToProps() {
|
|||||||
return createSelector(
|
return createSelector(
|
||||||
(state) => state.history,
|
(state) => state.history,
|
||||||
(state) => state.indexers,
|
(state) => state.indexers,
|
||||||
|
createCustomFiltersSelector('history'),
|
||||||
createCommandExecutingSelector(commandNames.CLEAR_HISTORY),
|
createCommandExecutingSelector(commandNames.CLEAR_HISTORY),
|
||||||
(history, indexers, isHistoryClearing) => {
|
(history, indexers, customFilters, isHistoryClearing) => {
|
||||||
return {
|
return {
|
||||||
isIndexersFetching: indexers.isFetching,
|
isIndexersFetching: indexers.isFetching,
|
||||||
isIndexersPopulated: indexers.isPopulated,
|
isIndexersPopulated: indexers.isPopulated,
|
||||||
indexersError: indexers.error,
|
indexersError: indexers.error,
|
||||||
isHistoryClearing,
|
isHistoryClearing,
|
||||||
|
customFilters,
|
||||||
...history
|
...history
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
54
frontend/src/History/HistoryFilterModal.tsx
Normal file
54
frontend/src/History/HistoryFilterModal.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
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 { setHistoryFilter } from 'Store/Actions/historyActions';
|
||||||
|
|
||||||
|
function createHistorySelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.items,
|
||||||
|
(queueItems) => {
|
||||||
|
return queueItems;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFilterBuilderPropsSelector() {
|
||||||
|
return createSelector(
|
||||||
|
(state: AppState) => state.history.filterBuilderProps,
|
||||||
|
(filterBuilderProps) => {
|
||||||
|
return filterBuilderProps;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HistoryFilterModalProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HistoryFilterModal(props: HistoryFilterModalProps) {
|
||||||
|
const sectionItems = useSelector(createHistorySelector());
|
||||||
|
const filterBuilderProps = useSelector(createFilterBuilderPropsSelector());
|
||||||
|
const customFilterType = 'history';
|
||||||
|
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
|
const dispatchSetFilter = useCallback(
|
||||||
|
(payload: unknown) => {
|
||||||
|
dispatch(setHistoryFilter(payload));
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FilterModal
|
||||||
|
// TODO: Don't spread all the props
|
||||||
|
{...props}
|
||||||
|
sectionItems={sectionItems}
|
||||||
|
filterBuilderProps={filterBuilderProps}
|
||||||
|
customFilterType={customFilterType}
|
||||||
|
dispatchSetFilter={dispatchSetFilter}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState';
|
|||||||
import { set, updateServerSideCollection } from '../baseActions';
|
import { set, updateServerSideCollection } from '../baseActions';
|
||||||
|
|
||||||
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) {
|
||||||
|
const [baseSection] = section.split('.');
|
||||||
|
|
||||||
return function(getState, payload, dispatch) {
|
return function(getState, payload, dispatch) {
|
||||||
dispatch(set({ section, isFetching: true }));
|
dispatch(set({ section, isFetching: true }));
|
||||||
|
|
||||||
@@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
selectedFilterKey,
|
selectedFilterKey,
|
||||||
filters,
|
filters
|
||||||
customFilters
|
|
||||||
} = sectionState;
|
} = sectionState;
|
||||||
|
|
||||||
|
const customFilters = getState().customFilters.items.filter((customFilter) => {
|
||||||
|
return customFilter.type === section || customFilter.type === baseSection;
|
||||||
|
});
|
||||||
|
|
||||||
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters);
|
||||||
|
|
||||||
selectedFilters.forEach((filter) => {
|
selectedFilters.forEach((filter) => {
|
||||||
@@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter
|
|||||||
|
|
||||||
const promise = createAjaxRequest({
|
const promise = createAjaxRequest({
|
||||||
url,
|
url,
|
||||||
data
|
data,
|
||||||
|
traditional: true
|
||||||
}).request;
|
}).request;
|
||||||
|
|
||||||
promise.done((response) => {
|
promise.done((response) => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { createAction } from 'redux-actions';
|
import { createAction } from 'redux-actions';
|
||||||
import { filterTypes, sortDirections } from 'Helpers/Props';
|
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } 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 serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
|
||||||
@@ -159,6 +159,27 @@ export const defaultState = {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
|
||||||
|
filterBuilderProps: [
|
||||||
|
{
|
||||||
|
name: 'eventType',
|
||||||
|
label: () => translate('EventType'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'indexerIds',
|
||||||
|
label: () => translate('Indexer'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.INDEXER
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'successful',
|
||||||
|
label: () => translate('Successful'),
|
||||||
|
type: filterBuilderTypes.EQUAL,
|
||||||
|
valueType: filterBuilderValueTypes.BOOL
|
||||||
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
};
|
};
|
||||||
|
@@ -74,8 +74,9 @@ export const defaultState = {
|
|||||||
valueType: filterBuilderValueTypes.TAG
|
valueType: filterBuilderValueTypes.TAG
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
selectedFilterKey: 'all',
|
|
||||||
customFilters: []
|
selectedFilterKey: 'all'
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const persistState = [
|
export const persistState = [
|
||||||
|
@@ -22,7 +22,7 @@ namespace Prowlarr.Api.V1.History
|
|||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Produces("application/json")]
|
[Produces("application/json")]
|
||||||
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[] eventTypes, bool? successful, string downloadId)
|
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[] eventTypes, bool? successful, string downloadId, [FromQuery] int[] indexerIds = null)
|
||||||
{
|
{
|
||||||
var pagingResource = new PagingResource<HistoryResource>(paging);
|
var pagingResource = new PagingResource<HistoryResource>(paging);
|
||||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending);
|
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, NzbDrone.Core.History.History>("date", SortDirection.Descending);
|
||||||
@@ -44,7 +44,12 @@ namespace Prowlarr.Api.V1.History
|
|||||||
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return pagingSpec.ApplyToPage(_historyService.Paged, MapToResource);
|
if (indexerIds != null && indexerIds.Any())
|
||||||
|
{
|
||||||
|
pagingSpec.FilterExpressions.Add(h => indexerIds.Contains(h.IndexerId));
|
||||||
|
}
|
||||||
|
|
||||||
|
return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec), MapToResource);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("since")]
|
[HttpGet("since")]
|
||||||
|
Reference in New Issue
Block a user