New: Server Side UI Filtering, Error Boundaries (#501)

Co-Authored-By: Mark McDowall <markus101@users.noreply.github.com>
This commit is contained in:
Qstick 2018-09-22 23:10:50 -04:00 committed by GitHub
parent a95191dc3b
commit 64a8d02f77
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 1564 additions and 431 deletions

View file

@ -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)
};
}

View file

@ -20,13 +20,13 @@ function createRemoveItemHandler(section, url) {
promise.done((data) => {
dispatch(batchActions([
removeItem({ section, id }),
set({
section,
isDeleting: false,
deleteError: null
})
}),
removeItem({ section, id })
]));
});

View file

@ -21,10 +21,11 @@ function createSaveProviderHandler(section, url, options = {}) {
const {
id,
queryParams = {}
queryParams = {},
...otherPayload
} = payload;
const saveData = getProviderState(payload, getState, section);
const saveData = getProviderState({ id, ...otherPayload }, getState, section);
const ajaxOptions = {
url: `${url}?${$.param(queryParams, true)}`,

View file

@ -2,7 +2,7 @@ import _ from 'lodash';
import $ from 'jquery';
import { createAction } from 'redux-actions';
import getMonitoringOptions from 'Utilities/Artist/getMonitoringOptions';
import { sortDirections } from 'Helpers/Props';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
@ -29,7 +29,55 @@ export const defaultState = {
selectedFilterKey: 'all',
filters,
filterPredicates,
customFilters: []
filterBuilderProps: [
{
name: 'monitored',
label: 'Monitored',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.BOOL
},
{
name: 'status',
label: 'Status',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.ARTIST_STATUS
},
{
name: 'artistType',
label: 'Artist Type',
type: filterBuilderTypes.EXACT
},
{
name: 'qualityProfileId',
label: 'Quality Profile',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.QUALITY_PROFILE
},
{
name: 'languageProfileId',
label: 'Language Profile',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.LANGUAGE_PROFILE
},
{
name: 'metadataProfileId',
label: 'Metadata Profile',
type: filterBuilderTypes.EXACT,
valueType: filterBuilderValueTypes.METADATA_PROFILE
},
{
name: 'rootFolderPath',
label: 'Root Folder Path',
type: filterBuilderTypes.EXACT
},
{
name: 'tags',
label: 'Tags',
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG
}
]
};
export const persistState = [

View file

@ -103,6 +103,20 @@ export const filterPredicates = {
const predicate = filterTypePredicates[type];
return predicate(item.ratings.value * 10, filterValue);
},
albumCount: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const albumCount = item.statistics ? item.statistics.albumCount : 0;
return predicate(albumCount, filterValue);
},
sizeOnDisk: function(item, filterValue, type) {
const predicate = filterTypePredicates[type];
const sizeOnDisk = item.statistics ? item.statistics.sizeOnDisk : 0;
return predicate(sizeOnDisk, filterValue);
}
};

View file

@ -1,12 +1,10 @@
import $ from 'jquery';
import { createAction } from 'redux-actions';
import { batchActions } from 'redux-batched-actions';
import customFilterHandlers from 'Utilities/customFilterHandlers';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
import createHandleActions from './Creators/createHandleActions';
import { set, updateItem } from './baseActions';
import { filters, filterPredicates } from './artistActions';
@ -79,8 +77,7 @@ export const defaultState = {
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG
}
],
customFilters: []
]
};
export const persistState = [
@ -97,8 +94,6 @@ export const SET_ARTIST_EDITOR_SORT = 'artistEditor/setArtistEditorSort';
export const SET_ARTIST_EDITOR_FILTER = 'artistEditor/setArtistEditorFilter';
export const SAVE_ARTIST_EDITOR = 'artistEditor/saveArtistEditor';
export const BULK_DELETE_ARTIST = 'artistEditor/bulkDeleteArtist';
export const REMOVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/removeArtistEditorCustomFilter';
export const SAVE_ARTIST_EDITOR_CUSTOM_FILTER = 'artistEditor/saveArtistEditorCustomFilter';
//
// Action Creators
@ -107,8 +102,6 @@ export const setArtistEditorSort = createAction(SET_ARTIST_EDITOR_SORT);
export const setArtistEditorFilter = createAction(SET_ARTIST_EDITOR_FILTER);
export const saveArtistEditor = createThunk(SAVE_ARTIST_EDITOR);
export const bulkDeleteArtist = createThunk(BULK_DELETE_ARTIST);
export const removeArtistEditorCustomFilter = createAction(REMOVE_ARTIST_EDITOR_CUSTOM_FILTER);
export const saveArtistEditorCustomFilter = createAction(SAVE_ARTIST_EDITOR_CUSTOM_FILTER);
//
// Action Handlers
@ -193,11 +186,6 @@ export const actionHandlers = handleThunks({
export const reducers = createHandleActions({
[SET_ARTIST_EDITOR_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section),
...createCustomFilterReducers(section, {
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_EDITOR_CUSTOM_FILTER,
[customFilterHandlers.SAVE]: SAVE_ARTIST_EDITOR_CUSTOM_FILTER
})
[SET_ARTIST_EDITOR_FILTER]: createSetClientSideCollectionFilterReducer(section)
}, defaultState, section);

View file

@ -1,12 +1,10 @@
import moment from 'moment';
import { createAction } from 'redux-actions';
import customFilterHandlers from 'Utilities/customFilterHandlers';
import sortByName from 'Utilities/Array/sortByName';
import { filterBuilderTypes, filterBuilderValueTypes, sortDirections } from 'Helpers/Props';
import createSetTableOptionReducer from './Creators/Reducers/createSetTableOptionReducer';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
import createHandleActions from './Creators/createHandleActions';
import { filters, filterPredicates } from './artistActions';
@ -292,7 +290,8 @@ export const defaultState = {
{
name: 'sizeOnDisk',
label: 'Size on Disk',
type: filterBuilderTypes.NUMBER
type: filterBuilderTypes.NUMBER,
valueType: filterBuilderValueTypes.BYTES
},
{
name: 'genres',
@ -324,8 +323,7 @@ export const defaultState = {
type: filterBuilderTypes.ARRAY,
valueType: filterBuilderValueTypes.TAG
}
],
customFilters: []
]
};
export const persistState = [
@ -350,8 +348,6 @@ export const SET_ARTIST_TABLE_OPTION = 'artistIndex/setArtistTableOption';
export const SET_ARTIST_POSTER_OPTION = 'artistIndex/setArtistPosterOption';
export const SET_ARTIST_BANNER_OPTION = 'artistIndex/setArtistBannerOption';
export const SET_ARTIST_OVERVIEW_OPTION = 'artistIndex/setArtistOverviewOption';
export const REMOVE_ARTIST_CUSTOM_FILTER = 'artistIndex/removeArtistCustomFilter';
export const SAVE_ARTIST_CUSTOM_FILTER = 'artistIndex/saveArtistCustomFilter';
//
// Action Creators
@ -363,8 +359,7 @@ export const setArtistTableOption = createAction(SET_ARTIST_TABLE_OPTION);
export const setArtistPosterOption = createAction(SET_ARTIST_POSTER_OPTION);
export const setArtistBannerOption = createAction(SET_ARTIST_BANNER_OPTION);
export const setArtistOverviewOption = createAction(SET_ARTIST_OVERVIEW_OPTION);
export const removeArtistCustomFilter = createAction(REMOVE_ARTIST_CUSTOM_FILTER);
export const saveArtistCustomFilter = createAction(SAVE_ARTIST_CUSTOM_FILTER);
//
// Reducers
@ -413,11 +408,6 @@ export const reducers = createHandleActions({
...payload
}
};
},
...createCustomFilterReducers(section, {
[customFilterHandlers.REMOVE]: REMOVE_ARTIST_CUSTOM_FILTER,
[customFilterHandlers.SAVE]: SAVE_ARTIST_CUSTOM_FILTER
})
}
}, defaultState, section);

View file

@ -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);

View file

@ -2,6 +2,7 @@ import * as addArtist from './addArtistActions';
import * as app from './appActions';
import * as blacklist from './blacklistActions';
import * as captcha from './captchaActions';
import * as customFilters from './customFilterActions';
import * as devices from './deviceActions';
import * as calendar from './calendarActions';
import * as commands from './commandActions';
@ -35,6 +36,7 @@ export default [
captcha,
calendar,
commands,
customFilters,
devices,
albums,
trackFiles,

View file

@ -10,6 +10,7 @@ import createHandleActions from './Creators/createHandleActions';
// Variables
export const section = 'oAuth';
const callbackUrl = `${window.location.origin}${window.Lidarr.urlBase}/oauth.html`;
//
// State
@ -64,6 +65,19 @@ function showOAuthWindow(url) {
return deferred.promise();
}
function executeIntermediateRequest(payload, ajaxOptions) {
return $.ajax(ajaxOptions).then((data) => {
return requestAction({
action: 'continueOAuth',
queryParams: {
...data,
callbackUrl
},
...payload
});
});
}
//
// Action Handlers
@ -72,7 +86,7 @@ export const actionHandlers = handleThunks({
[START_OAUTH]: function(getState, payload, dispatch) {
const actionPayload = {
action: 'startOAuth',
queryParams: { callbackUrl: `${window.location.origin}${window.Lidarr.urlBase}/oauth.html` },
queryParams: { callbackUrl },
...payload
};
@ -85,7 +99,16 @@ export const actionHandlers = handleThunks({
const promise = requestAction(actionPayload)
.then((response) => {
startResponse = response;
return showOAuthWindow(response.oauthUrl);
if (response.oauthUrl) {
return showOAuthWindow(response.oauthUrl);
}
return executeIntermediateRequest(payload, response).then((intermediateResponse) => {
startResponse = intermediateResponse;
return showOAuthWindow(intermediateResponse.oauthUrl);
});
})
.then((queryParams) => {
return requestAction({

View file

@ -1,11 +1,9 @@
import $ from 'jquery';
import { createAction } from 'redux-actions';
import customFilterHandlers from 'Utilities/customFilterHandlers';
import { filterBuilderTypes, filterBuilderValueTypes, filterTypes, sortDirections } from 'Helpers/Props';
import { createThunk, handleThunks } from 'Store/thunks';
import createSetClientSideCollectionSortReducer from './Creators/Reducers/createSetClientSideCollectionSortReducer';
import createSetClientSideCollectionFilterReducer from './Creators/Reducers/createSetClientSideCollectionFilterReducer';
import createCustomFilterReducers from './Creators/Reducers/createCustomFilterReducers';
import createFetchHandler from './Creators/createFetchHandler';
import createHandleActions from './Creators/createHandleActions';
@ -45,8 +43,6 @@ export const defaultState = {
}
},
selectedFilterKey: 'all',
filters: [
{
key: 'all',
@ -143,9 +139,7 @@ export const defaultState = {
label: 'Rejections',
type: filterBuilderTypes.NUMBER
}
],
customFilters: []
]
};
export const persistState = [
@ -163,9 +157,6 @@ export const CLEAR_RELEASES = 'releases/clearReleases';
export const GRAB_RELEASE = 'releases/grabRelease';
export const UPDATE_RELEASE = 'releases/updateRelease';
export const SET_RELEASES_FILTER = 'releases/setReleasesFilter';
export const ADD_RELEASES_CUSTOM_FILTER = 'releases/addReleasesCustomFilter';
export const REMOVE_RELEASES_CUSTOM_FILTER = 'releases/removeReleasesCustomFilter';
export const SAVE_RELEASES_CUSTOM_FILTER = 'releases/saveReleasesCustomFilter';
//
// Action Creators
@ -177,9 +168,6 @@ export const clearReleases = createAction(CLEAR_RELEASES);
export const grabRelease = createThunk(GRAB_RELEASE);
export const updateRelease = createAction(UPDATE_RELEASE);
export const setReleasesFilter = createAction(SET_RELEASES_FILTER);
export const addReleasesCustomFilter = createAction(ADD_RELEASES_CUSTOM_FILTER);
export const removeReleasesCustomFilter = createAction(REMOVE_RELEASES_CUSTOM_FILTER);
export const saveReleasesCustomFilter = createAction(SAVE_RELEASES_CUSTOM_FILTER);
//
// Helpers
@ -266,11 +254,6 @@ export const reducers = createHandleActions({
},
[SET_RELEASES_SORT]: createSetClientSideCollectionSortReducer(section),
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section),
...createCustomFilterReducers(section, {
[customFilterHandlers.REMOVE]: REMOVE_RELEASES_CUSTOM_FILTER,
[customFilterHandlers.SAVE]: SAVE_RELEASES_CUSTOM_FILTER
})
[SET_RELEASES_FILTER]: createSetClientSideCollectionFilterReducer(section)
}, defaultState, section);

View file

@ -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();
}

View file

@ -1,15 +1,15 @@
import { applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { routerMiddleware } from 'react-router-redux';
import sentryMiddleware from './sentryMiddleware';
import createSentryMiddleware from './createSentryMiddleware';
import persistState from './persistState';
export default function(history) {
const middlewares = [];
const ravenMiddleware = sentryMiddleware();
const sentryMiddleware = createSentryMiddleware();
if (ravenMiddleware) {
middlewares.push(ravenMiddleware);
if (sentryMiddleware) {
middlewares.push(sentryMiddleware);
}
middlewares.push(routerMiddleware(history));

View file

@ -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);
}

View file

@ -94,12 +94,24 @@ function sort(items, state) {
return _.orderBy(items, clauses, orders);
}
function createCustomFiltersSelector(type, alternateType) {
return createSelector(
(state) => state.customFilters.items,
(customFilters) => {
return customFilters.filter((customFilter) => {
return customFilter.type === type || customFilter.type === alternateType;
});
}
);
}
function createClientSideCollectionSelector(section, uiSection) {
return createSelector(
(state) => _.get(state, section),
(state) => _.get(state, uiSection),
(sectionState, uiSectionState = {}) => {
const state = Object.assign({}, sectionState, uiSectionState);
createCustomFiltersSelector(section, uiSection),
(sectionState, uiSectionState = {}, customFilters) => {
const state = Object.assign({}, sectionState, uiSectionState, { customFilters });
const filtered = filter(state.items, state);
const sorted = sort(filtered, state);
@ -107,6 +119,7 @@ function createClientSideCollectionSelector(section, uiSection) {
return {
...sectionState,
...uiSectionState,
customFilters,
items: sorted,
totalItems: state.items.length
};