fix: migrate most components to functional components
Co-authored-by: Matthias Fechner <matthias@fechner.net>
This commit is contained in:
parent
734113d187
commit
0ca5156fed
|
|
@ -7,94 +7,72 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import {NumberField} from '../common/NumberField';
|
import {NumberField} from '../common/NumberField';
|
||||||
import React, {Component} from 'react';
|
import React, {useState} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string, description: string, defaultPriority: number) => void;
|
fOnSubmit: (name: string, description: string, defaultPriority: number) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
export const AddApplicationDialog = ({fClose, fOnSubmit}: IProps) => {
|
||||||
name: string;
|
const [name, setName] = useState('');
|
||||||
description: string;
|
const [description, setDescription] = useState('');
|
||||||
defaultPriority: number;
|
const [defaultPriority, setDefaultPriority] = useState(0);
|
||||||
}
|
|
||||||
|
|
||||||
export default class AddDialog extends Component<IProps, IState> {
|
const submitEnabled = name.length !== 0;
|
||||||
public state = {name: '', description: '', defaultPriority: 0};
|
const submitAndClose = async () => {
|
||||||
|
await fOnSubmit(name, description, defaultPriority);
|
||||||
|
fClose();
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
return (
|
||||||
const {fClose, fOnSubmit} = this.props;
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title" id="app-dialog">
|
||||||
const {name, description, defaultPriority} = this.state;
|
<DialogTitle id="form-dialog-title">Create an application</DialogTitle>
|
||||||
const submitEnabled = this.state.name.length !== 0;
|
<DialogContent>
|
||||||
const submitAndClose = () => {
|
<DialogContentText>An application is allowed to send messages.</DialogContentText>
|
||||||
fOnSubmit(name, description, defaultPriority);
|
<TextField
|
||||||
fClose();
|
autoFocus
|
||||||
};
|
margin="dense"
|
||||||
return (
|
className="name"
|
||||||
<Dialog
|
label="Name *"
|
||||||
open={true}
|
type="text"
|
||||||
onClose={fClose}
|
value={name}
|
||||||
aria-labelledby="form-dialog-title"
|
onChange={(e) => setName(e.target.value)}
|
||||||
id="app-dialog">
|
fullWidth
|
||||||
<DialogTitle id="form-dialog-title">Create an application</DialogTitle>
|
/>
|
||||||
<DialogContent>
|
<TextField
|
||||||
<DialogContentText>
|
margin="dense"
|
||||||
An application is allowed to send messages.
|
className="description"
|
||||||
</DialogContentText>
|
label="Short Description"
|
||||||
<TextField
|
value={description}
|
||||||
autoFocus
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
margin="dense"
|
fullWidth
|
||||||
className="name"
|
multiline
|
||||||
label="Name *"
|
/>
|
||||||
type="text"
|
<NumberField
|
||||||
value={name}
|
margin="dense"
|
||||||
onChange={this.handleChange.bind(this, 'name')}
|
className="priority"
|
||||||
fullWidth
|
label="Default Priority"
|
||||||
/>
|
value={defaultPriority}
|
||||||
<TextField
|
onChange={(value) => setDefaultPriority(value)}
|
||||||
margin="dense"
|
fullWidth
|
||||||
className="description"
|
/>
|
||||||
label="Short Description"
|
</DialogContent>
|
||||||
value={description}
|
<DialogActions>
|
||||||
onChange={this.handleChange.bind(this, 'description')}
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
fullWidth
|
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
||||||
multiline
|
<div>
|
||||||
/>
|
<Button
|
||||||
<NumberField
|
className="create"
|
||||||
margin="dense"
|
disabled={!submitEnabled}
|
||||||
className="priority"
|
onClick={submitAndClose}
|
||||||
label="Default Priority"
|
color="primary"
|
||||||
value={defaultPriority}
|
variant="contained">
|
||||||
onChange={(value) => this.setState({defaultPriority: value})}
|
Create
|
||||||
fullWidth
|
</Button>
|
||||||
/>
|
</div>
|
||||||
</DialogContent>
|
</Tooltip>
|
||||||
<DialogActions>
|
</DialogActions>
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
</Dialog>
|
||||||
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
);
|
||||||
<div>
|
};
|
||||||
<Button
|
|
||||||
className="create"
|
|
||||||
disabled={!submitEnabled}
|
|
||||||
onClick={submitAndClose}
|
|
||||||
color="primary"
|
|
||||||
variant="contained">
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChange(
|
|
||||||
propertyName: 'description' | 'name',
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) {
|
|
||||||
const state = this.state;
|
|
||||||
state[propertyName] = event.target.value;
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, {ChangeEvent, useEffect, useRef, useState} from 'react';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
@ -9,144 +10,132 @@ import TableRow from '@mui/material/TableRow';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import CloudUpload from '@mui/icons-material/CloudUpload';
|
import CloudUpload from '@mui/icons-material/CloudUpload';
|
||||||
import React, {ChangeEvent, Component, SFC} from 'react';
|
import Button from '@mui/material/Button';
|
||||||
|
|
||||||
import ConfirmDialog from '../common/ConfirmDialog';
|
import ConfirmDialog from '../common/ConfirmDialog';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import CopyableSecret from '../common/CopyableSecret';
|
import CopyableSecret from '../common/CopyableSecret';
|
||||||
import AddApplicationDialog from './AddApplicationDialog';
|
import {AddApplicationDialog} from './AddApplicationDialog';
|
||||||
import {observer} from 'mobx-react';
|
|
||||||
import {observable} from 'mobx';
|
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import UpdateDialog from './UpdateApplicationDialog';
|
import {UpdateApplicationDialog} from './UpdateApplicationDialog';
|
||||||
import {IApplication} from '../types';
|
import {IApplication} from '../types';
|
||||||
import {LastUsedCell} from '../common/LastUsedCell';
|
import {LastUsedCell} from '../common/LastUsedCell';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
import {observer} from 'mobx-react-lite';
|
||||||
|
|
||||||
@observer
|
const Applications = observer(() => {
|
||||||
class Applications extends Component<Stores<'appStore'>> {
|
const {appStore} = useStores();
|
||||||
@observable
|
const apps = appStore.getItems();
|
||||||
private deleteId: number | false = false;
|
const [toDeleteApp, setToDeleteApp] = useState<IApplication>();
|
||||||
@observable
|
const [toUpdateApp, setToUpdateApp] = useState<IApplication>();
|
||||||
private updateId: number | false = false;
|
const [createDialog, setCreateDialog] = useState<boolean>(false);
|
||||||
@observable
|
|
||||||
private createDialog = false;
|
|
||||||
|
|
||||||
private uploadId = -1;
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
private upload: HTMLInputElement | null = null;
|
const uploadId = useRef(-1);
|
||||||
|
|
||||||
public componentDidMount = () => this.props.appStore.refresh();
|
useEffect(() => void appStore.refresh(), []);
|
||||||
|
|
||||||
public render() {
|
const handleImageUploadClick = (id: number) => {
|
||||||
const {
|
uploadId.current = id;
|
||||||
createDialog,
|
if (fileInputRef.current) {
|
||||||
deleteId,
|
fileInputRef.current.click();
|
||||||
updateId,
|
|
||||||
props: {appStore},
|
|
||||||
} = this;
|
|
||||||
const apps = appStore.getItems();
|
|
||||||
return (
|
|
||||||
<DefaultPage
|
|
||||||
title="Applications"
|
|
||||||
rightControl={
|
|
||||||
<Button
|
|
||||||
id="create-app"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => (this.createDialog = true)}>
|
|
||||||
Create Application
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
maxWidth={1000}>
|
|
||||||
<Grid size={{xs: 12}}>
|
|
||||||
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
|
||||||
<Table id="app-table">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell padding="checkbox" style={{width: 80}} />
|
|
||||||
<TableCell>Name</TableCell>
|
|
||||||
<TableCell>Token</TableCell>
|
|
||||||
<TableCell>Description</TableCell>
|
|
||||||
<TableCell>Priority</TableCell>
|
|
||||||
<TableCell>Last Used</TableCell>
|
|
||||||
<TableCell />
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{apps.map((app: IApplication) => (
|
|
||||||
<Row
|
|
||||||
key={app.id}
|
|
||||||
description={app.description}
|
|
||||||
defaultPriority={app.defaultPriority}
|
|
||||||
image={app.image}
|
|
||||||
name={app.name}
|
|
||||||
value={app.token}
|
|
||||||
lastUsed={app.lastUsed}
|
|
||||||
fUpload={() => this.uploadImage(app.id)}
|
|
||||||
fDelete={() => (this.deleteId = app.id)}
|
|
||||||
fEdit={() => (this.updateId = app.id)}
|
|
||||||
noDelete={app.internal}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<input
|
|
||||||
ref={(upload) => (this.upload = upload)}
|
|
||||||
type="file"
|
|
||||||
style={{display: 'none'}}
|
|
||||||
onChange={this.onUploadImage}
|
|
||||||
/>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
{createDialog && (
|
|
||||||
<AddApplicationDialog
|
|
||||||
fClose={() => (this.createDialog = false)}
|
|
||||||
fOnSubmit={appStore.create}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{updateId !== false && (
|
|
||||||
<UpdateDialog
|
|
||||||
fClose={() => (this.updateId = false)}
|
|
||||||
fOnSubmit={(name, description, defaultPriority) =>
|
|
||||||
appStore.update(updateId, name, description, defaultPriority)
|
|
||||||
}
|
|
||||||
initialDescription={appStore.getByID(updateId).description}
|
|
||||||
initialName={appStore.getByID(updateId).name}
|
|
||||||
initialDefaultPriority={appStore.getByID(updateId).defaultPriority}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{deleteId !== false && (
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Confirm Delete"
|
|
||||||
text={'Delete ' + appStore.getByID(deleteId).name + '?'}
|
|
||||||
fClose={() => (this.deleteId = false)}
|
|
||||||
fOnSubmit={() => appStore.remove(deleteId)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DefaultPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private uploadImage = (id: number) => {
|
|
||||||
this.uploadId = id;
|
|
||||||
if (this.upload) {
|
|
||||||
this.upload.click();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUploadImage = (e: ChangeEvent<HTMLInputElement>) => {
|
const onUploadImage = (e: ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = e.target.files?.[0];
|
const file = e.target.files?.[0];
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (['image/png', 'image/jpeg', 'image/gif'].indexOf(file.type) !== -1) {
|
if (['image/png', 'image/jpeg', 'image/gif'].indexOf(file.type) !== -1) {
|
||||||
this.props.appStore.uploadImage(this.uploadId, file);
|
appStore.uploadImage(uploadId.current, file);
|
||||||
} else {
|
} else {
|
||||||
alert('Uploaded file must be of type png, jpeg or gif.');
|
alert('Uploaded file must be of type png, jpeg or gif.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
return (
|
||||||
|
<DefaultPage
|
||||||
|
title="Applications"
|
||||||
|
rightControl={
|
||||||
|
<Button
|
||||||
|
id="create-app"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setCreateDialog(true)}>
|
||||||
|
Create Application
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
maxWidth={1000}>
|
||||||
|
<Grid size={12}>
|
||||||
|
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
||||||
|
<Table id="app-table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell padding="checkbox" style={{width: 80}} />
|
||||||
|
<TableCell>Name</TableCell>
|
||||||
|
<TableCell>Token</TableCell>
|
||||||
|
<TableCell>Description</TableCell>
|
||||||
|
<TableCell>Priority</TableCell>
|
||||||
|
<TableCell>Last Used</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
<TableCell />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{apps.map((app: IApplication) => (
|
||||||
|
<Row
|
||||||
|
key={app.id}
|
||||||
|
description={app.description}
|
||||||
|
defaultPriority={app.defaultPriority}
|
||||||
|
image={app.image}
|
||||||
|
name={app.name}
|
||||||
|
value={app.token}
|
||||||
|
lastUsed={app.lastUsed}
|
||||||
|
fUpload={() => handleImageUploadClick(app.id)}
|
||||||
|
fDelete={() => setToDeleteApp(app)}
|
||||||
|
fEdit={() => setToUpdateApp(app)}
|
||||||
|
noDelete={app.internal}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
<input
|
||||||
|
ref={fileInputRef}
|
||||||
|
type="file"
|
||||||
|
style={{display: 'none'}}
|
||||||
|
onChange={onUploadImage}
|
||||||
|
/>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
{createDialog && (
|
||||||
|
<AddApplicationDialog
|
||||||
|
fClose={() => setCreateDialog(false)}
|
||||||
|
fOnSubmit={appStore.create}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{toUpdateApp != null && (
|
||||||
|
<UpdateApplicationDialog
|
||||||
|
fClose={() => setToUpdateApp(undefined)}
|
||||||
|
fOnSubmit={(name, description, defaultPriority) =>
|
||||||
|
appStore.update(toUpdateApp.id, name, description, defaultPriority)
|
||||||
|
}
|
||||||
|
initialDescription={toUpdateApp?.description}
|
||||||
|
initialName={toUpdateApp?.name}
|
||||||
|
initialDefaultPriority={toUpdateApp?.defaultPriority}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{toDeleteApp != null && (
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Confirm Delete"
|
||||||
|
text={'Delete ' + toDeleteApp.name + '?'}
|
||||||
|
fClose={() => setToDeleteApp(undefined)}
|
||||||
|
fOnSubmit={() => appStore.remove(toDeleteApp.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DefaultPage>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -161,24 +150,24 @@ interface IRowProps {
|
||||||
fEdit: VoidFunction;
|
fEdit: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: SFC<IRowProps> = observer(
|
const Row = ({
|
||||||
({
|
name,
|
||||||
name,
|
value,
|
||||||
value,
|
noDelete,
|
||||||
noDelete,
|
description,
|
||||||
description,
|
defaultPriority,
|
||||||
defaultPriority,
|
lastUsed,
|
||||||
lastUsed,
|
fDelete,
|
||||||
fDelete,
|
fUpload,
|
||||||
fUpload,
|
image,
|
||||||
image,
|
fEdit,
|
||||||
fEdit,
|
}: IRowProps) => {
|
||||||
}) => (
|
return (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell padding="normal">
|
<TableCell padding="normal">
|
||||||
<div style={{display: 'flex'}}>
|
<div style={{display: 'flex'}}>
|
||||||
<img src={config.get('url') + image} alt="app logo" width="40" height="40" />
|
<img src={config.get('url') + image} alt="app logo" width="40" height="40" />
|
||||||
<IconButton onClick={fUpload} style={{height: 40}} size="large">
|
<IconButton onClick={fUpload} style={{height: 40}}>
|
||||||
<CloudUpload />
|
<CloudUpload />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -193,17 +182,17 @@ const Row: SFC<IRowProps> = observer(
|
||||||
<LastUsedCell lastUsed={lastUsed} />
|
<LastUsedCell lastUsed={lastUsed} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" padding="none">
|
<TableCell align="right" padding="none">
|
||||||
<IconButton onClick={fEdit} className="edit" size="large">
|
<IconButton onClick={fEdit} className="edit">
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" padding="none">
|
<TableCell align="right" padding="none">
|
||||||
<IconButton onClick={fDelete} className="delete" disabled={noDelete} size="large">
|
<IconButton onClick={fDelete} className="delete" disabled={noDelete}>
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
)
|
);
|
||||||
);
|
};
|
||||||
|
|
||||||
export default inject('appStore')(Applications);
|
export default Applications;
|
||||||
|
|
|
||||||
|
|
@ -7,106 +7,81 @@ import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import {NumberField} from '../common/NumberField';
|
import {NumberField} from '../common/NumberField';
|
||||||
import React, {Component} from 'react';
|
import React, {useState} from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string, description: string, defaultPriority: number) => void;
|
fOnSubmit: (name: string, description: string, defaultPriority: number) => Promise<void>;
|
||||||
initialName: string;
|
initialName: string;
|
||||||
initialDescription: string;
|
initialDescription: string;
|
||||||
initialDefaultPriority: number;
|
initialDefaultPriority: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
export const UpdateApplicationDialog = ({
|
||||||
name: string;
|
initialName,
|
||||||
description: string;
|
initialDescription,
|
||||||
defaultPriority: number;
|
initialDefaultPriority,
|
||||||
}
|
fClose,
|
||||||
|
fOnSubmit,
|
||||||
|
}: IProps) => {
|
||||||
|
const [name, setName] = useState(initialName);
|
||||||
|
const [description, setDescription] = useState(initialDescription);
|
||||||
|
const [defaultPriority, setDefaultPriority] = useState(initialDefaultPriority);
|
||||||
|
|
||||||
export default class UpdateDialog extends Component<IProps, IState> {
|
const submitEnabled = name.length !== 0;
|
||||||
public state = {name: '', description: '', defaultPriority: 0};
|
const submitAndClose = async () => {
|
||||||
|
await fOnSubmit(name, description, defaultPriority);
|
||||||
|
fClose();
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props: IProps) {
|
return (
|
||||||
super(props);
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title" id="app-dialog">
|
||||||
this.state = {
|
<DialogTitle id="form-dialog-title">Update an application</DialogTitle>
|
||||||
name: props.initialName,
|
<DialogContent>
|
||||||
description: props.initialDescription,
|
<DialogContentText>An application is allowed to send messages.</DialogContentText>
|
||||||
defaultPriority: props.initialDefaultPriority,
|
<TextField
|
||||||
};
|
autoFocus
|
||||||
}
|
margin="dense"
|
||||||
|
className="name"
|
||||||
public render() {
|
label="Name *"
|
||||||
const {fClose, fOnSubmit} = this.props;
|
type="text"
|
||||||
const {name, description, defaultPriority} = this.state;
|
value={name}
|
||||||
const submitEnabled = this.state.name.length !== 0;
|
onChange={(e) => setName(e.target.value)}
|
||||||
const submitAndClose = () => {
|
fullWidth
|
||||||
fOnSubmit(name, description, defaultPriority);
|
/>
|
||||||
fClose();
|
<TextField
|
||||||
};
|
margin="dense"
|
||||||
return (
|
className="description"
|
||||||
<Dialog
|
label="Short Description"
|
||||||
open={true}
|
value={description}
|
||||||
onClose={fClose}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
aria-labelledby="form-dialog-title"
|
fullWidth
|
||||||
id="app-dialog">
|
multiline
|
||||||
<DialogTitle id="form-dialog-title">Update an application</DialogTitle>
|
/>
|
||||||
<DialogContent>
|
<NumberField
|
||||||
<DialogContentText>
|
margin="dense"
|
||||||
An application is allowed to send messages.
|
className="priority"
|
||||||
</DialogContentText>
|
label="Default Priority"
|
||||||
<TextField
|
value={defaultPriority}
|
||||||
autoFocus
|
onChange={(e) => setDefaultPriority(e)}
|
||||||
margin="dense"
|
fullWidth
|
||||||
className="name"
|
/>
|
||||||
label="Name *"
|
</DialogContent>
|
||||||
type="text"
|
<DialogActions>
|
||||||
value={name}
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
onChange={this.handleChange.bind(this, 'name')}
|
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
||||||
fullWidth
|
<div>
|
||||||
/>
|
<Button
|
||||||
<TextField
|
className="update"
|
||||||
margin="dense"
|
disabled={!submitEnabled}
|
||||||
className="description"
|
onClick={submitAndClose}
|
||||||
label="Short Description"
|
color="primary"
|
||||||
value={description}
|
variant="contained">
|
||||||
onChange={this.handleChange.bind(this, 'description')}
|
Update
|
||||||
fullWidth
|
</Button>
|
||||||
multiline
|
</div>
|
||||||
/>
|
</Tooltip>
|
||||||
<NumberField
|
</DialogActions>
|
||||||
margin="dense"
|
</Dialog>
|
||||||
className="priority"
|
);
|
||||||
label="Default Priority"
|
};
|
||||||
value={defaultPriority}
|
|
||||||
onChange={(value) => this.setState({defaultPriority: value})}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
|
||||||
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="update"
|
|
||||||
disabled={!submitEnabled}
|
|
||||||
onClick={submitAndClose}
|
|
||||||
color="primary"
|
|
||||||
variant="contained">
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChange(
|
|
||||||
propertyName: 'name' | 'description',
|
|
||||||
event: React.ChangeEvent<HTMLInputElement>
|
|
||||||
) {
|
|
||||||
const state = this.state;
|
|
||||||
state[propertyName] = event.target.value;
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
|
@ -5,67 +6,53 @@ import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, {Component} from 'react';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string) => void;
|
fOnSubmit: (name: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class AddDialog extends Component<IProps, {name: string}> {
|
const AddClientDialog = ({fClose, fOnSubmit}: IProps) => {
|
||||||
public state = {name: ''};
|
const [name, setName] = useState('');
|
||||||
|
|
||||||
public render() {
|
const submitEnabled = name.length !== 0;
|
||||||
const {fClose, fOnSubmit} = this.props;
|
const submitAndClose = async () => {
|
||||||
const {name} = this.state;
|
await fOnSubmit(name);
|
||||||
const submitEnabled = this.state.name.length !== 0;
|
fClose();
|
||||||
const submitAndClose = () => {
|
};
|
||||||
fOnSubmit(name);
|
|
||||||
fClose();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={true}
|
|
||||||
onClose={fClose}
|
|
||||||
aria-labelledby="form-dialog-title"
|
|
||||||
id="client-dialog">
|
|
||||||
<DialogTitle id="form-dialog-title">Create a client</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
className="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'}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="create"
|
|
||||||
disabled={!submitEnabled}
|
|
||||||
onClick={submitAndClose}
|
|
||||||
color="primary"
|
|
||||||
variant="contained">
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChange(propertyName: 'name', event: React.ChangeEvent<HTMLInputElement>) {
|
return (
|
||||||
const state = this.state;
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title" id="client-dialog">
|
||||||
state[propertyName] = event.target.value;
|
<DialogTitle id="form-dialog-title">Create a client</DialogTitle>
|
||||||
this.setState(state);
|
<DialogContent>
|
||||||
}
|
<TextField
|
||||||
}
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
className="name"
|
||||||
|
label="Name *"
|
||||||
|
type="email"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
|
<Tooltip placement={'bottom-start'} title={submitEnabled ? '' : 'name is required'}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="create"
|
||||||
|
disabled={!submitEnabled}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="contained">
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddClientDialog;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, {useEffect, useState} from 'react';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
@ -8,103 +9,89 @@ import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import React, {Component, SFC} from 'react';
|
import Button from '@mui/material/Button';
|
||||||
import ConfirmDialog from '../common/ConfirmDialog';
|
import ConfirmDialog from '../common/ConfirmDialog';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
import Button from '@mui/material/Button';
|
|
||||||
import AddClientDialog from './AddClientDialog';
|
import AddClientDialog from './AddClientDialog';
|
||||||
import UpdateDialog from './UpdateClientDialog';
|
import UpdateClientDialog from './UpdateClientDialog';
|
||||||
import {observer} from 'mobx-react';
|
|
||||||
import {observable} from 'mobx';
|
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import {IClient} from '../types';
|
import {IClient} from '../types';
|
||||||
import CopyableSecret from '../common/CopyableSecret';
|
import CopyableSecret from '../common/CopyableSecret';
|
||||||
import {LastUsedCell} from '../common/LastUsedCell';
|
import {LastUsedCell} from '../common/LastUsedCell';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
@observer
|
const Clients = observer(() => {
|
||||||
class Clients extends Component<Stores<'clientStore'>> {
|
const {clientStore} = useStores();
|
||||||
@observable
|
const [toDeleteClient, setToDeleteClient] = useState<IClient>();
|
||||||
private showDialog = false;
|
const [toUpdateClient, setToUpdateClient] = useState<IClient>();
|
||||||
@observable
|
const [createDialog, setCreateDialog] = useState<boolean>(false);
|
||||||
private deleteId: false | number = false;
|
const clients = clientStore.getItems();
|
||||||
@observable
|
|
||||||
private updateId: false | number = false;
|
|
||||||
|
|
||||||
public componentDidMount = () => this.props.clientStore.refresh();
|
useEffect(() => void clientStore.refresh(), []);
|
||||||
|
|
||||||
public render() {
|
return (
|
||||||
const {
|
<DefaultPage
|
||||||
deleteId,
|
title="Clients"
|
||||||
updateId,
|
rightControl={
|
||||||
showDialog,
|
<Button
|
||||||
props: {clientStore},
|
id="create-client"
|
||||||
} = this;
|
variant="contained"
|
||||||
const clients = clientStore.getItems();
|
color="primary"
|
||||||
|
onClick={() => setCreateDialog(true)}>
|
||||||
return (
|
Create Client
|
||||||
<DefaultPage
|
</Button>
|
||||||
title="Clients"
|
}>
|
||||||
rightControl={
|
<Grid size={12}>
|
||||||
<Button
|
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
||||||
id="create-client"
|
<Table id="client-table">
|
||||||
variant="contained"
|
<TableHead>
|
||||||
color="primary"
|
<TableRow style={{textAlign: 'center'}}>
|
||||||
onClick={() => (this.showDialog = true)}>
|
<TableCell>Name</TableCell>
|
||||||
Create Client
|
<TableCell style={{width: 200}}>Token</TableCell>
|
||||||
</Button>
|
<TableCell>Last Used</TableCell>
|
||||||
}>
|
<TableCell />
|
||||||
<Grid size={{xs: 12}}>
|
<TableCell />
|
||||||
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
</TableRow>
|
||||||
<Table id="client-table">
|
</TableHead>
|
||||||
<TableHead>
|
<TableBody>
|
||||||
<TableRow style={{textAlign: 'center'}}>
|
{clients.map((client: IClient) => (
|
||||||
<TableCell>Name</TableCell>
|
<Row
|
||||||
<TableCell style={{width: 200}}>Token</TableCell>
|
key={client.id}
|
||||||
<TableCell>Last Used</TableCell>
|
name={client.name}
|
||||||
<TableCell />
|
value={client.token}
|
||||||
<TableCell />
|
lastUsed={client.lastUsed}
|
||||||
</TableRow>
|
fEdit={() => setToUpdateClient(client)}
|
||||||
</TableHead>
|
fDelete={() => setToDeleteClient(client)}
|
||||||
<TableBody>
|
/>
|
||||||
{clients.map((client: IClient) => (
|
))}
|
||||||
<Row
|
</TableBody>
|
||||||
key={client.id}
|
</Table>
|
||||||
name={client.name}
|
</Paper>
|
||||||
value={client.token}
|
</Grid>
|
||||||
lastUsed={client.lastUsed}
|
{createDialog && (
|
||||||
fEdit={() => (this.updateId = client.id)}
|
<AddClientDialog
|
||||||
fDelete={() => (this.deleteId = client.id)}
|
fClose={() => setCreateDialog(false)}
|
||||||
/>
|
fOnSubmit={clientStore.create}
|
||||||
))}
|
/>
|
||||||
</TableBody>
|
)}
|
||||||
</Table>
|
{toUpdateClient != null && (
|
||||||
</Paper>
|
<UpdateClientDialog
|
||||||
</Grid>
|
fClose={() => setToUpdateClient(undefined)}
|
||||||
{showDialog && (
|
fOnSubmit={(name) => clientStore.update(toUpdateClient.id, name)}
|
||||||
<AddClientDialog
|
initialName={toUpdateClient.name}
|
||||||
fClose={() => (this.showDialog = false)}
|
/>
|
||||||
fOnSubmit={clientStore.create}
|
)}
|
||||||
/>
|
{toDeleteClient != null && (
|
||||||
)}
|
<ConfirmDialog
|
||||||
{updateId !== false && (
|
title="Confirm Delete"
|
||||||
<UpdateDialog
|
text={'Delete ' + toDeleteClient.name + '?'}
|
||||||
fClose={() => (this.updateId = false)}
|
fClose={() => setToDeleteClient(undefined)}
|
||||||
fOnSubmit={(name) => clientStore.update(updateId, name)}
|
fOnSubmit={() => clientStore.remove(toDeleteClient.id)}
|
||||||
initialName={clientStore.getByID(updateId).name}
|
/>
|
||||||
/>
|
)}
|
||||||
)}
|
</DefaultPage>
|
||||||
{deleteId !== false && (
|
);
|
||||||
<ConfirmDialog
|
});
|
||||||
title="Confirm Delete"
|
|
||||||
text={'Delete ' + clientStore.getByID(deleteId).name + '?'}
|
|
||||||
fClose={() => (this.deleteId = false)}
|
|
||||||
fOnSubmit={() => clientStore.remove(deleteId)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DefaultPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -114,7 +101,7 @@ interface IRowProps {
|
||||||
fDelete: VoidFunction;
|
fDelete: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row: SFC<IRowProps> = ({name, value, lastUsed, fEdit, fDelete}) => (
|
const Row = ({name, value, lastUsed, fEdit, fDelete}: IRowProps) => (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{name}</TableCell>
|
<TableCell>{name}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
|
|
@ -127,16 +114,16 @@ const Row: SFC<IRowProps> = ({name, value, lastUsed, fEdit, fDelete}) => (
|
||||||
<LastUsedCell lastUsed={lastUsed} />
|
<LastUsedCell lastUsed={lastUsed} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" padding="none">
|
<TableCell align="right" padding="none">
|
||||||
<IconButton onClick={fEdit} className="edit" size="large">
|
<IconButton onClick={fEdit} className="edit">
|
||||||
<Edit />
|
<Edit />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell align="right" padding="none">
|
<TableCell align="right" padding="none">
|
||||||
<IconButton onClick={fDelete} className="delete" size="large">
|
<IconButton onClick={fDelete} className="delete">
|
||||||
<Delete />
|
<Delete />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default inject('clientStore')(Clients);
|
export default Clients;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
|
@ -6,81 +7,58 @@ import DialogContentText from '@mui/material/DialogContentText';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, {Component} from 'react';
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string) => void;
|
fOnSubmit: (name: string) => Promise<void>;
|
||||||
initialName: string;
|
initialName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
const UpdateClientDialog = ({fClose, fOnSubmit, initialName = ''}: IProps) => {
|
||||||
name: string;
|
const [name, setName] = useState(initialName);
|
||||||
}
|
|
||||||
|
|
||||||
export default class UpdateDialog extends Component<IProps, IState> {
|
const submitEnabled = name.length !== 0;
|
||||||
public state = {name: ''};
|
const submitAndClose = async () => {
|
||||||
|
await fOnSubmit(name);
|
||||||
|
fClose();
|
||||||
|
};
|
||||||
|
|
||||||
constructor(props: IProps) {
|
return (
|
||||||
super(props);
|
<Dialog open={true} onClose={fClose} aria-labelledby="form-dialog-title" id="client-dialog">
|
||||||
this.state = {
|
<DialogTitle id="form-dialog-title">Update a Client</DialogTitle>
|
||||||
name: props.initialName,
|
<DialogContent>
|
||||||
};
|
<DialogContentText>
|
||||||
}
|
A client manages messages, clients, applications and users (with admin
|
||||||
|
permissions).
|
||||||
|
</DialogContentText>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
className="name"
|
||||||
|
label="Name *"
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
|
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="update"
|
||||||
|
disabled={!submitEnabled}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="contained">
|
||||||
|
Update
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
public render() {
|
export default UpdateClientDialog;
|
||||||
const {fClose, fOnSubmit} = this.props;
|
|
||||||
const {name} = this.state;
|
|
||||||
const submitEnabled = this.state.name.length !== 0;
|
|
||||||
const submitAndClose = () => {
|
|
||||||
fOnSubmit(name);
|
|
||||||
fClose();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={true}
|
|
||||||
onClose={fClose}
|
|
||||||
aria-labelledby="form-dialog-title"
|
|
||||||
id="client-dialog">
|
|
||||||
<DialogTitle id="form-dialog-title">Update a Client</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText>
|
|
||||||
A client manages messages, clients, applications and users (with admin
|
|
||||||
permissions).
|
|
||||||
</DialogContentText>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
className="name"
|
|
||||||
label="Name *"
|
|
||||||
type="text"
|
|
||||||
value={name}
|
|
||||||
onChange={this.handleChange.bind(this, 'name')}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
|
||||||
<Tooltip title={submitEnabled ? '' : 'name is required'}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="update"
|
|
||||||
disabled={!submitEnabled}
|
|
||||||
onClick={submitAndClose}
|
|
||||||
color="primary"
|
|
||||||
variant="contained">
|
|
||||||
Update
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChange(propertyName: 'name', event: React.ChangeEvent<HTMLInputElement>) {
|
|
||||||
const state = this.state;
|
|
||||||
state[propertyName] = event.target.value;
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,19 @@
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
import {withStyles} from 'tss-react/mui';
|
import {makeStyles} from 'tss-react/mui';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
const styles = () =>
|
const useStyles = makeStyles()(() => ({
|
||||||
({
|
paper: {
|
||||||
paper: {
|
padding: 16,
|
||||||
padding: 16,
|
},
|
||||||
},
|
}));
|
||||||
} as const);
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container: React.FC<IProps> = ({children, style, ...props}) => {
|
const Container: React.FC<IProps> = ({children, style, ...props}) => {
|
||||||
const classes = withStyles.getClasses(props);
|
const {classes} = useStyles();
|
||||||
return (
|
return (
|
||||||
<Paper elevation={6} className={classes.paper} style={style}>
|
<Paper elevation={6} className={classes.paper} style={style}>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -23,4 +21,4 @@ const Container: React.FC<IProps> = ({children, style, ...props}) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withStyles(Container, styles);
|
export default Container;
|
||||||
|
|
|
||||||
|
|
@ -3,43 +3,20 @@ import Typography from '@mui/material/Typography';
|
||||||
import Visibility from '@mui/icons-material/Visibility';
|
import Visibility from '@mui/icons-material/Visibility';
|
||||||
import Copy from '@mui/icons-material/FileCopyOutlined';
|
import Copy from '@mui/icons-material/FileCopyOutlined';
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||||
import React, {Component, CSSProperties} from 'react';
|
import React, {CSSProperties} from 'react';
|
||||||
import {Stores, inject} from '../inject';
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
value: string;
|
value: string;
|
||||||
style?: CSSProperties;
|
style?: CSSProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
const CopyableSecret = ({value, style}: IProps) => {
|
||||||
visible: boolean;
|
const [visible, setVisible] = React.useState(false);
|
||||||
}
|
const text = visible ? value : '•••••••••••••••';
|
||||||
|
const {snackManager} = useStores();
|
||||||
class CopyableSecret extends Component<IProps & Stores<'snackManager'>, IState> {
|
const toggleVisibility = () => setVisible((b) => !b);
|
||||||
public state = {visible: false};
|
const copyToClipboard = async () => {
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {value, style} = this.props;
|
|
||||||
const text = this.state.visible ? value : '•••••••••••••••';
|
|
||||||
return (
|
|
||||||
<div style={style}>
|
|
||||||
<IconButton onClick={this.copyToClipboard} title="Copy to clipboard" size="large">
|
|
||||||
<Copy />
|
|
||||||
</IconButton>
|
|
||||||
<IconButton
|
|
||||||
onClick={this.toggleVisibility}
|
|
||||||
className="toggle-visibility"
|
|
||||||
size="large">
|
|
||||||
{this.state.visible ? <VisibilityOff /> : <Visibility />}
|
|
||||||
</IconButton>
|
|
||||||
<Typography style={{fontFamily: 'monospace', fontSize: 16}}>{text}</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private toggleVisibility = () => this.setState({visible: !this.state.visible});
|
|
||||||
private copyToClipboard = async () => {
|
|
||||||
const {snackManager, value} = this.props;
|
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(value);
|
await navigator.clipboard.writeText(value);
|
||||||
snackManager.snack('Copied to clipboard');
|
snackManager.snack('Copied to clipboard');
|
||||||
|
|
@ -48,6 +25,17 @@ class CopyableSecret extends Component<IProps & Stores<'snackManager'>, IState>
|
||||||
snackManager.snack('Failed to copy to clipboard');
|
snackManager.snack('Failed to copy to clipboard');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
return (
|
||||||
|
<div style={style}>
|
||||||
|
<IconButton onClick={copyToClipboard} title="Copy to clipboard" size="large">
|
||||||
|
<Copy />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={toggleVisibility} className="toggle-visibility" size="large">
|
||||||
|
{visible ? <VisibilityOff /> : <Visibility />}
|
||||||
|
</IconButton>
|
||||||
|
<Typography style={{fontFamily: 'monospace', fontSize: 16}}>{text}</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default inject('snackManager')(CopyableSecret);
|
export default CopyableSecret;
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,37 @@
|
||||||
import Fab from '@mui/material/Fab';
|
import Fab from '@mui/material/Fab';
|
||||||
import KeyboardArrowUp from '@mui/icons-material/KeyboardArrowUp';
|
import KeyboardArrowUp from '@mui/icons-material/KeyboardArrowUp';
|
||||||
import React, {Component} from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
class ScrollUpButton extends Component {
|
const ScrollUpButton = () => {
|
||||||
state = {
|
const [state, setState] = React.useState({display: 'none', opacity: 0});
|
||||||
display: 'none',
|
React.useEffect(() => {
|
||||||
opacity: 0,
|
const scrollHandler = () => {
|
||||||
};
|
const currentScrollPos = window.pageYOffset;
|
||||||
componentDidMount() {
|
const opacity = Math.min(currentScrollPos / 500, 1);
|
||||||
window.addEventListener('scroll', this.scrollHandler);
|
const nextState = {display: currentScrollPos > 0 ? 'inherit' : 'none', opacity};
|
||||||
}
|
if (state.display !== nextState.display || state.opacity !== nextState.opacity) {
|
||||||
|
setState(nextState);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', scrollHandler);
|
||||||
|
return () => window.removeEventListener('scroll', scrollHandler);
|
||||||
|
}, []);
|
||||||
|
|
||||||
componentWillUnmount() {
|
return (
|
||||||
window.removeEventListener('scroll', this.scrollHandler);
|
<Fab
|
||||||
}
|
color="primary"
|
||||||
|
style={{
|
||||||
scrollHandler = () => {
|
position: 'fixed',
|
||||||
const currentScrollPos = window.pageYOffset;
|
bottom: '30px',
|
||||||
const opacity = Math.min(currentScrollPos / 500, 1);
|
right: '30px',
|
||||||
const nextState = {display: currentScrollPos > 0 ? 'inherit' : 'none', opacity};
|
zIndex: 100000,
|
||||||
if (this.state.display !== nextState.display || this.state.opacity !== nextState.opacity) {
|
display: state.display,
|
||||||
this.setState(nextState);
|
opacity: state.opacity,
|
||||||
}
|
}}
|
||||||
};
|
onClick={() => window.scrollTo(0, 0)}>
|
||||||
|
<KeyboardArrowUp />
|
||||||
public render() {
|
</Fab>
|
||||||
return (
|
);
|
||||||
<Fab
|
};
|
||||||
color="primary"
|
|
||||||
style={{
|
|
||||||
position: 'fixed',
|
|
||||||
bottom: '30px',
|
|
||||||
right: '30px',
|
|
||||||
zIndex: 100000,
|
|
||||||
display: this.state.display,
|
|
||||||
opacity: this.state.opacity,
|
|
||||||
}}
|
|
||||||
onClick={this.scrollUp}>
|
|
||||||
<KeyboardArrowUp />
|
|
||||||
</Fab>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private scrollUp = () => window.scrollTo(0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ScrollUpButton;
|
export default ScrollUpButton;
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import React, {useState} from 'react';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Dialog from '@mui/material/Dialog';
|
import Dialog from '@mui/material/Dialog';
|
||||||
import DialogActions from '@mui/material/DialogActions';
|
import DialogActions from '@mui/material/DialogActions';
|
||||||
|
|
@ -5,64 +6,58 @@ import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, {Component} from 'react';
|
|
||||||
import {observable} from 'mobx';
|
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {inject, Stores} from '../inject';
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
const SettingsDialog = observer(({fClose}: IProps) => {
|
||||||
class SettingsDialog extends Component<IProps & Stores<'currentUser'>> {
|
const [pass, setPass] = useState('');
|
||||||
@observable
|
const {currentUser} = useStores();
|
||||||
private pass = '';
|
|
||||||
|
|
||||||
public render() {
|
const submitAndClose = async () => {
|
||||||
const {pass} = this;
|
currentUser.changePassword(pass);
|
||||||
const {fClose, currentUser} = this.props;
|
fClose();
|
||||||
const submitAndClose = () => {
|
};
|
||||||
currentUser.changePassword(pass);
|
|
||||||
fClose();
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={true}
|
|
||||||
onClose={fClose}
|
|
||||||
aria-labelledby="form-dialog-title"
|
|
||||||
id="changepw-dialog">
|
|
||||||
<DialogTitle id="form-dialog-title">Change Password</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<TextField
|
|
||||||
className="newpass"
|
|
||||||
autoFocus
|
|
||||||
margin="dense"
|
|
||||||
type="password"
|
|
||||||
label="New Password *"
|
|
||||||
value={pass}
|
|
||||||
onChange={(e) => (this.pass = e.target.value)}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
|
||||||
<Tooltip title={pass.length !== 0 ? '' : 'Password is required'}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="change"
|
|
||||||
disabled={pass.length === 0}
|
|
||||||
onClick={submitAndClose}
|
|
||||||
color="primary"
|
|
||||||
variant="contained">
|
|
||||||
Change
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default inject('currentUser')(SettingsDialog);
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
onClose={fClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
id="changepw-dialog">
|
||||||
|
<DialogTitle id="form-dialog-title">Change Password</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
className="newpass"
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
type="password"
|
||||||
|
label="New Password *"
|
||||||
|
value={pass}
|
||||||
|
onChange={(e) => setPass(e.target.value)}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
|
<Tooltip title={pass.length !== 0 ? '' : 'Password is required'}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="change"
|
||||||
|
disabled={pass.length === 0}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="contained">
|
||||||
|
Change
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default SettingsDialog;
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {MessagesStore} from './message/MessagesStore';
|
||||||
import {ClientStore} from './client/ClientStore';
|
import {ClientStore} from './client/ClientStore';
|
||||||
import {PluginStore} from './plugin/PluginStore';
|
import {PluginStore} from './plugin/PluginStore';
|
||||||
import {registerReactions} from './reactions';
|
import {registerReactions} from './reactions';
|
||||||
|
import {StoreContext} from './stores';
|
||||||
|
|
||||||
const {port, hostname, protocol, pathname} = window.location;
|
const {port, hostname, protocol, pathname} = window.location;
|
||||||
const slashes = protocol.concat('//');
|
const slashes = protocol.concat('//');
|
||||||
|
|
@ -61,9 +62,11 @@ const initStores = (): StoreMapping => {
|
||||||
};
|
};
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<InjectProvider stores={stores}>
|
<StoreContext.Provider value={stores}>
|
||||||
<Layout />
|
<InjectProvider stores={stores}>
|
||||||
</InjectProvider>,
|
<Layout />
|
||||||
|
</InjectProvider>
|
||||||
|
</StoreContext.Provider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
);
|
);
|
||||||
unregister();
|
unregister();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import AppBar from '@mui/material/AppBar';
|
||||||
import Button, {ButtonProps} from '@mui/material/Button';
|
import Button, {ButtonProps} from '@mui/material/Button';
|
||||||
import IconButton from '@mui/material/IconButton';
|
import IconButton from '@mui/material/IconButton';
|
||||||
import {Theme} from '@mui/material/styles';
|
import {Theme} from '@mui/material/styles';
|
||||||
import {withStyles} from 'tss-react/mui';
|
import {makeStyles} from 'tss-react/mui';
|
||||||
import Toolbar from '@mui/material/Toolbar';
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import AccountCircle from '@mui/icons-material/AccountCircle';
|
import AccountCircle from '@mui/icons-material/AccountCircle';
|
||||||
|
|
@ -14,62 +14,59 @@ import GitHubIcon from '@mui/icons-material/GitHub';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
import Apps from '@mui/icons-material/Apps';
|
import Apps from '@mui/icons-material/Apps';
|
||||||
import SupervisorAccount from '@mui/icons-material/SupervisorAccount';
|
import SupervisorAccount from '@mui/icons-material/SupervisorAccount';
|
||||||
import React, {Component, CSSProperties} from 'react';
|
import React, {CSSProperties} from 'react';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import {observer} from 'mobx-react';
|
|
||||||
import {useMediaQuery} from '@mui/material';
|
import {useMediaQuery} from '@mui/material';
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const useStyles = makeStyles()((theme: Theme) => ({
|
||||||
({
|
appBar: {
|
||||||
appBar: {
|
zIndex: theme.zIndex.drawer + 1,
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
[theme.breakpoints.down('sm')]: {
|
||||||
[theme.breakpoints.down('sm')]: {
|
paddingBottom: 10,
|
||||||
paddingBottom: 10,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
toolbar: {
|
},
|
||||||
|
toolbar: {
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
menuButtons: {
|
||||||
|
display: 'flex',
|
||||||
|
[theme.breakpoints.down('md')]: {
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
justifyContent: 'center',
|
||||||
|
[theme.breakpoints.down('sm')]: {
|
||||||
|
flexBasis: '100%',
|
||||||
|
marginTop: 5,
|
||||||
|
order: 1,
|
||||||
|
height: 50,
|
||||||
justifyContent: 'space-between',
|
justifyContent: 'space-between',
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
menuButtons: {
|
|
||||||
display: 'flex',
|
|
||||||
[theme.breakpoints.down('md')]: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
justifyContent: 'center',
|
|
||||||
[theme.breakpoints.down('sm')]: {
|
|
||||||
flexBasis: '100%',
|
|
||||||
marginTop: 5,
|
|
||||||
order: 1,
|
|
||||||
height: 50,
|
|
||||||
justifyContent: 'space-between',
|
|
||||||
alignItems: 'center',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
[theme.breakpoints.up('md')]: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
},
|
},
|
||||||
titleName: {
|
},
|
||||||
paddingRight: 10,
|
title: {
|
||||||
|
[theme.breakpoints.up('md')]: {
|
||||||
|
flex: 1,
|
||||||
},
|
},
|
||||||
link: {
|
display: 'flex',
|
||||||
color: 'inherit',
|
alignItems: 'center',
|
||||||
textDecoration: 'none',
|
},
|
||||||
},
|
titleName: {
|
||||||
} as const);
|
paddingRight: 10,
|
||||||
|
},
|
||||||
|
link: {
|
||||||
|
color: 'inherit',
|
||||||
|
textDecoration: 'none',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
version: string;
|
version: string;
|
||||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
|
||||||
toggleTheme: VoidFunction;
|
toggleTheme: VoidFunction;
|
||||||
showSettings: VoidFunction;
|
showSettings: VoidFunction;
|
||||||
logout: VoidFunction;
|
logout: VoidFunction;
|
||||||
|
|
@ -77,107 +74,123 @@ interface IProps {
|
||||||
setNavOpen: (open: boolean) => void;
|
setNavOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
const Header = ({
|
||||||
class Header extends Component<IProps> {
|
version,
|
||||||
public render() {
|
name,
|
||||||
const {version, name, loggedIn, admin, toggleTheme, logout, style, setNavOpen} = this.props;
|
loggedIn,
|
||||||
|
admin,
|
||||||
|
toggleTheme,
|
||||||
|
logout,
|
||||||
|
style,
|
||||||
|
setNavOpen,
|
||||||
|
showSettings,
|
||||||
|
}: IProps) => {
|
||||||
|
const {classes} = useStyles();
|
||||||
|
|
||||||
const classes = withStyles.getClasses(this.props);
|
return (
|
||||||
|
<AppBar
|
||||||
return (
|
sx={{position: {xs: 'sticky', sm: 'fixed'}}}
|
||||||
<AppBar
|
style={style}
|
||||||
sx={{position: {xs: 'sticky', sm: 'fixed'}}}
|
className={classes.appBar}>
|
||||||
style={style}
|
<Toolbar className={classes.toolbar}>
|
||||||
className={classes.appBar}>
|
<div className={classes.title}>
|
||||||
<Toolbar className={classes.toolbar}>
|
<Link to="/" className={classes.link}>
|
||||||
<div className={classes.title}>
|
<Typography variant="h5" className={classes.titleName} color="inherit">
|
||||||
<Link to="/" className={classes.link}>
|
Gotify
|
||||||
<Typography variant="h5" className={classes.titleName} color="inherit">
|
</Typography>
|
||||||
Gotify
|
|
||||||
</Typography>
|
|
||||||
</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, logout, setNavOpen)}
|
|
||||||
<div>
|
|
||||||
<IconButton onClick={toggleTheme} color="inherit" size="large">
|
|
||||||
<Highlight />
|
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href="https://github.com/gotify/server"
|
|
||||||
className={classes.link}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
<IconButton color="inherit" size="large">
|
|
||||||
<GitHubIcon />
|
|
||||||
</IconButton>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderButtons(
|
|
||||||
name: string,
|
|
||||||
admin: boolean,
|
|
||||||
logout: VoidFunction,
|
|
||||||
setNavOpen: (open: boolean) => void
|
|
||||||
) {
|
|
||||||
const classes = withStyles.getClasses(this.props);
|
|
||||||
const {showSettings} = this.props;
|
|
||||||
return (
|
|
||||||
<div className={classes.menuButtons}>
|
|
||||||
<ResponsiveButton
|
|
||||||
sx={{display: {sm: 'none', xs: 'block'}}}
|
|
||||||
icon={<MenuIcon />}
|
|
||||||
onClick={() => setNavOpen(true)}
|
|
||||||
label="menu"
|
|
||||||
color="inherit"
|
|
||||||
/>
|
|
||||||
{admin && (
|
|
||||||
<Link className={classes.link} to="/users" id="navigate-users">
|
|
||||||
<ResponsiveButton
|
|
||||||
icon={<SupervisorAccount />}
|
|
||||||
label="users"
|
|
||||||
color="inherit"
|
|
||||||
/>
|
|
||||||
</Link>
|
</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 && (
|
||||||
|
<Buttons
|
||||||
|
admin={admin}
|
||||||
|
name={name}
|
||||||
|
logout={logout}
|
||||||
|
setNavOpen={setNavOpen}
|
||||||
|
showSettings={showSettings}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
<Link className={classes.link} to="/applications" id="navigate-apps">
|
<div>
|
||||||
<ResponsiveButton icon={<Chat />} label="apps" color="inherit" />
|
<IconButton onClick={toggleTheme} color="inherit" size="large">
|
||||||
|
<Highlight />
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://github.com/gotify/server"
|
||||||
|
className={classes.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer">
|
||||||
|
<IconButton color="inherit" size="large">
|
||||||
|
<GitHubIcon />
|
||||||
|
</IconButton>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</Toolbar>
|
||||||
|
</AppBar>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Buttons = ({
|
||||||
|
showSettings,
|
||||||
|
name,
|
||||||
|
admin,
|
||||||
|
logout,
|
||||||
|
setNavOpen,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
admin: boolean;
|
||||||
|
logout: VoidFunction;
|
||||||
|
setNavOpen: (open: boolean) => void;
|
||||||
|
showSettings: VoidFunction;
|
||||||
|
}) => {
|
||||||
|
const {classes} = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.menuButtons}>
|
||||||
|
<ResponsiveButton
|
||||||
|
sx={{display: {sm: 'none', xs: 'block'}}}
|
||||||
|
icon={<MenuIcon />}
|
||||||
|
onClick={() => setNavOpen(true)}
|
||||||
|
label="menu"
|
||||||
|
color="inherit"
|
||||||
|
/>
|
||||||
|
{admin && (
|
||||||
|
<Link className={classes.link} to="/users" id="navigate-users">
|
||||||
|
<ResponsiveButton icon={<SupervisorAccount />} label="users" color="inherit" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link className={classes.link} to="/clients" id="navigate-clients">
|
)}
|
||||||
<ResponsiveButton icon={<DevicesOther />} label="clients" color="inherit" />
|
<Link className={classes.link} to="/applications" id="navigate-apps">
|
||||||
</Link>
|
<ResponsiveButton icon={<Chat />} label="apps" color="inherit" />
|
||||||
<Link className={classes.link} to="/plugins" id="navigate-plugins">
|
</Link>
|
||||||
<ResponsiveButton icon={<Apps />} label="plugins" color="inherit" />
|
<Link className={classes.link} to="/clients" id="navigate-clients">
|
||||||
</Link>
|
<ResponsiveButton icon={<DevicesOther />} label="clients" color="inherit" />
|
||||||
<ResponsiveButton
|
</Link>
|
||||||
icon={<AccountCircle />}
|
<Link className={classes.link} to="/plugins" id="navigate-plugins">
|
||||||
label={name}
|
<ResponsiveButton icon={<Apps />} label="plugins" color="inherit" />
|
||||||
onClick={showSettings}
|
</Link>
|
||||||
id="changepw"
|
<ResponsiveButton
|
||||||
color="inherit"
|
icon={<AccountCircle />}
|
||||||
/>
|
label={name}
|
||||||
<ResponsiveButton
|
onClick={showSettings}
|
||||||
icon={<ExitToApp />}
|
id="changepw"
|
||||||
label="Logout"
|
color="inherit"
|
||||||
onClick={logout}
|
/>
|
||||||
id="logout"
|
<ResponsiveButton
|
||||||
color="inherit"
|
icon={<ExitToApp />}
|
||||||
/>
|
label="Logout"
|
||||||
</div>
|
onClick={logout}
|
||||||
);
|
id="logout"
|
||||||
}
|
color="inherit"
|
||||||
}
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const ResponsiveButton: React.FC<{
|
const ResponsiveButton: React.FC<{
|
||||||
color: 'inherit';
|
color: 'inherit';
|
||||||
|
|
@ -202,4 +215,4 @@ const ResponsiveButton: React.FC<{
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withStyles(Header, styles);
|
export default Header;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import {createTheme, ThemeProvider, StyledEngineProvider, Theme} from '@mui/material';
|
import {createTheme, ThemeProvider, StyledEngineProvider, Theme} from '@mui/material';
|
||||||
import {withStyles} from 'tss-react/mui';
|
import {makeStyles} from 'tss-react/mui';
|
||||||
import CssBaseline from '@mui/material/CssBaseline';
|
import CssBaseline from '@mui/material/CssBaseline';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {HashRouter, Redirect, Route, Switch} from 'react-router-dom';
|
import {HashRouter, Redirect, Route, Switch} from 'react-router-dom';
|
||||||
|
|
@ -18,11 +18,10 @@ import Login from '../user/Login';
|
||||||
import Messages from '../message/Messages';
|
import Messages from '../message/Messages';
|
||||||
import Users from '../user/Users';
|
import Users from '../user/Users';
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {observable} from 'mobx';
|
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner';
|
import {ConnectionErrorBanner} from '../common/ConnectionErrorBanner';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
const styles = (theme: Theme) => ({
|
const useStyles = makeStyles()((theme: Theme) => ({
|
||||||
content: {
|
content: {
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
marginTop: 64,
|
marginTop: 64,
|
||||||
|
|
@ -32,7 +31,7 @@ const styles = (theme: Theme) => ({
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
}));
|
||||||
|
|
||||||
const localStorageThemeKey = 'gotify-theme';
|
const localStorageThemeKey = 'gotify-theme';
|
||||||
type ThemeKey = 'dark' | 'light';
|
type ThemeKey = 'dark' | 'light';
|
||||||
|
|
@ -52,127 +51,103 @@ const themeMap: Record<ThemeKey, Theme> = {
|
||||||
const isThemeKey = (value: string | null): value is ThemeKey =>
|
const isThemeKey = (value: string | null): value is ThemeKey =>
|
||||||
value === 'light' || value === 'dark';
|
value === 'light' || value === 'dark';
|
||||||
|
|
||||||
interface LayoutProps {
|
const Layout = observer(() => {
|
||||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
const {
|
||||||
}
|
currentUser: {
|
||||||
|
loggedIn,
|
||||||
|
authenticating,
|
||||||
|
user: {name, admin},
|
||||||
|
logout,
|
||||||
|
tryReconnect,
|
||||||
|
connectionErrorMessage,
|
||||||
|
},
|
||||||
|
} = useStores();
|
||||||
|
const {classes} = useStyles();
|
||||||
|
const [currentTheme, setCurrentTheme] = React.useState<ThemeKey>(() => {
|
||||||
|
const stored = window.localStorage.getItem(localStorageThemeKey);
|
||||||
|
return isThemeKey(stored) ? stored : 'dark';
|
||||||
|
});
|
||||||
|
const theme = themeMap[currentTheme];
|
||||||
|
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
|
||||||
|
const {version} = config.get('version');
|
||||||
|
const [navOpen, setNavOpen] = React.useState(false);
|
||||||
|
const [showSettings, setShowSettings] = React.useState(false);
|
||||||
|
|
||||||
@observer
|
const toggleTheme = () => {
|
||||||
class Layout extends React.Component<LayoutProps & Stores<'currentUser' | 'snackManager'>> {
|
const next = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
@observable
|
setCurrentTheme(next);
|
||||||
private currentTheme: ThemeKey = 'dark';
|
localStorage.setItem(localStorageThemeKey, next);
|
||||||
@observable
|
};
|
||||||
private showSettings = false;
|
|
||||||
@observable
|
|
||||||
private navOpen = false;
|
|
||||||
|
|
||||||
private setNavOpen(open: boolean) {
|
return (
|
||||||
this.navOpen = open;
|
<StyledEngineProvider injectFirst>
|
||||||
}
|
<ThemeProvider theme={theme}>
|
||||||
|
<HashRouter>
|
||||||
public componentDidMount() {
|
<div>
|
||||||
const localStorageTheme = window.localStorage.getItem(localStorageThemeKey);
|
{!connectionErrorMessage ? null : (
|
||||||
if (isThemeKey(localStorageTheme)) {
|
<ConnectionErrorBanner
|
||||||
this.currentTheme = localStorageTheme;
|
height={64}
|
||||||
} else {
|
retry={() => tryReconnect()}
|
||||||
window.localStorage.setItem(localStorageThemeKey, this.currentTheme);
|
message={connectionErrorMessage}
|
||||||
}
|
/>
|
||||||
}
|
)}
|
||||||
|
<div style={{display: 'flex', flexDirection: 'column'}}>
|
||||||
public render() {
|
<CssBaseline />
|
||||||
const {showSettings, currentTheme} = this;
|
<Header
|
||||||
const {
|
admin={admin}
|
||||||
currentUser: {
|
name={name}
|
||||||
loggedIn,
|
style={{top: !connectionErrorMessage ? 0 : 64}}
|
||||||
authenticating,
|
version={version}
|
||||||
user: {name, admin},
|
loggedIn={loggedIn}
|
||||||
logout,
|
toggleTheme={toggleTheme}
|
||||||
tryReconnect,
|
showSettings={() => setShowSettings(true)}
|
||||||
connectionErrorMessage,
|
logout={logout}
|
||||||
},
|
setNavOpen={setNavOpen}
|
||||||
} = this.props;
|
/>
|
||||||
const classes = withStyles.getClasses(this.props);
|
<div style={{display: 'flex'}}>
|
||||||
const theme = themeMap[currentTheme];
|
<Navigation
|
||||||
const loginRoute = () => (loggedIn ? <Redirect to="/" /> : <Login />);
|
|
||||||
const {version} = config.get('version');
|
|
||||||
return (
|
|
||||||
<StyledEngineProvider injectFirst>
|
|
||||||
<ThemeProvider theme={theme}>
|
|
||||||
<HashRouter>
|
|
||||||
<div>
|
|
||||||
{!connectionErrorMessage ? null : (
|
|
||||||
<ConnectionErrorBanner
|
|
||||||
height={64}
|
|
||||||
retry={() => tryReconnect()}
|
|
||||||
message={connectionErrorMessage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div style={{display: 'flex', flexDirection: 'column'}}>
|
|
||||||
<CssBaseline />
|
|
||||||
<Header
|
|
||||||
style={{top: !connectionErrorMessage ? 0 : 64}}
|
|
||||||
admin={admin}
|
|
||||||
name={name}
|
|
||||||
version={version}
|
|
||||||
loggedIn={loggedIn}
|
loggedIn={loggedIn}
|
||||||
toggleTheme={this.toggleTheme.bind(this)}
|
navOpen={navOpen}
|
||||||
showSettings={() => (this.showSettings = true)}
|
setNavOpen={setNavOpen}
|
||||||
logout={logout}
|
|
||||||
setNavOpen={this.setNavOpen.bind(this)}
|
|
||||||
/>
|
/>
|
||||||
<div style={{display: 'flex'}}>
|
<main className={classes.content}>
|
||||||
<Navigation
|
<Switch>
|
||||||
loggedIn={loggedIn}
|
{authenticating ? (
|
||||||
navOpen={this.navOpen}
|
<Route path="/">
|
||||||
setNavOpen={this.setNavOpen.bind(this)}
|
<LoadingSpinner />
|
||||||
/>
|
</Route>
|
||||||
<main className={classes.content}>
|
) : null}
|
||||||
<Switch>
|
<Route exact path="/login" render={loginRoute} />
|
||||||
{authenticating ? (
|
{loggedIn ? null : <Redirect to="/login" />}
|
||||||
<Route path="/">
|
<Route exact path="/" component={Messages} />
|
||||||
<LoadingSpinner />
|
<Route exact path="/messages/:id" component={Messages} />
|
||||||
</Route>
|
<Route
|
||||||
) : null}
|
exact
|
||||||
<Route exact path="/login" render={loginRoute} />
|
path="/applications"
|
||||||
{loggedIn ? null : <Redirect to="/login" />}
|
component={Applications}
|
||||||
<Route exact path="/" component={Messages} />
|
/>
|
||||||
<Route
|
<Route exact path="/clients" component={Clients} />
|
||||||
exact
|
<Route exact path="/users" component={Users} />
|
||||||
path="/messages/:id"
|
<Route exact path="/plugins" component={Plugins} />
|
||||||
component={Messages}
|
<Route
|
||||||
/>
|
exact
|
||||||
<Route
|
path="/plugins/:id"
|
||||||
exact
|
component={PluginDetailView}
|
||||||
path="/applications"
|
/>
|
||||||
component={Applications}
|
</Switch>
|
||||||
/>
|
</main>
|
||||||
<Route exact path="/clients" component={Clients} />
|
|
||||||
<Route exact path="/users" component={Users} />
|
|
||||||
<Route exact path="/plugins" component={Plugins} />
|
|
||||||
<Route
|
|
||||||
exact
|
|
||||||
path="/plugins/:id"
|
|
||||||
component={PluginDetailView}
|
|
||||||
/>
|
|
||||||
</Switch>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
{showSettings && (
|
|
||||||
<SettingsDialog fClose={() => (this.showSettings = false)} />
|
|
||||||
)}
|
|
||||||
<ScrollUpButton />
|
|
||||||
<SnackBarHandler />
|
|
||||||
</div>
|
</div>
|
||||||
|
{showSettings && (
|
||||||
|
<SettingsDialog fClose={() => setShowSettings(false)} />
|
||||||
|
)}
|
||||||
|
<ScrollUpButton />
|
||||||
|
<SnackBarHandler />
|
||||||
</div>
|
</div>
|
||||||
</HashRouter>
|
</div>
|
||||||
</ThemeProvider>
|
</HashRouter>
|
||||||
</StyledEngineProvider>
|
</ThemeProvider>
|
||||||
);
|
</StyledEngineProvider>
|
||||||
}
|
);
|
||||||
|
});
|
||||||
|
|
||||||
private toggleTheme() {
|
export default Layout;
|
||||||
this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark';
|
|
||||||
localStorage.setItem(localStorageThemeKey, this.currentTheme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withStyles(inject('currentUser', 'snackManager')(Layout), styles);
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
import Divider from '@mui/material/Divider';
|
import Divider from '@mui/material/Divider';
|
||||||
import Drawer from '@mui/material/Drawer';
|
import Drawer from '@mui/material/Drawer';
|
||||||
import {Theme} from '@mui/material/styles';
|
import {Theme} from '@mui/material/styles';
|
||||||
import React, {Component} from 'react';
|
import React from 'react';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import {mayAllowPermission, requestPermission} from '../snack/browserNotification';
|
import {mayAllowPermission, requestPermission} from '../snack/browserNotification';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -17,108 +16,100 @@ import {
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import {DrawerProps} from '@mui/material/Drawer/Drawer';
|
import {DrawerProps} from '@mui/material/Drawer/Drawer';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import {withStyles} from 'tss-react/mui';
|
import {makeStyles} from 'tss-react/mui';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const useStyles = makeStyles()((theme: Theme) => ({
|
||||||
({
|
root: {
|
||||||
root: {
|
height: '100%',
|
||||||
height: '100%',
|
},
|
||||||
},
|
drawerPaper: {
|
||||||
drawerPaper: {
|
position: 'relative',
|
||||||
position: 'relative',
|
width: 250,
|
||||||
width: 250,
|
minHeight: '100%',
|
||||||
minHeight: '100%',
|
height: '100vh',
|
||||||
height: '100vh',
|
},
|
||||||
},
|
// eslint-disable-next-line
|
||||||
// eslint-disable-next-line
|
toolbar: theme.mixins.toolbar as any,
|
||||||
toolbar: theme.mixins.toolbar as any,
|
link: {
|
||||||
link: {
|
color: 'inherit',
|
||||||
color: 'inherit',
|
textDecoration: 'none',
|
||||||
textDecoration: 'none',
|
},
|
||||||
},
|
}));
|
||||||
} as const);
|
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
loggedIn: boolean;
|
loggedIn: boolean;
|
||||||
navOpen: boolean;
|
navOpen: boolean;
|
||||||
classes?: Partial<Record<keyof ReturnType<typeof styles>, string>>;
|
|
||||||
setNavOpen: (open: boolean) => void;
|
setNavOpen: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
const Navigation = observer(({loggedIn, navOpen, setNavOpen}: IProps) => {
|
||||||
class Navigation extends Component<
|
const [showRequestNotification, setShowRequestNotification] =
|
||||||
IProps & Stores<'appStore'>,
|
React.useState(mayAllowPermission);
|
||||||
{showRequestNotification: boolean}
|
const {classes} = useStyles();
|
||||||
> {
|
const {appStore} = useStores();
|
||||||
public state = {showRequestNotification: mayAllowPermission()};
|
const apps = appStore.getItems();
|
||||||
|
|
||||||
public render() {
|
const userApps =
|
||||||
const {loggedIn, appStore, navOpen, setNavOpen} = this.props;
|
apps.length === 0
|
||||||
const classes = withStyles.getClasses(this.props);
|
? null
|
||||||
const {showRequestNotification} = this.state;
|
: apps.map((app) => (
|
||||||
const apps = appStore.getItems();
|
<Link
|
||||||
|
onClick={() => setNavOpen(false)}
|
||||||
|
className={`${classes.link} item`}
|
||||||
|
to={'/messages/' + app.id}
|
||||||
|
key={app.id}>
|
||||||
|
<ListItemButton>
|
||||||
|
<ListItemAvatar style={{minWidth: 42}}>
|
||||||
|
<Avatar
|
||||||
|
style={{width: 32, height: 32}}
|
||||||
|
src={app.image}
|
||||||
|
variant="square"
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary={app.name} />
|
||||||
|
</ListItemButton>
|
||||||
|
</Link>
|
||||||
|
));
|
||||||
|
|
||||||
const userApps =
|
const placeholderItems = [
|
||||||
apps.length === 0
|
<ListItemButton disabled key={-1}>
|
||||||
? null
|
<ListItemText primary="Some Server" />
|
||||||
: apps.map((app) => (
|
</ListItemButton>,
|
||||||
<Link
|
<ListItemButton disabled key={-2}>
|
||||||
onClick={() => setNavOpen(false)}
|
<ListItemText primary="A Raspberry PI" />
|
||||||
className={`${classes.link} item`}
|
</ListItemButton>,
|
||||||
to={'/messages/' + app.id}
|
];
|
||||||
key={app.id}>
|
|
||||||
<ListItemButton>
|
|
||||||
<ListItemAvatar style={{minWidth: 42}}>
|
|
||||||
<Avatar
|
|
||||||
style={{width: 32, height: 32}}
|
|
||||||
src={app.image}
|
|
||||||
variant="square"
|
|
||||||
/>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={app.name} />
|
|
||||||
</ListItemButton>
|
|
||||||
</Link>
|
|
||||||
));
|
|
||||||
|
|
||||||
const placeholderItems = [
|
return (
|
||||||
<ListItemButton disabled key={-1}>
|
<ResponsiveDrawer
|
||||||
<ListItemText primary="Some Server" />
|
classes={{root: classes.root, paper: classes.drawerPaper}}
|
||||||
</ListItemButton>,
|
navOpen={navOpen}
|
||||||
<ListItemButton disabled key={-2}>
|
setNavOpen={setNavOpen}
|
||||||
<ListItemText primary="A Raspberry PI" />
|
id="message-navigation">
|
||||||
</ListItemButton>,
|
<div className={classes.toolbar} />
|
||||||
];
|
<Link className={classes.link} to="/" onClick={() => setNavOpen(false)}>
|
||||||
|
<ListItemButton disabled={!loggedIn} className="all">
|
||||||
return (
|
<ListItemText primary="All Messages" />
|
||||||
<ResponsiveDrawer
|
</ListItemButton>
|
||||||
classes={{root: classes.root, paper: classes.drawerPaper}}
|
</Link>
|
||||||
navOpen={navOpen}
|
<Divider />
|
||||||
setNavOpen={setNavOpen}
|
<div>{loggedIn ? userApps : placeholderItems}</div>
|
||||||
id="message-navigation">
|
<Divider />
|
||||||
<div className={classes.toolbar} />
|
<Typography align="center" style={{marginTop: 10}}>
|
||||||
<Link className={classes.link} to="/" onClick={() => setNavOpen(false)}>
|
{showRequestNotification ? (
|
||||||
<ListItemButton disabled={!loggedIn} className="all">
|
<Button
|
||||||
<ListItemText primary="All Messages" />
|
onClick={() => {
|
||||||
</ListItemButton>
|
requestPermission();
|
||||||
</Link>
|
setShowRequestNotification(false);
|
||||||
<Divider />
|
}}>
|
||||||
<div>{loggedIn ? userApps : placeholderItems}</div>
|
Enable Notifications
|
||||||
<Divider />
|
</Button>
|
||||||
<Typography align="center" style={{marginTop: 10}}>
|
) : null}
|
||||||
{showRequestNotification ? (
|
</Typography>
|
||||||
<Button
|
</ResponsiveDrawer>
|
||||||
onClick={() => {
|
);
|
||||||
requestPermission();
|
});
|
||||||
this.setState({showRequestNotification: false});
|
|
||||||
}}>
|
|
||||||
Enable Notifications
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</Typography>
|
|
||||||
</ResponsiveDrawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const ResponsiveDrawer: React.FC<
|
const ResponsiveDrawer: React.FC<
|
||||||
DrawerProps & {navOpen: boolean; setNavOpen: (open: boolean) => void}
|
DrawerProps & {navOpen: boolean; setNavOpen: (open: boolean) => void}
|
||||||
|
|
@ -140,4 +131,4 @@ const ResponsiveDrawer: React.FC<
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export default withStyles(inject('appStore')(Navigation), styles);
|
export default Navigation;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, {Component} from 'react';
|
import React from 'react';
|
||||||
import {RouteComponentProps} from 'react-router';
|
import {useParams} from 'react-router';
|
||||||
import {Markdown} from '../common/Markdown';
|
import {Markdown} from '../common/Markdown';
|
||||||
import {UnControlled as CodeMirror} from 'react-codemirror2';
|
import {UnControlled as CodeMirror} from 'react-codemirror2';
|
||||||
import 'codemirror/lib/codemirror.css';
|
import 'codemirror/lib/codemirror.css';
|
||||||
|
|
@ -14,110 +14,80 @@ import Typography from '@mui/material/Typography';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import Container from '../common/Container';
|
import Container from '../common/Container';
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import {IPlugin} from '../types';
|
import {IPlugin} from '../types';
|
||||||
import LoadingSpinner from '../common/LoadingSpinner';
|
import LoadingSpinner from '../common/LoadingSpinner';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
type IProps = RouteComponentProps<{id: string}>;
|
const PluginDetailView = () => {
|
||||||
|
const {id} = useParams<{id: string}>();
|
||||||
|
const pluginID = parseInt(id as string, 10);
|
||||||
|
const {pluginStore} = useStores();
|
||||||
|
const [currentConfig, setCurrentConfig] = React.useState<string>();
|
||||||
|
const [displayText, setDisplayText] = React.useState<string>();
|
||||||
|
|
||||||
interface IState {
|
const pluginInfo = pluginStore.getByIDOrUndefined(pluginID);
|
||||||
displayText: string | null;
|
|
||||||
currentConfig: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class PluginDetailView extends Component<IProps & Stores<'pluginStore'>, IState> {
|
const refreshFeatures = async () => {
|
||||||
private pluginID: number = parseInt(this.props.match.params.id, 10);
|
await pluginStore.refreshIfMissing(pluginID);
|
||||||
private pluginInfo = () => this.props.pluginStore.getByID(this.pluginID);
|
await Promise.all([refreshConfigurer(), refreshDisplayer()]);
|
||||||
|
|
||||||
public state: IState = {
|
|
||||||
displayText: null,
|
|
||||||
currentConfig: null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public componentWillMount() {
|
React.useEffect(() => void refreshFeatures(), [pluginID]);
|
||||||
this.refreshFeatures();
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillReceiveProps(nextProps: IProps & Stores<'pluginStore'>) {
|
const refreshConfigurer = async () => {
|
||||||
this.pluginID = parseInt(nextProps.match.params.id, 10);
|
if (pluginInfo?.capabilities.indexOf('configurer') !== -1) {
|
||||||
this.refreshFeatures();
|
setCurrentConfig(await pluginStore.requestConfig(pluginID));
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshFeatures() {
|
|
||||||
await this.props.pluginStore.refreshIfMissing(this.pluginID);
|
|
||||||
return await Promise.all([this.refreshConfigurer(), this.refreshDisplayer()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async refreshConfigurer() {
|
|
||||||
const {
|
|
||||||
props: {pluginStore},
|
|
||||||
} = this;
|
|
||||||
if (this.pluginInfo().capabilities.indexOf('configurer') !== -1) {
|
|
||||||
const response = await pluginStore.requestConfig(this.pluginID);
|
|
||||||
this.setState({currentConfig: response});
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshDisplayer = async () => {
|
||||||
|
if (pluginInfo?.capabilities.indexOf('displayer') !== -1) {
|
||||||
|
setDisplayText(await pluginStore.requestDisplay(pluginID));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (pluginInfo == null) {
|
||||||
|
return <LoadingSpinner />;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshDisplayer() {
|
const handleSaveConfig = async (newConfig: string) => {
|
||||||
const {
|
await pluginStore.changeConfig(pluginID, newConfig);
|
||||||
props: {pluginStore},
|
await refreshFeatures();
|
||||||
} = this;
|
};
|
||||||
if (this.pluginInfo().capabilities.indexOf('displayer') !== -1) {
|
|
||||||
const response = await pluginStore.requestDisplay(this.pluginID);
|
|
||||||
this.setState({displayText: response});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
return (
|
||||||
const pluginInfo = this.props.pluginStore.getByIDOrUndefined(this.pluginID);
|
<DefaultPage title={pluginInfo.name} maxWidth={1000}>
|
||||||
if (pluginInfo === undefined) {
|
<PanelWrapper name={'Plugin Info'} icon={Info}>
|
||||||
return <LoadingSpinner />;
|
<PluginInfo pluginInfo={pluginInfo} />
|
||||||
}
|
</PanelWrapper>
|
||||||
return (
|
{pluginInfo.capabilities.indexOf('configurer') !== -1 ? (
|
||||||
<DefaultPage title={pluginInfo.name} maxWidth={1000}>
|
<PanelWrapper
|
||||||
<PanelWrapper name={'Plugin Info'} icon={Info}>
|
name={'Configurer'}
|
||||||
<PluginInfo pluginInfo={pluginInfo} />
|
description={'This is the configuration panel for this plugin.'}
|
||||||
|
icon={Build}
|
||||||
|
refresh={refreshConfigurer}>
|
||||||
|
<ConfigurerPanel
|
||||||
|
pluginInfo={pluginInfo}
|
||||||
|
initialConfig={currentConfig != null ? currentConfig : 'Loading...'}
|
||||||
|
save={handleSaveConfig}
|
||||||
|
/>
|
||||||
</PanelWrapper>
|
</PanelWrapper>
|
||||||
{pluginInfo.capabilities.indexOf('configurer') !== -1 ? (
|
) : null}{' '}
|
||||||
<PanelWrapper
|
{pluginInfo.capabilities.indexOf('displayer') !== -1 ? (
|
||||||
name={'Configurer'}
|
<PanelWrapper
|
||||||
description={'This is the configuration panel for this plugin.'}
|
name={'Displayer'}
|
||||||
icon={Build}
|
description={'This is the information generated by the plugin.'}
|
||||||
refresh={this.refreshConfigurer.bind(this)}>
|
refresh={refreshDisplayer}
|
||||||
<ConfigurerPanel
|
icon={Subject}>
|
||||||
pluginInfo={pluginInfo}
|
<DisplayerPanel
|
||||||
initialConfig={
|
pluginInfo={pluginInfo}
|
||||||
this.state.currentConfig !== null
|
displayText={displayText != null ? displayText : 'Loading...'}
|
||||||
? this.state.currentConfig
|
/>
|
||||||
: 'Loading...'
|
</PanelWrapper>
|
||||||
}
|
) : null}
|
||||||
save={async (newConfig) => {
|
</DefaultPage>
|
||||||
await this.props.pluginStore.changeConfig(this.pluginID, newConfig);
|
);
|
||||||
await this.refreshFeatures();
|
};
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</PanelWrapper>
|
|
||||||
) : null}{' '}
|
|
||||||
{pluginInfo.capabilities.indexOf('displayer') !== -1 ? (
|
|
||||||
<PanelWrapper
|
|
||||||
name={'Displayer'}
|
|
||||||
description={'This is the information generated by the plugin.'}
|
|
||||||
refresh={this.refreshDisplayer.bind(this)}
|
|
||||||
icon={Subject}>
|
|
||||||
<DisplayerPanel
|
|
||||||
pluginInfo={pluginInfo}
|
|
||||||
displayText={
|
|
||||||
this.state.displayText !== null
|
|
||||||
? this.state.displayText
|
|
||||||
: 'Loading...'
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PanelWrapper>
|
|
||||||
) : null}
|
|
||||||
</DefaultPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IPanelWrapperProps {
|
interface IPanelWrapperProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -178,49 +148,43 @@ interface IConfigurerPanelProps {
|
||||||
initialConfig: string;
|
initialConfig: string;
|
||||||
save: (newConfig: string) => Promise<void>;
|
save: (newConfig: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
class ConfigurerPanel extends Component<IConfigurerPanelProps, {unsavedChanges: string | null}> {
|
const ConfigurerPanel = ({initialConfig, save}: IConfigurerPanelProps) => {
|
||||||
public state = {unsavedChanges: null};
|
const [unsavedChanges, setUnsavedChanges] = React.useState<string | null>(null);
|
||||||
|
return (
|
||||||
public render() {
|
<div>
|
||||||
return (
|
<CodeMirror
|
||||||
<div>
|
value={initialConfig}
|
||||||
<CodeMirror
|
options={{
|
||||||
value={this.props.initialConfig}
|
mode: 'yaml',
|
||||||
options={{
|
theme: 'material',
|
||||||
mode: 'yaml',
|
lineNumbers: true,
|
||||||
theme: 'material',
|
}}
|
||||||
lineNumbers: true,
|
onChange={(_, _1, value) => {
|
||||||
}}
|
let newConf: string | null = value;
|
||||||
onChange={(_, _1, value) => {
|
if (value === initialConfig) {
|
||||||
let newConf: string | null = value;
|
newConf = null;
|
||||||
if (value === this.props.initialConfig) {
|
|
||||||
newConf = null;
|
|
||||||
}
|
|
||||||
this.setState({unsavedChanges: newConf});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
fullWidth={true}
|
|
||||||
disabled={
|
|
||||||
this.state.unsavedChanges === null ||
|
|
||||||
this.state.unsavedChanges === this.props.initialConfig
|
|
||||||
}
|
}
|
||||||
className="config-save"
|
setUnsavedChanges(newConf);
|
||||||
onClick={() => {
|
}}
|
||||||
const newConfig = this.state.unsavedChanges;
|
/>
|
||||||
this.props.save(newConfig!).then(() => {
|
<br />
|
||||||
this.setState({unsavedChanges: null});
|
<Button
|
||||||
});
|
variant="contained"
|
||||||
}}>
|
color="primary"
|
||||||
<Typography variant="button">Save</Typography>
|
fullWidth={true}
|
||||||
</Button>
|
disabled={unsavedChanges === null || unsavedChanges === initialConfig}
|
||||||
</div>
|
className="config-save"
|
||||||
);
|
onClick={() => {
|
||||||
}
|
const newConfig = unsavedChanges;
|
||||||
}
|
save(newConfig!).then(() => {
|
||||||
|
setUnsavedChanges(null);
|
||||||
|
});
|
||||||
|
}}>
|
||||||
|
<Typography variant="button">Save</Typography>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IDisplayerPanelProps {
|
interface IDisplayerPanelProps {
|
||||||
pluginInfo: IPlugin;
|
pluginInfo: IPlugin;
|
||||||
|
|
@ -232,58 +196,57 @@ const DisplayerPanel: React.FC<IDisplayerPanelProps> = ({displayText}) => (
|
||||||
</Typography>
|
</Typography>
|
||||||
);
|
);
|
||||||
|
|
||||||
class PluginInfo extends Component<{pluginInfo: IPlugin}> {
|
interface IPluginInfo {
|
||||||
public render() {
|
pluginInfo: IPlugin;
|
||||||
const {
|
|
||||||
props: {
|
|
||||||
pluginInfo: {name, author, modulePath, website, license, capabilities, id, token},
|
|
||||||
},
|
|
||||||
} = this;
|
|
||||||
return (
|
|
||||||
<div style={{wordWrap: 'break-word'}}>
|
|
||||||
{name ? (
|
|
||||||
<Typography variant="body2" className="name">
|
|
||||||
Name: <span>{name}</span>
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
{author ? (
|
|
||||||
<Typography variant="body2" className="author">
|
|
||||||
Author: <span>{author}</span>
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
<Typography variant="body2" className="module-path">
|
|
||||||
Module Path: <span>{modulePath}</span>
|
|
||||||
</Typography>
|
|
||||||
{website ? (
|
|
||||||
<Typography variant="body2" className="website">
|
|
||||||
Website: <span>{website}</span>
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
{license ? (
|
|
||||||
<Typography variant="body2" className="license">
|
|
||||||
License: <span>{license}</span>
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
<Typography variant="body2" className="capabilities">
|
|
||||||
Capabilities: <span>{capabilities.join(', ')}</span>
|
|
||||||
</Typography>
|
|
||||||
{capabilities.indexOf('webhooker') !== -1 ? (
|
|
||||||
<Typography variant="body2">
|
|
||||||
Custom Route Prefix:{' '}
|
|
||||||
{((url) => (
|
|
||||||
<a
|
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="custom-route">
|
|
||||||
{url}
|
|
||||||
</a>
|
|
||||||
))(`${config.get('url')}plugin/${id}/custom/${token}/`)}
|
|
||||||
</Typography>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default inject('pluginStore')(PluginDetailView);
|
const PluginInfo = ({pluginInfo}: IPluginInfo) => {
|
||||||
|
const {name, author, modulePath, website, license, capabilities, id, token} = pluginInfo;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{wordWrap: 'break-word'}}>
|
||||||
|
{name ? (
|
||||||
|
<Typography variant="body2" className="name">
|
||||||
|
Name: <span>{name}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{author ? (
|
||||||
|
<Typography variant="body2" className="author">
|
||||||
|
Author: <span>{author}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Typography variant="body2" className="module-path">
|
||||||
|
Module Path: <span>{modulePath}</span>
|
||||||
|
</Typography>
|
||||||
|
{website ? (
|
||||||
|
<Typography variant="body2" className="website">
|
||||||
|
Website: <span>{website}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
{license ? (
|
||||||
|
<Typography variant="body2" className="license">
|
||||||
|
License: <span>{license}</span>
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
<Typography variant="body2" className="capabilities">
|
||||||
|
Capabilities: <span>{capabilities.join(', ')}</span>
|
||||||
|
</Typography>
|
||||||
|
{capabilities.indexOf('webhooker') !== -1 ? (
|
||||||
|
<Typography variant="body2">
|
||||||
|
Custom Route Prefix:{' '}
|
||||||
|
{((url) => (
|
||||||
|
<a
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="custom-route">
|
||||||
|
{url}
|
||||||
|
</a>
|
||||||
|
))(`${config.get('url')}plugin/${id}/custom/${token}/`)}
|
||||||
|
</Typography>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PluginDetailView;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, {Component, SFC} from 'react';
|
import React, {SFC} from 'react';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import Paper from '@mui/material/Paper';
|
import Paper from '@mui/material/Paper';
|
||||||
|
|
@ -12,56 +12,47 @@ import {Switch, Button} from '@mui/material';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
import CopyableSecret from '../common/CopyableSecret';
|
import CopyableSecret from '../common/CopyableSecret';
|
||||||
import {observer} from 'mobx-react';
|
import {observer} from 'mobx-react';
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import {IPlugin} from '../types';
|
import {IPlugin} from '../types';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
|
||||||
@observer
|
const Plugins = observer(() => {
|
||||||
class Plugins extends Component<Stores<'pluginStore'>> {
|
const {pluginStore} = useStores();
|
||||||
public componentDidMount = () => this.props.pluginStore.refresh();
|
React.useEffect(() => void pluginStore.refresh(), []);
|
||||||
|
const plugins = pluginStore.getItems();
|
||||||
public render() {
|
return (
|
||||||
const {
|
<DefaultPage title="Plugins" maxWidth={1000}>
|
||||||
props: {pluginStore},
|
<Grid size={{xs: 12}}>
|
||||||
} = this;
|
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
||||||
const plugins = pluginStore.getItems();
|
<Table id="plugin-table">
|
||||||
return (
|
<TableHead>
|
||||||
<DefaultPage title="Plugins" maxWidth={1000}>
|
<TableRow>
|
||||||
<Grid size={{xs: 12}}>
|
<TableCell>ID</TableCell>
|
||||||
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
<TableCell>Enabled</TableCell>
|
||||||
<Table id="plugin-table">
|
<TableCell>Name</TableCell>
|
||||||
<TableHead>
|
<TableCell>Token</TableCell>
|
||||||
<TableRow>
|
<TableCell>Details</TableCell>
|
||||||
<TableCell>ID</TableCell>
|
</TableRow>
|
||||||
<TableCell>Enabled</TableCell>
|
</TableHead>
|
||||||
<TableCell>Name</TableCell>
|
<TableBody>
|
||||||
<TableCell>Token</TableCell>
|
{plugins.map((plugin: IPlugin) => (
|
||||||
<TableCell>Details</TableCell>
|
<Row
|
||||||
</TableRow>
|
key={plugin.token}
|
||||||
</TableHead>
|
id={plugin.id}
|
||||||
<TableBody>
|
token={plugin.token}
|
||||||
{plugins.map((plugin: IPlugin) => (
|
name={plugin.name}
|
||||||
<Row
|
enabled={plugin.enabled}
|
||||||
key={plugin.token}
|
fToggleStatus={() =>
|
||||||
id={plugin.id}
|
pluginStore.changeEnabledState(plugin.id, !plugin.enabled)
|
||||||
token={plugin.token}
|
}
|
||||||
name={plugin.name}
|
/>
|
||||||
enabled={plugin.enabled}
|
))}
|
||||||
fToggleStatus={() =>
|
</TableBody>
|
||||||
this.props.pluginStore.changeEnabledState(
|
</Table>
|
||||||
plugin.id,
|
</Paper>
|
||||||
!plugin.enabled
|
</Grid>
|
||||||
)
|
</DefaultPage>
|
||||||
}
|
);
|
||||||
/>
|
});
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
</DefaultPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -96,4 +87,4 @@ const Row: SFC<IRowProps> = observer(({name, id, token, enabled, fToggleStatus})
|
||||||
</TableRow>
|
</TableRow>
|
||||||
));
|
));
|
||||||
|
|
||||||
export default inject('pluginStore')(Plugins);
|
export default Plugins;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import * as React from 'react';
|
||||||
|
import {UserStore} from './user/UserStore';
|
||||||
|
import {SnackManager} from './snack/SnackManager';
|
||||||
|
import {MessagesStore} from './message/MessagesStore';
|
||||||
|
import {CurrentUser} from './CurrentUser';
|
||||||
|
import {ClientStore} from './client/ClientStore';
|
||||||
|
import {AppStore} from './application/AppStore';
|
||||||
|
import {WebSocketStore} from './message/WebSocketStore';
|
||||||
|
import {PluginStore} from './plugin/PluginStore';
|
||||||
|
|
||||||
|
export interface StoreMapping {
|
||||||
|
userStore: UserStore;
|
||||||
|
snackManager: SnackManager;
|
||||||
|
messagesStore: MessagesStore;
|
||||||
|
currentUser: CurrentUser;
|
||||||
|
clientStore: ClientStore;
|
||||||
|
appStore: AppStore;
|
||||||
|
pluginStore: PluginStore;
|
||||||
|
wsStore: WebSocketStore;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreContext = React.createContext<StoreMapping | undefined>(undefined);
|
||||||
|
|
||||||
|
export const useStores = (): StoreMapping => {
|
||||||
|
const mapping = React.useContext(StoreContext);
|
||||||
|
if (!mapping) throw new Error('uninitialized');
|
||||||
|
return mapping;
|
||||||
|
};
|
||||||
|
|
@ -7,118 +7,101 @@ import FormControlLabel from '@mui/material/FormControlLabel';
|
||||||
import Switch from '@mui/material/Switch';
|
import Switch from '@mui/material/Switch';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, {ChangeEvent, Component} from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
admin?: boolean;
|
admin?: boolean;
|
||||||
fClose: VoidFunction;
|
fClose: VoidFunction;
|
||||||
fOnSubmit: (name: string, pass: string, admin: boolean) => void;
|
fOnSubmit: (name: string, pass: string, admin: boolean) => Promise<void>;
|
||||||
isEdit?: boolean;
|
isEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
const AddEditUserDialog = ({
|
||||||
name: string;
|
fClose,
|
||||||
pass: string;
|
fOnSubmit,
|
||||||
admin: boolean;
|
isEdit,
|
||||||
}
|
name: initialName = '',
|
||||||
|
admin: initialAdmin = false,
|
||||||
|
}: IProps) => {
|
||||||
|
const [name, setName] = React.useState(initialName);
|
||||||
|
const [pass, setPass] = React.useState('');
|
||||||
|
const [admin, setAdmin] = React.useState(initialAdmin);
|
||||||
|
|
||||||
export default class AddEditDialog extends Component<IProps, IState> {
|
const namePresent = name.length !== 0;
|
||||||
public state = {
|
const passPresent = pass.length !== 0 || isEdit;
|
||||||
name: this.props.name ?? '',
|
const submitAndClose = async () => {
|
||||||
pass: '',
|
await fOnSubmit(name, pass, admin);
|
||||||
admin: this.props.admin ?? false,
|
fClose();
|
||||||
};
|
};
|
||||||
|
return (
|
||||||
public render() {
|
<Dialog
|
||||||
const {fClose, fOnSubmit, isEdit} = this.props;
|
open={true}
|
||||||
const {name, pass, admin} = this.state;
|
onClose={fClose}
|
||||||
const namePresent = this.state.name.length !== 0;
|
aria-labelledby="form-dialog-title"
|
||||||
const passPresent = this.state.pass.length !== 0 || isEdit;
|
id="add-edit-user-dialog">
|
||||||
const submitAndClose = () => {
|
<DialogTitle id="form-dialog-title">
|
||||||
fOnSubmit(name, pass, admin);
|
{isEdit ? 'Edit ' + name : 'Add a user'}
|
||||||
fClose();
|
</DialogTitle>
|
||||||
};
|
<DialogContent>
|
||||||
return (
|
<TextField
|
||||||
<Dialog
|
autoFocus
|
||||||
open={true}
|
margin="dense"
|
||||||
onClose={fClose}
|
className="name"
|
||||||
aria-labelledby="form-dialog-title"
|
label="Username *"
|
||||||
id="add-edit-user-dialog">
|
value={name}
|
||||||
<DialogTitle id="form-dialog-title">
|
name="username"
|
||||||
{isEdit ? 'Edit ' + this.props.name : 'Add a user'}
|
id="username"
|
||||||
</DialogTitle>
|
onChange={(e) => setName(e.target.value)}
|
||||||
<DialogContent>
|
fullWidth
|
||||||
<TextField
|
/>
|
||||||
autoFocus
|
<TextField
|
||||||
margin="dense"
|
margin="dense"
|
||||||
className="name"
|
className="password"
|
||||||
label="Username *"
|
type="password"
|
||||||
value={name}
|
value={pass}
|
||||||
name="username"
|
fullWidth
|
||||||
id="username"
|
label={isEdit ? 'Password (empty if no change)' : 'Password *'}
|
||||||
onChange={this.handleChange.bind(this, 'name')}
|
name="password"
|
||||||
fullWidth
|
id="password"
|
||||||
/>
|
onChange={(e) => setPass(e.target.value)}
|
||||||
<TextField
|
/>
|
||||||
margin="dense"
|
<FormControlLabel
|
||||||
className="password"
|
control={
|
||||||
type="password"
|
<Switch
|
||||||
value={pass}
|
checked={admin}
|
||||||
fullWidth
|
className="admin-rights"
|
||||||
label={isEdit ? 'Password (empty if no change)' : 'Password *'}
|
onChange={(e) => setAdmin(e.target.checked)}
|
||||||
name="password"
|
value="admin"
|
||||||
id="password"
|
/>
|
||||||
onChange={this.handleChange.bind(this, 'pass')}
|
}
|
||||||
/>
|
label="has administrator rights"
|
||||||
<FormControlLabel
|
/>
|
||||||
control={
|
</DialogContent>
|
||||||
<Switch
|
<DialogActions>
|
||||||
checked={admin}
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
className="admin-rights"
|
<Tooltip
|
||||||
onChange={this.handleChecked.bind(this, 'admin')}
|
placement={'bottom-start'}
|
||||||
value="admin"
|
title={
|
||||||
/>
|
namePresent
|
||||||
}
|
? passPresent
|
||||||
label="has administrator rights"
|
? ''
|
||||||
/>
|
: 'password is required'
|
||||||
</DialogContent>
|
: 'username is required'
|
||||||
<DialogActions>
|
}>
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
<div>
|
||||||
<Tooltip
|
<Button
|
||||||
placement={'bottom-start'}
|
className="save-create"
|
||||||
title={
|
disabled={!passPresent || !namePresent}
|
||||||
namePresent
|
onClick={submitAndClose}
|
||||||
? passPresent
|
color="primary"
|
||||||
? ''
|
variant="contained">
|
||||||
: 'password is required'
|
{isEdit ? 'Save' : 'Create'}
|
||||||
: 'username is required'
|
</Button>
|
||||||
}>
|
</div>
|
||||||
<div>
|
</Tooltip>
|
||||||
<Button
|
</DialogActions>
|
||||||
className="save-create"
|
</Dialog>
|
||||||
disabled={!passPresent || !namePresent}
|
);
|
||||||
onClick={submitAndClose}
|
};
|
||||||
color="primary"
|
export default AddEditUserDialog;
|
||||||
variant="contained">
|
|
||||||
{isEdit ? 'Save' : 'Create'}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChange(propertyName: 'name' | 'pass', event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
const state = this.state;
|
|
||||||
state[propertyName] = event.target.value;
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChecked(propertyName: 'admin', event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
const state = this.state;
|
|
||||||
state[propertyName] = event.target.checked;
|
|
||||||
this.setState(state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,97 +1,85 @@
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import Grid from '@mui/material/Grid';
|
import Grid from '@mui/material/Grid';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import React, {Component, FormEvent} from 'react';
|
import React from 'react';
|
||||||
import Container from '../common/Container';
|
import Container from '../common/Container';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
import {observable} from 'mobx';
|
|
||||||
import {observer} from 'mobx-react';
|
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import * as config from '../config';
|
import * as config from '../config';
|
||||||
import RegistrationDialog from './Register';
|
import RegistrationDialog from './Register';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
|
||||||
@observer
|
const Login = observer(() => {
|
||||||
class Login extends Component<Stores<'currentUser'>> {
|
const [username, setUsername] = React.useState('');
|
||||||
@observable
|
const [password, setPassword] = React.useState('');
|
||||||
private username = '';
|
const [registerDialog, setRegisterDialog] = React.useState(false);
|
||||||
@observable
|
const {currentUser} = useStores();
|
||||||
private password = '';
|
const registerButton = () => {
|
||||||
@observable
|
|
||||||
private registerDialog = false;
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {username, password, registerDialog} = this;
|
|
||||||
return (
|
|
||||||
<DefaultPage title="Login" rightControl={this.registerButton()} maxWidth={250}>
|
|
||||||
<Grid size={{xs: 12}} style={{textAlign: 'center'}}>
|
|
||||||
<Container>
|
|
||||||
<form onSubmit={this.preventDefault} id="login-form">
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
id="username"
|
|
||||||
className="name"
|
|
||||||
label="Username"
|
|
||||||
name="username"
|
|
||||||
margin="dense"
|
|
||||||
autoComplete="username"
|
|
||||||
value={username}
|
|
||||||
onChange={(e) => (this.username = e.target.value)}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="password"
|
|
||||||
type="password"
|
|
||||||
className="password"
|
|
||||||
label="Password"
|
|
||||||
name="password"
|
|
||||||
margin="normal"
|
|
||||||
autoComplete="current-password"
|
|
||||||
value={password}
|
|
||||||
onChange={(e) => (this.password = e.target.value)}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
variant="contained"
|
|
||||||
size="large"
|
|
||||||
className="login"
|
|
||||||
color="primary"
|
|
||||||
disabled={!!this.props.currentUser.connectionErrorMessage}
|
|
||||||
style={{marginTop: 15, marginBottom: 5}}
|
|
||||||
onClick={this.login}>
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Container>
|
|
||||||
</Grid>
|
|
||||||
{registerDialog && (
|
|
||||||
<RegistrationDialog
|
|
||||||
fClose={() => (this.registerDialog = false)}
|
|
||||||
fOnSubmit={this.props.currentUser.register}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DefaultPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private login = (e: React.MouseEvent<HTMLButtonElement>) => {
|
|
||||||
e.preventDefault();
|
|
||||||
this.props.currentUser.login(this.username, this.password);
|
|
||||||
};
|
|
||||||
|
|
||||||
private registerButton = () => {
|
|
||||||
if (config.get('register'))
|
if (config.get('register'))
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
id="register"
|
id="register"
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={() => (this.registerDialog = true)}>
|
onClick={() => setRegisterDialog(true)}>
|
||||||
Register
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
else return null;
|
else return null;
|
||||||
};
|
};
|
||||||
|
const login = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
currentUser.login(username, password);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DefaultPage title="Login" rightControl={registerButton()} maxWidth={250}>
|
||||||
|
<Grid size={{xs: 12}} style={{textAlign: 'center'}}>
|
||||||
|
<Container>
|
||||||
|
<form onSubmit={(e) => e.preventDefault()} id="login-form">
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
id="username"
|
||||||
|
className="name"
|
||||||
|
label="Username"
|
||||||
|
name="username"
|
||||||
|
margin="dense"
|
||||||
|
autoComplete="username"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
className="password"
|
||||||
|
label="Password"
|
||||||
|
name="password"
|
||||||
|
margin="normal"
|
||||||
|
autoComplete="current-password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
size="large"
|
||||||
|
className="login"
|
||||||
|
color="primary"
|
||||||
|
disabled={!!currentUser.connectionErrorMessage}
|
||||||
|
style={{marginTop: 15, marginBottom: 5}}
|
||||||
|
onClick={login}>
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</form>
|
||||||
|
</Container>
|
||||||
|
</Grid>
|
||||||
|
{registerDialog && (
|
||||||
|
<RegistrationDialog
|
||||||
|
fClose={() => setRegisterDialog(false)}
|
||||||
|
fOnSubmit={currentUser.register}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DefaultPage>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
private preventDefault = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
|
export default Login;
|
||||||
}
|
|
||||||
|
|
||||||
export default inject('currentUser')(Login);
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import DialogContent from '@mui/material/DialogContent';
|
||||||
import DialogTitle from '@mui/material/DialogTitle';
|
import DialogTitle from '@mui/material/DialogTitle';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import Tooltip from '@mui/material/Tooltip';
|
import Tooltip from '@mui/material/Tooltip';
|
||||||
import React, {ChangeEvent, Component} from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
@ -13,92 +13,85 @@ interface IProps {
|
||||||
fOnSubmit: (name: string, pass: string) => Promise<boolean>;
|
fOnSubmit: (name: string, pass: string) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
const RegistrationDialog = ({fClose, fOnSubmit, name: initialName = ''}: IProps) => {
|
||||||
name: string;
|
const [name, setName] = React.useState(initialName);
|
||||||
pass: string;
|
const [pass, setPass] = React.useState('');
|
||||||
}
|
const namePresent = name.length !== 0;
|
||||||
|
const passPresent = pass.length !== 0;
|
||||||
|
|
||||||
export default class RegistrationDialog extends Component<IProps, IState> {
|
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
public state = {
|
setName(e.target.value);
|
||||||
name: '',
|
|
||||||
pass: '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
public render() {
|
const handlePassChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const {fClose, fOnSubmit} = this.props;
|
setPass(e.target.value);
|
||||||
const {name, pass} = this.state;
|
};
|
||||||
const namePresent = this.state.name.length !== 0;
|
|
||||||
const passPresent = this.state.pass.length !== 0;
|
|
||||||
const submitAndClose = (): void => {
|
|
||||||
fOnSubmit(name, pass).then((success) => {
|
|
||||||
if (success) {
|
|
||||||
fClose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open={true}
|
|
||||||
onClose={fClose}
|
|
||||||
aria-labelledby="form-dialog-title"
|
|
||||||
id="add-edit-user-dialog">
|
|
||||||
<DialogTitle id="form-dialog-title">Registration</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<TextField
|
|
||||||
autoFocus
|
|
||||||
id="register-username"
|
|
||||||
margin="dense"
|
|
||||||
className="name"
|
|
||||||
label="Username *"
|
|
||||||
name="username"
|
|
||||||
value={name}
|
|
||||||
autoComplete="username"
|
|
||||||
onChange={this.handleChange.bind(this, 'name')}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
id="register-password"
|
|
||||||
margin="dense"
|
|
||||||
className="password"
|
|
||||||
type="password"
|
|
||||||
value={pass}
|
|
||||||
fullWidth
|
|
||||||
label="Password *"
|
|
||||||
name="password"
|
|
||||||
autoComplete="new-password"
|
|
||||||
onChange={this.handleChange.bind(this, 'pass')}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={fClose}>Cancel</Button>
|
|
||||||
<Tooltip
|
|
||||||
placement={'bottom-start'}
|
|
||||||
title={
|
|
||||||
namePresent
|
|
||||||
? passPresent
|
|
||||||
? ''
|
|
||||||
: 'password is required'
|
|
||||||
: 'username is required'
|
|
||||||
}>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="save-create"
|
|
||||||
disabled={!passPresent || !namePresent}
|
|
||||||
onClick={submitAndClose}
|
|
||||||
color="primary"
|
|
||||||
variant="contained">
|
|
||||||
Register
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleChange(propertyName: keyof IState, event: ChangeEvent<HTMLInputElement>) {
|
const submitAndClose = (): void => {
|
||||||
const state = this.state;
|
fOnSubmit(name, pass).then((success) => {
|
||||||
state[propertyName] = event.target.value;
|
if (success) {
|
||||||
this.setState(state);
|
fClose();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open={true}
|
||||||
|
onClose={fClose}
|
||||||
|
aria-labelledby="form-dialog-title"
|
||||||
|
id="add-edit-user-dialog">
|
||||||
|
<DialogTitle id="form-dialog-title">Registration</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
id="register-username"
|
||||||
|
margin="dense"
|
||||||
|
className="name"
|
||||||
|
label="Username *"
|
||||||
|
name="username"
|
||||||
|
value={name}
|
||||||
|
autoComplete="username"
|
||||||
|
onChange={handleNameChange}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
id="register-password"
|
||||||
|
margin="dense"
|
||||||
|
className="password"
|
||||||
|
type="password"
|
||||||
|
value={pass}
|
||||||
|
fullWidth
|
||||||
|
label="Password *"
|
||||||
|
name="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
onChange={handlePassChange}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={fClose}>Cancel</Button>
|
||||||
|
<Tooltip
|
||||||
|
placement={'bottom-start'}
|
||||||
|
title={
|
||||||
|
namePresent
|
||||||
|
? passPresent
|
||||||
|
? ''
|
||||||
|
: 'password is required'
|
||||||
|
: 'username is required'
|
||||||
|
}>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="save-create"
|
||||||
|
disabled={!passPresent || !namePresent}
|
||||||
|
onClick={submitAndClose}
|
||||||
|
color="primary"
|
||||||
|
variant="contained">
|
||||||
|
Register
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default RegistrationDialog;
|
||||||
|
|
|
||||||
|
|
@ -8,15 +8,14 @@ import TableHead from '@mui/material/TableHead';
|
||||||
import TableRow from '@mui/material/TableRow';
|
import TableRow from '@mui/material/TableRow';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import Edit from '@mui/icons-material/Edit';
|
import Edit from '@mui/icons-material/Edit';
|
||||||
import React, {Component, SFC} from 'react';
|
import React from 'react';
|
||||||
import ConfirmDialog from '../common/ConfirmDialog';
|
import ConfirmDialog from '../common/ConfirmDialog';
|
||||||
import DefaultPage from '../common/DefaultPage';
|
import DefaultPage from '../common/DefaultPage';
|
||||||
import Button from '@mui/material/Button';
|
import Button from '@mui/material/Button';
|
||||||
import AddEditDialog from './AddEditUserDialog';
|
import AddEditDialog from './AddEditUserDialog';
|
||||||
import {observer} from 'mobx-react';
|
|
||||||
import {observable} from 'mobx';
|
|
||||||
import {inject, Stores} from '../inject';
|
|
||||||
import {IUser} from '../types';
|
import {IUser} from '../types';
|
||||||
|
import {useStores} from '../stores';
|
||||||
|
import {observer} from 'mobx-react';
|
||||||
|
|
||||||
interface IRowProps {
|
interface IRowProps {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
@ -25,7 +24,7 @@ interface IRowProps {
|
||||||
fEdit: VoidFunction;
|
fEdit: VoidFunction;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
|
const UserRow: React.FC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell>{name}</TableCell>
|
<TableCell>{name}</TableCell>
|
||||||
<TableCell>{admin ? 'Yes' : 'No'}</TableCell>
|
<TableCell>{admin ? 'Yes' : 'No'}</TableCell>
|
||||||
|
|
@ -40,87 +39,71 @@ const UserRow: SFC<IRowProps> = ({name, admin, fDelete, fEdit}) => (
|
||||||
</TableRow>
|
</TableRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
@observer
|
const Users = observer(() => {
|
||||||
class Users extends Component<Stores<'userStore'>> {
|
const [deleteUser, setDeleteUser] = React.useState<IUser>();
|
||||||
@observable
|
const [editUser, setEditUser] = React.useState<IUser>();
|
||||||
private createDialog = false;
|
const [createDialog, setCreateDialog] = React.useState(false);
|
||||||
@observable
|
const {userStore} = useStores();
|
||||||
private deleteId: number | false = false;
|
React.useEffect(() => void userStore.refresh(), []);
|
||||||
@observable
|
const users = userStore.getItems();
|
||||||
private editId: number | false = false;
|
return (
|
||||||
|
<DefaultPage
|
||||||
|
title="Users"
|
||||||
|
rightControl={
|
||||||
|
<Button
|
||||||
|
id="create-user"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
onClick={() => setCreateDialog(true)}>
|
||||||
|
Create User
|
||||||
|
</Button>
|
||||||
|
}>
|
||||||
|
<Grid size={{xs: 12}}>
|
||||||
|
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
||||||
|
<Table id="user-table">
|
||||||
|
<TableHead>
|
||||||
|
<TableRow style={{textAlign: 'center'}}>
|
||||||
|
<TableCell>Username</TableCell>
|
||||||
|
<TableCell>Admin</TableCell>
|
||||||
|
<TableCell />
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{users.map((user: IUser) => (
|
||||||
|
<UserRow
|
||||||
|
key={user.id}
|
||||||
|
name={user.name}
|
||||||
|
admin={user.admin}
|
||||||
|
fDelete={() => setDeleteUser(user)}
|
||||||
|
fEdit={() => setEditUser(user)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</Paper>
|
||||||
|
</Grid>
|
||||||
|
{createDialog && (
|
||||||
|
<AddEditDialog fClose={() => setCreateDialog(false)} fOnSubmit={userStore.create} />
|
||||||
|
)}
|
||||||
|
{editUser && (
|
||||||
|
<AddEditDialog
|
||||||
|
fClose={() => setEditUser(undefined)}
|
||||||
|
fOnSubmit={userStore.update.bind(this, editUser.id)}
|
||||||
|
name={editUser.name}
|
||||||
|
admin={editUser.admin}
|
||||||
|
isEdit={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{deleteUser && (
|
||||||
|
<ConfirmDialog
|
||||||
|
title="Confirm Delete"
|
||||||
|
text={'Delete ' + deleteUser.name + '?'}
|
||||||
|
fClose={() => setDeleteUser(undefined)}
|
||||||
|
fOnSubmit={() => userStore.remove(deleteUser.id)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</DefaultPage>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
public componentDidMount = () => this.props.userStore.refresh();
|
export default Users;
|
||||||
|
|
||||||
public render() {
|
|
||||||
const {
|
|
||||||
deleteId,
|
|
||||||
editId,
|
|
||||||
createDialog,
|
|
||||||
props: {userStore},
|
|
||||||
} = this;
|
|
||||||
const users = userStore.getItems();
|
|
||||||
return (
|
|
||||||
<DefaultPage
|
|
||||||
title="Users"
|
|
||||||
rightControl={
|
|
||||||
<Button
|
|
||||||
id="create-user"
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={() => (this.createDialog = true)}>
|
|
||||||
Create User
|
|
||||||
</Button>
|
|
||||||
}>
|
|
||||||
<Grid size={{xs: 12}}>
|
|
||||||
<Paper elevation={6} style={{overflowX: 'auto'}}>
|
|
||||||
<Table id="user-table">
|
|
||||||
<TableHead>
|
|
||||||
<TableRow style={{textAlign: 'center'}}>
|
|
||||||
<TableCell>Username</TableCell>
|
|
||||||
<TableCell>Admin</TableCell>
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{users.map((user: IUser) => (
|
|
||||||
<UserRow
|
|
||||||
key={user.id}
|
|
||||||
name={user.name}
|
|
||||||
admin={user.admin}
|
|
||||||
fDelete={() => (this.deleteId = user.id)}
|
|
||||||
fEdit={() => (this.editId = user.id)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</Paper>
|
|
||||||
</Grid>
|
|
||||||
{createDialog && (
|
|
||||||
<AddEditDialog
|
|
||||||
fClose={() => (this.createDialog = false)}
|
|
||||||
fOnSubmit={userStore.create}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{editId !== false && (
|
|
||||||
<AddEditDialog
|
|
||||||
fClose={() => (this.editId = false)}
|
|
||||||
fOnSubmit={userStore.update.bind(this, editId)}
|
|
||||||
name={userStore.getByID(editId).name}
|
|
||||||
admin={userStore.getByID(editId).admin}
|
|
||||||
isEdit={true}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{deleteId !== false && (
|
|
||||||
<ConfirmDialog
|
|
||||||
title="Confirm Delete"
|
|
||||||
text={'Delete ' + userStore.getByID(deleteId).name + '?'}
|
|
||||||
fClose={() => (this.deleteId = false)}
|
|
||||||
fOnSubmit={() => userStore.remove(deleteId)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</DefaultPage>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default inject('userStore')(Users);
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue