diff --git a/frontend/src/App/State/AppSectionState.ts b/frontend/src/App/State/AppSectionState.ts index d511963fc..cabc39b1c 100644 --- a/frontend/src/App/State/AppSectionState.ts +++ b/frontend/src/App/State/AppSectionState.ts @@ -1,4 +1,5 @@ import SortDirection from 'Helpers/Props/SortDirection'; +import { FilterBuilderProp } from './AppState'; export interface Error { responseJSON: { @@ -20,6 +21,10 @@ export interface PagedAppSectionState { pageSize: number; } +export interface AppSectionFilterState { + filterBuilderProps: FilterBuilderProp[]; +} + export interface AppSectionSchemaState { isSchemaFetching: boolean; isSchemaPopulated: boolean; diff --git a/frontend/src/App/State/HistoryAppState.ts b/frontend/src/App/State/HistoryAppState.ts index 357ed29f0..3bb0e85f5 100644 --- a/frontend/src/App/State/HistoryAppState.ts +++ b/frontend/src/App/State/HistoryAppState.ts @@ -1,8 +1,12 @@ -import AppSectionState from 'App/State/AppSectionState'; +import AppSectionState, { + AppSectionFilterState, +} from 'App/State/AppSectionState'; import Column from 'Components/Table/Column'; import History from 'typings/History'; -interface HistoryAppState extends AppSectionState { +interface HistoryAppState + extends AppSectionState, + AppSectionFilterState { pageSize: number; columns: Column[]; } diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js index ec676f87c..8899eaf5b 100644 --- a/frontend/src/Components/Filter/Builder/FilterBuilderRow.js +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRow.js @@ -7,6 +7,7 @@ import AppProfileFilterBuilderRowValueConnector from './AppProfileFilterBuilderR import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue'; import DateFilterBuilderRowValue from './DateFilterBuilderRowValue'; import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector'; +import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue'; import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector'; import PrivacyFilterBuilderRowValue from './PrivacyFilterBuilderRowValue'; import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue'; @@ -58,6 +59,9 @@ function getRowValueConnector(selectedFilterBuilderProp) { case filterBuilderValueTypes.DATE: return DateFilterBuilderRowValue; + case filterBuilderValueTypes.HISTORY_EVENT_TYPE: + return HistoryEventTypeFilterBuilderRowValue; + case filterBuilderValueTypes.INDEXER: return IndexerFilterBuilderRowValueConnector; diff --git a/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts new file mode 100644 index 000000000..5bf9e5785 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/FilterBuilderRowValueProps.ts @@ -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; + sectionItem: unknown[]; + onChange: (payload: FilterBuilderRowOnChangeProps) => void; +} + +export default FilterBuilderRowValueProps; diff --git a/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx new file mode 100644 index 000000000..03c5f7227 --- /dev/null +++ b/frontend/src/Components/Filter/Builder/HistoryEventTypeFilterBuilderRowValue.tsx @@ -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 ; +} + +export default HistoryEventTypeFilterBuilderRowValue; diff --git a/frontend/src/Helpers/Props/filterBuilderValueTypes.js b/frontend/src/Helpers/Props/filterBuilderValueTypes.js index 7fed535f2..19c8ccd9c 100644 --- a/frontend/src/Helpers/Props/filterBuilderValueTypes.js +++ b/frontend/src/Helpers/Props/filterBuilderValueTypes.js @@ -2,6 +2,7 @@ export const BOOL = 'bool'; export const BYTES = 'bytes'; export const DATE = 'date'; export const DEFAULT = 'default'; +export const HISTORY_EVENT_TYPE = 'historyEventType'; export const INDEXER = 'indexer'; export const PROTOCOL = 'protocol'; export const PRIVACY = 'privacy'; diff --git a/frontend/src/History/History.js b/frontend/src/History/History.js index c26a8e7ae..a8392d95d 100644 --- a/frontend/src/History/History.js +++ b/frontend/src/History/History.js @@ -17,6 +17,7 @@ import { align, icons, kinds } from 'Helpers/Props'; import translate from 'Utilities/String/translate'; import HistoryOptionsConnector from './HistoryOptionsConnector'; import HistoryRowConnector from './HistoryRowConnector'; +import HistoryFilterModal from './HistoryFilterModal'; class History extends Component { @@ -63,6 +64,7 @@ class History extends Component { columns, selectedFilterKey, filters, + customFilters, totalRecords, onFilterSelect, onFirstPagePress, @@ -108,7 +110,8 @@ class History extends Component { alignMenu={align.RIGHT} selectedFilterKey={selectedFilterKey} filters={filters} - customFilters={[]} + customFilters={customFilters} + filterModalConnectorComponent={HistoryFilterModal} onFilterSelect={onFilterSelect} /> @@ -193,8 +196,9 @@ History.propTypes = { indexersError: PropTypes.object, items: 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, + customFilters: PropTypes.arrayOf(PropTypes.object).isRequired, totalRecords: PropTypes.number, onFilterSelect: PropTypes.func.isRequired, onFirstPagePress: PropTypes.func.isRequired, diff --git a/frontend/src/History/HistoryConnector.js b/frontend/src/History/HistoryConnector.js index cd634ca11..9b29b7d6e 100644 --- a/frontend/src/History/HistoryConnector.js +++ b/frontend/src/History/HistoryConnector.js @@ -6,6 +6,7 @@ import * as commandNames from 'Commands/commandNames'; import withCurrentPage from 'Components/withCurrentPage'; import { executeCommand } from 'Store/Actions/commandActions'; import * as historyActions from 'Store/Actions/historyActions'; +import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector'; import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector'; import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator'; import History from './History'; @@ -14,13 +15,15 @@ function createMapStateToProps() { return createSelector( (state) => state.history, (state) => state.indexers, + createCustomFiltersSelector('history'), createCommandExecutingSelector(commandNames.CLEAR_HISTORY), - (history, indexers, isHistoryClearing) => { + (history, indexers, customFilters, isHistoryClearing) => { return { isIndexersFetching: indexers.isFetching, isIndexersPopulated: indexers.isPopulated, indexersError: indexers.error, isHistoryClearing, + customFilters, ...history }; } diff --git a/frontend/src/History/HistoryFilterModal.tsx b/frontend/src/History/HistoryFilterModal.tsx new file mode 100644 index 000000000..f4ad2e57c --- /dev/null +++ b/frontend/src/History/HistoryFilterModal.tsx @@ -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 ( + + ); +} diff --git a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js index a80ee1e45..f5ef10a4d 100644 --- a/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js +++ b/frontend/src/Store/Actions/Creators/createFetchServerSideCollectionHandler.js @@ -6,6 +6,8 @@ import getSectionState from 'Utilities/State/getSectionState'; import { set, updateServerSideCollection } from '../baseActions'; function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter) { + const [baseSection] = section.split('.'); + return function(getState, payload, dispatch) { dispatch(set({ section, isFetching: true })); @@ -25,10 +27,13 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const { selectedFilterKey, - filters, - customFilters + filters } = sectionState; + const customFilters = getState().customFilters.items.filter((customFilter) => { + return customFilter.type === section || customFilter.type === baseSection; + }); + const selectedFilters = findSelectedFilters(selectedFilterKey, filters, customFilters); selectedFilters.forEach((filter) => { @@ -37,7 +42,8 @@ function createFetchServerSideCollectionHandler(section, url, fetchDataAugmenter const promise = createAjaxRequest({ url, - data + data, + traditional: true }).request; promise.done((response) => { diff --git a/frontend/src/Store/Actions/historyActions.js b/frontend/src/Store/Actions/historyActions.js index 2e133f61e..95ea992b1 100644 --- a/frontend/src/Store/Actions/historyActions.js +++ b/frontend/src/Store/Actions/historyActions.js @@ -1,5 +1,5 @@ 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 createAjaxRequest from 'Utilities/createAjaxRequest'; 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 + } ] }; diff --git a/frontend/src/Store/Actions/indexerStatsActions.js b/frontend/src/Store/Actions/indexerStatsActions.js index fe13c6db4..06c9586b5 100644 --- a/frontend/src/Store/Actions/indexerStatsActions.js +++ b/frontend/src/Store/Actions/indexerStatsActions.js @@ -74,8 +74,9 @@ export const defaultState = { valueType: filterBuilderValueTypes.TAG } ], - selectedFilterKey: 'all', - customFilters: [] + + selectedFilterKey: 'all' + }; export const persistState = [ diff --git a/src/Prowlarr.Api.V1/History/HistoryController.cs b/src/Prowlarr.Api.V1/History/HistoryController.cs index d5b2b7545..19c29834b 100644 --- a/src/Prowlarr.Api.V1/History/HistoryController.cs +++ b/src/Prowlarr.Api.V1/History/HistoryController.cs @@ -22,7 +22,7 @@ namespace Prowlarr.Api.V1.History [HttpGet] [Produces("application/json")] - public PagingResource GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[] eventTypes, bool? successful, string downloadId) + public PagingResource GetHistory([FromQuery] PagingRequestResource paging, [FromQuery(Name = "eventType")] int[] eventTypes, bool? successful, string downloadId, [FromQuery] int[] indexerIds = null) { var pagingResource = new PagingResource(paging); var pagingSpec = pagingResource.MapToPagingSpec("date", SortDirection.Descending); @@ -44,7 +44,12 @@ namespace Prowlarr.Api.V1.History 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")]