Add plugin view
This commit is contained in:
parent
5220d2e5c9
commit
f97b39f4e3
@ -82,6 +82,7 @@ export const deleteInstance = id => defaultDelete("instance", id)
|
|||||||
|
|
||||||
export const getPlugins = () => defaultGet("/plugins")
|
export const getPlugins = () => defaultGet("/plugins")
|
||||||
export const getPlugin = id => defaultGet(`/plugin/${id}`)
|
export const getPlugin = id => defaultGet(`/plugin/${id}`)
|
||||||
|
export const deletePlugin = id => defaultDelete("plugin", id)
|
||||||
|
|
||||||
export async function uploadPlugin(data, id) {
|
export async function uploadPlugin(data, id) {
|
||||||
let resp
|
let resp
|
||||||
@ -124,6 +125,6 @@ export default {
|
|||||||
BASE_PATH,
|
BASE_PATH,
|
||||||
login, ping,
|
login, ping,
|
||||||
getInstances, getInstance, putInstance, deleteInstance,
|
getInstances, getInstance, putInstance, deleteInstance,
|
||||||
getPlugins, getPlugin, uploadPlugin,
|
getPlugins, getPlugin, uploadPlugin, deletePlugin,
|
||||||
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
|
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
|
||||||
}
|
}
|
||||||
|
@ -220,10 +220,6 @@ class Client extends Component {
|
|||||||
</PrefTable>
|
</PrefTable>
|
||||||
)
|
)
|
||||||
|
|
||||||
get hasInstances() {
|
|
||||||
return this.state.instances.length > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPrefButtons = () => <>
|
renderPrefButtons = () => <>
|
||||||
<div className="buttons">
|
<div className="buttons">
|
||||||
{!this.isNew && (
|
{!this.isNew && (
|
||||||
@ -240,6 +236,10 @@ class Client extends Component {
|
|||||||
<div className="error">{this.state.error}</div>
|
<div className="error">{this.state.error}</div>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
|
get hasInstances() {
|
||||||
|
return this.state.instances.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
renderInstances = () => !this.isNew && (
|
renderInstances = () => !this.isNew && (
|
||||||
<div className="instances">
|
<div className="instances">
|
||||||
<h3>{this.hasInstances ? "Instances" : "No instances :("}</h3>
|
<h3>{this.hasInstances ? "Instances" : "No instances :("}</h3>
|
||||||
|
@ -14,8 +14,12 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
import React, { Component } from "react"
|
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 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 }) => (
|
const PluginListEntry = ({ plugin }) => (
|
||||||
<NavLink className="plugin entry" to={`/plugin/${plugin.id}`}>
|
<NavLink className="plugin entry" to={`/plugin/${plugin.id}`}>
|
||||||
@ -28,8 +32,115 @@ const PluginListEntry = ({ plugin }) => (
|
|||||||
class Plugin extends Component {
|
class Plugin extends Component {
|
||||||
static ListEntry = PluginListEntry
|
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() {
|
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>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +46,10 @@
|
|||||||
&:hover:not(:disabled)
|
&:hover:not(:disabled)
|
||||||
background-color: $primary-dark
|
background-color: $primary-dark
|
||||||
|
|
||||||
|
&:disabled.disabled-bg
|
||||||
|
background-color: $background-dark !important
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
.button
|
.button
|
||||||
+button
|
+button
|
||||||
|
|
||||||
|
@ -22,5 +22,8 @@
|
|||||||
@import lib/preferencetable
|
@import lib/preferencetable
|
||||||
@import lib/switch
|
@import lib/switch
|
||||||
|
|
||||||
|
@import pages/mixins/upload-container
|
||||||
|
@import pages/mixins/instancelist
|
||||||
|
|
||||||
@import pages/login
|
@import pages/login
|
||||||
@import pages/dashboard
|
@import pages/dashboard
|
||||||
|
@ -51,6 +51,7 @@
|
|||||||
border: none
|
border: none
|
||||||
height: 2rem
|
height: 2rem
|
||||||
width: 100%
|
width: 100%
|
||||||
|
color: $text-color
|
||||||
|
|
||||||
box-sizing: border-box
|
box-sizing: border-box
|
||||||
|
|
||||||
|
@ -15,15 +15,11 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
> div.avatar-container
|
> div.avatar-container
|
||||||
position: relative
|
+upload-box
|
||||||
|
|
||||||
width: 8rem
|
width: 8rem
|
||||||
height: 8rem
|
height: 8rem
|
||||||
border-radius: 50%
|
border-radius: 50%
|
||||||
overflow: hidden
|
|
||||||
|
|
||||||
display: flex
|
|
||||||
align-items: center
|
|
||||||
justify-content: center
|
|
||||||
|
|
||||||
> img.avatar
|
> img.avatar
|
||||||
position: absolute
|
position: absolute
|
||||||
@ -33,30 +29,16 @@
|
|||||||
user-select: none
|
user-select: none
|
||||||
|
|
||||||
> svg.upload
|
> svg.upload
|
||||||
position: absolute
|
|
||||||
display: block
|
|
||||||
visibility: hidden
|
visibility: hidden
|
||||||
|
|
||||||
width: 6rem
|
width: 6rem
|
||||||
height: 6rem
|
height: 6rem
|
||||||
|
|
||||||
padding: 1rem
|
|
||||||
user-select: none
|
|
||||||
|
|
||||||
> input.file-selector
|
> input.file-selector
|
||||||
position: absolute
|
|
||||||
width: 8rem
|
width: 8rem
|
||||||
height: 8rem
|
height: 8rem
|
||||||
user-select: none
|
|
||||||
opacity: 0
|
|
||||||
|
|
||||||
> div.spinner
|
|
||||||
+thick-spinner
|
|
||||||
|
|
||||||
&:not(.uploading)
|
&:not(.uploading)
|
||||||
> input.file-selector
|
|
||||||
cursor: pointer
|
|
||||||
|
|
||||||
&:hover, &.drag
|
&:hover, &.drag
|
||||||
> img.avatar
|
> img.avatar
|
||||||
opacity: .25
|
opacity: .25
|
||||||
|
@ -34,4 +34,5 @@
|
|||||||
vertical-align: top
|
vertical-align: top
|
||||||
flex: 1
|
flex: 1
|
||||||
|
|
||||||
@import instances
|
> div.instances
|
||||||
|
+instancelist
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
div.error
|
div.error
|
||||||
+notification($error)
|
+notification($error)
|
||||||
margin-top: 1rem
|
margin: 1rem .5rem
|
||||||
|
|
||||||
&:empty
|
&:empty
|
||||||
display: none
|
display: none
|
||||||
|
@ -14,7 +14,7 @@
|
|||||||
// You should have received a copy of the GNU Affero General Public License
|
// 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/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
> div.instances
|
=instancelist()
|
||||||
margin: 1rem 0
|
margin: 1rem 0
|
||||||
|
|
||||||
display: flex
|
display: flex
|
@ -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
|
@ -15,4 +15,41 @@
|
|||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
> .plugin
|
> .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
|
||||||
|
Loading…
Reference in New Issue
Block a user