diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js
index e266383..bad6458 100644
--- a/maubot/management/frontend/src/api.js
+++ b/maubot/management/frontend/src/api.js
@@ -82,6 +82,7 @@ export const deleteInstance = id => defaultDelete("instance", id)
export const getPlugins = () => defaultGet("/plugins")
export const getPlugin = id => defaultGet(`/plugin/${id}`)
+export const deletePlugin = id => defaultDelete("plugin", id)
export async function uploadPlugin(data, id) {
let resp
@@ -124,6 +125,6 @@ export default {
BASE_PATH,
login, ping,
getInstances, getInstance, putInstance, deleteInstance,
- getPlugins, getPlugin, uploadPlugin,
+ getPlugins, getPlugin, uploadPlugin, deletePlugin,
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
}
diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js
index 24e91f2..5dcb0d2 100644
--- a/maubot/management/frontend/src/pages/dashboard/Client.js
+++ b/maubot/management/frontend/src/pages/dashboard/Client.js
@@ -220,10 +220,6 @@ class Client extends Component {
)
- get hasInstances() {
- return this.state.instances.length > 0
- }
-
renderPrefButtons = () => <>
{!this.isNew && (
@@ -240,6 +236,10 @@ class Client extends Component {
{this.state.error}
>
+ get hasInstances() {
+ return this.state.instances.length > 0
+ }
+
renderInstances = () => !this.isNew && (
{this.hasInstances ? "Instances" : "No instances :("}
diff --git a/maubot/management/frontend/src/pages/dashboard/Plugin.js b/maubot/management/frontend/src/pages/dashboard/Plugin.js
index eae4022..b7c510e 100644
--- a/maubot/management/frontend/src/pages/dashboard/Plugin.js
+++ b/maubot/management/frontend/src/pages/dashboard/Plugin.js
@@ -14,8 +14,12 @@
// 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 } from "react-router-dom"
+import { NavLink, Link } 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"
const PluginListEntry = ({ plugin }) => (
@@ -28,8 +32,115 @@ const PluginListEntry = ({ plugin }) => (
class Plugin extends Component {
static ListEntry = PluginListEntry
+ constructor(props) {
+ super(props)
+ this.state = Object.assign(this.initialState, props.plugin)
+ }
+
+ get initialState() {
+ return {
+ id: "",
+ version: "",
+
+ instances: [],
+
+ uploading: false,
+ deleting: false,
+ error: "",
+ }
+ }
+
+ 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({
+ uploadingAvatar: true,
+ })
+ const data = await this.readFile(file)
+ const resp = await api.uploadPlugin(data, this.state.id)
+ if (resp.id) {
+ if (this.isNew) {
+ this.props.history.push(`/plugin/${resp.id}`)
+ } else {
+ this.setState({ saving: false, error: "" })
+ }
+ this.props.onChange(resp)
+ } else {
+ this.setState({ saving: false, error: resp.error })
+ }
+ }
+
+ 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 {this.props.id}
+ return
+
+
+ evt.target.parentElement.classList.add("drag")}
+ onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/>
+ {this.state.uploading && }
+
+ {!this.isNew && <>
+
+
+
+
+
+
+
+ >}
+
{this.state.error}
+ {!this.isNew && this.renderInstances()}
+
}
}
diff --git a/maubot/management/frontend/src/style/base/elements.sass b/maubot/management/frontend/src/style/base/elements.sass
index 53fd27a..fc0b24f 100644
--- a/maubot/management/frontend/src/style/base/elements.sass
+++ b/maubot/management/frontend/src/style/base/elements.sass
@@ -46,6 +46,10 @@
&:hover:not(:disabled)
background-color: $primary-dark
+ &:disabled.disabled-bg
+ background-color: $background-dark !important
+ color: $text-color
+
.button
+button
diff --git a/maubot/management/frontend/src/style/index.sass b/maubot/management/frontend/src/style/index.sass
index c876eac..b496d8b 100644
--- a/maubot/management/frontend/src/style/index.sass
+++ b/maubot/management/frontend/src/style/index.sass
@@ -22,5 +22,8 @@
@import lib/preferencetable
@import lib/switch
+@import pages/mixins/upload-container
+@import pages/mixins/instancelist
+
@import pages/login
@import pages/dashboard
diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass
index d6ac7ce..66fcad7 100644
--- a/maubot/management/frontend/src/style/lib/preferencetable.sass
+++ b/maubot/management/frontend/src/style/lib/preferencetable.sass
@@ -51,6 +51,7 @@
border: none
height: 2rem
width: 100%
+ color: $text-color
box-sizing: border-box
diff --git a/maubot/management/frontend/src/style/pages/client/avatar.sass b/maubot/management/frontend/src/style/pages/client/avatar.sass
index 9174ba8..062a605 100644
--- a/maubot/management/frontend/src/style/pages/client/avatar.sass
+++ b/maubot/management/frontend/src/style/pages/client/avatar.sass
@@ -15,15 +15,11 @@
// along with this program. If not, see .
> div.avatar-container
- position: relative
+ +upload-box
+
width: 8rem
height: 8rem
border-radius: 50%
- overflow: hidden
-
- display: flex
- align-items: center
- justify-content: center
> img.avatar
position: absolute
@@ -33,30 +29,16 @@
user-select: none
> svg.upload
- position: absolute
- display: block
visibility: hidden
width: 6rem
height: 6rem
- padding: 1rem
- user-select: none
-
> input.file-selector
- position: absolute
width: 8rem
height: 8rem
- user-select: none
- opacity: 0
-
- > div.spinner
- +thick-spinner
&:not(.uploading)
- > input.file-selector
- cursor: pointer
-
&:hover, &.drag
> img.avatar
opacity: .25
diff --git a/maubot/management/frontend/src/style/pages/client/index.sass b/maubot/management/frontend/src/style/pages/client/index.sass
index 2359059..9444513 100644
--- a/maubot/management/frontend/src/style/pages/client/index.sass
+++ b/maubot/management/frontend/src/style/pages/client/index.sass
@@ -34,4 +34,5 @@
vertical-align: top
flex: 1
- @import instances
+ > div.instances
+ +instancelist
diff --git a/maubot/management/frontend/src/style/pages/dashboard.sass b/maubot/management/frontend/src/style/pages/dashboard.sass
index 2d608ce..c5fe3e9 100644
--- a/maubot/management/frontend/src/style/pages/dashboard.sass
+++ b/maubot/management/frontend/src/style/pages/dashboard.sass
@@ -77,7 +77,7 @@
div.error
+notification($error)
- margin-top: 1rem
+ margin: 1rem .5rem
&:empty
display: none
diff --git a/maubot/management/frontend/src/style/pages/client/instances.sass b/maubot/management/frontend/src/style/pages/mixins/instancelist.sass
similarity index 98%
rename from maubot/management/frontend/src/style/pages/client/instances.sass
rename to maubot/management/frontend/src/style/pages/mixins/instancelist.sass
index 561a4b6..3ee93df 100644
--- a/maubot/management/frontend/src/style/pages/client/instances.sass
+++ b/maubot/management/frontend/src/style/pages/mixins/instancelist.sass
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-> div.instances
+=instancelist()
margin: 1rem 0
display: flex
diff --git a/maubot/management/frontend/src/style/pages/mixins/upload-container.sass b/maubot/management/frontend/src/style/pages/mixins/upload-container.sass
new file mode 100644
index 0000000..882c547
--- /dev/null
+++ b/maubot/management/frontend/src/style/pages/mixins/upload-container.sass
@@ -0,0 +1,43 @@
+// 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 .
+
+=upload-box()
+ position: relative
+ overflow: hidden
+
+ display: flex
+ align-items: center
+ justify-content: center
+
+ > svg.upload
+ position: absolute
+ display: block
+
+ padding: 1rem
+ user-select: none
+
+
+ > input.file-selector
+ position: absolute
+ user-select: none
+ opacity: 0
+
+ > div.spinner
+ +thick-spinner
+
+ &:not(.uploading)
+ > input.file-selector
+ cursor: pointer
diff --git a/maubot/management/frontend/src/style/pages/plugin.sass b/maubot/management/frontend/src/style/pages/plugin.sass
index e1376b5..b45d867 100644
--- a/maubot/management/frontend/src/style/pages/plugin.sass
+++ b/maubot/management/frontend/src/style/pages/plugin.sass
@@ -15,4 +15,41 @@
// along with this program. If not, see .
> .plugin
- margin: 1rem
+ margin: 2rem 4rem
+
+ > .upload-box
+ +upload-box
+
+ width: calc(100% - 1rem)
+ height: 10rem
+ margin: .5rem
+ border-radius: .5rem
+ box-sizing: border-box
+
+ border: .25rem dotted $primary
+
+ > svg.upload
+ width: 8rem
+ height: 8rem
+
+ opacity: .5
+
+ > input.file-selector
+ width: 100%
+ height: 100%
+
+ &:not(.uploading):hover, &:not(.uploading).drag
+ border: .25rem solid $primary
+ background-color: $primary-light
+
+ > svg.upload
+ opacity: 1
+
+ &.uploading
+ > svg.upload
+ visibility: hidden
+ > input.file-selector
+ cursor: default
+
+ > div.instances
+ +instancelist