diff --git a/maubot/client.py b/maubot/client.py
index ea7db0d..5b91d8c 100644
--- a/maubot/client.py
+++ b/maubot/client.py
@@ -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
diff --git a/maubot/management/api/client.py b/maubot/management/api/client.py
index 051f9be..5bee0af 100644
--- a/maubot/management/api/client.py
+++ b/maubot/management/api/client.py
@@ -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))
diff --git a/maubot/management/api/responses.py b/maubot/management/api/responses.py
index be9b3b0..5204927 100644
--- a/maubot/management/api/responses.py
+++ b/maubot/management/api/responses.py
@@ -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)
diff --git a/maubot/management/frontend/src/api.js b/maubot/management/frontend/src/api.js
index 8721685..e9fc926 100644
--- a/maubot/management/frontend/src/api.js
+++ b/maubot/management/frontend/src/api.js
@@ -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,
}
diff --git a/maubot/management/frontend/src/pages/dashboard/Client.js b/maubot/management/frontend/src/pages/dashboard/Client.js
index 2019444..9b7dbd1 100644
--- a/maubot/management/frontend/src/pages/dashboard/Client.js
+++ b/maubot/management/frontend/src/pages/dashboard/Client.js
@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
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
-
+ {!this.isNew &&
- {this.props.client && (<>
-
-
- {this.state.started ? "Started" : "Stopped"}
-
-
+ )}
+
}
-
+
@@ -167,21 +211,33 @@ class Client extends Component {
+
this.setState({ sync })}/>
this.setState({ autojoin })}/>
this.setState({ enabled })}/>
+ onToggle={enabled => this.setState({
+ enabled,
+ started: enabled && this.state.started,
+ })}/>
-
-
- {this.state.saving ? : "Save"}
-
+
+ {!this.isNew && (
+
+ {this.state.deleting ? : "Delete"}
+
+ )}
+
+ {this.state.saving ? : (this.isNew ? "Create" : "Save")}
+
+
+
{this.state.error}
}
}
-export default Client
+export default withRouter(Client)
diff --git a/maubot/management/frontend/src/pages/dashboard/index.js b/maubot/management/frontend/src/pages/dashboard/index.js
index 5d9a38d..2ca434f 100644
--- a/maubot/management/frontend/src/pages/dashboard/index.js
+++ b/maubot/management/frontend/src/pages/dashboard/index.js
@@ -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 {
"Hello, World!"}/>
}/>
- }/>
+ this.add("clients", newEntry)}/>}/>
}/>
this.renderView("instance", InstanceView, match.params.id)}/>
diff --git a/maubot/management/frontend/src/style/lib/preferencetable.sass b/maubot/management/frontend/src/style/lib/preferencetable.sass
index 59e6a4a..28ea689 100644
--- a/maubot/management/frontend/src/style/lib/preferencetable.sass
+++ b/maubot/management/frontend/src/style/lib/preferencetable.sass
@@ -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
diff --git a/maubot/management/frontend/src/style/pages/client.sass b/maubot/management/frontend/src/style/pages/client.sass
index bfb41bc..e0ffb66 100644
--- a/maubot/management/frontend/src/style/pages/client.sass
+++ b/maubot/management/frontend/src/style/pages/client.sass
@@ -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