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 { connect } from 'react-redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
|
import { saveCustomFilter, deleteCustomFilter } from 'Store/Actions/customFilterActions';
|
||||||
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
import FilterBuilderModalContent from './FilterBuilderModalContent';
|
||||||
|
|
||||||
function createMapStateToProps() {
|
function createMapStateToProps() {
|
||||||
return createSelector(
|
return createSelector(
|
||||||
(state, { customFilters }) => customFilters,
|
(state, { customFilters }) => customFilters,
|
||||||
(state, { customFilterKey }) => customFilterKey,
|
(state, { id }) => id,
|
||||||
(customFilters, customFilterKey) => {
|
(state) => state.customFilters.isSaving,
|
||||||
if (customFilterKey) {
|
(state) => state.customFilters.saveError,
|
||||||
const customFilter = customFilters.find((c) => c.key === customFilterKey);
|
(customFilters, id, isSaving, saveError) => {
|
||||||
|
if (id) {
|
||||||
|
const customFilter = customFilters.find((c) => c.id === id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
customFilterKey: customFilter.key,
|
id: customFilter.id,
|
||||||
label: customFilter.label,
|
label: customFilter.label,
|
||||||
filters: customFilter.filters
|
filters: customFilter.filters,
|
||||||
|
customFilters,
|
||||||
|
isSaving,
|
||||||
|
saveError
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: '',
|
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