New: Indexer history in indexer info modal

pull/1853/head
Qstick 9 months ago
parent 0f160707d3
commit 3dd3c80b54

@ -1,5 +1,7 @@
import CommandAppState from './CommandAppState';
import HistoryAppState from './HistoryAppState';
import IndexerAppState, {
IndexerHistoryAppState,
IndexerIndexAppState,
IndexerStatusAppState,
} from './IndexerAppState';
@ -42,6 +44,8 @@ export interface CustomFilter {
interface AppState {
commands: CommandAppState;
history: HistoryAppState;
indexerHistory: IndexerHistoryAppState;
indexerIndex: IndexerIndexAppState;
indexerStats: IndexerStatsAppState;
indexerStatus: IndexerStatusAppState;

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

@ -1,6 +1,7 @@
import Column from 'Components/Table/Column';
import SortDirection from 'Helpers/Props/SortDirection';
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
import History from 'typings/History';
import AppSectionState, {
AppSectionDeleteState,
AppSectionSaveState,
@ -34,4 +35,6 @@ interface IndexerAppState
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
export type IndexerHistoryAppState = AppSectionState<History>;
export default IndexerAppState;

@ -1,58 +1,66 @@
import PropTypes from 'prop-types';
import React, { PureComponent } from 'react';
import React from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import formatDateTime from 'Utilities/Date/formatDateTime';
import getRelativeDate from 'Utilities/Date/getRelativeDate';
import TableRowCell from './TableRowCell';
import styles from './RelativeDateCell.css';
class RelativeDateCell extends PureComponent {
function createRelativeDateCellSelector() {
return createSelector(createUISettingsSelector(), (uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
});
}
function RelativeDateCell(props) {
//
// Render
render() {
const {
className,
date,
includeSeconds,
showRelativeDates,
shortDateFormat,
longDateFormat,
timeFormat,
component: Component,
dispatch,
...otherProps
} = this.props;
const {
className,
date,
includeSeconds,
component: Component,
dispatch,
...otherProps
} = props;
if (!date) {
return (
<Component
className={className}
{...otherProps}
/>
);
}
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
useSelector(createRelativeDateCellSelector());
return (
<Component
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
</Component>
);
if (!date) {
return <Component className={className} {...otherProps} />;
}
return (
<Component
className={className}
title={formatDateTime(date, longDateFormat, timeFormat, {
includeSeconds,
includeRelativeDay: !showRelativeDates
})}
{...otherProps}
>
{getRelativeDate(date, shortDateFormat, showRelativeDates, {
timeFormat,
includeSeconds,
timeForToday: true
})}
</Component>
);
}
RelativeDateCell.propTypes = {
className: PropTypes.string.isRequired,
date: PropTypes.string,
includeSeconds: PropTypes.bool.isRequired,
showRelativeDates: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
longDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
component: PropTypes.elementType,
dispatch: PropTypes.func
};

@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import RelativeDateCell from './RelativeDateCell';
function createMapStateToProps() {
return createSelector(
createUISettingsSelector(),
(uiSettings) => {
return {
showRelativeDates: uiSettings.showRelativeDates,
shortDateFormat: uiSettings.shortDateFormat,
longDateFormat: uiSettings.longDateFormat,
timeFormat: uiSettings.timeFormat
};
}
);
}
export default connect(createMapStateToProps, null)(RelativeDateCell);

@ -1,5 +0,0 @@
.markAsFailedButton {
composes: button from '~Components/Link/Button.css';
margin-right: auto;
}

@ -1,16 +1,13 @@
import PropTypes from 'prop-types';
import React from 'react';
import Button from 'Components/Link/Button';
import SpinnerButton from 'Components/Link/SpinnerButton';
import Modal from 'Components/Modal/Modal';
import ModalBody from 'Components/Modal/ModalBody';
import ModalContent from 'Components/Modal/ModalContent';
import ModalFooter from 'Components/Modal/ModalFooter';
import ModalHeader from 'Components/Modal/ModalHeader';
import { kinds } from 'Helpers/Props';
import translate from 'Utilities/String/translate';
import HistoryDetails from './HistoryDetails';
import styles from './HistoryDetailsModal.css';
function getHeaderTitle(eventType) {
switch (eventType) {
@ -33,10 +30,8 @@ function HistoryDetailsModal(props) {
eventType,
indexer,
data,
isMarkingAsFailed,
shortDateFormat,
timeFormat,
onMarkAsFailedPress,
onModalClose
} = props;
@ -61,18 +56,6 @@ function HistoryDetailsModal(props) {
</ModalBody>
<ModalFooter>
{
eventType === 'grabbed' &&
<SpinnerButton
className={styles.markAsFailedButton}
kind={kinds.DANGER}
isSpinning={isMarkingAsFailed}
onPress={onMarkAsFailedPress}
>
Mark as Failed
</SpinnerButton>
}
<Button
onPress={onModalClose}
>
@ -89,10 +72,8 @@ HistoryDetailsModal.propTypes = {
eventType: PropTypes.string.isRequired,
indexer: PropTypes.object.isRequired,
data: PropTypes.object.isRequired,
isMarkingAsFailed: PropTypes.bool.isRequired,
shortDateFormat: PropTypes.string.isRequired,
timeFormat: PropTypes.string.isRequired,
onMarkAsFailedPress: PropTypes.func.isRequired,
onModalClose: PropTypes.func.isRequired
};

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
@ -353,7 +353,7 @@ class HistoryRow extends Component {
if (name === 'date') {
return (
<RelativeDateCellConnector
<RelativeDateCell
key={name}
date={date}
className={styles.date}

@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { useSelect } from 'App/SelectContext';
import Label from 'Components/Label';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
import Column from 'Components/Table/Column';
@ -204,7 +204,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
<RelativeDateCell
key={name}
className={styles[name]}
date={added.toString()}
@ -217,7 +217,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
return (
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore ts(2739)
<RelativeDateCellConnector
<RelativeDateCell
key={name}
className={styles[name]}
date={vipExpiration}

@ -0,0 +1,134 @@
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { createSelector } from 'reselect';
import AppState from 'App/State/AppState';
import { IndexerHistoryAppState } from 'App/State/IndexerAppState';
import Alert from 'Components/Alert';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import Table from 'Components/Table/Table';
import TableBody from 'Components/Table/TableBody';
import { kinds } from 'Helpers/Props';
import Indexer from 'Indexer/Indexer';
import {
clearIndexerHistory,
fetchIndexerHistory,
} from 'Store/Actions/indexerHistoryActions';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import createUISettingsSelector from 'Store/Selectors/createUISettingsSelector';
import translate from 'Utilities/String/translate';
import IndexerHistoryRow from './IndexerHistoryRow';
const columns = [
{
name: 'eventType',
isVisible: true,
},
{
name: 'query',
label: () => translate('Query'),
isVisible: true,
},
{
name: 'date',
label: () => translate('Date'),
isVisible: true,
},
{
name: 'source',
label: () => translate('Source'),
isVisible: true,
},
{
name: 'details',
label: () => translate('Details'),
isVisible: true,
},
];
function createIndexerHistorySelector() {
return createSelector(
(state: AppState) => state.indexerHistory,
createUISettingsSelector(),
(state: AppState) => state.history.pageSize,
(indexerHistory: IndexerHistoryAppState, uiSettings, pageSize) => {
return {
...indexerHistory,
shortDateFormat: uiSettings.shortDateFormat,
timeFormat: uiSettings.timeFormat,
pageSize,
};
}
);
}
interface IndexerHistoryProps {
indexerId: number;
}
function IndexerHistory(props: IndexerHistoryProps) {
const {
isFetching,
isPopulated,
error,
items,
shortDateFormat,
timeFormat,
pageSize,
} = useSelector(createIndexerHistorySelector());
const indexer = useSelector(
createIndexerSelectorForHook(props.indexerId)
) as Indexer;
const dispatch = useDispatch();
useEffect(() => {
dispatch(
fetchIndexerHistory({ indexerId: props.indexerId, limit: pageSize })
);
return () => {
dispatch(clearIndexerHistory());
};
}, [props, pageSize, dispatch]);
const hasItems = !!items.length;
if (isFetching) {
return <LoadingIndicator />;
}
if (!isFetching && !!error) {
return (
<Alert kind={kinds.DANGER}>{translate('IndexerHistoryLoadError')}</Alert>
);
}
if (isPopulated && !hasItems && !error) {
return <Alert kind={kinds.INFO}>{translate('NoIndexerHistory')}</Alert>;
}
if (isPopulated && hasItems && !error) {
return (
<Table columns={columns}>
<TableBody>
{items.map((item) => {
return (
<IndexerHistoryRow
key={item.id}
indexer={indexer}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
{...item}
/>
);
})}
</TableBody>
</Table>
);
}
return null;
}
export default IndexerHistory;

@ -0,0 +1,18 @@
.query {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 120px;
}
.elapsedTime,
.source {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 150px;
}
.details {
composes: cell from '~Components/Table/Cells/TableRowCell.css';
width: 70px;
}

@ -1,7 +1,10 @@
// This file is automatically generated.
// Please do not change this file!
interface CssExports {
'markAsFailedButton': string;
'details': string;
'elapsedTime': string;
'query': string;
'source': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -0,0 +1,83 @@
import React, { useCallback, useState } from 'react';
import IconButton from 'Components/Link/IconButton';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons } from 'Helpers/Props';
import HistoryDetailsModal from 'History/Details/HistoryDetailsModal';
import HistoryEventTypeCell from 'History/HistoryEventTypeCell';
import Indexer from 'Indexer/Indexer';
import { HistoryData } from 'typings/History';
import translate from 'Utilities/String/translate';
import styles from './IndexerHistoryRow.css';
interface IndexerHistoryRowProps {
data: HistoryData;
date: string;
eventType: string;
successful: boolean;
indexer: Indexer;
shortDateFormat: string;
timeFormat: string;
}
function IndexerHistoryRow(props: IndexerHistoryRowProps) {
const {
data,
date,
eventType,
successful,
indexer,
shortDateFormat,
timeFormat,
} = props;
const [isDetailsModalOpen, setIsDetailsModalOpen] = useState(false);
const onDetailsModalPress = useCallback(() => {
setIsDetailsModalOpen(true);
}, [setIsDetailsModalOpen]);
const onDetailsModalClose = useCallback(() => {
setIsDetailsModalOpen(false);
}, [setIsDetailsModalOpen]);
return (
<TableRow>
<HistoryEventTypeCell
indexer={indexer}
eventType={eventType}
data={data}
successful={successful}
/>
<TableRowCell className={styles.query}>{data.query}</TableRowCell>
<RelativeDateCell date={date} />
<TableRowCell className={styles.source}>
{data.source ? data.source : null}
</TableRowCell>
<TableRowCell className={styles.details}>
<IconButton
name={icons.INFO}
onPress={onDetailsModalPress}
title={translate('HistoryDetails')}
/>
</TableRowCell>
<HistoryDetailsModal
isOpen={isDetailsModalOpen}
eventType={eventType}
data={data}
indexer={indexer}
shortDateFormat={shortDateFormat}
timeFormat={timeFormat}
onModalClose={onDetailsModalClose}
/>
</TableRow>
);
}
export default IndexerHistoryRow;

@ -14,7 +14,7 @@ function IndexerInfoModal(props: IndexerInfoModalProps) {
const { isOpen, indexerId, onModalClose, onCloneIndexerPress } = props;
return (
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
<Modal size={sizes.LARGE} isOpen={isOpen} onModalClose={onModalClose}>
<IndexerInfoModalContent
indexerId={indexerId}
onModalClose={onModalClose}

@ -9,3 +9,34 @@
margin-right: auto;
}
.tabs {
margin-top: -32px;
}
.tabList {
margin: 0;
padding: 0;
}
.tab {
position: relative;
bottom: -1px;
display: inline-block;
padding: 6px 12px;
border: 1px solid transparent;
border-top: none;
list-style: none;
cursor: pointer;
}
.selectedTab {
border-color: var(--borderColor);
border-radius: 0 0 5px 5px;
background-color: rgba(239, 239, 239, 0.4);
color: var(--black);
}
.tabContent {
margin-top: 20px;
}

@ -3,6 +3,11 @@
interface CssExports {
'deleteButton': string;
'description': string;
'selectedTab': string;
'tab': string;
'tabContent': string;
'tabList': string;
'tabs': string;
}
export const cssExports: CssExports;
export default cssExports;

@ -1,6 +1,7 @@
import { uniqBy } from 'lodash';
import React, { useCallback, useState } from 'react';
import { useSelector } from 'react-redux';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import { createSelector } from 'reselect';
import DescriptionList from 'Components/DescriptionList/DescriptionList';
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
@ -25,6 +26,7 @@ import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
import Indexer from 'Indexer/Indexer';
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
import translate from 'Utilities/String/translate';
import IndexerHistory from './History/IndexerHistory';
import styles from './IndexerInfoModalContent.css';
function createIndexerInfoItemSelector(indexerId: number) {
@ -38,6 +40,8 @@ function createIndexerInfoItemSelector(indexerId: number) {
);
}
const tabs = ['details', 'categories', 'history', 'stats'];
interface IndexerInfoModalContentProps {
indexerId: number;
onModalClose(): void;
@ -71,10 +75,19 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
const vipExpiration =
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
const [selectedTab, setSelectedTab] = useState(tabs[0]);
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
useState(false);
const onTabSelect = useCallback(
(index: number) => {
const selectedTab = tabs[index];
setSelectedTab(selectedTab);
},
[setSelectedTab]
);
const onEditIndexerPress = useCallback(() => {
setIsEditIndexerModalOpen(true);
}, [setIsEditIndexerModalOpen]);
@ -103,204 +116,237 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
<ModalHeader>{`${name}`}</ModalHeader>
<ModalBody>
<FieldSet legend={translate('IndexerDetails')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Id')}
data={id}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Description')}
data={description ? description : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Encoding')}
data={encoding ? encoding : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Language')}
data={language ?? '-'}
/>
{vipExpiration ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('VipExpiration')}
data={vipExpiration}
/>
) : null}
<DescriptionListItemTitle>
{translate('IndexerSite')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{baseUrl ? (
<Link to={baseUrl}>
{baseUrl.replace(/(:\/\/)api\./, '$1')}
</Link>
) : (
'-'
)}
</DescriptionListItemDescription>
<DescriptionListItemTitle>
{protocol === 'usenet'
? translate('NewznabUrl')
: translate('TorznabUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
{tags.length > 0 ? (
<>
<DescriptionListItemTitle>
{translate('Tags')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<TagListConnector tags={tags} />
</DescriptionListItemDescription>
</>
) : null}
</DescriptionList>
</div>
</FieldSet>
<Tabs
className={styles.tabs}
selectedIndex={tabs.indexOf(selectedTab)}
onSelect={onTabSelect}
>
<TabList className={styles.tabList}>
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
{translate('Details')}
</Tab>
<FieldSet legend={translate('SearchCapabilities')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('RawSearchSupported')}
data={
capabilities.supportsRawSearch
? translate('Yes')
: translate('No')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('SearchTypes')}
data={
capabilities.searchParams.length === 0 ? (
translate('NotSupported')
) : (
<Label kind={kinds.PRIMARY}>
{capabilities.searchParams[0]}
</Label>
)
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('TVSearchTypes')}
data={
capabilities.tvSearchParams.length === 0
? translate('NotSupported')
: capabilities.tvSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieSearchTypes')}
data={
capabilities.movieSearchParams.length === 0
? translate('NotSupported')
: capabilities.movieSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('BookSearchTypes')}
data={
capabilities.bookSearchParams.length === 0
? translate('NotSupported')
: capabilities.bookSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MusicSearchTypes')}
data={
capabilities.musicSearchParams.length === 0
? translate('NotSupported')
: capabilities.musicSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
</DescriptionList>
</div>
</FieldSet>
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
{translate('Categories')}
</Tab>
{capabilities?.categories?.length > 0 ? (
<FieldSet legend={translate('IndexerCategories')}>
<Table
columns={[
{
name: 'id',
label: translate('Id'),
isVisible: true,
},
{
name: 'name',
label: translate('Name'),
isVisible: true,
},
]}
>
{uniqBy(capabilities.categories, 'id')
.sort((a, b) => a.id - b.id)
.map((category) => {
return (
<TableBody key={category.id}>
<TableRow key={category.id}>
<TableRowCell>{category.id}</TableRowCell>
<TableRowCell>{category.name}</TableRowCell>
</TableRow>
{category?.subCategories?.length > 0
? uniqBy(category.subCategories, 'id')
.sort((a, b) => a.id - b.id)
.map((subCategory) => {
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
{translate('History')}
</Tab>
</TabList>
<TabPanel>
<div className={styles.tabContent}>
<FieldSet legend={translate('IndexerDetails')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Id')}
data={id}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Description')}
data={description ? description : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Encoding')}
data={encoding ? encoding : '-'}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('Language')}
data={language ?? '-'}
/>
{vipExpiration ? (
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('VipExpiration')}
data={vipExpiration}
/>
) : null}
<DescriptionListItemTitle>
{translate('IndexerSite')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{baseUrl ? (
<Link to={baseUrl}>
{baseUrl.replace(/(:\/\/)api\./, '$1')}
</Link>
) : (
'-'
)}
</DescriptionListItemDescription>
<DescriptionListItemTitle>
{protocol === 'usenet'
? translate('NewznabUrl')
: translate('TorznabUrl')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
{`${window.location.origin}${window.Prowlarr.urlBase}/${id}/api`}
</DescriptionListItemDescription>
{tags.length > 0 ? (
<>
<DescriptionListItemTitle>
{translate('Tags')}
</DescriptionListItemTitle>
<DescriptionListItemDescription>
<TagListConnector tags={tags} />
</DescriptionListItemDescription>
</>
) : null}
</DescriptionList>
</div>
</FieldSet>
<FieldSet legend={translate('SearchCapabilities')}>
<div>
<DescriptionList>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('RawSearchSupported')}
data={
capabilities.supportsRawSearch
? translate('Yes')
: translate('No')
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('SearchTypes')}
data={
capabilities.searchParams.length === 0 ? (
translate('NotSupported')
) : (
<Label kind={kinds.PRIMARY}>
{capabilities.searchParams[0]}
</Label>
)
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('TVSearchTypes')}
data={
capabilities.tvSearchParams.length === 0
? translate('NotSupported')
: capabilities.tvSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MovieSearchTypes')}
data={
capabilities.movieSearchParams.length === 0
? translate('NotSupported')
: capabilities.movieSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('BookSearchTypes')}
data={
capabilities.bookSearchParams.length === 0
? translate('NotSupported')
: capabilities.bookSearchParams.map((p) => {
return (
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
}
/>
<DescriptionListItem
descriptionClassName={styles.description}
title={translate('MusicSearchTypes')}
data={
capabilities.musicSearchParams.length === 0
? translate('NotSupported')
: capabilities.musicSearchParams.map((p) => {
return (
<TableRow key={subCategory.id}>
<TableRowCell>{subCategory.id}</TableRowCell>
<TableRowCell>
{subCategory.name}
</TableRowCell>
</TableRow>
<Label key={p} kind={kinds.PRIMARY}>
{p}
</Label>
);
})
: null}
</TableBody>
);
})}
</Table>
</FieldSet>
) : null}
}
/>
</DescriptionList>
</div>
</FieldSet>
</div>
</TabPanel>
<TabPanel>
<div className={styles.tabContent}>
{capabilities?.categories?.length > 0 ? (
<FieldSet legend={translate('IndexerCategories')}>
<Table
columns={[
{
name: 'id',
label: translate('Id'),
isVisible: true,
},
{
name: 'name',
label: translate('Name'),
isVisible: true,
},
]}
>
{uniqBy(capabilities.categories, 'id')
.sort((a, b) => a.id - b.id)
.map((category) => {
return (
<TableBody key={category.id}>
<TableRow key={category.id}>
<TableRowCell>{category.id}</TableRowCell>
<TableRowCell>{category.name}</TableRowCell>
</TableRow>
{category?.subCategories?.length > 0
? uniqBy(category.subCategories, 'id')
.sort((a, b) => a.id - b.id)
.map((subCategory) => {
return (
<TableRow key={subCategory.id}>
<TableRowCell>
{subCategory.id}
</TableRowCell>
<TableRowCell>
{subCategory.name}
</TableRowCell>
</TableRow>
);
})
: null}
</TableBody>
);
})}
</Table>
</FieldSet>
) : null}
</div>
</TabPanel>
<TabPanel>
<div className={styles.tabContent}>
<IndexerHistory indexerId={id} />
</div>
</TabPanel>
</Tabs>
</ModalBody>
<ModalFooter>
<Button

@ -4,6 +4,7 @@ import * as commands from './commandActions';
import * as customFilters from './customFilterActions';
import * as history from './historyActions';
import * as indexers from './indexerActions';
import * as indexerHistory from './indexerHistoryActions';
import * as indexerIndex from './indexerIndexActions';
import * as indexerStats from './indexerStatsActions';
import * as indexerStatus from './indexerStatusActions';
@ -28,6 +29,7 @@ export default [
releases,
localization,
indexers,
indexerHistory,
indexerIndex,
indexerStats,
indexerStatus,

@ -0,0 +1,81 @@
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import { createThunk, handleThunks } from 'Store/thunks';
import createAjaxRequest from 'Utilities/createAjaxRequest';
import { set, update } from './baseActions';
import createHandleActions from './Creators/createHandleActions';
//
// Variables
export const section = 'indexerHistory';
//
// State
export const defaultState = {
isFetching: false,
isPopulated: false,
error: null,
items: []
};
//
// Actions Types
export const FETCH_INDEXER_HISTORY = 'indexerHistory/fetchIndexerHistory';
export const CLEAR_INDEXER_HISTORY = 'indexerHistory/clearIndexerHistory';
//
// Action Creators
export const fetchIndexerHistory = createThunk(FETCH_INDEXER_HISTORY);
export const clearIndexerHistory = createAction(CLEAR_INDEXER_HISTORY);
//
// Action Handlers
export const actionHandlers = handleThunks({
[FETCH_INDEXER_HISTORY]: function(getState, payload, dispatch) {
dispatch(set({ section, isFetching: true }));
const promise = createAjaxRequest({
url: '/history/indexer',
data: payload
}).request;
promise.done((data) => {
dispatch(batchActions([
update({ section, data }),
set({
section,
isFetching: false,
isPopulated: true,
error: null
})
]));
});
promise.fail((xhr) => {
dispatch(set({
section,
isFetching: false,
isPopulated: false,
error: xhr
}));
});
}
});
//
// Reducers
export const reducers = createHandleActions({
[CLEAR_INDEXER_HISTORY]: (state) => {
return Object.assign({}, state, defaultState);
}
}, defaultState, section);

@ -4,7 +4,7 @@ import Icon from 'Components/Icon';
import IconButton from 'Components/Link/IconButton';
import Link from 'Components/Link/Link';
import ConfirmModal from 'Components/Modal/ConfirmModal';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import { icons, kinds } from 'Helpers/Props';
@ -110,7 +110,7 @@ class BackupRow extends Component {
{formatBytes(size)}
</TableRowCell>
<RelativeDateCellConnector
<RelativeDateCell
date={time}
/>

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Icon from 'Components/Icon';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRowButton from 'Components/Table/TableRowButton';
import { icons } from 'Helpers/Props';
@ -98,7 +98,7 @@ class LogsTableRow extends Component {
if (name === 'time') {
return (
<RelativeDateCellConnector
<RelativeDateCell
key={name}
date={time}
/>

@ -1,7 +1,7 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import Link from 'Components/Link/Link';
import RelativeDateCellConnector from 'Components/Table/Cells/RelativeDateCellConnector';
import RelativeDateCell from 'Components/Table/Cells/RelativeDateCell';
import TableRowCell from 'Components/Table/Cells/TableRowCell';
import TableRow from 'Components/Table/TableRow';
import styles from './LogFilesTableRow.css';
@ -22,7 +22,7 @@ class LogFilesTableRow extends Component {
<TableRow>
<TableRowCell>{filename}</TableRowCell>
<RelativeDateCellConnector
<RelativeDateCell
date={lastWriteTime}
/>

@ -0,0 +1,21 @@
import ModelBase from 'App/ModelBase';
export interface HistoryData {
source: string;
host: string;
limit: number;
offset: number;
elapsedTime: number;
query: string;
queryType: string;
}
interface History extends ModelBase {
indexerId: number;
date: string;
successful: boolean;
eventType: string;
data: HistoryData;
}
export default History;

@ -71,6 +71,7 @@
"react-redux": "7.2.4",
"react-router": "5.2.0",
"react-router-dom": "5.2.0",
"react-tabs": "4.3.0",
"react-text-truncate": "0.19.0",
"react-use-measure": "2.1.1",
"react-virtualized": "9.21.1",

@ -258,6 +258,7 @@
"IndexerFailureRate": "Indexer Failure Rate",
"IndexerFlags": "Indexer Flags",
"IndexerHealthCheckNoIndexers": "No indexers enabled, Prowlarr will not return search results",
"IndexerHistoryLoadError": "Error loading indexer history",
"IndexerInfo": "Indexer Info",
"IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours",
"IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {0}",
@ -334,6 +335,7 @@
"NoDownloadClientsFound": "No download clients found",
"NoHistoryFound": "No history found",
"NoIndexersFound": "No indexers found",
"NoIndexerHistory": "No history found for this indexer",
"NoLeaveIt": "No, Leave It",
"NoLinks": "No Links",
"NoLogFiles": "No log files",

@ -67,8 +67,13 @@ namespace Prowlarr.Api.V1.History
[HttpGet("indexer")]
[Produces("application/json")]
public List<HistoryResource> GetIndexerHistory(int indexerId, HistoryEventType? eventType = null)
public List<HistoryResource> GetIndexerHistory(int indexerId, HistoryEventType? eventType = null, int? limit = null)
{
if (limit.HasValue)
{
return _historyService.GetByIndexerId(indexerId, eventType).Select(h => MapToResource(h)).Take(limit.Value).ToList();
}
return _historyService.GetByIndexerId(indexerId, eventType).Select(h => MapToResource(h)).ToList();
}
}

@ -2451,7 +2451,7 @@ clone@^1.0.2:
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
clsx@^1.0.1:
clsx@^1.0.1, clsx@^1.1.0:
version "1.2.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
@ -5564,6 +5564,14 @@ react-side-effect@^1.0.2:
dependencies:
shallowequal "^1.0.1"
react-tabs@4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/react-tabs/-/react-tabs-4.3.0.tgz#9f4db0fd209ba4ab2c1e78993ff964435f84af62"
integrity sha512-2GfoG+f41kiBIIyd3gF+/GRCCYtamC8/2zlAcD8cqQmqI9Q+YVz7fJLHMmU9pXDVYYHpJeCgUSBJju85vu5q8Q==
dependencies:
clsx "^1.1.0"
prop-types "^15.5.0"
react-text-truncate@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.19.0.tgz#60bc5ecf29a03ebc256f31f90a2d8402176aac91"

Loading…
Cancel
Save