parent
5d7c94f8e9
commit
5342416659
@ -1,14 +0,0 @@
|
||||
import AppSectionState, {
|
||||
AppSectionFilterState,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState,
|
||||
} from 'App/State/AppSectionState';
|
||||
import LogEvent from 'typings/LogEvent';
|
||||
|
||||
interface LogsAppState
|
||||
extends AppSectionState<LogEvent>,
|
||||
AppSectionFilterState<LogEvent>,
|
||||
PagedAppSectionState,
|
||||
TableAppSectionState {}
|
||||
|
||||
export default LogsAppState;
|
@ -0,0 +1,36 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import { create } from 'zustand';
|
||||
|
||||
interface PageStore {
|
||||
events: number;
|
||||
}
|
||||
|
||||
const pageStore = create<PageStore>(() => ({
|
||||
events: 1,
|
||||
}));
|
||||
|
||||
const usePage = (kind: keyof PageStore) => {
|
||||
const { action } = useHistory();
|
||||
|
||||
const goToPage = (page: number) => {
|
||||
pageStore.setState({ [kind]: page });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (action === 'POP') {
|
||||
pageStore.setState({ [kind]: 1 });
|
||||
}
|
||||
}, [action, kind]);
|
||||
|
||||
return {
|
||||
page: pageStore((state) => state[kind]),
|
||||
goToPage,
|
||||
};
|
||||
};
|
||||
|
||||
export default usePage;
|
||||
|
||||
export const resetPage = (kind: keyof PageStore) => {
|
||||
pageStore.setState({ [kind]: 1 });
|
||||
};
|
@ -0,0 +1,81 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMemo } from 'react';
|
||||
import { PropertyFilter } from 'App/State/AppState';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import fetchJson from 'Utilities/Fetch/fetchJson';
|
||||
import getQueryPath from 'Utilities/Fetch/getQueryPath';
|
||||
import getQueryString from 'Utilities/Fetch/getQueryString';
|
||||
import { QueryOptions } from './useApiQuery';
|
||||
|
||||
interface PagedQueryOptions<T> extends QueryOptions<PagedQueryResponse<T>> {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey?: string;
|
||||
sortDirection?: SortDirection;
|
||||
filters?: PropertyFilter[];
|
||||
}
|
||||
|
||||
interface PagedQueryResponse<T> {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
sortKey: string;
|
||||
sortDirection: string;
|
||||
totalRecords: number;
|
||||
totalPages: number;
|
||||
records: T[];
|
||||
}
|
||||
|
||||
const usePagedApiQuery = <T>(options: PagedQueryOptions<T>) => {
|
||||
const requestOptions = useMemo(() => {
|
||||
const {
|
||||
path,
|
||||
page,
|
||||
pageSize,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
filters,
|
||||
queryParams,
|
||||
queryOptions,
|
||||
...otherOptions
|
||||
} = options;
|
||||
|
||||
return {
|
||||
...otherOptions,
|
||||
path:
|
||||
getQueryPath(path) +
|
||||
getQueryString({
|
||||
...queryParams,
|
||||
page,
|
||||
pageSize,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
filters,
|
||||
}),
|
||||
headers: {
|
||||
...options.headers,
|
||||
'X-Api-Key': window.Sonarr.apiKey,
|
||||
},
|
||||
};
|
||||
}, [options]);
|
||||
|
||||
return useQuery({
|
||||
...options.queryOptions,
|
||||
queryKey: [requestOptions.path],
|
||||
queryFn: async ({ signal }) => {
|
||||
const response = await fetchJson<PagedQueryResponse<T>, unknown>({
|
||||
...requestOptions,
|
||||
signal,
|
||||
});
|
||||
|
||||
return {
|
||||
...response,
|
||||
totalPages: Math.max(
|
||||
Math.ceil(response.totalRecords / options.pageSize),
|
||||
1
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
export default usePagedApiQuery;
|
@ -0,0 +1,85 @@
|
||||
import Column from 'Components/Table/Column';
|
||||
import { createPersist, mergeColumns } from 'Helpers/createPersist';
|
||||
import { SortDirection } from 'Helpers/Props/sortDirections';
|
||||
import translate from 'Utilities/String/translate';
|
||||
|
||||
export interface EventOptions {
|
||||
pageSize: number;
|
||||
selectedFilterKey: string | number;
|
||||
sortKey: string;
|
||||
sortDirection: SortDirection;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
const eventOptionsStore = createPersist<EventOptions>(
|
||||
'event_options',
|
||||
() => {
|
||||
return {
|
||||
pageSize: 50,
|
||||
selectedFilterKey: 'all',
|
||||
sortKey: 'time',
|
||||
sortDirection: 'descending',
|
||||
columns: [
|
||||
{
|
||||
name: 'level',
|
||||
label: '',
|
||||
columnLabel: () => translate('Level'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
{
|
||||
name: 'time',
|
||||
label: () => translate('Time'),
|
||||
isSortable: true,
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
{
|
||||
name: 'logger',
|
||||
label: () => translate('Component'),
|
||||
isSortable: false,
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
label: () => translate('Message'),
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
{
|
||||
name: 'actions',
|
||||
label: '',
|
||||
columnLabel: () => translate('Actions'),
|
||||
isVisible: true,
|
||||
isModifiable: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
{
|
||||
merge: mergeColumns,
|
||||
}
|
||||
);
|
||||
|
||||
export const useEventOptions = () => {
|
||||
return eventOptionsStore((state) => state);
|
||||
};
|
||||
|
||||
export const setEventOptions = (options: Partial<EventOptions>) => {
|
||||
eventOptionsStore.setState((state) => ({
|
||||
...state,
|
||||
...options,
|
||||
}));
|
||||
};
|
||||
|
||||
export const setEventOption = <K extends keyof EventOptions>(
|
||||
key: K,
|
||||
value: EventOptions[K]
|
||||
) => {
|
||||
eventOptionsStore.setState((state) => ({
|
||||
...state,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
@ -0,0 +1,92 @@
|
||||
import { keepPreviousData } from '@tanstack/react-query';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { Filter } from 'App/State/AppState';
|
||||
import usePage from 'Helpers/Hooks/usePage';
|
||||
import usePagedApiQuery from 'Helpers/Hooks/usePagedApiQuery';
|
||||
import LogEvent from 'typings/LogEvent';
|
||||
import translate from 'Utilities/String/translate';
|
||||
import { useEventOptions } from './eventOptionsStore';
|
||||
|
||||
export const FILTERS: Filter[] = [
|
||||
{
|
||||
key: 'all',
|
||||
label: () => translate('All'),
|
||||
filters: [],
|
||||
},
|
||||
{
|
||||
key: 'info',
|
||||
label: () => translate('Info'),
|
||||
filters: [
|
||||
{
|
||||
key: 'level',
|
||||
value: 'info',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'warn',
|
||||
label: () => translate('Warn'),
|
||||
filters: [
|
||||
{
|
||||
key: 'level',
|
||||
value: 'warn',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'error',
|
||||
label: () => translate('Error'),
|
||||
filters: [
|
||||
{
|
||||
key: 'level',
|
||||
value: 'error',
|
||||
type: 'equal',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const useEvents = () => {
|
||||
const { page, goToPage } = usePage('events');
|
||||
const { pageSize, selectedFilterKey, sortKey, sortDirection } =
|
||||
useEventOptions();
|
||||
|
||||
const filters = useMemo(() => {
|
||||
return FILTERS.find((f) => f.key === selectedFilterKey)?.filters;
|
||||
}, [selectedFilterKey]);
|
||||
|
||||
const { refetch, ...query } = usePagedApiQuery<LogEvent>({
|
||||
path: '/log',
|
||||
page,
|
||||
pageSize,
|
||||
filters,
|
||||
sortKey,
|
||||
sortDirection,
|
||||
queryOptions: {
|
||||
placeholderData: keepPreviousData,
|
||||
},
|
||||
});
|
||||
|
||||
const handleGoToPage = useCallback(
|
||||
(page: number) => {
|
||||
goToPage(page);
|
||||
refetch();
|
||||
},
|
||||
[goToPage, refetch]
|
||||
);
|
||||
|
||||
return {
|
||||
...query,
|
||||
goToPage: handleGoToPage,
|
||||
page,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEvents;
|
||||
|
||||
export const useFilters = () => {
|
||||
return FILTERS;
|
||||
};
|
@ -0,0 +1,7 @@
|
||||
import { apiRoot, urlBase } from 'Utilities/Fetch/fetchJson';
|
||||
|
||||
const getQueryPath = (path: string) => {
|
||||
return urlBase + apiRoot + path;
|
||||
};
|
||||
|
||||
export default getQueryPath;
|
@ -0,0 +1,37 @@
|
||||
import { PropertyFilter } from 'App/State/AppState';
|
||||
|
||||
export interface QueryParams {
|
||||
[key: string]: string | number | boolean | PropertyFilter[] | undefined;
|
||||
}
|
||||
|
||||
const getQueryString = (queryParams?: QueryParams) => {
|
||||
if (!queryParams) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const filteredParams = Object.keys(queryParams).reduce<
|
||||
Record<string, string>
|
||||
>((acc, key) => {
|
||||
const value = queryParams[key];
|
||||
|
||||
if (value == null) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((filter) => {
|
||||
acc[filter.key] = String(filter.value);
|
||||
});
|
||||
} else {
|
||||
acc[key] = String(value);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const paramsString = new URLSearchParams(filteredParams).toString();
|
||||
|
||||
return `?${paramsString}`;
|
||||
};
|
||||
|
||||
export default getQueryString;
|
@ -0,0 +1,78 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NzbDrone.Common.Extensions;
|
||||
using NzbDrone.Core.Configuration;
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using Sonarr.Http;
|
||||
using Sonarr.Http.Extensions;
|
||||
|
||||
namespace Sonarr.Api.V5.Logs
|
||||
{
|
||||
[V5ApiController]
|
||||
public class LogController : Controller
|
||||
{
|
||||
private readonly ILogService _logService;
|
||||
private readonly IConfigFileProvider _configFileProvider;
|
||||
|
||||
public LogController(ILogService logService, IConfigFileProvider configFileProvider)
|
||||
{
|
||||
_logService = logService;
|
||||
_configFileProvider = configFileProvider;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
[Produces("application/json")]
|
||||
public PagingResource<LogResource> GetLogs([FromQuery] PagingRequestResource paging, string? level)
|
||||
{
|
||||
if (!_configFileProvider.LogDbEnabled)
|
||||
{
|
||||
return new PagingResource<LogResource>();
|
||||
}
|
||||
|
||||
var pagingResource = new PagingResource<LogResource>(paging);
|
||||
var pageSpec = pagingResource.MapToPagingSpec<LogResource, Log>(new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"id",
|
||||
"time"
|
||||
});
|
||||
|
||||
if (pageSpec.SortKey == "time")
|
||||
{
|
||||
pageSpec.SortKey = "id";
|
||||
}
|
||||
|
||||
if (level.IsNotNullOrWhiteSpace())
|
||||
{
|
||||
switch (level)
|
||||
{
|
||||
case "fatal":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal");
|
||||
break;
|
||||
case "error":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error");
|
||||
break;
|
||||
case "warn":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn");
|
||||
break;
|
||||
case "info":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info");
|
||||
break;
|
||||
case "debug":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug");
|
||||
break;
|
||||
case "trace":
|
||||
pageSpec.FilterExpressions.Add(h => h.Level == "Fatal" || h.Level == "Error" || h.Level == "Warn" || h.Level == "Info" || h.Level == "Debug" || h.Level == "Trace");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var response = pageSpec.ApplyToPage(_logService.Paged, LogResourceMapper.ToResource);
|
||||
|
||||
if (pageSpec.SortKey == "id")
|
||||
{
|
||||
response.SortKey = "time";
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
using NzbDrone.Core.Instrumentation;
|
||||
using Sonarr.Http.REST;
|
||||
|
||||
namespace Sonarr.Api.V5.Logs
|
||||
{
|
||||
public class LogResource : RestResource
|
||||
{
|
||||
public DateTime Time { get; set; }
|
||||
public string? Exception { get; set; }
|
||||
public string? ExceptionType { get; set; }
|
||||
public required string Level { get; set; }
|
||||
public required string Logger { get; set; }
|
||||
public required string Message { get; set; }
|
||||
}
|
||||
|
||||
public static class LogResourceMapper
|
||||
{
|
||||
public static LogResource ToResource(this Log model)
|
||||
{
|
||||
return new LogResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Time = model.Time,
|
||||
Exception = model.Exception,
|
||||
ExceptionType = model.ExceptionType,
|
||||
Level = model.Level.ToLowerInvariant(),
|
||||
Logger = model.Logger,
|
||||
Message = model.Message
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in new issue