Format
This commit is contained in:
parent
1afa51959f
commit
a3f081307b
|
|
@ -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> {
|
||||
|
|
@ -64,7 +64,7 @@ class Layout extends React.Component<WithStyles<'content'>, IState> {
|
|||
|
||||
public componentDidMount() {
|
||||
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});
|
||||
});
|
||||
}
|
||||
|
|
@ -97,31 +97,41 @@ 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}/>
|
||||
<Navigation loggedIn={loggedIn}/>
|
||||
<CssBaseline />
|
||||
<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}
|
||||
<Route exact path="/login" render={loginRoute}/>
|
||||
{loggedIn ? null : <Redirect to="/login"/>}
|
||||
<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}/>
|
||||
{authenticating ? (
|
||||
<Route path="/">
|
||||
<LoadingSpinner />
|
||||
</Route>
|
||||
) : null}
|
||||
<Route exact path="/login" render={loginRoute} />
|
||||
{loggedIn ? null : <Redirect to="/login" />}
|
||||
<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>
|
||||
</main>
|
||||
{showSettings && <SettingsDialog fClose={this.hideSettings}/>}
|
||||
<ScrollUpButton/>
|
||||
<SnackBarHandler/>
|
||||
{showSettings && <SettingsDialog fClose={this.hideSettings} />}
|
||||
<ScrollUpButton />
|
||||
<SnackBarHandler />
|
||||
</div>
|
||||
</HashRouter>
|
||||
</MuiThemeProvider>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
fetchApps();
|
||||
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
|
||||
}).then(() => snack('Application deleted'));
|
||||
axios
|
||||
.delete(config.get('url') + 'application/' + id)
|
||||
.then(() => {
|
||||
fetchApps();
|
||||
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
|
||||
})
|
||||
.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'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>) => {
|
||||
newMessages(-1, resp.data);
|
||||
});
|
||||
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,11 +44,10 @@ export function deleteMessagesByApp(id: number) {
|
|||
snack('Messages deleted');
|
||||
});
|
||||
} else {
|
||||
axios.delete(config.get('url') + 'application/' + id + '/message')
|
||||
.then(() => {
|
||||
dispatcher.dispatch({type: 'DELETE_MESSAGES', payload: id});
|
||||
snack('Deleted all messages from the application');
|
||||
});
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -16,20 +16,29 @@ export function login(username: string, password: string) {
|
|||
const browser = detect();
|
||||
const name = (browser && browser.name + ' ' + browser.version) || 'unknown browser';
|
||||
authenticating();
|
||||
axios.create().request({
|
||||
url: config.get('url') + 'client',
|
||||
method: 'POST',
|
||||
data: {name},
|
||||
auth: {username, password},
|
||||
}).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(() => {
|
||||
snack('Login failed');
|
||||
noAuthentication();
|
||||
});
|
||||
axios
|
||||
.create()
|
||||
.request({
|
||||
url: config.get('url') + 'client',
|
||||
method: 'POST',
|
||||
data: {name},
|
||||
auth: {username, password},
|
||||
})
|
||||
.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(() => {
|
||||
snack('Login failed');
|
||||
noAuthentication();
|
||||
});
|
||||
}
|
||||
|
||||
/** Log the user out. */
|
||||
|
|
@ -44,17 +53,21 @@ export function logout() {
|
|||
}
|
||||
|
||||
export function tryAuthenticate() {
|
||||
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) => {
|
||||
if (getToken()) {
|
||||
setAuthorizationToken(null);
|
||||
snack('Authentication failed, try to re-login. (client or user was deleted)');
|
||||
}
|
||||
noAuthentication();
|
||||
return Promise.reject(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) => {
|
||||
if (getToken()) {
|
||||
setAuthorizationToken(null);
|
||||
snack('Authentication failed, try to re-login. (client or user was deleted)');
|
||||
}
|
||||
noAuthentication();
|
||||
return Promise.reject(resp);
|
||||
});
|
||||
}
|
||||
|
||||
export function checkIfAlreadyLoggedIn() {
|
||||
|
|
@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
// @ts-ignore accessing private member
|
||||
delete this.cache[index];
|
||||
});
|
||||
Object.keys(this.cache)
|
||||
.filter((index) => +index >= startIndex)
|
||||
.forEach((index) => {
|
||||
// @ts-ignore accessing private member
|
||||
delete this.cache[index];
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public componentDidUpdate() {
|
||||
// @ts-ignore accessing private member
|
||||
const hasCacheForLastRenderedItem = Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]];
|
||||
const hasCacheForLastRenderedItem =
|
||||
// @ts-ignore accessing private member
|
||||
Object.keys(this.cache).length && this.cache[this.getVisibleRange()[1]];
|
||||
// @ts-ignore accessing private member
|
||||
super.componentDidUpdate();
|
||||
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 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/> users</Button></Link>
|
||||
: ''}
|
||||
{admin ? (
|
||||
<Link className={classes.link} to="/users">
|
||||
<Button color="inherit">
|
||||
<SupervisorAccount />
|
||||
users
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
<Link className={classes.link} to="/applications">
|
||||
<Button color="inherit"><Chat/> apps</Button>
|
||||
<Button color="inherit">
|
||||
<Chat />
|
||||
apps
|
||||
</Button>
|
||||
</Link>
|
||||
<Link className={classes.link} to="/clients"><Button color="inherit">
|
||||
<DevicesOther/> clients</Button>
|
||||
<Link className={classes.link} to="/clients">
|
||||
<Button color="inherit">
|
||||
<DevicesOther />
|
||||
clients
|
||||
</Button>
|
||||
</Link>
|
||||
<Button color="inherit" onClick={showSettings}><AccountCircle/> {name}</Button>
|
||||
<Button color="inherit" onClick={UserAction.logout}><ExitToApp/> Logout</Button>
|
||||
<Button color="inherit" onClick={showSettings}>
|
||||
<AccountCircle />
|
||||
|
||||
{name}
|
||||
</Button>
|
||||
<Button color="inherit" onClick={UserAction.logout}>
|
||||
<ExitToApp />
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@ export default function LoadingSpinner() {
|
|||
return (
|
||||
<DefaultPage title="" maxWidth={250} hideButton={true}>
|
||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||
<CircularProgress size={150}/>
|
||||
<CircularProgress size={150} />
|
||||
</Grid>
|
||||
</DefaultPage>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}>
|
||||
|
|
@ -55,9 +69,11 @@ function Message({fDelete, classes, title, date, content, image}: IProps & Style
|
|||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
<TimeAgo date={date}/>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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,36 +45,39 @@ 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) => {
|
||||
return (
|
||||
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
|
||||
<ListItem button>
|
||||
<ListItemText primary={app.name}/>
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
const userApps =
|
||||
apps.length === 0
|
||||
? null
|
||||
: apps.map((app) => {
|
||||
return (
|
||||
<Link className={classes.link} to={'/messages/' + app.id} key={app.id}>
|
||||
<ListItem button>
|
||||
<ListItemText primary={app.name} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
});
|
||||
|
||||
const placeholderItems = [
|
||||
<ListItem button disabled key={-1}>
|
||||
<ListItemText primary="Some Server"/>
|
||||
<ListItemText primary="Some Server" />
|
||||
</ListItem>,
|
||||
<ListItem button disabled key={-2}>
|
||||
<ListItemText primary="A Raspberry PI"/>
|
||||
<ListItemText primary="A Raspberry PI" />
|
||||
</ListItem>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Drawer variant="permanent" classes={{paper: classes.drawerPaper}}>
|
||||
<div className={classes.toolbar}/>
|
||||
<div className={classes.toolbar} />
|
||||
<Link className={classes.link} to="/">
|
||||
<ListItem button disabled={!loggedIn}>
|
||||
<ListItemText primary="All Messages"/>
|
||||
<ListItemText primary="All Messages" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Divider/>
|
||||
<Divider />
|
||||
<div>{loggedIn ? userApps : placeholderItems}</div>
|
||||
<Divider/>
|
||||
<Divider />
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
|
@ -82,4 +85,4 @@ class Navigation extends Component<IProps & Styles, IState> {
|
|||
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 {
|
||||
public render() {
|
||||
return (
|
||||
<Button variant="fab" color="primary"
|
||||
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
|
||||
onClick={this.scrollUp}>
|
||||
<KeyboardArrowUp/>
|
||||
<Button
|
||||
variant="fab"
|
||||
color="primary"
|
||||
style={{position: 'fixed', bottom: '30px', right: '30px', zIndex: 100000}}
|
||||
onClick={this.scrollUp}>
|
||||
<KeyboardArrowUp />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,15 +27,25 @@ 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"
|
||||
variant="raised">
|
||||
<Button
|
||||
disabled={pass.length === 0}
|
||||
onClick={submitAndClose}
|
||||
color="primary"
|
||||
variant="raised">
|
||||
Change
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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,12 +39,18 @@ 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}>
|
||||
<Close/>
|
||||
<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
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
@ -22,11 +22,9 @@ class ToggleVisibility extends Component<IProps, IState> {
|
|||
return (
|
||||
<div style={style}>
|
||||
<IconButton onClick={this.toggleVisibility}>
|
||||
{this.state.visible ? <VisibilityOff/> : <Visibility/>}
|
||||
{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;
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
@ -36,6 +36,6 @@ declare global {
|
|||
config.set(window.config || defaultDevConfig);
|
||||
}
|
||||
UserAction.checkIfAlreadyLoggedIn();
|
||||
ReactDOM.render(<Layout/>, document.getElementById('root'));
|
||||
ReactDOM.render(<Layout />, document.getElementById('root'));
|
||||
registerServiceWorker();
|
||||
}());
|
||||
})();
|
||||
|
|
|
|||
|
|
@ -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,40 +36,61 @@ class Applications extends Component<{}, IState> {
|
|||
public render() {
|
||||
const {apps, createDialog, deleteId} = this.state;
|
||||
return (
|
||||
<DefaultPage title="Applications" buttonTitle="Create Application" maxWidth={1000}
|
||||
fButton={this.showCreateDialog}>
|
||||
<DefaultPage
|
||||
title="Applications"
|
||||
buttonTitle="Create Application"
|
||||
maxWidth={1000}
|
||||
fButton={this.showCreateDialog}>
|
||||
<Grid item xs={12}>
|
||||
<Paper elevation={6}>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell padding="checkbox" style={{width: 80}}/>
|
||||
<TableCell padding="checkbox" style={{width: 80}} />
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Token</TableCell>
|
||||
<TableCell>Description</TableCell>
|
||||
<TableCell/>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<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"
|
||||
text={'Delete ' + AppStore.getById(deleteId).name + '?'}
|
||||
fClose={this.hideCloseDialog}
|
||||
fOnSubmit={() => AppAction.deleteApp(deleteId)}
|
||||
/>}
|
||||
{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,28 +123,33 @@ 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>
|
||||
<TableCell>
|
||||
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center'}}/>
|
||||
<ToggleVisibility value={value} style={{display: 'flex', alignItems: 'center'}} />
|
||||
</TableCell>
|
||||
<TableCell>{description}</TableCell>
|
||||
<TableCell numeric padding="none">
|
||||
<IconButton onClick={fDelete}><Delete/></IconButton>
|
||||
<IconButton onClick={fDelete}>
|
||||
<Delete />
|
||||
</IconButton>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -40,25 +43,38 @@ class Clients extends Component<{}, IState> {
|
|||
<TableRow style={{textAlign: 'center'}}>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell style={{width: 200}}>token</TableCell>
|
||||
<TableCell/>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<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"
|
||||
text={'Delete ' + ClientStore.getById(this.state.deleteId).name + '?'}
|
||||
fClose={this.hideDeleteDelete}
|
||||
fOnSubmit={this.deleteClient}/>}
|
||||
{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}
|
||||
/>
|
||||
)}
|
||||
</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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,20 +10,18 @@ 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> {
|
||||
class Messages extends Component<IProps, IState> {
|
||||
private static appId(props: IProps) {
|
||||
if (props === undefined) {
|
||||
return -1;
|
||||
|
|
@ -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
|
||||
? (
|
||||
<div style={{width: '100%'}}>
|
||||
<ReactList key={appId}
|
||||
ref={(el: ReactList) => this.list = el}
|
||||
itemRenderer={this.renderMessage}
|
||||
length={messages.length}
|
||||
threshold={1000}
|
||||
pageSize={30}
|
||||
type='variable'
|
||||
/>
|
||||
{hasMore
|
||||
? <Grid item xs={12} style={{textAlign: 'center'}}><CircularProgress size={100}/></Grid>
|
||||
: this.label('You\'ve reached the end')}
|
||||
</div>
|
||||
)
|
||||
: this.label('No messages')
|
||||
}
|
||||
<DefaultPage
|
||||
title={name}
|
||||
buttonTitle="Delete All"
|
||||
fButton={deleteMessages}
|
||||
buttonDisabled={!hasMessages}>
|
||||
{hasMessages ? (
|
||||
<div style={{width: '100%'}}>
|
||||
<ReactList
|
||||
key={appId}
|
||||
ref={(el: ReactList) => (this.list = el)}
|
||||
itemRenderer={this.renderMessage}
|
||||
length={messages.length}
|
||||
threshold={1000}
|
||||
pageSize={30}
|
||||
type="variable"
|
||||
/>
|
||||
{hasMore ? (
|
||||
<Grid item xs={12} style={{textAlign: 'center'}}>
|
||||
<CircularProgress size={100} />
|
||||
</Grid>
|
||||
) : (
|
||||
this.label("You've reached the end")
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
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}
|
||||
fDelete={this.deleteMessage(message)}
|
||||
title={message.title}
|
||||
date={message.date}
|
||||
content={message.message}
|
||||
image={message.image}/>
|
||||
<Message
|
||||
key={key}
|
||||
fDelete={this.deleteMessage(message)}
|
||||
title={message.title}
|
||||
date={message.date}
|
||||
content={message.message}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
|
|
@ -68,33 +72,48 @@ class Users extends Component<WithStyles<'wrapper'>, IState> {
|
|||
<TableRow style={{textAlign: 'center'}}>
|
||||
<TableCell>Name</TableCell>
|
||||
<TableCell>Admin</TableCell>
|
||||
<TableCell/>
|
||||
<TableCell />
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user: IUser) => {
|
||||
return (
|
||||
<UserRow key={user.id} name={user.name} admin={user.admin}
|
||||
fDelete={() => this.showDeleteDialog(user.id)}
|
||||
fEdit={() => this.showEditDialog(user.id)}/>
|
||||
<UserRow
|
||||
key={user.id}
|
||||
name={user.name}
|
||||
admin={user.admin}
|
||||
fDelete={() => this.showDeleteDialog(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}
|
||||
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"
|
||||
text={'Delete ' + UserStore.getById(this.state.deleteId).name + '?'}
|
||||
fClose={this.hideDeleteDialog}
|
||||
fOnSubmit={() => UserAction.deleteUser(this.state.deleteId)}
|
||||
/>}
|
||||
{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"
|
||||
text={'Delete ' + UserStore.getById(this.state.deleteId).name + '?'}
|
||||
fClose={this.hideDeleteDialog}
|
||||
fOnSubmit={() => UserAction.deleteUser(this.state.deleteId)}
|
||||
/>
|
||||
)}
|
||||
</DefaultPage>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ 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 }> {
|
||||
export default class AddDialog extends Component<IProps, {name: string}> {
|
||||
public state = {name: ''};
|
||||
|
||||
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">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
label={isEdit ? 'Pass (empty if no change)' : 'Pass *'}
|
||||
onChange={this.handleChange.bind(this, 'pass')}/>
|
||||
<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')}
|
||||
/>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -11,21 +11,16 @@
|
|||
|
||||
const isLocalhost = Boolean(
|
||||
window.location.hostname === 'localhost' ||
|
||||
// [::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}$/
|
||||
)
|
||||
// [::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}$/)
|
||||
);
|
||||
|
||||
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
|
||||
|
|
@ -45,7 +40,7 @@ export default function register() {
|
|||
navigator.serviceWorker.ready.then(() => {
|
||||
console.log(
|
||||
'This web app is being served cache-first by a service ' +
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
'worker. To learn more, visit https://goo.gl/SC7cgQ'
|
||||
);
|
||||
});
|
||||
} else {
|
||||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -4,8 +4,7 @@ import AppStore from './AppStore';
|
|||
import dispatcher, {IEvent} from './dispatcher';
|
||||
|
||||
class MessageStore extends EventEmitter {
|
||||
|
||||
private appToMessages: { [appId: number]: IAppMessages } = {};
|
||||
private appToMessages: {[appId: number]: IAppMessages} = {};
|
||||
private reset: false | number = false;
|
||||
private resetOnAll: false | number = false;
|
||||
private loading = 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;
|
||||
|
|
@ -97,11 +100,11 @@ class MessageStore extends EventEmitter {
|
|||
}
|
||||
|
||||
private updateApps = (): void => {
|
||||
const appToUrl: { [appId: number]: string } = {};
|
||||
AppStore.get().forEach((app) => appToUrl[app.id] = app.image);
|
||||
const appToUrl: {[appId: number]: string} = {};
|
||||
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]));
|
||||
});
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,16 +27,18 @@ function closeAfterTimeout(event: Event) {
|
|||
}, 5000);
|
||||
}
|
||||
|
||||
dispatcher.register((data: IEvent): void => {
|
||||
if (data.type === 'ONE_MESSAGE') {
|
||||
const msg = data.payload;
|
||||
dispatcher.register(
|
||||
(data: IEvent): void => {
|
||||
if (data.type === 'ONE_MESSAGE') {
|
||||
const msg = data.payload;
|
||||
|
||||
const notify = new Notify(msg.title, {
|
||||
body: msg.message,
|
||||
icon: msg.image,
|
||||
notifyClick: closeAndFocus,
|
||||
notifyShow: closeAfterTimeout,
|
||||
});
|
||||
notify.show();
|
||||
const notify = new Notify(msg.title, {
|
||||
body: msg.message,
|
||||
icon: msg.image,
|
||||
notifyClick: closeAndFocus,
|
||||
notifyShow: closeAfterTimeout,
|
||||
});
|
||||
notify.show();
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import {Dispatcher} from 'flux';
|
||||
|
||||
export interface IEvent {
|
||||
type: string
|
||||
payload?: any
|
||||
type: string;
|
||||
payload?: any;
|
||||
}
|
||||
|
||||
export default new Dispatcher<IEvent>();
|
||||
|
|
|
|||
|
|
@ -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> {}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Reference in New Issue