Add client delete button and fix minor things

This commit is contained in:
Tulir Asokan 2018-11-10 00:03:36 +02:00
parent 29adf50ae0
commit 641ae49376
8 changed files with 162 additions and 66 deletions

View File

@ -177,13 +177,13 @@ class Client:
await self.stop()
async def update_displayname(self, displayname: str) -> None:
if not displayname or displayname == self.displayname:
if displayname is None or displayname == self.displayname:
return
self.db_instance.displayname = displayname
await self.client.set_displayname(self.displayname)
async def update_avatar_url(self, avatar_url: ContentURI) -> None:
if not avatar_url or avatar_url == self.avatar_url:
if avatar_url is None or avatar_url == self.avatar_url:
return
self.db_instance.avatar_url = avatar_url
await self.client.set_avatar_url(self.avatar_url)
@ -198,7 +198,7 @@ class Client:
client_session=self.http_client, log=self.log)
mxid = await new_client.whoami()
if mxid != self.id:
raise ValueError("MXID mismatch")
raise ValueError(f"MXID mismatch: {mxid}")
new_client.store = self.db_instance
self.stop_sync()
self.client = new_client

View File

@ -20,7 +20,7 @@ from http import HTTPStatus
from aiohttp import web
from mautrix.types import UserID, SyncToken, FilterID
from mautrix.errors import MatrixRequestError, MatrixInvalidToken
from mautrix.errors import MatrixRequestError, MatrixConnectionError, MatrixInvalidToken
from mautrix.client import Client as MatrixClient
from ...db import DBClient
@ -54,12 +54,14 @@ async def _create_client(user_id: Optional[UserID], data: dict) -> web.Response:
return resp.bad_client_access_token
except MatrixRequestError:
return resp.bad_client_access_details
except MatrixConnectionError:
return resp.bad_client_connection_details
if user_id is None:
existing_client = Client.get(mxid, None)
if existing_client is not None:
return resp.user_exists
elif mxid != user_id:
return resp.mxid_mismatch
return resp.mxid_mismatch(mxid)
db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token,
enabled=data.get("enabled", True), next_batch=SyncToken(""),
filter_id=FilterID(""), sync=data.get("sync", True),
@ -81,8 +83,8 @@ async def _update_client(client: Client, data: dict) -> web.Response:
return resp.bad_client_access_token
except MatrixRequestError:
return resp.bad_client_access_details
except ValueError:
return resp.mxid_mismatch
except ValueError as e:
return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):])
await client.update_avatar_url(data.get("avatar_url", None))
await client.update_displayname(data.get("displayname", None))
await client.update_started(data.get("started", None))

View File

@ -55,9 +55,16 @@ class _Response:
}, status=HTTPStatus.BAD_REQUEST)
@property
def mxid_mismatch(self) -> web.Response:
def bad_client_connection_details(self) -> web.Response:
return web.json_response({
"error": "The Matrix user ID of the client and the user ID of the access token don't match",
"error": "Could not connect to homeserver",
"errcode": "bad_client_connection_details"
}, status=HTTPStatus.BAD_REQUEST)
def mxid_mismatch(self, found: str) -> web.Response:
return web.json_response({
"error": "The Matrix user ID of the client and the user ID of the access token don't "
f"match. Access token is for user {found}",
"errcode": "mxid_mismatch",
}, status=HTTPStatus.BAD_REQUEST)

View File

@ -114,9 +114,22 @@ export async function putClient(client) {
return await resp.json()
}
export async function deleteClient(id) {
const resp = await fetch(`${BASE_PATH}/client/${id}`, {
headers: getHeaders(),
method: "DELETE",
})
if (resp.status === 204) {
return {
"success": true,
}
}
return await resp.json()
}
export default {
login, ping,
getInstances, getInstance,
getPlugins, getPlugin, uploadPlugin,
getClients, getClient, uploadAvatar, putClient,
getClients, getClient, uploadAvatar, putClient, deleteClient,
}

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/>.
import React, { Component } from "react"
import { Link } from "react-router-dom"
import { Link, withRouter } from "react-router-dom"
import { ReactComponent as ChevronRight } from "../../res/chevron-right.svg"
import { ReactComponent as UploadButton } from "../../res/upload.svg"
import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable"
@ -67,10 +67,24 @@ class Client extends Component {
uploadingAvatar: false,
saving: false,
deleting: false,
startingOrStopping: false,
deleted: false,
error: "",
}
}
get clientInState() {
const client = Object.assign({}, this.state)
delete client.uploadingAvatar
delete client.saving
delete client.deleting
delete client.startingOrStopping
delete client.deleted
delete client.error
return client
}
componentWillReceiveProps(nextProps) {
this.setState(Object.assign(this.initialState, nextProps.client))
}
@ -106,12 +120,30 @@ class Client extends Component {
save = async () => {
this.setState({ saving: true })
const resp = await api.putClient(this.state)
const resp = await api.putClient(this.clientInState)
if (resp.id) {
resp.saving = false
this.setState(resp)
this.props.onChange(resp)
if (this.isNew) {
this.props.history.push(`/client/${resp.id}`)
} else {
this.setState({ saving: false, error: "" })
}
} else {
console.error(resp)
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.deleteClient(this.state.id)
if (resp.success) {
this.props.onDelete()
this.props.history.push("/")
} else {
this.setState({ deleting: false, error: resp.error })
}
}
@ -122,42 +154,54 @@ class Client extends Component {
started: !this.state.started,
})
if (resp.id) {
resp.startingOrStopping = false
this.setState(resp)
this.props.onChange(resp)
this.setState({ startingOrStopping: false, error: "" })
} else {
console.error(resp)
this.setState({ startingOrStopping: false, error: resp.error })
}
}
get loading() {
return this.state.saving || this.state.startingOrStopping || this.state.deleting
}
get isNew() {
return !Boolean(this.props.client)
}
render() {
return <div className="client">
<div className="sidebar">
{!this.isNew && <div className="sidebar">
<div className={`avatar-container ${this.state.avatar_url ? "" : "no-avatar"}
${this.state.uploadingAvatar ? "uploading" : ""}`}>
<img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/>
<UploadButton className="upload"/>
<input className="file-selector" type="file" accept="image/png, image/jpeg"
onChange={this.avatarUpload} disabled={this.state.uploadingAvatar}/>
onChange={this.avatarUpload} disabled={this.state.uploadingAvatar}
onDragEnter={evt => evt.target.parentElement.classList.add("drag")}
onDragLeave={evt => evt.target.parentElement.classList.remove("drag")}/>
{this.state.uploadingAvatar && <Spinner/>}
</div>
{this.props.client && (<>
<div className="started-container">
<span className={`started ${this.state.started}`}/>
<span className="text">{this.state.started ? "Started" : "Stopped"}</span>
</div>
<button className="save" onClick={this.startOrStop}
disabled={this.state.saving || this.state.startingOrStopping}>
<div className="started-container">
<span className={`started ${this.props.client.started}
${this.props.client.enabled ? "" : "disabled"}`}/>
<span className="text">
{this.props.client.started ? "Started" :
(this.props.client.enabled ? "Stopped" : "Disabled")}
</span>
</div>
{(this.props.client.started || this.props.client.enabled) && (
<button className="save" onClick={this.startOrStop} disabled={this.loading}>
{this.state.startingOrStopping ? <Spinner/>
: (this.state.started ? "Stop" : "Start")}
: (this.props.client.started ? "Stop" : "Start")}
</button>
</>)}
</div>
)}
</div>}
<div className="info-container">
<PrefTable>
<PrefInput rowName="User ID" type="text" disabled={!!this.props.client}
name={this.props.client ? "" : "id"}
value={this.state.id} placeholder="@fancybot:example.com"
onChange={this.inputChange}/>
<PrefInput rowName="User ID" type="text" disabled={!this.isNew}
name={!this.isNew ? "" : "id"} value={this.state.id}
placeholder="@fancybot:example.com" onChange={this.inputChange}/>
<PrefInput rowName="Display name" type="text" name="displayname"
value={this.state.displayname} placeholder="My fancy bot"
onChange={this.inputChange}/>
@ -167,21 +211,33 @@ class Client extends Component {
<PrefInput rowName="Access token" type="text" name="access_token"
value={this.state.access_token} onChange={this.inputChange}
placeholder="MDAxYWxvY2F0aW9uIG1hdHJpeC5sb2NhbAowMDEzaWRlbnRpZmllc"/>
<PrefInput rowName="Avatar URL" type="text" name="avatar_url"
value={this.state.avatar_url} onChange={this.inputChange}
placeholder="mxc://example.com/mbmwyoTvPhEQPiCskcUsppko"/>
<PrefSwitch rowName="Sync" active={this.state.sync}
onToggle={sync => this.setState({ sync })}/>
<PrefSwitch rowName="Autojoin" active={this.state.autojoin}
onToggle={autojoin => this.setState({ autojoin })}/>
<PrefSwitch rowName="Enabled" active={this.state.enabled}
onToggle={enabled => this.setState({ enabled })}/>
onToggle={enabled => this.setState({
enabled,
started: enabled && this.state.started,
})}/>
</PrefTable>
<button className="save" onClick={this.save}
disabled={this.state.saving || this.state.startingOrStopping}>
{this.state.saving ? <Spinner/> : "Save"}
</button>
<div className="buttons">
{!this.isNew && (
<button className="delete" onClick={this.delete} disabled={this.loading}>
{this.state.deleting ? <Spinner/> : "Delete"}
</button>
)}
<button className="save" onClick={this.save} disabled={this.loading}>
{this.state.saving ? <Spinner/> : (this.isNew ? "Create" : "Save")}
</button>
</div>
<div className="error">{this.state.error}</div>
</div>
</div>
}
}
export default Client
export default withRouter(Client)

View File

@ -57,6 +57,18 @@ class Dashboard extends Component {
React.createElement(type, { key: entry.id, [field]: entry }))
}
delete(stateField, id) {
const data = Object.assign({}, this.state[stateField])
delete data[id]
this.setState({ [stateField]: data })
}
add(stateField, entry) {
const data = Object.assign({}, this.state[stateField])
data[entry.id] = entry
this.setState({ [stateField]: data })
}
renderView(field, type, id) {
const stateField = field + "s"
const entry = this.state[stateField][id]
@ -65,11 +77,8 @@ class Dashboard extends Component {
}
return React.createElement(type, {
[field]: entry,
onChange: newEntry => this.setState({
[stateField]: Object.assign({}, this.state[stateField], {
[id]: newEntry,
}),
}),
onDelete: () => this.delete(stateField, id),
onChange: newEntry => this.add(stateField, newEntry),
})
}
@ -111,7 +120,8 @@ class Dashboard extends Component {
<Switch>
<Route path="/" exact render={() => "Hello, World!"}/>
<Route path="/new/instance" render={() => <InstanceView/>}/>
<Route path="/new/client" render={() => <Client/>}/>
<Route path="/new/client" render={() => <Client
onChange={newEntry => this.add("clients", newEntry)}/>}/>
<Route path="/new/plugin" render={() => <PluginView/>}/>
<Route path="/instance/:id" render={({ match }) =>
this.renderView("instance", InstanceView, match.params.id)}/>

View File

@ -48,10 +48,13 @@
font-size: 1rem
border-bottom: 1px dotted $primary
border-bottom: 1px solid $background
&:hover:not(:disabled)
border-bottom: 1px solid $primary
&:not(:disabled)
border-bottom: 1px dotted $primary
&:focus:not(:disabled)
border-bottom: 2px solid $primary
&:hover
border-bottom: 1px solid $primary
&:focus
border-bottom: 2px solid $primary

View File

@ -21,6 +21,7 @@
> div.sidebar
vertical-align: top
text-align: center
width: 8rem
> div
margin-bottom: 1rem
@ -68,7 +69,7 @@
> input.file-selector
cursor: pointer
&:hover
&:hover, &.drag
> img.avatar
opacity: .25
@ -105,6 +106,10 @@
background-color: $error-light
box-shadow: 0 0 .75rem .75rem $error-light
&.disabled
background-color: $border-color
box-shadow: 0 0 .75rem .75rem $border-color
> span.text
display: inline-block
margin-left: 1rem
@ -115,7 +120,19 @@
margin: 0 1rem
flex: 1
button.save
> .buttons
display: flex
+button-group
> .error
margin-top: 1rem
+notification($error)
&:empty
display: none
button.save, button.delete
+button
+main-color-button
width: 100%
@ -126,15 +143,3 @@
+thick-spinner
+white-spinner
width: 2rem
//> .client
display: table
> .field
display: table-row
width: 100%
> .name, > .value
display: table-cell
width: 50%
text-align: center