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

View file

@ -20,7 +20,7 @@ from http import HTTPStatus
from aiohttp import web from aiohttp import web
from mautrix.types import UserID, SyncToken, FilterID 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 mautrix.client import Client as MatrixClient
from ...db import DBClient 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 return resp.bad_client_access_token
except MatrixRequestError: except MatrixRequestError:
return resp.bad_client_access_details return resp.bad_client_access_details
except MatrixConnectionError:
return resp.bad_client_connection_details
if user_id is None: if user_id is None:
existing_client = Client.get(mxid, None) existing_client = Client.get(mxid, None)
if existing_client is not None: if existing_client is not None:
return resp.user_exists return resp.user_exists
elif mxid != user_id: elif mxid != user_id:
return resp.mxid_mismatch return resp.mxid_mismatch(mxid)
db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token, db_instance = DBClient(id=mxid, homeserver=homeserver, access_token=access_token,
enabled=data.get("enabled", True), next_batch=SyncToken(""), enabled=data.get("enabled", True), next_batch=SyncToken(""),
filter_id=FilterID(""), sync=data.get("sync", True), 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 return resp.bad_client_access_token
except MatrixRequestError: except MatrixRequestError:
return resp.bad_client_access_details return resp.bad_client_access_details
except ValueError: except ValueError as e:
return resp.mxid_mismatch return resp.mxid_mismatch(str(e)[len("MXID mismatch: "):])
await client.update_avatar_url(data.get("avatar_url", None)) await client.update_avatar_url(data.get("avatar_url", None))
await client.update_displayname(data.get("displayname", None)) await client.update_displayname(data.get("displayname", None))
await client.update_started(data.get("started", None)) await client.update_started(data.get("started", None))

View file

@ -55,9 +55,16 @@ class _Response:
}, status=HTTPStatus.BAD_REQUEST) }, status=HTTPStatus.BAD_REQUEST)
@property @property
def mxid_mismatch(self) -> web.Response: def bad_client_connection_details(self) -> web.Response:
return web.json_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", "errcode": "mxid_mismatch",
}, status=HTTPStatus.BAD_REQUEST) }, status=HTTPStatus.BAD_REQUEST)

View file

@ -114,9 +114,22 @@ export async function putClient(client) {
return await resp.json() 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 { export default {
login, ping, login, ping,
getInstances, getInstance, getInstances, getInstance,
getPlugins, getPlugin, uploadPlugin, 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 // 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 { Link } from "react-router-dom" import { Link, withRouter } 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 { ReactComponent as UploadButton } from "../../res/upload.svg"
import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable" import { PrefTable, PrefSwitch, PrefInput } from "../../components/PreferenceTable"
@ -67,10 +67,24 @@ class Client extends Component {
uploadingAvatar: false, uploadingAvatar: false,
saving: false, saving: false,
deleting: false,
startingOrStopping: 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) { componentWillReceiveProps(nextProps) {
this.setState(Object.assign(this.initialState, nextProps.client)) this.setState(Object.assign(this.initialState, nextProps.client))
} }
@ -106,12 +120,30 @@ class Client extends Component {
save = async () => { save = async () => {
this.setState({ saving: true }) this.setState({ saving: true })
const resp = await api.putClient(this.state) const resp = await api.putClient(this.clientInState)
if (resp.id) { if (resp.id) {
resp.saving = false this.props.onChange(resp)
this.setState(resp) if (this.isNew) {
this.props.history.push(`/client/${resp.id}`)
} else {
this.setState({ saving: false, error: "" })
}
} else { } 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, started: !this.state.started,
}) })
if (resp.id) { if (resp.id) {
resp.startingOrStopping = false this.props.onChange(resp)
this.setState(resp) this.setState({ startingOrStopping: false, error: "" })
} else { } 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() { render() {
return <div className="client"> return <div className="client">
<div className="sidebar"> {!this.isNew && <div className="sidebar">
<div className={`avatar-container ${this.state.avatar_url ? "" : "no-avatar"} <div className={`avatar-container ${this.state.avatar_url ? "" : "no-avatar"}
${this.state.uploadingAvatar ? "uploading" : ""}`}> ${this.state.uploadingAvatar ? "uploading" : ""}`}>
<img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/> <img className="avatar" src={getAvatarURL(this.state)} alt="Avatar"/>
<UploadButton className="upload"/> <UploadButton className="upload"/>
<input className="file-selector" type="file" accept="image/png, image/jpeg" <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/>} {this.state.uploadingAvatar && <Spinner/>}
</div> </div>
{this.props.client && (<> <div className="started-container">
<div className="started-container"> <span className={`started ${this.props.client.started}
<span className={`started ${this.state.started}`}/> ${this.props.client.enabled ? "" : "disabled"}`}/>
<span className="text">{this.state.started ? "Started" : "Stopped"}</span> <span className="text">
</div> {this.props.client.started ? "Started" :
<button className="save" onClick={this.startOrStop} (this.props.client.enabled ? "Stopped" : "Disabled")}
disabled={this.state.saving || this.state.startingOrStopping}> </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.startingOrStopping ? <Spinner/>
: (this.state.started ? "Stop" : "Start")} : (this.props.client.started ? "Stop" : "Start")}
</button> </button>
</>)} )}
</div> </div>}
<div className="info-container"> <div className="info-container">
<PrefTable> <PrefTable>
<PrefInput rowName="User ID" type="text" disabled={!!this.props.client} <PrefInput rowName="User ID" type="text" disabled={!this.isNew}
name={this.props.client ? "" : "id"} name={!this.isNew ? "" : "id"} value={this.state.id}
value={this.state.id} placeholder="@fancybot:example.com" placeholder="@fancybot:example.com" onChange={this.inputChange}/>
onChange={this.inputChange}/>
<PrefInput rowName="Display name" type="text" name="displayname" <PrefInput rowName="Display name" type="text" name="displayname"
value={this.state.displayname} placeholder="My fancy bot" value={this.state.displayname} placeholder="My fancy bot"
onChange={this.inputChange}/> onChange={this.inputChange}/>
@ -167,21 +211,33 @@ class Client extends Component {
<PrefInput rowName="Access token" type="text" name="access_token" <PrefInput rowName="Access token" type="text" name="access_token"
value={this.state.access_token} onChange={this.inputChange} value={this.state.access_token} onChange={this.inputChange}
placeholder="MDAxYWxvY2F0aW9uIG1hdHJpeC5sb2NhbAowMDEzaWRlbnRpZmllc"/> 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} <PrefSwitch rowName="Sync" active={this.state.sync}
onToggle={sync => this.setState({ sync })}/> onToggle={sync => this.setState({ sync })}/>
<PrefSwitch rowName="Autojoin" active={this.state.autojoin} <PrefSwitch rowName="Autojoin" active={this.state.autojoin}
onToggle={autojoin => this.setState({ autojoin })}/> onToggle={autojoin => this.setState({ autojoin })}/>
<PrefSwitch rowName="Enabled" active={this.state.enabled} <PrefSwitch rowName="Enabled" active={this.state.enabled}
onToggle={enabled => this.setState({ enabled })}/> onToggle={enabled => this.setState({
enabled,
started: enabled && this.state.started,
})}/>
</PrefTable> </PrefTable>
<div className="buttons">
<button className="save" onClick={this.save} {!this.isNew && (
disabled={this.state.saving || this.state.startingOrStopping}> <button className="delete" onClick={this.delete} disabled={this.loading}>
{this.state.saving ? <Spinner/> : "Save"} {this.state.deleting ? <Spinner/> : "Delete"}
</button> </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>
</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 })) 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) { renderView(field, type, id) {
const stateField = field + "s" const stateField = field + "s"
const entry = this.state[stateField][id] const entry = this.state[stateField][id]
@ -65,11 +77,8 @@ class Dashboard extends Component {
} }
return React.createElement(type, { return React.createElement(type, {
[field]: entry, [field]: entry,
onChange: newEntry => this.setState({ onDelete: () => this.delete(stateField, id),
[stateField]: Object.assign({}, this.state[stateField], { onChange: newEntry => this.add(stateField, newEntry),
[id]: newEntry,
}),
}),
}) })
} }
@ -111,7 +120,8 @@ class Dashboard extends Component {
<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={() => <Client/>}/> <Route path="/new/client" render={() => <Client
onChange={newEntry => this.add("clients", newEntry)}/>}/>
<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)}/>

View file

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

View file

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