Add option to enable/disable feed failure notices. (#716)

* Add new notifyOnFailure option

* Disable state buttons when updating is set

* Linting
This commit is contained in:
Will Hunt 2023-04-21 11:47:29 +01:00 committed by GitHub
parent c6a04c2ebd
commit a9537c7961
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 55 additions and 29 deletions

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

@ -0,0 +1 @@
Notifications for RSS feed failures can now be toggled on and off. The feature is now **off** by default.

View File

@ -25,6 +25,7 @@ export interface FeedConnectionState extends IConnectionState {
url: string; url: string;
label?: string; label?: string;
template?: string; template?: string;
notifyOnFailure?: boolean;
} }
export interface FeedConnectionSecrets { export interface FeedConnectionSecrets {
@ -225,6 +226,10 @@ export class FeedConnection extends BaseConnection implements IConnection {
// To avoid short term failures bubbling up, if the error is serious, we still bubble. // To avoid short term failures bubbling up, if the error is serious, we still bubble.
return; return;
} }
if (!this.state.notifyOnFailure) {
// User hasn't opted into notifications on failure
return;
}
if (!this.hasError) { if (!this.hasError) {
await this.intent.sendEvent(this.roomId, { await this.intent.sendEvent(this.roomId, {
msgtype: 'm.notice', msgtype: 'm.notice',

View File

@ -4,7 +4,7 @@ import emoji from "node-emoji";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
import { JiraIssue } from './Jira/Types'; import { JiraIssue } from './Jira/Types';
import { formatLabels, getPartialBodyForJiraIssue, hashId, getPartialBodyForGithubIssue, getPartialBodyForGithubRepo, MinimalGitHubRepo, MinimalGitHubIssue } from "./libRs"; import { formatLabels, getPartialBodyForJiraIssue, hashId, getPartialBodyForGithubIssue, getPartialBodyForGithubRepo, MinimalGitHubIssue } from "./libRs";
interface IMinimalPR { interface IMinimalPR {
html_url: string; html_url: string;

View File

@ -29,8 +29,5 @@ export class GitLabWatcher extends EventEmitter implements NotificationWatcherTa
private async getNotifications() { private async getNotifications() {
log.info(`Fetching events from GitLab for ${this.userId}`); log.info(`Fetching events from GitLab for ${this.userId}`);
const events = await this.client.getEvents({
after: new Date(this.since)
});
} }
} }

View File

@ -2,7 +2,7 @@ import { Logger } from "matrix-appservice-bridge";
import axios from "axios"; import axios from "axios";
import { FeedConnection, FeedConnectionState, GitHubRepoConnection, GitHubRepoConnectionState } from "../Connections"; import { FeedConnection, FeedConnectionState, GitHubRepoConnection, GitHubRepoConnectionState } from "../Connections";
import { AllowedEvents as GitHubAllowedEvents, AllowedEventsNames as GitHubAllowedEventsNames } from "../Connections/GithubRepo"; import { AllowedEventsNames as GitHubAllowedEventsNames } from "../Connections/GithubRepo";
const log = new Logger("GoNebMigrator"); const log = new Logger("GoNebMigrator");

View File

@ -14,7 +14,7 @@ class MockConnectionManager extends EventEmitter {
super(); super();
} }
getAllConnectionsOfType(type: unknown) { getAllConnectionsOfType() {
return this.connections; return this.connections;
} }
} }
@ -32,7 +32,7 @@ class MockMessageQueue extends EventEmitter implements MessageQueue {
this.emit('pushed', data, single); this.emit('pushed', data, single);
} }
async pushWait<T, X>(data: MessageQueueMessage<T>, timeout?: number, single?: boolean): Promise<X> { async pushWait<X>(): Promise<X> {
throw new Error('Not yet implemented'); throw new Error('Not yet implemented');
} }
} }
@ -73,13 +73,13 @@ describe("FeedReader", () => {
config, cm, mq, config, cm, mq,
{ {
getAccountData: <T>() => Promise.resolve({ 'http://test/': [] } as unknown as T), getAccountData: <T>() => Promise.resolve({ 'http://test/': [] } as unknown as T),
setAccountData: <T>() => Promise.resolve(), setAccountData: () => Promise.resolve(),
}, },
new MockHttpClient({ headers: {}, data: feedContents } as AxiosResponse) as unknown as AxiosStatic, new MockHttpClient({ headers: {}, data: feedContents } as AxiosResponse) as unknown as AxiosStatic,
); );
const event: any = await new Promise((resolve) => { const event: any = await new Promise((resolve) => {
mq.on('pushed', (data, _) => { resolve(data); feedReader.stop() }); mq.on('pushed', (data) => { resolve(data); feedReader.stop() });
}); });
expect(event.eventName).to.equal('feed.entry'); expect(event.eventName).to.equal('feed.entry');

View File

@ -22,4 +22,8 @@
.remove { .remove {
color: #FF5B55; color: #FF5B55;
background-color: transparent; background-color: transparent;
&:disabled {
background-color: transparent;
}
} }

View File

@ -28,10 +28,11 @@ const FeedRecentResults: FunctionComponent<{item: FeedResponseItem}> = ({ item }
} }
const DOCUMENTATION_LINK = "https://matrix-org.github.io/matrix-hookshot/latest/setup/feeds.html#feed-templates"; const DOCUMENTATION_LINK = "https://matrix-org.github.io/matrix-hookshot/latest/setup/feeds.html#feed-templates";
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ServiceConfig, FeedResponseItem, FeedConnectionState>> = ({existingConnection, onSave, onRemove, isMigrationCandidate}) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ServiceConfig, FeedResponseItem, FeedConnectionState>> = ({existingConnection, onSave, onRemove, isMigrationCandidate, isUpdating}) => {
const urlRef = createRef<HTMLInputElement>(); const urlRef = createRef<HTMLInputElement>();
const labelRef = createRef<HTMLInputElement>(); const labelRef = createRef<HTMLInputElement>();
const templateRef = createRef<HTMLInputElement>(); const templateRef = createRef<HTMLInputElement>();
const notifyRef = createRef<HTMLInputElement>();
const canSave = !existingConnection?.id || (existingConnection?.canEdit ?? false); const canSave = !existingConnection?.id || (existingConnection?.canEdit ?? false);
const canEdit = canSave && !isMigrationCandidate; const canEdit = canSave && !isMigrationCandidate;
const handleSave = useCallback((evt: Event) => { const handleSave = useCallback((evt: Event) => {
@ -45,10 +46,13 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
url, url,
label: labelRef?.current?.value || existingConnection?.config.label, label: labelRef?.current?.value || existingConnection?.config.label,
template: templateRef.current?.value || existingConnection?.config.template, template: templateRef.current?.value || existingConnection?.config.template,
}); notifyOnFailure: notifyRef.current?.checked || existingConnection?.config.notifyOnFailure,
})
} }
}, [canSave, onSave, urlRef, labelRef, templateRef, existingConnection]); }, [canSave, onSave, urlRef, labelRef, templateRef, notifyRef, existingConnection]);
const onlyVisibleOnExistingConnection = !!existingConnection;
return <form onSubmit={handleSave}> return <form onSubmit={handleSave}>
{ existingConnection && <FeedRecentResults item={existingConnection} />} { existingConnection && <FeedRecentResults item={existingConnection} />}
@ -58,14 +62,16 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
<InputField visible={true} label="Label" noPadding={true}> <InputField visible={true} label="Label" noPadding={true}>
<input ref={labelRef} disabled={!canSave} type="text" value={existingConnection?.config.label} /> <input ref={labelRef} disabled={!canSave} type="text" value={existingConnection?.config.label} />
</InputField> </InputField>
<InputField visible={true} label="Template" noPadding={true}> <InputField visible={onlyVisibleOnExistingConnection} label="Send a notice on read failure" noPadding={true}>
<input ref={notifyRef} disabled={!canSave} type="checkbox" checked={existingConnection?.config.notifyOnFailure} />
</InputField>
<InputField visible={onlyVisibleOnExistingConnection} label="Template" noPadding={true}>
<input ref={templateRef} disabled={!canSave} type="text" value={existingConnection?.config.template} placeholder={DEFAULT_TEMPLATE} /> <input ref={templateRef} disabled={!canSave} type="text" value={existingConnection?.config.template} placeholder={DEFAULT_TEMPLATE} />
<p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing templates. </p> <p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing templates. </p>
</InputField> </InputField>
<ButtonSet> <ButtonSet>
{ canSave && <Button type="submit">{ existingConnection?.id ? "Save" : "Subscribe" }</Button>} { canSave && <Button type="submit" disabled={isUpdating}>{ existingConnection?.id ? "Save" : "Subscribe" }</Button>}
{ canEdit && existingConnection?.id && <Button intent="remove" onClick={onRemove}>Unsubscribe</Button>} { canEdit && existingConnection?.id && <Button intent="remove" onClick={onRemove} disabled={isUpdating}>Unsubscribe</Button>}
</ButtonSet> </ButtonSet>
</form>; </form>;

View File

@ -28,7 +28,7 @@ const EXAMPLE_SCRIPT = `if (data.counter === undefined) {
const DOCUMENTATION_LINK = "https://matrix-org.github.io/matrix-hookshot/latest/setup/webhooks.html#script-api"; const DOCUMENTATION_LINK = "https://matrix-org.github.io/matrix-hookshot/latest/setup/webhooks.html#script-api";
const CODE_MIRROR_EXTENSIONS = [javascript({})]; const CODE_MIRROR_EXTENSIONS = [javascript({})];
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ServiceConfig, GenericHookResponseItem, GenericHookConnectionState>> = ({serviceConfig, existingConnection, onSave, onRemove}) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ServiceConfig, GenericHookResponseItem, GenericHookConnectionState>> = ({serviceConfig, existingConnection, onSave, onRemove, isUpdating}) => {
const [transFn, setTransFn] = useState<string>(existingConnection?.config.transformationFunction as string || EXAMPLE_SCRIPT); const [transFn, setTransFn] = useState<string>(existingConnection?.config.transformationFunction as string || EXAMPLE_SCRIPT);
const [transFnEnabled, setTransFnEnabled] = useState(serviceConfig.allowJsTransformationFunctions && !!existingConnection?.config.transformationFunction); const [transFnEnabled, setTransFnEnabled] = useState(serviceConfig.allowJsTransformationFunctions && !!existingConnection?.config.transformationFunction);
const nameRef = createRef<HTMLInputElement>(); const nameRef = createRef<HTMLInputElement>();
@ -67,8 +67,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
<p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing transformation functions </p> <p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing transformation functions </p>
</InputField> </InputField>
<ButtonSet> <ButtonSet>
{ canEdit && <Button type="submit">{ existingConnection ? "Save" : "Add webhook" }</Button>} { canEdit && <Button disabled={isUpdating} type="submit">{ existingConnection ? "Save" : "Add webhook" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Remove webhook</Button>} { canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove webhook</Button>}
</ButtonSet> </ButtonSet>
</form>; </form>;
}; };

View File

@ -18,7 +18,7 @@ function getRepoFullName(state: GitHubRepoConnectionState) {
} }
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitHubRepoResponseItem, GitHubRepoConnectionState>> = ({ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitHubRepoResponseItem, GitHubRepoConnectionState>> = ({
showAuthPrompt, loginLabel, serviceConfig, api, existingConnection, onSave, onRemove showAuthPrompt, loginLabel, serviceConfig, api, existingConnection, onSave, onRemove, isUpdating
}) => { }) => {
// Assume true if we have no auth prompt. // Assume true if we have no auth prompt.
const [authedResponse, setAuthResponse] = useState<GetAuthResponse|null>(null); const [authedResponse, setAuthResponse] = useState<GetAuthResponse|null>(null);
@ -159,8 +159,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
</ul> </ul>
</InputField> </InputField>
<ButtonSet> <ButtonSet>
{ canEdit && consideredAuthenticated && <Button type="submit" disabled={!existingConnection && !connectionState}>{ existingConnection?.id ? "Save" : "Add repository" }</Button>} { canEdit && consideredAuthenticated && <Button type="submit" disabled={isUpdating || !existingConnection && !connectionState}>{ existingConnection?.id ? "Save" : "Add repository" }</Button>}
{ canEdit && existingConnection?.id && <Button intent="remove" onClick={onRemove}>Remove repository</Button>} { canEdit && existingConnection?.id && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove repository</Button>}
</ButtonSet> </ButtonSet>
</form>; </form>;
}; };

View File

@ -10,7 +10,7 @@ import { DropItem } from "../elements/DropdownSearch";
import { ConnectionSearch } from "../elements/ConnectionSearch"; import { ConnectionSearch } from "../elements/ConnectionSearch";
const EventType = "uk.half-shot.matrix-hookshot.gitlab.repository"; const EventType = "uk.half-shot.matrix-hookshot.gitlab.repository";
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitLabRepoResponseItem, GitLabRepoConnectionState>> = ({api, existingConnection, onSave, onRemove }) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, GitLabRepoResponseItem, GitLabRepoConnectionState>> = ({api, existingConnection, onSave, onRemove, isUpdating }) => {
const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []); const [enabledHooks, setEnabledHooks] = useState<string[]>(existingConnection?.config.enableHooks || []);
const toggleEnabledHook = useCallback((evt: any) => { const toggleEnabledHook = useCallback((evt: any) => {
@ -107,8 +107,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
</ul> </ul>
</InputField> </InputField>
<ButtonSet> <ButtonSet>
{ canEdit && <Button type="submit" disabled={!existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>} { canEdit && <Button type="submit" disabled={isUpdating || !existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Remove project</Button>} { canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove project</Button>}
</ButtonSet> </ButtonSet>
</form>; </form>;
}; };

View File

@ -11,7 +11,7 @@ import { DropItem } from "../elements/DropdownSearch";
const EventType = "uk.half-shot.matrix-hookshot.jira.project"; const EventType = "uk.half-shot.matrix-hookshot.jira.project";
const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, JiraProjectResponseItem, JiraProjectConnectionState>> = ({api, existingConnection, onSave, onRemove }) => { const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<never, JiraProjectResponseItem, JiraProjectConnectionState>> = ({api, existingConnection, onSave, onRemove, isUpdating }) => {
const [allowedEvents, setAllowedEvents] = useState<string[]>(existingConnection?.config.events || ['issue_created']); const [allowedEvents, setAllowedEvents] = useState<string[]>(existingConnection?.config.events || ['issue_created']);
const toggleEvent = useCallback((evt: Event) => { const toggleEvent = useCallback((evt: Event) => {
@ -93,8 +93,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
</ul> </ul>
</InputField> </InputField>
<ButtonSet> <ButtonSet>
{ canEdit && <Button type="submit" disabled={!existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>} { canEdit && <Button type="submit" disabled={isUpdating || !existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Remove project</Button>} { canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove project</Button>}
</ButtonSet> </ButtonSet>
</form>; </form>;
}; };

View File

@ -13,6 +13,8 @@ export interface ConnectionConfigurationProps<SConfig, ConnectionType extends Ge
loginLabel?: string; loginLabel?: string;
showAuthPrompt?: boolean; showAuthPrompt?: boolean;
onSave: (newConfig: ConnectionState) => void, onSave: (newConfig: ConnectionState) => void,
isUpdating: boolean,
isMigrationCandidate?: boolean,
existingConnection?: ConnectionType; existingConnection?: ConnectionType;
onRemove?: () => void, onRemove?: () => void,
api: BridgeAPI; api: BridgeAPI;
@ -64,6 +66,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
const [ canEditRoom, setCanEditRoom ] = useState<boolean>(false); const [ canEditRoom, setCanEditRoom ] = useState<boolean>(false);
// We need to increment this every time we create a connection in order to properly reset the state. // 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 [ newConnectionKey, incrementConnectionKey ] = useReducer<number, undefined>(n => n+1, 0);
const [ updatingConnection, isUpdatingConnection ] = useState<boolean>(false);
const clearCurrentError = () => { const clearCurrentError = () => {
setError(error => error?.forPrevious ? error : null); setError(error => error?.forPrevious ? error : null);
@ -125,6 +128,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
}, [api, type]); }, [api, type]);
const handleSaveOnCreation = useCallback((config: ConnectionState) => { const handleSaveOnCreation = useCallback((config: ConnectionState) => {
isUpdatingConnection(true);
api.createConnection(roomId, connectionEventType, config).then(result => { api.createConnection(roomId, connectionEventType, config).then(result => {
// Force reload // Force reload
incrementConnectionKey(undefined); incrementConnectionKey(undefined);
@ -140,6 +144,8 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
header: "Failed to create connection", header: "Failed to create connection",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error" message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
}); });
}).finally(() => {
isUpdatingConnection(false);
}); });
}, [api, roomId, connectionEventType]); }, [api, roomId, connectionEventType]);
@ -167,6 +173,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
onSave={handleSaveOnCreation} onSave={handleSaveOnCreation}
loginLabel={text.login} loginLabel={text.login}
showAuthPrompt={showAuthPrompt} showAuthPrompt={showAuthPrompt}
isUpdating={updatingConnection}
/>} />}
</section>} </section>}
{ !error && connections === null && <LoadingSpinner /> } { !error && connections === null && <LoadingSpinner /> }
@ -178,6 +185,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
serviceConfig={serviceConfig} serviceConfig={serviceConfig}
existingConnection={c} existingConnection={c}
onSave={(config) => { onSave={(config) => {
isUpdatingConnection(true);
api.updateConnection(roomId, c.id, config).then(() => { api.updateConnection(roomId, c.id, config).then(() => {
c.config = config; c.config = config;
// Force reload // Force reload
@ -189,6 +197,8 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
header: "Failed to create connection", header: "Failed to create connection",
message: ex instanceof BridgeAPIError ? ex.message : "Unknown error" message: ex instanceof BridgeAPIError ? ex.message : "Unknown error"
}); });
}).finally(() => {
isUpdatingConnection(false);
}); });
}} }}
onRemove={() => { onRemove={() => {
@ -203,6 +213,7 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
}); });
}); });
}} }}
isUpdating={updatingConnection}
/> />
</ListItem>) </ListItem>)
} }
@ -214,6 +225,8 @@ export const RoomConfig = function<SConfig, ConnectionType extends GetConnection
api={api} api={api}
serviceConfig={serviceConfig} serviceConfig={serviceConfig}
existingConnection={c} existingConnection={c}
isUpdating={updatingConnection}
isMigrationCandidate={true}
onSave={handleSaveOnCreation} onSave={handleSaveOnCreation}
/> />
</ListItem>) } </ListItem>) }