diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js
index d0acbb6..8721685 100644
--- a/maubot/management/frontend/src/api.js
+++ b/maubot/management/frontend/src/api.js
@@ -72,7 +72,7 @@ export async function uploadPlugin(data, id) {
let resp
if (id) {
resp = await fetch(`${BASE_PATH}/plugin/${id}`, {
- headers: getHeaders("applcation/zip"),
+ headers: getHeaders("application/zip"),
body: data,
method: "PUT",
})
@@ -96,9 +96,27 @@ export async function getClient(id) {
return await resp.json()
}
+export async function uploadAvatar(id, data, mime) {
+ const resp = await fetch(`${BASE_PATH}/client/${id}/avatar`, {
+ headers: getHeaders(mime),
+ body: data,
+ method: "POST",
+ })
+ return await resp.json()
+}
+
+export async function putClient(client) {
+ const resp = await fetch(`${BASE_PATH}/client/${client.id}`, {
+ headers: getHeaders(),
+ body: JSON.stringify(client),
+ method: "PUT",
+ })
+ return await resp.json()
+}
+
export default {
login, ping,
getInstances, getInstance,
- getPlugins, getPlugin, uploadPlugin,
- getClients, getClient,
+ getPlugins, getPlugin, uploadPlugin,
+ getClients, getClient, uploadAvatar, putClient,
}
diff --git a/maubot/management/frontend/src/components/PreferenceTable.js b/maubot/management/frontend/src/components/PreferenceTable.js
new file mode 100644
index 0000000..28ddfab
--- /dev/null
+++ b/maubot/management/frontend/src/components/PreferenceTable.js
@@ -0,0 +1,55 @@
+// maubot - A plugin-based Matrix bot system.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+import React from "react"
+import Switch from "./Switch"
+
+export const PrefTable = ({ children, wrapperClass }) => {
+ if (wrapperClass) {
+ return (
+
+ )
+ }
+ return (
+
+ {children}
+
+ )
+}
+
+export const PrefRow = ({ name, children }) => (
+
+)
+
+export const PrefInput = ({ rowName, ...args }) => (
+
+
+
+)
+
+export const PrefSwitch = ({ rowName, ...args }) => (
+
+
+
+)
+
+export default PrefTable
diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js
index d352b31..2019444 100644
--- a/maubot/management/frontend/src/pages/dashboard/Client.js
+++ b/maubot/management/frontend/src/pages/dashboard/Client.js
@@ -15,11 +15,16 @@
// along with this program. If not, see .
import React, { Component } from "react"
import { Link } from "react-router-dom"
-import Switch from "../../components/Switch"
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"
function getAvatarURL(client) {
+ if (!client.avatar_url) {
+ return ""
+ }
const id = client.avatar_url.substr("mxc://".length)
return `${client.homeserver}/_matrix/media/r0/download/${id}`
}
@@ -45,68 +50,136 @@ class Client extends Component {
constructor(props) {
super(props)
- this.state = props
+ this.state = Object.assign(this.initialState, props.client)
+ }
+
+ get initialState() {
+ return {
+ id: "",
+ displayname: "",
+ homeserver: "",
+ avatar_url: "",
+ access_token: "",
+ sync: true,
+ autojoin: true,
+ enabled: true,
+ started: false,
+
+ uploadingAvatar: false,
+ saving: false,
+ startingOrStopping: false,
+ }
}
componentWillReceiveProps(nextProps) {
- this.setState(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({
+ uploadingAvatar: true,
+ })
+ const data = await this.readFile(file)
+ const resp = await api.uploadAvatar(this.state.id, data, file.type)
+ this.setState({
+ uploadingAvatar: false,
+ avatar_url: resp.content_uri,
+ })
+ }
+
+ save = async () => {
+ this.setState({ saving: true })
+ const resp = await api.putClient(this.state)
+ if (resp.id) {
+ resp.saving = false
+ this.setState(resp)
+ } else {
+ console.error(resp)
+ }
+ }
+
+ startOrStop = async () => {
+ this.setState({ startingOrStopping: true })
+ const resp = await api.putClient({
+ id: this.state.id,
+ started: !this.state.started,
+ })
+ if (resp.id) {
+ resp.startingOrStopping = false
+ this.setState(resp)
+ } else {
+ console.error(resp)
+ }
+ }
+
render() {
return
-
-
-
+
+
+
+
+
+ {this.state.uploadingAvatar &&
}
+
+ {this.props.client && (<>
+
+
+ {this.state.started ? "Started" : "Stopped"}
+
+
+ {this.state.startingOrStopping ?
+ : (this.state.started ? "Stop" : "Start")}
+
+ >)}
-
-
-
-
-
-
Sync
-
-
+
this.setState({ sync })}/>
-
-
-
-
Enabled
-
-
this.setState({ autojoin })}/>
+ this.setState({ enabled })}/>
-
-
-
+
+
+ {this.state.saving ? : "Save"}
+
+
}
}
diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js
index 34dbb0c..5d9a38d 100644
--- a/maubot/management/frontend/src/pages/dashboard/index.js
+++ b/maubot/management/frontend/src/pages/dashboard/index.js
@@ -58,11 +58,19 @@ class Dashboard extends Component {
}
renderView(field, type, id) {
- const entry = this.state[field + "s"][id]
+ const stateField = field + "s"
+ const entry = this.state[stateField][id]
if (!entry) {
return "Not found :("
}
- return React.createElement(type, entry)
+ return React.createElement(type, {
+ [field]: entry,
+ onChange: newEntry => this.setState({
+ [stateField]: Object.assign({}, this.state[stateField], {
+ [id]: newEntry,
+ }),
+ }),
+ })
}
render() {
@@ -72,7 +80,9 @@ class Dashboard extends Component {
Maubot Manager
- {localStorage.username}
+
+ {localStorage.username}
+
diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass
index 76f3a4e..6270aba 100644
--- a/maubot/management/frontend/src/style/base/elements.sass
+++ b/maubot/management/frontend/src/style/base/elements.sass
@@ -25,10 +25,12 @@
color: $inverted-text-color
box-sizing: border-box
font-size: 1rem
- cursor: pointer
- &:hover
- background-color: darken($background, 10%)
+ &:not(:disabled)
+ cursor: pointer
+
+ &:hover
+ background-color: darken($background, 10%)
=link-button()
display: inline-block
@@ -37,7 +39,7 @@
=main-color-button()
background-color: $primary
- &:hover
+ &:hover:not(:disabled)
background-color: $primary-dark
.button
diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass
index f6b6033..c876eac 100644
--- a/maubot/management/frontend/src/style/index.sass
+++ b/maubot/management/frontend/src/style/index.sass
@@ -14,9 +14,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
@import lib/spinner
+
@import base/vars
@import base/body
@import base/elements
+
+@import lib/preferencetable
@import lib/switch
@import pages/login
diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass
new file mode 100644
index 0000000..59e6a4a
--- /dev/null
+++ b/maubot/management/frontend/src/style/lib/preferencetable.sass
@@ -0,0 +1,57 @@
+// maubot - A plugin-based Matrix bot system.
+// Copyright (C) 2018 Tulir Asokan
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see .
+
+.preference-table
+ display: table
+
+ width: 100%
+
+ > .row
+ display: table-row
+
+ > .key, > .value
+ display: table-cell
+ padding-bottom: .5rem
+
+ > .key
+ width: 7rem
+
+ > .value
+ margin: .5rem
+
+ > .switch
+ width: auto
+ height: 2rem
+
+ > input
+ border: none
+ height: 2rem
+ width: 100%
+
+ box-sizing: border-box
+
+ padding: .375rem 0
+ background-color: $background
+
+ font-size: 1rem
+
+ border-bottom: 1px dotted $primary
+
+ &:hover:not(:disabled)
+ border-bottom: 1px solid $primary
+
+ &:focus:not(:disabled)
+ border-bottom: 2px solid $primary
diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass
index a6a6f6a..bfb41bc 100644
--- a/maubot/management/frontend/src/style/pages/client.sass
+++ b/maubot/management/frontend/src/style/pages/client.sass
@@ -16,85 +16,116 @@
> .client
margin: 1rem
+ display: flex
- div.avatar-container
- position: relative
- display: inline-block
- width: 8rem
- height: 8rem
- border-radius: 100%
- cursor: pointer
+ > div.sidebar
vertical-align: top
+ text-align: center
- > img.avatar
- display: block
- max-width: 8rem
- max-height: 8rem
- border-radius: 100%
- position: absolute
- left: 50%
- top: 50%
- -webkit-transform: translateY(-50%) translateX(-50%)
+ > div
+ margin-bottom: 1rem
- > svg.upload
- position: absolute
- display: block
- visibility: hidden
+ > div.avatar-container
+ position: relative
+ width: 8rem
+ height: 8rem
+ border-radius: 50%
+ overflow: hidden
- width: 6rem
- height: 6rem
+ display: flex
+ align-items: center
+ justify-content: center
- padding: 1rem
-
- &:hover
> img.avatar
- opacity: .25
+ position: absolute
+ display: block
+ max-width: 8rem
+ max-height: 8rem
+ user-select: none
> svg.upload
- visibility: visible
+ position: absolute
+ display: block
+ visibility: hidden
- div.info-container
- display: inline-table
- vertical-align: top
+ width: 6rem
+ height: 6rem
- margin: 1rem 2rem
+ padding: 1rem
+ user-select: none
- > .row
- display: table-row
+ > input.file-selector
+ position: absolute
+ width: 8rem
+ height: 8rem
+ user-select: none
+ opacity: 0
- > .key, > .value
- display: table-cell
- padding-bottom: .5rem
+ > div.spinner
+ +thick-spinner
- > .key
- width: 6.5rem
+ &:not(.uploading)
+ > input.file-selector
+ cursor: pointer
- > .value
+ &:hover
+ > img.avatar
+ opacity: .25
+
+ > svg.upload
+ visibility: visible
+
+ &.no-avatar
+ > img.avatar
+ visibility: hidden
+
+ > svg.upload
+ visibility: visible
+ opacity: .5
+
+ &.uploading
+ > img.avatar
+ opacity: .25
+
+ > div.started-container
+ display: inline-flex
+
+ > span.started
+ display: inline-block
+ height: 0
+ width: 0
+ border-radius: 50%
margin: .5rem
- > .value > .switch
- width: auto
- height: 2rem
+ &.true
+ background-color: $primary
+ box-shadow: 0 0 .75rem .75rem $primary
- > .value > input
- border: none
- height: 2rem
- width: 100%
+ &.false
+ background-color: $error-light
+ box-shadow: 0 0 .75rem .75rem $error-light
- box-sizing: border-box
+ > span.text
+ display: inline-block
+ margin-left: 1rem
- padding: .375rem 0
- background-color: $background
+ > div.info-container
+ vertical-align: top
- font-size: 1rem
+ margin: 0 1rem
+ flex: 1
- border-bottom: 1px solid transparent
+ button.save
+ +button
+ +main-color-button
+ width: 100%
+ height: 2.5rem
+ padding: 0
- &:hover:not(:disabled)
- border-bottom: 1px solid $primary
-
- &:focus:not(:disabled)
- border-bottom: 2px solid $primary
+ > .spinner
+ +thick-spinner
+ +white-spinner
+ width: 2rem
//> .client
display: table
diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass
index 801fc86..184c4db 100644
--- a/maubot/management/frontend/src/style/pages/dashboard.sass
+++ b/maubot/management/frontend/src/style/pages/dashboard.sass
@@ -46,9 +46,19 @@
grid-area: topbar
display: flex
align-items: center
- justify-content: center
+ justify-content: right
background-color: $primary
box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .2)
+ padding: .5rem 1rem
+
+ > div.user
+ display: inline-flex
+ align-items: center
+ height: 100%
+ padding: 0 1rem
+ box-sizing: border-box
+ background-color: $primary-dark
+ border-radius: .25rem
@import "sidebar"