From a3f081307b1cdca02ca992e49f870bc2142e5dde Mon Sep 17 00:00:00 2001 From: Jannis Mattheis Date: Wed, 22 Aug 2018 20:17:51 +0200 Subject: [PATCH] Format --- ui/src/Layout.tsx | 60 +++++++------ ui/src/actions/AppAction.ts | 21 +++-- ui/src/actions/ClientAction.ts | 10 ++- ui/src/actions/GlobalAction.ts | 2 +- ui/src/actions/MessageAction.ts | 31 ++++--- ui/src/actions/UserAction.ts | 77 ++++++++++------- ui/src/component/ConfirmDialog.tsx | 21 +++-- ui/src/component/Container.tsx | 4 +- ui/src/component/DefaultPage.tsx | 33 ++++++-- ui/src/component/FixedReactList.tsx | 18 ++-- ui/src/component/Header.tsx | 66 ++++++++++----- ui/src/component/LoadingSpinner.tsx | 4 +- ui/src/component/Message.tsx | 36 +++++--- ui/src/component/Navigation.tsx | 43 +++++----- ui/src/component/ScrollUpButton.tsx | 10 ++- ui/src/component/SettingsDialog.tsx | 22 +++-- ui/src/component/SnackBarHandler.tsx | 28 ++++--- ui/src/component/ToggleVisibility.tsx | 13 ++- ui/src/config.ts | 6 +- ui/src/index.tsx | 6 +- ui/src/pages/Applications.tsx | 80 ++++++++++++------ ui/src/pages/Clients.tsx | 56 +++++++++---- ui/src/pages/Login.tsx | 32 +++++-- ui/src/pages/Messages.tsx | 88 +++++++++++--------- ui/src/pages/Users.tsx | 75 ++++++++++------- ui/src/pages/dialog/AddApplicationDialog.tsx | 50 ++++++++--- ui/src/pages/dialog/AddClientDialog.tsx | 30 +++++-- ui/src/pages/dialog/AddEditUserDialog.tsx | 75 ++++++++++++----- ui/src/registerServiceWorker.ts | 31 +++---- ui/src/stores/AppStore.ts | 2 +- ui/src/stores/ClientStore.ts | 3 +- ui/src/stores/MessageStore.ts | 17 ++-- ui/src/stores/Notifications.ts | 30 ++++--- ui/src/stores/SnackBarStore.ts | 2 +- ui/src/stores/dispatcher.ts | 4 +- ui/src/typedef/notifyjs.d.ts | 2 +- ui/src/typedef/react-timeago.d.ts | 5 +- ui/src/types.ts | 64 +++++++------- 38 files changed, 729 insertions(+), 428 deletions(-) diff --git a/ui/src/Layout.tsx b/ui/src/Layout.tsx index 2309e18..f67b622 100644 --- a/ui/src/Layout.tsx +++ b/ui/src/Layout.tsx @@ -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, IState> { @@ -64,7 +64,7 @@ class Layout extends React.Component, IState> { public componentDidMount() { if (this.state.version === Layout.defaultVersion) { - axios.get(config.get('url') + 'version').then((resp:AxiosResponse) => { + axios.get(config.get('url') + 'version').then((resp: AxiosResponse) => { this.setState({...this.state, version: resp.data.version}); }); } @@ -97,31 +97,41 @@ class Layout extends React.Component, IState> { const {name, admin, version, loggedIn, showSettings, authenticating} = this.state; const {classes} = this.props; const theme = this.state.darkTheme ? darkTheme : lightTheme; - const loginRoute = () => (loggedIn ? () : ()); + const loginRoute = () => (loggedIn ? : ); return (
- -
- + +
+
- {authenticating ? : null} - - {loggedIn ? null : } - - - - - + {authenticating ? ( + + + + ) : null} + + {loggedIn ? null : } + + + + +
- {showSettings && } - - + {showSettings && } + +
diff --git a/ui/src/actions/AppAction.ts b/ui/src/actions/AppAction.ts index 56f480e..c482518 100644 --- a/ui/src/actions/AppAction.ts +++ b/ui/src/actions/AppAction.ts @@ -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')); } diff --git a/ui/src/actions/ClientAction.ts b/ui/src/actions/ClientAction.ts index 1d62d02..83cc543 100644 --- a/ui/src/actions/ClientAction.ts +++ b/ui/src/actions/ClientAction.ts @@ -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')); } diff --git a/ui/src/actions/GlobalAction.ts b/ui/src/actions/GlobalAction.ts index ceef559..f60df32 100644 --- a/ui/src/actions/GlobalAction.ts +++ b/ui/src/actions/GlobalAction.ts @@ -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'; diff --git a/ui/src/actions/MessageAction.ts b/ui/src/actions/MessageAction.ts index aaaf9a6..cd6cf49 100644 --- a/ui/src/actions/MessageAction.ts +++ b/ui/src/actions/MessageAction.ts @@ -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) => { - newMessages(-1, resp.data); - }); + return axios + .get(config.get('url') + 'message?since=' + since) + .then((resp: AxiosResponse) => { + 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) => { 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; diff --git a/ui/src/actions/UserAction.ts b/ui/src/actions/UserAction.ts index c44fd18..aa2a273 100644 --- a/ui/src/actions/UserAction.ts +++ b/ui/src/actions/UserAction.ts @@ -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')); } /** diff --git a/ui/src/component/ConfirmDialog.tsx b/ui/src/component/ConfirmDialog.tsx index 73bc8b4..0592d98 100644 --- a/ui/src/component/ConfirmDialog.tsx +++ b/ui/src/component/ConfirmDialog.tsx @@ -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,8 +27,10 @@ export default function ConfirmDialog({title, text, fClose, fOnSubmit}: IProps) - + ); -} \ No newline at end of file +} diff --git a/ui/src/component/Container.tsx b/ui/src/component/Container.tsx index 01d8d34..fbf6cd6 100644 --- a/ui/src/component/Container.tsx +++ b/ui/src/component/Container.tsx @@ -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> = ({classes, children, style}) => { diff --git a/ui/src/component/DefaultPage.tsx b/ui/src/component/DefaultPage.tsx index fdbf927..f9e05fb 100644 --- a/ui/src/component/DefaultPage.tsx +++ b/ui/src/component/DefaultPage.tsx @@ -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 = ({title, buttonTitle, fButton, buttonDisabled = false, maxWidth = 700, hideButton, children}) => ( +const DefaultPage: SFC = ({ + title, + buttonTitle, + fButton, + buttonDisabled = false, + maxWidth = 700, + hideButton, + children, +}) => (
{title} - {hideButton ? null : } + {hideButton ? null : ( + + )} {children} diff --git a/ui/src/component/FixedReactList.tsx b/ui/src/component/FixedReactList.tsx index 7925f7b..920d8a6 100644 --- a/ui/src/component/FixedReactList.tsx +++ b/ui/src/component/FixedReactList.tsx @@ -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) { diff --git a/ui/src/component/Header.tsx b/ui/src/component/Header.tsx index b007b10..86891c5 100644 --- a/ui/src/component/Header.tsx +++ b/ui/src/component/Header.tsx @@ -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 { @@ -53,18 +53,25 @@ class Header extends Component { {loggedIn && this.renderButtons(name, admin)} - + + + ); @@ -74,18 +81,37 @@ class Header extends Component { const {classes, showSettings} = this.props; return (
- {admin - ? - - : ''} + {admin ? ( + + + + ) : ( + '' + )} - + - + + - - + +
); } diff --git a/ui/src/component/LoadingSpinner.tsx b/ui/src/component/LoadingSpinner.tsx index bed8439..36cacbe 100644 --- a/ui/src/component/LoadingSpinner.tsx +++ b/ui/src/component/LoadingSpinner.tsx @@ -7,8 +7,8 @@ export default function LoadingSpinner() { return ( - + ); -}; +} diff --git a/ui/src/component/Message.tsx b/ui/src/component/Message.tsx index 9ca8ee9..7b65c06 100644 --- a/ui/src/component/Message.tsx +++ b/ui/src/component/Message.tsx @@ -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
- app logo + app logo
@@ -55,9 +69,11 @@ function Message({fDelete, classes, title, date, content, image}: IProps & Style {title} - + - + + +
{content}
diff --git a/ui/src/component/Navigation.tsx b/ui/src/component/Navigation.tsx index daee908..dde7d0d 100644 --- a/ui/src/component/Navigation.tsx +++ b/ui/src/component/Navigation.tsx @@ -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 { @@ -45,36 +45,39 @@ class Navigation extends Component { const {classes, loggedIn} = this.props; const {apps} = this.state; - const userApps = apps.length === 0 ? null : apps.map((app) => { - return ( - - - - - - ); - }); + const userApps = + apps.length === 0 + ? null + : apps.map((app) => { + return ( + + + + + + ); + }); const placeholderItems = [ - + , - + , ]; return ( -
+
- + - +
{loggedIn ? userApps : placeholderItems}
- + ); } @@ -82,4 +85,4 @@ class Navigation extends Component { private updateApps = () => this.setState({apps: AppStore.get()}); } -export default withStyles(styles,{withTheme: true})(Navigation); +export default withStyles(styles, {withTheme: true})(Navigation); diff --git a/ui/src/component/ScrollUpButton.tsx b/ui/src/component/ScrollUpButton.tsx index 38bd9ea..325a6ed 100644 --- a/ui/src/component/ScrollUpButton.tsx +++ b/ui/src/component/ScrollUpButton.tsx @@ -5,10 +5,12 @@ import React, {Component} from 'react'; class ScrollUpButton extends Component { public render() { return ( - ); } diff --git a/ui/src/component/SettingsDialog.tsx b/ui/src/component/SettingsDialog.tsx index d82d045..f03634a 100644 --- a/ui/src/component/SettingsDialog.tsx +++ b/ui/src/component/SettingsDialog.tsx @@ -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 { @@ -27,15 +27,25 @@ export default class SettingsDialog extends Component { Change Password - +
-
diff --git a/ui/src/component/SnackBarHandler.tsx b/ui/src/component/SnackBarHandler.tsx index 4530308..772eeed 100644 --- a/ui/src/component/SnackBarHandler.tsx +++ b/ui/src/component/SnackBarHandler.tsx @@ -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 ( {current}} action={ - - + + } /> @@ -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 + ); } }; diff --git a/ui/src/component/ToggleVisibility.tsx b/ui/src/component/ToggleVisibility.tsx index 96a0e06..e3f978f 100644 --- a/ui/src/component/ToggleVisibility.tsx +++ b/ui/src/component/ToggleVisibility.tsx @@ -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 { @@ -22,11 +22,9 @@ class ToggleVisibility extends Component { return (
- {this.state.visible ? : } + {this.state.visible ? : } - - {text} - + {text}
); } @@ -34,5 +32,4 @@ class ToggleVisibility extends Component { private toggleVisibility = () => this.setState({visible: !this.state.visible}); } - export default ToggleVisibility; diff --git a/ui/src/config.ts b/ui/src/config.ts index 03791ec..40cd0bc 100644 --- a/ui/src/config.ts +++ b/ui/src/config.ts @@ -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]; -} \ No newline at end of file +} diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 4c5a98e..83d4104 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -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(, document.getElementById('root')); + ReactDOM.render(, document.getElementById('root')); registerServiceWorker(); -}()); +})(); diff --git a/ui/src/pages/Applications.tsx b/ui/src/pages/Applications.tsx index 632c7f5..16567d9 100644 --- a/ui/src/pages/Applications.tsx +++ b/ui/src/pages/Applications.tsx @@ -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 ( - + - + Name Token Description - + {apps.map((app: IApplication) => { return ( - this.uploadImage(app.id)} - fDelete={() => this.showCloseDialog(app.id)}/> + this.uploadImage(app.id)} + fDelete={() => this.showCloseDialog(app.id)} + /> ); })}
- this.upload = upload} type="file" style={{display: 'none'}} - onChange={this.onUploadImage}/> + (this.upload = upload)} + type="file" + style={{display: 'none'}} + onChange={this.onUploadImage} + />
- {createDialog && } - {deleteId !== -1 && AppAction.deleteApp(deleteId)} - />} + {createDialog && ( + + )} + {deleteId !== -1 && ( + AppAction.deleteApp(deleteId)} + /> + )}
); } @@ -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 = ({name, value, description, fDelete, fUpload, image}) => (
- + + + +
{name} - + {description} - + + +
); diff --git a/ui/src/pages/Clients.tsx b/ui/src/pages/Clients.tsx index d8a0ed4..ddb686f 100644 --- a/ui/src/pages/Clients.tsx +++ b/ui/src/pages/Clients.tsx @@ -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 ( - + @@ -40,25 +43,38 @@ class Clients extends Component<{}, IState> { Name token - + {clients.map((client: IClient) => { return ( - this.showDeleteDialog(client.id)}/> + this.showDeleteDialog(client.id)} + /> ); })}
- {showDialog && } - {deleteId !== -1 && } + {showDialog && ( + + )} + {deleteId !== -1 && ( + + )}
); } @@ -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 = ({name, value, fDelete}) => ( {name} - + - + + + ); - export default Clients; diff --git a/ui/src/pages/Login.tsx b/ui/src/pages/Login.tsx index 8963214..e529c7a 100644 --- a/ui/src/pages/Login.tsx +++ b/ui/src/pages/Login.tsx @@ -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> {
- - - diff --git a/ui/src/pages/Messages.tsx b/ui/src/pages/Messages.tsx index 30be4f9..4d15eeb 100644 --- a/ui/src/pages/Messages.tsx +++ b/ui/src/pages/Messages.tsx @@ -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 { -} +interface IProps extends RouteComponentProps {} 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 { +class Messages extends Component { private static appId(props: IProps) { if (props === undefined) { return -1; @@ -57,25 +55,33 @@ class Messages extends Component { const deleteMessages = () => MessageAction.deleteMessagesByApp(appId); return ( - - {hasMessages - ? ( -
- this.list = el} - itemRenderer={this.renderMessage} - length={messages.length} - threshold={1000} - pageSize={30} - type='variable' - /> - {hasMore - ? - : this.label('You\'ve reached the end')} -
- ) - : this.label('No messages') - } + + {hasMessages ? ( +
+ (this.list = el)} + itemRenderer={this.renderMessage} + length={messages.length} + threshold={1000} + pageSize={30} + type="variable" + /> + {hasMore ? ( + + + + ) : ( + this.label("You've reached the end") + )} +
+ ) : ( + this.label('No messages') + )}
); } @@ -102,12 +108,14 @@ class Messages extends Component { this.checkIfLoadMore(); const message: IMessage = this.state.messages[index]; return ( - + ); }; @@ -115,14 +123,18 @@ class Messages extends Component { 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) => ( - {text} + + + {text} + + ); } diff --git a/ui/src/pages/Users.tsx b/ui/src/pages/Users.tsx index 5fdec79..ccd635e 100644 --- a/ui/src/pages/Users.tsx +++ b/ui/src/pages/Users.tsx @@ -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 = ({name, admin, fDelete, fEdit}) => ( @@ -32,17 +32,21 @@ const UserRow: SFC = ({name, admin, fDelete, fEdit}) => ( {name} {admin ? 'Yes' : 'No'} - - + + + + + + ); interface IState { - users: IUser[] - createDialog: boolean - deleteId: number - editId: number + users: IUser[]; + createDialog: boolean; + deleteId: number; + editId: number; } class Users extends Component, IState> { @@ -68,33 +72,48 @@ class Users extends Component, IState> { Name Admin - + {users.map((user: IUser) => { return ( - this.showDeleteDialog(user.id)} - fEdit={() => this.showEditDialog(user.id)}/> + this.showDeleteDialog(user.id)} + fEdit={() => this.showEditDialog(user.id)} + /> ); })}
- {this.state.createDialog && } - {editId !== -1 && } - {deleteId !== -1 && UserAction.deleteUser(this.state.deleteId)} - />} + {this.state.createDialog && ( + + )} + {editId !== -1 && ( + + )} + {deleteId !== -1 && ( + UserAction.deleteUser(this.state.deleteId)} + /> + )}
); } diff --git a/ui/src/pages/dialog/AddApplicationDialog.tsx b/ui/src/pages/dialog/AddApplicationDialog.tsx index 7abddea..bab514e 100644 --- a/ui/src/pages/dialog/AddApplicationDialog.tsx +++ b/ui/src/pages/dialog/AddApplicationDialog.tsx @@ -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 { @@ -29,17 +34,38 @@ export default class AddDialog extends Component { Create an application - An application is allowed to send messages. - - + + An application is allowed to send messages. + + +
-
@@ -54,4 +80,4 @@ export default class AddDialog extends Component { state[propertyName] = event.target.value; this.setState(state); } -} \ No newline at end of file +} diff --git a/ui/src/pages/dialog/AddClientDialog.tsx b/ui/src/pages/dialog/AddClientDialog.tsx index 3703fef..f38a91d 100644 --- a/ui/src/pages/dialog/AddClientDialog.tsx +++ b/ui/src/pages/dialog/AddClientDialog.tsx @@ -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 { +export default class AddDialog extends Component { public state = {name: ''}; public render() { @@ -24,14 +24,28 @@ export default class AddDialog extends Component { Create a client - + - +
-
@@ -46,4 +60,4 @@ export default class AddDialog extends Component { state[propertyName] = event.target.value; this.setState(state); } -} \ No newline at end of file +} diff --git a/ui/src/pages/dialog/AddEditUserDialog.tsx b/ui/src/pages/dialog/AddEditUserDialog.tsx index f5c176c..6fa5f8f 100644 --- a/ui/src/pages/dialog/AddEditUserDialog.tsx +++ b/ui/src/pages/dialog/AddEditUserDialog.tsx @@ -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 { @@ -38,24 +38,57 @@ export default class AddEditDialog extends Component { }; return ( - {isEdit ? 'Edit ' + this.props.name : 'Add a user'} + + {isEdit ? 'Edit ' + this.props.name : 'Add a user'} + - - + + } label="has administrator rights"/> + control={ + + } + label="has administrator rights" + /> - +
-
@@ -81,4 +114,4 @@ export default class AddEditDialog extends Component { state[propertyName] = event.target.checked; this.setState(state); } -} \ No newline at end of file +} diff --git a/ui/src/registerServiceWorker.ts b/ui/src/registerServiceWorker.ts index 4c1d2dc..dfc0e96 100644 --- a/ui/src/registerServiceWorker.ts +++ b/ui/src/registerServiceWorker.ts @@ -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(); }); } diff --git a/ui/src/stores/AppStore.ts b/ui/src/stores/AppStore.ts index f713da8..6bf57af 100644 --- a/ui/src/stores/AppStore.ts +++ b/ui/src/stores/AppStore.ts @@ -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; } diff --git a/ui/src/stores/ClientStore.ts b/ui/src/stores/ClientStore.ts index 4106bb4..0f948b8 100644 --- a/ui/src/stores/ClientStore.ts +++ b/ui/src/stores/ClientStore.ts @@ -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; diff --git a/ui/src/stores/MessageStore.ts b/ui/src/stores/MessageStore.ts index 02ee1b9..f34a894 100644 --- a/ui/src/stores/MessageStore.ts +++ b/ui/src/stores/MessageStore.ts @@ -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])); }); }; } diff --git a/ui/src/stores/Notifications.ts b/ui/src/stores/Notifications.ts index 88955e1..f5e65b3 100644 --- a/ui/src/stores/Notifications.ts +++ b/ui/src/stores/Notifications.ts @@ -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(); + } } -}); +); diff --git a/ui/src/stores/SnackBarStore.ts b/ui/src/stores/SnackBarStore.ts index 4000d6f..9db0a09 100644 --- a/ui/src/stores/SnackBarStore.ts +++ b/ui/src/stores/SnackBarStore.ts @@ -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; } diff --git a/ui/src/stores/dispatcher.ts b/ui/src/stores/dispatcher.ts index da16c2f..67bfd31 100644 --- a/ui/src/stores/dispatcher.ts +++ b/ui/src/stores/dispatcher.ts @@ -1,8 +1,8 @@ import {Dispatcher} from 'flux'; export interface IEvent { - type: string - payload?: any + type: string; + payload?: any; } export default new Dispatcher(); diff --git a/ui/src/typedef/notifyjs.d.ts b/ui/src/typedef/notifyjs.d.ts index 69c26a6..364e612 100644 --- a/ui/src/typedef/notifyjs.d.ts +++ b/ui/src/typedef/notifyjs.d.ts @@ -1,3 +1,3 @@ declare module 'notifyjs' { export default Notify; -} \ No newline at end of file +} diff --git a/ui/src/typedef/react-timeago.d.ts b/ui/src/typedef/react-timeago.d.ts index 14905ba..835759f 100644 --- a/ui/src/typedef/react-timeago.d.ts +++ b/ui/src/typedef/react-timeago.d.ts @@ -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 {} } - diff --git a/ui/src/types.ts b/ui/src/types.ts index 4934452..c8ef55f 100644 --- a/ui/src/types.ts +++ b/ui/src/types.ts @@ -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 -} \ No newline at end of file + messages: IMessage[]; + hasMore: boolean; + nextSince: number; + id?: number; +}