Add initial parts of client view

This commit is contained in:
Tulir Asokan 2018-11-09 00:54:42 +02:00
parent 3e661aa887
commit ed16ee8860
22 changed files with 425 additions and 72 deletions

View File

@ -0,0 +1,54 @@
// 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/>.
import React, { Component } from "react"
class Switch extends Component {
constructor(props) {
super(props)
this.state = {
active: props.active,
}
}
componentWillReceiveProps(nextProps) {
this.setState({
active: nextProps.active,
})
}
toggle = () => {
if (this.props.onToggle) {
this.props.onToggle(!this.state.active)
} else {
this.setState({ active: !this.state.active })
}
}
render() {
return (
<div className="switch" data-active={this.state.active} onClick={this.toggle}>
<div className="box">
<span className="text">
<span className="on">{this.props.onText || "On"}</span>
<span className="off">{this.props.offText || "Off"}</span>
</span>
</div>
</div>
)
}
}
export default Switch

View File

@ -1,30 +0,0 @@
// 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/>.
import React from "react"
import { Link } from "react-router-dom"
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg"
const ClientListEntry = ({ client }) => (
<Link className="client entry" to={`/client/${client.id}`}>
<img className="avatar"
src={`${client.homeserver}/_matrix/media/r0/download/${client.avatar_url.substr("mxc://".length)}`}
alt={client.id.substr(1, 1)}/>
<span className="displayname">{client.displayname || client.id}</span>
<ChevronRight/>
</Link>
)
export default ClientListEntry

View File

@ -16,6 +16,6 @@
import React from "react" import React from "react"
import ReactDOM from "react-dom" import ReactDOM from "react-dom"
import "./style/index.sass" import "./style/index.sass"
import App from "./MaubotRouter" import App from "./pages/Main"
ReactDOM.render(<App/>, document.getElementById("root")) ReactDOM.render(<App/>, document.getElementById("root"))

View File

@ -14,8 +14,8 @@
// 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 Spinner from "./components/Spinner" import Spinner from "../components/Spinner"
import api from "./api" import api from "../api"
class Login extends Component { class Login extends Component {
constructor(props, context) { constructor(props, context) {

View File

@ -15,13 +15,13 @@
// 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 { BrowserRouter as Router, Switch } from "react-router-dom" import { BrowserRouter as Router, Switch } from "react-router-dom"
import PrivateRoute from "./components/PrivateRoute" import PrivateRoute from "../components/PrivateRoute"
import Spinner from "../components/Spinner"
import api from "../api"
import Dashboard from "./dashboard" import Dashboard from "./dashboard"
import Login from "./Login" import Login from "./Login"
import Spinner from "./components/Spinner"
import api from "./api"
class MaubotRouter extends Component { class Main extends Component {
constructor(props) { constructor(props) {
super(props) super(props)
this.state = { this.state = {
@ -72,4 +72,4 @@ class MaubotRouter extends Component {
} }
} }
export default MaubotRouter export default Main

View File

@ -0,0 +1,114 @@
// 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/>.
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"
function getAvatarURL(client) {
const id = client.avatar_url.substr("mxc://".length)
return `${client.homeserver}/_matrix/media/r0/download/${id}`
}
const ClientListEntry = ({ client }) => {
const classes = ["client", "entry"]
if (!client.enabled) {
classes.push("disabled")
} else if (!client.started) {
classes.push("stopped")
}
return (
<Link className={classes.join(" ")} to={`/client/${client.id}`}>
<img className="avatar" src={getAvatarURL(client)} alt={client.id.substr(1, 1)}/>
<span className="displayname">{client.displayname || client.id}</span>
<ChevronRight/>
</Link>
)
}
class Client extends Component {
static ListEntry = ClientListEntry
constructor(props) {
super(props)
this.state = props
}
componentWillReceiveProps(nextProps) {
this.setState(nextProps)
}
inputChange = event => {
this.setState({ [event.target.name]: event.target.value })
}
render() {
return <div className="client">
<div className="avatar-container">
<img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/>
<UploadButton className="upload"/>
</div>
<div className="info-container">
<div className="row">
<div className="key">User ID</div>
<div className="value">
<input type="text" disabled value={this.props.id}
onChange={this.inputChange}/>
</div>
</div>
<div className="row">
<div className="key">Display name</div>
<div className="value">
<input type="text" name="displayname" value={this.state.displayname}
onChange={this.inputChange}/>
</div>
</div>
<div className="row">
<div className="key">Homeserver</div>
<div className="value">
<input type="text" name="homeserver" value={this.state.homeserver}
onChange={this.inputChange}/>
</div>
</div>
<div className="row">
<div className="key">Access token</div>
<div className="value">
<input type="text" name="access_token" value={this.state.access_token}
onChange={this.inputChange}/>
</div>
</div>
<div className="row">
<div className="key">Sync</div>
<div className="value">
<Switch active={this.state.sync}
onToggle={sync => this.setState({ sync })}/>
</div>
</div>
<div className="row">
<div className="key">Enabled</div>
<div className="value">
<Switch active={this.state.enabled}
onToggle={enabled => this.setState({ enabled })}/>
</div>
</div>
</div>
</div>
}
}
export default Client

View File

@ -15,12 +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/>.
import React, { Component } from "react" import React, { Component } from "react"
import { Route, Switch, Link } from "react-router-dom" import { Route, Switch, Link } from "react-router-dom"
import api from "../api" import api from "../../api"
import { ReactComponent as Plus } from "../res/plus.svg" import { ReactComponent as Plus } from "../../res/plus.svg"
import InstanceListEntry from "./instance/ListEntry" import InstanceListEntry from "./instance/ListEntry"
import InstanceView from "./instance/View" import InstanceView from "./instance/View"
import ClientListEntry from "./client/ListEntry" import Client from "./Client"
import ClientView from "./client/View"
import PluginListEntry from "./plugin/ListEntry" import PluginListEntry from "./plugin/ListEntry"
import PluginView from "./plugin/View" import PluginView from "./plugin/View"
@ -88,7 +87,7 @@ class Dashboard extends Component {
<h2>Clients</h2> <h2>Clients</h2>
<Link to="/new/client"><Plus/></Link> <Link to="/new/client"><Plus/></Link>
</div> </div>
{this.renderList("client", ClientListEntry)} {this.renderList("client", Client.ListEntry)}
</div> </div>
<div className="plugins list"> <div className="plugins list">
<div className="title"> <div className="title">
@ -98,16 +97,16 @@ class Dashboard extends Component {
{this.renderList("plugin", PluginListEntry)} {this.renderList("plugin", PluginListEntry)}
</div> </div>
</nav> </nav>
<main className="dashboard"> <main className="view">
<Switch> <Switch>
<Route path="/" exact render={() => "Hello, World!"}/> <Route path="/" exact render={() => "Hello, World!"}/>
<Route path="/new/instance" render={() => <InstanceView/>}/> <Route path="/new/instance" render={() => <InstanceView/>}/>
<Route path="/new/client" render={() => <ClientView/>}/> <Route path="/new/client" render={() => <Client/>}/>
<Route path="/new/plugin" render={() => <PluginView/>}/> <Route path="/new/plugin" render={() => <PluginView/>}/>
<Route path="/instance/:id" render={({ match }) => <Route path="/instance/:id" render={({ match }) =>
this.renderView("instance", InstanceView, match.params.id)}/> this.renderView("instance", InstanceView, match.params.id)}/>
<Route path="/client/:id" render={({ match }) => <Route path="/client/:id" render={({ match }) =>
this.renderView("client", ClientView, match.params.id)}/> this.renderView("client", Client, match.params.id)}/>
<Route path="/plugin/:id" render={({ match }) => <Route path="/plugin/:id" render={({ match }) =>
this.renderView("plugin", PluginView, match.params.id)}/> this.renderView("plugin", PluginView, match.params.id)}/>
<Route render={() => "Not found :("}/> <Route render={() => "Not found :("}/>

View File

@ -15,7 +15,7 @@
// 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 from "react" import React from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as ChevronRight } from "../../../res/chevron-right.svg"
const InstanceListEntry = ({ instance }) => ( const InstanceListEntry = ({ instance }) => (
<Link className="instance entry" to={`/instance/${instance.id}`}> <Link className="instance entry" to={`/instance/${instance.id}`}>

View File

@ -15,7 +15,7 @@
// 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 from "react" import React from "react"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg" import { ReactComponent as ChevronRight } from "../../../res/chevron-right.svg"
const PluginListEntry = ({ plugin }) => ( const PluginListEntry = ({ plugin }) => (
<Link className="plugin entry" to={`/plugin/${plugin.id}`}> <Link className="plugin entry" to={`/plugin/${plugin.id}`}>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24">
<path fill="#000000" d="M9,16V10H5L12,3L19,10H15V16H9M5,20V18H19V20H5Z" />
</svg>

After

Width:  |  Height:  |  Size: 365 B

View File

@ -18,7 +18,6 @@ body
margin: 0 margin: 0
padding: 0 padding: 0
font-size: 16px font-size: 16px
background-color: $background-color
#root #root
position: fixed position: fixed
@ -33,6 +32,10 @@ body
bottom: 0 bottom: 0
left: 0 left: 0
right: 0 right: 0
background-color: $background-dark
> *
background-color: $background
.maubot-loading .maubot-loading
margin-top: 10rem margin-top: 10rem

View File

@ -19,7 +19,7 @@
padding: $padding padding: $padding
width: $width width: $width
height: $height height: $height
background-color: $background-color background-color: $background
border: none border: none
border-radius: .25rem border-radius: .25rem
color: $inverted-text-color color: $inverted-text-color
@ -28,7 +28,7 @@
cursor: pointer cursor: pointer
&:hover &:hover
background-color: darken($background-color, 10%) background-color: darken($background, 10%)
=link-button() =link-button()
display: inline-block display: inline-block
@ -81,7 +81,7 @@
=input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem) =input($width: null, $height: null, $vertical-padding: .375rem, $horizontal-padding: 1rem, $font-size: 1rem)
font-family: $font-stack font-family: $font-stack
border: 1px solid $border-color border: 1px solid $border-color
background-color: $background-color background-color: $background
color: $text-color color: $text-color
width: $width width: $width
height: $height height: $height

View File

@ -13,6 +13,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/>.
$primary: #00C853 $primary: #00C853
$primary-dark: #009624 $primary-dark: #009624
$primary-light: #5EFC82 $primary-light: #5EFC82
@ -25,6 +26,7 @@ $error-light: #F05545
$border-color: #DDD $border-color: #DDD
$text-color: #212121 $text-color: #212121
$background-color: #FAFAFA $background: #FAFAFA
$inverted-text-color: $background-color $background-dark: #E7E7E7
$inverted-text-color: $background
$font-stack: sans-serif $font-stack: sans-serif

View File

@ -14,10 +14,10 @@
// 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 lib/spinner @import lib/spinner
@import base/vars @import base/vars
@import base/body @import base/body
@import base/elements @import base/elements
@import lib/switch
@import pages/login @import pages/login
@import pages/dashboard @import pages/dashboard

View File

@ -0,0 +1,79 @@
// 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/>.
.switch
display: flex
width: 100%
height: 2rem
cursor: pointer
border: 1px solid $primary
border-radius: .25rem
background-color: $background
box-sizing: border-box
> .box
box-sizing: border-box
width: 50%
height: 100%
transition: .5s
text-align: center
color: $inverted-text-color
border-radius: .15rem 0 0 .15rem
background-color: $primary
align-items: center
> .text
box-sizing: border-box
width: 100%
text-align: center
vertical-align: middle
color: $inverted-text-color
font-size: 1rem
user-select: none
.on
display: none
.off
display: inline
&[data-active=true]
> .box
transform: translateX(100%)
border-radius: 0 .15rem .15rem 0
background-color: $primary
.on
display: inline
.off
display: none

View File

@ -0,0 +1,109 @@
// 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/>.
> .client
margin: 1rem
div.avatar-container
position: relative
display: inline-block
width: 8rem
height: 8rem
border-radius: 100%
cursor: pointer
vertical-align: top
> 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%)
> svg.upload
position: absolute
display: block
visibility: hidden
width: 6rem
height: 6rem
padding: 1rem
&:hover
> img.avatar
opacity: .25
> svg.upload
visibility: visible
div.info-container
display: inline-table
vertical-align: top
margin: 1rem 2rem
> .row
display: table-row
> .key, > .value
display: table-cell
padding-bottom: .5rem
> .key
width: 6.5rem
> .value
margin: .5rem
> .value > .switch
width: auto
height: 2rem
> .value > input
border: none
height: 2rem
width: 100%
box-sizing: border-box
padding: .375rem 0
background-color: $background
font-size: 1rem
border-bottom: 1px solid transparent
&:hover:not(:disabled)
border-bottom: 1px solid $primary
&:focus:not(:disabled)
border-bottom: 2px solid $primary
//> .client
display: table
> .field
display: table-row
width: 100%
> .name, > .value
display: table-cell
width: 50%
text-align: center

View File

@ -18,6 +18,9 @@
.dashboard .dashboard
display: grid display: grid
height: 100% height: 100%
max-width: 60rem
margin: auto
box-shadow: 0 .5rem .5rem rgba(0, 0, 0, 0.5)
> a.title > a.title
grid-area: title grid-area: title
@ -31,9 +34,7 @@
color: $text-color color: $text-color
text-decoration: none text-decoration: none
z-index: 1 background-color: white
background-color: $background-color
border-right: 1px solid $primary border-right: 1px solid $primary
border-bottom: 1px solid $border-color border-bottom: 1px solid $border-color
@ -47,12 +48,14 @@
align-items: center align-items: center
justify-content: center justify-content: center
background-color: $primary background-color: $primary
width: 110% box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .2)
margin: 0 -5%
box-shadow: 0 .25rem .25rem rgba(0, 0, 0, .25)
@import "sidebar" @import "sidebar"
> main.dashboard > main.view
grid-area: main grid-area: main
@import "client"
@import "instance"
@import "plugin"

View File

@ -13,12 +13,6 @@
// //
// 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"
class ClientView extends Component { > .instance
render() { margin: 1rem
return <div>{this.props.displayname}</div>
}
}
export default ClientView

View File

@ -0,0 +1,18 @@
// 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/>.
> .plugin
margin: 1rem

View File

@ -16,13 +16,16 @@
> .sidebar > .sidebar
grid-area: sidebar grid-area: sidebar
background-color: $background-color background-color: white
border-right: 1px solid $border-color border-right: 1px solid $border-color
padding: .5rem padding: .5rem
overflow-y: auto
div.list div.list
margin-bottom: 1.5rem &:not(:last-of-type)
margin-bottom: 1.5rem
div.title div.title
h2 h2