From ec9f44b2b1bd37431bb6c5ed3256a2d71a6c25af Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti <andrewf@element.io> Date: Fri, 4 Nov 2022 11:54:47 -0400 Subject: [PATCH 1/4] Allow GitLab connections without hook permissions Warn instead of fail when connecting a GitLab project that Hookshot cannot provision a webhook for. --- changelog.d/567.feature | 1 + .../room_configuration/gitlab_project.md | 4 ++-- src/ConnectionManager.ts | 8 +++---- src/Connections/GitlabRepo.ts | 16 +++++++++---- src/Connections/IConnection.ts | 4 ++-- src/Connections/SetupConnection.ts | 4 ++-- src/Widgets/BridgeWidgetApi.ts | 9 +++++--- src/provisioning/api.ts | 6 +++++ src/provisioning/provisioner.ts | 15 +++++++----- web/BridgeAPI.ts | 2 +- web/components/elements/ErrorPane.module.scss | 4 ++++ web/components/elements/ErrorPane.tsx | 9 ++++---- web/components/roomConfig/RoomConfig.tsx | 23 ++++++++++++------- web/icons/error-badge.svg | 5 ++++ web/icons/warning-badge.svg | 2 +- web/typings/images.d.ts | 4 ++++ 16 files changed, 79 insertions(+), 37 deletions(-) create mode 100644 changelog.d/567.feature create mode 100644 web/icons/error-badge.svg diff --git a/changelog.d/567.feature b/changelog.d/567.feature new file mode 100644 index 000000000..33461aac9 --- /dev/null +++ b/changelog.d/567.feature @@ -0,0 +1 @@ +Allow adding connections to GitLab projects even when Hookshot doesn't have permissions to automatically provision a webhook for it. When that occurs, tell the user to ask a project admin to add the webhook. diff --git a/docs/usage/room_configuration/gitlab_project.md b/docs/usage/room_configuration/gitlab_project.md index 9a31bc9fe..8046c07fd 100644 --- a/docs/usage/room_configuration/gitlab_project.md +++ b/docs/usage/room_configuration/gitlab_project.md @@ -14,8 +14,8 @@ To set up a connection to a GitLab project in a new room: 3. Give the bridge bot moderator permissions or higher (power level 50) (or otherwise configure the room so the bot can edit room state). 4. Send the command `!hookshot gitlab project https://mydomain/my/project`. 5. If you have permission to bridge this repo, the bridge will respond with a confirmation message. (Users with `Developer` permissions or greater can bridge projects.) - 6. If you have configured the bridge with a `publicUrl` inside `gitlab.webhook`, it will automatically provision the webhook for you. - 7. Otherwise, you'll need to manually configure the webhook to point to your public address for the webhooks listener. +6. If you have configured the bridge with a `publicUrl` inside `gitlab.webhook`, and you have `Maintainer` permissions or greater on the project, the bot will automatically provision the webhook for you. +7. Otherwise, you'll need to manually configure the project with a webhook that points to your public address for the webhooks listener, and with all relevant Triggers enabled (as Hookshot can only bridge events for enabled Triggers). This can be configured on the GitLab webpage for the project under Settings > Webhook Settings. If you do not have access to this page, you must ask someone who does (i.e. someone with at least `Maintainer` permissions on the project) to add the webhook for you. ## Configuration diff --git a/src/ConnectionManager.ts b/src/ConnectionManager.ts index 6badf88fa..0b3af130d 100644 --- a/src/ConnectionManager.ts +++ b/src/ConnectionManager.ts @@ -68,14 +68,14 @@ export class ConnectionManager extends EventEmitter { * @param data The data corresponding to the connection state. This will be validated. * @returns The resulting connection. */ - public async provisionConnection(roomId: string, userId: string, type: string, data: Record<string, unknown>): Promise<IConnection> { + public async provisionConnection(roomId: string, userId: string, type: string, data: Record<string, unknown>) { log.info(`Looking to provision connection for ${roomId} ${type} for ${userId} with data ${JSON.stringify(data)}`); const connectionType = ConnectionDeclarations.find(c => c.EventTypes.includes(type)); if (connectionType?.provisionConnection) { if (!this.config.checkPermission(userId, connectionType.ServiceCategory, BridgePermissionLevel.manageConnections)) { throw new ApiError(`User is not permitted to provision connections for this type of service.`, ErrCode.ForbiddenUser); } - const { connection } = await connectionType.provisionConnection(roomId, userId, data, { + const result = await connectionType.provisionConnection(roomId, userId, data, { as: this.as, config: this.config, tokenStore: this.tokenStore, @@ -85,8 +85,8 @@ export class ConnectionManager extends EventEmitter { github: this.github, getAllConnectionsOfType: this.getAllConnectionsOfType.bind(this), }); - this.push(connection); - return connection; + this.push(result.connection); + return result; } throw new ApiError(`Connection type not known`); } diff --git a/src/Connections/GitlabRepo.ts b/src/Connections/GitlabRepo.ts index c8c1131c4..741fe2e73 100644 --- a/src/Connections/GitlabRepo.ts +++ b/src/Connections/GitlabRepo.ts @@ -10,7 +10,7 @@ import { BridgeConfigGitLab, GitLabInstance } from "../Config/Config"; import { IGitlabMergeRequest, IGitlabProject, IGitlabUser, IGitLabWebhookMREvent, IGitLabWebhookNoteEvent, IGitLabWebhookPushEvent, IGitLabWebhookReleaseEvent, IGitLabWebhookTagPushEvent, IGitLabWebhookWikiPageEvent } from "../Gitlab/WebhookTypes"; import { CommandConnection } from "./CommandConnection"; import { Connection, IConnection, IConnectionState, InstantiateConnectionOpts, ProvisionConnectionOpts } from "./IConnection"; -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api"; import { ErrCode, ApiError, ValidatorApiError } from "../api" import { AccessLevel } from "../Gitlab/Types"; import Ajv, { JSONSchemaType } from "ajv"; @@ -217,7 +217,9 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection } // Try to setup a webhook - if (gitlabConfig.webhook.publicUrl) { + // Requires at least a "Maintainer" role: https://docs.gitlab.com/ee/user/permissions.html + let warning: ConnectionWarning | undefined; + if (gitlabConfig.webhook.publicUrl && permissionLevel >= AccessLevel.Maintainer) { const hooks = await client.projects.hooks.list(project.id); const hasHook = hooks.find(h => h.url === gitlabConfig.webhook.publicUrl); if (!hasHook) { @@ -235,11 +237,17 @@ export class GitLabRepoConnection extends CommandConnection<GitLabRepoConnection wiki_page_events: true, }); } - } else { + } else if (!gitlabConfig.webhook.publicUrl) { log.info(`Not creating webhook, webhookUrl is not defined in config`); + } else { + warning = { + header: "Cannot create webhook", + message: "You have insufficient permissions on this project to provision a webhook for it. Ask a Maintainer or Owner of the project to add the webhook for you.", + }; + log.warn(`Not creating webhook, permission level is insufficient (${permissionLevel} < ${AccessLevel.Maintainer})`) } await as.botIntent.underlyingClient.sendStateEvent(roomId, this.CanonicalEventType, connection.stateKey, validData); - return {connection}; + return {connection, warning}; } public static getProvisionerDetails(botUserId: string) { diff --git a/src/Connections/IConnection.ts b/src/Connections/IConnection.ts index c197b4a17..2905a2855 100644 --- a/src/Connections/IConnection.ts +++ b/src/Connections/IConnection.ts @@ -1,6 +1,6 @@ import { MatrixEvent, MatrixMessageContent } from "../MatrixEvent"; import { IssuesOpenedEvent, IssuesEditedEvent } from "@octokit/webhooks-types"; -import { GetConnectionsResponseItem } from "../provisioning/api"; +import { ConnectionWarning, GetConnectionsResponseItem } from "../provisioning/api"; import { Appservice, IRichReplyMetadata, StateEvent } from "matrix-bot-sdk"; import { BridgeConfig, BridgePermissionLevel } from "../Config/Config"; import { UserTokenStore } from "../UserTokenStore"; @@ -80,7 +80,7 @@ export interface IConnection { export interface ConnectionDeclaration<C extends IConnection = IConnection> { EventTypes: string[]; ServiceCategory: string; - provisionConnection?: (roomId: string, userId: string, data: Record<string, unknown>, opts: ProvisionConnectionOpts) => Promise<{connection: C}>; + provisionConnection?: (roomId: string, userId: string, data: Record<string, unknown>, opts: ProvisionConnectionOpts) => Promise<{connection: C, warning?: ConnectionWarning}>; createConnectionForState: (roomId: string, state: StateEvent<Record<string, unknown>>, opts: InstantiateConnectionOpts) => C|Promise<C> } diff --git a/src/Connections/SetupConnection.ts b/src/Connections/SetupConnection.ts index 17fe1fe1b..6ef889c7a 100644 --- a/src/Connections/SetupConnection.ts +++ b/src/Connections/SetupConnection.ts @@ -108,9 +108,9 @@ export class SetupConnection extends CommandConnection { if (!path) { throw new CommandError("Invalid GitLab url", "The GitLab project url you entered was not valid."); } - const {connection} = await GitLabRepoConnection.provisionConnection(this.roomId, userId, {path, instance: name}, this.provisionOpts); + const {connection, warning} = await GitLabRepoConnection.provisionConnection(this.roomId, userId, {path, instance: name}, this.provisionOpts); this.pushConnections(connection); - await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${connection.prettyPath}`); + await this.as.botClient.sendNotice(this.roomId, `Room configured to bridge ${connection.prettyPath}` + (warning ? `\n${warning.header}: ${warning.message}` : "")); } private async checkJiraLogin(userId: string, urlStr: string) { diff --git a/src/Widgets/BridgeWidgetApi.ts b/src/Widgets/BridgeWidgetApi.ts index 7b57057d4..795cfdd24 100644 --- a/src/Widgets/BridgeWidgetApi.ts +++ b/src/Widgets/BridgeWidgetApi.ts @@ -134,11 +134,14 @@ export class BridgeWidgetApi { throw new ApiError("A JSON body must be provided", ErrCode.BadValue); } this.connMan.validateCommandPrefix(req.params.roomId, req.body); - const connection = await this.connMan.provisionConnection(req.params.roomId as string, req.userId, req.params.type as string, req.body as Record<string, unknown>); - if (!connection.getProvisionerDetails) { + const result = await this.connMan.provisionConnection(req.params.roomId, req.userId, req.params.type, req.body); + if (!result.connection.getProvisionerDetails) { throw new Error('Connection supported provisioning but not getProvisionerDetails'); } - res.send(connection.getProvisionerDetails(true)); + res.send({ + ...result.connection.getProvisionerDetails(true), + warning: result.warning, + }); } catch (ex) { log.error(`Failed to create connection for ${req.params.roomId}`, ex); throw ex; diff --git a/src/provisioning/api.ts b/src/provisioning/api.ts index 0cb448691..207600ab2 100644 --- a/src/provisioning/api.ts +++ b/src/provisioning/api.ts @@ -9,11 +9,17 @@ export interface GetConnectionTypeResponseItem { botUserId: string; } +export interface ConnectionWarning { + header: string, + message: string, +} + export interface GetConnectionsResponseItem<Config = object, Secrets = object> extends GetConnectionTypeResponseItem { id: string; config: Config; secrets?: Secrets; canEdit?: boolean; + warning?: ConnectionWarning; } const log = new Logger("Provisioner.api"); diff --git a/src/provisioning/provisioner.ts b/src/provisioning/provisioner.ts index 1f4480655..005ac944b 100644 --- a/src/provisioning/provisioner.ts +++ b/src/provisioning/provisioner.ts @@ -129,18 +129,21 @@ export class Provisioner { return res.send(connection.getProvisionerDetails()); } - private async putConnection(req: Request<{roomId: string, type: string}, unknown, Record<string, unknown>, {userId: string}>, res: Response, next: NextFunction) { + private async putConnection(req: Request<{roomId: string, type: string}, unknown, Record<string, unknown>, {userId: string}>, res: Response<GetConnectionsResponseItem>, next: NextFunction) { // Need to figure out which connections are available try { if (!req.body || typeof req.body !== "object") { - throw new ApiError("A JSON body must be provided.", ErrCode.BadValue); + throw new ApiError("A JSON body must be provided", ErrCode.BadValue); } this.connMan.validateCommandPrefix(req.params.roomId, req.body); - const connection = await this.connMan.provisionConnection(req.params.roomId, req.query.userId, req.params.type, req.body); - if (!connection.getProvisionerDetails) { - throw new Error('Connection supported provisioning but not getProvisionerDetails.'); + const result = await this.connMan.provisionConnection(req.params.roomId, req.query.userId, req.params.type, req.body); + if (!result.connection.getProvisionerDetails) { + throw new Error('Connection supported provisioning but not getProvisionerDetails'); } - res.send(connection.getProvisionerDetails(true)); + res.send({ + ...result.connection.getProvisionerDetails(true), + warning: result.warning, + }); } catch (ex) { log.error(`Failed to create connection for ${req.params.roomId}`, ex); return next(ex); diff --git a/web/BridgeAPI.ts b/web/BridgeAPI.ts index 22b4defae..79a25339c 100644 --- a/web/BridgeAPI.ts +++ b/web/BridgeAPI.ts @@ -117,7 +117,7 @@ export class BridgeAPI { return this.request('GET', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections/${encodeURIComponent(service)}`); } - async createConnection(roomId: string, type: string, config: IConnectionState) { + async createConnection(roomId: string, type: string, config: IConnectionState): Promise<GetConnectionsResponseItem> { return this.request('POST', `/widgetapi/v1/${encodeURIComponent(roomId)}/connections/${encodeURIComponent(type)}`, config); } diff --git a/web/components/elements/ErrorPane.module.scss b/web/components/elements/ErrorPane.module.scss index 811603b5b..1be7bae73 100644 --- a/web/components/elements/ErrorPane.module.scss +++ b/web/components/elements/ErrorPane.module.scss @@ -1,4 +1,8 @@ .errorPane { max-width: 480px; color: #FF4B55; +} +.warningPane { + max-width: 480px; + color: #CCAA00; } \ No newline at end of file diff --git a/web/components/elements/ErrorPane.tsx b/web/components/elements/ErrorPane.tsx index c481a603b..56563ea58 100644 --- a/web/components/elements/ErrorPane.tsx +++ b/web/components/elements/ErrorPane.tsx @@ -1,9 +1,10 @@ import { h, FunctionComponent } from "preact"; -import ErrorBadge from "../../icons/warning-badge.svg"; +import ErrorBadge from "../../icons/error-badge.svg"; +import WarningBadge from "../../icons/warning-badge.svg"; import style from "./ErrorPane.module.scss"; -export const ErrorPane: FunctionComponent<{header?: string}> = ({ children, header }) => { - return <div class={`card error ${style.errorPane}`}> - <p><strong><img src={ErrorBadge} /> { header || "Error occured during widget load" }</strong>: {children}</p> +export const ErrorPane: FunctionComponent<{header?: string, isWarning?: boolean}> = ({ children, header, isWarning }) => { + return <div class={`card error ${isWarning ? style.warningPane : style.errorPane}`}> + <p><strong><img src={isWarning ? WarningBadge : ErrorBadge } /> { header || `${isWarning ? "Problem" : "Error"} occured during widget load` }</strong>: {children}</p> </div>; }; \ No newline at end of file diff --git a/web/components/roomConfig/RoomConfig.tsx b/web/components/roomConfig/RoomConfig.tsx index bbe98dbd4..9e0fc48a3 100644 --- a/web/components/roomConfig/RoomConfig.tsx +++ b/web/components/roomConfig/RoomConfig.tsx @@ -34,18 +34,22 @@ interface IRoomConfigProps<SConfig, ConnectionType extends GetConnectionsRespons export const RoomConfig = function<SConfig, ConnectionType extends GetConnectionsResponseItem, ConnectionState extends IConnectionState>(props: IRoomConfigProps<SConfig, ConnectionType, ConnectionState>) { const { api, roomId, type, headerImg, text, listItemName, connectionEventType } = props; const ConnectionConfigComponent = props.connectionConfigComponent; - const [ error, setError ] = useState<null|{header?: string, message: string}>(null); + const [ error, setError ] = useState<null|{header?: string, message: string, isWarning?: boolean, forPrevious?: boolean}>(null); const [ connections, setConnections ] = useState<ConnectionType[]|null>(null); const [ serviceConfig, setServiceConfig ] = useState<SConfig|null>(null); const [ canEditRoom, setCanEditRoom ] = useState<boolean>(false); // We need to increment this every time we create a connection in order to properly reset the state. const [ newConnectionKey, incrementConnectionKey ] = useReducer<number, undefined>(n => n+1, 0); + const clearCurrentError = () => { + setError(error => error?.forPrevious ? error : null); + } + useEffect(() => { api.getConnectionsForService<ConnectionType>(roomId, type).then(res => { setCanEditRoom(res.canEdit); setConnections(res.connections); - setError(null); + clearCurrentError(); }).catch(ex => { console.warn("Failed to fetch existing connections", ex); setError({ @@ -58,9 +62,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection useEffect(() => { api.getServiceConfig<SConfig>(type) .then(setServiceConfig) - .then(() => { - setError(null); - }) + .then(clearCurrentError) .catch(ex => { console.warn("Failed to fetch service config", ex); setError({ @@ -71,10 +73,15 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection }, [api, type]); const handleSaveOnCreation = useCallback((config: ConnectionState) => { - api.createConnection(roomId, connectionEventType, config).then(() => { + api.createConnection(roomId, connectionEventType, config).then(result => { // Force reload incrementConnectionKey(undefined); - setError(null); + setError(!result.warning ? null : { + header: result.warning.header, + message: result.warning.message, + isWarning: true, + forPrevious: true, + }); }).catch(ex => { console.warn("Failed to create connection", ex); setError({ @@ -86,7 +93,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection return <main> { - error && <ErrorPane header={error.header || "Error"}>{error.message}</ErrorPane> + error && <ErrorPane header={error.header || "Error"} isWarning={error.isWarning}>{error.message}</ErrorPane> } <header className={style.header}> <img src={headerImg} /> diff --git a/web/icons/error-badge.svg b/web/icons/error-badge.svg new file mode 100644 index 000000000..b35dabb7c --- /dev/null +++ b/web/icons/error-badge.svg @@ -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> diff --git a/web/icons/warning-badge.svg b/web/icons/warning-badge.svg index b35dabb7c..1cacf1ffd 100644 --- a/web/icons/warning-badge.svg +++ b/web/icons/warning-badge.svg @@ -1,5 +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"/> +<circle cx="8" cy="8" r="8" fill="#CCAA00"/> <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> diff --git a/web/typings/images.d.ts b/web/typings/images.d.ts index 476fa54d8..68826e08b 100644 --- a/web/typings/images.d.ts +++ b/web/typings/images.d.ts @@ -1,4 +1,8 @@ declare module "*.png" { const content: string export = content +} +declare module "*.svg" { + const content: string + export = content } \ No newline at end of file From 5fe618b81e691f801c0701c355c7f45af968181d Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti <andrewf@element.io> Date: Fri, 4 Nov 2022 15:15:35 -0400 Subject: [PATCH 2/4] Mention manual "Secret token" for GitLab webhooks --- docs/usage/room_configuration/gitlab_project.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/room_configuration/gitlab_project.md b/docs/usage/room_configuration/gitlab_project.md index 8046c07fd..80819070e 100644 --- a/docs/usage/room_configuration/gitlab_project.md +++ b/docs/usage/room_configuration/gitlab_project.md @@ -15,7 +15,7 @@ To set up a connection to a GitLab project in a new room: 4. Send the command `!hookshot gitlab project https://mydomain/my/project`. 5. If you have permission to bridge this repo, the bridge will respond with a confirmation message. (Users with `Developer` permissions or greater can bridge projects.) 6. If you have configured the bridge with a `publicUrl` inside `gitlab.webhook`, and you have `Maintainer` permissions or greater on the project, the bot will automatically provision the webhook for you. -7. Otherwise, you'll need to manually configure the project with a webhook that points to your public address for the webhooks listener, and with all relevant Triggers enabled (as Hookshot can only bridge events for enabled Triggers). This can be configured on the GitLab webpage for the project under Settings > Webhook Settings. If you do not have access to this page, you must ask someone who does (i.e. someone with at least `Maintainer` permissions on the project) to add the webhook for you. +7. Otherwise, you'll need to manually configure the project with a webhook that points to your public address for the webhooks listener, sets the "Secret token" to the one you put in your Hookshot configuration (`gitlab.webhook.secret`), and enables all Triggers that need to be bridged (as Hookshot can only bridge events for enabled Triggers). This can be configured on the GitLab webpage for the project under Settings > Webhook Settings. If you do not have access to this page, you must ask someone who does (i.e. someone with at least `Maintainer` permissions on the project) to add the webhook for you. ## Configuration From af8a635a4f59661befe6ce417507389f391bee7e Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti <andrewf@element.io> Date: Mon, 7 Nov 2022 10:26:24 -0500 Subject: [PATCH 3/4] Refactor warning pane into a separate component --- web/components/elements/ErrorPane.module.scss | 4 ---- web/components/elements/ErrorPane.tsx | 7 +++---- web/components/elements/WarningPane.module.scss | 4 ++++ web/components/elements/WarningPane.tsx | 9 +++++++++ web/components/elements/index.ts | 3 ++- web/components/roomConfig/RoomConfig.tsx | 8 ++++++-- 6 files changed, 24 insertions(+), 11 deletions(-) create mode 100644 web/components/elements/WarningPane.module.scss create mode 100644 web/components/elements/WarningPane.tsx diff --git a/web/components/elements/ErrorPane.module.scss b/web/components/elements/ErrorPane.module.scss index 1be7bae73..811603b5b 100644 --- a/web/components/elements/ErrorPane.module.scss +++ b/web/components/elements/ErrorPane.module.scss @@ -1,8 +1,4 @@ .errorPane { max-width: 480px; color: #FF4B55; -} -.warningPane { - max-width: 480px; - color: #CCAA00; } \ No newline at end of file diff --git a/web/components/elements/ErrorPane.tsx b/web/components/elements/ErrorPane.tsx index 56563ea58..54628acc4 100644 --- a/web/components/elements/ErrorPane.tsx +++ b/web/components/elements/ErrorPane.tsx @@ -1,10 +1,9 @@ import { h, FunctionComponent } from "preact"; import ErrorBadge from "../../icons/error-badge.svg"; -import WarningBadge from "../../icons/warning-badge.svg"; import style from "./ErrorPane.module.scss"; -export const ErrorPane: FunctionComponent<{header?: string, isWarning?: boolean}> = ({ children, header, isWarning }) => { - return <div class={`card error ${isWarning ? style.warningPane : style.errorPane}`}> - <p><strong><img src={isWarning ? WarningBadge : ErrorBadge } /> { header || `${isWarning ? "Problem" : "Error"} occured during widget load` }</strong>: {children}</p> +export const ErrorPane: FunctionComponent<{header?: string}> = ({ children, header }) => { + return <div class={`card error ${style.errorPane}`}> + <p><strong><img src={ErrorBadge} /> { header || "Error occured during widget load" }</strong>: {children}</p> </div>; }; \ No newline at end of file diff --git a/web/components/elements/WarningPane.module.scss b/web/components/elements/WarningPane.module.scss new file mode 100644 index 000000000..e7b7aeeba --- /dev/null +++ b/web/components/elements/WarningPane.module.scss @@ -0,0 +1,4 @@ +.warningPane { + max-width: 480px; + color: #CCAA00; +} \ No newline at end of file diff --git a/web/components/elements/WarningPane.tsx b/web/components/elements/WarningPane.tsx new file mode 100644 index 000000000..021edd511 --- /dev/null +++ b/web/components/elements/WarningPane.tsx @@ -0,0 +1,9 @@ +import { h, FunctionComponent } from "preact"; +import WarningBadge from "../../icons/warning-badge.svg"; +import style from "./WarningPane.module.scss"; + +export const WarningPane: FunctionComponent<{header?: string}> = ({ children, header }) => { + return <div class={`card error ${style.warningPane}`}> + <p><strong><img src={WarningBadge} /> { header || "Problem occured during widget load" }</strong>: {children}</p> + </div>; +}; \ No newline at end of file diff --git a/web/components/elements/index.ts b/web/components/elements/index.ts index db676c550..11f01d719 100644 --- a/web/components/elements/index.ts +++ b/web/components/elements/index.ts @@ -2,4 +2,5 @@ export * from "./Button"; export * from "./ButtonSet"; export * from "./ErrorPane"; export * from "./InputField"; -export * from "./ListItem"; \ No newline at end of file +export * from "./ListItem"; +export * from "./WarningPane"; \ No newline at end of file diff --git a/web/components/roomConfig/RoomConfig.tsx b/web/components/roomConfig/RoomConfig.tsx index 9e0fc48a3..3fbea7b9e 100644 --- a/web/components/roomConfig/RoomConfig.tsx +++ b/web/components/roomConfig/RoomConfig.tsx @@ -1,7 +1,7 @@ import { h, FunctionComponent } from "preact"; import { useCallback, useEffect, useReducer, useState } from "preact/hooks" import { BridgeAPI, BridgeAPIError } from "../../BridgeAPI"; -import { ErrorPane, ListItem } from "../elements"; +import { ErrorPane, ListItem, WarningPane } from "../elements"; import style from "./RoomConfig.module.scss"; import { GetConnectionsResponseItem } from "../../../src/provisioning/api"; import { IConnectionState } from "../../../src/Connections"; @@ -93,7 +93,11 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection return <main> { - error && <ErrorPane header={error.header || "Error"} isWarning={error.isWarning}>{error.message}</ErrorPane> + error && + (!error.isWarning + ? <ErrorPane header={error.header || "Error"}>{error.message}</ErrorPane> + : <WarningPane header={error.header || "Warning"}>{error.message}</WarningPane> + ) } <header className={style.header}> <img src={headerImg} /> From f25cccb36c3bea13a9e080a5aa1e2731bf6222b5 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti <andrewf@element.io> Date: Mon, 7 Nov 2022 10:26:44 -0500 Subject: [PATCH 4/4] Recolour warning pane for better contrast --- web/components/elements/WarningPane.module.scss | 2 +- web/icons/warning-badge.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/web/components/elements/WarningPane.module.scss b/web/components/elements/WarningPane.module.scss index e7b7aeeba..1797885e1 100644 --- a/web/components/elements/WarningPane.module.scss +++ b/web/components/elements/WarningPane.module.scss @@ -1,4 +1,4 @@ .warningPane { max-width: 480px; - color: #CCAA00; + color: #FF812D; } \ No newline at end of file diff --git a/web/icons/warning-badge.svg b/web/icons/warning-badge.svg index 1cacf1ffd..95e99e478 100644 --- a/web/icons/warning-badge.svg +++ b/web/icons/warning-badge.svg @@ -1,5 +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="#CCAA00"/> +<circle cx="8" cy="8" r="8" fill="#FF812D"/> <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>