Add fancier server selector

This commit is contained in:
Tulir Asokan 2019-07-16 03:12:13 +03:00
parent 924f627c58
commit e5e614fc4b
6 changed files with 73 additions and 18 deletions

View File

@ -47,7 +47,7 @@ def generate_mac(secret: str, nonce: str, user: str, password: str, admin: bool
@routes.get("/client/auth/servers") @routes.get("/client/auth/servers")
async def get_registerable_servers(_: web.Request) -> web.Response: async def get_registerable_servers(_: web.Request) -> web.Response:
return web.json_response(list(registration_secrets().keys())) return web.json_response({key: value["url"] for key, value in registration_secrets().items()})
AuthRequestInfo = NamedTuple("AuthRequestInfo", api=HTTPAPI, secret=str, username=str, password=str) AuthRequestInfo = NamedTuple("AuthRequestInfo", api=HTTPAPI, secret=str, username=str, password=str)

View File

@ -410,13 +410,14 @@ paths:
content: content:
application/json: application/json:
schema: schema:
type: array type: object
items: description: Key-value map from server name to homeserver URL
additionalProperties:
type: string type: string
description: The homeserver URL
example: example:
- maunium.net maunium.net: https://maunium.net
- example.com example.com: https://matrix.example.org
- matrix.org
401: 401:
$ref: '#/components/responses/Unauthorized' $ref: '#/components/responses/Unauthorized'
'/client/auth/{server}/register': '/client/auth/{server}/register':

View File

@ -36,8 +36,8 @@ async function defaultDelete(type, id) {
return await resp.json() return await resp.json()
} }
async function defaultPut(type, entry, id = undefined) { async function defaultPut(type, entry, id = undefined, suffix = undefined) {
const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}`, { const resp = await fetch(`${BASE_PATH}/${type}/${id || entry.id}${suffix}`, {
headers: getHeaders(), headers: getHeaders(),
body: JSON.stringify(entry), body: JSON.stringify(entry),
method: "PUT", method: "PUT",
@ -221,6 +221,17 @@ export function getAvatarURL({ id, avatar_url }) {
export const putClient = client => defaultPut("client", client) export const putClient = client => defaultPut("client", client)
export const deleteClient = id => defaultDelete("client", id) export const deleteClient = id => defaultDelete("client", id)
export const getClientAuthServers = () => defaultGet("/client/auth/servers")
export async function doClientAuth(server, type, username, password) {
const resp = await fetch(`${BASE_PATH}/client/auth/${server}/${type}`, {
headers: getHeaders(),
body: JSON.stringify({ username, password }),
method: "POST",
})
return await resp.json()
}
export default { export default {
BASE_PATH, BASE_PATH,
login, ping, getFeatures, remoteGetFeatures, login, ping, getFeatures, remoteGetFeatures,
@ -230,4 +241,5 @@ export default {
getInstanceDatabase, queryInstanceDatabase, getInstanceDatabase, queryInstanceDatabase,
getPlugins, getPlugin, uploadPlugin, deletePlugin, getPlugins, getPlugin, uploadPlugin, deletePlugin,
getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient, getClients, getClient, uploadAvatar, getAvatarURL, putClient, deleteClient,
getClientAuthServers, doClientAuth
} }

View File

@ -15,6 +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 Select from "react-select" import Select from "react-select"
import CreatableSelect from "react-select/creatable"
import Switch from "./Switch" import Switch from "./Switch"
export const PrefTable = ({ children, wrapperClass }) => { export const PrefTable = ({ children, wrapperClass }) => {
@ -56,10 +57,12 @@ export const PrefSwitch = ({ rowName, active, origActive, fullWidth = false, ...
</PrefRow> </PrefRow>
) )
export const PrefSelect = ({ rowName, value, origValue, fullWidth = false, ...args }) => ( export const PrefSelect = ({ rowName, value, origValue, fullWidth = false, creatable = false, ...args }) => (
<PrefRow name={rowName} fullWidth={fullWidth} labelFor={rowName} <PrefRow name={rowName} fullWidth={fullWidth} labelFor={rowName}
changed={origValue !== undefined && value.id !== origValue}> changed={origValue !== undefined && value.id !== origValue}>
<Select className="select" {...args} id={rowName} value={value}/> {creatable
? <CreatableSelect className="select" {...args} id={rowName} value={value}/>
: <Select className="select" {...args} id={rowName} value={value}/>}
</PrefRow> </PrefRow>
) )

View File

@ -17,7 +17,7 @@ import React from "react"
import { NavLink, withRouter } from "react-router-dom" import { NavLink, 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, PrefSelect } from "../../components/PreferenceTable"
import Spinner from "../../components/Spinner" import Spinner from "../../components/Spinner"
import api from "../../api" import api from "../../api"
import BaseMainView from "./BaseMainView" import BaseMainView from "./BaseMainView"
@ -48,7 +48,7 @@ class Client extends BaseMainView {
get entryKeys() { get entryKeys() {
return ["id", "displayname", "homeserver", "avatar_url", "access_token", "sync", return ["id", "displayname", "homeserver", "avatar_url", "access_token", "sync",
"autojoin", "enabled", "started"] "autojoin", "enabled", "started"]
} }
get initialState() { get initialState() {
@ -84,6 +84,36 @@ class Client extends BaseMainView {
return client return client
} }
get selectedHomeserver() {
return this.state.homeserver
? this.homeserverEntry([this.props.ctx.homeserversByURL[this.state.homeserver],
this.state.homeserver])
: {}
}
homeserverEntry = ([serverName, serverURL]) => serverURL && {
id: serverURL,
value: serverURL,
label: serverName || serverURL,
}
componentWillReceiveProps(nextProps) {
super.componentWillReceiveProps(nextProps)
this.updateHomeserverOptions()
}
updateHomeserverOptions() {
this.homeserverOptions = Object.entries(this.props.ctx.homeserversByName).map(this.homeserverEntry)
}
isValidHomeserver(value) {
try {
return Boolean(new URL(value))
} catch (err) {
return false
}
}
avatarUpload = async event => { avatarUpload = async event => {
const file = event.target.files[0] const file = event.target.files[0]
this.setState({ this.setState({
@ -165,9 +195,10 @@ class Client extends BaseMainView {
name={this.isNew ? "id" : ""} className="id" name={this.isNew ? "id" : ""} className="id"
value={this.state.id} origValue={this.props.entry.id} value={this.state.id} origValue={this.props.entry.id}
placeholder="@fancybot:example.com" onChange={this.inputChange}/> placeholder="@fancybot:example.com" onChange={this.inputChange}/>
<PrefInput rowName="Homeserver" type="text" name="homeserver" <PrefSelect rowName="Homeserver" options={this.homeserverOptions} isSearchable={true}
value={this.state.homeserver} origValue={this.props.entry.homeserver} value={this.selectedHomeserver} origValue={this.props.entry.homeserver}
placeholder="https://example.com" onChange={this.inputChange}/> onChange={({ id }) => this.setState({ homeserver: id })}
creatable={true} isValidNewOption={this.isValidHomeserver}/>
<PrefInput rowName="Access token" type="text" name="access_token" <PrefInput rowName="Access token" type="text" name="access_token"
value={this.state.access_token} origValue={this.props.entry.access_token} value={this.state.access_token} origValue={this.props.entry.access_token}
placeholder="MDAxYWxvY2F0aW9uIG1hdHJpeC5sb2NhbAowMDEzaWRlbnRpZmllc" placeholder="MDAxYWxvY2F0aW9uIG1hdHJpeC5sb2NhbAowMDEzaWRlbnRpZmllc"

View File

@ -31,6 +31,8 @@ class Dashboard extends Component {
instances: {}, instances: {},
clients: {}, clients: {},
plugins: {}, plugins: {},
homeserversByName: {},
homeserversByURL: {},
sidebarOpen: false, sidebarOpen: false,
modalOpen: false, modalOpen: false,
logFocus: null, logFocus: null,
@ -50,8 +52,8 @@ class Dashboard extends Component {
} }
async componentWillMount() { async componentWillMount() {
const [instanceList, clientList, pluginList] = await Promise.all([ const [instanceList, clientList, pluginList, homeservers] = await Promise.all([
api.getInstances(), api.getClients(), api.getPlugins(), api.getInstances(), api.getClients(), api.getPlugins(), api.getClientAuthServers(),
api.updateDebugOpenFileEnabled()]) api.updateDebugOpenFileEnabled()])
const instances = {} const instances = {}
if (api.getFeatures().instance) { if (api.getFeatures().instance) {
@ -71,7 +73,13 @@ class Dashboard extends Component {
plugins[plugin.id] = plugin plugins[plugin.id] = plugin
} }
} }
this.setState({ instances, clients, plugins }) const homeserversByName = homeservers
const homeserversByURL = {}
for (const [key, value] of Object.entries(homeservers)) {
homeserversByURL[value] = key
}
console.log(homeserversByName, homeserversByURL)
this.setState({ instances, clients, plugins, homeserversByName, homeserversByURL })
await this.enableLogs() await this.enableLogs()
} }