Add client delete button and fix minor things
This commit is contained in:
parent
29adf50ae0
commit
641ae49376
8 changed files with 162 additions and 66 deletions
|
@ -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
|
||||||
|
|
|
@ -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))
|
||||||
|
|
|
@ -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)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)}/>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
Loading…
Reference in a new issue