Convert bridgeAPI usages to preact context. (#871)

* Fix widget client only talking to localhost

* Improve error text around widget communication.

* changelog

* Remove unused.

* Simplify code by using a context for bridge API.
This commit is contained in:
Will Hunt 2024-01-16 09:44:51 +00:00 committed by GitHub
parent caaabbc300
commit 6d3800a018
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 71 additions and 65 deletions

1
changelog.d/870.bugfix Normal file
View File

@ -0,0 +1 @@
Fix widgets failing with "Request timed out".

View File

@ -7,6 +7,7 @@ import { LoadingSpinner } from './components/elements/LoadingSpinner';
import AdminSettings from './components/AdminSettings';
import RoomConfigView from './components/RoomConfigView';
import { Alert } from '@vector-im/compound-web';
import { BridgeContext } from './context';
interface IMinimalState {
error: string|null,
@ -115,43 +116,39 @@ export default class App extends Component<void, IState> {
}
render() {
const style = {
padding: 'embedType' in this.state && this.state.embedType === EmbedType.IntegrationManager ? "0" : "16px",
};
if (this.state.error) {
return <div style={style}><Alert type="critical" title="An error occured">{this.state.error}</Alert></div>;
} else if (this.state.busy) {
return <div style={style}><LoadingSpinner /></div>;
} else if ("kind" in this.state === false) {
console.warn("invalid state", this.state);
return <div style={style}><Alert type="critical" title="An error occured">Widget got into an invalid state.</Alert></div>;
}
// Return the App component.
let content;
if (this.state.error) {
content = <Alert type="critical" title="An error occured">{this.state.error}</Alert>;
} else if (this.state.busy) {
content = <LoadingSpinner />;
}
if ("kind" in this.state) {
if (this.state.roomState && this.state.kind === "admin") {
content = <AdminSettings bridgeApi={this.state.bridgeApi} roomState={this.state.roomState} />;
} else if (this.state.kind === "invite") {
// Fall through for now, we don't support invite widgets *just* yet.
} else if (this.state.kind === "roomConfig") {
content = <RoomConfigView
roomId={this.state.roomId}
supportedServices={this.state.supportedServices}
serviceScope={this.state.serviceScope}
embedType={this.state.embedType}
bridgeApi={this.state.bridgeApi}
widgetApi={this.state.widgetApi}
/>;
}
if (this.state.kind === "admin") {
content = <AdminSettings roomState={this.state.roomState} />;
}else if (this.state.kind === "roomConfig") {
content = <RoomConfigView
roomId={this.state.roomId}
supportedServices={this.state.supportedServices}
serviceScope={this.state.serviceScope}
embedType={this.state.embedType}
/>;
} else {
return <div style={style}><Alert type="critical" title="An error occured">Unknown widget kind.</Alert></div>;
}
if (!content) {
console.warn("invalid state", this.state);
content = <b>Invalid state</b>;
}
const embedType = 'embedType' in this.state ? this.state.embedType : EmbedType.Default;
return (
<div style={{
padding: embedType === "integration-manager" ? "0" : "16px",
}}>
{content}
<div style={style}>
<BridgeContext.Provider value={{bridgeApi: this.state.bridgeApi}}>
{content}
</BridgeContext.Provider>
</div>
);
}

View File

@ -171,7 +171,6 @@ export enum EmbedType {
}
export type BridgeConfig = FunctionComponent<{
api: BridgeAPI,
roomId: string,
showHeader: boolean,
}>;

View File

@ -1,6 +1,5 @@
import { WidgetApi } from "matrix-widget-api";
import { useState } from "preact/hooks"
import { BridgeAPI, BridgeConfig, EmbedType } from "../BridgeAPI";
import { BridgeConfig, EmbedType } from "../BridgeAPI";
import style from "./RoomConfigView.module.scss";
import { ConnectionCard } from "./ConnectionCard";
import { FeedsConfig } from "./roomConfig/FeedsConfig";
@ -17,8 +16,6 @@ import WebhookIcon from "../icons/webhook.png";
interface IProps {
widgetApi: WidgetApi,
bridgeApi: BridgeAPI,
supportedServices: {[service: string]: boolean},
serviceScope?: string,
embedType: EmbedType,
@ -86,7 +83,6 @@ export default function RoomConfigView(props: IProps) {
const ConfigComponent = connections[activeConnectionType].component;
content = <ConfigComponent
roomId={props.roomId}
api={props.bridgeApi}
showHeader={props.embedType !== EmbedType.IntegrationManager}
/>;
} else {
@ -109,6 +105,7 @@ export default function RoomConfigView(props: IProps) {
}
return <div className={style.root}>
{!serviceScope && activeConnectionType &&
<header>
<span className={style.backButton} onClick={() => setActiveConnectionType(null)}>

View File

@ -1,24 +1,23 @@
import { useCallback, useEffect, useState } from "preact/hooks";
import { useCallback, useContext, useEffect, useState } from "preact/hooks";
import { GetAuthResponse } from "../../../src/Widgets/BridgeWidgetInterface";
import { BridgeAPI } from "../../BridgeAPI";
import { Button } from "../elements";
import { BridgeContext } from "../../context";
const PollAuthEveryMs = 3000;
export const ServiceAuth = ({
api,
service,
loginLabel = "Log in",
authState,
onAuthSucceeded,
}: {
api: BridgeAPI,
service: string,
authState: GetAuthResponse,
onAuthSucceeded: () => void,
loginLabel?: string,
}) => {
const api = useContext(BridgeContext).bridgeApi;
const [pollStateId, setPollStateId] = useState<string|null>();
const pollAuth = useCallback(async (pollId) => {

View File

@ -92,12 +92,11 @@ const roomConfigText: IRoomConfigText = {
const RoomConfigListItemFunc = (c: FeedResponseItem) => c.config.label || c.config.url;
export const FeedsConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
export const FeedsConfig: BridgeConfig = ({ roomId, showHeader }) => {
return <RoomConfig<ServiceConfig, FeedResponseItem, FeedConnectionState>
headerImg={FeedsIcon}
showHeader={showHeader}
api={api}
roomId={roomId}
type="feeds"
connectionEventType="uk.half-shot.matrix-hookshot.feed"

View File

@ -111,12 +111,11 @@ const RoomConfigText = {
const RoomConfigListItemFunc = (c: GenericHookResponseItem) => c.config.name;
export const GenericWebhookConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
export const GenericWebhookConfig: BridgeConfig = ({ roomId, showHeader }) => {
return <RoomConfig<ServiceConfig, GenericHookResponseItem, GenericHookConnectionState>
headerImg={WebhookIcon}
darkHeaderImg={true}
showHeader={showHeader}
api={api}
roomId={roomId}
type="generic"
connectionEventType="uk.half-shot.matrix-hookshot.generic.hook"

View File

@ -5,11 +5,12 @@ import { EventHookCheckbox } from '../elements/EventHookCheckbox';
import { FunctionComponent, createRef } from "preact";
import { GitHubRepoConnectionState, GitHubRepoResponseItem, GitHubRepoConnectionRepoTarget, GitHubRepoConnectionOrgTarget } from "../../../src/Connections/GithubRepo";
import { InputField, ButtonSet, Button } from "../elements";
import { useState, useCallback, useMemo, useEffect } from "preact/hooks";
import { useState, useCallback, useMemo, useEffect, useContext } from "preact/hooks";
import { DropItem } from "../elements/DropdownSearch";
import ConnectionSearch from "../elements/ConnectionSearch";
import { ServiceAuth } from "./Auth";
import { GetAuthResponse } from "../../../src/Widgets/BridgeWidgetInterface";
import { BridgeContext } from "../../context";
const EventType = "uk.half-shot.matrix-hookshot.github.repository";
@ -18,11 +19,12 @@ function getRepoFullName(state: GitHubRepoConnectionState) {
}
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitHubRepoResponseItem, GitHubRepoConnectionState>> = ({
showAuthPrompt, loginLabel, serviceConfig, api, existingConnection, onSave, onRemove, isUpdating
showAuthPrompt, loginLabel, serviceConfig, existingConnection, onSave, onRemove, isUpdating
}) => {
// Assume true if we have no auth prompt.
const [authedResponse, setAuthResponse] = useState<GetAuthResponse|null>(null);
const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []);
const api = useContext(BridgeContext).bridgeApi;
const checkAuth = useCallback(() => {
api.getAuth("github").then((res) => {
@ -106,7 +108,7 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
const consideredAuthenticated = (authedResponse?.authenticated || !showAuthPrompt);
return <form onSubmit={handleSave}>
{authedResponse && <ServiceAuth onAuthSucceeded={checkAuth} authState={authedResponse} service="github" loginLabel={loginLabel} api={api} />}
{authedResponse && <ServiceAuth onAuthSucceeded={checkAuth} authState={authedResponse} service="github" loginLabel={loginLabel} />}
{!existingConnection && consideredAuthenticated && <ConnectionSearch
serviceName="GitHub"
addNewInstanceUrl={newInstallationUrl}
@ -175,12 +177,11 @@ const roomConfigText: IRoomConfigText = {
const RoomConfigListItemFunc = (c: GitHubRepoResponseItem) => getRepoFullName(c.config);
export const GithubRepoConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
export const GithubRepoConfig: BridgeConfig = ({ roomId, showHeader }) => {
return <RoomConfig<never, GitHubRepoResponseItem, GitHubRepoConnectionState>
headerImg={GitHubIcon}
darkHeaderImg={true}
showHeader={showHeader}
api={api}
roomId={roomId}
type="github"
showAuthPrompt={true}

View File

@ -5,13 +5,15 @@ import { EventHookCheckbox } from '../elements/EventHookCheckbox';
import { GitLabRepoConnectionState, GitLabRepoResponseItem, GitLabRepoConnectionProjectTarget, GitLabRepoConnectionInstanceTarget } from "../../../src/Connections/GitlabRepo";
import { InputField, ButtonSet, Button } from "../elements";
import { FunctionComponent, createRef } from "preact";
import { useState, useCallback, useMemo } from "preact/hooks";
import { useState, useCallback, useMemo, useContext } from "preact/hooks";
import { DropItem } from "../elements/DropdownSearch";
import { ConnectionSearch } from "../elements/ConnectionSearch";
import { BridgeContext } from "../../context";
const EventType = "uk.half-shot.matrix-hookshot.gitlab.repository";
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitLabRepoResponseItem, GitLabRepoConnectionState>> = ({api, existingConnection, onSave, onRemove, isUpdating }) => {
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitLabRepoResponseItem, GitLabRepoConnectionState>> = ({existingConnection, onSave, onRemove, isUpdating }) => {
const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []);
const api = useContext(BridgeContext).bridgeApi;
const toggleEnabledHook = useCallback((evt: any) => {
const key = (evt.target as HTMLElement).getAttribute('x-event-name');
@ -123,11 +125,10 @@ const RoomConfigText = {
const RoomConfigListItemFunc = (c: GitLabRepoResponseItem) => c.config.path;
export const GitlabRepoConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
export const GitlabRepoConfig: BridgeConfig = ({ roomId, showHeader }) => {
return <RoomConfig<never, GitLabRepoResponseItem, GitLabRepoConnectionState>
headerImg={GitLabIcon}
showHeader={showHeader}
api={api}
roomId={roomId}
type="gitlab"
text={RoomConfigText}

View File

@ -1,5 +1,5 @@
import { FunctionComponent, createRef } from "preact";
import { useState, useCallback, useMemo } from "preact/hooks";
import { useState, useCallback, useMemo, useContext } from "preact/hooks";
import { BridgeConfig } from "../../BridgeAPI";
import { ConnectionConfigurationProps, RoomConfig } from "./RoomConfig";
import { JiraProjectConnectionState, JiraProjectResponseItem, JiraProjectConnectionProjectTarget, JiraProjectConnectionInstanceTarget } from "../../../src/Connections/JiraProject";
@ -8,11 +8,13 @@ import { EventHookCheckbox } from '../elements/EventHookCheckbox';
import JiraIcon from "../../icons/jira.png";
import ConnectionSearch from "../elements/ConnectionSearch";
import { DropItem } from "../elements/DropdownSearch";
import { BridgeContext } from "../../context";
const EventType = "uk.half-shot.matrix-hookshot.jira.project";
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, JiraProjectResponseItem, JiraProjectConnectionState>> = ({api, existingConnection, onSave, onRemove, isUpdating }) => {
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, JiraProjectResponseItem, JiraProjectConnectionState>> = ({existingConnection, onSave, onRemove, isUpdating }) => {
const [allowedEvents, setAllowedEvents] = useState<string[]>(existingConnection?.config.events || ['issue_created']);
const api = useContext(BridgeContext).bridgeApi;
const toggleEvent = useCallback((evt: Event) => {
const key = (evt.target as HTMLElement).getAttribute('x-event-name');
@ -108,11 +110,10 @@ const RoomConfigText = {
const RoomConfigListItemFunc = (c: JiraProjectResponseItem) => c.config.url;
export const JiraProjectConfig: BridgeConfig = ({ api, roomId, showHeader }) => {
export const JiraProjectConfig: BridgeConfig = ({ roomId, showHeader }) => {
return <RoomConfig<never, JiraProjectResponseItem, JiraProjectConnectionState>
headerImg={JiraIcon}
showHeader={showHeader}
api={api}
roomId={roomId}
type="jira"
text={RoomConfigText}

View File

@ -1,6 +1,6 @@
import { FunctionComponent } from "preact";
import { useCallback, useEffect, useReducer, useState } from "preact/hooks"
import { BridgeAPI, BridgeAPIError } from "../../BridgeAPI";
import { useCallback, useContext, useEffect, useReducer, useState } from "preact/hooks"
import { BridgeAPIError } from "../../BridgeAPI";
import { ListItem, Card } from "../elements";
import style from "./RoomConfig.module.scss";
import { GetConnectionsResponseItem } from "../../../src/provisioning/api";
@ -9,6 +9,7 @@ import { LoadingSpinner } from '../elements/LoadingSpinner';
import { ErrCode } from "../../../src/api";
import { retry } from "../../../src/PromiseUtil";
import { Alert } from "@vector-im/compound-web";
import { BridgeContext } from "../../context";
export interface ConnectionConfigurationProps<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState extends IConnectionState> {
serviceConfig: SConfig;
@ -19,7 +20,6 @@ export interface ConnectionConfigurationProps<SConfig, ConnectionType extends Ge
isMigrationCandidate?: boolean,
existingConnection?: ConnectionType;
onRemove?: () => void,
api: BridgeAPI;
}
export interface IRoomConfigText {
@ -31,7 +31,6 @@ export interface IRoomConfigText {
}
interface IRoomConfigProps<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState extends IConnectionState> {
api: BridgeAPI;
roomId: string;
type: string;
showAuthPrompt?: boolean;
@ -48,7 +47,6 @@ const MAX_CONNECTION_FETCH_ATTEMPTS = 10;
export const RoomConfig = function<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState extends IConnectionState>(props: IRoomConfigProps<SConfig, ConnectionType, ConnectionState>) {
const {
api,
roomId,
type,
showAuthPrompt = false,
@ -59,6 +57,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
listItemName,
connectionEventType,
} = props;
const api = useContext(BridgeContext).bridgeApi;
const ConnectionConfigComponent = props.connectionConfigComponent;
const [ error, setError ] = useState<null|{header?: string, message: string, isWarning?: boolean, forPrevious?: boolean}>(null);
const [ connections, setConnections ] = useState<ConnectionType[]|null>(null);
@ -153,7 +152,6 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
<h2>{text.createNew}</h2>
{serviceConfig && <ConnectionConfigComponent
key={newConnectionKey}
api={api}
serviceConfig={serviceConfig}
onSave={handleSaveOnCreation}
loginLabel={text.login}
@ -166,7 +164,6 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
<h2>{ canEditRoom ? text.listCanEdit : text.listCantEdit }</h2>
{ serviceConfig && connections?.map(c => <ListItem key={c.id} text={listItemName(c)}>
<ConnectionConfigComponent
api={api}
serviceConfig={serviceConfig}
existingConnection={c}
onSave={(config) => {

15
web/context.ts Normal file
View File

@ -0,0 +1,15 @@
import { createContext } from "preact";
import type { BridgeAPI } from "./BridgeAPI";
interface IBridgeContext {
bridgeApi: BridgeAPI;
}
const fakeBridgeContext = {
get bridgeApi(): BridgeAPI {
throw Error('No context provided');
}
}
export const BridgeContext = createContext<IBridgeContext>(fakeBridgeContext);