diff --git a/maubot/management/frontend/src/pages/dashboard/BaseMainView.js b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js new file mode 100644 index 0000000..172a302 --- /dev/null +++ b/maubot/management/frontend/src/pages/dashboard/BaseMainView.js @@ -0,0 +1,68 @@ +import React, { Component } from "react" +import { Link } from "react-router-dom" + +class BaseMainView extends Component { + constructor(props) { + super(props) + this.state = Object.assign(this.initialState, props.entry) + } + + componentWillReceiveProps(nextProps) { + this.setState(Object.assign(this.initialState, nextProps.entry)) + } + + delete = async () => { + if (!window.confirm(`Are you sure you want to delete ${this.state.id}?`)) { + return + } + this.setState({ deleting: true }) + const resp = await this.deleteFunc(this.state.id) + if (resp.success) { + this.props.history.push("/") + this.props.onDelete() + } else { + this.setState({ deleting: false, error: resp.error }) + } + } + + get initialState() { + throw Error("Not implemented") + } + + get hasInstances() { + return this.state.instances && this.state.instances.length > 0 + } + + get isNew() { + return !this.props.entry + } + + inputChange = event => { + if (!event.target.name) { + return + } + this.setState({ [event.target.name]: event.target.value }) + } + + async readFile(file) { + return new Promise((resolve, reject) => { + const reader = new FileReader() + reader.readAsArrayBuffer(file) + reader.onload = evt => resolve(evt.target.result) + reader.onerror = err => reject(err) + }) + } + + renderInstances = () => !this.isNew && ( +
+

{this.hasInstances ? "Instances" : "No instances :("}

+ {this.state.instances.map(instance => ( + + {instance.id} + + ))} +
+ ) +} + +export default BaseMainView diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js index 5dcb0d2..04d195f 100644 --- a/maubot/management/frontend/src/pages/dashboard/Client.js +++ b/maubot/management/frontend/src/pages/dashboard/Client.js @@ -13,36 +13,37 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -import { Link, NavLink, withRouter } from "react-router-dom" +import React from "react" +import { NavLink, withRouter } from "react-router-dom" import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as UploadButton } from "../../res/upload.svg" import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable" import Spinner from "../../components/Spinner" import api from "../../api" +import BaseMainView from "./BaseMainView" -const ClientListEntry = ({ client }) => { +const ClientListEntry = ({ entry }) => { const classes = ["client", "entry"] - if (!client.enabled) { + if (!entry.enabled) { classes.push("disabled") - } else if (!client.started) { + } else if (!entry.started) { classes.push("stopped") } return ( - - - {client.displayname || client.id} + + + {entry.displayname || entry.id} ) } -class Client extends Component { +class Client extends BaseMainView { static ListEntry = ClientListEntry constructor(props) { super(props) - this.state = Object.assign(this.initialState, props.client) + this.deleteFunc = api.deleteClient } get initialState() { @@ -78,26 +79,6 @@ class Client extends Component { return client } - componentWillReceiveProps(nextProps) { - this.setState(Object.assign(this.initialState, nextProps.client)) - } - - inputChange = event => { - if (!event.target.name) { - return - } - this.setState({ [event.target.name]: event.target.value }) - } - - async readFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.readAsArrayBuffer(file) - reader.onload = evt => resolve(evt.target.result) - reader.onerror = err => reject(err) - }) - } - avatarUpload = async event => { const file = event.target.files[0] this.setState({ @@ -126,25 +107,11 @@ class Client extends Component { } } - delete = async () => { - if (!window.confirm(`Are you sure you want to delete ${this.state.id}?`)) { - return - } - this.setState({ deleting: true }) - const resp = await api.deleteClient(this.state.id) - if (resp.success) { - this.props.history.push("/") - this.props.onDelete() - } else { - this.setState({ deleting: false, error: resp.error }) - } - } - startOrStop = async () => { this.setState({ startingOrStopping: true }) const resp = await api.putClient({ - id: this.props.client.id, - started: !this.props.client.started, + id: this.props.entry.id, + started: !this.props.entry.started, }) if (resp.id) { this.props.onChange(resp) @@ -158,10 +125,6 @@ class Client extends Component { return this.state.saving || this.state.startingOrStopping || this.state.deleting } - get isNew() { - return !Boolean(this.props.client) - } - renderSidebar = () => !this.isNew && (
}
- + - {this.props.client.started ? "Started" : - (this.props.client.enabled ? "Stopped" : "Disabled")} + {this.props.entry.started ? "Started" : + (this.props.entry.enabled ? "Stopped" : "Disabled")}
- {(this.props.client.started || this.props.client.enabled) && ( + {(this.props.entry.started || this.props.entry.enabled) && ( )}
@@ -236,21 +199,6 @@ class Client extends Component {
{this.state.error}
- get hasInstances() { - return this.state.instances.length > 0 - } - - renderInstances = () => !this.isNew && ( -
-

{this.hasInstances ? "Instances" : "No instances :("}

- {this.state.instances.map(instance => ( - - {instance.id} - - ))} -
- ) - render() { return
{this.renderSidebar()} diff --git a/maubot/management/frontend/src/pages/dashboard/Instance.js b/maubot/management/frontend/src/pages/dashboard/Instance.js index f983938..7e73587 100644 --- a/maubot/management/frontend/src/pages/dashboard/Instance.js +++ b/maubot/management/frontend/src/pages/dashboard/Instance.js @@ -13,7 +13,7 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" +import React from "react" import { NavLink, withRouter } from "react-router-dom" import AceEditor from "react-ace" import "brace/mode/yaml" @@ -22,20 +22,21 @@ import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import PrefTable, { PrefInput, PrefSelect, PrefSwitch } from "../../components/PreferenceTable" import api from "../../api" import Spinner from "../../components/Spinner" +import BaseMainView from "./BaseMainView" -const InstanceListEntry = ({ instance }) => ( - - {instance.id} +const InstanceListEntry = ({ entry }) => ( + + {entry.id} ) -class Instance extends Component { +class Instance extends BaseMainView { static ListEntry = InstanceListEntry constructor(props) { super(props) - this.state = Object.assign(this.initialState, props.instance) + this.deleteFunc = api.deleteInstance this.updateClientOptions() } @@ -63,7 +64,7 @@ class Instance extends Component { } componentWillReceiveProps(nextProps) { - this.setState(Object.assign(this.initialState, nextProps.instance)) + super.componentWillReceiveProps(nextProps) this.updateClientOptions() } @@ -82,22 +83,15 @@ class Instance extends Component { this.clientOptions = Object.values(this.props.ctx.clients).map(this.clientSelectEntry) } - inputChange = event => { - if (!event.target.name) { - return - } - this.setState({ [event.target.name]: event.target.value }) - } - save = async () => { this.setState({ saving: true }) - const resp = await api.putInstance(this.instanceInState, this.props.instance - ? this.props.instance.id : undefined) + const resp = await api.putInstance(this.instanceInState, this.props.entry + ? this.props.entry.id : undefined) if (resp.id) { if (this.isNew) { this.props.history.push(`/instance/${resp.id}`) } else { - if (resp.id !== this.props.instance.id) { + if (resp.id !== this.props.entry.id) { this.props.history.replace(`/instance/${resp.id}`) } this.setState({ saving: false, error: "" }) @@ -108,24 +102,6 @@ class Instance extends Component { } } - delete = async () => { - if (!window.confirm(`Are you sure you want to delete ${this.state.id}?`)) { - return - } - this.setState({ deleting: true }) - const resp = await api.deleteInstance(this.state.id) - if (resp.success) { - this.props.history.push("/") - this.props.onDelete() - } else { - this.setState({ deleting: false, error: resp.error }) - } - } - - get isNew() { - return !Boolean(this.props.instance) - } - get selectedClientEntry() { return this.state.primary_user ? this.clientSelectEntry(this.props.ctx.clients[this.state.primary_user]) diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js index b7c510e..bb3ea4e 100644 --- a/maubot/management/frontend/src/pages/dashboard/Plugin.js +++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js @@ -13,30 +13,26 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import React, { Component } from "react" -import { NavLink, Link } from "react-router-dom" +import React from "react" +import { NavLink } from "react-router-dom" import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as UploadButton } from "../../res/upload.svg" import PrefTable, { PrefInput } from "../../components/PreferenceTable" import Spinner from "../../components/Spinner" import api from "../../api" +import BaseMainView from "./BaseMainView" -const PluginListEntry = ({ plugin }) => ( - - {plugin.id} +const PluginListEntry = ({ entry }) => ( + + {entry.id} ) -class Plugin extends Component { +class Plugin extends BaseMainView { static ListEntry = PluginListEntry - constructor(props) { - super(props) - this.state = Object.assign(this.initialState, props.plugin) - } - get initialState() { return { id: "", @@ -50,18 +46,6 @@ class Plugin extends Component { } } - componentWillReceiveProps(nextProps) { - this.setState(Object.assign(this.initialState, nextProps.plugin)) - } - - async readFile(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader() - reader.readAsArrayBuffer(file) - reader.onload = evt => resolve(evt.target.result) - reader.onerror = err => reject(err) - }) - } upload = async event => { const file = event.target.files[0] this.setState({ @@ -81,39 +65,6 @@ class Plugin extends Component { } } - delete = async () => { - if (!window.confirm(`Are you sure you want to delete ${this.state.id}?`)) { - return - } - this.setState({ deleting: true }) - const resp = await api.deletePlugin(this.state.id) - if (resp.success) { - this.props.history.push("/") - this.props.onDelete() - } else { - this.setState({ deleting: false, error: resp.error }) - } - } - - get isNew() { - return !Boolean(this.props.plugin) - } - - get hasInstances() { - return this.state.instances.length > 0 - } - - renderInstances = () => !this.isNew && ( -
-

{this.hasInstances ? "Instances" : "No instances :("}

- {this.state.instances.map(instance => ( - - {instance.id} - - ))} -
- ) - render() { return
diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js index eafc943..f356db9 100644 --- a/maubot/management/frontend/src/pages/dashboard/index.js +++ b/maubot/management/frontend/src/pages/dashboard/index.js @@ -29,7 +29,7 @@ class Dashboard extends Component { clients: {}, plugins: {}, } - global.maubot = this + window.maubot = this } async componentWillMount() { @@ -51,8 +51,8 @@ class Dashboard extends Component { } renderList(field, type) { - return Object.values(this.state[field + "s"]).map(entry => - React.createElement(type, { key: entry.id, [field]: entry })) + return this.state[field] && Object.values(this.state[field]).map(entry => + React.createElement(type, { key: entry.id, entry })) } delete(stateField, id) { @@ -71,19 +71,24 @@ class Dashboard extends Component { } renderView(field, type, id) { - const stateField = field + "s" - const entry = this.state[stateField][id] + const entry = this.state[field][id] if (!entry) { - return "Not found :(" + return this.renderNotFound(field.slice(0, -1)) } return React.createElement(type, { - [field]: entry, - onDelete: () => this.delete(stateField, id), - onChange: newEntry => this.add(stateField, newEntry, id), + entry, + onDelete: () => this.delete(field, id), + onChange: newEntry => this.add(field, newEntry, id), ctx: this.state, }) } + renderNotFound = (thing = "path") => ( +
+ Oops! I'm afraid that {thing} couldn't be found. +
+ ) + render() { return
@@ -100,21 +105,21 @@ class Dashboard extends Component {

Instances

- {this.renderList("instance", Instance.ListEntry)} + {this.renderList("instances", Instance.ListEntry)}

Clients

- {this.renderList("client", Client.ListEntry)} + {this.renderList("clients", Client.ListEntry)}

Plugins

- {this.renderList("plugin", Plugin.ListEntry)} + {this.renderList("plugins", Plugin.ListEntry)}
@@ -128,12 +133,12 @@ class Dashboard extends Component { this.add("plugins", newEntry)}/>}/> - this.renderView("instance", Instance, match.params.id)}/> + this.renderView("instances", Instance, match.params.id)}/> - this.renderView("client", Client, match.params.id)}/> + this.renderView("clients", Client, match.params.id)}/> this.renderView("plugin", Plugin, match.params.id)}/> - "Not found :("}/> + this.renderNotFound()}/>
diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass index c5fe3e9..56269f2 100644 --- a/maubot/management/frontend/src/style/pages/dashboard.sass +++ b/maubot/management/frontend/src/style/pages/dashboard.sass @@ -69,6 +69,11 @@ @import instance @import plugin + > .not-found + text-align: center + margin-top: 5rem + font-size: 1.5rem + div.buttons +button-group display: flex