Format
This commit is contained in:
parent
1afa51959f
commit
a3f081307b
|
|
@ -38,14 +38,14 @@ const styles = (theme: Theme) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
darkTheme: boolean
|
darkTheme: boolean;
|
||||||
redirect: boolean
|
redirect: boolean;
|
||||||
showSettings: boolean
|
showSettings: boolean;
|
||||||
loggedIn: boolean
|
loggedIn: boolean;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
name: string
|
name: string;
|
||||||
authenticating: boolean
|
authenticating: boolean;
|
||||||
version: string
|
version: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Layout extends React.Component<WithStyles<'content'>, IState> {
|
class Layout extends React.Component<WithStyles<'content'>, IState> {
|
||||||
|
|
@ -64,7 +64,7 @@ class Layout extends React.Component<WithStyles<'content'>, IState> {
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
if (this.state.version === Layout.defaultVersion) {
|
if (this.state.version === Layout.defaultVersion) {
|
||||||
axios.get(config.get('url') + 'version').then((resp:AxiosResponse<IVersion>) => {
|
axios.get(config.get('url') + 'version').then((resp: AxiosResponse<IVersion>) => {
|
||||||
this.setState({...this.state, version: resp.data.version});
|
this.setState({...this.state, version: resp.data.version});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -97,31 +97,41 @@ class Layout extends React.Component<WithStyles<'content'>, IState> {
|
||||||
const {name, admin, version, loggedIn, showSettings, authenticating} = this.state;
|
const {name, admin, version, loggedIn, showSettings, authenticating} = this.state;
|
||||||
const {classes} = this.props;
|
const {classes} = this.props;
|
||||||
const theme = this.state.darkTheme ? darkTheme : lightTheme;
|
const theme = this.state.darkTheme ? darkTheme : lightTheme;
|
||||||
const loginRoute = () => (loggedIn ? (<Redirect to="/"/>) : (<Login/>));
|
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
|
||||||
return (
|
return (
|
||||||
<MuiThemeProvider theme={theme}>
|
<MuiThemeProvider theme={theme}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<div style={{display: 'flex'}}>
|
<div style={{display: 'flex'}}>
|
||||||
<CssBaseline/>
|
<CssBaseline />
|
||||||
<Header admin={admin} name={name} version={version} loggedIn={loggedIn}
|
<Header
|
||||||
toggleTheme={this.toggleTheme} showSettings={this.showSettings}/>
|
admin={admin}
|
||||||
<Navigation loggedIn={loggedIn}/>
|
name={name}
|
||||||
|
version={version}
|
||||||
|
loggedIn={loggedIn}
|
||||||
|
toggleTheme={this.toggleTheme}
|
||||||
|
showSettings={this.showSettings}
|
||||||
|
/>
|
||||||
|
<Navigation loggedIn={loggedIn} />
|
||||||
|
|
||||||
<main className={classes.content}>
|
<main className={classes.content}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{authenticating ? <Route path="/"><LoadingSpinner/></Route> : null}
|
{authenticating ? (
|
||||||
<Route exact path="/login" render={loginRoute}/>
|
<Route path="/">
|
||||||
{loggedIn ? null : <Redirect to="/login"/>}
|
<LoadingSpinner />
|
||||||
<Route exact path="/" component={Messages}/>
|
</Route>
|
||||||
<Route exact path="/messages/:id" component={Messages}/>
|
) : null}
|
||||||
<Route exact path="/applications" component={Applications}/>
|
<Route exact path="/login" render={loginRoute} />
|
||||||
<Route exact path="/clients" component={Clients}/>
|
{loggedIn ? null : <Redirect to="/login" />}
|
||||||
<Route exact path="/users" component={Users}/>
|
<Route exact path="/" component={Messages} />
|
||||||
|
<Route exact path="/messages/:id" component={Messages} />
|
||||||
|
<Route exact path="/applications" component={Applications} />
|
||||||
|
<Route exact path="/clients" component={Clients} />
|
||||||
|
<Route exact path="/users" component={Users} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
{showSettings && <SettingsDialog fClose={this.hideSettings}/>}
|
{showSettings && <SettingsDialog fClose={this.hideSettings} />}
|
||||||
<ScrollUpButton/>
|
<ScrollUpButton />
|
||||||
<SnackBarHandler/>
|
<SnackBarHandler />
|
||||||
</div>
|
</div>
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</MuiThemeProvider>
|
</MuiThemeProvider>
|
||||||
|
|
|
||||||
|
|
@ -15,10 +15,13 @@ export function fetchApps() {
|
||||||
* @param {int} id the application id
|
* @param {int} id the application id
|
||||||
*/
|
*/
|
||||||
export function deleteApp(id: number) {
|
export function deleteApp(id: number) {
|
||||||
axios.delete(config.get('url') + 'application/' + id).then(() => {
|
axios
|
||||||
|
.delete(config.get('url') + 'application/' + id)
|
||||||
|
.then(() => {
|
||||||
fetchApps();
|
fetchApps();
|
||||||
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
|
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.
|
* @param {string} description the description of the application.
|
||||||
*/
|
*/
|
||||||
export function createApp(name: string, description: string) {
|
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(fetchApps)
|
||||||
.then(() => snack('Application created'));
|
.then(() => snack('Application created'));
|
||||||
}
|
}
|
||||||
|
|
@ -40,7 +44,10 @@ export function createApp(name: string, description: string) {
|
||||||
export function uploadImage(id: number, file: Blob) {
|
export function uploadImage(id: number, file: Blob) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('file', file);
|
formData.append('file', file);
|
||||||
axios.post(config.get('url') + 'application/' + id + '/image', formData,
|
axios
|
||||||
{headers: {'content-type': 'multipart/form-data'}}).then(fetchApps)
|
.post(config.get('url') + 'application/' + id + '/image', formData, {
|
||||||
|
headers: {'content-type': 'multipart/form-data'},
|
||||||
|
})
|
||||||
|
.then(fetchApps)
|
||||||
.then(() => snack('Application image updated'));
|
.then(() => snack('Application image updated'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,10 @@ export function fetchClients() {
|
||||||
* @param {int} id the client id
|
* @param {int} id the client id
|
||||||
*/
|
*/
|
||||||
export function deleteClient(id: number) {
|
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
|
* @param {string} name the client name
|
||||||
*/
|
*/
|
||||||
export function createClient(name: string) {
|
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'));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {AxiosResponse} from "axios";
|
import {AxiosResponse} from 'axios';
|
||||||
import dispatcher from '../stores/dispatcher';
|
import dispatcher from '../stores/dispatcher';
|
||||||
import * as AppAction from './AppAction';
|
import * as AppAction from './AppAction';
|
||||||
import * as ClientAction from './ClientAction';
|
import * as ClientAction from './ClientAction';
|
||||||
|
|
|
||||||
|
|
@ -7,11 +7,14 @@ import * as UserAction from './UserAction';
|
||||||
|
|
||||||
export function fetchMessagesApp(id: number, since: number) {
|
export function fetchMessagesApp(id: number, since: number) {
|
||||||
if (id === -1) {
|
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);
|
newMessages(-1, resp.data);
|
||||||
});
|
});
|
||||||
} else {
|
} 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>) => {
|
.then((resp: AxiosResponse<IPagedMessages>) => {
|
||||||
newMessages(id, resp.data);
|
newMessages(id, resp.data);
|
||||||
});
|
});
|
||||||
|
|
@ -20,7 +23,8 @@ export function fetchMessagesApp(id: number, since: number) {
|
||||||
|
|
||||||
function newMessages(id: number, data: IPagedMessages) {
|
function newMessages(id: number, data: IPagedMessages) {
|
||||||
dispatcher.dispatch({
|
dispatcher.dispatch({
|
||||||
type: 'UPDATE_MESSAGES', payload: {
|
type: 'UPDATE_MESSAGES',
|
||||||
|
payload: {
|
||||||
messages: data.messages,
|
messages: data.messages,
|
||||||
hasMore: 'next' in data.paging,
|
hasMore: 'next' in data.paging,
|
||||||
nextSince: data.paging.since,
|
nextSince: data.paging.since,
|
||||||
|
|
@ -40,8 +44,7 @@ export function deleteMessagesByApp(id: number) {
|
||||||
snack('Messages deleted');
|
snack('Messages deleted');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
axios.delete(config.get('url') + 'application/' + id + '/message')
|
axios.delete(config.get('url') + 'application/' + id + '/message').then(() => {
|
||||||
.then(() => {
|
|
||||||
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
|
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
|
||||||
snack('Deleted all messages from the application');
|
snack('Deleted all messages from the application');
|
||||||
});
|
});
|
||||||
|
|
@ -66,7 +69,10 @@ export function listenToWebSocket() {
|
||||||
}
|
}
|
||||||
wsActive = true;
|
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());
|
const ws = new WebSocket(wsUrl + 'stream?token=' + getToken());
|
||||||
|
|
||||||
ws.onerror = (e) => {
|
ws.onerror = (e) => {
|
||||||
|
|
@ -74,7 +80,8 @@ export function listenToWebSocket() {
|
||||||
console.log('WebSocket connection errored', e);
|
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 = () => {
|
ws.onclose = () => {
|
||||||
wsActive = false;
|
wsActive = false;
|
||||||
|
|
|
||||||
|
|
@ -16,17 +16,26 @@ export function login(username: string, password: string) {
|
||||||
const browser = detect();
|
const browser = detect();
|
||||||
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
|
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
|
||||||
authenticating();
|
authenticating();
|
||||||
axios.create().request({
|
axios
|
||||||
|
.create()
|
||||||
|
.request({
|
||||||
url: config.get('url') + 'client',
|
url: config.get('url') + 'client',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
data: {name},
|
data: {name},
|
||||||
auth: {username, password},
|
auth: {username, password},
|
||||||
}).then((resp) => {
|
})
|
||||||
|
.then((resp) => {
|
||||||
snack(`A client named '${name}' was created for your session.`);
|
snack(`A client named '${name}' was created for your session.`);
|
||||||
setAuthorizationToken(resp.data.token);
|
setAuthorizationToken(resp.data.token);
|
||||||
tryAuthenticate().then(GlobalAction.initialLoad)
|
tryAuthenticate()
|
||||||
.catch(() => console.log('create client succeeded, but authenticated with given token failed'));
|
.then(GlobalAction.initialLoad)
|
||||||
}).catch(() => {
|
.catch(() =>
|
||||||
|
console.log(
|
||||||
|
'create client succeeded, but authenticated with given token failed'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
snack('Login failed');
|
snack('Login failed');
|
||||||
noAuthentication();
|
noAuthentication();
|
||||||
});
|
});
|
||||||
|
|
@ -44,10 +53,14 @@ export function logout() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function tryAuthenticate() {
|
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});
|
dispatcher.dispatch({type: 'AUTHENTICATED', payload: resp.data});
|
||||||
return resp;
|
return resp;
|
||||||
}).catch((resp) => {
|
})
|
||||||
|
.catch((resp) => {
|
||||||
if (getToken()) {
|
if (getToken()) {
|
||||||
setAuthorizationToken(null);
|
setAuthorizationToken(null);
|
||||||
snack('Authentication failed, try to re-login. (client or user was deleted)');
|
snack('Authentication failed, try to re-login. (client or user was deleted)');
|
||||||
|
|
@ -80,7 +93,9 @@ function authenticating() {
|
||||||
* @param {string} pass
|
* @param {string} pass
|
||||||
*/
|
*/
|
||||||
export function changeCurrentUser(pass: string) {
|
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. */
|
/** Fetches all users. */
|
||||||
|
|
@ -95,7 +110,10 @@ export function fetchUsers() {
|
||||||
* @param {int} id the user id
|
* @param {int} id the user id
|
||||||
*/
|
*/
|
||||||
export function deleteUser(id: number) {
|
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
|
* @param {bool} admin if true, the user is an administrator
|
||||||
*/
|
*/
|
||||||
export function createUser(name: string, pass: string, admin: boolean) {
|
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'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,17 @@
|
||||||
import Button from 'material-ui/Button';
|
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';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
title: string
|
title: string;
|
||||||
text: string
|
text: string;
|
||||||
fClose: VoidFunction
|
fClose: VoidFunction;
|
||||||
fOnSubmit: VoidFunction
|
fOnSubmit: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ConfirmDialog({title, text, fClose, fOnSubmit}: IProps) {
|
export default function ConfirmDialog({title, text, fClose, fOnSubmit}: IProps) {
|
||||||
|
|
@ -22,7 +27,9 @@ export default function ConfirmDialog({title, text, fClose, fOnSubmit}: IProps)
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={fClose}>No</Button>
|
<Button onClick={fClose}>No</Button>
|
||||||
<Button onClick={submitAndClose} color="primary" variant="raised">Yes</Button>
|
<Button onClick={submitAndClose} color="primary" variant="raised">
|
||||||
|
Yes
|
||||||
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {WithStyles} from "material-ui";
|
import {WithStyles} from 'material-ui';
|
||||||
import Paper from 'material-ui/Paper';
|
import Paper from 'material-ui/Paper';
|
||||||
import {withStyles} from 'material-ui/styles';
|
import {withStyles} from 'material-ui/styles';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
@ -10,7 +10,7 @@ const styles = () => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
style?: object,
|
style?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container: React.SFC<IProps & WithStyles<'paper'>> = ({classes, children, style}) => {
|
const Container: React.SFC<IProps & WithStyles<'paper'>> = ({classes, children, style}) => {
|
||||||
|
|
|
||||||
|
|
@ -4,23 +4,38 @@ import Typography from 'material-ui/Typography';
|
||||||
import React, {SFC} from 'react';
|
import React, {SFC} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
title: string
|
title: string;
|
||||||
buttonTitle?: string
|
buttonTitle?: string;
|
||||||
fButton?: VoidFunction
|
fButton?: VoidFunction;
|
||||||
buttonDisabled?: boolean
|
buttonDisabled?: boolean;
|
||||||
maxWidth?: number
|
maxWidth?: number;
|
||||||
hideButton?: boolean
|
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}}>
|
<main style={{margin: '0 auto', maxWidth}}>
|
||||||
<Grid container spacing={24}>
|
<Grid container spacing={24}>
|
||||||
<Grid item xs={12} style={{display: 'flex'}}>
|
<Grid item xs={12} style={{display: 'flex'}}>
|
||||||
<Typography variant="display1" style={{flex: 1}}>
|
<Typography variant="display1" style={{flex: 1}}>
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
{hideButton ? null : <Button variant="raised" color="primary" disabled={buttonDisabled}
|
{hideButton ? null : (
|
||||||
onClick={fButton}>{buttonTitle}</Button>}
|
<Button
|
||||||
|
variant="raised"
|
||||||
|
color="primary"
|
||||||
|
disabled={buttonDisabled}
|
||||||
|
onClick={fButton}>
|
||||||
|
{buttonTitle}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
{children}
|
{children}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ class FixedReactList extends ReactList {
|
||||||
super.cacheSizes();
|
super.cacheSizes();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public clearCacheFromIndex(startIndex: number): void {
|
public clearCacheFromIndex(startIndex: number): void {
|
||||||
this.ignoreNextCacheUpdate = true;
|
this.ignoreNextCacheUpdate = true;
|
||||||
|
|
||||||
|
|
@ -25,16 +24,19 @@ class FixedReactList extends ReactList {
|
||||||
this.cache = {};
|
this.cache = {};
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore accessing private member
|
// @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
|
// @ts-ignore accessing private member
|
||||||
delete this.cache[index];
|
delete this.cache[index];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
public componentDidUpdate() {
|
public componentDidUpdate() {
|
||||||
|
const hasCacheForLastRenderedItem =
|
||||||
// @ts-ignore accessing private member
|
// @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
|
// @ts-ignore accessing private member
|
||||||
super.componentDidUpdate();
|
super.componentDidUpdate();
|
||||||
if (!hasCacheForLastRenderedItem) {
|
if (!hasCacheForLastRenderedItem) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {Theme, WithStyles} from "material-ui";
|
import {Theme, WithStyles} from 'material-ui';
|
||||||
import AccountCircle from 'material-ui-icons/AccountCircle';
|
import AccountCircle from 'material-ui-icons/AccountCircle';
|
||||||
import Chat from 'material-ui-icons/Chat';
|
import Chat from 'material-ui-icons/Chat';
|
||||||
import DevicesOther from 'material-ui-icons/DevicesOther';
|
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 {
|
interface IProps {
|
||||||
loggedIn: boolean
|
loggedIn: boolean;
|
||||||
name: string
|
name: string;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
version: string
|
version: string;
|
||||||
toggleTheme: VoidFunction
|
toggleTheme: VoidFunction;
|
||||||
showSettings: VoidFunction
|
showSettings: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Header extends Component<IProps & Styles> {
|
class Header extends Component<IProps & Styles> {
|
||||||
|
|
@ -53,18 +53,25 @@ class Header extends Component<IProps & Styles> {
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<div className={classes.title}>
|
<div className={classes.title}>
|
||||||
<a href="https://github.com/gotify/server" className={classes.link}>
|
<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
|
Gotify
|
||||||
</Typography>
|
</Typography>
|
||||||
</a>
|
</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">
|
<Typography variant="button" color="inherit">
|
||||||
@{version}
|
@{version}
|
||||||
</Typography>
|
</Typography>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{loggedIn && this.renderButtons(name, admin)}
|
{loggedIn && this.renderButtons(name, admin)}
|
||||||
<IconButton onClick={toggleTheme} color="inherit"><LightbulbOutline/></IconButton>
|
<IconButton onClick={toggleTheme} color="inherit">
|
||||||
|
<LightbulbOutline />
|
||||||
|
</IconButton>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
|
@ -74,18 +81,37 @@ class Header extends Component<IProps & Styles> {
|
||||||
const {classes, showSettings} = this.props;
|
const {classes, showSettings} = this.props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{admin
|
{admin ? (
|
||||||
? <Link className={classes.link} to="/users">
|
<Link className={classes.link} to="/users">
|
||||||
<Button color="inherit"><SupervisorAccount/> users</Button></Link>
|
<Button color="inherit">
|
||||||
: ''}
|
<SupervisorAccount />
|
||||||
|
users
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
<Link className={classes.link} to="/applications">
|
<Link className={classes.link} to="/applications">
|
||||||
<Button color="inherit"><Chat/> apps</Button>
|
<Button color="inherit">
|
||||||
|
<Chat />
|
||||||
|
apps
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link className={classes.link} to="/clients"><Button color="inherit">
|
<Link className={classes.link} to="/clients">
|
||||||
<DevicesOther/> clients</Button>
|
<Button color="inherit">
|
||||||
|
<DevicesOther />
|
||||||
|
clients
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Button color="inherit" onClick={showSettings}><AccountCircle/> {name}</Button>
|
<Button color="inherit" onClick={showSettings}>
|
||||||
<Button color="inherit" onClick={UserAction.logout}><ExitToApp/> Logout</Button>
|
<AccountCircle />
|
||||||
|
|
||||||
|
{name}
|
||||||
|
</Button>
|
||||||
|
<Button color="inherit" onClick={UserAction.logout}>
|
||||||
|
<ExitToApp />
|
||||||
|
Logout
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ export default function LoadingSpinner() {
|
||||||
return (
|
return (
|
||||||
<DefaultPage title="" maxWidth={250} hideButton={true}>
|
<DefaultPage title="" maxWidth={250} hideButton={true}>
|
||||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||||
<CircularProgress size={150}/>
|
<CircularProgress size={150} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</DefaultPage>
|
</DefaultPage>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {WithStyles} from "material-ui";
|
import {WithStyles} from 'material-ui';
|
||||||
import Delete from 'material-ui-icons/Delete';
|
import Delete from 'material-ui-icons/Delete';
|
||||||
import IconButton from 'material-ui/IconButton';
|
import IconButton from 'material-ui/IconButton';
|
||||||
import {withStyles} from 'material-ui/styles';
|
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 {
|
interface IProps {
|
||||||
title: string
|
title: string;
|
||||||
image?: string
|
image?: string;
|
||||||
date: string
|
date: string;
|
||||||
content: string
|
content: string;
|
||||||
fDelete: VoidFunction
|
fDelete: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Message({fDelete, classes, title, date, content, image}: IProps & Style) {
|
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}>
|
<div className={classes.wrapperPadding}>
|
||||||
<Container style={{display: 'flex'}}>
|
<Container style={{display: 'flex'}}>
|
||||||
<div className={classes.imageWrapper}>
|
<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>
|
||||||
<div className={classes.messageContentWrapper}>
|
<div className={classes.messageContentWrapper}>
|
||||||
<div className={classes.header}>
|
<div className={classes.header}>
|
||||||
|
|
@ -55,9 +69,11 @@ function Message({fDelete, classes, title, date, content, image}: IProps & Style
|
||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1">
|
<Typography variant="body1">
|
||||||
<TimeAgo date={date}/>
|
<TimeAgo date={date} />
|
||||||
</Typography>
|
</Typography>
|
||||||
<IconButton onClick={fDelete} className={classes.trash}><Delete/></IconButton>
|
<IconButton onClick={fDelete} className={classes.trash}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
<Typography component="p">{content}</Typography>
|
<Typography component="p">{content}</Typography>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {Theme, WithStyles} from "material-ui";
|
import {Theme, WithStyles} from 'material-ui';
|
||||||
import Divider from 'material-ui/Divider';
|
import Divider from 'material-ui/Divider';
|
||||||
import Drawer from 'material-ui/Drawer';
|
import Drawer from 'material-ui/Drawer';
|
||||||
import {ListItem, ListItemText} from 'material-ui/List';
|
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 {
|
interface IProps {
|
||||||
loggedIn: boolean
|
loggedIn: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
apps: IApplication[]
|
apps: IApplication[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class Navigation extends Component<IProps & Styles, IState> {
|
class Navigation extends Component<IProps & Styles, IState> {
|
||||||
|
|
@ -45,11 +45,14 @@ class Navigation extends Component<IProps & Styles, IState> {
|
||||||
const {classes, loggedIn} = this.props;
|
const {classes, loggedIn} = this.props;
|
||||||
const {apps} = this.state;
|
const {apps} = this.state;
|
||||||
|
|
||||||
const userApps = apps.length === 0 ? null : apps.map((app) => {
|
const userApps =
|
||||||
|
apps.length === 0
|
||||||
|
? null
|
||||||
|
: apps.map((app) => {
|
||||||
return (
|
return (
|
||||||
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
|
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
|
||||||
<ListItem button>
|
<ListItem button>
|
||||||
<ListItemText primary={app.name}/>
|
<ListItemText primary={app.name} />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
@ -57,24 +60,24 @@ class Navigation extends Component<IProps & Styles, IState> {
|
||||||
|
|
||||||
const placeholderItems = [
|
const placeholderItems = [
|
||||||
<ListItem button disabled key={-1}>
|
<ListItem button disabled key={-1}>
|
||||||
<ListItemText primary="Some Server"/>
|
<ListItemText primary="Some Server" />
|
||||||
</ListItem>,
|
</ListItem>,
|
||||||
<ListItem button disabled key={-2}>
|
<ListItem button disabled key={-2}>
|
||||||
<ListItemText primary="A Raspberry PI"/>
|
<ListItemText primary="A Raspberry PI" />
|
||||||
</ListItem>,
|
</ListItem>,
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer variant="permanent" classes={{paper: classes.drawerPaper}}>
|
<Drawer variant="permanent" classes={{paper: classes.drawerPaper}}>
|
||||||
<div className={classes.toolbar}/>
|
<div className={classes.toolbar} />
|
||||||
<Link className={classes.link} to="/">
|
<Link className={classes.link} to="/">
|
||||||
<ListItem button disabled={!loggedIn}>
|
<ListItem button disabled={!loggedIn}>
|
||||||
<ListItemText primary="All Messages"/>
|
<ListItemText primary="All Messages" />
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</Link>
|
</Link>
|
||||||
<Divider/>
|
<Divider />
|
||||||
<div>{loggedIn ? userApps : placeholderItems}</div>
|
<div>{loggedIn ? userApps : placeholderItems}</div>
|
||||||
<Divider/>
|
<Divider />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -82,4 +85,4 @@ class Navigation extends Component<IProps & Styles, IState> {
|
||||||
private updateApps = () => this.setState({apps: AppStore.get()});
|
private updateApps = () => this.setState({apps: AppStore.get()});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles,{withTheme: true})<IProps>(Navigation);
|
export default withStyles(styles, {withTheme: true})<IProps>(Navigation);
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,12 @@ import React, {Component} from 'react';
|
||||||
class ScrollUpButton extends Component {
|
class ScrollUpButton extends Component {
|
||||||
public render() {
|
public render() {
|
||||||
return (
|
return (
|
||||||
<Button variant="fab" color="primary"
|
<Button
|
||||||
|
variant="fab"
|
||||||
|
color="primary"
|
||||||
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
|
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
|
||||||
onClick={this.scrollUp}>
|
onClick={this.scrollUp}>
|
||||||
<KeyboardArrowUp/>
|
<KeyboardArrowUp />
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,11 @@ import React, {ChangeEvent, Component} from 'react';
|
||||||
import * as UserAction from '../actions/UserAction';
|
import * as UserAction from '../actions/UserAction';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
pass: string
|
pass: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction
|
fClose: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SettingsDialog extends Component<IProps, IState> {
|
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">
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
|
||||||
<DialogTitle id="form-dialog-title">Change Password</DialogTitle>
|
<DialogTitle id="form-dialog-title">Change Password</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField autoFocus margin="dense" type="password" label="New Pass *" value={pass}
|
<TextField
|
||||||
onChange={this.handleChange.bind(this, 'pass')} fullWidth/>
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
label="New Pass *"
|
||||||
|
value={pass}
|
||||||
|
onChange={this.handleChange.bind(this, 'pass')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
<Tooltip title={pass.length !== 0 ? '' : 'pass is required'}>
|
<Tooltip title={pass.length !== 0 ? '' : 'pass is required'}>
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={pass.length === 0} onClick={submitAndClose} color="primary"
|
<Button
|
||||||
|
disabled={pass.length === 0}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
variant="raised">
|
variant="raised">
|
||||||
Change
|
Change
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,11 @@ import Snackbar from 'material-ui/Snackbar';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import SnackBarStore from '../stores/SnackBarStore';
|
import SnackBarStore from '../stores/SnackBarStore';
|
||||||
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
current: string
|
current: string;
|
||||||
hasNext: boolean
|
hasNext: boolean;
|
||||||
open: boolean
|
open: boolean;
|
||||||
openWhen: number
|
openWhen: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SnackBarHandler extends Component<{}, IState> {
|
class SnackBarHandler extends Component<{}, IState> {
|
||||||
|
|
@ -40,12 +39,18 @@ class SnackBarHandler extends Component<{}, IState> {
|
||||||
return (
|
return (
|
||||||
<Snackbar
|
<Snackbar
|
||||||
anchorOrigin={{vertical: 'bottom', horizontal: 'left'}}
|
anchorOrigin={{vertical: 'bottom', horizontal: 'left'}}
|
||||||
open={open} autoHideDuration={duration}
|
open={open}
|
||||||
onClose={this.closeCurrentSnack} onExited={this.openNextSnack}
|
autoHideDuration={duration}
|
||||||
|
onClose={this.closeCurrentSnack}
|
||||||
|
onExited={this.openNextSnack}
|
||||||
message={<span id="message-id">{current}</span>}
|
message={<span id="message-id">{current}</span>}
|
||||||
action={
|
action={
|
||||||
<IconButton key="close" aria-label="Close" color="inherit" onClick={this.closeCurrentSnack}>
|
<IconButton
|
||||||
<Close/>
|
key="close"
|
||||||
|
aria-label="Close"
|
||||||
|
color="inherit"
|
||||||
|
onClick={this.closeCurrentSnack}>
|
||||||
|
<Close />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -64,7 +69,10 @@ class SnackBarHandler extends Component<{}, IState> {
|
||||||
if (snackOpenSince > SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS) {
|
if (snackOpenSince > SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS) {
|
||||||
this.closeCurrentSnack();
|
this.closeCurrentSnack();
|
||||||
} else {
|
} else {
|
||||||
setTimeout(this.closeCurrentSnack, SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS - snackOpenSince);
|
setTimeout(
|
||||||
|
this.closeCurrentSnack,
|
||||||
|
SnackBarHandler.MIN_VISIBLE_SNACK_TIME_IN_MS - snackOpenSince
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,12 @@ import Typography from 'material-ui/Typography';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
value: string
|
value: string;
|
||||||
style?: object
|
style?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
visible: boolean
|
visible: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ToggleVisibility extends Component<IProps, IState> {
|
class ToggleVisibility extends Component<IProps, IState> {
|
||||||
|
|
@ -22,11 +22,9 @@ class ToggleVisibility extends Component<IProps, IState> {
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<IconButton onClick={this.toggleVisibility}>
|
<IconButton onClick={this.toggleVisibility}>
|
||||||
{this.state.visible ? <VisibilityOff/> : <Visibility/>}
|
{this.state.visible ? <VisibilityOff /> : <Visibility />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography style={{fontFamily: '\'Roboto Mono\', monospace'}}>
|
<Typography style={{fontFamily: "'Roboto Mono', monospace"}}>{text}</Typography>
|
||||||
{text}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -34,5 +32,4 @@ class ToggleVisibility extends Component<IProps, IState> {
|
||||||
private toggleVisibility = () => this.setState({visible: !this.state.visible});
|
private toggleVisibility = () => this.setState({visible: !this.state.visible});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default ToggleVisibility;
|
export default ToggleVisibility;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
export interface IConfig {
|
export interface IConfig {
|
||||||
url: string
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let config: IConfig;
|
let config: IConfig;
|
||||||
|
|
@ -8,6 +8,6 @@ export function set(c: IConfig) {
|
||||||
config = c;
|
config = c;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get(val: "url"): string {
|
export function get(val: 'url'): string {
|
||||||
return config[val];
|
return config[val];
|
||||||
}
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ import * as ReactDOM from 'react-dom';
|
||||||
import 'typeface-roboto';
|
import 'typeface-roboto';
|
||||||
import 'typeface-roboto-mono';
|
import 'typeface-roboto-mono';
|
||||||
import * as UserAction from './actions/UserAction';
|
import * as UserAction from './actions/UserAction';
|
||||||
import * as config from './config'
|
import * as config from './config';
|
||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
import registerServiceWorker from './registerServiceWorker';
|
import registerServiceWorker from './registerServiceWorker';
|
||||||
import * as Notifications from './stores/Notifications';
|
import * as Notifications from './stores/Notifications';
|
||||||
|
|
@ -36,6 +36,6 @@ declare global {
|
||||||
config.set(window.config || defaultDevConfig);
|
config.set(window.config || defaultDevConfig);
|
||||||
}
|
}
|
||||||
UserAction.checkIfAlreadyLoggedIn();
|
UserAction.checkIfAlreadyLoggedIn();
|
||||||
ReactDOM.render(<Layout/>, document.getElementById('root'));
|
ReactDOM.render(<Layout />, document.getElementById('root'));
|
||||||
registerServiceWorker();
|
registerServiceWorker();
|
||||||
}());
|
})();
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,9 @@ import AppStore from '../stores/AppStore';
|
||||||
import AddApplicationDialog from './dialog/AddApplicationDialog';
|
import AddApplicationDialog from './dialog/AddApplicationDialog';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
apps: IApplication[]
|
apps: IApplication[];
|
||||||
createDialog: boolean
|
createDialog: boolean;
|
||||||
deleteId: number
|
deleteId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Applications extends Component<{}, IState> {
|
class Applications extends Component<{}, IState> {
|
||||||
|
|
@ -36,40 +36,61 @@ class Applications extends Component<{}, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
const {apps, createDialog, deleteId} = this.state;
|
const {apps, createDialog, deleteId} = this.state;
|
||||||
return (
|
return (
|
||||||
<DefaultPage title="Applications" buttonTitle="Create Application" maxWidth={1000}
|
<DefaultPage
|
||||||
|
title="Applications"
|
||||||
|
buttonTitle="Create Application"
|
||||||
|
maxWidth={1000}
|
||||||
fButton={this.showCreateDialog}>
|
fButton={this.showCreateDialog}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Paper elevation={6}>
|
<Paper elevation={6}>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell padding="checkbox" style={{width: 80}}/>
|
<TableCell padding="checkbox" style={{width: 80}} />
|
||||||
<TableCell>Name</TableCell>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell>Token</TableCell>
|
<TableCell>Token</TableCell>
|
||||||
<TableCell>Description</TableCell>
|
<TableCell>Description</TableCell>
|
||||||
<TableCell/>
|
<TableCell />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{apps.map((app: IApplication) => {
|
{apps.map((app: IApplication) => {
|
||||||
return (
|
return (
|
||||||
<Row key={app.id} description={app.description} image={app.image}
|
<Row
|
||||||
name={app.name} value={app.token} fUpload={() => this.uploadImage(app.id)}
|
key={app.id}
|
||||||
fDelete={() => this.showCloseDialog(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>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
<input ref={(upload) => this.upload = upload} type="file" style={{display: 'none'}}
|
<input
|
||||||
onChange={this.onUploadImage}/>
|
ref={(upload) => (this.upload = upload)}
|
||||||
|
type="file"
|
||||||
|
style={{display: 'none'}}
|
||||||
|
onChange={this.onUploadImage}
|
||||||
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
{createDialog && <AddApplicationDialog fClose={this.hideCreateDialog} fOnSubmit={AppAction.createApp}/>}
|
{createDialog && (
|
||||||
{deleteId !== -1 && <ConfirmDialog title="Confirm Delete"
|
<AddApplicationDialog
|
||||||
|
fClose={this.hideCreateDialog}
|
||||||
|
fOnSubmit={AppAction.createApp}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{deleteId !== -1 && (
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Confirm Delete"
|
||||||
text={'Delete ' + AppStore.getById(deleteId).name + '?'}
|
text={'Delete ' + AppStore.getById(deleteId).name + '?'}
|
||||||
fClose={this.hideCloseDialog}
|
fClose={this.hideCloseDialog}
|
||||||
fOnSubmit={() => AppAction.deleteApp(deleteId)}
|
fOnSubmit={() => AppAction.deleteApp(deleteId)}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</DefaultPage>
|
</DefaultPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -102,28 +123,33 @@ class Applications extends Component<{}, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string
|
name: string;
|
||||||
value: string
|
value: string;
|
||||||
description: string
|
description: string;
|
||||||
fUpload: VoidFunction
|
fUpload: VoidFunction;
|
||||||
image: string
|
image: string;
|
||||||
fDelete: VoidFunction
|
fDelete: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: SFC<IRowProps> = ({name, value, description, fDelete, fUpload, image}) => (
|
const Row: SFC<IRowProps> = ({name, value, description, fDelete, fUpload, image}) => (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell padding="checkbox">
|
<TableCell padding="checkbox">
|
||||||
<div style={{display: 'flex'}}>
|
<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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{name}</TableCell>
|
<TableCell>{name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center'}}/>
|
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center'}} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{description}</TableCell>
|
<TableCell>{description}</TableCell>
|
||||||
<TableCell numeric padding="none">
|
<TableCell numeric padding="none">
|
||||||
<IconButton onClick={fDelete}><Delete/></IconButton>
|
<IconButton onClick={fDelete}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import ClientStore from '../stores/ClientStore';
|
||||||
import AddClientDialog from './dialog/AddClientDialog';
|
import AddClientDialog from './dialog/AddClientDialog';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
clients: IClient[]
|
clients: IClient[];
|
||||||
showDialog: boolean
|
showDialog: boolean;
|
||||||
deleteId: number
|
deleteId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Clients extends Component<{}, IState> {
|
class Clients extends Component<{}, IState> {
|
||||||
|
|
@ -32,7 +32,10 @@ class Clients extends Component<{}, IState> {
|
||||||
public render() {
|
public render() {
|
||||||
const {clients, deleteId, showDialog} = this.state;
|
const {clients, deleteId, showDialog} = this.state;
|
||||||
return (
|
return (
|
||||||
<DefaultPage title="Clients" buttonTitle="Create Client" fButton={this.showCreateDialog}>
|
<DefaultPage
|
||||||
|
title="Clients"
|
||||||
|
buttonTitle="Create Client"
|
||||||
|
fButton={this.showCreateDialog}>
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Paper elevation={6}>
|
<Paper elevation={6}>
|
||||||
<Table>
|
<Table>
|
||||||
|
|
@ -40,25 +43,38 @@ class Clients extends Component<{}, IState> {
|
||||||
<TableRow style={{textAlign: 'center'}}>
|
<TableRow style={{textAlign: 'center'}}>
|
||||||
<TableCell>Name</TableCell>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell style={{width: 200}}>token</TableCell>
|
<TableCell style={{width: 200}}>token</TableCell>
|
||||||
<TableCell/>
|
<TableCell />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{clients.map((client: IClient) => {
|
{clients.map((client: IClient) => {
|
||||||
return (
|
return (
|
||||||
<Row key={client.id} name={client.name}
|
<Row
|
||||||
value={client.token} fDelete={() => this.showDeleteDialog(client.id)}/>
|
key={client.id}
|
||||||
|
name={client.name}
|
||||||
|
value={client.token}
|
||||||
|
fDelete={() => this.showDeleteDialog(client.id)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
{showDialog && <AddClientDialog fClose={this.hideCreateDialog} fOnSubmit={ClientAction.createClient}/>}
|
{showDialog && (
|
||||||
{deleteId !== -1 && <ConfirmDialog title="Confirm Delete"
|
<AddClientDialog
|
||||||
|
fClose={this.hideCreateDialog}
|
||||||
|
fOnSubmit={ClientAction.createClient}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{deleteId !== -1 && (
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Confirm Delete"
|
||||||
text={'Delete ' + ClientStore.getById(this.state.deleteId).name + '?'}
|
text={'Delete ' + ClientStore.getById(this.state.deleteId).name + '?'}
|
||||||
fClose={this.hideDeleteDelete}
|
fClose={this.hideDeleteDelete}
|
||||||
fOnSubmit={this.deleteClient}/>}
|
fOnSubmit={this.deleteClient}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</DefaultPage>
|
</DefaultPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -75,22 +91,26 @@ class Clients extends Component<{}, IState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string
|
name: string;
|
||||||
value: string
|
value: string;
|
||||||
fDelete: VoidFunction
|
fDelete: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: SFC<IRowProps> = ({name, value, fDelete}) => (
|
const Row: SFC<IRowProps> = ({name, value, fDelete}) => (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{name}</TableCell>
|
<TableCell>{name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center', width: 200}}/>
|
<ToggleVisibility
|
||||||
|
value={value}
|
||||||
|
style={{display: 'flex', alignItems: 'center', width: 200}}
|
||||||
|
/>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell numeric padding="none">
|
<TableCell numeric padding="none">
|
||||||
<IconButton onClick={fDelete}><Delete/></IconButton>
|
<IconButton onClick={fDelete}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export default Clients;
|
export default Clients;
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,8 @@ import Container from '../component/Container';
|
||||||
import DefaultPage from '../component/DefaultPage';
|
import DefaultPage from '../component/DefaultPage';
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
username: string
|
username: string;
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Login extends Component<{}, IState> {
|
class Login extends Component<{}, IState> {
|
||||||
|
|
@ -21,12 +21,28 @@ class Login extends Component<{}, IState> {
|
||||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||||
<Container>
|
<Container>
|
||||||
<form onSubmit={this.preventDefault}>
|
<form onSubmit={this.preventDefault}>
|
||||||
<TextField id="name" label="Username" margin="dense" value={username}
|
<TextField
|
||||||
onChange={this.handleChange.bind(this, 'username')}/>
|
id="name"
|
||||||
<TextField type="password" id="password" label="Password" margin="normal"
|
label="Username"
|
||||||
value={password} onChange={this.handleChange.bind(this, 'password')}/>
|
margin="dense"
|
||||||
<Button type="submit" variant="raised" size="large" color="primary"
|
value={username}
|
||||||
style={{marginTop: 15, marginBottom: 5}} onClick={this.login}>
|
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
|
Login
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import Grid from 'material-ui/Grid';
|
||||||
import {CircularProgress} from 'material-ui/Progress';
|
import {CircularProgress} from 'material-ui/Progress';
|
||||||
import Typography from 'material-ui/Typography';
|
import Typography from 'material-ui/Typography';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
import {RouteComponentProps} from "react-router";
|
import {RouteComponentProps} from 'react-router';
|
||||||
import * as MessageAction from '../actions/MessageAction';
|
import * as MessageAction from '../actions/MessageAction';
|
||||||
import DefaultPage from '../component/DefaultPage';
|
import DefaultPage from '../component/DefaultPage';
|
||||||
import ReactList from '../component/FixedReactList';
|
import ReactList from '../component/FixedReactList';
|
||||||
|
|
@ -10,20 +10,18 @@ import Message from '../component/Message';
|
||||||
import AppStore from '../stores/AppStore';
|
import AppStore from '../stores/AppStore';
|
||||||
import MessageStore from '../stores/MessageStore';
|
import MessageStore from '../stores/MessageStore';
|
||||||
|
|
||||||
|
interface IProps extends RouteComponentProps<any> {}
|
||||||
interface IProps extends RouteComponentProps<any> {
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
appId: number
|
appId: number;
|
||||||
messages: IMessage[]
|
messages: IMessage[];
|
||||||
name: string
|
name: string;
|
||||||
hasMore: boolean
|
hasMore: boolean;
|
||||||
nextSince?: number
|
nextSince?: number;
|
||||||
id?: number
|
id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Messages extends Component<IProps , IState> {
|
class Messages extends Component<IProps, IState> {
|
||||||
private static appId(props: IProps) {
|
private static appId(props: IProps) {
|
||||||
if (props === undefined) {
|
if (props === undefined) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|
@ -57,25 +55,33 @@ class Messages extends Component<IProps , IState> {
|
||||||
const deleteMessages = () => MessageAction.deleteMessagesByApp(appId);
|
const deleteMessages = () => MessageAction.deleteMessagesByApp(appId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultPage title={name} buttonTitle="Delete All" fButton={deleteMessages} buttonDisabled={!hasMessages}>
|
<DefaultPage
|
||||||
{hasMessages
|
title={name}
|
||||||
? (
|
buttonTitle="Delete All"
|
||||||
|
fButton={deleteMessages}
|
||||||
|
buttonDisabled={!hasMessages}>
|
||||||
|
{hasMessages ? (
|
||||||
<div style={{width: '100%'}}>
|
<div style={{width: '100%'}}>
|
||||||
<ReactList key={appId}
|
<ReactList
|
||||||
ref={(el: ReactList) => this.list = el}
|
key={appId}
|
||||||
|
ref={(el: ReactList) => (this.list = el)}
|
||||||
itemRenderer={this.renderMessage}
|
itemRenderer={this.renderMessage}
|
||||||
length={messages.length}
|
length={messages.length}
|
||||||
threshold={1000}
|
threshold={1000}
|
||||||
pageSize={30}
|
pageSize={30}
|
||||||
type='variable'
|
type="variable"
|
||||||
/>
|
/>
|
||||||
{hasMore
|
{hasMore ? (
|
||||||
? <Grid item xs={12} style={{textAlign: 'center'}}><CircularProgress size={100}/></Grid>
|
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||||
: this.label('You\'ve reached the end')}
|
<CircularProgress size={100} />
|
||||||
|
</Grid>
|
||||||
|
) : (
|
||||||
|
this.label("You've reached the end")
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
) : (
|
||||||
: this.label('No messages')
|
this.label('No messages')
|
||||||
}
|
)}
|
||||||
</DefaultPage>
|
</DefaultPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -102,12 +108,14 @@ class Messages extends Component<IProps , IState> {
|
||||||
this.checkIfLoadMore();
|
this.checkIfLoadMore();
|
||||||
const message: IMessage = this.state.messages[index];
|
const message: IMessage = this.state.messages[index];
|
||||||
return (
|
return (
|
||||||
<Message key={key}
|
<Message
|
||||||
|
key={key}
|
||||||
fDelete={this.deleteMessage(message)}
|
fDelete={this.deleteMessage(message)}
|
||||||
title={message.title}
|
title={message.title}
|
||||||
date={message.date}
|
date={message.date}
|
||||||
content={message.message}
|
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;
|
const {hasMore, messages, appId} = this.state;
|
||||||
if (hasMore) {
|
if (hasMore) {
|
||||||
const [, maxRenderedIndex] = (this.list && this.list.getVisibleRange()) || [0, 0];
|
const [, maxRenderedIndex] = (this.list && this.list.getVisibleRange()) || [0, 0];
|
||||||
if (maxRenderedIndex > (messages.length - 30)) {
|
if (maxRenderedIndex > messages.length - 30) {
|
||||||
MessageStore.loadNext(appId);
|
MessageStore.loadNext(appId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private label = (text: string) => (
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {WithStyles} from "material-ui";
|
import {WithStyles} from 'material-ui';
|
||||||
import Delete from 'material-ui-icons/Delete';
|
import Delete from 'material-ui-icons/Delete';
|
||||||
import Edit from 'material-ui-icons/Edit';
|
import Edit from 'material-ui-icons/Edit';
|
||||||
import Grid from 'material-ui/Grid';
|
import Grid from 'material-ui/Grid';
|
||||||
|
|
@ -11,7 +11,7 @@ import * as UserAction from '../actions/UserAction';
|
||||||
import ConfirmDialog from '../component/ConfirmDialog';
|
import ConfirmDialog from '../component/ConfirmDialog';
|
||||||
import DefaultPage from '../component/DefaultPage';
|
import DefaultPage from '../component/DefaultPage';
|
||||||
import UserStore from '../stores/UserStore';
|
import UserStore from '../stores/UserStore';
|
||||||
import AddEditDialog from "./dialog/AddEditUserDialog";
|
import AddEditDialog from './dialog/AddEditUserDialog';
|
||||||
|
|
||||||
const styles = () => ({
|
const styles = () => ({
|
||||||
wrapper: {
|
wrapper: {
|
||||||
|
|
@ -21,10 +21,10 @@ const styles = () => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string
|
name: string;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
fDelete: VoidFunction
|
fDelete: VoidFunction;
|
||||||
fEdit: VoidFunction
|
fEdit: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
|
const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
|
||||||
|
|
@ -32,17 +32,21 @@ const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
|
||||||
<TableCell>{name}</TableCell>
|
<TableCell>{name}</TableCell>
|
||||||
<TableCell>{admin ? 'Yes' : 'No'}</TableCell>
|
<TableCell>{admin ? 'Yes' : 'No'}</TableCell>
|
||||||
<TableCell numeric padding="none">
|
<TableCell numeric padding="none">
|
||||||
<IconButton onClick={fEdit}><Edit/></IconButton>
|
<IconButton onClick={fEdit}>
|
||||||
<IconButton onClick={fDelete}><Delete/></IconButton>
|
<Edit />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={fDelete}>
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
users: IUser[]
|
users: IUser[];
|
||||||
createDialog: boolean
|
createDialog: boolean;
|
||||||
deleteId: number
|
deleteId: number;
|
||||||
editId: number
|
editId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Users extends Component<WithStyles<'wrapper'>, IState> {
|
class Users extends Component<WithStyles<'wrapper'>, IState> {
|
||||||
|
|
@ -68,33 +72,48 @@ class Users extends Component<WithStyles<'wrapper'>, IState> {
|
||||||
<TableRow style={{textAlign: 'center'}}>
|
<TableRow style={{textAlign: 'center'}}>
|
||||||
<TableCell>Name</TableCell>
|
<TableCell>Name</TableCell>
|
||||||
<TableCell>Admin</TableCell>
|
<TableCell>Admin</TableCell>
|
||||||
<TableCell/>
|
<TableCell />
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{users.map((user: IUser) => {
|
{users.map((user: IUser) => {
|
||||||
return (
|
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)}
|
fDelete={() => this.showDeleteDialog(user.id)}
|
||||||
fEdit={() => this.showEditDialog(user.id)}/>
|
fEdit={() => this.showEditDialog(user.id)}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Grid>
|
</Grid>
|
||||||
{this.state.createDialog && <AddEditDialog fClose={this.hideCreateDialog}
|
{this.state.createDialog && (
|
||||||
fOnSubmit={UserAction.createUser}/>}
|
<AddEditDialog
|
||||||
{editId !== -1 && <AddEditDialog fClose={this.hideEditDialog}
|
fClose={this.hideCreateDialog}
|
||||||
|
fOnSubmit={UserAction.createUser}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{editId !== -1 && (
|
||||||
|
<AddEditDialog
|
||||||
|
fClose={this.hideEditDialog}
|
||||||
fOnSubmit={UserAction.updateUser.bind(this, editId)}
|
fOnSubmit={UserAction.updateUser.bind(this, editId)}
|
||||||
name={UserStore.getById(this.state.editId).name}
|
name={UserStore.getById(this.state.editId).name}
|
||||||
admin={UserStore.getById(this.state.editId).admin}
|
admin={UserStore.getById(this.state.editId).admin}
|
||||||
isEdit={true}/>}
|
isEdit={true}
|
||||||
{deleteId !== -1 && <ConfirmDialog title="Confirm Delete"
|
/>
|
||||||
|
)}
|
||||||
|
{deleteId !== -1 && (
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Confirm Delete"
|
||||||
text={'Delete ' + UserStore.getById(this.state.deleteId).name + '?'}
|
text={'Delete ' + UserStore.getById(this.state.deleteId).name + '?'}
|
||||||
fClose={this.hideDeleteDialog}
|
fClose={this.hideDeleteDialog}
|
||||||
fOnSubmit={() => UserAction.deleteUser(this.state.deleteId)}
|
fOnSubmit={() => UserAction.deleteUser(this.state.deleteId)}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</DefaultPage>
|
</DefaultPage>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,22 @@
|
||||||
import Button from 'material-ui/Button';
|
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 TextField from 'material-ui/TextField';
|
||||||
import Tooltip from 'material-ui/Tooltip';
|
import Tooltip from 'material-ui/Tooltip';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string, description: string) => void
|
fOnSubmit: (name: string, description: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AddDialog extends Component<IProps, IState> {
|
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">
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
|
||||||
<DialogTitle id="form-dialog-title">Create an application</DialogTitle>
|
<DialogTitle id="form-dialog-title">Create an application</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText>An application is allowed to send messages.</DialogContentText>
|
<DialogContentText>
|
||||||
<TextField autoFocus margin="dense" id="name" label="Name *" type="email" value={name}
|
An application is allowed to send messages.
|
||||||
onChange={this.handleChange.bind(this, 'name')} fullWidth/>
|
</DialogContentText>
|
||||||
<TextField margin="dense" id="description" label="Short Description" value={description}
|
<TextField
|
||||||
onChange={this.handleChange.bind(this, 'description')} fullWidth multiline/>
|
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>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={!submitEnabled} onClick={submitAndClose} color="primary" variant="raised">
|
<Button
|
||||||
|
disabled={!submitEnabled}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="raised">
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,11 @@ import Tooltip from 'material-ui/Tooltip';
|
||||||
import React, {Component} from 'react';
|
import React, {Component} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string) => void
|
fOnSubmit: (name: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AddDialog extends Component<IProps, { name: string }> {
|
export default class AddDialog extends Component<IProps, {name: string}> {
|
||||||
public state = {name: ''};
|
public state = {name: ''};
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
|
@ -24,14 +24,28 @@ export default class AddDialog extends Component<IProps, { name: string }> {
|
||||||
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
|
||||||
<DialogTitle id="form-dialog-title">Create a client</DialogTitle>
|
<DialogTitle id="form-dialog-title">Create a client</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField autoFocus margin="dense" id="name" label="Name *" type="email" value={name}
|
<TextField
|
||||||
onChange={this.handleChange.bind(this, 'name')} fullWidth/>
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
id="name"
|
||||||
|
label="Name *"
|
||||||
|
type="email"
|
||||||
|
value={name}
|
||||||
|
onChange={this.handleChange.bind(this, 'name')}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
<Tooltip placement={'bottom-start'} title={submitEnabled ? '' : 'name is required'}>
|
<Tooltip
|
||||||
|
placement={'bottom-start'}
|
||||||
|
title={submitEnabled ? '' : 'name is required'}>
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={!submitEnabled} onClick={submitAndClose} color="primary" variant="raised">
|
<Button
|
||||||
|
disabled={!submitEnabled}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="raised">
|
||||||
Create
|
Create
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,17 @@ import Tooltip from 'material-ui/Tooltip';
|
||||||
import React, {ChangeEvent, Component} from 'react';
|
import React, {ChangeEvent, Component} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name?: string
|
name?: string;
|
||||||
admin?: boolean
|
admin?: boolean;
|
||||||
fClose: VoidFunction
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string, pass: string, admin: boolean) => void
|
fOnSubmit: (name: string, pass: string, admin: boolean) => void;
|
||||||
isEdit?: boolean
|
isEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
name: string
|
name: string;
|
||||||
pass: string
|
pass: string;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AddEditDialog extends Component<IProps, IState> {
|
export default class AddEditDialog extends Component<IProps, IState> {
|
||||||
|
|
@ -38,24 +38,57 @@ export default class AddEditDialog extends Component<IProps, IState> {
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title">
|
<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>
|
<DialogContent>
|
||||||
<TextField autoFocus margin="dense" id="name" label="Name *" type="email" value={name}
|
<TextField
|
||||||
onChange={this.handleChange.bind(this, 'name')} fullWidth/>
|
autoFocus
|
||||||
<TextField margin="dense" id="description" type="password" value={pass} fullWidth
|
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 *'}
|
label={isEdit ? 'Pass (empty if no change)' : 'Pass *'}
|
||||||
onChange={this.handleChange.bind(this, 'pass')}/>
|
onChange={this.handleChange.bind(this, 'pass')}
|
||||||
|
/>
|
||||||
<FormControlLabel
|
<FormControlLabel
|
||||||
control={<Switch checked={admin} onChange={this.handleChecked.bind(this, 'admin')}
|
control={
|
||||||
value="admin"/>} label="has administrator rights"/>
|
<Switch
|
||||||
|
checked={admin}
|
||||||
|
onChange={this.handleChecked.bind(this, 'admin')}
|
||||||
|
value="admin"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label="has administrator rights"
|
||||||
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
<Tooltip placement={'bottom-start'}
|
<Tooltip
|
||||||
title={namePresent ? (passPresent ? '' : 'password is required') : 'name is required'}>
|
placement={'bottom-start'}
|
||||||
|
title={
|
||||||
|
namePresent
|
||||||
|
? passPresent
|
||||||
|
? ''
|
||||||
|
: 'password is required'
|
||||||
|
: 'name is required'
|
||||||
|
}>
|
||||||
<div>
|
<div>
|
||||||
<Button disabled={!passPresent || !namePresent} onClick={submitAndClose}
|
<Button
|
||||||
color="primary" variant="raised">
|
disabled={!passPresent || !namePresent}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="raised">
|
||||||
{isEdit ? 'Save' : 'Create'}
|
{isEdit ? 'Save' : 'Create'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,18 +14,13 @@ const isLocalhost = Boolean(
|
||||||
// [::1] is the IPv6 localhost address.
|
// [::1] is the IPv6 localhost address.
|
||||||
window.location.hostname === '[::1]' ||
|
window.location.hostname === '[::1]' ||
|
||||||
// 127.0.0.1/8 is considered localhost for IPv4.
|
// 127.0.0.1/8 is considered localhost for IPv4.
|
||||||
window.location.hostname.match(
|
window.location.hostname.match(/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/)
|
||||||
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
export default function register() {
|
export default function register() {
|
||||||
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
|
||||||
// The URL constructor is available in all browsers that support SW.
|
// The URL constructor is available in all browsers that support SW.
|
||||||
const publicUrl = new URL(
|
const publicUrl = new URL(process.env.PUBLIC_URL!, window.location.toString());
|
||||||
process.env.PUBLIC_URL!,
|
|
||||||
window.location.toString()
|
|
||||||
);
|
|
||||||
if (publicUrl.origin !== window.location.origin) {
|
if (publicUrl.origin !== window.location.origin) {
|
||||||
// Our service worker won't work if PUBLIC_URL is on a different 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
|
// 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
|
navigator.serviceWorker
|
||||||
.register(swUrl)
|
.register(swUrl)
|
||||||
.then(registration => {
|
.then((registration) => {
|
||||||
registration.onupdatefound = () => {
|
registration.onupdatefound = () => {
|
||||||
const installingWorker = registration.installing;
|
const installingWorker = registration.installing;
|
||||||
if (installingWorker) {
|
if (installingWorker) {
|
||||||
|
|
@ -85,7 +80,7 @@ function registerValidSW(swUrl: string) {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch((error) => {
|
||||||
console.error('Error during service worker registration:', error);
|
console.error('Error during service worker registration:', error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -93,14 +88,14 @@ function registerValidSW(swUrl: string) {
|
||||||
function checkValidServiceWorker(swUrl: string) {
|
function checkValidServiceWorker(swUrl: string) {
|
||||||
// Check if the service worker can be found. If it can't reload the page.
|
// Check if the service worker can be found. If it can't reload the page.
|
||||||
fetch(swUrl)
|
fetch(swUrl)
|
||||||
.then(response => {
|
.then((response) => {
|
||||||
// Ensure service worker exists, and that we really are getting a JS file.
|
// Ensure service worker exists, and that we really are getting a JS file.
|
||||||
if (
|
if (
|
||||||
response.status === 404 ||
|
response.status === 404 ||
|
||||||
response.headers.get('content-type')!.indexOf('javascript') === -1
|
response.headers.get('content-type')!.indexOf('javascript') === -1
|
||||||
) {
|
) {
|
||||||
// No service worker found. Probably a different app. Reload the page.
|
// No service worker found. Probably a different app. Reload the page.
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister().then(() => {
|
registration.unregister().then(() => {
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
});
|
});
|
||||||
|
|
@ -111,15 +106,13 @@ function checkValidServiceWorker(swUrl: string) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
console.log(
|
console.log('No internet connection found. App is running in offline mode.');
|
||||||
'No internet connection found. App is running in offline mode.'
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unregister() {
|
export function unregister() {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
navigator.serviceWorker.ready.then((registration) => {
|
||||||
registration.unregister();
|
registration.unregister();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class AppStore extends EventEmitter {
|
||||||
public getById(id: number): IApplication {
|
public getById(id: number): IApplication {
|
||||||
const app = this.getByIdOrUndefined(id);
|
const app = this.getByIdOrUndefined(id);
|
||||||
if (!app) {
|
if (!app) {
|
||||||
throw new Error('app is required to exist')
|
throw new Error('app is required to exist');
|
||||||
}
|
}
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ class ClientStore extends EventEmitter {
|
||||||
public getById(id: number): IClient {
|
public getById(id: number): IClient {
|
||||||
const client = this.clients.find((c) => c.id === id);
|
const client = this.clients.find((c) => c.id === id);
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error('client is required to exist')
|
throw new Error('client is required to exist');
|
||||||
}
|
}
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
@ -29,7 +29,6 @@ class ClientStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const store = new ClientStore();
|
const store = new ClientStore();
|
||||||
dispatcher.register(store.handle.bind(store));
|
dispatcher.register(store.handle.bind(store));
|
||||||
export default store;
|
export default store;
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import AppStore from './AppStore';
|
||||||
import dispatcher, {IEvent} from './dispatcher';
|
import dispatcher, {IEvent} from './dispatcher';
|
||||||
|
|
||||||
class MessageStore extends EventEmitter {
|
class MessageStore extends EventEmitter {
|
||||||
|
private appToMessages: {[appId: number]: IAppMessages} = {};
|
||||||
private appToMessages: { [appId: number]: IAppMessages } = {};
|
|
||||||
private reset: false | number = false;
|
private reset: false | number = false;
|
||||||
private resetOnAll: false | number = false;
|
private resetOnAll: false | number = false;
|
||||||
private loading = false;
|
private loading = false;
|
||||||
|
|
@ -32,7 +31,9 @@ class MessageStore extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.loading = true;
|
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 {
|
public get(id: number): IAppMessages {
|
||||||
|
|
@ -87,7 +88,9 @@ class MessageStore extends EventEmitter {
|
||||||
|
|
||||||
private removeFromList(messages: IAppMessages, messageToDelete: IMessage): false | number {
|
private removeFromList(messages: IAppMessages, messageToDelete: IMessage): false | number {
|
||||||
if (messages) {
|
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) {
|
if (index !== -1) {
|
||||||
messages.messages.splice(index, 1);
|
messages.messages.splice(index, 1);
|
||||||
return index;
|
return index;
|
||||||
|
|
@ -97,11 +100,11 @@ class MessageStore extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateApps = (): void => {
|
private updateApps = (): void => {
|
||||||
const appToUrl: { [appId: number]: string } = {};
|
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) => {
|
Object.keys(this.appToMessages).forEach((key) => {
|
||||||
const appMessages: IAppMessages = this.appToMessages[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]));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,10 @@ import dispatcher, {IEvent} from './dispatcher';
|
||||||
|
|
||||||
export function requestPermission() {
|
export function requestPermission() {
|
||||||
if (Notify.needsPermission && Notify.isSupported()) {
|
if (Notify.needsPermission && Notify.isSupported()) {
|
||||||
Notify.requestPermission(() => console.log('granted notification permissions'),
|
Notify.requestPermission(
|
||||||
() => console.log('notification permission denied'));
|
() => console.log('granted notification permissions'),
|
||||||
|
() => console.log('notification permission denied')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -25,7 +27,8 @@ function closeAfterTimeout(event: Event) {
|
||||||
}, 5000);
|
}, 5000);
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatcher.register((data: IEvent): void => {
|
dispatcher.register(
|
||||||
|
(data: IEvent): void => {
|
||||||
if (data.type === 'ONE_MESSAGE') {
|
if (data.type === 'ONE_MESSAGE') {
|
||||||
const msg = data.payload;
|
const msg = data.payload;
|
||||||
|
|
||||||
|
|
@ -37,4 +40,5 @@ dispatcher.register((data: IEvent): void => {
|
||||||
});
|
});
|
||||||
notify.show();
|
notify.show();
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class SnackBarStore extends EventEmitter {
|
||||||
|
|
||||||
public next(): string {
|
public next(): string {
|
||||||
if (!this.hasNext()) {
|
if (!this.hasNext()) {
|
||||||
throw new Error("no such element")
|
throw new Error('no such element');
|
||||||
}
|
}
|
||||||
return this.messages.shift() as string;
|
return this.messages.shift() as string;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import {Dispatcher} from 'flux';
|
import {Dispatcher} from 'flux';
|
||||||
|
|
||||||
export interface IEvent {
|
export interface IEvent {
|
||||||
type: string
|
type: string;
|
||||||
payload?: any
|
payload?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new Dispatcher<IEvent>();
|
export default new Dispatcher<IEvent>();
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
declare module 'react-timeago' {
|
declare module 'react-timeago' {
|
||||||
import React from "react";
|
import React from 'react';
|
||||||
|
|
||||||
export interface ITimeAgoProps {
|
export interface ITimeAgoProps {
|
||||||
date: string
|
date: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TimeAgo extends React.Component<ITimeAgoProps, any> {}
|
export default class TimeAgo extends React.Component<ITimeAgoProps, any> {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,54 @@
|
||||||
interface IApplication {
|
interface IApplication {
|
||||||
id: number
|
id: number;
|
||||||
token: string
|
token: string;
|
||||||
name: string
|
name: string;
|
||||||
description: string
|
description: string;
|
||||||
image: string
|
image: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IClient {
|
interface IClient {
|
||||||
id: number
|
id: number;
|
||||||
token: string
|
token: string;
|
||||||
name: string
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMessage {
|
interface IMessage {
|
||||||
id: number
|
id: number;
|
||||||
appid: number
|
appid: number;
|
||||||
message: string
|
message: string;
|
||||||
title: string
|
title: string;
|
||||||
priority: number
|
priority: number;
|
||||||
date: string
|
date: string;
|
||||||
image?: string
|
image?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPagedMessages {
|
interface IPagedMessages {
|
||||||
paging: IPaging
|
paging: IPaging;
|
||||||
messages: IMessage[]
|
messages: IMessage[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IPaging {
|
interface IPaging {
|
||||||
next?: string
|
next?: string;
|
||||||
since?: number
|
since?: number;
|
||||||
size: number
|
size: number;
|
||||||
limit: number
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IUser {
|
interface IUser {
|
||||||
id: number
|
id: number;
|
||||||
name: string
|
name: string;
|
||||||
admin: boolean
|
admin: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IVersion {
|
interface IVersion {
|
||||||
version: string
|
version: string;
|
||||||
commit: string
|
commit: string;
|
||||||
buildDate: string
|
buildDate: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAppMessages {
|
interface IAppMessages {
|
||||||
messages: IMessage[]
|
messages: IMessage[];
|
||||||
hasMore: boolean
|
hasMore: boolean;
|
||||||
nextSince: number,
|
nextSince: number;
|
||||||
id?: number
|
id?: number;
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue