Refactor feed errors and error presentation (#459)

* Add support for setting a global feed timeout

* Track failures, and don't show the first non-serious error

* Tidy up error reporting

* Make a prettier error pane

* Use the prettier event pane

* Show failed feed attempts in the widget

* Ensure we catch connection resets

* Add styling file

* Ensure we only track one result per fetch

* changelog

* Refactor to use better status codes

* Make feed results cheaper

* splice
This commit is contained in:
Will Hunt 2022-08-31 14:01:43 +01:00 committed by GitHub
parent 5f84cda39c
commit 63ab457b45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 413 additions and 73 deletions

1
changelog.d/459.feature Normal file
View File

@ -0,0 +1 @@
Added new config option `feeds.pollTimeoutSeconds` to explictly set how long to wait for a feed response.

View File

@ -87,6 +87,7 @@ feeds:
# #
enabled: false enabled: false
pollIntervalSeconds: 600 pollIntervalSeconds: 600
pollTimeoutSeconds: 10
provisioning: provisioning:
# (Optional) Provisioning API for integration managers # (Optional) Provisioning API for integration managers
# #

View File

@ -49,6 +49,7 @@
"cors": "^2.8.5", "cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"figma-js": "^1.14.0", "figma-js": "^1.14.0",
"http-status-codes": "^2.2.0",
"ioredis": "^4.28.0", "ioredis": "^4.28.0",
"jira-client": "^8.0.0", "jira-client": "^8.0.0",
"markdown-it": "^12.3.2", "markdown-it": "^12.3.2",
@ -85,7 +86,7 @@
"@types/micromatch": "^4.0.1", "@types/micromatch": "^4.0.1",
"@types/mime": "^2.0.3", "@types/mime": "^2.0.3",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/node": "^14", "@types/node": "^16",
"@types/node-emoji": "^1.8.1", "@types/node-emoji": "^1.8.1",
"@types/uuid": "^8.3.3", "@types/uuid": "^8.3.3",
"@types/xml2js": "^0.4.11", "@types/xml2js": "^0.4.11",
@ -103,6 +104,7 @@
"sass": "^1.51.0", "sass": "^1.51.0",
"ts-node": "^10.4.0", "ts-node": "^10.4.0",
"typescript": "^4.5.2", "typescript": "^4.5.2",
"vite": "^2.9.13" "vite": "^2.9.13",
"vite-svg-loader": "^3.4.0"
} }
} }

View File

@ -613,6 +613,11 @@ export class Bridge {
(data) => connManager.getConnectionsForFeedUrl(data.feed.url), (data) => connManager.getConnectionsForFeedUrl(data.feed.url),
(c, data) => c.handleFeedEntry(data), (c, data) => c.handleFeedEntry(data),
); );
this.bindHandlerToQueue<FeedEntry, FeedConnection>(
"feed.success",
(data) => connManager.getConnectionsForFeedUrl(data.feed.url),
c => c.handleFeedSuccess(),
);
this.bindHandlerToQueue<FeedError, FeedConnection>( this.bindHandlerToQueue<FeedError, FeedConnection>(
"feed.error", "feed.error",
(data) => connManager.getConnectionsForFeedUrl(data.url), (data) => connManager.getConnectionsForFeedUrl(data.url),

View File

@ -232,15 +232,20 @@ export class BridgeConfigGitLab {
export interface BridgeConfigFeedsYAML { export interface BridgeConfigFeedsYAML {
enabled: boolean; enabled: boolean;
pollIntervalSeconds: number; pollIntervalSeconds: number;
pollTimeoutSeconds?: number;
} }
export class BridgeConfigFeeds { export class BridgeConfigFeeds {
public enabled: boolean; public enabled: boolean;
public pollIntervalSeconds: number; public pollIntervalSeconds: number;
public pollTimeoutSeconds: number;
constructor(yaml: BridgeConfigFeedsYAML) { constructor(yaml: BridgeConfigFeedsYAML) {
this.enabled = yaml.enabled; this.enabled = yaml.enabled;
this.pollIntervalSeconds = yaml.pollIntervalSeconds; this.pollIntervalSeconds = yaml.pollIntervalSeconds;
assert.strictEqual(typeof this.pollIntervalSeconds, "number");
this.pollTimeoutSeconds = yaml.pollTimeoutSeconds ?? 10;
assert.strictEqual(typeof this.pollTimeoutSeconds, "number");
} }
@hideKey() @hideKey()

View File

@ -111,6 +111,7 @@ export const DefaultConfig = new BridgeConfig({
feeds: { feeds: {
enabled: false, enabled: false,
pollIntervalSeconds: 600, pollIntervalSeconds: 600,
pollTimeoutSeconds: 10,
}, },
provisioning: { provisioning: {
secret: "!secretToken" secret: "!secretToken"

View File

@ -10,16 +10,33 @@ import axios from "axios";
import markdown from "markdown-it"; import markdown from "markdown-it";
import { Connection, ProvisionConnectionOpts } from "./IConnection"; import { Connection, ProvisionConnectionOpts } from "./IConnection";
import { GetConnectionsResponseItem } from "../provisioning/api"; import { GetConnectionsResponseItem } from "../provisioning/api";
import { StatusCodes } from "http-status-codes";
const log = new LogWrapper("FeedConnection"); const log = new LogWrapper("FeedConnection");
const md = new markdown(); const md = new markdown();
export interface LastResultOk {
timestamp: number;
ok: true;
}
export interface LastResultFail {
timestamp: number;
ok: false;
error?: string;
}
export interface FeedConnectionState extends IConnectionState { export interface FeedConnectionState extends IConnectionState {
url: string; url: string;
label?: string; label?: string;
} }
export type FeedResponseItem = GetConnectionsResponseItem<FeedConnectionState, object>; export interface FeedConnectionSecrets {
lastResults: Array<LastResultOk|LastResultFail>;
}
export type FeedResponseItem = GetConnectionsResponseItem<FeedConnectionState, FeedConnectionSecrets>;
const MAX_LAST_RESULT_ITEMS = 5;
@Connection @Connection
export class FeedConnection extends BaseConnection implements IConnection { export class FeedConnection extends BaseConnection implements IConnection {
@ -37,14 +54,23 @@ export class FeedConnection extends BaseConnection implements IConnection {
static async validateUrl(url: string): Promise<void> { static async validateUrl(url: string): Promise<void> {
try { try {
new URL(url); new URL(url);
const res = await axios.head(url).catch(_ => axios.get(url)); } catch (ex) {
const contentType = res.headers['content-type']; throw new ApiError("Feed URL doesn't appear valid", ErrCode.BadValue);
// we're deliberately liberal here, since different things pop up in the wild }
if (!contentType.match(/xml/)) { let res;
throw new Error(`${contentType} doesn't look like an RSS/Atom feed`); try {
} res = await axios.head(url).catch(() => axios.get(url));
} catch (err) { } catch (ex) {
throw new Error(`${url} doesn't look like a valid feed URL: ${err}`); throw new ApiError(`Could not read from URL: ${ex.message}`, ErrCode.BadValue);
}
const contentType = res.headers['content-type'];
// we're deliberately liberal here, since different things pop up in the wild
if (!contentType.match(/xml/)) {
throw new ApiError(
`Feed responded with a content type of "${contentType}", which doesn't look like an RSS/Atom feed`,
ErrCode.BadValue,
StatusCodes.UNSUPPORTED_MEDIA_TYPE
);
} }
} }
@ -57,11 +83,7 @@ export class FeedConnection extends BaseConnection implements IConnection {
if (typeof url !== 'string') { if (typeof url !== 'string') {
throw new ApiError('No URL specified', ErrCode.BadValue); throw new ApiError('No URL specified', ErrCode.BadValue);
} }
try { await FeedConnection.validateUrl(url);
await FeedConnection.validateUrl(url);
} catch (err: any) {
throw new ApiError(err.toString(), ErrCode.BadValue);
}
if (typeof data.label !== 'undefined' && typeof data.label !== 'string') { if (typeof data.label !== 'undefined' && typeof data.label !== 'string') {
throw new ApiError('Label must be a string', ErrCode.BadValue); throw new ApiError('Label must be a string', ErrCode.BadValue);
} }
@ -95,10 +117,14 @@ export class FeedConnection extends BaseConnection implements IConnection {
url: this.feedUrl, url: this.feedUrl,
label: this.state.label, label: this.state.label,
}, },
secrets: {
lastResults: this.lastResults,
}
} }
} }
private hasError = false; private hasError = false;
private readonly lastResults = new Array<LastResultOk|LastResultFail>();
public get feedUrl(): string { public get feedUrl(): string {
return this.state.url; return this.state.url;
@ -121,7 +147,6 @@ export class FeedConnection extends BaseConnection implements IConnection {
} }
public async handleFeedEntry(entry: FeedEntry): Promise<void> { public async handleFeedEntry(entry: FeedEntry): Promise<void> {
this.hasError = false;
let entryDetails; let entryDetails;
if (entry.title && entry.link) { if (entry.title && entry.link) {
@ -143,7 +168,27 @@ export class FeedConnection extends BaseConnection implements IConnection {
}); });
} }
handleFeedSuccess() {
this.hasError = false;
this.lastResults.unshift({
ok: true,
timestamp: Date.now(),
});
this.lastResults.splice(MAX_LAST_RESULT_ITEMS-1, 1);
}
public async handleFeedError(error: FeedError): Promise<void> { public async handleFeedError(error: FeedError): Promise<void> {
this.lastResults.unshift({
ok: false,
timestamp: Date.now(),
error: error.message,
});
this.lastResults.splice(MAX_LAST_RESULT_ITEMS-1, 1);
const wasLastResultSuccessful = this.lastResults[0]?.ok !== false;
if (wasLastResultSuccessful && error.shouldErrorBeSilent) {
// To avoid short term failures bubbling up, if the error is serious, we still bubble.
return;
}
if (!this.hasError) { if (!this.hasError) {
await this.as.botIntent.sendEvent(this.roomId, { await this.as.botIntent.sendEvent(this.roomId, {
msgtype: 'm.notice', msgtype: 'm.notice',

View File

@ -14,6 +14,7 @@ import { AdminRoom } from "../AdminRoom";
import { GitLabRepoConnection } from "./GitlabRepo"; import { GitLabRepoConnection } from "./GitlabRepo";
import { IConnectionState, ProvisionConnectionOpts } from "./IConnection"; import { IConnectionState, ProvisionConnectionOpts } from "./IConnection";
import LogWrapper from "../LogWrapper"; import LogWrapper from "../LogWrapper";
import { ApiError } from "matrix-appservice-bridge";
const md = new markdown(); const md = new markdown();
const log = new LogWrapper("SetupConnection"); const log = new LogWrapper("SetupConnection");
@ -182,7 +183,11 @@ export class SetupConnection extends CommandConnection {
await FeedConnection.validateUrl(url); await FeedConnection.validateUrl(url);
} catch (err: unknown) { } catch (err: unknown) {
log.debug(`Feed URL '${url}' failed validation: ${err}`); log.debug(`Feed URL '${url}' failed validation: ${err}`);
throw new CommandError("Invalid URL", `${url} doesn't look like a valid feed URL`); if (err instanceof ApiError) {
throw new CommandError("Invalid URL", err.error);
} else {
throw new CommandError("Invalid URL", `${url} doesn't look like a valid feed URL`);
}
} }
await FeedConnection.provisionConnection(this.roomId, userId, { url, label }, this.provisionOpts); await FeedConnection.provisionConnection(this.roomId, userId, { url, label }, this.provisionOpts);

View File

@ -1,5 +1,6 @@
import { ErrorObject } from "ajv"; import { ErrorObject } from "ajv";
import { NextFunction, Response, Request } from "express"; import { NextFunction, Response, Request } from "express";
import { StatusCodes } from "http-status-codes";
import { IApiError } from "matrix-appservice-bridge"; import { IApiError } from "matrix-appservice-bridge";
import LogWrapper from "../LogWrapper"; import LogWrapper from "../LogWrapper";
@ -53,26 +54,26 @@ export enum ErrCode {
MethodNotAllowed = "HS_METHOD_NOT_ALLOWED" MethodNotAllowed = "HS_METHOD_NOT_ALLOWED"
} }
const ErrCodeToStatusCode: Record<ErrCode, number> = { const ErrCodeToStatusCode: Record<ErrCode, StatusCodes> = {
HS_UNKNOWN: 500, HS_UNKNOWN: StatusCodes.INTERNAL_SERVER_ERROR,
HS_NOTFOUND: 404, HS_NOTFOUND: StatusCodes.NOT_FOUND,
HS_UNSUPPORTED_OPERATION: 400, HS_UNSUPPORTED_OPERATION: StatusCodes.BAD_REQUEST,
HS_FORBIDDEN_USER: 403, HS_FORBIDDEN_USER: StatusCodes.FORBIDDEN,
HS_FORBIDDEN_BOT: 403, HS_FORBIDDEN_BOT: StatusCodes.FORBIDDEN,
HS_NOT_IN_ROOM: 403, HS_NOT_IN_ROOM: StatusCodes.FORBIDDEN,
HS_BAD_VALUE: 400, HS_BAD_VALUE: StatusCodes.BAD_REQUEST,
HS_BAD_TOKEN: 401, HS_BAD_TOKEN: StatusCodes.UNAUTHORIZED,
HS_DISABLED_FEATURE: 500, HS_DISABLED_FEATURE: StatusCodes.INTERNAL_SERVER_ERROR,
HS_ADDITIONAL_ACTION_REQUIRED: 400, HS_ADDITIONAL_ACTION_REQUIRED: StatusCodes.BAD_REQUEST,
HS_CONFLICTING_CONNECTION: 409, HS_CONFLICTING_CONNECTION: StatusCodes.CONFLICT,
HS_METHOD_NOT_ALLOWED: 405, HS_METHOD_NOT_ALLOWED: StatusCodes.METHOD_NOT_ALLOWED,
} }
export class ApiError extends Error implements IApiError { export class ApiError extends Error implements IApiError {
constructor( constructor(
public readonly error: string, public readonly error: string,
public readonly errcode = ErrCode.Unknown, public readonly errcode = ErrCode.Unknown,
public readonly statusCode = -1, public readonly statusCode: number|StatusCodes = -1,
public readonly additionalContent: Record<string, unknown> = {}, public readonly additionalContent: Record<string, unknown> = {},
) { ) {
super(`API error ${errcode}: ${error}`); super(`API error ${errcode}: ${error}`);

View File

@ -9,6 +9,8 @@ import Ajv from "ajv";
import axios from "axios"; import axios from "axios";
import Parser from "rss-parser"; import Parser from "rss-parser";
import Metrics from "../Metrics"; import Metrics from "../Metrics";
import UserAgent from "../UserAgent";
import { randomUUID } from "crypto";
const log = new LogWrapper("FeedReader"); const log = new LogWrapper("FeedReader");
@ -16,9 +18,28 @@ export class FeedError extends Error {
constructor( constructor(
public url: string, public url: string,
public cause: Error, public cause: Error,
public readonly fetchKey: string,
) { ) {
super(`Error fetching feed ${url}: ${cause.message}`); super(`Error fetching feed ${url}: ${cause.message}`);
} }
get shouldErrorBeSilent() {
if (axios.isAxiosError(this.cause) && this.cause.response?.status) {
if (this.cause.response.status % 500 < 100) {
// 5XX error, retry these as it might be a server screwup.
return true;
} else if (this.cause.response.status % 400 < 100) {
// 4XX error, actually report these because the server is explicity stating we can't read the resource.
return false;
}
}
if (axios.isAxiosError(this.cause) && this.cause.code === 'ECONNRESET') {
// Fuzzy match this, because it's usually a tempoary error.
return true;
}
// Err on the side of safety and report the rest
return false;
}
} }
export interface FeedEntry { export interface FeedEntry {
@ -28,6 +49,10 @@ export interface FeedEntry {
}, },
title: string|null, title: string|null,
link: string|null, link: string|null,
/**
* Unique key to identify the specific fetch across entries.
*/
fetchKey: string,
} }
interface AccountData { interface AccountData {
@ -65,10 +90,10 @@ export class FeedReader {
static readonly seenEntriesEventType = "uk.half-shot.matrix-hookshot.feed.reader.seenEntries"; static readonly seenEntriesEventType = "uk.half-shot.matrix-hookshot.feed.reader.seenEntries";
constructor( constructor(
private config: BridgeConfigFeeds, private readonly config: BridgeConfigFeeds,
private connectionManager: ConnectionManager, private readonly connectionManager: ConnectionManager,
private queue: MessageQueue, private readonly queue: MessageQueue,
private matrixClient: MatrixClient, private readonly matrixClient: MatrixClient,
) { ) {
this.connections = this.connectionManager.getAllConnectionsOfType(FeedConnection); this.connections = this.connectionManager.getAllConnectionsOfType(FeedConnection);
this.calculateFeedUrls(); this.calculateFeedUrls();
@ -145,8 +170,15 @@ export class FeedReader {
const fetchingStarted = Date.now(); const fetchingStarted = Date.now();
for (const url of this.observedFeedUrls.values()) { for (const url of this.observedFeedUrls.values()) {
const fetchKey = randomUUID();
try { try {
const res = await axios.get(url.toString()); const res = await axios.get(url.toString(), {
headers: {
'User-Agent': UserAgent,
},
// We don't want to wait forever for the feed.
timeout: this.config.pollTimeoutSeconds * 1000,
});
const feed = await (new Parser()).parseString(res.data); const feed = await (new Parser()).parseString(res.data);
let initialSync = false; let initialSync = false;
let seenGuids = this.seenEntries.get(url); let seenGuids = this.seenEntries.get(url);
@ -182,6 +214,7 @@ export class FeedReader {
}, },
title: item.title ? stripHtml(item.title) : null, title: item.title ? stripHtml(item.title) : null,
link: item.link || null, link: item.link || null,
fetchKey
}; };
log.debug('New entry:', entry); log.debug('New entry:', entry);
@ -200,10 +233,12 @@ export class FeedReader {
const newSeenItems = Array.from(new Set([ ...newGuids, ...seenGuids ]).values()).slice(0, maxGuids); const newSeenItems = Array.from(new Set([ ...newGuids, ...seenGuids ]).values()).slice(0, maxGuids);
this.seenEntries.set(url, newSeenItems); this.seenEntries.set(url, newSeenItems);
} }
} catch (err: any) { this.queue.push<undefined>({ eventName: 'feed.success', sender: 'FeedReader', data: undefined});
const error = new FeedError(url.toString(), err); } catch (err: unknown) {
log.error(error.message); const error = err instanceof Error ? err : new Error(`Unknown error ${err}`);
this.queue.push<FeedError>({ eventName: 'feed.error', sender: 'FeedReader', data: error }); const feedError = new FeedError(url.toString(), error, fetchKey);
log.error("Unable to read feed:", feedError.message);
this.queue.push<FeedError>({ eventName: 'feed.error', sender: 'FeedReader', data: feedError});
} }
} }
if (seenEntriesChanged) await this.saveSeenEntries(); if (seenEntriesChanged) await this.saveSeenEntries();

View File

@ -1,9 +1,10 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import preact from '@preact/preset-vite' import preact from '@preact/preset-vite'
import svgLoader from 'vite-svg-loader'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [preact()], plugins: [preact(), svgLoader({ defaultImport: 'url'})],
root: 'web', root: 'web',
base: '', base: '',
build: { build: {

View File

View File

@ -1,3 +0,0 @@
.error-pane {
max-width: 480px;
}

View File

@ -0,0 +1,4 @@
.errorPane {
max-width: 480px;
color: #FF4B55;
}

View File

@ -1,9 +1,9 @@
import { h, FunctionComponent } from "preact"; import { h, FunctionComponent } from "preact";
import "./ErrorPane.css"; import ErrorBadge from "../../icons/warning-badge.svg";
import style from "./ErrorPane.module.scss";
export const ErrorPane: FunctionComponent<{header?: string}> = ({ children, header }) => { export const ErrorPane: FunctionComponent<{header?: string}> = ({ children, header }) => {
return <div class="card error error-pane"> return <div class={`card error ${style.errorPane}`}>
<h3>{ header || "Error occured during widget load" }</h3> <p><strong><img src={ErrorBadge} /> { header || "Error occured during widget load" }</strong>: {children}</p>
<p>{children}</p>
</div>; </div>;
}; };

View File

@ -0,0 +1,4 @@
.resultListItem {
list-style: none;
padding-bottom: 1rem;
}

View File

@ -4,9 +4,28 @@ import { BridgeConfig } from "../../BridgeAPI";
import { FeedConnectionState, FeedResponseItem } from "../../../src/Connections/FeedConnection"; import { FeedConnectionState, FeedResponseItem } from "../../../src/Connections/FeedConnection";
import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig"; import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig";
import { Button, ButtonSet, InputField } from "../elements"; import { Button, ButtonSet, InputField } from "../elements";
import styles from "./FeedConnection.module.scss";
import FeedsIcon from "../../icons/feeds.png"; import FeedsIcon from "../../icons/feeds.png";
const FeedRecentResults: FunctionComponent<{item: FeedResponseItem}> = ({ item }) => {
if (!item.secrets) {
return null;
}
return <>
<h3>Recent feed results</h3>
{!item.secrets.lastResults.length && <span>There have been no recent updates for this feed.</span>}
<ul>
{item.secrets.lastResults.map(item => <li styles={styles.resultListItem} key={item.timestamp}>
{new Date(item.timestamp).toLocaleString()}:
{item.ok && `✅ Successful fetch`}
{!item.ok && `⚠️ ${item.error}`}
</li>
)}
</ul>
</>;
}
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ServiceConfig, FeedResponseItem, FeedConnectionState>> = ({existingConnection, onSave, onRemove}) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ServiceConfig, FeedResponseItem, FeedConnectionState>> = ({existingConnection, onSave, onRemove}) => {
const urlRef = createRef<HTMLInputElement>(); const urlRef = createRef<HTMLInputElement>();
const labelRef = createRef<HTMLInputElement>(); const labelRef = createRef<HTMLInputElement>();
@ -27,15 +46,20 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
}, [canEdit, onSave, urlRef, labelRef, existingConnection]); }, [canEdit, onSave, urlRef, labelRef, existingConnection]);
return <form onSubmit={handleSave}> return <form onSubmit={handleSave}>
<InputField visible={!existingConnection} label="URL" noPadding={true}> { existingConnection && <FeedRecentResults item={existingConnection} />}
<input ref={urlRef} disabled={!canEdit} placeholder="Feed URL" type="text" value={existingConnection?.config.url} />
<input ref={labelRef} disabled={!canEdit} placeholder="Label (optional)" type="text" value={existingConnection?.config.label} />
</InputField>
<InputField visible={!existingConnection} label="URL" noPadding={true}>
<input ref={urlRef} disabled={!canEdit} type="text" value={existingConnection?.config.url} />
</InputField>
<InputField visible={!existingConnection} label="Label" noPadding={true}>
<input ref={labelRef} disabled={!canEdit} type="text" value={existingConnection?.config.label} />
</InputField>
<ButtonSet> <ButtonSet>
{ canEdit && <Button type="submit">{ existingConnection ? "Save" : "Subscribe" }</Button>} { canEdit && <Button type="submit">{ existingConnection ? "Save" : "Subscribe" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Unsubscribe</Button>} { canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Unsubscribe</Button>}
</ButtonSet> </ButtonSet>
</form>; </form>;
}; };

View File

@ -1,6 +1,6 @@
import { h, FunctionComponent } from "preact"; import { h, FunctionComponent } from "preact";
import { useCallback, useEffect, useReducer, useState } from "preact/hooks" import { useCallback, useEffect, useReducer, useState } from "preact/hooks"
import { BridgeAPI } from "../../BridgeAPI"; import { BridgeAPI, BridgeAPIError } from "../../BridgeAPI";
import { ErrorPane, ListItem } from "../elements"; import { ErrorPane, ListItem } from "../elements";
import style from "./RoomConfig.module.scss"; import style from "./RoomConfig.module.scss";
import { GetConnectionsResponseItem } from "../../../src/provisioning/api"; import { GetConnectionsResponseItem } from "../../../src/provisioning/api";
@ -33,7 +33,7 @@ interface IRoomConfigProps<SConfig, ConnectionType extends GetConnectionsRespons
export const RoomConfig = function<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState>(props: IRoomConfigProps<SConfig, ConnectionType, ConnectionState>) { export const RoomConfig = function<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState>(props: IRoomConfigProps<SConfig, ConnectionType, ConnectionState>) {
const { api, roomId, type, headerImg, text, listItemName, connectionEventType } = props; const { api, roomId, type, headerImg, text, listItemName, connectionEventType } = props;
const ConnectionConfigComponent = props.connectionConfigComponent; const ConnectionConfigComponent = props.connectionConfigComponent;
const [ error, setError ] = useState<null|string>(null); const [ error, setError ] = useState<null|{header?: string, message: string}>(null);
const [ connections, setConnections ] = useState<ConnectionType[]|null>(null); const [ connections, setConnections ] = useState<ConnectionType[]|null>(null);
const [ serviceConfig, setServiceConfig ] = useState<SConfig|null>(null); const [ serviceConfig, setServiceConfig ] = useState<SConfig|null>(null);
const [ canEditRoom, setCanEditRoom ] = useState<boolean>(false); const [ canEditRoom, setCanEditRoom ] = useState<boolean>(false);
@ -44,18 +44,28 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
api.getConnectionsForService<ConnectionType>(roomId, type).then(res => { api.getConnectionsForService<ConnectionType>(roomId, type).then(res => {
setCanEditRoom(res.canEdit); setCanEditRoom(res.canEdit);
setConnections(res.connections); setConnections(res.connections);
setError(null);
}).catch(ex => { }).catch(ex => {
console.warn("Failed to fetch existing connections", ex); console.warn("Failed to fetch existing connections", ex);
setError("Failed to fetch existing connections"); setError({
header: "Failed to fetch existing connections",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
});
}); });
}, [api, roomId, type, newConnectionKey]); }, [api, roomId, type, newConnectionKey]);
useEffect(() => { useEffect(() => {
api.getServiceConfig<SConfig>(type) api.getServiceConfig<SConfig>(type)
.then(setServiceConfig) .then(setServiceConfig)
.then(() => {
setError(null);
})
.catch(ex => { .catch(ex => {
console.warn("Failed to fetch service config", ex); console.warn("Failed to fetch service config", ex);
setError("Failed to fetch service config"); setError({
header: "Failed to fetch service config",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
});
}) })
}, [api, type]); }, [api, type]);
@ -63,15 +73,19 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
api.createConnection(roomId, connectionEventType, config).then(() => { api.createConnection(roomId, connectionEventType, config).then(() => {
// Force reload // Force reload
incrementConnectionKey(undefined); incrementConnectionKey(undefined);
setError(null);
}).catch(ex => { }).catch(ex => {
console.warn("Failed to create connection", ex); console.warn("Failed to create connection", ex);
setError("Failed to create connection"); setError({
header: "Failed to create connection",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
});
}); });
}, [api, roomId, connectionEventType]); }, [api, roomId, connectionEventType]);
return <main> return <main>
{ {
error && <ErrorPane header="Error">{error}</ErrorPane> error && <ErrorPane header={error.header || "Error"}>{error.message}</ErrorPane>
} }
<header className={style.header}> <header className={style.header}>
<img src={headerImg} /> <img src={headerImg} />
@ -97,17 +111,25 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
api.updateConnection(roomId, c.id, config).then(() => { api.updateConnection(roomId, c.id, config).then(() => {
// Force reload // Force reload
incrementConnectionKey(undefined); incrementConnectionKey(undefined);
setError(null);
}).catch(ex => { }).catch(ex => {
console.warn("Failed to create connection", ex); console.warn("Failed to create connection", ex);
setError("Failed to create connection"); setError({
header: "Failed to create connection",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
});
}); });
}} }}
onRemove={() => { onRemove={() => {
api.removeConnection(roomId, c.id).then(() => { api.removeConnection(roomId, c.id).then(() => {
setConnections(conn => conn.filter(conn => c.id !== conn.id)); setConnections(conn => conn.filter(conn => c.id !== conn.id));
setError(null);
}).catch(ex => { }).catch(ex => {
console.warn("Failed to remove connection", ex); console.warn("Failed to remove connection", ex);
setError("Failed to remove connection"); setError({
header: "Failed to remove connection",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
});
}); });
}} }}
/> />

View File

@ -0,0 +1,5 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="8" fill="#FF4B55"/>
<rect x="7" y="3" width="2" height="6" rx="1" fill="white"/>
<rect x="7" y="11" width="2" height="2" rx="1" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

204
yarn.lock
View File

@ -330,6 +330,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.6.tgz#8f194828193e8fa79166f34a4b4e52f3e769a314"
integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ== integrity sha512-Gr86ujcNuPDnNOY8mi383Hvi8IYrJVJYuf3XcuBM/Dgd+bINn/7tHqsj+tKkoreMbmGsFLsltI/JJd8fOFWGDQ==
"@babel/parser@^7.16.4":
version "7.18.13"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.13.tgz#5b2dd21cae4a2c5145f1fbd8ca103f9313d3b7e4"
integrity sha512-dgXcIfMuQ0kgzLB2b9tRZs7TTFFaGM2AbtA4fJgUUYukzGH4jwsS7hzQHEGs67jdehpm22vkgKwvbU+aEflgwg==
"@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8": "@babel/parser@^7.16.7", "@babel/parser@^7.17.3", "@babel/parser@^7.17.8":
version "7.17.8" version "7.17.8"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.8.tgz#2817fb9d885dd8132ea0f8eb615a6388cca1c240"
@ -1077,6 +1082,11 @@
domhandler "^4.2.0" domhandler "^4.2.0"
selderee "^0.6.0" selderee "^0.6.0"
"@trysound/sax@0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad"
integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==
"@tsconfig/node10@^1.0.7": "@tsconfig/node10@^1.0.7":
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9"
@ -1250,10 +1260,10 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d" resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.31.tgz#a5bb84ecfa27eec5e1c802c6bbf8139bdb163a5d"
integrity sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q== integrity sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q==
"@types/node@^14": "@types/node@^16":
version "14.18.12" version "16.11.56"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.12.tgz#0d4557fd3b94497d793efd4e7d92df2f83b4ef24" resolved "https://registry.yarnpkg.com/@types/node/-/node-16.11.56.tgz#dcbb617669481e158e0f1c6204d1c768cd675901"
integrity sha512-q4jlIR71hUpWTnGhXWcakgkZeHa3CCjcQcnuzU8M891BAWA2jHiziiWEPEkdS5pFsz7H9HJiy8BrK7tBRNrY7A== integrity sha512-aFcUkv7EddxxOa/9f74DINReQ/celqH8DiB3fRYgVDM2Xm5QJL8sl80QKuAnGvwAsMn+H3IFA6WCrQh1CY7m1A==
"@types/qs@*": "@types/qs@*":
version "6.9.7" version "6.9.7"
@ -1439,6 +1449,64 @@
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44" resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q== integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
"@vue/compiler-core@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.37.tgz#b3c42e04c0e0f2c496ff1784e543fbefe91e215a"
integrity sha512-81KhEjo7YAOh0vQJoSmAD68wLfYqJvoiD4ulyedzF+OEk/bk6/hx3fTNVfuzugIIaTrOx4PGx6pAiBRe5e9Zmg==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/shared" "3.2.37"
estree-walker "^2.0.2"
source-map "^0.6.1"
"@vue/compiler-dom@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.37.tgz#10d2427a789e7c707c872da9d678c82a0c6582b5"
integrity sha512-yxJLH167fucHKxaqXpYk7x8z7mMEnXOw3G2q62FTkmsvNxu4FQSu5+3UMb+L7fjKa26DEzhrmCxAgFLLIzVfqQ==
dependencies:
"@vue/compiler-core" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/compiler-sfc@^3.2.20":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.37.tgz#3103af3da2f40286edcd85ea495dcb35bc7f5ff4"
integrity sha512-+7i/2+9LYlpqDv+KTtWhOZH+pa8/HnX/905MdVmAcI/mPQOBwkHHIzrsEsucyOIZQYMkXUiTkmZq5am/NyXKkg==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.37"
"@vue/compiler-dom" "3.2.37"
"@vue/compiler-ssr" "3.2.37"
"@vue/reactivity-transform" "3.2.37"
"@vue/shared" "3.2.37"
estree-walker "^2.0.2"
magic-string "^0.25.7"
postcss "^8.1.10"
source-map "^0.6.1"
"@vue/compiler-ssr@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.37.tgz#4899d19f3a5fafd61524a9d1aee8eb0505313cff"
integrity sha512-7mQJD7HdXxQjktmsWp/J67lThEIcxLemz1Vb5I6rYJHR5vI+lON3nPGOH3ubmbvYGt8xEUaAr1j7/tIFWiEOqw==
dependencies:
"@vue/compiler-dom" "3.2.37"
"@vue/shared" "3.2.37"
"@vue/reactivity-transform@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.37.tgz#0caa47c4344df4ae59f5a05dde2a8758829f8eca"
integrity sha512-IWopkKEb+8qpu/1eMKVeXrK0NLw9HicGviJzhJDEyfxTR9e1WtpnnbYkJWurX6WwoFP0sz10xQg8yL8lgskAZg==
dependencies:
"@babel/parser" "^7.16.4"
"@vue/compiler-core" "3.2.37"
"@vue/shared" "3.2.37"
estree-walker "^2.0.2"
magic-string "^0.25.7"
"@vue/shared@3.2.37":
version "3.2.37"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.37.tgz#8e6adc3f2759af52f0e85863dfb0b711ecc5c702"
integrity sha512-4rSJemR2NQIo9Klm1vabqWjD8rs/ZaJSzMxkMNeJS6lHiUjjUeYFbooN19NgFjztubEKh3WlZUeOLVdbbUWHsw==
abbrev@1: abbrev@1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@ -1777,6 +1845,11 @@ body-parser@^1.19.0:
raw-body "2.4.3" raw-body "2.4.3"
type-is "~1.6.18" type-is "~1.6.18"
boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==
brace-expansion@^1.1.7: brace-expansion@^1.1.7:
version "1.1.11" version "1.1.11"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
@ -2032,6 +2105,11 @@ commander@^2.19.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33"
integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==
commander@^7.2.0:
version "7.2.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
concat-map@0.0.1: concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -2108,6 +2186,37 @@ cross-spawn@^7.0.2:
shebang-command "^2.0.0" shebang-command "^2.0.0"
which "^2.0.1" which "^2.0.1"
css-select@^4.1.3:
version "4.3.0"
resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b"
integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==
dependencies:
boolbase "^1.0.0"
css-what "^6.0.1"
domhandler "^4.3.1"
domutils "^2.8.0"
nth-check "^2.0.1"
css-tree@^1.1.2, css-tree@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d"
integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
dependencies:
mdn-data "2.0.14"
source-map "^0.6.1"
css-what@^6.0.1:
version "6.1.0"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4"
integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==
csso@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529"
integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==
dependencies:
css-tree "^1.1.2"
dashdash@^1.12.0: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
@ -2257,14 +2366,14 @@ domelementtype@^2.0.1, domelementtype@^2.2.0:
resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d"
integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==
domhandler@^4.0.0, domhandler@^4.2.0: domhandler@^4.0.0, domhandler@^4.2.0, domhandler@^4.3.1:
version "4.3.1" version "4.3.1"
resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c"
integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==
dependencies: dependencies:
domelementtype "^2.2.0" domelementtype "^2.2.0"
domutils@^2.5.2: domutils@^2.5.2, domutils@^2.8.0:
version "2.8.0" version "2.8.0"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135"
integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==
@ -2695,7 +2804,7 @@ estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0:
resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123"
integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
estree-walker@^2.0.1: estree-walker@^2.0.1, estree-walker@^2.0.2:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
@ -3265,6 +3374,11 @@ http-signature@~1.3.1:
jsprim "^2.0.2" jsprim "^2.0.2"
sshpk "^1.14.1" sshpk "^1.14.1"
http-status-codes@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/http-status-codes/-/http-status-codes-2.2.0.tgz#bb2efe63d941dfc2be18e15f703da525169622be"
integrity sha512-feERVo9iWxvnejp3SEfm/+oNG517npqL2/PIA8ORjyOZjGC7TwCRQsZylciLS64i6pJ0wRYz3rkXLRwbtFa8Ng==
iconv-lite@0.4.24: iconv-lite@0.4.24:
version "0.4.24" version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@ -3853,6 +3967,13 @@ lru-cache@^7.10.1:
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.13.1.tgz#267a81fbd0881327c46a81c5922606a2cfe336c4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.13.1.tgz#267a81fbd0881327c46a81c5922606a2cfe336c4"
integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ== integrity sha512-CHqbAq7NFlW3RSnoWXLJBxCWaZVBrfa9UEHId2M3AW8iEBurbqduNexEUCGc3SHc6iCYXNJCDi903LajSVAEPQ==
magic-string@^0.25.7:
version "0.25.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c"
integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==
dependencies:
sourcemap-codec "^1.4.8"
make-error@^1.1.1: make-error@^1.1.1:
version "1.3.6" version "1.3.6"
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
@ -3935,6 +4056,11 @@ matrix-widget-api@^1.0.0:
"@types/events" "^3.0.0" "@types/events" "^3.0.0"
events "^3.2.0" events "^3.2.0"
mdn-data@2.0.14:
version "2.0.14"
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
mdurl@^1.0.1: mdurl@^1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e"
@ -4115,7 +4241,7 @@ nanoid@3.1.20:
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788"
integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw==
nanoid@^3.3.3: nanoid@^3.1.30, nanoid@^3.3.3, nanoid@^3.3.4:
version "3.3.4" version "3.3.4"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab"
integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==
@ -4197,6 +4323,13 @@ normalize-path@^3.0.0, normalize-path@~3.0.0:
resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65"
integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
nth-check@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d"
integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==
dependencies:
boolbase "^1.0.0"
oauth-sign@~0.9.0: oauth-sign@~0.9.0:
version "0.9.0" version "0.9.0"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
@ -4436,7 +4569,25 @@ pify@^3.0.0:
resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176" resolved "https://registry.yarnpkg.com/pify/-/pify-3.0.0.tgz#e5a4acd2c101fdf3d9a4d07f0dbc4db49dd28176"
integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY= integrity sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=
postcss@^8.3.11, postcss@^8.4.13: postcss@^8.1.10:
version "8.4.16"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.16.tgz#33a1d675fac39941f5f445db0de4db2b6e01d43c"
integrity sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==
dependencies:
nanoid "^3.3.4"
picocolors "^1.0.0"
source-map-js "^1.0.2"
postcss@^8.3.11:
version "8.4.5"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.5.tgz#bae665764dfd4c6fcc24dc0fdf7e7aa00cc77f95"
integrity sha512-jBDboWM8qpaqwkMwItqTQTiFikhs/67OYVvblFFTM7MrZjt6yMKd6r2kgXizEbTTljacm4NldIlZnhbjr84QYg==
dependencies:
nanoid "^3.1.30"
picocolors "^1.0.0"
source-map-js "^1.0.1"
postcss@^8.4.13:
version "8.4.13" version "8.4.13"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.13.tgz#7c87bc268e79f7f86524235821dfdf9f73e5d575"
integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA== integrity sha512-jtL6eTBrza5MPzy8oJLFuUscHDXTV5KcLlqAWHl5q5WYRfnNRGSmOZmOZ1T6Gy7A99mOZfqungmZMpMmCVJ8ZA==
@ -4971,7 +5122,7 @@ slash@^3.0.0:
resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634"
integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: "source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.1, source-map-js@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
@ -4989,11 +5140,16 @@ source-map@^0.5.0:
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w= integrity sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=
source-map@^0.6.0: source-map@^0.6.0, source-map@^0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263"
integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
sourcemap-codec@^1.4.8:
version "1.4.8"
resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4"
integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==
sprintf-js@1.1.2: sprintf-js@1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673"
@ -5029,6 +5185,11 @@ sshpk@^1.7.0:
safer-buffer "^2.0.2" safer-buffer "^2.0.2"
tweetnacl "~0.14.0" tweetnacl "~0.14.0"
stable@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
stack-trace@0.0.x: stack-trace@0.0.x:
version "0.0.10" version "0.0.10"
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0" resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
@ -5177,6 +5338,19 @@ supports-preserve-symlinks-flag@^1.0.0:
resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09"
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
svgo@^2.7.0:
version "2.8.0"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24"
integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==
dependencies:
"@trysound/sax" "0.2.0"
commander "^7.2.0"
css-select "^4.1.3"
css-tree "^1.1.3"
csso "^4.2.0"
picocolors "^1.0.0"
stable "^0.1.8"
tdigest@^0.1.1: tdigest@^0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021" resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021"
@ -5390,6 +5564,14 @@ verror@1.10.0:
core-util-is "1.0.2" core-util-is "1.0.2"
extsprintf "^1.2.0" extsprintf "^1.2.0"
vite-svg-loader@^3.4.0:
version "3.4.0"
resolved "https://registry.yarnpkg.com/vite-svg-loader/-/vite-svg-loader-3.4.0.tgz#4638827fe86b85ecfcea1ad61dd972c351d5befd"
integrity sha512-xD3yb1FX+f4l9/TmsYIqyki8ncpcVsZ2gEJFh/wLuNNqt55C8OJ+JlcMWOA/Z9gRA+ylV/TA1wmJLxzZkCRqlA==
dependencies:
"@vue/compiler-sfc" "^3.2.20"
svgo "^2.7.0"
vite@^2.9.13: vite@^2.9.13:
version "2.9.13" version "2.9.13"
resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.13.tgz#859cb5d4c316c0d8c6ec9866045c0f7858ca6abc" resolved "https://registry.yarnpkg.com/vite/-/vite-2.9.13.tgz#859cb5d4c316c0d8c6ec9866045c0f7858ca6abc"