Add plugin view
This commit is contained in:
		
							parent
							
								
									5220d2e5c9
								
							
						
					
					
						commit
						f97b39f4e3
					
				
					 12 changed files with 214 additions and 31 deletions
				
			
		|  | @ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue