New: History custom filters

(cherry picked from commit 2fe8f3084c90688e6dd01d600796396e74f43ff9)

Closes #4213
Closes #4235
Closes #4236
pull/4269/head
Mark McDowall 12 months ago committed by Bogdan
parent efc6c69a0f
commit e408c6f055

@ -15,6 +15,7 @@ import TablePager from 'Components/Table/TablePager';
import { align, icons, kinds } from 'Helpers/Props';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import translate from 'Utilities/String/translate';
import HistoryFilterModal from './HistoryFilterModal';
import HistoryRowConnector from './HistoryRowConnector';
class History extends Component {
@ -52,6 +53,7 @@ class History extends Component {
columns,
selectedFilterKey,
filters,
customFilters,
totalRecords,
isArtistFetching,
isArtistPopulated,
@ -94,7 +96,8 @@ class History extends Component {
alignMenu={align.RIGHT}
selectedFilterKey={selectedFilterKey}
filters={filters}
customFilters={[]}
customFilters={customFilters}
filterModalConnectorComponent={HistoryFilterModal}
onFilterSelect={onFilterSelect}
/>
</PageToolbarSection>
@ -165,8 +168,9 @@ History.propTypes = {
error: 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,
isArtistFetching: PropTypes.bool.isRequired,
isArtistPopulated: PropTypes.bool.isRequired,

@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage';
import { clearAlbums, fetchAlbums } from 'Store/Actions/albumActions';
import * as historyActions from 'Store/Actions/historyActions';
import { clearTracks, fetchTracks } from 'Store/Actions/trackActions';
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
@ -17,7 +18,8 @@ function createMapStateToProps() {
(state) => state.artist,
(state) => state.albums,
(state) => state.tracks,
(history, artist, albums, tracks) => {
createCustomFiltersSelector('history'),
(history, artist, albums, tracks, customFilters) => {
return {
isArtistFetching: artist.isFetching,
isArtistPopulated: artist.isPopulated,
@ -27,6 +29,7 @@ function createMapStateToProps() {
isTracksFetching: tracks.isFetching,
isTracksPopulated: tracks.isPopulated,
tracksError: tracks.error,
customFilters,
...history
};
}

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

@ -1,5 +1,6 @@
import AlbumAppState from './AlbumAppState';
import ArtistAppState, { ArtistIndexAppState } from './ArtistAppState';
import HistoryAppState from './HistoryAppState';
import QueueAppState from './QueueAppState';
import SettingsAppState from './SettingsAppState';
import TagsAppState from './TagsAppState';
@ -42,6 +43,7 @@ interface AppState {
albums: AlbumAppState;
artist: ArtistAppState;
artistIndex: ArtistIndexAppState;
history: HistoryAppState;
queue: QueueAppState;
settings: SettingsAppState;
tags: TagsAppState;

@ -0,0 +1,10 @@
import AppSectionState, {
AppSectionFilterState,
} from 'App/State/AppSectionState';
import History from 'typings/History';
interface HistoryAppState
extends AppSectionState<History>,
AppSectionFilterState<History> {}
export default HistoryAppState;

@ -8,6 +8,7 @@ import ArtistStatusFilterBuilderRowValue from './ArtistStatusFilterBuilderRowVal
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
import MetadataProfileFilterBuilderRowValueConnector from './MetadataProfileFilterBuilderRowValueConnector';
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;

@ -0,0 +1,69 @@
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('TrackImported');
},
},
{
id: 4,
get name() {
return translate('DownloadFailed');
},
},
{
id: 7,
get name() {
return translate('ImportFailed');
},
},
{
id: 8,
get name() {
return translate('DownloadImported');
},
},
{
id: 5,
get name() {
return translate('Deleted');
},
},
{
id: 6,
get name() {
return translate('Renamed');
},
},
{
id: 9,
get name() {
return translate('Retagged');
},
},
{
id: 7,
get name() {
return translate('Ignored');
},
},
];
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 DATE = 'date';
export const DEFAULT = 'default';
export const HISTORY_EVENT_TYPE = 'historyEventType';
export const INDEXER = 'indexer';
export const METADATA_PROFILE = 'metadataProfile';
export const PROTOCOL = 'protocol';

@ -1,7 +1,7 @@
import React from 'react';
import { createAction } from 'redux-actions';
import Icon from 'Components/Icon';
import { filterTypes, icons, sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, icons, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import serverSideCollectionHandlers from 'Utilities/serverSideCollectionHandlers';
@ -219,6 +219,27 @@ export const defaultState = {
}
]
}
],
filterBuilderProps: [
{
name: 'eventType',
label: () => translate('EventType'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
},
{
name: 'artistIds',
label: () => translate('Artist'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.ARTIST
},
{
name: 'quality',
label: () => translate('Quality'),
type: filterBuilderTypes.EQUAL,
valueType: filterBuilderValueTypes.QUALITY
}
]
};

@ -0,0 +1,29 @@
import { QualityModel } from 'Quality/Quality';
import CustomFormat from './CustomFormat';
export type HistoryEventType =
| 'grabbed'
| 'artistFolderImported'
| 'trackFileImported'
| 'downloadFailed'
| 'trackFileDeleted'
| 'trackFileRenamed'
| 'albumImportIncomplete'
| 'downloadImported'
| 'trackFileRetagged'
| 'downloadIgnored';
export default interface History {
episodeId: number;
seriesId: number;
sourceTitle: string;
quality: QualityModel;
customFormats: CustomFormat[];
customFormatScore: number;
qualityCutoffNotMet: boolean;
date: string;
downloadId: string;
eventType: HistoryEventType;
data: unknown;
id: number;
}

@ -68,7 +68,7 @@ namespace Lidarr.Api.V1.History
[HttpGet]
[Produces("application/json")]
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeArtist, bool includeAlbum, bool includeTrack, int? eventType, int? albumId, string downloadId)
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeArtist, bool includeAlbum, bool includeTrack, int? eventType, int? albumId, string downloadId, [FromQuery] int[] artistIds = null, [FromQuery] int[] quality = null)
{
var pagingResource = new PagingResource<HistoryResource>(paging);
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EntityHistory>("date", SortDirection.Descending);
@ -89,7 +89,12 @@ namespace Lidarr.Api.V1.History
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
}
return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
if (artistIds != null && artistIds.Any())
{
pagingSpec.FilterExpressions.Add(h => artistIds.Contains(h.ArtistId));
}
return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, quality), h => MapToResource(h, includeArtist, includeAlbum, includeTrack));
}
[HttpGet("since")]

@ -30,7 +30,7 @@ namespace NzbDrone.Core.Analytics
{
get
{
var lastRecord = _historyService.Paged(new PagingSpec<EntityHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
var lastRecord = _historyService.Paged(new PagingSpec<EntityHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null);
var monthAgo = DateTime.UtcNow.AddMonths(-1);
return lastRecord.Records.Any(v => v.Date > monthAgo);

@ -407,7 +407,7 @@ namespace NzbDrone.Core.Datastore
return pagingSpec;
}
private void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
protected void AddFilters(SqlBuilder builder, PagingSpec<TModel> pagingSpec)
{
var filters = pagingSpec.FilterExpressions;

@ -18,6 +18,7 @@ namespace NzbDrone.Core.History
List<EntityHistory> FindDownloadHistory(int idArtistId, QualityModel quality);
void DeleteForArtists(List<int> artistIds);
List<EntityHistory> Since(DateTime date, EntityHistoryEventType? eventType);
PagingSpec<EntityHistory> GetPaged(PagingSpec<EntityHistory> pagingSpec, int[] qualities);
}
public class EntityHistoryRepository : BasicRepository<EntityHistory>, IHistoryRepository
@ -98,19 +99,6 @@ namespace NzbDrone.Core.History
Delete(c => artistIds.Contains(c.ArtistId));
}
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
.Join<EntityHistory, Artist>((h, a) => h.ArtistId == a.Id)
.Join<EntityHistory, Album>((h, a) => h.AlbumId == a.Id)
.LeftJoin<EntityHistory, Track>((h, t) => h.TrackId == t.Id);
protected override IEnumerable<EntityHistory> PagedQuery(SqlBuilder builder) =>
_database.QueryJoined<EntityHistory, Artist, Album, Track>(builder, (history, artist, album, track) =>
{
history.Artist = artist;
history.Album = album;
history.Track = track;
return history;
});
public List<EntityHistory> Since(DateTime date, EntityHistoryEventType? eventType)
{
var builder = Builder()
@ -130,5 +118,53 @@ namespace NzbDrone.Core.History
return history;
}).OrderBy(h => h.Date).ToList();
}
public PagingSpec<EntityHistory> GetPaged(PagingSpec<EntityHistory> pagingSpec, int[] qualities)
{
pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, qualities), pagingSpec, PagedQuery);
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(EntityHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, qualities).Select(typeof(EntityHistory)), pagingSpec, countTemplate);
return pagingSpec;
}
private SqlBuilder PagedBuilder(PagingSpec<EntityHistory> pagingSpec, int[] qualities)
{
var builder = Builder()
.Join<EntityHistory, Artist>((h, a) => h.ArtistId == a.Id)
.Join<EntityHistory, Album>((h, a) => h.AlbumId == a.Id)
.LeftJoin<EntityHistory, Track>((h, t) => h.TrackId == t.Id);
AddFilters(builder, pagingSpec);
if (qualities is { Length: > 0 })
{
builder.Where($"({BuildQualityWhereClause(qualities)})");
}
return builder;
}
protected override IEnumerable<EntityHistory> PagedQuery(SqlBuilder builder) =>
_database.QueryJoined<EntityHistory, Artist, Album, Track>(builder, (history, artist, album, track) =>
{
history.Artist = artist;
history.Album = album;
history.Track = track;
return history;
});
private string BuildQualityWhereClause(int[] qualities)
{
var clauses = new List<string>();
foreach (var quality in qualities)
{
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EntityHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'");
}
return $"({string.Join(" OR ", clauses)})";
}
}
}

@ -18,7 +18,7 @@ namespace NzbDrone.Core.History
{
public interface IHistoryService
{
PagingSpec<EntityHistory> Paged(PagingSpec<EntityHistory> pagingSpec);
PagingSpec<EntityHistory> Paged(PagingSpec<EntityHistory> pagingSpec, int[] qualities);
EntityHistory MostRecentForAlbum(int albumId);
EntityHistory MostRecentForDownloadId(string downloadId);
EntityHistory Get(int historyId);
@ -52,9 +52,9 @@ namespace NzbDrone.Core.History
_logger = logger;
}
public PagingSpec<EntityHistory> Paged(PagingSpec<EntityHistory> pagingSpec)
public PagingSpec<EntityHistory> Paged(PagingSpec<EntityHistory> pagingSpec, int[] qualities)
{
return _historyRepository.GetPaged(pagingSpec);
return _historyRepository.GetPaged(pagingSpec, qualities);
}
public EntityHistory MostRecentForAlbum(int albumId)

Loading…
Cancel
Save