mirror of https://github.com/vector-im/riot-web
Support changing your integration manager in the UI
Part of https://github.com/vector-im/riot-web/issues/10161pull/21833/head
parent
e21c12c2c9
commit
03d735f4ed
|
@ -169,6 +169,7 @@
|
|||
@import "./views/settings/_PhoneNumbers.scss";
|
||||
@import "./views/settings/_ProfileSettings.scss";
|
||||
@import "./views/settings/_SetIdServer.scss";
|
||||
@import "./views/settings/_SetIntegrationManager.scss";
|
||||
@import "./views/settings/tabs/_SettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_GeneralRoomSettingsTab.scss";
|
||||
@import "./views/settings/tabs/room/_RolesRoomSettingsTab.scss";
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
.mx_SetIntegrationManager .mx_Field_input {
|
||||
margin-right: 100px; // Align with the other fields on the page
|
||||
}
|
||||
|
||||
.mx_SetIntegrationManager {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_SetIntegrationManager > .mx_SettingsTab_heading {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.mx_SetIntegrationManager > .mx_SettingsTab_heading > .mx_SettingsTab_subheading {
|
||||
display: inline-block;
|
||||
padding-left: 5px;
|
||||
font-size: 14px;
|
||||
}
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
Copyright 2019 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import {_t} from "../../../languageHandler";
|
||||
import sdk from '../../../index';
|
||||
import Field from "../elements/Field";
|
||||
import {IntegrationManagers} from "../../../integrations/IntegrationManagers";
|
||||
|
||||
export default class SetIntegrationManager extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
const currentManager = IntegrationManagers.sharedInstance().getPrimaryManager();
|
||||
|
||||
this.state = {
|
||||
currentManager,
|
||||
url: "", // user-entered text
|
||||
error: null,
|
||||
busy: false,
|
||||
};
|
||||
}
|
||||
|
||||
_onUrlChanged = (ev) => {
|
||||
const u = ev.target.value;
|
||||
this.setState({url: u});
|
||||
};
|
||||
|
||||
_getTooltip = () => {
|
||||
if (this.state.busy) {
|
||||
const InlineSpinner = sdk.getComponent('views.elements.InlineSpinner');
|
||||
return <div>
|
||||
<InlineSpinner />
|
||||
{ _t("Checking server") }
|
||||
</div>;
|
||||
} else if (this.state.error) {
|
||||
return this.state.error;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
_canChange = () => {
|
||||
return !!this.state.url && !this.state.busy;
|
||||
};
|
||||
|
||||
_setManager = async (ev) => {
|
||||
// Don't reload the page when the user hits enter in the form.
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
this.setState({busy: true});
|
||||
|
||||
const manager = await IntegrationManagers.sharedInstance().tryDiscoverManager(this.state.url);
|
||||
if (!manager) {
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: _t("Integration manager offline or not accessible."),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await IntegrationManagers.sharedInstance().overwriteManagerOnAccount(manager);
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: null,
|
||||
currentManager: IntegrationManagers.sharedInstance().getPrimaryManager(),
|
||||
url: "", // clear input
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
this.setState({
|
||||
busy: false,
|
||||
error: _t("Failed to update integration manager"),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const AccessibleButton = sdk.getComponent('views.elements.AccessibleButton');
|
||||
|
||||
const currentManager = this.state.currentManager;
|
||||
let managerName;
|
||||
let bodyText;
|
||||
if (currentManager) {
|
||||
managerName = `(${currentManager.name})`;
|
||||
bodyText = _t(
|
||||
"You are currently using <b>%(serverName)s</b> to manage your bots, widgets, " +
|
||||
"and sticker packs.",
|
||||
{serverName: currentManager.name},
|
||||
{ b: sub => <b>{sub}</b> },
|
||||
);
|
||||
} else {
|
||||
bodyText = _t(
|
||||
"Add which integration manager you want to manage your bots, widgets, " +
|
||||
"and sticker packs.",
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="mx_SettingsTab_section mx_SetIntegrationManager" onSubmit={this._setManager}>
|
||||
<div className="mx_SettingsTab_heading">
|
||||
<span>{_t("Integration Manager")}</span>
|
||||
<span className="mx_SettingsTab_subheading">{managerName}</span>
|
||||
</div>
|
||||
<span className="mx_SettingsTab_subsectionText">
|
||||
{bodyText}
|
||||
</span>
|
||||
<Field label={_t("Enter a new integration manager")}
|
||||
id="mx_SetIntegrationManager_newUrl"
|
||||
type="text" value={this.state.url} autoComplete="off"
|
||||
onChange={this._onUrlChanged}
|
||||
tooltip={this._getTooltip()}
|
||||
/>
|
||||
<AccessibleButton
|
||||
kind="primary_sm"
|
||||
type="submit"
|
||||
disabled={!this._canChange()}
|
||||
onClick={this._setManager}
|
||||
>{_t("Change")}</AccessibleButton>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -204,6 +204,17 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
);
|
||||
}
|
||||
|
||||
_renderIntegrationManagerSection() {
|
||||
const SetIntegrationManager = sdk.getComponent("views.settings.SetIntegrationManager");
|
||||
|
||||
return (
|
||||
<div className="mx_SettingsTab_section">
|
||||
{ /* has its own heading as it includes the current integration manager */ }
|
||||
<SetIntegrationManager />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="mx_SettingsTab">
|
||||
|
@ -214,6 +225,7 @@ export default class GeneralUserSettingsTab extends React.Component {
|
|||
{this._renderThemeSection()}
|
||||
<div className="mx_SettingsTab_heading">{_t("Discovery")}</div>
|
||||
{this._renderDiscoverySection()}
|
||||
{this._renderIntegrationManagerSection() /* Has its own title */}
|
||||
<div className="mx_SettingsTab_heading">{_t("Deactivate account")}</div>
|
||||
{this._renderManagementSection()}
|
||||
</div>
|
||||
|
|
|
@ -548,6 +548,13 @@
|
|||
"Identity Server": "Identity Server",
|
||||
"You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below": "You are not currently using an Identity Server. To discover and be discoverable by existing contacts you know, add one below",
|
||||
"Change": "Change",
|
||||
"Checking server": "Checking server",
|
||||
"Integration manager offline or not accessible.": "Integration manager offline or not accessible.",
|
||||
"Failed to update integration manager": "Failed to update integration manager",
|
||||
"You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.": "You are currently using <b>%(serverName)s</b> to manage your bots, widgets, and sticker packs.",
|
||||
"Add which integration manager you want to manage your bots, widgets, and sticker packs.": "Add which integration manager you want to manage your bots, widgets, and sticker packs.",
|
||||
"Integration Manager": "Integration Manager",
|
||||
"Enter a new integration manager": "Enter a new integration manager",
|
||||
"Flair": "Flair",
|
||||
"Failed to change password. Is your password correct?": "Failed to change password. Is your password correct?",
|
||||
"Success": "Success",
|
||||
|
|
|
@ -19,12 +19,18 @@ import sdk from "../index";
|
|||
import {dialogTermsInteractionCallback, TermsNotSignedError} from "../Terms";
|
||||
import type {Room} from "matrix-js-sdk";
|
||||
import Modal from '../Modal';
|
||||
import url from 'url';
|
||||
|
||||
export const KIND_ACCOUNT = "account";
|
||||
export const KIND_CONFIG = "config";
|
||||
|
||||
export class IntegrationManagerInstance {
|
||||
apiUrl: string;
|
||||
uiUrl: string;
|
||||
kind: string;
|
||||
|
||||
constructor(apiUrl: string, uiUrl: string) {
|
||||
constructor(kind: string, apiUrl: string, uiUrl: string) {
|
||||
this.kind = kind;
|
||||
this.apiUrl = apiUrl;
|
||||
this.uiUrl = uiUrl;
|
||||
|
||||
|
@ -32,6 +38,11 @@ export class IntegrationManagerInstance {
|
|||
if (!this.uiUrl) this.uiUrl = this.apiUrl;
|
||||
}
|
||||
|
||||
get name(): string {
|
||||
const parsed = url.parse(this.uiUrl);
|
||||
return parsed.hostname;
|
||||
}
|
||||
|
||||
getScalarClient(): ScalarAuthClient {
|
||||
return new ScalarAuthClient(this.apiUrl, this.uiUrl);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import SdkConfig from '../SdkConfig';
|
||||
import sdk from "../index";
|
||||
import Modal from '../Modal';
|
||||
import {IntegrationManagerInstance} from "./IntegrationManagerInstance";
|
||||
import {IntegrationManagerInstance, KIND_ACCOUNT, KIND_CONFIG} from "./IntegrationManagerInstance";
|
||||
import type {MatrixClient, MatrixEvent} from "matrix-js-sdk";
|
||||
import WidgetUtils from "../utils/WidgetUtils";
|
||||
import MatrixClientPeg from "../MatrixClientPeg";
|
||||
|
@ -62,7 +62,7 @@ export class IntegrationManagers {
|
|||
const uiUrl = SdkConfig.get()['integrations_ui_url'];
|
||||
|
||||
if (apiUrl && uiUrl) {
|
||||
this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl));
|
||||
this._managers.push(new IntegrationManagerInstance(KIND_CONFIG, apiUrl, uiUrl));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -77,7 +77,7 @@ export class IntegrationManagers {
|
|||
const apiUrl = data['api_url'];
|
||||
if (!apiUrl || !uiUrl) return;
|
||||
|
||||
this._managers.push(new IntegrationManagerInstance(apiUrl, uiUrl));
|
||||
this._managers.push(new IntegrationManagerInstance(KIND_ACCOUNT, apiUrl, uiUrl));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -107,6 +107,74 @@ export class IntegrationManagers {
|
|||
{configured: false}, 'mx_IntegrationsManager',
|
||||
);
|
||||
}
|
||||
|
||||
async overwriteManagerOnAccount(manager: IntegrationManagerInstance) {
|
||||
// TODO: TravisR - We should be logging out of scalar clients.
|
||||
await WidgetUtils.removeIntegrationManagerWidgets();
|
||||
|
||||
// TODO: TravisR - We should actually be carrying over the discovery response verbatim.
|
||||
await WidgetUtils.setUserWidget(
|
||||
"integration_manager_" + (new Date().getTime()),
|
||||
"m.integration_manager",
|
||||
manager.uiUrl,
|
||||
"Integration Manager",
|
||||
{"api_url": manager.apiUrl},
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to discover an integration manager using only its name.
|
||||
* @param {string} domainName The domain name to look up.
|
||||
* @returns {Promise<IntegrationManagerInstance>} Resolves to an integration manager instance,
|
||||
* or null if none was found.
|
||||
*/
|
||||
async tryDiscoverManager(domainName: string): IntegrationManagerInstance {
|
||||
console.log("Looking up integration manager via .well-known");
|
||||
if (domainName.startsWith("http:") || domainName.startsWith("https:")) {
|
||||
// trim off the scheme and just use the domain
|
||||
const url = url.parse(domainName);
|
||||
domainName = url.host;
|
||||
}
|
||||
|
||||
let wkConfig;
|
||||
try {
|
||||
const result = await fetch(`https://${domainName}/.well-known/matrix/integrations`);
|
||||
wkConfig = await result.json();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn("Failed to locate integration manager");
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!wkConfig || !wkConfig["m.integrations_widget"]) {
|
||||
console.warn("Missing integrations widget on .well-known response");
|
||||
return null;
|
||||
}
|
||||
|
||||
const widget = wkConfig["m.integrations_widget"];
|
||||
if (!widget["url"] || !widget["data"] || !widget["data"]["api_url"]) {
|
||||
console.warn("Malformed .well-known response for integrations widget");
|
||||
return null;
|
||||
}
|
||||
|
||||
// All discovered managers are per-user managers
|
||||
const manager = new IntegrationManagerInstance(KIND_ACCOUNT, widget["data"]["api_url"], widget["url"]);
|
||||
console.log("Got integration manager response, checking for responsiveness");
|
||||
|
||||
// Test the manager
|
||||
const client = manager.getScalarClient();
|
||||
try {
|
||||
// not throwing an error is a success here
|
||||
await client.connect();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
console.warn("Integration manager failed liveliness check");
|
||||
return null;
|
||||
}
|
||||
|
||||
console.log("Integration manager is alive and functioning");
|
||||
return manager;
|
||||
}
|
||||
}
|
||||
|
||||
// For debugging
|
||||
|
|
|
@ -351,6 +351,20 @@ export default class WidgetUtils {
|
|||
return widgets.filter(w => w.content && imTypes.includes(w.content.type));
|
||||
}
|
||||
|
||||
static removeIntegrationManagerWidgets() {
|
||||
const client = MatrixClientPeg.get();
|
||||
if (!client) {
|
||||
throw new Error('User not logged in');
|
||||
}
|
||||
const userWidgets = client.getAccountData('m.widgets').getContent() || {};
|
||||
Object.entries(userWidgets).forEach(([key, widget]) => {
|
||||
if (widget.content && widget.content.type === 'm.integration_manager') {
|
||||
delete userWidgets[key];
|
||||
}
|
||||
});
|
||||
return client.setAccountData('m.widgets', userWidgets);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all stickerpicker widgets (stickerpickers are user widgets by nature)
|
||||
* @return {Promise} Resolves on account data updated
|
||||
|
|
Loading…
Reference in New Issue