From ddac13d308f867d07dc319c108641438efc23f77 Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Thu, 7 Jan 2021 14:17:06 -0600 Subject: [PATCH] Account backups --- app/soapbox/actions/backups.js | 31 +++++++ app/soapbox/features/backups/index.js | 93 +++++++++++++++++++ app/soapbox/features/ui/index.js | 2 + .../features/ui/util/async-components.js | 4 + app/soapbox/reducers/backups.js | 27 ++++++ app/soapbox/reducers/index.js | 2 + app/styles/application.scss | 1 + app/styles/components/backups.scss | 13 +++ 8 files changed, 173 insertions(+) create mode 100644 app/soapbox/actions/backups.js create mode 100644 app/soapbox/features/backups/index.js create mode 100644 app/soapbox/reducers/backups.js create mode 100644 app/styles/components/backups.scss diff --git a/app/soapbox/actions/backups.js b/app/soapbox/actions/backups.js new file mode 100644 index 000000000..844c55ce5 --- /dev/null +++ b/app/soapbox/actions/backups.js @@ -0,0 +1,31 @@ +import api from '../api'; + +export const BACKUPS_FETCH_REQUEST = 'BACKUPS_FETCH_REQUEST'; +export const BACKUPS_FETCH_SUCCESS = 'BACKUPS_FETCH_SUCCESS'; +export const BACKUPS_FETCH_FAIL = 'BACKUPS_FETCH_FAIL'; + +export const BACKUPS_CREATE_REQUEST = 'BACKUPS_CREATE_REQUEST'; +export const BACKUPS_CREATE_SUCCESS = 'BACKUPS_CREATE_SUCCESS'; +export const BACKUPS_CREATE_FAIL = 'BACKUPS_CREATE_FAIL'; + +export function fetchBackups() { + return (dispatch, getState) => { + dispatch({ type: BACKUPS_FETCH_REQUEST }); + return api(getState).get('/api/v1/pleroma/backups').then(({ data: backups }) => { + dispatch({ type: BACKUPS_FETCH_SUCCESS, backups }); + }).catch(error => { + dispatch({ type: BACKUPS_FETCH_FAIL, error }); + }); + }; +} + +export function createBackup() { + return (dispatch, getState) => { + dispatch({ type: BACKUPS_CREATE_REQUEST }); + return api(getState).post('/api/v1/pleroma/backups').then(({ data: backups }) => { + dispatch({ type: BACKUPS_CREATE_SUCCESS, backups }); + }).catch(error => { + dispatch({ type: BACKUPS_CREATE_FAIL, error }); + }); + }; +} diff --git a/app/soapbox/features/backups/index.js b/app/soapbox/features/backups/index.js new file mode 100644 index 000000000..7b161cc35 --- /dev/null +++ b/app/soapbox/features/backups/index.js @@ -0,0 +1,93 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import PropTypes from 'prop-types'; +import Column from '../ui/components/better_column'; +import { + fetchBackups, + createBackup, +} from 'soapbox/actions/backups'; +import ScrollableList from 'soapbox/components/scrollable_list'; +import classNames from 'classnames'; + +const messages = defineMessages({ + heading: { id: 'column.backups', defaultMessage: 'Backups' }, + create: { id: 'backups.actions.create', defaultMessage: 'Create backup' }, + emptyMessage: { id: 'backups.empty_message', defaultMessage: 'No backups found. {action}' }, + emptyMessageAction: { id: 'backups.empty_message.action', defaultMessage: 'Create one now?' }, + pending: { id: 'backups.pending', defaultMessage: 'Pending' }, +}); + +const mapStateToProps = state => ({ + backups: state.get('backups').toList().sortBy(backup => backup.get('inserted_at')), +}); + +export default @connect(mapStateToProps) +@injectIntl +class Backups extends ImmutablePureComponent { + + static propTypes = { + intl: PropTypes.object.isRequired, + }; + + state = { + isLoading: true, + } + + handleCreateBackup = e => { + this.props.dispatch(createBackup()); + e.preventDefault(); + } + + componentDidMount() { + this.props.dispatch(fetchBackups()).then(() => { + this.setState({ isLoading: false }); + }).catch(() => {}); + } + + makeColumnMenu = () => { + const { intl } = this.props; + + return [{ + text: intl.formatMessage(messages.create), + action: this.handleCreateBackup, + }]; + } + + render() { + const { intl, backups } = this.props; + const { isLoading } = this.state; + const showLoading = isLoading && backups.count() === 0; + + const emptyMessageAction = ( + + {intl.formatMessage(messages.emptyMessageAction)} + + ); + + return ( + + + {backups.map(backup => ( +
+ {backup.get('processed') + ? {backup.get('inserted_at')} + :
{intl.formatMessage(messages.pending)}: {backup.get('inserted_at')}
+ } +
+ ))} +
+
+ ); + } + +} diff --git a/app/soapbox/features/ui/index.js b/app/soapbox/features/ui/index.js index 6a556510c..010ee253f 100644 --- a/app/soapbox/features/ui/index.js +++ b/app/soapbox/features/ui/index.js @@ -81,6 +81,7 @@ import { EditProfile, SoapboxConfig, ImportData, + Backups, PasswordReset, SecurityForm, MfaForm, @@ -277,6 +278,7 @@ class SwitchingColumnsArea extends React.PureComponent { + diff --git a/app/soapbox/features/ui/util/async-components.js b/app/soapbox/features/ui/util/async-components.js index bc0c98ca8..52400138f 100644 --- a/app/soapbox/features/ui/util/async-components.js +++ b/app/soapbox/features/ui/util/async-components.js @@ -194,6 +194,10 @@ export function ImportData() { return import(/* webpackChunkName: "features/import_data" */'../../import_data'); } +export function Backups() { + return import(/* webpackChunkName: "features/backups" */'../../backups'); +} + export function PasswordReset() { return import(/* webpackChunkName: "features/auth_login" */'../../auth_login/components/password_reset'); } diff --git a/app/soapbox/reducers/backups.js b/app/soapbox/reducers/backups.js new file mode 100644 index 000000000..913038ee8 --- /dev/null +++ b/app/soapbox/reducers/backups.js @@ -0,0 +1,27 @@ +import { + BACKUPS_FETCH_SUCCESS, + BACKUPS_CREATE_SUCCESS, +} from '../actions/backups'; +import { Map as ImmutableMap, fromJS } from 'immutable'; + +const initialState = ImmutableMap(); + +const importBackup = (state, backup) => { + return state.set(backup.get('inserted_at'), backup); +}; + +const importBackups = (state, backups) => { + return state.withMutations(mutable => { + backups.forEach(backup => importBackup(mutable, backup)); + }); +}; + +export default function backups(state = initialState, action) { + switch(action.type) { + case BACKUPS_FETCH_SUCCESS: + case BACKUPS_CREATE_SUCCESS: + return importBackups(state, fromJS(action.backups)); + default: + return state; + } +}; diff --git a/app/soapbox/reducers/index.js b/app/soapbox/reducers/index.js index 00db40b1e..e8cb4821b 100644 --- a/app/soapbox/reducers/index.js +++ b/app/soapbox/reducers/index.js @@ -49,6 +49,7 @@ import chats from './chats'; import chat_messages from './chat_messages'; import chat_message_lists from './chat_message_lists'; import profile_hover_card from './profile_hover_card'; +import backups from './backups'; const appReducer = combineReducers({ dropdown_menu, @@ -99,6 +100,7 @@ const appReducer = combineReducers({ chat_messages, chat_message_lists, profile_hover_card, + backups, }); // Clear the state (mostly) when the user logs out diff --git a/app/styles/application.scss b/app/styles/application.scss index 91f5ec192..366b7fcae 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -80,6 +80,7 @@ @import 'components/accordion'; @import 'components/server-info'; @import 'components/admin'; +@import 'components/backups'; // Holiday @import 'holiday/halloween'; diff --git a/app/styles/components/backups.scss b/app/styles/components/backups.scss new file mode 100644 index 000000000..57f1144f7 --- /dev/null +++ b/app/styles/components/backups.scss @@ -0,0 +1,13 @@ +.backup { + padding: 15px; + border-bottom: 1px solid var(--brand-color--faint); + + a { + color: var(--brand-color--hicontrast); + } + + &--pending { + font-style: italic; + color: var(--primary-text-color--faint); + } +}