This commit is contained in:
Jannis Mattheis 2018-08-22 20:17:51 +02:00
parent 1afa51959f
commit a3f081307b
38 changed files with 729 additions and 428 deletions

View File

@ -38,14 +38,14 @@ const styles = (theme: Theme) => ({
});
interface IState {
darkTheme: boolean
redirect: boolean
showSettings: boolean
loggedIn: boolean
admin: boolean
name: string
authenticating: boolean
version: string
darkTheme: boolean;
redirect: boolean;
showSettings: boolean;
loggedIn: boolean;
admin: boolean;
name: string;
authenticating: boolean;
version: string;
}
class Layout extends React.Component<WithStyles<'content'>, IState> {
@ -97,19 +97,29 @@ class Layout extends React.Component<WithStyles<'content'>, IState> {
const {name, admin, version, loggedIn, showSettings, authenticating} = this.state;
const {classes} = this.props;
const theme = this.state.darkTheme ? darkTheme : lightTheme;
const loginRoute = () => (loggedIn ? (<Redirect to="/"/>) : (<Login/>));
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
return (
<MuiThemeProvider theme={theme}>
<HashRouter>
<div style={{display: 'flex'}}>
<CssBaseline />
<Header admin={admin} name={name} version={version} loggedIn={loggedIn}
toggleTheme={this.toggleTheme} showSettings={this.showSettings}/>
<Header
admin={admin}
name={name}
version={version}
loggedIn={loggedIn}
toggleTheme={this.toggleTheme}
showSettings={this.showSettings}
/>
<Navigation loggedIn={loggedIn} />
<main className={classes.content}>
<Switch>
{authenticating ? <Route path="/"><LoadingSpinner/></Route> : null}
{authenticating ? (
<Route path="/">
<LoadingSpinner />
</Route>
) : null}
<Route exact path="/login" render={loginRoute} />
{loggedIn ? null : <Redirect to="/login" />}
<Route exact path="/" component={Messages} />

View File

@ -15,10 +15,13 @@ export function fetchApps() {
* @param {int} id the application id
*/
export function deleteApp(id: number) {
axios.delete(config.get('url') + 'application/' + id).then(() => {
axios
.delete(config.get('url') + 'application/' + id)
.then(() => {
fetchApps();
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
}).then(() => snack('Application deleted'));
})
.then(() => snack('Application deleted'));
}
/**
@ -27,7 +30,8 @@ export function deleteApp(id: number) {
* @param {string} description the description of the application.
*/
export function createApp(name: string, description: string) {
axios.post(config.get('url') + 'application', {name, description})
axios
.post(config.get('url') + 'application', {name, description})
.then(fetchApps)
.then(() => snack('Application created'));
}
@ -40,7 +44,10 @@ export function createApp(name: string, description: string) {
export function uploadImage(id: number, file: Blob) {
const formData = new FormData();
formData.append('file', file);
axios.post(config.get('url') + 'application/' + id + '/image', formData,
{headers: {'content-type': 'multipart/form-data'}}).then(fetchApps)
axios
.post(config.get('url') + 'application/' + id + '/image', formData, {
headers: {'content-type': 'multipart/form-data'},
})
.then(fetchApps)
.then(() => snack('Application image updated'));
}

View File

@ -18,7 +18,10 @@ export function fetchClients() {
* @param {int} id the client id
*/
export function deleteClient(id: number) {
axios.delete(config.get('url') + 'client/' + id).then(fetchClients).then(() => snack('Client deleted'));
axios
.delete(config.get('url') + 'client/' + id)
.then(fetchClients)
.then(() => snack('Client deleted'));
}
/**
@ -26,5 +29,8 @@ export function deleteClient(id: number) {
* @param {string} name the client name
*/
export function createClient(name: string) {
axios.post(config.get('url') + 'client', {name}).then(fetchClients).then(() => snack('Client created'));
axios
.post(config.get('url') + 'client', {name})
.then(fetchClients)
.then(() => snack('Client created'));
}

View File

@ -1,4 +1,4 @@
import {AxiosResponse} from "axios";
import {AxiosResponse} from 'axios';
import dispatcher from '../stores/dispatcher';
import * as AppAction from './AppAction';
import * as ClientAction from './ClientAction';

View File

@ -7,11 +7,14 @@ import * as UserAction from './UserAction';
export function fetchMessagesApp(id: number, since: number) {
if (id === -1) {
return axios.get(config.get('url') + 'message?since=' + since).then((resp: AxiosResponse<IPagedMessages>) => {
return axios
.get(config.get('url') + 'message?since=' + since)
.then((resp: AxiosResponse<IPagedMessages>) => {
newMessages(-1, resp.data);
});
} else {
return axios.get(config.get('url') + 'application/' + id + '/message?since=' + since)
return axios
.get(config.get('url') + 'application/' + id + '/message?since=' + since)
.then((resp: AxiosResponse<IPagedMessages>) => {
newMessages(id, resp.data);
});
@ -20,7 +23,8 @@ export function fetchMessagesApp(id: number, since: number) {
function newMessages(id: number, data: IPagedMessages) {
dispatcher.dispatch({
type: 'UPDATE_MESSAGES', payload: {
type: 'UPDATE_MESSAGES',
payload: {
messages: data.messages,
hasMore: 'next' in data.paging,
nextSince: data.paging.since,
@ -40,8 +44,7 @@ export function deleteMessagesByApp(id: number) {
snack('Messages deleted');
});
} else {
axios.delete(config.get('url') + 'application/' + id + '/message')
.then(() => {
axios.delete(config.get('url') + 'application/' + id + '/message').then(() => {
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
snack('Deleted all messages from the application');
});
@ -66,7 +69,10 @@ export function listenToWebSocket() {
}
wsActive = true;
const wsUrl = config.get('url').replace('http', 'ws').replace('https', 'wss');
const wsUrl = config
.get('url')
.replace('http', 'ws')
.replace('https', 'wss');
const ws = new WebSocket(wsUrl + 'stream?token=' + getToken());
ws.onerror = (e) => {
@ -74,7 +80,8 @@ export function listenToWebSocket() {
console.log('WebSocket connection errored', e);
};
ws.onmessage = (data) => dispatcher.dispatch({type: 'ONE_MESSAGE', payload: JSON.parse(data.data) as IMessage});
ws.onmessage = (data) =>
dispatcher.dispatch({type: 'ONE_MESSAGE', payload: JSON.parse(data.data) as IMessage});
ws.onclose = () => {
wsActive = false;

View File

@ -16,17 +16,26 @@ export function login(username: string, password: string) {
const browser = detect();
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
authenticating();
axios.create().request({
axios
.create()
.request({
url: config.get('url') + 'client',
method: 'POST',
data: {name},
auth: {username, password},
}).then((resp) => {
})
.then((resp) => {
snack(`A client named '${name}' was created for your session.`);
setAuthorizationToken(resp.data.token);
tryAuthenticate().then(GlobalAction.initialLoad)
.catch(() => console.log('create client succeeded, but authenticated with given token failed'));
}).catch(() => {
tryAuthenticate()
.then(GlobalAction.initialLoad)
.catch(() =>
console.log(
'create client succeeded, but authenticated with given token failed'
)
);
})
.catch(() => {
snack('Login failed');
noAuthentication();
});
@ -44,10 +53,14 @@ export function logout() {
}
export function tryAuthenticate() {
return axios.create().get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': getToken()}}).then((resp) => {
return axios
.create()
.get(config.get('url') + 'current/user', {headers: {'X-Gotify-Key': getToken()}})
.then((resp) => {
dispatcher.dispatch({type: 'AUTHENTICATED', payload: resp.data});
return resp;
}).catch((resp) => {
})
.catch((resp) => {
if (getToken()) {
setAuthorizationToken(null);
snack('Authentication failed, try to re-login. (client or user was deleted)');
@ -80,7 +93,9 @@ function authenticating() {
* @param {string} pass
*/
export function changeCurrentUser(pass: string) {
axios.post(config.get('url') + 'current/user/password', {pass}).then(() => snack('Password changed'));
axios
.post(config.get('url') + 'current/user/password', {pass})
.then(() => snack('Password changed'));
}
/** Fetches all users. */
@ -95,7 +110,10 @@ export function fetchUsers() {
* @param {int} id the user id
*/
export function deleteUser(id: number) {
axios.delete(config.get('url') + 'user/' + id).then(fetchUsers).then(() => snack('User deleted'));
axios
.delete(config.get('url') + 'user/' + id)
.then(fetchUsers)
.then(() => snack('User deleted'));
}
/**
@ -105,7 +123,10 @@ export function deleteUser(id: number) {
* @param {bool} admin if true, the user is an administrator
*/
export function createUser(name: string, pass: string, admin: boolean) {
axios.post(config.get('url') + 'user', {name, pass, admin}).then(fetchUsers).then(() => snack('User created'));
axios
.post(config.get('url') + 'user', {name, pass, admin})
.then(fetchUsers)
.then(() => snack('User created'));
}
/**

View File

@ -1,12 +1,17 @@
import Button from 'material-ui/Button';
import Dialog, {DialogActions, DialogContent, DialogContentText, DialogTitle} from 'material-ui/Dialog';
import Dialog, {
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from 'material-ui/Dialog';
import React from 'react';
interface IProps {
title: string
text: string
fClose: VoidFunction
fOnSubmit: VoidFunction
title: string;
text: string;
fClose: VoidFunction;
fOnSubmit: VoidFunction;
}
export default function ConfirmDialog({title, text, fClose, fOnSubmit}: IProps) {
@ -22,7 +27,9 @@ export default function ConfirmDialog({title, text, fClose, fOnSubmit}: IProps)
</DialogContent>
<DialogActions>
<Button onClick={fClose}>No</Button>
<Button onClick={submitAndClose} color="primary" variant="raised">Yes</Button>
<Button onClick={submitAndClose} color="primary" variant="raised">
Yes
</Button>
</DialogActions>
</Dialog>
);

View File

@ -1,4 +1,4 @@
import {WithStyles} from "material-ui";
import {WithStyles} from 'material-ui';
import Paper from 'material-ui/Paper';
import {withStyles} from 'material-ui/styles';
import * as React from 'react';
@ -10,7 +10,7 @@ const styles = () => ({
});
interface IProps {
style?: object,
style?: object;
}
const Container: React.SFC<IProps & WithStyles<'paper'>> = ({classes, children, style}) => {

View File

@ -4,23 +4,38 @@ import Typography from 'material-ui/Typography';
import React, {SFC} from 'react';
interface IProps {
title: string
buttonTitle?: string
fButton?: VoidFunction
buttonDisabled?: boolean
maxWidth?: number
hideButton?: boolean
title: string;
buttonTitle?: string;
fButton?: VoidFunction;
buttonDisabled?: boolean;
maxWidth?: number;
hideButton?: boolean;
}
const DefaultPage: SFC<IProps> = ({title, buttonTitle, fButton, buttonDisabled = false, maxWidth = 700, hideButton, children}) => (
const DefaultPage: SFC<IProps> = ({
title,
buttonTitle,
fButton,
buttonDisabled = false,
maxWidth = 700,
hideButton,
children,
}) => (
<main style={{margin: '0 auto', maxWidth}}>
<Grid container spacing={24}>
<Grid item xs={12} style={{display: 'flex'}}>
<Typography variant="display1" style={{flex: 1}}>
{title}
</Typography>
{hideButton ? null : <Button variant="raised" color="primary" disabled={buttonDisabled}
onClick={fButton}>{buttonTitle}</Button>}
{hideButton ? null : (
<Button
variant="raised"
color="primary"
disabled={buttonDisabled}
onClick={fButton}>
{buttonTitle}
</Button>
)}
</Grid>
{children}
</Grid>

View File

@ -16,7 +16,6 @@ class FixedReactList extends ReactList {
super.cacheSizes();
}
public clearCacheFromIndex(startIndex: number): void {
this.ignoreNextCacheUpdate = true;
@ -25,16 +24,19 @@ class FixedReactList extends ReactList {
this.cache = {};
} else {
// @ts-ignore accessing private member
Object.keys(this.cache).filter((index) => index >= startIndex).forEach((index) => {
Object.keys(this.cache)
.filter((index) => +index >= startIndex)
.forEach((index) => {
// @ts-ignore accessing private member
delete this.cache[index];
});
}
};
}
public componentDidUpdate() {
const hasCacheForLastRenderedItem =
// @ts-ignore accessing private member
const hasCacheForLastRenderedItem = Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]];
Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]];
// @ts-ignore accessing private member
super.componentDidUpdate();
if (!hasCacheForLastRenderedItem) {

View File

@ -1,4 +1,4 @@
import {Theme, WithStyles} from "material-ui";
import {Theme, WithStyles} from 'material-ui';
import AccountCircle from 'material-ui-icons/AccountCircle';
import Chat from 'material-ui-icons/Chat';
import DevicesOther from 'material-ui-icons/DevicesOther';
@ -33,15 +33,15 @@ const styles = (theme: Theme) => ({
},
});
type Styles = WithStyles<'link' | 'titleName' | 'title' | 'appBar'>
type Styles = WithStyles<'link' | 'titleName' | 'title' | 'appBar'>;
interface IProps {
loggedIn: boolean
name: string
admin: boolean
version: string
toggleTheme: VoidFunction
showSettings: VoidFunction
loggedIn: boolean;
name: string;
admin: boolean;
version: string;
toggleTheme: VoidFunction;
showSettings: VoidFunction;
}
class Header extends Component<IProps & Styles> {
@ -53,18 +53,25 @@ class Header extends Component<IProps & Styles> {
<Toolbar>
<div className={classes.title}>
<a href="https://github.com/gotify/server" className={classes.link}>
<Typography variant="headline" className={classes.titleName} color="inherit">
<Typography
variant="headline"
className={classes.titleName}
color="inherit">
Gotify
</Typography>
</a>
<a href={'https://github.com/gotify/server/releases/tag/v' + version} className={classes.link}>
<a
href={'https://github.com/gotify/server/releases/tag/v' + version}
className={classes.link}>
<Typography variant="button" color="inherit">
@{version}
</Typography>
</a>
</div>
{loggedIn && this.renderButtons(name, admin)}
<IconButton onClick={toggleTheme} color="inherit"><LightbulbOutline/></IconButton>
<IconButton onClick={toggleTheme} color="inherit">
<LightbulbOutline />
</IconButton>
</Toolbar>
</AppBar>
);
@ -74,18 +81,37 @@ class Header extends Component<IProps & Styles> {
const {classes, showSettings} = this.props;
return (
<div>
{admin
? <Link className={classes.link} to="/users">
<Button color="inherit"><SupervisorAccount/>&nbsp;users</Button></Link>
: ''}
{admin ? (
<Link className={classes.link} to="/users">
<Button color="inherit">
<SupervisorAccount />
&nbsp;users
</Button>
</Link>
) : (
''
)}
<Link className={classes.link} to="/applications">
<Button color="inherit"><Chat/>&nbsp;apps</Button>
<Button color="inherit">
<Chat />
&nbsp;apps
</Button>
</Link>
<Link className={classes.link} to="/clients"><Button color="inherit">
<DevicesOther/>&nbsp;clients</Button>
<Link className={classes.link} to="/clients">
<Button color="inherit">
<DevicesOther />
&nbsp;clients
</Button>
</Link>
<Button color="inherit" onClick={showSettings}><AccountCircle/>&nbsp;{name}</Button>
<Button color="inherit" onClick={UserAction.logout}><ExitToApp/>&nbsp;Logout</Button>
<Button color="inherit" onClick={showSettings}>
<AccountCircle />
&nbsp;
{name}
</Button>
<Button color="inherit" onClick={UserAction.logout}>
<ExitToApp />
&nbsp;Logout
</Button>
</div>
);
}

View File

@ -11,4 +11,4 @@ export default function LoadingSpinner() {
</Grid>
</DefaultPage>
);
};
}

View File

@ -1,4 +1,4 @@
import {WithStyles} from "material-ui";
import {WithStyles} from 'material-ui';
import Delete from 'material-ui-icons/Delete';
import IconButton from 'material-ui/IconButton';
import {withStyles} from 'material-ui/styles';
@ -32,14 +32,22 @@ const styles = () => ({
},
});
type Style = WithStyles<'header' | 'headerTitle' | 'trash' | 'wrapperPadding' | 'messageContentWrapper' | 'image' | 'imageWrapper'>;
type Style = WithStyles<
| 'header'
| 'headerTitle'
| 'trash'
| 'wrapperPadding'
| 'messageContentWrapper'
| 'image'
| 'imageWrapper'
>;
interface IProps {
title: string
image?: string
date: string
content: string
fDelete: VoidFunction
title: string;
image?: string;
date: string;
content: string;
fDelete: VoidFunction;
}
function Message({fDelete, classes, title, date, content, image}: IProps & Style) {
@ -47,7 +55,13 @@ function Message({fDelete, classes, title, date, content, image}: IProps & Style
<div className={classes.wrapperPadding}>
<Container style={{display: 'flex'}}>
<div className={classes.imageWrapper}>
<img src={image} alt="app logo" width="70" height="70" className={classes.image}/>
<img
src={image}
alt="app logo"
width="70"
height="70"
className={classes.image}
/>
</div>
<div className={classes.messageContentWrapper}>
<div className={classes.header}>
@ -57,7 +71,9 @@ function Message({fDelete, classes, title, date, content, image}: IProps & Style
<Typography variant="body1">
<TimeAgo date={date} />
</Typography>
<IconButton onClick={fDelete} className={classes.trash}><Delete/></IconButton>
<IconButton onClick={fDelete} className={classes.trash}>
<Delete />
</IconButton>
</div>
<Typography component="p">{content}</Typography>
</div>

View File

@ -1,4 +1,4 @@
import {Theme, WithStyles} from "material-ui";
import {Theme, WithStyles} from 'material-ui';
import Divider from 'material-ui/Divider';
import Drawer from 'material-ui/Drawer';
import {ListItem, ListItemText} from 'material-ui/List';
@ -20,14 +20,14 @@ const styles = (theme: Theme) => ({
},
});
type Styles = WithStyles<'drawerPaper' | 'toolbar' | 'link'>
type Styles = WithStyles<'drawerPaper' | 'toolbar' | 'link'>;
interface IProps {
loggedIn: boolean
loggedIn: boolean;
}
interface IState {
apps: IApplication[]
apps: IApplication[];
}
class Navigation extends Component<IProps & Styles, IState> {
@ -45,7 +45,10 @@ class Navigation extends Component<IProps & Styles, IState> {
const {classes, loggedIn} = this.props;
const {apps} = this.state;
const userApps = apps.length === 0 ? null : apps.map((app) => {
const userApps =
apps.length === 0
? null
: apps.map((app) => {
return (
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
<ListItem button>

View File

@ -5,7 +5,9 @@ import React, {Component} from 'react';
class ScrollUpButton extends Component {
public render() {
return (
<Button variant="fab" color="primary"
<Button
variant="fab"
color="primary"
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
onClick={this.scrollUp}>
<KeyboardArrowUp />

View File

@ -6,11 +6,11 @@ import React, {ChangeEvent, Component} from 'react';
import * as UserAction from '../actions/UserAction';
interface IState {
pass: string
pass: string;
}
interface IProps {
fClose: VoidFunction
fClose: VoidFunction;
}
export default class SettingsDialog extends Component<IProps, IState> {
@ -27,14 +27,24 @@ export default class SettingsDialog extends Component<IProps, IState> {
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Change Password</DialogTitle>
<DialogContent>
<TextField autoFocus margin="dense" type="password" label="New Pass *" value={pass}
onChange={this.handleChange.bind(this, 'pass')} fullWidth/>
<TextField
autoFocus
margin="dense"
type="password"
label="New Pass *"
value={pass}
onChange={this.handleChange.bind(this, 'pass')}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip title={pass.length !== 0 ? '' : 'pass is required'}>
<div>
<Button disabled={pass.length === 0} onClick={submitAndClose} color="primary"
<Button
disabled={pass.length === 0}
onClick={submitAndClose}
color="primary"
variant="raised">
Change
</Button>

View File

@ -4,12 +4,11 @@ import Snackbar from 'material-ui/Snackbar';
import React, {Component} from 'react';
import SnackBarStore from '../stores/SnackBarStore';
interface IState {
current: string
hasNext: boolean
open: boolean
openWhen: number
current: string;
hasNext: boolean;
open: boolean;
openWhen: number;
}
class SnackBarHandler extends Component<{}, IState> {
@ -40,11 +39,17 @@ class SnackBarHandler extends Component<{}, IState> {
return (
<Snackbar
anchorOrigin={{vertical: 'bottom', horizontal: 'left'}}
open={open} autoHideDuration={duration}
onClose={this.closeCurrentSnack} onExited={this.openNextSnack}
open={open}
autoHideDuration={duration}
onClose={this.closeCurrentSnack}
onExited={this.openNextSnack}
message={<span id="message-id">{current}</span>}
action={
<IconButton key="close" aria-label="Close" color="inherit" onClick={this.closeCurrentSnack}>
<IconButton
key="close"
aria-label="Close"
color="inherit"
onClick={this.closeCurrentSnack}>
<Close />
</IconButton>
}
@ -64,7 +69,10 @@ class SnackBarHandler extends Component<{}, IState> {
if (snackOpenSince > SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS) {
this.closeCurrentSnack();
} else {
setTimeout(this.closeCurrentSnack, SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS - snackOpenSince);
setTimeout(
this.closeCurrentSnack,
SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS - snackOpenSince
);
}
};

View File

@ -5,12 +5,12 @@ import Typography from 'material-ui/Typography';
import React, {Component} from 'react';
interface IProps {
value: string
style?: object
value: string;
style?: object;
}
interface IState {
visible: boolean
visible: boolean;
}
class ToggleVisibility extends Component<IProps, IState> {
@ -24,9 +24,7 @@ class ToggleVisibility extends Component<IProps, IState> {
<IconButton onClick={this.toggleVisibility}>
{this.state.visible ? <VisibilityOff /> : <Visibility />}
</IconButton>
<Typography style={{fontFamily: '\'Roboto Mono\', monospace'}}>
{text}
</Typography>
<Typography style={{fontFamily: "'Roboto Mono', monospace"}}>{text}</Typography>
</div>
);
}
@ -34,5 +32,4 @@ class ToggleVisibility extends Component<IProps, IState> {
private toggleVisibility = () => this.setState({visible: !this.state.visible});
}
export default ToggleVisibility;

View File

@ -1,5 +1,5 @@
export interface IConfig {
url: string
url: string;
}
let config: IConfig;
@ -8,6 +8,6 @@ export function set(c: IConfig) {
config = c;
}
export function get(val: "url"): string {
export function get(val: 'url'): string {
return config[val];
}

View File

@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom';
import 'typeface-roboto';
import 'typeface-roboto-mono';
import * as UserAction from './actions/UserAction';
import * as config from './config'
import * as config from './config';
import Layout from './Layout';
import registerServiceWorker from './registerServiceWorker';
import * as Notifications from './stores/Notifications';
@ -38,4 +38,4 @@ declare global {
UserAction.checkIfAlreadyLoggedIn();
ReactDOM.render(<Layout />, document.getElementById('root'));
registerServiceWorker();
}());
})();

View File

@ -14,9 +14,9 @@ import AppStore from '../stores/AppStore';
import AddApplicationDialog from './dialog/AddApplicationDialog';
interface IState {
apps: IApplication[]
createDialog: boolean
deleteId: number
apps: IApplication[];
createDialog: boolean;
deleteId: number;
}
class Applications extends Component<{}, IState> {
@ -36,7 +36,10 @@ class Applications extends Component<{}, IState> {
public render() {
const {apps, createDialog, deleteId} = this.state;
return (
<DefaultPage title="Applications" buttonTitle="Create Application" maxWidth={1000}
<DefaultPage
title="Applications"
buttonTitle="Create Application"
maxWidth={1000}
fButton={this.showCreateDialog}>
<Grid item xs={12}>
<Paper elevation={6}>
@ -53,23 +56,41 @@ class Applications extends Component<{}, IState> {
<TableBody>
{apps.map((app: IApplication) => {
return (
<Row key={app.id} description={app.description} image={app.image}
name={app.name} value={app.token} fUpload={() => this.uploadImage(app.id)}
fDelete={() => this.showCloseDialog(app.id)}/>
<Row
key={app.id}
description={app.description}
image={app.image}
name={app.name}
value={app.token}
fUpload={() => this.uploadImage(app.id)}
fDelete={() => this.showCloseDialog(app.id)}
/>
);
})}
</TableBody>
</Table>
<input ref={(upload) => this.upload = upload} type="file" style={{display: 'none'}}
onChange={this.onUploadImage}/>
<input
ref={(upload) => (this.upload = upload)}
type="file"
style={{display: 'none'}}
onChange={this.onUploadImage}
/>
</Paper>
</Grid>
{createDialog && <AddApplicationDialog fClose={this.hideCreateDialog} fOnSubmit={AppAction.createApp}/>}
{deleteId !== -1 && <ConfirmDialog title="Confirm Delete"
{createDialog && (
<AddApplicationDialog
fClose={this.hideCreateDialog}
fOnSubmit={AppAction.createApp}
/>
)}
{deleteId !== -1 && (
<ConfirmDialog
title="Confirm Delete"
text={'Delete ' + AppStore.getById(deleteId).name + '?'}
fClose={this.hideCloseDialog}
fOnSubmit={() => AppAction.deleteApp(deleteId)}
/>}
/>
)}
</DefaultPage>
);
}
@ -102,19 +123,22 @@ class Applications extends Component<{}, IState> {
}
interface IRowProps {
name: string
value: string
description: string
fUpload: VoidFunction
image: string
fDelete: VoidFunction
name: string;
value: string;
description: string;
fUpload: VoidFunction;
image: string;
fDelete: VoidFunction;
}
const Row: SFC<IRowProps> = ({name, value, description, fDelete, fUpload, image}) => (
<TableRow>
<TableCell padding="checkbox">
<div style={{display: 'flex'}}>
<Avatar src={image}/><IconButton onClick={fUpload} style={{height: 40}}><Edit/></IconButton>
<Avatar src={image} />
<IconButton onClick={fUpload} style={{height: 40}}>
<Edit />
</IconButton>
</div>
</TableCell>
<TableCell>{name}</TableCell>
@ -123,7 +147,9 @@ const Row: SFC<IRowProps> = ({name, value, description, fDelete, fUpload, image}
</TableCell>
<TableCell>{description}</TableCell>
<TableCell numeric padding="none">
<IconButton onClick={fDelete}><Delete/></IconButton>
<IconButton onClick={fDelete}>
<Delete />
</IconButton>
</TableCell>
</TableRow>
);

View File

@ -12,9 +12,9 @@ import ClientStore from '../stores/ClientStore';
import AddClientDialog from './dialog/AddClientDialog';
interface IState {
clients: IClient[]
showDialog: boolean
deleteId: number
clients: IClient[];
showDialog: boolean;
deleteId: number;
}
class Clients extends Component<{}, IState> {
@ -32,7 +32,10 @@ class Clients extends Component<{}, IState> {
public render() {
const {clients, deleteId, showDialog} = this.state;
return (
<DefaultPage title="Clients" buttonTitle="Create Client" fButton={this.showCreateDialog}>
<DefaultPage
title="Clients"
buttonTitle="Create Client"
fButton={this.showCreateDialog}>
<Grid item xs={12}>
<Paper elevation={6}>
<Table>
@ -46,19 +49,32 @@ class Clients extends Component<{}, IState> {
<TableBody>
{clients.map((client: IClient) => {
return (
<Row key={client.id} name={client.name}
value={client.token} fDelete={() => this.showDeleteDialog(client.id)}/>
<Row
key={client.id}
name={client.name}
value={client.token}
fDelete={() => this.showDeleteDialog(client.id)}
/>
);
})}
</TableBody>
</Table>
</Paper>
</Grid>
{showDialog && <AddClientDialog fClose={this.hideCreateDialog} fOnSubmit={ClientAction.createClient}/>}
{deleteId !== -1 && <ConfirmDialog title="Confirm Delete"
{showDialog && (
<AddClientDialog
fClose={this.hideCreateDialog}
fOnSubmit={ClientAction.createClient}
/>
)}
{deleteId !== -1 && (
<ConfirmDialog
title="Confirm Delete"
text={'Delete ' + ClientStore.getById(this.state.deleteId).name + '?'}
fClose={this.hideDeleteDelete}
fOnSubmit={this.deleteClient}/>}
fOnSubmit={this.deleteClient}
/>
)}
</DefaultPage>
);
}
@ -75,22 +91,26 @@ class Clients extends Component<{}, IState> {
}
interface IRowProps {
name: string
value: string
fDelete: VoidFunction
name: string;
value: string;
fDelete: VoidFunction;
}
const Row: SFC<IRowProps> = ({name, value, fDelete}) => (
<TableRow>
<TableCell>{name}</TableCell>
<TableCell>
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center', width: 200}}/>
<ToggleVisibility
value={value}
style={{display: 'flex', alignItems: 'center', width: 200}}
/>
</TableCell>
<TableCell numeric padding="none">
<IconButton onClick={fDelete}><Delete/></IconButton>
<IconButton onClick={fDelete}>
<Delete />
</IconButton>
</TableCell>
</TableRow>
);
export default Clients;

View File

@ -7,8 +7,8 @@ import Container from '../component/Container';
import DefaultPage from '../component/DefaultPage';
interface IState {
username: string
password: string
username: string;
password: string;
}
class Login extends Component<{}, IState> {
@ -21,12 +21,28 @@ class Login extends Component<{}, IState> {
<Grid item xs={12} style={{textAlign: 'center'}}>
<Container>
<form onSubmit={this.preventDefault}>
<TextField id="name" label="Username" margin="dense" value={username}
onChange={this.handleChange.bind(this, 'username')}/>
<TextField type="password" id="password" label="Password" margin="normal"
value={password} onChange={this.handleChange.bind(this, 'password')}/>
<Button type="submit" variant="raised" size="large" color="primary"
style={{marginTop: 15, marginBottom: 5}} onClick={this.login}>
<TextField
id="name"
label="Username"
margin="dense"
value={username}
onChange={this.handleChange.bind(this, 'username')}
/>
<TextField
type="password"
id="password"
label="Password"
margin="normal"
value={password}
onChange={this.handleChange.bind(this, 'password')}
/>
<Button
type="submit"
variant="raised"
size="large"
color="primary"
style={{marginTop: 15, marginBottom: 5}}
onClick={this.login}>
Login
</Button>
</form>

View File

@ -2,7 +2,7 @@ import Grid from 'material-ui/Grid';
import {CircularProgress} from 'material-ui/Progress';
import Typography from 'material-ui/Typography';
import React, {Component} from 'react';
import {RouteComponentProps} from "react-router";
import {RouteComponentProps} from 'react-router';
import * as MessageAction from '../actions/MessageAction';
import DefaultPage from '../component/DefaultPage';
import ReactList from '../component/FixedReactList';
@ -10,17 +10,15 @@ import Message from '../component/Message';
import AppStore from '../stores/AppStore';
import MessageStore from '../stores/MessageStore';
interface IProps extends RouteComponentProps<any> {
}
interface IProps extends RouteComponentProps<any> {}
interface IState {
appId: number
messages: IMessage[]
name: string
hasMore: boolean
nextSince?: number
id?: number
appId: number;
messages: IMessage[];
name: string;
hasMore: boolean;
nextSince?: number;
id?: number;
}
class Messages extends Component<IProps, IState> {
@ -57,25 +55,33 @@ class Messages extends Component<IProps , IState> {
const deleteMessages = () => MessageAction.deleteMessagesByApp(appId);
return (
<DefaultPage title={name} buttonTitle="Delete All" fButton={deleteMessages} buttonDisabled={!hasMessages}>
{hasMessages
? (
<DefaultPage
title={name}
buttonTitle="Delete All"
fButton={deleteMessages}
buttonDisabled={!hasMessages}>
{hasMessages ? (
<div style={{width: '100%'}}>
<ReactList key={appId}
ref={(el: ReactList) => this.list = el}
<ReactList
key={appId}
ref={(el: ReactList) => (this.list = el)}
itemRenderer={this.renderMessage}
length={messages.length}
threshold={1000}
pageSize={30}
type='variable'
type="variable"
/>
{hasMore
? <Grid item xs={12} style={{textAlign: 'center'}}><CircularProgress size={100}/></Grid>
: this.label('You\'ve reached the end')}
{hasMore ? (
<Grid item xs={12} style={{textAlign: 'center'}}>
<CircularProgress size={100} />
</Grid>
) : (
this.label("You've reached the end")
)}
</div>
)
: this.label('No messages')
}
) : (
this.label('No messages')
)}
</DefaultPage>
);
}
@ -102,12 +108,14 @@ class Messages extends Component<IProps , IState> {
this.checkIfLoadMore();
const message: IMessage = this.state.messages[index];
return (
<Message key={key}
<Message
key={key}
fDelete={this.deleteMessage(message)}
title={message.title}
date={message.date}
content={message.message}
image={message.image}/>
image={message.image}
/>
);
};
@ -115,14 +123,18 @@ class Messages extends Component<IProps , IState> {
const {hasMore, messages, appId} = this.state;
if (hasMore) {
const [, maxRenderedIndex] = (this.list && this.list.getVisibleRange()) || [0, 0];
if (maxRenderedIndex > (messages.length - 30)) {
if (maxRenderedIndex > messages.length - 30) {
MessageStore.loadNext(appId);
}
}
}
private label = (text: string) => (
<Grid item xs={12}><Typography variant="caption" gutterBottom align="center">{text}</Typography></Grid>
<Grid item xs={12}>
<Typography variant="caption" gutterBottom align="center">
{text}
</Typography>
</Grid>
);
}

View File

@ -1,4 +1,4 @@
import {WithStyles} from "material-ui";
import {WithStyles} from 'material-ui';
import Delete from 'material-ui-icons/Delete';
import Edit from 'material-ui-icons/Edit';
import Grid from 'material-ui/Grid';
@ -11,7 +11,7 @@ import * as UserAction from '../actions/UserAction';
import ConfirmDialog from '../component/ConfirmDialog';
import DefaultPage from '../component/DefaultPage';
import UserStore from '../stores/UserStore';
import AddEditDialog from "./dialog/AddEditUserDialog";
import AddEditDialog from './dialog/AddEditUserDialog';
const styles = () => ({
wrapper: {
@ -21,10 +21,10 @@ const styles = () => ({
});
interface IRowProps {
name: string
admin: boolean
fDelete: VoidFunction
fEdit: VoidFunction
name: string;
admin: boolean;
fDelete: VoidFunction;
fEdit: VoidFunction;
}
const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
@ -32,17 +32,21 @@ const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
<TableCell>{name}</TableCell>
<TableCell>{admin ? 'Yes' : 'No'}</TableCell>
<TableCell numeric padding="none">
<IconButton onClick={fEdit}><Edit/></IconButton>
<IconButton onClick={fDelete}><Delete/></IconButton>
<IconButton onClick={fEdit}>
<Edit />
</IconButton>
<IconButton onClick={fDelete}>
<Delete />
</IconButton>
</TableCell>
</TableRow>
);
interface IState {
users: IUser[]
createDialog: boolean
deleteId: number
editId: number
users: IUser[];
createDialog: boolean;
deleteId: number;
editId: number;
}
class Users extends Component<WithStyles<'wrapper'>, IState> {
@ -74,27 +78,42 @@ class Users extends Component<WithStyles<'wrapper'>, IState> {
<TableBody>
{users.map((user: IUser) => {
return (
<UserRow key={user.id} name={user.name} admin={user.admin}
<UserRow
key={user.id}
name={user.name}
admin={user.admin}
fDelete={() => this.showDeleteDialog(user.id)}
fEdit={() => this.showEditDialog(user.id)}/>
fEdit={() => this.showEditDialog(user.id)}
/>
);
})}
</TableBody>
</Table>
</Paper>
</Grid>
{this.state.createDialog && <AddEditDialog fClose={this.hideCreateDialog}
fOnSubmit={UserAction.createUser}/>}
{editId !== -1 && <AddEditDialog fClose={this.hideEditDialog}
{this.state.createDialog && (
<AddEditDialog
fClose={this.hideCreateDialog}
fOnSubmit={UserAction.createUser}
/>
)}
{editId !== -1 && (
<AddEditDialog
fClose={this.hideEditDialog}
fOnSubmit={UserAction.updateUser.bind(this, editId)}
name={UserStore.getById(this.state.editId).name}
admin={UserStore.getById(this.state.editId).admin}
isEdit={true}/>}
{deleteId !== -1 && <ConfirmDialog title="Confirm Delete"
isEdit={true}
/>
)}
{deleteId !== -1 && (
<ConfirmDialog
title="Confirm Delete"
text={'Delete ' + UserStore.getById(this.state.deleteId).name + '?'}
fClose={this.hideDeleteDialog}
fOnSubmit={() => UserAction.deleteUser(this.state.deleteId)}
/>}
/>
)}
</DefaultPage>
);
}

View File

@ -1,17 +1,22 @@
import Button from 'material-ui/Button';
import Dialog, {DialogActions, DialogContent, DialogContentText, DialogTitle} from 'material-ui/Dialog';
import Dialog, {
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from 'material-ui/Dialog';
import TextField from 'material-ui/TextField';
import Tooltip from 'material-ui/Tooltip';
import React, {Component} from 'react';
interface IProps {
fClose: VoidFunction
fOnSubmit: (name: string, description: string) => void
fClose: VoidFunction;
fOnSubmit: (name: string, description: string) => void;
}
interface IState {
name: string
description: string
name: string;
description: string;
}
export default class AddDialog extends Component<IProps, IState> {
@ -29,17 +34,38 @@ export default class AddDialog extends Component<IProps, IState> {
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Create an application</DialogTitle>
<DialogContent>
<DialogContentText>An application is allowed to send messages.</DialogContentText>
<TextField autoFocus margin="dense" id="name" label="Name *" type="email" value={name}
onChange={this.handleChange.bind(this, 'name')} fullWidth/>
<TextField margin="dense" id="description" label="Short Description" value={description}
onChange={this.handleChange.bind(this, 'description')} fullWidth multiline/>
<DialogContentText>
An application is allowed to send messages.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="Name *"
type="email"
value={name}
onChange={this.handleChange.bind(this, 'name')}
fullWidth
/>
<TextField
margin="dense"
id="description"
label="Short Description"
value={description}
onChange={this.handleChange.bind(this, 'description')}
fullWidth
multiline
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip title={submitEnabled ? '' : 'name is required'}>
<div>
<Button disabled={!submitEnabled} onClick={submitAndClose} color="primary" variant="raised">
<Button
disabled={!submitEnabled}
onClick={submitAndClose}
color="primary"
variant="raised">
Create
</Button>
</div>

View File

@ -5,8 +5,8 @@ import Tooltip from 'material-ui/Tooltip';
import React, {Component} from 'react';
interface IProps {
fClose: VoidFunction
fOnSubmit: (name: string) => void
fClose: VoidFunction;
fOnSubmit: (name: string) => void;
}
export default class AddDialog extends Component<IProps, {name: string}> {
@ -24,14 +24,28 @@ export default class AddDialog extends Component<IProps, { name: string }> {
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">Create a client</DialogTitle>
<DialogContent>
<TextField autoFocus margin="dense" id="name" label="Name *" type="email" value={name}
onChange={this.handleChange.bind(this, 'name')} fullWidth/>
<TextField
autoFocus
margin="dense"
id="name"
label="Name *"
type="email"
value={name}
onChange={this.handleChange.bind(this, 'name')}
fullWidth
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip placement={'bottom-start'} title={submitEnabled ? '' : 'name is required'}>
<Tooltip
placement={'bottom-start'}
title={submitEnabled ? '' : 'name is required'}>
<div>
<Button disabled={!submitEnabled} onClick={submitAndClose} color="primary" variant="raised">
<Button
disabled={!submitEnabled}
onClick={submitAndClose}
color="primary"
variant="raised">
Create
</Button>
</div>

View File

@ -7,17 +7,17 @@ import Tooltip from 'material-ui/Tooltip';
import React, {ChangeEvent, Component} from 'react';
interface IProps {
name?: string
admin?: boolean
fClose: VoidFunction
fOnSubmit: (name: string, pass: string, admin: boolean) => void
isEdit?: boolean
name?: string;
admin?: boolean;
fClose: VoidFunction;
fOnSubmit: (name: string, pass: string, admin: boolean) => void;
isEdit?: boolean;
}
interface IState {
name: string
pass: string
admin: boolean
name: string;
pass: string;
admin: boolean;
}
export default class AddEditDialog extends Component<IProps, IState> {
@ -38,24 +38,57 @@ export default class AddEditDialog extends Component<IProps, IState> {
};
return (
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
<DialogTitle id="form-dialog-title">{isEdit ? 'Edit ' + this.props.name : 'Add a user'}</DialogTitle>
<DialogTitle id="form-dialog-title">
{isEdit ? 'Edit ' + this.props.name : 'Add a user'}
</DialogTitle>
<DialogContent>
<TextField autoFocus margin="dense" id="name" label="Name *" type="email" value={name}
onChange={this.handleChange.bind(this, 'name')} fullWidth/>
<TextField margin="dense" id="description" type="password" value={pass} fullWidth
<TextField
autoFocus
margin="dense"
id="name"
label="Name *"
type="email"
value={name}
onChange={this.handleChange.bind(this, 'name')}
fullWidth
/>
<TextField
margin="dense"
id="description"
type="password"
value={pass}
fullWidth
label={isEdit ? 'Pass (empty if no change)' : 'Pass *'}
onChange={this.handleChange.bind(this, 'pass')}/>
onChange={this.handleChange.bind(this, 'pass')}
/>
<FormControlLabel
control={<Switch checked={admin} onChange={this.handleChecked.bind(this, 'admin')}
value="admin"/>} label="has administrator rights"/>
control={
<Switch
checked={admin}
onChange={this.handleChecked.bind(this, 'admin')}
value="admin"
/>
}
label="has administrator rights"
/>
</DialogContent>
<DialogActions>
<Button onClick={fClose}>Cancel</Button>
<Tooltip placement={'bottom-start'}
title={namePresent ? (passPresent ? '' : 'password is required') : 'name is required'}>
<Tooltip
placement={'bottom-start'}
title={
namePresent
? passPresent
? ''
: 'password is required'
: 'name is required'
}>
<div>
<Button disabled={!passPresent || !namePresent} onClick={submitAndClose}
color="primary" variant="raised">
<Button
disabled={!passPresent || !namePresent}
onClick={submitAndClose}
color="primary"
variant="raised">
{isEdit ? 'Save' : 'Create'}
</Button>
</div>

View File

@ -14,18 +14,13 @@ const isLocalhost = Boolean(
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString()
);
const publicUrl = new URL(process.env.PUBLIC_URL!, window.location.toString());
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
@ -62,7 +57,7 @@ function registerValidSW(swUrl: string) {
}
navigator.serviceWorker
.register(swUrl)
.then(registration => {
.then((registration) => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
@ -85,7 +80,7 @@ function registerValidSW(swUrl: string) {
}
};
})
.catch(error => {
.catch((error) => {
console.error('Error during service worker registration:', error);
});
}
@ -93,14 +88,14 @@ function registerValidSW(swUrl: string) {
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
.then((response) => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type')!.indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
@ -111,15 +106,13 @@ function checkValidServiceWorker(swUrl: string) {
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
console.log('No internet connection found. App is running in offline mode.');
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}

View File

@ -11,7 +11,7 @@ class AppStore extends EventEmitter {
public getById(id: number): IApplication {
const app = this.getByIdOrUndefined(id);
if (!app) {
throw new Error('app is required to exist')
throw new Error('app is required to exist');
}
return app;
}

View File

@ -11,7 +11,7 @@ class ClientStore extends EventEmitter {
public getById(id: number): IClient {
const client = this.clients.find((c) => c.id === id);
if (!client) {
throw new Error('client is required to exist')
throw new Error('client is required to exist');
}
return client;
}
@ -29,7 +29,6 @@ class ClientStore extends EventEmitter {
}
}
const store = new ClientStore();
dispatcher.register(store.handle.bind(store));
export default store;

View File

@ -4,7 +4,6 @@ import AppStore from './AppStore';
import dispatcher, {IEvent} from './dispatcher';
class MessageStore extends EventEmitter {
private appToMessages: {[appId: number]: IAppMessages} = {};
private reset: false | number = false;
private resetOnAll: false | number = false;
@ -32,7 +31,9 @@ class MessageStore extends EventEmitter {
return;
}
this.loading = true;
MessageAction.fetchMessagesApp(id, this.get(id).nextSince).catch(() => this.loading = false);
MessageAction.fetchMessagesApp(id, this.get(id).nextSince).catch(
() => (this.loading = false)
);
}
public get(id: number): IAppMessages {
@ -87,7 +88,9 @@ class MessageStore extends EventEmitter {
private removeFromList(messages: IAppMessages, messageToDelete: IMessage): false | number {
if (messages) {
const index = messages.messages.findIndex((message) => message.id === messageToDelete.id);
const index = messages.messages.findIndex(
(message) => message.id === messageToDelete.id
);
if (index !== -1) {
messages.messages.splice(index, 1);
return index;
@ -98,10 +101,10 @@ class MessageStore extends EventEmitter {
private updateApps = (): void => {
const appToUrl: {[appId: number]: string} = {};
AppStore.get().forEach((app) => appToUrl[app.id] = app.image);
AppStore.get().forEach((app) => (appToUrl[app.id] = app.image));
Object.keys(this.appToMessages).forEach((key) => {
const appMessages: IAppMessages = this.appToMessages[key];
appMessages.messages.forEach((message) => message.image = appToUrl[message.appid]);
appMessages.messages.forEach((message) => (message.image = appToUrl[message.appid]));
});
};
}

View File

@ -3,8 +3,10 @@ import dispatcher, {IEvent} from './dispatcher';
export function requestPermission() {
if (Notify.needsPermission && Notify.isSupported()) {
Notify.requestPermission(() => console.log('granted notification permissions'),
() => console.log('notification permission denied'));
Notify.requestPermission(
() => console.log('granted notification permissions'),
() => console.log('notification permission denied')
);
}
}
@ -25,7 +27,8 @@ function closeAfterTimeout(event: Event) {
}, 5000);
}
dispatcher.register((data: IEvent): void => {
dispatcher.register(
(data: IEvent): void => {
if (data.type === 'ONE_MESSAGE') {
const msg = data.payload;
@ -37,4 +40,5 @@ dispatcher.register((data: IEvent): void => {
});
notify.show();
}
});
}
);

View File

@ -6,7 +6,7 @@ class SnackBarStore extends EventEmitter {
public next(): string {
if (!this.hasNext()) {
throw new Error("no such element")
throw new Error('no such element');
}
return this.messages.shift() as string;
}

View File

@ -1,8 +1,8 @@
import {Dispatcher} from 'flux';
export interface IEvent {
type: string
payload?: any
type: string;
payload?: any;
}
export default new Dispatcher<IEvent>();

View File

@ -1,10 +1,9 @@
declare module 'react-timeago' {
import React from "react";
import React from 'react';
export interface ITimeAgoProps {
date: string
date: string;
}
export default class TimeAgo extends React.Component<ITimeAgoProps, any> {}
}

View File

@ -1,54 +1,54 @@
interface IApplication {
id: number
token: string
name: string
description: string
image: string
id: number;
token: string;
name: string;
description: string;
image: string;
}
interface IClient {
id: number
token: string
name: string
id: number;
token: string;
name: string;
}
interface IMessage {
id: number
appid: number
message: string
title: string
priority: number
date: string
image?: string
id: number;
appid: number;
message: string;
title: string;
priority: number;
date: string;
image?: string;
}
interface IPagedMessages {
paging: IPaging
messages: IMessage[]
paging: IPaging;
messages: IMessage[];
}
interface IPaging {
next?: string
since?: number
size: number
limit: number
next?: string;
since?: number;
size: number;
limit: number;
}
interface IUser {
id: number
name: string
admin: boolean
id: number;
name: string;
admin: boolean;
}
interface IVersion {
version: string
commit: string
buildDate: string
version: string;
commit: string;
buildDate: string;
}
interface IAppMessages {
messages: IMessage[]
hasMore: boolean
nextSince: number,
id?: number
messages: IMessage[];
hasMore: boolean;
nextSince: number;
id?: number;
}