parent
e357d17b18
commit
2fe8f3084c
|
@ -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,
|
||||
isEpisodesFetching,
|
||||
isEpisodesPopulated,
|
||||
|
@ -92,7 +94,8 @@ class History extends Component {
|
|||
alignMenu={align.RIGHT}
|
||||
selectedFilterKey={selectedFilterKey}
|
||||
filters={filters}
|
||||
customFilters={[]}
|
||||
customFilters={customFilters}
|
||||
filterModalConnectorComponent={HistoryFilterModal}
|
||||
onFilterSelect={onFilterSelect}
|
||||
/>
|
||||
</PageToolbarSection>
|
||||
|
@ -163,8 +166,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,
|
||||
isEpisodesFetching: PropTypes.bool.isRequired,
|
||||
isEpisodesPopulated: PropTypes.bool.isRequired,
|
||||
|
|
|
@ -6,6 +6,7 @@ import withCurrentPage from 'Components/withCurrentPage';
|
|||
import { clearEpisodes, fetchEpisodes } from 'Store/Actions/episodeActions';
|
||||
import { clearEpisodeFiles } from 'Store/Actions/episodeFileActions';
|
||||
import * as historyActions from 'Store/Actions/historyActions';
|
||||
import { createCustomFiltersSelector } from 'Store/Selectors/createClientSideCollectionSelector';
|
||||
import hasDifferentItems from 'Utilities/Object/hasDifferentItems';
|
||||
import selectUniqueIds from 'Utilities/Object/selectUniqueIds';
|
||||
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
|
||||
|
@ -15,11 +16,13 @@ function createMapStateToProps() {
|
|||
return createSelector(
|
||||
(state) => state.history,
|
||||
(state) => state.episodes,
|
||||
(history, episodes) => {
|
||||
createCustomFiltersSelector('history'),
|
||||
(history, episodes, customFilters) => {
|
||||
return {
|
||||
isEpisodesFetching: episodes.isFetching,
|
||||
isEpisodesPopulated: episodes.isPopulated,
|
||||
episodesError: episodes.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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -3,6 +3,7 @@ import CalendarAppState from './CalendarAppState';
|
|||
import CommandAppState from './CommandAppState';
|
||||
import EpisodeFilesAppState from './EpisodeFilesAppState';
|
||||
import EpisodesAppState from './EpisodesAppState';
|
||||
import HistoryAppState from './HistoryAppState';
|
||||
import ParseAppState from './ParseAppState';
|
||||
import QueueAppState from './QueueAppState';
|
||||
import RootFolderAppState from './RootFolderAppState';
|
||||
|
@ -48,6 +49,7 @@ interface AppState {
|
|||
commands: CommandAppState;
|
||||
episodeFiles: EpisodeFilesAppState;
|
||||
episodesSelection: EpisodesAppState;
|
||||
history: HistoryAppState;
|
||||
interactiveImport: InteractiveImportAppState;
|
||||
parse: ParseAppState;
|
||||
queue: QueueAppState;
|
||||
|
|
|
@ -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;
|
|
@ -6,6 +6,7 @@ import { filterBuilderTypes, filterBuilderValueTypes, icons } from 'Helpers/Prop
|
|||
import BoolFilterBuilderRowValue from './BoolFilterBuilderRowValue';
|
||||
import DateFilterBuilderRowValue from './DateFilterBuilderRowValue';
|
||||
import FilterBuilderRowValueConnector from './FilterBuilderRowValueConnector';
|
||||
import HistoryEventTypeFilterBuilderRowValue from './HistoryEventTypeFilterBuilderRowValue';
|
||||
import IndexerFilterBuilderRowValueConnector from './IndexerFilterBuilderRowValueConnector';
|
||||
import LanguageFilterBuilderRowValue from './LanguageFilterBuilderRowValue';
|
||||
import ProtocolFilterBuilderRowValue from './ProtocolFilterBuilderRowValue';
|
||||
|
@ -59,6 +60,9 @@ function getRowValueConnector(selectedFilterBuilderProp) {
|
|||
case filterBuilderValueTypes.DATE:
|
||||
return DateFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.HISTORY_EVENT_TYPE:
|
||||
return HistoryEventTypeFilterBuilderRowValue;
|
||||
|
||||
case filterBuilderValueTypes.INDEXER:
|
||||
return IndexerFilterBuilderRowValueConnector;
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import React from 'react';
|
||||
import FilterBuilderRowValue from './FilterBuilderRowValue';
|
||||
import FilterBuilderRowValueProps from './FilterBuilderRowValueProps';
|
||||
|
||||
const EVENT_TYPE_OPTIONS = [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Grabbed',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'Imported',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Failed',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: 'Deleted',
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: 'Renamed',
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
name: '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 LANGUAGE = 'language';
|
||||
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';
|
||||
|
@ -185,6 +185,33 @@ export const defaultState = {
|
|||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
filterBuilderProps: [
|
||||
{
|
||||
name: 'eventType',
|
||||
label: 'Event Type',
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.HISTORY_EVENT_TYPE
|
||||
},
|
||||
{
|
||||
name: 'seriesIds',
|
||||
label: 'Series',
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.SERIES
|
||||
},
|
||||
{
|
||||
name: 'quality',
|
||||
label: 'Quality',
|
||||
type: filterBuilderTypes.EQUAL,
|
||||
valueType: filterBuilderValueTypes.QUALITY
|
||||
},
|
||||
{
|
||||
name: 'languages',
|
||||
label: 'Languages',
|
||||
type: filterBuilderTypes.CONTAINS,
|
||||
valueType: filterBuilderValueTypes.LANGUAGE
|
||||
}
|
||||
]
|
||||
|
||||
};
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import Language from 'Language/Language';
|
||||
import { QualityModel } from 'Quality/Quality';
|
||||
import CustomFormat from './CustomFormat';
|
||||
|
||||
export type HistoryEventType =
|
||||
| 'grabbed'
|
||||
| 'seriesFolderImported'
|
||||
| 'downloadFolderImported'
|
||||
| 'downloadFailed'
|
||||
| 'episodeFileDeleted'
|
||||
| 'episodeFileRenamed'
|
||||
| 'downloadIgnored';
|
||||
|
||||
export default interface History {
|
||||
episodeId: number;
|
||||
seriesId: number;
|
||||
sourceTitle: string;
|
||||
languages: Language[];
|
||||
quality: QualityModel;
|
||||
customFormats: CustomFormat[];
|
||||
customFormatScore: number;
|
||||
qualityCutoffNotMet: boolean;
|
||||
date: string;
|
||||
downloadId: string;
|
||||
eventType: HistoryEventType;
|
||||
data: unknown;
|
||||
id: number;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.EnvironmentInfo;
|
||||
using NzbDrone.Core.Configuration;
|
||||
|
@ -30,7 +30,7 @@ namespace NzbDrone.Core.Analytics
|
|||
{
|
||||
get
|
||||
{
|
||||
var lastRecord = _historyService.Paged(new PagingSpec<EpisodeHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending });
|
||||
var lastRecord = _historyService.Paged(new PagingSpec<EpisodeHistory>() { Page = 0, PageSize = 1, SortKey = "date", SortDirection = SortDirection.Descending }, null, 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;
|
||||
|
||||
|
|
|
@ -19,6 +19,7 @@ namespace NzbDrone.Core.History
|
|||
List<EpisodeHistory> FindDownloadHistory(int idSeriesId, QualityModel quality);
|
||||
void DeleteForSeries(List<int> seriesIds);
|
||||
List<EpisodeHistory> Since(DateTime date, EpisodeHistoryEventType? eventType);
|
||||
PagingSpec<EpisodeHistory> GetPaged(PagingSpec<EpisodeHistory> pagingSpec, int[] languages, int[] qualities);
|
||||
}
|
||||
|
||||
public class HistoryRepository : BasicRepository<EpisodeHistory>, IHistoryRepository
|
||||
|
@ -101,18 +102,6 @@ namespace NzbDrone.Core.History
|
|||
Delete(c => seriesIds.Contains(c.SeriesId));
|
||||
}
|
||||
|
||||
protected override SqlBuilder PagedBuilder() => new SqlBuilder(_database.DatabaseType)
|
||||
.Join<EpisodeHistory, Series>((h, a) => h.SeriesId == a.Id)
|
||||
.Join<EpisodeHistory, Episode>((h, a) => h.EpisodeId == a.Id);
|
||||
|
||||
protected override IEnumerable<EpisodeHistory> PagedQuery(SqlBuilder builder) =>
|
||||
_database.QueryJoined<EpisodeHistory, Series, Episode>(builder, (history, series, episode) =>
|
||||
{
|
||||
history.Series = series;
|
||||
history.Episode = episode;
|
||||
return history;
|
||||
});
|
||||
|
||||
public List<EpisodeHistory> Since(DateTime date, EpisodeHistoryEventType? eventType)
|
||||
{
|
||||
var builder = Builder()
|
||||
|
@ -132,5 +121,76 @@ namespace NzbDrone.Core.History
|
|||
return history;
|
||||
}).OrderBy(h => h.Date).ToList();
|
||||
}
|
||||
|
||||
public PagingSpec<EpisodeHistory> GetPaged(PagingSpec<EpisodeHistory> pagingSpec, int[] languages, int[] qualities)
|
||||
{
|
||||
pagingSpec.Records = GetPagedRecords(PagedBuilder(pagingSpec, languages, qualities), pagingSpec, PagedQuery);
|
||||
|
||||
var countTemplate = $"SELECT COUNT(*) FROM (SELECT /**select**/ FROM \"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\" /**join**/ /**innerjoin**/ /**leftjoin**/ /**where**/ /**groupby**/ /**having**/) AS \"Inner\"";
|
||||
pagingSpec.TotalRecords = GetPagedRecordCount(PagedBuilder(pagingSpec, languages, qualities).Select(typeof(EpisodeHistory)), pagingSpec, countTemplate);
|
||||
|
||||
return pagingSpec;
|
||||
}
|
||||
|
||||
private SqlBuilder PagedBuilder(PagingSpec<EpisodeHistory> pagingSpec, int[] languages, int[] qualities)
|
||||
{
|
||||
var builder = Builder()
|
||||
.Join<EpisodeHistory, Series>((h, a) => h.SeriesId == a.Id)
|
||||
.Join<EpisodeHistory, Episode>((h, a) => h.EpisodeId == a.Id);
|
||||
|
||||
AddFilters(builder, pagingSpec);
|
||||
|
||||
if (languages is { Length: > 0 })
|
||||
{
|
||||
builder.Where($"({BuildLanguageWhereClause(languages)})");
|
||||
}
|
||||
|
||||
if (qualities is { Length: > 0 })
|
||||
{
|
||||
builder.Where($"({BuildQualityWhereClause(qualities)})");
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected override IEnumerable<EpisodeHistory> PagedQuery(SqlBuilder builder) =>
|
||||
_database.QueryJoined<EpisodeHistory, Series, Episode>(builder, (history, series, episode) =>
|
||||
{
|
||||
history.Series = series;
|
||||
history.Episode = episode;
|
||||
return history;
|
||||
});
|
||||
|
||||
private string BuildLanguageWhereClause(int[] languages)
|
||||
{
|
||||
var clauses = new List<string>();
|
||||
|
||||
foreach (var language in languages)
|
||||
{
|
||||
// There are 4 different types of values we should see:
|
||||
// - Not the last value in the array
|
||||
// - When it's the last value in the array and on different OSes
|
||||
// - When it was converted from a single language
|
||||
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[% {language},%]'");
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(13) || '%]'");
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[% {language}' || CHAR(10) || '%]'");
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Languages\" LIKE '[{language}]'");
|
||||
}
|
||||
|
||||
return $"({string.Join(" OR ", clauses)})";
|
||||
}
|
||||
|
||||
private string BuildQualityWhereClause(int[] qualities)
|
||||
{
|
||||
var clauses = new List<string>();
|
||||
|
||||
foreach (var quality in qualities)
|
||||
{
|
||||
clauses.Add($"\"{TableMapping.Mapper.TableNameMapping(typeof(EpisodeHistory))}\".\"Quality\" LIKE '%_quality_: {quality},%'");
|
||||
}
|
||||
|
||||
return $"({string.Join(" OR ", clauses)})";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@ namespace NzbDrone.Core.History
|
|||
{
|
||||
public interface IHistoryService
|
||||
{
|
||||
PagingSpec<EpisodeHistory> Paged(PagingSpec<EpisodeHistory> pagingSpec);
|
||||
PagingSpec<EpisodeHistory> Paged(PagingSpec<EpisodeHistory> pagingSpec, int[] languages, int[] qualities);
|
||||
EpisodeHistory MostRecentForEpisode(int episodeId);
|
||||
List<EpisodeHistory> FindByEpisodeId(int episodeId);
|
||||
EpisodeHistory MostRecentForDownloadId(string downloadId);
|
||||
|
@ -47,9 +47,9 @@ namespace NzbDrone.Core.History
|
|||
_logger = logger;
|
||||
}
|
||||
|
||||
public PagingSpec<EpisodeHistory> Paged(PagingSpec<EpisodeHistory> pagingSpec)
|
||||
public PagingSpec<EpisodeHistory> Paged(PagingSpec<EpisodeHistory> pagingSpec, int[] languages, int[] qualities)
|
||||
{
|
||||
return _historyRepository.GetPaged(pagingSpec);
|
||||
return _historyRepository.GetPaged(pagingSpec, languages, qualities);
|
||||
}
|
||||
|
||||
public EpisodeHistory MostRecentForEpisode(int episodeId)
|
||||
|
|
|
@ -62,7 +62,7 @@ namespace Sonarr.Api.V3.History
|
|||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, int? eventType, int? episodeId, string downloadId)
|
||||
public PagingResource<HistoryResource> GetHistory([FromQuery] PagingRequestResource paging, bool includeSeries, bool includeEpisode, int? eventType, int? episodeId, string downloadId, [FromQuery] int[] seriesIds = null, [FromQuery] int[] languages = null, [FromQuery] int[] quality = null)
|
||||
{
|
||||
var pagingResource = new PagingResource<HistoryResource>(paging);
|
||||
var pagingSpec = pagingResource.MapToPagingSpec<HistoryResource, EpisodeHistory>("date", SortDirection.Descending);
|
||||
|
@ -83,7 +83,17 @@ namespace Sonarr.Api.V3.History
|
|||
pagingSpec.FilterExpressions.Add(h => h.DownloadId == downloadId);
|
||||
}
|
||||
|
||||
return pagingSpec.ApplyToPage(_historyService.Paged, h => MapToResource(h, includeSeries, includeEpisode));
|
||||
if (seriesIds != null && seriesIds.Any())
|
||||
{
|
||||
pagingSpec.FilterExpressions.Add(h => seriesIds.Contains(h.SeriesId));
|
||||
}
|
||||
|
||||
if (seriesIds != null && seriesIds.Any())
|
||||
{
|
||||
pagingSpec.FilterExpressions.Add(h => seriesIds.Contains(h.SeriesId));
|
||||
}
|
||||
|
||||
return pagingSpec.ApplyToPage(h => _historyService.Paged(pagingSpec, languages, quality), h => MapToResource(h, includeSeries, includeEpisode));
|
||||
}
|
||||
|
||||
[HttpGet("since")]
|
||||
|
|
Loading…
Reference in New Issue