diff --git a/ui/src/Layout.tsx b/ui/src/Layout.tsx index 9088d38..5ae9591 100644 --- a/ui/src/Layout.tsx +++ b/ui/src/Layout.tsx @@ -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> { +class Layout extends React.Component & Stores<'currentUser'>> { private static defaultVersion = '0.0.0'; @observable @@ -59,14 +59,16 @@ class Layout extends React.Component> { } 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 ? : ); return ( @@ -81,6 +83,7 @@ class Layout extends React.Component> { loggedIn={loggedIn} toggleTheme={() => (this.darkThemeVisible = !this.darkThemeVisible)} showSettings={() => (this.showSettings = true)} + logout={logout} /> @@ -112,4 +115,4 @@ class Layout extends React.Component> { } } -export default withStyles(styles, {withTheme: true})<{}>(Layout); +export default withStyles(styles, {withTheme: true})<{}>(inject('currentUser')(Layout)); diff --git a/ui/src/actions/axios.ts b/ui/src/actions/axios.ts new file mode 100644 index 0000000..d6272eb --- /dev/null +++ b/ui/src/actions/axios.ts @@ -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); + }); +}; diff --git a/ui/src/actions/defaultAxios.ts b/ui/src/actions/defaultAxios.ts deleted file mode 100644 index 6e74259..0000000 --- a/ui/src/actions/defaultAxios.ts +++ /dev/null @@ -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); -}); diff --git a/ui/src/component/Header.tsx b/ui/src/component/Header.tsx index 6dd1fcf..afec630 100644 --- a/ui/src/component/Header.tsx +++ b/ui/src/component/Header.tsx @@ -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 { public render() { - const {classes, version, name, loggedIn, admin, toggleTheme} = this.props; + const {classes, version, name, loggedIn, admin, toggleTheme, logout} = this.props; return ( @@ -69,7 +69,7 @@ class Header extends Component { - {loggedIn && this.renderButtons(name, admin)} + {loggedIn && this.renderButtons(name, admin, logout)} @@ -78,7 +78,7 @@ class Header extends Component { ); } - private renderButtons(name: string, admin: boolean) { + private renderButtons(name: string, admin: boolean, logout: VoidFunction) { const {classes, showSettings} = this.props; return (
@@ -109,7 +109,7 @@ class Header extends Component {   {name} - diff --git a/ui/src/component/Navigation.tsx b/ui/src/component/Navigation.tsx index 4ad4fce..cfcb399 100644 --- a/ui/src/component/Navigation.tsx +++ b/ui/src/component/Navigation.tsx @@ -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 { +class Navigation extends Component> { 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 { } } -export default withStyles(styles, {withTheme: true})(Navigation); +export default withStyles(styles, {withTheme: true})(inject('appStore')(Navigation)); diff --git a/ui/src/component/SettingsDialog.tsx b/ui/src/component/SettingsDialog.tsx index c206d65..1aeed48 100644 --- a/ui/src/component/SettingsDialog.tsx +++ b/ui/src/component/SettingsDialog.tsx @@ -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 { +class SettingsDialog extends Component> { @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 { ); } } + +export default inject('currentUser')(SettingsDialog); diff --git a/ui/src/component/SnackBarHandler.tsx b/ui/src/component/SnackBarHandler.tsx index 38a76e8..5d08fb9 100644 --- a/ui/src/component/SnackBarHandler.tsx +++ b/ui/src/component/SnackBarHandler.tsx @@ -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> { 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); diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8ee5315..7b631a1 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -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(, document.getElementById('root')); + stores.currentUser.tryAuthenticate(); + + ReactDOM.render( + + + , + document.getElementById('root') + ); registerServiceWorker(); })(); diff --git a/ui/src/pages/Applications.tsx b/ui/src/pages/Applications.tsx index 4aaea65..fd5683d 100644 --- a/ui/src/pages/Applications.tsx +++ b/ui/src/pages/Applications.tsx @@ -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> { @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 ( (this.createDialog = false)} - fOnSubmit={AppStore.create} + fOnSubmit={appStore.create} /> )} {deleteId !== false && ( (this.deleteId = false)} - fOnSubmit={() => AppStore.remove(deleteId)} + fOnSubmit={() => appStore.remove(deleteId)} /> )} @@ -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 = observer(({name, value, description, fDelete, fUploa )); -export default Applications; +export default inject('appStore')(Applications); diff --git a/ui/src/pages/Clients.tsx b/ui/src/pages/Clients.tsx index 36ccfbd..ead68c6 100644 --- a/ui/src/pages/Clients.tsx +++ b/ui/src/pages/Clients.tsx @@ -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> { @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 ( (this.showDialog = false)} - fOnSubmit={ClientStore.create} + fOnSubmit={clientStore.create} /> )} {deleteId !== false && ( (this.deleteId = false)} - fOnSubmit={() => ClientStore.remove(deleteId)} + fOnSubmit={() => clientStore.remove(deleteId)} /> )} @@ -102,4 +106,4 @@ const Row: SFC = ({name, value, fDelete}) => ( ); -export default Clients; +export default inject('clientStore')(Clients); diff --git a/ui/src/pages/Login.tsx b/ui/src/pages/Login.tsx index b3a0d34..8f8c2d5 100644 --- a/ui/src/pages/Login.tsx +++ b/ui/src/pages/Login.tsx @@ -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> { @observable private username = ''; @observable @@ -57,10 +57,10 @@ class Login extends Component { private login = (e: React.MouseEvent) => { e.preventDefault(); - currentUser.login(this.username, this.password); + this.props.currentUser.login(this.username, this.password); }; private preventDefault = (e: FormEvent) => e.preventDefault(); } -export default Login; +export default inject('currentUser')(Login); diff --git a/ui/src/pages/Messages.tsx b/ui/src/pages/Messages.tsx index ec1d12d..f8052b8 100644 --- a/ui/src/pages/Messages.tsx +++ b/ui/src/pages/Messages.tsx @@ -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 { +class Messages extends Component, IState> { private static appId(props: IProps) { if (props === undefined) { return -1; @@ -31,7 +30,7 @@ class Messages extends Component { 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 { 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 { title={name} buttonTitle="Delete All" buttonId="delete-all" - fButton={() => MessagesStore.removeByApp(appId)} + fButton={() => messagesStore.removeByApp(appId)} buttonDisabled={!hasMessages}> {hasMessages ? (
@@ -84,18 +84,19 @@ class Messages extends Component { ); } - 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 { 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 { ); } -export default Messages; +export default inject('messagesStore', 'appStore')(Messages); diff --git a/ui/src/pages/Users.tsx b/ui/src/pages/Users.tsx index 7e8ad3b..b5b1b13 100644 --- a/ui/src/pages/Users.tsx +++ b/ui/src/pages/Users.tsx @@ -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 = ({name, admin, fDelete, fEdit}) => ( ); @observer -class Users extends Component> { +class Users extends Component & Stores<'userStore'>> { @observable private createDialog = false; @observable @@ -55,11 +55,16 @@ class Users extends Component> { @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 ( > { {createDialog && ( (this.createDialog = false)} - fOnSubmit={UserStore.create} + fOnSubmit={userStore.create} /> )} {editId !== false && ( (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 && ( (this.deleteId = false)} - fOnSubmit={() => UserStore.remove(deleteId)} + fOnSubmit={() => userStore.remove(deleteId)} /> )} @@ -120,4 +125,4 @@ class Users extends Component> { } } -export default withStyles(styles)(Users); +export default withStyles(styles)(inject('userStore')(Users));