Add plugin view

This commit is contained in:
Tulir Asokan 2018-11-10 19:58:28 +02:00
parent 5220d2e5c9
commit f97b39f4e3
12 changed files with 214 additions and 31 deletions

View File

@ -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,
}

View File

@ -220,10 +220,6 @@ class Client extends Component {
</PrefTable>
)
get hasInstances() {
return this.state.instances.length > 0
}
renderPrefButtons = () => <>
<div className="buttons">
{!this.isNew && (
@ -240,6 +236,10 @@ class Client extends Component {
<div className="error">{this.state.error}</div>
</>
get hasInstances() {
return this.state.instances.length > 0
}
renderInstances = () => !this.isNew && (
<div className="instances">
<h3>{this.hasInstances ? "Instances" : "No instances :("}</h3>

View File

@ -14,8 +14,12 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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 }) => (
<NavLink className="plugin entry" to={`/plugin/${plugin.id}`}>
@ -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 && (
<div className="instances">
<h3>{this.hasInstances ? "Instances" : "No instances :("}</h3>
{this.state.instances.map(instance => (
<Link className="instance" key={instance.id} to={`/instance/${instance.id}`}>
{instance.id}
</Link>
))}
</div>
)
render() {
return <div>{this.props.id}</div>
return <div className="plugin">
<div className={`upload-box ${this.state.uploading ? "uploading" : ""}`}>
<UploadButton className="upload"/>
<input className="file-selector" type="file" accept="application/zip"
onChange={this.upload} disabled={this.state.uploading || this.state.deleting}
onDragEnter={evt => evt.target.parentElement.classList.add("drag")}
onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/>
{this.state.uploading && <Spinner/>}
</div>
{!this.isNew && <>
<PrefTable>
<PrefInput rowName="ID" type="text" value={this.state.id} disabled={true}/>
<PrefInput rowName="Version" type="text" value={this.state.version}
disabled={true}/>
</PrefTable>
<div className="buttons">
<button className={`delete ${this.hasInstances ? "disabled-bg" : ""}`}
onClick={this.delete} disabled={this.loading || this.hasInstances}
title={this.hasInstances ? "Can't delete plugin that is in use" : ""}>
{this.state.deleting ? <Spinner/> : "Delete"}
</button>
</div>
</>}
<div className="error">{this.state.error}</div>
{!this.isNew && this.renderInstances()}
</div>
}
}

View File

@ -46,6 +46,10 @@
&:hover:not(:disabled)
background-color: $primary-dark
&:disabled.disabled-bg
background-color: $background-dark !important
color: $text-color
.button
+button

View File

@ -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

View File

@ -51,6 +51,7 @@
border: none
height: 2rem
width: 100%
color: $text-color
box-sizing: border-box

View File

@ -15,15 +15,11 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
> 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

View File

@ -34,4 +34,5 @@
vertical-align: top
flex: 1
@import instances
> div.instances
+instancelist

View File

@ -77,7 +77,7 @@
div.error
+notification($error)
margin-top: 1rem
margin: 1rem .5rem
&:empty
display: none

View File

@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
> div.instances
=instancelist()
margin: 1rem 0
display: flex

View File

@ -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 <https://www.gnu.org/licenses/>.
=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

View File

@ -15,4 +15,41 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
> .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