Use inject everywhere

This commit is contained in:
Jannis Mattheis 2018-10-21 15:46:08 +02:00
parent bbb344be72
commit d8c413df03
13 changed files with 172 additions and 119 deletions

View File

@ -15,9 +15,9 @@ import Clients from './pages/Clients';
import Login from './pages/Login';
import Messages from './pages/Messages';
import Users from './pages/Users';
import {currentUser} from './stores/CurrentUser';
import {observer} from 'mobx-react';
import {observable} from 'mobx';
import {inject, Stores} from './inject';
const lightTheme = createMuiTheme({
palette: {
@ -40,7 +40,7 @@ const styles = (theme: Theme) => ({
});
@observer
class Layout extends React.Component<WithStyles<'content'>> {
class Layout extends React.Component<WithStyles<'content'> & Stores<'currentUser'>> {
private static defaultVersion = '0.0.0';
@observable
@ -59,14 +59,16 @@ class Layout extends React.Component<WithStyles<'content'>> {
}
public render() {
const {
loggedIn,
authenticating,
user: {name, admin},
} = currentUser;
const {version, showSettings, darkThemeVisible} = this;
const {classes} = this.props;
const {
classes,
currentUser: {
loggedIn,
authenticating,
user: {name, admin},
logout,
},
} = this.props;
const theme = darkThemeVisible ? darkTheme : lightTheme;
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
return (
@ -81,6 +83,7 @@ class Layout extends React.Component<WithStyles<'content'>> {
loggedIn={loggedIn}
toggleTheme={() => (this.darkThemeVisible = !this.darkThemeVisible)}
showSettings={() => (this.showSettings = true)}
logout={logout}
/>
<Navigation loggedIn={loggedIn} />
@ -112,4 +115,4 @@ class Layout extends React.Component<WithStyles<'content'>> {
}
}
export default withStyles(styles, {withTheme: true})<{}>(Layout);
export default withStyles(styles, {withTheme: true})<{}>(inject('currentUser')(Layout));

29
ui/src/actions/axios.ts Normal file
View File

@ -0,0 +1,29 @@
import axios from 'axios';
import {CurrentUser} from '../stores/CurrentUser';
import {SnackReporter} from '../stores/SnackManager';
export const initAxios = (currentUser: CurrentUser, snack: SnackReporter) => {
axios.interceptors.request.use((config) => {
config.headers['X-Gotify-Key'] = currentUser.token();
return config;
});
axios.interceptors.response.use(undefined, (error) => {
if (!error.response) {
snack('Gotify server is not reachable, try refreshing the page.');
return Promise.reject(error);
}
const status = error.response.status;
if (status === 401) {
currentUser.tryAuthenticate().then(() => snack('Could not complete request.'));
}
if (status === 400) {
snack(error.response.data.error + ': ' + error.response.data.errorDescription);
}
return Promise.reject(error);
});
};

View File

@ -1,27 +0,0 @@
import axios from 'axios';
import {currentUser} from '../stores/CurrentUser';
import SnackManager from '../stores/SnackManager';
axios.interceptors.request.use((config) => {
config.headers['X-Gotify-Key'] = currentUser.token();
return config;
});
axios.interceptors.response.use(undefined, (error) => {
if (!error.response) {
SnackManager.snack('Gotify server is not reachable, try refreshing the page.');
return Promise.reject(error);
}
const status = error.response.status;
if (status === 401) {
currentUser.tryAuthenticate().then(() => SnackManager.snack('Could not complete request.'));
}
if (status === 400) {
SnackManager.snack(error.response.data.error + ': ' + error.response.data.errorDescription);
}
return Promise.reject(error);
});

View File

@ -12,7 +12,6 @@ import Highlight from '@material-ui/icons/Highlight';
import SupervisorAccount from '@material-ui/icons/SupervisorAccount';
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import {currentUser} from '../stores/CurrentUser';
import {observer} from 'mobx-react';
const styles = (theme: Theme) => ({
@ -42,12 +41,13 @@ interface IProps {
version: string;
toggleTheme: VoidFunction;
showSettings: VoidFunction;
logout: VoidFunction;
}
@observer
class Header extends Component<IProps & Styles> {
public render() {
const {classes, version, name, loggedIn, admin, toggleTheme} = this.props;
const {classes, version, name, loggedIn, admin, toggleTheme, logout} = this.props;
return (
<AppBar position="absolute" className={classes.appBar}>
@ -69,7 +69,7 @@ class Header extends Component<IProps & Styles> {
</Typography>
</a>
</div>
{loggedIn && this.renderButtons(name, admin)}
{loggedIn && this.renderButtons(name, admin, logout)}
<IconButton onClick={toggleTheme} color="inherit">
<Highlight />
</IconButton>
@ -78,7 +78,7 @@ class Header extends Component<IProps & Styles> {
);
}
private renderButtons(name: string, admin: boolean) {
private renderButtons(name: string, admin: boolean, logout: VoidFunction) {
const {classes, showSettings} = this.props;
return (
<div>
@ -109,7 +109,7 @@ class Header extends Component<IProps & Styles> {
&nbsp;
{name}
</Button>
<Button color="inherit" onClick={currentUser.logout} id="logout">
<Button color="inherit" onClick={logout} id="logout">
<ExitToApp />
&nbsp;Logout
</Button>

View File

@ -5,8 +5,8 @@ import ListItemText from '@material-ui/core/ListItemText';
import {StyleRules, Theme, WithStyles, withStyles} from '@material-ui/core/styles';
import React, {Component} from 'react';
import {Link} from 'react-router-dom';
import AppStore from '../stores/AppStore';
import {observer} from 'mobx-react';
import {inject, Stores} from '../inject';
const styles = (theme: Theme): StyleRules<'drawerPaper' | 'toolbar' | 'link'> => ({
drawerPaper: {
@ -29,10 +29,10 @@ interface IProps {
}
@observer
class Navigation extends Component<IProps & Styles> {
class Navigation extends Component<IProps & Styles & Stores<'appStore'>> {
public render() {
const {classes, loggedIn} = this.props;
const apps = AppStore.getItems();
const {classes, loggedIn, appStore} = this.props;
const apps = appStore.getItems();
const userApps =
apps.length === 0
@ -78,4 +78,4 @@ class Navigation extends Component<IProps & Styles> {
}
}
export default withStyles(styles, {withTheme: true})<IProps>(Navigation);
export default withStyles(styles, {withTheme: true})<IProps>(inject('appStore')(Navigation));

View File

@ -6,22 +6,22 @@ import DialogTitle from '@material-ui/core/DialogTitle';
import TextField from '@material-ui/core/TextField';
import Tooltip from '@material-ui/core/Tooltip';
import React, {Component} from 'react';
import {currentUser} from '../stores/CurrentUser';
import {observable} from 'mobx';
import {observer} from 'mobx-react';
import {inject, Stores} from '../inject';
interface IProps {
fClose: VoidFunction;
}
@observer
export default class SettingsDialog extends Component<IProps> {
class SettingsDialog extends Component<IProps & Stores<'currentUser'>> {
@observable
private pass = '';
public render() {
const {pass} = this;
const {fClose} = this.props;
const {fClose, currentUser} = this.props;
const submitAndClose = () => {
currentUser.changePassword(pass);
fClose();
@ -64,3 +64,5 @@ export default class SettingsDialog extends Component<IProps> {
);
}
}
export default inject('currentUser')(SettingsDialog);

View File

@ -4,10 +4,10 @@ import Close from '@material-ui/icons/Close';
import React, {Component} from 'react';
import {observable, reaction} from 'mobx';
import {observer} from 'mobx-react';
import SnackManager from '../stores/SnackManager';
import {inject, Stores} from '../inject';
@observer
class SnackBarHandler extends Component {
class SnackBarHandler extends Component<Stores<'snackManager'>> {
private static MAX_VISIBLE_SNACK_TIME_IN_MS = 6000;
private static MIN_VISIBLE_SNACK_TIME_IN_MS = 1000;
@ -19,12 +19,12 @@ class SnackBarHandler extends Component {
private dispose: () => void = () => {};
public componentDidMount = () =>
(this.dispose = reaction(() => SnackManager.counter, this.onNewSnack));
(this.dispose = reaction(() => this.props.snackManager.counter, this.onNewSnack));
public componentWillUnmount = () => this.dispose();
public render() {
const {message: current, hasNext} = SnackManager;
const {message: current, hasNext} = this.props.snackManager;
const duration = hasNext()
? SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS
: SnackBarHandler.MAX_VISIBLE_SNACK_TIME_IN_MS;
@ -70,14 +70,14 @@ class SnackBarHandler extends Component {
};
private openNextSnack = () => {
if (SnackManager.hasNext()) {
if (this.props.snackManager.hasNext()) {
this.open = true;
this.openWhen = Date.now();
SnackManager.next();
this.props.snackManager.next();
}
};
private closeCurrentSnack = () => (this.open = false);
}
export default SnackBarHandler;
export default inject('snackManager')(SnackBarHandler);

View File

@ -2,16 +2,20 @@ import * as React from 'react';
import * as ReactDOM from 'react-dom';
import 'typeface-roboto';
import 'typeface-roboto-mono';
import './actions/defaultAxios';
import {initAxios} from './actions/axios';
import * as config from './config';
import Layout from './Layout';
import registerServiceWorker from './registerServiceWorker';
import * as Notifications from './stores/Notifications';
import {currentUser} from './stores/CurrentUser';
import AppStore from './stores/AppStore';
import {CurrentUser} from './stores/CurrentUser';
import {AppStore} from './stores/AppStore';
import {reaction} from 'mobx';
import {WebSocketStore} from './stores/WebSocketStore';
import SnackManager from './stores/SnackManager';
import {SnackManager} from './stores/SnackManager';
import {InjectProvider, StoreMapping} from './inject';
import {UserStore} from './stores/UserStore';
import {MessagesStore} from './stores/MessagesStore';
import {ClientStore} from './stores/ClientStore';
const defaultDevConfig = {
url: 'http://localhost:80/',
@ -33,6 +37,26 @@ declare global {
}
}
const initStores = (): StoreMapping => {
const snackManager = new SnackManager();
const appStore = new AppStore(snackManager.snack);
const userStore = new UserStore(snackManager.snack);
const messagesStore = new MessagesStore(appStore, snackManager.snack);
const currentUser = new CurrentUser(snackManager.snack);
const clientStore = new ClientStore(snackManager.snack);
const wsStore = new WebSocketStore(snackManager.snack, currentUser, messagesStore);
return {
appStore,
snackManager,
userStore,
messagesStore,
currentUser,
clientStore,
wsStore,
};
};
(function clientJS() {
Notifications.requestPermission();
if (process.env.NODE_ENV === 'production') {
@ -40,20 +64,28 @@ declare global {
} else {
config.set(window.config || defaultDevConfig);
}
const ws = new WebSocketStore(SnackManager.snack);
const stores = initStores();
initAxios(stores.currentUser, stores.snackManager.snack);
reaction(
() => currentUser.loggedIn,
() => stores.currentUser.loggedIn,
(loggedIn) => {
if (loggedIn) {
ws.listen();
stores.wsStore.listen();
} else {
ws.close();
stores.wsStore.close();
}
AppStore.refresh();
stores.appStore.refresh();
}
);
currentUser.tryAuthenticate();
ReactDOM.render(<Layout />, document.getElementById('root'));
stores.currentUser.tryAuthenticate();
ReactDOM.render(
<InjectProvider stores={stores}>
<Layout />
</InjectProvider>,
document.getElementById('root')
);
registerServiceWorker();
})();

View File

@ -14,12 +14,12 @@ import ConfirmDialog from '../component/ConfirmDialog';
import DefaultPage from '../component/DefaultPage';
import ToggleVisibility from '../component/ToggleVisibility';
import AddApplicationDialog from './dialog/AddApplicationDialog';
import AppStore from '../stores/AppStore';
import {observer} from 'mobx-react';
import {observable} from 'mobx';
import {inject, Stores} from '../inject';
@observer
class Applications extends Component {
class Applications extends Component<Stores<'appStore'>> {
@observable
private deleteId: number | false = false;
@observable
@ -28,11 +28,15 @@ class Applications extends Component {
private uploadId = -1;
private upload: HTMLInputElement | null = null;
public componentDidMount = AppStore.refresh;
public componentDidMount = () => this.props.appStore.refresh();
public render() {
const {createDialog, deleteId} = this;
const apps = AppStore.getItems();
const {
createDialog,
deleteId,
props: {appStore},
} = this;
const apps = appStore.getItems();
return (
<DefaultPage
title="Applications"
@ -79,15 +83,15 @@ class Applications extends Component {
{createDialog && (
<AddApplicationDialog
fClose={() => (this.createDialog = false)}
fOnSubmit={AppStore.create}
fOnSubmit={appStore.create}
/>
)}
{deleteId !== false && (
<ConfirmDialog
title="Confirm Delete"
text={'Delete ' + AppStore.getByID(deleteId).name + '?'}
text={'Delete ' + appStore.getByID(deleteId).name + '?'}
fClose={() => (this.deleteId = false)}
fOnSubmit={() => AppStore.remove(deleteId)}
fOnSubmit={() => appStore.remove(deleteId)}
/>
)}
</DefaultPage>
@ -107,7 +111,7 @@ class Applications extends Component {
return;
}
if (['image/png', 'image/jpeg', 'image/gif'].indexOf(file.type) !== -1) {
AppStore.uploadImage(this.uploadId, file);
this.props.appStore.uploadImage(this.uploadId, file);
} else {
alert('Uploaded file must be of type png, jpeg or gif.');
}
@ -146,4 +150,4 @@ const Row: SFC<IRowProps> = observer(({name, value, description, fDelete, fUploa
</TableRow>
));
export default Applications;
export default inject('appStore')(Applications);

View File

@ -12,22 +12,26 @@ import ConfirmDialog from '../component/ConfirmDialog';
import DefaultPage from '../component/DefaultPage';
import ToggleVisibility from '../component/ToggleVisibility';
import AddClientDialog from './dialog/AddClientDialog';
import ClientStore from '../stores/ClientStore';
import {observer} from 'mobx-react';
import {observable} from 'mobx';
import {inject, Stores} from '../inject';
@observer
class Clients extends Component {
class Clients extends Component<Stores<'clientStore'>> {
@observable
private showDialog = false;
@observable
private deleteId: false | number = false;
public componentDidMount = ClientStore.refresh;
public componentDidMount = () => this.props.clientStore.refresh();
public render() {
const {deleteId, showDialog} = this;
const clients = ClientStore.getItems();
const {
deleteId,
showDialog,
props: {clientStore},
} = this;
const clients = clientStore.getItems();
return (
<DefaultPage
@ -63,15 +67,15 @@ class Clients extends Component {
{showDialog && (
<AddClientDialog
fClose={() => (this.showDialog = false)}
fOnSubmit={ClientStore.create}
fOnSubmit={clientStore.create}
/>
)}
{deleteId !== false && (
<ConfirmDialog
title="Confirm Delete"
text={'Delete ' + ClientStore.getByID(deleteId).name + '?'}
text={'Delete ' + clientStore.getByID(deleteId).name + '?'}
fClose={() => (this.deleteId = false)}
fOnSubmit={() => ClientStore.remove(deleteId)}
fOnSubmit={() => clientStore.remove(deleteId)}
/>
)}
</DefaultPage>
@ -102,4 +106,4 @@ const Row: SFC<IRowProps> = ({name, value, fDelete}) => (
</TableRow>
);
export default Clients;
export default inject('clientStore')(Clients);

View File

@ -4,12 +4,12 @@ import TextField from '@material-ui/core/TextField';
import React, {Component, FormEvent} from 'react';
import Container from '../component/Container';
import DefaultPage from '../component/DefaultPage';
import {currentUser} from '../stores/CurrentUser';
import {observable} from 'mobx';
import {observer} from 'mobx-react';
import {inject, Stores} from '../inject';
@observer
class Login extends Component {
class Login extends Component<Stores<'currentUser'>> {
@observable
private username = '';
@observable
@ -57,10 +57,10 @@ class Login extends Component {
private login = (e: React.MouseEvent<HTMLInputElement>) => {
e.preventDefault();
currentUser.login(this.username, this.password);
this.props.currentUser.login(this.username, this.password);
};
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
}
export default Login;
export default inject('currentUser')(Login);

View File

@ -5,11 +5,10 @@ import React, {Component} from 'react';
import {RouteComponentProps} from 'react-router';
import DefaultPage from '../component/DefaultPage';
import Message from '../component/Message';
import AppStore from '../stores/AppStore';
import MessagesStore from '../stores/MessagesStore';
import {observer} from 'mobx-react';
// @ts-ignore
import InfiniteAnyHeight from 'react-infinite-any-height';
import {inject, Stores} from '../inject';
interface IProps extends RouteComponentProps<{id: string}> {}
@ -18,7 +17,7 @@ interface IState {
}
@observer
class Messages extends Component<IProps, IState> {
class Messages extends Component<IProps & Stores<'messagesStore' | 'appStore'>, IState> {
private static appId(props: IProps) {
if (props === undefined) {
return -1;
@ -31,7 +30,7 @@ class Messages extends Component<IProps, IState> {
private isLoadingMore = false;
public componentWillReceiveProps(nextProps: IProps) {
public componentWillReceiveProps(nextProps: IProps & Stores<'messagesStore' | 'appStore'>) {
this.updateAllWithProps(nextProps);
}
@ -49,9 +48,10 @@ class Messages extends Component<IProps, IState> {
public render() {
const {appId} = this.state;
const messages = MessagesStore.get(appId);
const hasMore = MessagesStore.canLoadMore(appId);
const name = AppStore.getName(appId);
const {messagesStore, appStore} = this.props;
const messages = messagesStore.get(appId);
const hasMore = messagesStore.canLoadMore(appId);
const name = appStore.getName(appId);
const hasMessages = messages.length !== 0;
return (
@ -59,7 +59,7 @@ class Messages extends Component<IProps, IState> {
title={name}
buttonTitle="Delete All"
buttonId="delete-all"
fButton={() => MessagesStore.removeByApp(appId)}
fButton={() => messagesStore.removeByApp(appId)}
buttonDisabled={!hasMessages}>
{hasMessages ? (
<div style={{width: '100%'}} id="messages">
@ -84,18 +84,19 @@ class Messages extends Component<IProps, IState> {
);
}
private updateAllWithProps = (props: IProps) => {
private updateAllWithProps = (props: IProps & Stores<'messagesStore'>) => {
const appId = Messages.appId(props);
console.log('props', props);
this.setState({appId});
if (!MessagesStore.exists(appId)) {
MessagesStore.loadMore(appId);
if (!props.messagesStore.exists(appId)) {
props.messagesStore.loadMore(appId);
}
};
private updateAll = () => this.updateAllWithProps(this.props);
private deleteMessage = (message: IMessage) => () => MessagesStore.removeSingle(message);
private deleteMessage = (message: IMessage) => () =>
this.props.messagesStore.removeSingle(message);
private renderMessage = (message: IMessage) => {
this.checkIfLoadMore();
@ -113,9 +114,9 @@ class Messages extends Component<IProps, IState> {
private checkIfLoadMore() {
const {appId} = this.state;
if (!this.isLoadingMore && MessagesStore.canLoadMore(appId)) {
if (!this.isLoadingMore && this.props.messagesStore.canLoadMore(appId)) {
this.isLoadingMore = true;
MessagesStore.loadMore(appId).then(() => (this.isLoadingMore = false));
this.props.messagesStore.loadMore(appId).then(() => (this.isLoadingMore = false));
}
}
@ -128,4 +129,4 @@ class Messages extends Component<IProps, IState> {
);
}
export default Messages;
export default inject('messagesStore', 'appStore')(Messages);

View File

@ -13,9 +13,9 @@ import React, {Component, SFC} from 'react';
import ConfirmDialog from '../component/ConfirmDialog';
import DefaultPage from '../component/DefaultPage';
import AddEditDialog from './dialog/AddEditUserDialog';
import UserStore from '../stores/UserStore';
import {observer} from 'mobx-react';
import {observable} from 'mobx';
import {inject, Stores} from '../inject';
const styles = () => ({
wrapper: {
@ -47,7 +47,7 @@ const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
);
@observer
class Users extends Component<WithStyles<'wrapper'>> {
class Users extends Component<WithStyles<'wrapper'> & Stores<'userStore'>> {
@observable
private createDialog = false;
@observable
@ -55,11 +55,16 @@ class Users extends Component<WithStyles<'wrapper'>> {
@observable
private editId: number | false = false;
public componentDidMount = UserStore.refresh;
public componentDidMount = () => this.props.userStore.refresh();
public render() {
const users = UserStore.getItems();
const {deleteId, editId, createDialog} = this;
const {
deleteId,
editId,
createDialog,
props: {userStore},
} = this;
const users = userStore.getItems();
return (
<DefaultPage
title="Users"
@ -95,24 +100,24 @@ class Users extends Component<WithStyles<'wrapper'>> {
{createDialog && (
<AddEditDialog
fClose={() => (this.createDialog = false)}
fOnSubmit={UserStore.create}
fOnSubmit={userStore.create}
/>
)}
{editId !== false && (
<AddEditDialog
fClose={() => (this.editId = false)}
fOnSubmit={UserStore.update.bind(this, editId)}
name={UserStore.getByID(editId).name}
admin={UserStore.getByID(editId).admin}
fOnSubmit={userStore.update.bind(this, editId)}
name={userStore.getByID(editId).name}
admin={userStore.getByID(editId).admin}
isEdit={true}
/>
)}
{deleteId !== false && (
<ConfirmDialog
title="Confirm Delete"
text={'Delete ' + UserStore.getByID(deleteId).name + '?'}
text={'Delete ' + userStore.getByID(deleteId).name + '?'}
fClose={() => (this.deleteId = false)}
fOnSubmit={() => UserStore.remove(deleteId)}
fOnSubmit={() => userStore.remove(deleteId)}
/>
)}
</DefaultPage>
@ -120,4 +125,4 @@ class Users extends Component<WithStyles<'wrapper'>> {
}
}
export default withStyles(styles)(Users);
export default withStyles(styles)(inject('userStore')(Users));