mirror of
https://github.com/Prowlarr/Prowlarr.git
synced 2025-09-17 17:14:18 +02:00
New: Indexer history in indexer info modal
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import CommandAppState from './CommandAppState';
|
import CommandAppState from './CommandAppState';
|
||||||
|
import HistoryAppState from './HistoryAppState';
|
||||||
import IndexerAppState, {
|
import IndexerAppState, {
|
||||||
|
IndexerHistoryAppState,
|
||||||
IndexerIndexAppState,
|
IndexerIndexAppState,
|
||||||
IndexerStatusAppState,
|
IndexerStatusAppState,
|
||||||
} from './IndexerAppState';
|
} from './IndexerAppState';
|
||||||
@@ -42,6 +44,8 @@ export interface CustomFilter {
|
|||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
commands: CommandAppState;
|
commands: CommandAppState;
|
||||||
|
history: HistoryAppState;
|
||||||
|
indexerHistory: IndexerHistoryAppState;
|
||||||
indexerIndex: IndexerIndexAppState;
|
indexerIndex: IndexerIndexAppState;
|
||||||
indexerStats: IndexerStatsAppState;
|
indexerStats: IndexerStatsAppState;
|
||||||
indexerStatus: IndexerStatusAppState;
|
indexerStatus: IndexerStatusAppState;
|
||||||
|
8
frontend/src/App/State/HistoryAppState.ts
Normal file
8
frontend/src/App/State/HistoryAppState.ts
Normal file
@@ -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 Column from 'Components/Table/Column';
|
||||||
import SortDirection from 'Helpers/Props/SortDirection';
|
import SortDirection from 'Helpers/Props/SortDirection';
|
||||||
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
|
import Indexer, { IndexerStatus } from 'Indexer/Indexer';
|
||||||
|
import History from 'typings/History';
|
||||||
import AppSectionState, {
|
import AppSectionState, {
|
||||||
AppSectionDeleteState,
|
AppSectionDeleteState,
|
||||||
AppSectionSaveState,
|
AppSectionSaveState,
|
||||||
@@ -34,4 +35,6 @@ interface IndexerAppState
|
|||||||
|
|
||||||
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
export type IndexerStatusAppState = AppSectionState<IndexerStatus>;
|
||||||
|
|
||||||
|
export type IndexerHistoryAppState = AppSectionState<History>;
|
||||||
|
|
||||||
export default IndexerAppState;
|
export default IndexerAppState;
|
||||||
|
@@ -1,58 +1,66 @@
|
|||||||
import PropTypes from 'prop-types';
|
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 formatDateTime from 'Utilities/Date/formatDateTime';
|
||||||
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
import getRelativeDate from 'Utilities/Date/getRelativeDate';
|
||||||
import TableRowCell from './TableRowCell';
|
import TableRowCell from './TableRowCell';
|
||||||
import styles from './RelativeDateCell.css';
|
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
|
||||||
|
|
||||||
render() {
|
const {
|
||||||
const {
|
className,
|
||||||
className,
|
date,
|
||||||
date,
|
includeSeconds,
|
||||||
includeSeconds,
|
component: Component,
|
||||||
showRelativeDates,
|
dispatch,
|
||||||
shortDateFormat,
|
...otherProps
|
||||||
longDateFormat,
|
} = props;
|
||||||
timeFormat,
|
|
||||||
component: Component,
|
|
||||||
dispatch,
|
|
||||||
...otherProps
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!date) {
|
const { showRelativeDates, shortDateFormat, longDateFormat, timeFormat } =
|
||||||
return (
|
useSelector(createRelativeDateCellSelector());
|
||||||
<Component
|
|
||||||
className={className}
|
|
||||||
{...otherProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
if (!date) {
|
||||||
<Component
|
return <Component className={className} {...otherProps} />;
|
||||||
className={className}
|
|
||||||
title={formatDateTime(date, longDateFormat, timeFormat, { includeSeconds, includeRelativeDay: !showRelativeDates })}
|
|
||||||
{...otherProps}
|
|
||||||
>
|
|
||||||
{getRelativeDate(date, shortDateFormat, showRelativeDates, { timeFormat, includeSeconds, timeForToday: true })}
|
|
||||||
</Component>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
className={className}
|
||||||
|
title={formatDateTime(date, longDateFormat, timeFormat, {
|
||||||
|
includeSeconds,
|
||||||
|
includeRelativeDay: !showRelativeDates
|
||||||
|
})}
|
||||||
|
{...otherProps}
|
||||||
|
>
|
||||||
|
{getRelativeDate(date, shortDateFormat, showRelativeDates, {
|
||||||
|
timeFormat,
|
||||||
|
includeSeconds,
|
||||||
|
timeForToday: true
|
||||||
|
})}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
RelativeDateCell.propTypes = {
|
RelativeDateCell.propTypes = {
|
||||||
className: PropTypes.string.isRequired,
|
className: PropTypes.string.isRequired,
|
||||||
date: PropTypes.string,
|
date: PropTypes.string,
|
||||||
includeSeconds: PropTypes.bool.isRequired,
|
includeSeconds: PropTypes.bool.isRequired,
|
||||||
showRelativeDates: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
|
||||||
longDateFormat: PropTypes.string.isRequired,
|
|
||||||
timeFormat: PropTypes.string.isRequired,
|
|
||||||
component: PropTypes.elementType,
|
component: PropTypes.elementType,
|
||||||
dispatch: PropTypes.func
|
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 PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Button from 'Components/Link/Button';
|
import Button from 'Components/Link/Button';
|
||||||
import SpinnerButton from 'Components/Link/SpinnerButton';
|
|
||||||
import Modal from 'Components/Modal/Modal';
|
import Modal from 'Components/Modal/Modal';
|
||||||
import ModalBody from 'Components/Modal/ModalBody';
|
import ModalBody from 'Components/Modal/ModalBody';
|
||||||
import ModalContent from 'Components/Modal/ModalContent';
|
import ModalContent from 'Components/Modal/ModalContent';
|
||||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||||
import { kinds } from 'Helpers/Props';
|
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
import HistoryDetails from './HistoryDetails';
|
import HistoryDetails from './HistoryDetails';
|
||||||
import styles from './HistoryDetailsModal.css';
|
|
||||||
|
|
||||||
function getHeaderTitle(eventType) {
|
function getHeaderTitle(eventType) {
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
@@ -33,10 +30,8 @@ function HistoryDetailsModal(props) {
|
|||||||
eventType,
|
eventType,
|
||||||
indexer,
|
indexer,
|
||||||
data,
|
data,
|
||||||
isMarkingAsFailed,
|
|
||||||
shortDateFormat,
|
shortDateFormat,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
onMarkAsFailedPress,
|
|
||||||
onModalClose
|
onModalClose
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
@@ -61,18 +56,6 @@ function HistoryDetailsModal(props) {
|
|||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
{
|
|
||||||
eventType === 'grabbed' &&
|
|
||||||
<SpinnerButton
|
|
||||||
className={styles.markAsFailedButton}
|
|
||||||
kind={kinds.DANGER}
|
|
||||||
isSpinning={isMarkingAsFailed}
|
|
||||||
onPress={onMarkAsFailedPress}
|
|
||||||
>
|
|
||||||
Mark as Failed
|
|
||||||
</SpinnerButton>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onPress={onModalClose}
|
onPress={onModalClose}
|
||||||
>
|
>
|
||||||
@@ -89,10 +72,8 @@ HistoryDetailsModal.propTypes = {
|
|||||||
eventType: PropTypes.string.isRequired,
|
eventType: PropTypes.string.isRequired,
|
||||||
indexer: PropTypes.object.isRequired,
|
indexer: PropTypes.object.isRequired,
|
||||||
data: PropTypes.object.isRequired,
|
data: PropTypes.object.isRequired,
|
||||||
isMarkingAsFailed: PropTypes.bool.isRequired,
|
|
||||||
shortDateFormat: PropTypes.string.isRequired,
|
shortDateFormat: PropTypes.string.isRequired,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string.isRequired,
|
||||||
onMarkAsFailedPress: PropTypes.func.isRequired,
|
|
||||||
onModalClose: PropTypes.func.isRequired
|
onModalClose: PropTypes.func.isRequired
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
|
|||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
@@ -353,7 +353,7 @@ class HistoryRow extends Component {
|
|||||||
|
|
||||||
if (name === 'date') {
|
if (name === 'date') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={date}
|
date={date}
|
||||||
className={styles.date}
|
className={styles.date}
|
||||||
|
@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
|||||||
import { useSelect } from 'App/SelectContext';
|
import { useSelect } from 'App/SelectContext';
|
||||||
import Label from 'Components/Label';
|
import Label from 'Components/Label';
|
||||||
import IconButton from 'Components/Link/IconButton';
|
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 VirtualTableRowCell from 'Components/Table/Cells/VirtualTableRowCell';
|
||||||
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
import VirtualTableSelectCell from 'Components/Table/Cells/VirtualTableSelectCell';
|
||||||
import Column from 'Components/Table/Column';
|
import Column from 'Components/Table/Column';
|
||||||
@@ -204,7 +204,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ts(2739)
|
// @ts-ignore ts(2739)
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
date={added.toString()}
|
date={added.toString()}
|
||||||
@@ -217,7 +217,7 @@ function IndexerIndexRow(props: IndexerIndexRowProps) {
|
|||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore ts(2739)
|
// @ts-ignore ts(2739)
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
className={styles[name]}
|
className={styles[name]}
|
||||||
date={vipExpiration}
|
date={vipExpiration}
|
||||||
|
134
frontend/src/Indexer/Info/History/IndexerHistory.tsx
Normal file
134
frontend/src/Indexer/Info/History/IndexerHistory.tsx
Normal file
@@ -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;
|
18
frontend/src/Indexer/Info/History/IndexerHistoryRow.css
Normal file
18
frontend/src/Indexer/Info/History/IndexerHistoryRow.css
Normal file
@@ -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.
|
// This file is automatically generated.
|
||||||
// Please do not change this file!
|
// Please do not change this file!
|
||||||
interface CssExports {
|
interface CssExports {
|
||||||
'markAsFailedButton': string;
|
'details': string;
|
||||||
|
'elapsedTime': string;
|
||||||
|
'query': string;
|
||||||
|
'source': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
83
frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx
Normal file
83
frontend/src/Indexer/Info/History/IndexerHistoryRow.tsx
Normal file
@@ -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;
|
const { isOpen, indexerId, onModalClose, onCloneIndexerPress } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal size={sizes.MEDIUM} isOpen={isOpen} onModalClose={onModalClose}>
|
<Modal size={sizes.LARGE} isOpen={isOpen} onModalClose={onModalClose}>
|
||||||
<IndexerInfoModalContent
|
<IndexerInfoModalContent
|
||||||
indexerId={indexerId}
|
indexerId={indexerId}
|
||||||
onModalClose={onModalClose}
|
onModalClose={onModalClose}
|
||||||
|
@@ -9,3 +9,34 @@
|
|||||||
|
|
||||||
margin-right: auto;
|
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 {
|
interface CssExports {
|
||||||
'deleteButton': string;
|
'deleteButton': string;
|
||||||
'description': string;
|
'description': string;
|
||||||
|
'selectedTab': string;
|
||||||
|
'tab': string;
|
||||||
|
'tabContent': string;
|
||||||
|
'tabList': string;
|
||||||
|
'tabs': string;
|
||||||
}
|
}
|
||||||
export const cssExports: CssExports;
|
export const cssExports: CssExports;
|
||||||
export default cssExports;
|
export default cssExports;
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
|
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
import DescriptionList from 'Components/DescriptionList/DescriptionList';
|
||||||
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
import DescriptionListItem from 'Components/DescriptionList/DescriptionListItem';
|
||||||
@@ -25,6 +26,7 @@ import EditIndexerModalConnector from 'Indexer/Edit/EditIndexerModalConnector';
|
|||||||
import Indexer from 'Indexer/Indexer';
|
import Indexer from 'Indexer/Indexer';
|
||||||
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
import { createIndexerSelectorForHook } from 'Store/Selectors/createIndexerSelector';
|
||||||
import translate from 'Utilities/String/translate';
|
import translate from 'Utilities/String/translate';
|
||||||
|
import IndexerHistory from './History/IndexerHistory';
|
||||||
import styles from './IndexerInfoModalContent.css';
|
import styles from './IndexerInfoModalContent.css';
|
||||||
|
|
||||||
function createIndexerInfoItemSelector(indexerId: number) {
|
function createIndexerInfoItemSelector(indexerId: number) {
|
||||||
@@ -38,6 +40,8 @@ function createIndexerInfoItemSelector(indexerId: number) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabs = ['details', 'categories', 'history', 'stats'];
|
||||||
|
|
||||||
interface IndexerInfoModalContentProps {
|
interface IndexerInfoModalContentProps {
|
||||||
indexerId: number;
|
indexerId: number;
|
||||||
onModalClose(): void;
|
onModalClose(): void;
|
||||||
@@ -71,10 +75,19 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
|||||||
const vipExpiration =
|
const vipExpiration =
|
||||||
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
|
fields.find((field) => field.name === 'vipExpiration')?.value ?? undefined;
|
||||||
|
|
||||||
|
const [selectedTab, setSelectedTab] = useState(tabs[0]);
|
||||||
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
const [isEditIndexerModalOpen, setIsEditIndexerModalOpen] = useState(false);
|
||||||
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
|
const [isDeleteIndexerModalOpen, setIsDeleteIndexerModalOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
|
const onTabSelect = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
const selectedTab = tabs[index];
|
||||||
|
setSelectedTab(selectedTab);
|
||||||
|
},
|
||||||
|
[setSelectedTab]
|
||||||
|
);
|
||||||
|
|
||||||
const onEditIndexerPress = useCallback(() => {
|
const onEditIndexerPress = useCallback(() => {
|
||||||
setIsEditIndexerModalOpen(true);
|
setIsEditIndexerModalOpen(true);
|
||||||
}, [setIsEditIndexerModalOpen]);
|
}, [setIsEditIndexerModalOpen]);
|
||||||
@@ -103,204 +116,237 @@ function IndexerInfoModalContent(props: IndexerInfoModalContentProps) {
|
|||||||
<ModalHeader>{`${name}`}</ModalHeader>
|
<ModalHeader>{`${name}`}</ModalHeader>
|
||||||
|
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<FieldSet legend={translate('IndexerDetails')}>
|
<Tabs
|
||||||
<div>
|
className={styles.tabs}
|
||||||
<DescriptionList>
|
selectedIndex={tabs.indexOf(selectedTab)}
|
||||||
<DescriptionListItem
|
onSelect={onTabSelect}
|
||||||
descriptionClassName={styles.description}
|
>
|
||||||
title={translate('Id')}
|
<TabList className={styles.tabList}>
|
||||||
data={id}
|
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||||
/>
|
{translate('Details')}
|
||||||
<DescriptionListItem
|
</Tab>
|
||||||
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')}>
|
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||||
<div>
|
{translate('Categories')}
|
||||||
<DescriptionList>
|
</Tab>
|
||||||
<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>
|
|
||||||
|
|
||||||
{capabilities?.categories?.length > 0 ? (
|
<Tab className={styles.tab} selectedClassName={styles.selectedTab}>
|
||||||
<FieldSet legend={translate('IndexerCategories')}>
|
{translate('History')}
|
||||||
<Table
|
</Tab>
|
||||||
columns={[
|
</TabList>
|
||||||
{
|
<TabPanel>
|
||||||
name: 'id',
|
<div className={styles.tabContent}>
|
||||||
label: translate('Id'),
|
<FieldSet legend={translate('IndexerDetails')}>
|
||||||
isVisible: true,
|
<div>
|
||||||
},
|
<DescriptionList>
|
||||||
{
|
<DescriptionListItem
|
||||||
name: 'name',
|
descriptionClassName={styles.description}
|
||||||
label: translate('Name'),
|
title={translate('Id')}
|
||||||
isVisible: true,
|
data={id}
|
||||||
},
|
/>
|
||||||
]}
|
<DescriptionListItem
|
||||||
>
|
descriptionClassName={styles.description}
|
||||||
{uniqBy(capabilities.categories, 'id')
|
title={translate('Description')}
|
||||||
.sort((a, b) => a.id - b.id)
|
data={description ? description : '-'}
|
||||||
.map((category) => {
|
/>
|
||||||
return (
|
<DescriptionListItem
|
||||||
<TableBody key={category.id}>
|
descriptionClassName={styles.description}
|
||||||
<TableRow key={category.id}>
|
title={translate('Encoding')}
|
||||||
<TableRowCell>{category.id}</TableRowCell>
|
data={encoding ? encoding : '-'}
|
||||||
<TableRowCell>{category.name}</TableRowCell>
|
/>
|
||||||
</TableRow>
|
<DescriptionListItem
|
||||||
{category?.subCategories?.length > 0
|
descriptionClassName={styles.description}
|
||||||
? uniqBy(category.subCategories, 'id')
|
title={translate('Language')}
|
||||||
.sort((a, b) => a.id - b.id)
|
data={language ?? '-'}
|
||||||
.map((subCategory) => {
|
/>
|
||||||
|
{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 (
|
return (
|
||||||
<TableRow key={subCategory.id}>
|
<Label key={p} kind={kinds.PRIMARY}>
|
||||||
<TableRowCell>{subCategory.id}</TableRowCell>
|
{p}
|
||||||
<TableRowCell>
|
</Label>
|
||||||
{subCategory.name}
|
|
||||||
</TableRowCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
: null}
|
}
|
||||||
</TableBody>
|
/>
|
||||||
);
|
<DescriptionListItem
|
||||||
})}
|
descriptionClassName={styles.description}
|
||||||
</Table>
|
title={translate('MovieSearchTypes')}
|
||||||
</FieldSet>
|
data={
|
||||||
) : null}
|
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>
|
||||||
|
</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>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter>
|
||||||
<Button
|
<Button
|
||||||
|
@@ -4,6 +4,7 @@ import * as commands from './commandActions';
|
|||||||
import * as customFilters from './customFilterActions';
|
import * as customFilters from './customFilterActions';
|
||||||
import * as history from './historyActions';
|
import * as history from './historyActions';
|
||||||
import * as indexers from './indexerActions';
|
import * as indexers from './indexerActions';
|
||||||
|
import * as indexerHistory from './indexerHistoryActions';
|
||||||
import * as indexerIndex from './indexerIndexActions';
|
import * as indexerIndex from './indexerIndexActions';
|
||||||
import * as indexerStats from './indexerStatsActions';
|
import * as indexerStats from './indexerStatsActions';
|
||||||
import * as indexerStatus from './indexerStatusActions';
|
import * as indexerStatus from './indexerStatusActions';
|
||||||
@@ -28,6 +29,7 @@ export default [
|
|||||||
releases,
|
releases,
|
||||||
localization,
|
localization,
|
||||||
indexers,
|
indexers,
|
||||||
|
indexerHistory,
|
||||||
indexerIndex,
|
indexerIndex,
|
||||||
indexerStats,
|
indexerStats,
|
||||||
indexerStatus,
|
indexerStatus,
|
||||||
|
81
frontend/src/Store/Actions/indexerHistoryActions.js
Normal file
81
frontend/src/Store/Actions/indexerHistoryActions.js
Normal file
@@ -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 IconButton from 'Components/Link/IconButton';
|
||||||
import Link from 'Components/Link/Link';
|
import Link from 'Components/Link/Link';
|
||||||
import ConfirmModal from 'Components/Modal/ConfirmModal';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import { icons, kinds } from 'Helpers/Props';
|
import { icons, kinds } from 'Helpers/Props';
|
||||||
@@ -110,7 +110,7 @@ class BackupRow extends Component {
|
|||||||
{formatBytes(size)}
|
{formatBytes(size)}
|
||||||
</TableRowCell>
|
</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
date={time}
|
date={time}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Icon from 'Components/Icon';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRowButton from 'Components/Table/TableRowButton';
|
import TableRowButton from 'Components/Table/TableRowButton';
|
||||||
import { icons } from 'Helpers/Props';
|
import { icons } from 'Helpers/Props';
|
||||||
@@ -98,7 +98,7 @@ class LogsTableRow extends Component {
|
|||||||
|
|
||||||
if (name === 'time') {
|
if (name === 'time') {
|
||||||
return (
|
return (
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
key={name}
|
key={name}
|
||||||
date={time}
|
date={time}
|
||||||
/>
|
/>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from 'react';
|
||||||
import Link from 'Components/Link/Link';
|
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 TableRowCell from 'Components/Table/Cells/TableRowCell';
|
||||||
import TableRow from 'Components/Table/TableRow';
|
import TableRow from 'Components/Table/TableRow';
|
||||||
import styles from './LogFilesTableRow.css';
|
import styles from './LogFilesTableRow.css';
|
||||||
@@ -22,7 +22,7 @@ class LogFilesTableRow extends Component {
|
|||||||
<TableRow>
|
<TableRow>
|
||||||
<TableRowCell>{filename}</TableRowCell>
|
<TableRowCell>{filename}</TableRowCell>
|
||||||
|
|
||||||
<RelativeDateCellConnector
|
<RelativeDateCell
|
||||||
date={lastWriteTime}
|
date={lastWriteTime}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
21
frontend/src/typings/History.ts
Normal file
21
frontend/src/typings/History.ts
Normal file
@@ -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-redux": "7.2.4",
|
||||||
"react-router": "5.2.0",
|
"react-router": "5.2.0",
|
||||||
"react-router-dom": "5.2.0",
|
"react-router-dom": "5.2.0",
|
||||||
|
"react-tabs": "4.3.0",
|
||||||
"react-text-truncate": "0.19.0",
|
"react-text-truncate": "0.19.0",
|
||||||
"react-use-measure": "2.1.1",
|
"react-use-measure": "2.1.1",
|
||||||
"react-virtualized": "9.21.1",
|
"react-virtualized": "9.21.1",
|
||||||
|
@@ -258,6 +258,7 @@
|
|||||||
"IndexerFailureRate": "Indexer Failure Rate",
|
"IndexerFailureRate": "Indexer Failure Rate",
|
||||||
"IndexerFlags": "Indexer Flags",
|
"IndexerFlags": "Indexer Flags",
|
||||||
"IndexerHealthCheckNoIndexers": "No indexers enabled, Prowlarr will not return search results",
|
"IndexerHealthCheckNoIndexers": "No indexers enabled, Prowlarr will not return search results",
|
||||||
|
"IndexerHistoryLoadError": "Error loading indexer history",
|
||||||
"IndexerInfo": "Indexer Info",
|
"IndexerInfo": "Indexer Info",
|
||||||
"IndexerLongTermStatusCheckAllClientMessage": "All indexers are unavailable due to failures for more than 6 hours",
|
"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}",
|
"IndexerLongTermStatusCheckSingleClientMessage": "Indexers unavailable due to failures for more than 6 hours: {0}",
|
||||||
@@ -334,6 +335,7 @@
|
|||||||
"NoDownloadClientsFound": "No download clients found",
|
"NoDownloadClientsFound": "No download clients found",
|
||||||
"NoHistoryFound": "No history found",
|
"NoHistoryFound": "No history found",
|
||||||
"NoIndexersFound": "No indexers found",
|
"NoIndexersFound": "No indexers found",
|
||||||
|
"NoIndexerHistory": "No history found for this indexer",
|
||||||
"NoLeaveIt": "No, Leave It",
|
"NoLeaveIt": "No, Leave It",
|
||||||
"NoLinks": "No Links",
|
"NoLinks": "No Links",
|
||||||
"NoLogFiles": "No log files",
|
"NoLogFiles": "No log files",
|
||||||
|
@@ -67,8 +67,13 @@ namespace Prowlarr.Api.V1.History
|
|||||||
|
|
||||||
[HttpGet("indexer")]
|
[HttpGet("indexer")]
|
||||||
[Produces("application/json")]
|
[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();
|
return _historyService.GetByIndexerId(indexerId, eventType).Select(h => MapToResource(h)).ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10
yarn.lock
10
yarn.lock
@@ -2451,7 +2451,7 @@ clone@^1.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
|
||||||
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==
|
||||||
|
|
||||||
clsx@^1.0.1:
|
clsx@^1.0.1, clsx@^1.1.0:
|
||||||
version "1.2.1"
|
version "1.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||||
@@ -5564,6 +5564,14 @@ react-side-effect@^1.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
shallowequal "^1.0.1"
|
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:
|
react-text-truncate@0.19.0:
|
||||||
version "0.19.0"
|
version "0.19.0"
|
||||||
resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.19.0.tgz#60bc5ecf29a03ebc256f31f90a2d8402176aac91"
|
resolved "https://registry.yarnpkg.com/react-text-truncate/-/react-text-truncate-0.19.0.tgz#60bc5ecf29a03ebc256f31f90a2d8402176aac91"
|
||||||
|
Reference in New Issue
Block a user