New: Server Side UI Filtering, Error Boundaries (#501)
Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>pull/505/head
parent
a95191dc3b
commit
64a8d02f77
@ -0,0 +1,24 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { setAlbumStudioFilter } from 'Store/Actions/albumStudioActions';
|
||||
import FilterModal from 'Components/Filter/FilterModal';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.artist.items,
|
||||
(state) => state.albumStudio.filterBuilderProps,
|
||||
(sectionItems, filterBuilderProps) => {
|
||||
return {
|
||||
sectionItems,
|
||||
filterBuilderProps,
|
||||
customFilterType: 'albumStudio'
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchSetFilter: setAlbumStudioFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterModal);
|
@ -0,0 +1,62 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import * as sentry from '@sentry/browser';
|
||||
|
||||
class ErrorBoundary extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
info: null
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error, info) {
|
||||
this.setState({
|
||||
error,
|
||||
info
|
||||
});
|
||||
|
||||
sentry.captureException(error);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
children,
|
||||
errorComponent: ErrorComponent,
|
||||
...otherProps
|
||||
} = this.props;
|
||||
|
||||
const {
|
||||
error,
|
||||
info
|
||||
} = this.state;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorComponent
|
||||
error={error}
|
||||
info={info}
|
||||
{...otherProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
errorComponent: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
@ -0,0 +1,38 @@
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin: 50px 0;
|
||||
text-align: center;
|
||||
font-weight: 300;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.imageContainer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.image {
|
||||
height: 350px;
|
||||
}
|
||||
|
||||
.details {
|
||||
margin: 20px;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointMedium) {
|
||||
.image {
|
||||
height: 250px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width: $breakpointSmall) {
|
||||
.image {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import styles from './ErrorBoundaryError.css';
|
||||
|
||||
function ErrorBoundaryError(props) {
|
||||
const {
|
||||
className,
|
||||
messageClassName,
|
||||
detailsClassName,
|
||||
message,
|
||||
error,
|
||||
info
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={messageClassName}>
|
||||
{message}
|
||||
</div>
|
||||
|
||||
<div className={styles.imageContainer}>
|
||||
<img
|
||||
className={styles.image}
|
||||
src={`${window.Lidarr.urlBase}/Content/Images/error.png`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<details className={detailsClassName}>
|
||||
{
|
||||
error &&
|
||||
<div>
|
||||
{error.toString()}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className={styles.info}>
|
||||
{info.componentStack}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorBoundaryError.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
messageClassName: PropTypes.string.isRequired,
|
||||
detailsClassName: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
error: PropTypes.object.isRequired,
|
||||
info: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
ErrorBoundaryError.defaultProps = {
|
||||
className: styles.container,
|
||||
messageClassName: styles.message,
|
||||
detailsClassName: styles.details,
|
||||
message: 'There was an error loading this content'
|
||||
};
|
||||
|
||||
export default ErrorBoundaryError;
|
@ -1,28 +1,42 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { saveCustomFilter, deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state, { customFilters }) => customFilters,
|
||||
(state, { customFilterKey }) => customFilterKey,
|
||||
(customFilters, customFilterKey) => {
|
||||
if (customFilterKey) {
|
||||
const customFilter = customFilters.find((c) => c.key === customFilterKey);
|
||||
(state, { id }) => id,
|
||||
(state) => state.customFilters.isSaving,
|
||||
(state) => state.customFilters.saveError,
|
||||
(customFilters, id, isSaving, saveError) => {
|
||||
if (id) {
|
||||
const customFilter = customFilters.find((c) => c.id === id);
|
||||
|
||||
return {
|
||||
customFilterKey: customFilter.key,
|
||||
id: customFilter.id,
|
||||
label: customFilter.label,
|
||||
filters: customFilter.filters
|
||||
filters: customFilter.filters,
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
label: '',
|
||||
filters: []
|
||||
filters: [],
|
||||
customFilters,
|
||||
isSaving,
|
||||
saveError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(createMapStateToProps)(FilterBuilderModalContent);
|
||||
const mapDispatchToProps = {
|
||||
onSaveCustomFilterPress: saveCustomFilter,
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(FilterBuilderModalContent);
|
||||
|
@ -0,0 +1,23 @@
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||
import CustomFiltersModalContent from './CustomFiltersModalContent';
|
||||
|
||||
function createMapStateToProps() {
|
||||
return createSelector(
|
||||
(state) => state.customFilters.isDeleting,
|
||||
(state) => state.customFilters.deleteError,
|
||||
(isDeleting, deleteError) => {
|
||||
return {
|
||||
isDeleting,
|
||||
deleteError
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const mapDispatchToProps = {
|
||||
dispatchDeleteCustomFilter: deleteCustomFilter
|
||||
};
|
||||
|
||||
export default connect(createMapStateToProps, mapDispatchToProps)(CustomFiltersModalContent);
|
@ -0,0 +1,38 @@
|
||||
import _ from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import ReactMeasure from 'react-measure';
|
||||
|
||||
class Measure extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
componentWillUnmount() {
|
||||
this.onMeasure.cancel();
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onMeasure = _.debounce((payload) => {
|
||||
this.props.onMeasure(payload);
|
||||
}, 250, { leading: true, trailing: false })
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ReactMeasure
|
||||
{...this.props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Measure.propTypes = {
|
||||
onMeasure: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default Measure;
|
@ -0,0 +1,15 @@
|
||||
.message {
|
||||
composes: message from 'Components/Error/ErrorBoundaryError.css';
|
||||
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
font-weight: normal;
|
||||
font-size: 26px;
|
||||
}
|
||||
|
||||
.details {
|
||||
composes: details from 'Components/Error/ErrorBoundaryError.css';
|
||||
|
||||
margin: 0;
|
||||
margin-top: 20px;
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import Button from 'Components/Link/Button';
|
||||
import ModalContent from 'Components/Modal/ModalContent';
|
||||
import ModalHeader from 'Components/Modal/ModalHeader';
|
||||
import ModalBody from 'Components/Modal/ModalBody';
|
||||
import ModalFooter from 'Components/Modal/ModalFooter';
|
||||
import styles from './ModalError.css';
|
||||
|
||||
function ModalError(props) {
|
||||
const {
|
||||
onModalClose,
|
||||
...otherProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<ModalContent onModalClose={onModalClose}>
|
||||
<ModalHeader>
|
||||
Error
|
||||
</ModalHeader>
|
||||
|
||||
<ModalBody>
|
||||
<ErrorBoundaryError
|
||||
messageClassName={styles.message}
|
||||
detailsClassName={styles.details}
|
||||
{...otherProps}
|
||||
message='There was an error loading this item'
|
||||
/>
|
||||
</ModalBody>
|
||||
|
||||
<ModalFooter>
|
||||
<Button
|
||||
onPress={onModalClose}
|
||||
>
|
||||
Close
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>);
|
||||
}
|
||||
|
||||
ModalError.propTypes = {
|
||||
onModalClose: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default ModalError;
|
@ -0,0 +1,3 @@
|
||||
.content {
|
||||
composes: content from './PageContent.css';
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
import ErrorBoundaryError from 'Components/Error/ErrorBoundaryError';
|
||||
import PageContentBodyConnector from './PageContentBodyConnector';
|
||||
import styles from './PageContentError.css';
|
||||
|
||||
function PageContentError(props) {
|
||||
return (
|
||||
<div className={styles.content}>
|
||||
<PageContentBodyConnector>
|
||||
<ErrorBoundaryError
|
||||
{...props}
|
||||
message='There was an error loading this page'
|
||||
/>
|
||||
</PageContentBodyConnector>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PageContentError;
|
After Width: | Height: | Size: 160 KiB |
@ -1,65 +0,0 @@
|
||||
import customFilterHandlers from 'Utilities/customFilterHandlers';
|
||||
import getSectionState from 'Utilities/State/getSectionState';
|
||||
import updateSectionState from 'Utilities/State/updateSectionState';
|
||||
import generateUUIDv4 from 'Utilities/String/generateUUIDv4';
|
||||
|
||||
function createRemoveCustomFilterReducer(section) {
|
||||
return (state, { payload }) => {
|
||||
const newState = getSectionState(state, section);
|
||||
const index = newState.customFilters.findIndex((c) => c.key === payload.key);
|
||||
|
||||
newState.customFilters = [...newState.customFilters];
|
||||
newState.customFilters.splice(index, 1);
|
||||
|
||||
// Reset the selected filter to the first filter if the selected filter
|
||||
// is being deleted.
|
||||
// TODO: Server side collections need to have their collections refetched
|
||||
|
||||
if (newState.selectedFilterKey === payload.key) {
|
||||
newState.selectedFilterKey = newState.filters[0].key;
|
||||
}
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
};
|
||||
}
|
||||
|
||||
function createSaveCustomFilterReducer(section) {
|
||||
return (state, { payload }) => {
|
||||
const newState = getSectionState(state, section);
|
||||
|
||||
const {
|
||||
label,
|
||||
filters
|
||||
} = payload;
|
||||
|
||||
let key = payload.key;
|
||||
|
||||
newState.customFilters = [...newState.customFilters];
|
||||
|
||||
if (key) {
|
||||
const index = newState.customFilters.findIndex((c) => c.key === key);
|
||||
|
||||
newState.customFilters.splice(index, 1, { key, label, filters });
|
||||
} else {
|
||||
key = generateUUIDv4();
|
||||
|
||||
newState.customFilters.push({
|
||||
key,
|
||||
label,
|
||||
filters
|
||||
});
|
||||
}
|
||||
|
||||
// TODO: Server side collections need to have their collections refetched
|
||||
newState.selectedFilterKey = key;
|
||||
|
||||
return updateSectionState(state, section, newState);
|
||||
};
|
||||
}
|
||||
|
||||
export default function createCustomFilterReducers(section, handlers) {
|
||||
return {
|
||||
[handlers[customFilterHandlers.REMOVE]]: createRemoveCustomFilterReducer(section),
|
||||
[handlers[customFilterHandlers.SAVE]]: createSaveCustomFilterReducer(section)
|
||||
};
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
import { createThunk, handleThunks } from 'Store/thunks';
|
||||
import createFetchHandler from './Creators/createFetchHandler';
|
||||
import createRemoveItemHandler from './Creators/createRemoveItemHandler';
|
||||
import createSaveProviderHandler from './Creators/createSaveProviderHandler';
|
||||
import createHandleActions from './Creators/createHandleActions';
|
||||
|
||||
//
|
||||
// Variables
|
||||
|
||||
export const section = 'customFilters';
|
||||
|
||||
//
|
||||
// State
|
||||
|
||||
export const defaultState = {
|
||||
isFetching: false,
|
||||
isPopulated: false,
|
||||
error: null,
|
||||
isSaving: false,
|
||||
saveError: null,
|
||||
isDeleting: false,
|
||||
deleteError: null,
|
||||
items: [],
|
||||
pendingChanges: {}
|
||||
};
|
||||
|
||||
//
|
||||
// Actions Types
|
||||
|
||||
export const FETCH_CUSTOM_FILTERS = 'customFilters/fetchCustomFilters';
|
||||
export const SAVE_CUSTOM_FILTER = 'customFilters/saveCustomFilter';
|
||||
export const DELETE_CUSTOM_FILTER = 'customFilters/deleteCustomFilter';
|
||||
|
||||
//
|
||||
// Action Creators
|
||||
|
||||
export const fetchCustomFilters = createThunk(FETCH_CUSTOM_FILTERS);
|
||||
export const saveCustomFilter = createThunk(SAVE_CUSTOM_FILTER);
|
||||
export const deleteCustomFilter = createThunk(DELETE_CUSTOM_FILTER);
|
||||
|
||||
//
|
||||
// Action Handlers
|
||||
|
||||
export const actionHandlers = handleThunks({
|
||||
[FETCH_CUSTOM_FILTERS]: createFetchHandler(section, '/customFilter'),
|
||||
|
||||
[SAVE_CUSTOM_FILTER]: createSaveProviderHandler(section, '/customFilter'),
|
||||
|
||||
[DELETE_CUSTOM_FILTER]: createRemoveItemHandler(section, '/customFilter')
|
||||
|
||||
});
|
||||
|
||||
//
|
||||
// Reducers
|
||||
export const reducers = createHandleActions({}, defaultState, section);
|
@ -0,0 +1,91 @@
|
||||
import _ from 'lodash';
|
||||
import * as sentry from '@sentry/browser';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
|
||||
function cleanseUrl(url) {
|
||||
const properties = parseUrl(url);
|
||||
|
||||
return `${properties.pathname}${properties.search}`;
|
||||
}
|
||||
|
||||
function cleanseData(data) {
|
||||
const result = _.cloneDeep(data);
|
||||
|
||||
result.transaction = cleanseUrl(result.transaction);
|
||||
|
||||
if (result.exception) {
|
||||
result.exception.values.forEach((exception) => {
|
||||
const stacktrace = exception.stacktrace;
|
||||
|
||||
if (stacktrace) {
|
||||
stacktrace.frames.forEach((frame) => {
|
||||
frame.filename = cleanseUrl(frame.filename);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
result.request.url = cleanseUrl(result.request.url);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function identity(stuff) {
|
||||
return stuff;
|
||||
}
|
||||
|
||||
function createMiddleware() {
|
||||
return (store) => (next) => (action) => {
|
||||
try {
|
||||
// Adds a breadcrumb for reporting later (if necessary).
|
||||
sentry.addBreadcrumb({
|
||||
category: 'redux',
|
||||
message: action.type
|
||||
});
|
||||
|
||||
return next(action);
|
||||
} catch (err) {
|
||||
console.error(`[sentry] Reporting error to Sentry: ${err}`);
|
||||
|
||||
// Send the report including breadcrumbs.
|
||||
sentry.captureException(err, {
|
||||
extra: {
|
||||
action: identity(action),
|
||||
state: identity(store.getState())
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export default function createSentryMiddleware() {
|
||||
const {
|
||||
analytics,
|
||||
branch,
|
||||
version,
|
||||
release,
|
||||
isProduction
|
||||
} = window.Lidarr;
|
||||
|
||||
if (!analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' :
|
||||
'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427';
|
||||
|
||||
sentry.init({
|
||||
dsn,
|
||||
environment: isProduction ? 'production' : 'development',
|
||||
release,
|
||||
sendDefaultPii: true,
|
||||
beforeSend: cleanseData
|
||||
});
|
||||
|
||||
sentry.configureScope((scope) => {
|
||||
scope.setTag('branch', branch);
|
||||
scope.setTag('version', version);
|
||||
});
|
||||
|
||||
return createMiddleware();
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
import _ from 'lodash';
|
||||
import Raven from 'raven-js';
|
||||
import createRavenMiddleware from 'raven-for-redux';
|
||||
import parseUrl from 'Utilities/String/parseUrl';
|
||||
|
||||
function cleanseUrl(url) {
|
||||
const properties = parseUrl(url);
|
||||
|
||||
return `${properties.pathname}${properties.search}`;
|
||||
}
|
||||
|
||||
function cleanseData(data) {
|
||||
const result = _.cloneDeep(data);
|
||||
|
||||
result.culprit = cleanseUrl(result.culprit);
|
||||
result.request.url = cleanseUrl(result.request.url);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export default function sentryMiddleware() {
|
||||
const {
|
||||
analytics,
|
||||
branch,
|
||||
version,
|
||||
release,
|
||||
isProduction
|
||||
} = window.Lidarr;
|
||||
|
||||
if (!analytics) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dsn = isProduction ? 'https://c3a5b33e08de4e18b7d0505e942dbc95@sentry.io/216290' :
|
||||
'https://baede6f14da54cf48ff431479e400adf@sentry.io/1249427';
|
||||
|
||||
Raven.config(
|
||||
dsn,
|
||||
{
|
||||
environment: isProduction ? 'production' : 'development',
|
||||
release,
|
||||
tags: {
|
||||
branch,
|
||||
version
|
||||
},
|
||||
dataCallback: cleanseData
|
||||
}
|
||||
).install();
|
||||
|
||||
return createRavenMiddleware(Raven);
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
import moment from 'moment';
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import formatDateTime from 'Utilities/Date/formatDateTime';
|
||||
import formatTimeSpan from 'Utilities/Date/formatTimeSpan';
|
||||
|
||||
function getUptime(startTime) {
|
||||
return formatTimeSpan(moment().diff(startTime));
|
||||
}
|
||||
|
||||
class StartTime extends Component {
|
||||
|
||||
//
|
||||
// Lifecycle
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = props;
|
||||
|
||||
this._timeoutId = null;
|
||||
|
||||
this.state = {
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
const {
|
||||
startTime,
|
||||
timeFormat,
|
||||
longDateFormat
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
startTime !== prevProps.startTime ||
|
||||
timeFormat !== prevProps.timeFormat ||
|
||||
longDateFormat !== prevProps.longDateFormat
|
||||
) {
|
||||
this.setState({
|
||||
uptime: getUptime(startTime),
|
||||
startTime: formatDateTime(startTime, longDateFormat, timeFormat, { includeSeconds: true })
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
if (this._timeoutId) {
|
||||
this._timeoutId = clearTimeout(this._timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Listeners
|
||||
|
||||
onTimeout = () => {
|
||||
this.setState({ uptime: getUptime(this.props.startTime) });
|
||||
this._timeoutId = setTimeout(this.onTimeout, 1000);
|
||||
}
|
||||
|
||||
//
|
||||
// Render
|
||||
|
||||
render() {
|
||||
const {
|
||||
uptime,
|
||||
startTime
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<span title={startTime}>
|
||||
{uptime}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
StartTime.propTypes = {
|
||||
startTime: PropTypes.string.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
longDateFormat: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
export default StartTime;
|
@ -0,0 +1,15 @@
|
||||
function convertToBytes(input, power, binaryPrefix) {
|
||||
const size = Number(input);
|
||||
|
||||
if (isNaN(size)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const prefix = binaryPrefix ? 1024 : 1000;
|
||||
const multiplier = Math.pow(prefix, power);
|
||||
const result = size * multiplier;
|
||||
|
||||
return Math.round(result);
|
||||
}
|
||||
|
||||
export default convertToBytes;
|
@ -1,6 +0,0 @@
|
||||
const customFilterHandlers = {
|
||||
REMOVE: 'remove',
|
||||
SAVE: 'save'
|
||||
};
|
||||
|
||||
export default customFilterHandlers;
|
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Lidarr.Http;
|
||||
|
||||
namespace Lidarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterModule : LidarrRestModule<CustomFilterResource>
|
||||
{
|
||||
private readonly ICustomFilterService _customFilterService;
|
||||
|
||||
public CustomFilterModule(ICustomFilterService customFilterService)
|
||||
{
|
||||
_customFilterService = customFilterService;
|
||||
|
||||
GetResourceById = GetCustomFilter;
|
||||
GetResourceAll = GetCustomFilters;
|
||||
CreateResource = AddCustomFilter;
|
||||
UpdateResource = UpdateCustomFilter;
|
||||
DeleteResource = DeleteCustomResource;
|
||||
}
|
||||
|
||||
private CustomFilterResource GetCustomFilter(int id)
|
||||
{
|
||||
return _customFilterService.Get(id).ToResource();
|
||||
}
|
||||
|
||||
private List<CustomFilterResource> GetCustomFilters()
|
||||
{
|
||||
return _customFilterService.All().ToResource();
|
||||
}
|
||||
|
||||
private int AddCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
var customFilter = _customFilterService.Add(resource.ToModel());
|
||||
|
||||
return customFilter.Id;
|
||||
}
|
||||
|
||||
private void UpdateCustomFilter(CustomFilterResource resource)
|
||||
{
|
||||
_customFilterService.Update(resource.ToModel());
|
||||
}
|
||||
|
||||
private void DeleteCustomResource(int id)
|
||||
{
|
||||
_customFilterService.Delete(id);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using NzbDrone.Common.Serializer;
|
||||
using NzbDrone.Core.CustomFilters;
|
||||
using Lidarr.Http.REST;
|
||||
|
||||
namespace Lidarr.Api.V1.CustomFilters
|
||||
{
|
||||
public class CustomFilterResource : RestResource
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Label { get; set; }
|
||||
public List<dynamic> Filters { get; set; }
|
||||
}
|
||||
|
||||
public static class CustomFilterResourceMapper
|
||||
{
|
||||
public static CustomFilterResource ToResource(this CustomFilter model)
|
||||
{
|
||||
if (model == null) return null;
|
||||
|
||||
return new CustomFilterResource
|
||||
{
|
||||
Id = model.Id,
|
||||
Type = model.Type,
|
||||
Label = model.Label,
|
||||
Filters = Json.Deserialize<List<dynamic>>(model.Filters)
|
||||
};
|
||||
}
|
||||
|
||||
public static CustomFilter ToModel(this CustomFilterResource resource)
|
||||
{
|
||||
if (resource == null) return null;
|
||||
|
||||
return new CustomFilter
|
||||
{
|
||||
Id = resource.Id,
|
||||
Type = resource.Type,
|
||||
Label = resource.Label,
|
||||
Filters = Json.ToJson(resource.Filters)
|
||||
};
|
||||
}
|
||||
|
||||
public static List<CustomFilterResource> ToResource(this IEnumerable<CustomFilter> filters)
|
||||
{
|
||||
return filters.Select(ToResource).ToList();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
|
||||
namespace NzbDrone.Core.CustomFilters
|
||||
{
|
||||
public class CustomFilter : ModelBase
|
||||
{
|
||||
public string Type { get; set; }
|
||||
public string Label { get; set; }
|
||||
public string Filters { get; set; }
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
using NzbDrone.Core.Datastore;
|
||||
using NzbDrone.Core.Messaging.Events;
|
||||
|
||||
namespace NzbDrone.Core.CustomFilters
|
||||
{
|
||||
public interface ICustomFilterRepository : IBasicRepository<CustomFilter>
|
||||
{
|
||||
}
|
||||
|
||||
public class CustomFilterRepository : BasicRepository<CustomFilter>, ICustomFilterRepository
|
||||
{
|
||||
public CustomFilterRepository(IMainDatabase database, IEventAggregator eventAggregator)
|
||||
: base(database, eventAggregator)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue