Remove the client side filtering from the room dir

This removes the ability for the client to filter remote room
directories by network, since the /thirdparty/protocols API does
not yet work for remote servers. They would only get the main list
now though anyway, so there is no point in us continuing to support
it.
This commit is contained in:
David Baker 2016-12-16 14:17:13 +00:00
parent fb08910db3
commit 994bc9279f
5 changed files with 134 additions and 355 deletions

View file

@ -81,33 +81,9 @@ You can configure the app by copying `config.sample.json` to
and https://vector.im. In future identity servers will be decentralised.
1. `integrations_ui_url`: URL to the web interface for the integrations server.
1. `integrations_rest_url`: URL to the REST interface for the integrations server.
1. `roomDirectory`: config for the public room directory. This section encodes behaviour
on the room directory screen for filtering the list by server / network type and joining
third party networks. This config section will disappear once APIs are available to
get this information for home servers. This section is optional.
1. `roomDirectory`: config for the public room directory. This section is optional.
1. `roomDirectory.servers`: List of other Home Servers' directories to include in the drop
down list. Optional.
1. `roomDirectory.serverConfig`: Config for each server in `roomDirectory.servers`. Optional.
1. `roomDirectory.serverConfig.<server_name>.networks`: List of networks (named
in `roomDirectory.networks`) to include for this server. Optional. If set, this will
override any networks sent by the Home Server (eg. if ASes are configured).
1. `roomDirectory.networks`: config for each network type. Optional.
1. `roomDirectory.<network_type>.name`: Human-readable name for the network. Required.
1. `roomDirectory.<network_type>.protocol`: Protocol as given by the server in
`/_matrix/client/unstable/thirdparty/protocols` response. Required to be able to join
this type of third party network.
1. `roomDirectory.<network_type>.domain`: Domain as given by the server in
`/_matrix/client/unstable/thirdparty/protocols` response, if present. Required to be
able to join this type of third party network, if present in `thirdparty/protocols`.
1. `roomDirectory.<network_type>.portalRoomPattern`: Regular expression matching aliases
for portal rooms to locations on this network. Required.
1. `roomDirectory.<network_type>.icon`: URL to an icon to be displayed for this network. Required.
1. `roomDirectory.<network_type>.example`: Textual example of a location on this network,
eg. '#channel' for an IRC network. Optional.
1. `roomDirectory.<network_type>.nativePattern`: Regular expression that matches a
valid location on this network. This is used as a hint to the user to indicate
when a valid location has been entered so it's not necessary for this to be
exactly correct. Optional.
1. `update_base_url` (electron app only): HTTPS URL to a web server to download
updates from. This should be the path to the directory containing `macos`
and `win32` (for update packages, not installer packages).

View file

@ -8,64 +8,6 @@
"roomDirectory": {
"servers": [
"matrix.org"
],
"serverConfig": {
"matrix.org": {
"networks": [
"_matrix",
"gitter",
"irc:freenode",
"irc:mozilla",
"irc:snoonet",
"irc:oftc"
]
}
},
"networks": {
"gitter": {
"protocol": "gitter",
"portalRoomPattern": "#gitter_.*:matrix.org",
"name": "Gitter",
"icon": "//gitter.im/favicon.ico",
"example": "org/community",
"nativePattern": "[^\\s]+/[^\\s]+$"
},
"irc:freenode": {
"protocol": "irc",
"domain": "chat.freenode.net",
"portalRoomPattern": "#freenode_.*:matrix.org",
"name": "Freenode",
"icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
"example": "#channel",
"nativePattern": "^#[^\\s]+$"
},
"irc:mozilla": {
"protocol": "irc",
"domain": "irc.mozilla.org",
"portalRoomPattern": "#mozilla_.*:matrix.org",
"name": "Mozilla",
"icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
"example": "#channel",
"nativePattern": "^#[^\\s]+$"
},
"irc:snoonet": {
"protocol": "irc",
"domain": "ipv6-irc.snoonet.org",
"portalRoomPattern": "#_snoonet_.*:matrix.org",
"name": "Snoonet",
"icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
"example": "#channel",
"nativePattern": "^#[^\\s]+$"
},
"irc:oftc": {
"protocol": "irc",
"domain": "irc.oftc.net",
"portalRoomPattern": "#_oftc_.*:matrix.org",
"name": "OFTC",
"icon": "//matrix.org/_matrix/media/v1/download/matrix.org/DHLHpDDgWNNejFmrewvwEAHX",
"example": "#channel",
"nativePattern": "^#[^\\s]+$"
}
}
]
}
}

View file

@ -31,6 +31,8 @@ var linkifyMatrix = require('matrix-react-sdk/lib/linkify-matrix');
var sanitizeHtml = require('sanitize-html');
var q = require('q');
import {instanceForInstanceId, protocolNameForInstanceId} from '../../utils/DirectoryUtils';
linkifyMatrix(linkify);
module.exports = React.createClass({
@ -42,9 +44,7 @@ module.exports = React.createClass({
getDefaultProps: function() {
return {
config: {
networks: [],
},
config: {},
}
},
@ -52,37 +52,26 @@ module.exports = React.createClass({
return {
publicRooms: [],
loading: true,
network: null,
instance_id: null,
protocolsLoading: true,
instanceId: null,
includeAll: false,
roomServer: null,
filterString: null,
}
},
componentWillMount: function() {
// precompile Regexps
this.portalRoomPatterns = {};
this.nativePatterns = {};
if (this.props.config.networks) {
for (const network of Object.keys(this.props.config.networks)) {
const network_info = this.props.config.networks[network];
if (network_info.portalRoomPattern) {
this.portalRoomPatterns[network] = new RegExp(network_info.portalRoomPattern);
}
if (network_info.nativePattern) {
this.nativePatterns[network] = new RegExp(network_info.nativePattern);
}
}
}
this.nextBatch = null;
this.filterTimeout = null;
this.scrollPanel = null;
this.protocols = null;
this.setState({protocolsLoading: true});
MatrixClientPeg.get().getThirdpartyProtocols().done((response) => {
this.protocols = response;
this.setState({protocolsLoading: false});
}, (err) => {
this.setState({protocolsLoading: false});
if (MatrixClientPeg.get().isGuest()) {
// Guests currently aren't allowed to use this API, so
// ignore this as otherwise this error is literally the
@ -132,9 +121,9 @@ module.exports = React.createClass({
if (my_server != MatrixClientPeg.getHomeServerName()) {
opts.server = my_server;
}
if (this.state.instance_id) {
opts.third_party_instance_id = this.state.instance_id;
} else if (this.state.network !== '_matrix') {
if (this.state.instanceId) {
opts.third_party_instanceId = this.state.instanceId;
} else if (this.state.includeAll) {
opts.include_all_networks = true;
}
if (this.nextBatch) opts.since = this.nextBatch;
@ -237,7 +226,7 @@ module.exports = React.createClass({
}
},
onOptionChange: function(server, network, instance_id) {
onOptionChange: function(server, instanceId, includeAll) {
// clear next batch so we don't try to load more rooms
this.nextBatch = null;
this.setState({
@ -246,8 +235,8 @@ module.exports = React.createClass({
// to clear the list anyway.
publicRooms: [],
roomServer: server,
network: network,
instance_id: instance_id,
instanceId: instanceId,
includeAll: includeAll,
}, this.refreshRoomList);
// We also refresh the room list each time even though this
// filtering is client-side. It hopefully won't be client side
@ -278,7 +267,7 @@ module.exports = React.createClass({
this.filterTimeout = setTimeout(() => {
this.filterTimeout = null;
this.refreshRoomList();
}, 300);
}, 700);
},
onFilterClear: function() {
@ -295,12 +284,19 @@ module.exports = React.createClass({
onJoinClick: function(alias) {
// If we're on the 'Matrix' network (or all networks),
// just show that rooms alias
if (this.state.network == null || this.state.network == '_matrix') {
if (!this.state.instanceId) {
// If the user specified an alias without a domain, add on whichever server is selected
// in the dropdown
if (alias.indexOf(':') == -1) {
alias = alias + ':' + this.state.roomServer;
}
this.showRoomAlias(alias);
} else {
// This is a 3rd party protocol. Let's see if we
// can join it
const fields = this._getFieldsForThirdPartyLocation(alias, this.state.network);
const protocol_name = protocolNameForInstanceId(this.protocols, this.state.instanceId);
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
const fields = this._getFieldsForThirdPartyLocation(alias, this.protocols[protocol_name], instance);
if (!fields) {
const ErrorDialog = sdk.getComponent("dialogs.ErrorDialog");
Modal.createDialog(ErrorDialog, {
@ -309,8 +305,7 @@ module.exports = React.createClass({
});
return;
}
const protocol = this._protocolForThirdPartyNetwork(this.state.network);
MatrixClientPeg.get().getThirdpartyLocation(protocol, fields).done((resp) => {
MatrixClientPeg.get().getThirdpartyLocation(protocol_name, fields).done((resp) => {
if (resp.length > 0 && resp[0].alias) {
this.showRoomAlias(resp[0].alias);
} else {
@ -379,13 +374,7 @@ module.exports = React.createClass({
if (!this.state.publicRooms) return [];
var rooms = this.state.publicRooms.filter((a) => {
if (this.state.network) {
if (!this._isRoomInNetwork(a, this.state.roomServer, this.state.network)) return false;
}
return true;
});
var rooms = this.state.publicRooms;
var rows = [];
var self = this;
var guestRead, guestJoin, perms;
@ -447,119 +436,46 @@ module.exports = React.createClass({
this.scrollPanel = element;
},
/**
* Terrible temporary function that guess what network a public room
* entry is in, until synapse is able to tell us
*/
_isRoomInNetwork: function(room, server, network) {
// We carve rooms into two categories here. 'portal' rooms are
// rooms created by a user joining a bridge 'portal' alias to
// participate in that room or a foreign network. A room is a
// portal room if it has exactly one alias and that alias matches
// a pattern defined in the config. Its network is the key
// of the pattern that it matches.
// All other rooms are considered 'native matrix' rooms, and
// go into the special '_matrix' network.
let roomNetwork = '_matrix';
if (room.aliases && room.aliases.length == 1) {
if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
for (const n of this.props.config.serverConfig[server].networks) {
const pat = this.portalRoomPatterns[n];
if (pat && pat.test(room.aliases[0])) {
roomNetwork = n;
}
}
}
}
return roomNetwork == network;
},
_stringLooksLikeId: function(s, network) {
_stringLooksLikeId: function(s, field_type) {
let pat = /^#[^\s]+:[^\s]/;
if (
network && network != '_matrix' &&
this.nativePatterns[network]
) {
pat = this.nativePatterns[network];
if (field_type && field_type.regexp) {
pat = new RegExp(field_type.regexp);
}
return pat.test(s);
},
_protocolForThirdPartyNetwork: function(network) {
if (
this.props.config.networks &&
this.props.config.networks[network] &&
this.props.config.networks[network].protocol
) {
return this.props.config.networks[network].protocol;
}
},
_getFieldsForThirdPartyLocation: function(user_input, network) {
if (!this.props.config.networks || !this.props.config.networks[network]) return null;
const network_info = this.props.config.networks[network];
if (!network_info.protocol) return null;
if (!this.protocols) return null;
let matched_instance;
// Try to find which instance in the 'protocols' response
// matches this network. We look for a matching protocol
// and the existence of a 'domain' field and if present,
// its value.
if (
this.protocols[network_info.protocol] &&
this.protocols[network_info.protocol].instances &&
this.protocols[network_info.protocol].instances.length == 1
) {
const the_instance = this.protocols[network_info.protocol].instances[0];
// If there's only one instance in this protocol, use it
// as long as it has no domain (which we assume to mean it's
// there is only one possible instance).
if (
(
the_instance.fields.domain === undefined &&
network_info.domain === undefined
) ||
(
the_instance.fields.domain !== undefined &&
the_instance.fields.domain == network_info.domain
)
) {
matched_instance = the_instance;
}
} else if (network_info.domain) {
// otherwise, we look for one with a matching domain.
for (const this_instance of this.protocols[network_info.protocol].instances) {
if (this_instance.fields.domain == network_info.domain) {
matched_instance = this_instance;
}
}
}
if (matched_instance === undefined) return null;
// now make an object with the fields specified by that protocol. We
_getFieldsForThirdPartyLocation: function(user_input, protocol, instance) {
// make an object with the fields specified by that protocol. We
// require that the values of all but the last field come from the
// instance. The last is the user input.
const required_fields = this.protocols[network_info.protocol].location_fields;
const required_fields = protocol.location_fields;
if (!required_fields) return null;
const fields = {};
for (let i = 0; i < required_fields.length - 1; ++i) {
const this_field = required_fields[i];
if (matched_instance.fields[this_field] === undefined) return null;
fields[this_field] = matched_instance.fields[this_field];
if (instance.fields[this_field] === undefined) return null;
fields[this_field] = instance.fields[this_field];
}
fields[required_fields[required_fields.length - 1]] = user_input;
return fields;
},
render: function() {
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const Loader = sdk.getComponent("elements.Spinner");
if (this.state.protocolsLoading) {
return (
<div className="mx_RoomDirectory">
<SimpleRoomHeader title="Directory" />
<Loader />
</div>
);
}
let content;
if (this.state.loading) {
const Loader = sdk.getComponent("elements.Spinner");
content = <div className="mx_RoomDirectory">
<Loader />
</div>;
@ -590,26 +506,35 @@ module.exports = React.createClass({
</ScrollPanel>;
}
let placeholder = 'Search for a room';
if (this.state.network === null || this.state.network === '_matrix') {
placeholder = '#example:' + this.state.roomServer;
} else if (
this.props.config.networks &&
this.props.config.networks[this.state.network] &&
this.props.config.networks[this.state.network].example &&
this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network)
const protocol_name = protocolNameForInstanceId(this.protocols, this.state.instanceId);
let instance_expected_field_type;
if (
protocol_name &&
this.protocols &&
this.protocols[protocol_name] &&
this.protocols[protocol_name].location_fields.length > 0 &&
this.protocols[protocol_name].field_types
) {
placeholder = this.props.config.networks[this.state.network].example;
const last_field = this.protocols[protocol_name].location_fields.slice(-1)[0];
instance_expected_field_type = this.protocols[protocol_name].field_types[last_field];
}
let showJoinButton = this._stringLooksLikeId(this.state.filterString, this.state.network);
if (this.state.network && this.state.network != '_matrix') {
if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.state.network) === null) {
let placeholder = 'Search for a room';
if (!this.state.instanceId) {
placeholder = '#example:' + this.state.roomServer;
} else if (instance_expected_field_type) {
placeholder = instance_expected_field_type.placeholder;
}
let showJoinButton = this._stringLooksLikeId(this.state.filterString, instance_expected_field_type);
if (protocol_name) {
const instance = instanceForInstanceId(this.protocols, this.state.instanceId);
if (this._getFieldsForThirdPartyLocation(this.state.filterString, this.protocols[protocol_name], instance) === null) {
showJoinButton = false;
}
}
const SimpleRoomHeader = sdk.getComponent('rooms.SimpleRoomHeader');
const NetworkDropdown = sdk.getComponent('directory.NetworkDropdown');
const DirectorySearchBox = sdk.getComponent('elements.DirectorySearchBox');
return (

View file

@ -16,6 +16,7 @@ limitations under the License.
import React from 'react';
import MatrixClientPeg from 'matrix-react-sdk/lib/MatrixClientPeg';
import {instanceForInstanceId} from '../../../utils/DirectoryUtils';
const DEFAULT_ICON_URL = "img/network-matrix.svg";
@ -30,7 +31,6 @@ export default class NetworkDropdown extends React.Component {
this.onRootClick = this.onRootClick.bind(this);
this.onDocumentClick = this.onDocumentClick.bind(this);
this.onMenuOptionClick = this.onMenuOptionClick.bind(this);
this.onMenuOptionClickProtocolInstance = this.onMenuOptionClickProtocolInstance.bind(this);
this.onInputKeyUp = this.onInputKeyUp.bind(this);
this.collectRoot = this.collectRoot.bind(this);
this.collectInputTextBox = this.collectInputTextBox.bind(this);
@ -38,20 +38,11 @@ export default class NetworkDropdown extends React.Component {
this.inputTextBox = null;
const server = MatrixClientPeg.getHomeServerName();
let defaultNetwork = null;
if (
this.props.config.serverConfig &&
this.props.config.serverConfig[server] &&
this.props.config.serverConfig[server].networks &&
this.props.config.serverConfig[server].networks.indexOf('_matrix') > -1
) {
defaultNetwork = '_matrix';
}
this.state = {
expanded: false,
selectedServer: server,
selectedNetwork: defaultNetwork,
selectedInstance: null,
includeAllNetworks: false,
};
}
@ -61,7 +52,7 @@ export default class NetworkDropdown extends React.Component {
document.addEventListener('click', this.onDocumentClick, false);
// fire this now so the defaults can be set up
this.props.onOptionChange(this.state.selectedServer, this.state.selectedNetwork);
this.props.onOptionChange(this.state.selectedServer, this.state.selectedInstance, this.state.includeAllNetworks);
}
componentWillUnmount() {
@ -101,24 +92,14 @@ export default class NetworkDropdown extends React.Component {
ev.preventDefault();
}
onMenuOptionClick(server, network) {
onMenuOptionClick(server, instance, includeAll) {
this.setState({
expanded: false,
selectedServer: server,
selectedNetwork: network,
selectedInstanceId: null,
selectedInstanceId: instance ? instance.instance_id : null,
includeAll: includeAll,
});
this.props.onOptionChange(server, network);
}
onMenuOptionClickProtocolInstance(server, instance_id) {
this.setState({
expanded: false,
selectedServer: server,
selectedNetwork: null,
selectedInstanceId: instance_id,
});
this.props.onOptionChange(server, null, instance_id);
this.props.onOptionChange(server, instance ? instance.instance_id : null, includeAll);
}
onInputKeyUp(e) {
@ -158,33 +139,21 @@ export default class NetworkDropdown extends React.Component {
servers.unshift(MatrixClientPeg.getHomeServerName());
}
// if the thirdparty/protocols entries have instance_ids,
// we can get the local server listings from here. If not,
// the server is too old.
let use_protocols = true;
for (const proto of Object.keys(this.props.protocols)) {
if (!this.props.protocols[proto].instances) continue;
for (const instance of this.props.protocols[proto].instances) {
if (!instance.instance_id) use_protocols = false;
}
}
// For our own HS, we can use the instance_ids given in the third party protocols
// response to get the server to filter the room list by network for us (if the
// server is new enough), although for now we prefer the config if it exists.
// For remote HSes, we use the data from the config.
// response to get the server to filter the room list by network for us.
// We can't get thirdparty protocols for remote server yet though, so for those
// we can only show the default room list.
for (const server of servers) {
options.push(this._makeMenuOption(server, null));
if (this.props.config.serverConfig && this.props.config.serverConfig[server] && this.props.config.serverConfig[server].networks) {
for (const network of this.props.config.serverConfig[server].networks) {
options.push(this._makeMenuOption(server, network));
}
} else if (server == MatrixClientPeg.getHomeServerName() && use_protocols) {
options.push(this._makeMenuOption(server, '_matrix'));
for (const proto of Object.keys(this.props.protocols)) {
if (!this.props.protocols[proto].instances) continue;
for (const instance of this.props.protocols[proto].instances) {
options.push(this._makeMenuOptionFromProtocolInstance(server, this.props.protocols[proto], instance));
options.push(this._makeMenuOption(server, null, true));
if (server == MatrixClientPeg.getHomeServerName()) {
options.push(this._makeMenuOption(server, null, false));
if (this.props.protocols) {
for (const proto of Object.keys(this.props.protocols)) {
if (!this.props.protocols[proto].instances) continue;
for (const instance of this.props.protocols[proto].instances) {
if (!instance.instance_id) continue;
options.push(this._makeMenuOption(server, instance, false));
}
}
}
}
@ -193,82 +162,36 @@ export default class NetworkDropdown extends React.Component {
return options;
}
_makeMenuOptionFromProtocolInstance(server, protocol, instance, handleClicks) {
_makeMenuOption(server, instance, includeAll, handleClicks) {
if (handleClicks === undefined) handleClicks = true;
const name = instance.desc;
const icon = <img src={instance.icon || DEFAULT_ICON_URL} />;
const key = instance.instance_id;
const click_handler = handleClicks ? this.onMenuOptionClickProtocolInstance.bind(this, server, instance.instance_id) : null;
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
{icon}
<span className="mx_NetworkDropdown_menu_network">{name}</span>
</div>;
}
_makeMenuOption(server, network, handleClicks) {
if (handleClicks === undefined) handleClicks = true;
let icon;
let name;
let span_class;
let key;
if (network === null) {
if (!instance && includeAll) {
key = server;
name = server;
span_class = 'mx_NetworkDropdown_menu_all';
} else if (network == '_matrix') {
} else if (!instance) {
key = server + '_all';
name = 'Matrix';
icon = <img src="img/network-matrix.svg" />;
span_class = 'mx_NetworkDropdown_menu_network';
} else {
if (this.props.config.networks[network] === undefined) {
throw new Error(network + ' network missing from config');
}
if (this.props.config.networks[network].name) {
name = this.props.config.networks[network].name;
} else {
name = network;
}
if (this.props.config.networks[network].icon) {
// omit height here so if people define a non-square logo in the config, it
// will keep the aspect when it scales
icon = <img src={this.props.config.networks[network].icon} />;
} else {
icon = <img src={iconPath} />;
}
key = server + '_inst_'+instance.instance_id;
icon = <img src={instance.icon || DEFAULT_ICON_URL} />;
name = instance.desc;
span_class = 'mx_NetworkDropdown_menu_network';
}
const click_handler = handleClicks ? this.onMenuOptionClick.bind(this, server, network) : null;
let key = server;
if (network !== null) {
key += '_' + network;
}
const click_handler = handleClicks ? this.onMenuOptionClick.bind(this, server, instance, includeAll) : null;
return <div key={key} className="mx_NetworkDropdown_networkoption" onClick={click_handler}>
{icon}
<span className={span_class}>{name}</span>
</div>;
}
_protocolNameForInstanceId(instance_id) {
for (const proto of Object.keys(this.props.protocols)) {
if (!this.props.protocols[proto].instances) continue;
for (const instance of this.props.protocols[proto].instances) {
if (instance.instance_id == instance_id) return proto;
}
}
}
instanceForInstanceId(instance_id) {
for (const proto of Object.keys(this.props.protocols)) {
if (!this.props.protocols[proto].instances) continue;
for (const instance of this.props.protocols[proto].instances) {
if (instance.instance_id == instance_id) return instance;
}
}
<span className="mx_NetworkDropdown_menu_network">{name}</span>
</div>
}
render() {
@ -285,17 +208,10 @@ export default class NetworkDropdown extends React.Component {
placeholder="matrix.org" // 'matrix.org' as an example of an HS name
/>
} else {
if (this.state.selectedInstanceId) {
const protocolName = this._protocolNameForInstanceId(this.state.selectedInstanceId);
const instance = this.instanceForInstanceId(this.state.selectedInstanceId);
current_value = this._makeMenuOptionFromProtocolInstance(
this.state.selectedServer, this.props.protocols[protocolName], instance, false
);
} else {
current_value = this._makeMenuOption(
this.state.selectedServer, this.state.selectedNetwork, false
);
}
const instance = instanceForInstanceId(this.props.protocols, this.state.selectedInstanceId);
current_value = this._makeMenuOption(
this.state.selectedServer, instance, this.state.includeAll, false
);
}
return <div className="mx_NetworkDropdown" ref={this.collectRoot}>
@ -310,14 +226,11 @@ export default class NetworkDropdown extends React.Component {
NetworkDropdown.propTypes = {
onOptionChange: React.PropTypes.func.isRequired,
config: React.PropTypes.object,
protocols: React.PropTypes.object,
config: React.PropTypes.object,
};
NetworkDropdown.defaultProps = {
config: {
networks: [],
},
protocols: {},
config: {},
};

View file

@ -0,0 +1,23 @@
// Find a protocol 'instance' with a given instance_id
// in the supplied protocols dict
export function instanceForInstanceId(protocols, instance_id) {
if (!instance_id) return null;
for (const proto of Object.keys(protocols)) {
if (!protocols[proto].instances) continue;
for (const instance of protocols[proto].instances) {
if (instance.instance_id == instance_id) return instance;
}
}
}
// given an instance_id, return the name of the protocol for
// that instance ID in the supplied protocols dict
export function protocolNameForInstanceId(protocols, instance_id) {
if (!instance_id) return null;
for (const proto of Object.keys(protocols)) {
if (!protocols[proto].instances) continue;
for (const instance of protocols[proto].instances) {
if (instance.instance_id == instance_id) return proto;
}
}
}