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;
label?: string;
template?: string;
notifyOnFailure?: boolean;
}
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.
return;
}
if (!this.state.notifyOnFailure) {
// User hasn't opted into notifications on failure
return;
}
if (!this.hasError) {
await this.intent.sendEvent(this.roomId, {
msgtype: 'm.notice',

View File

@ -4,7 +4,7 @@ import emoji from "node-emoji";
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
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 {
html_url: string;

View File

@ -29,8 +29,5 @@ export class GitLabWatcher extends EventEmitter implements NotificationWatcherTa
private async getNotifications() {
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 { 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");

View File

@ -14,7 +14,7 @@ class MockConnectionManager extends EventEmitter {
super();
}
getAllConnectionsOfType(type: unknown) {
getAllConnectionsOfType() {
return this.connections;
}
}
@ -32,7 +32,7 @@ class MockMessageQueue extends EventEmitter implements MessageQueue {
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');
}
}
@ -73,13 +73,13 @@ describe("FeedReader", () => {
config, cm, mq,
{
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,
);
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');

View File

@ -22,4 +22,8 @@
.remove {
color: #FF5B55;
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 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 labelRef = createRef<HTMLInputElement>();
const templateRef = createRef<HTMLInputElement>();
const notifyRef = createRef<HTMLInputElement>();
const canSave = !existingConnection?.id || (existingConnection?.canEdit ?? false);
const canEdit = canSave && !isMigrationCandidate;
const handleSave = useCallback((evt: Event) => {
@ -45,10 +46,13 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
url,
label: labelRef?.current?.value || existingConnection?.config.label,
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}>
{ existingConnection && <FeedRecentResults item={existingConnection} />}
@ -58,14 +62,16 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<Se
<InputField visible={true} label="Label" noPadding={true}>
<input ref={labelRef} disabled={!canSave} type="text" value={existingConnection?.config.label} />
</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} />
<p> See the <a target="_blank" rel="noopener noreferrer" href={DOCUMENTATION_LINK}>documentation</a> for help writing templates. </p>
</InputField>
<ButtonSet>
{ canSave && <Button type="submit">{ existingConnection?.id ? "Save" : "Subscribe" }</Button>}
{ canEdit && existingConnection?.id && <Button intent="remove" onClick={onRemove}>Unsubscribe</Button>}
{ canSave && <Button type="submit" disabled={isUpdating}>{ existingConnection?.id ? "Save" : "Subscribe" }</Button>}
{ canEdit && existingConnection?.id && <Button intent="remove" onClick={onRemove} disabled={isUpdating}>Unsubscribe</Button>}
</ButtonSet>
</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 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 [transFnEnabled, setTransFnEnabled] = useState(serviceConfig.allowJsTransformationFunctions && !!existingConnection?.config.transformationFunction);
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>
</InputField>
<ButtonSet>
{ canEdit && <Button type="submit">{ existingConnection ? "Save" : "Add webhook" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Remove webhook</Button>}
{ canEdit && <Button disabled={isUpdating} type="submit">{ existingConnection ? "Save" : "Add webhook" }</Button>}
{ canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove webhook</Button>}
</ButtonSet>
</form>;
};

View File

@ -18,7 +18,7 @@ function getRepoFullName(state: 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.
const [authedResponse, setAuthResponse] = useState<GetAuthResponse|null>(null);
@ -159,8 +159,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
</ul>
</InputField>
<ButtonSet>
{ canEdit && consideredAuthenticated && <Button type="submit" disabled={!existingConnection && !connectionState}>{ existingConnection?.id ? "Save" : "Add repository" }</Button>}
{ canEdit && existingConnection?.id && <Button intent="remove" onClick={onRemove}>Remove repository</Button>}
{ canEdit && consideredAuthenticated && <Button type="submit" disabled={isUpdating || !existingConnection && !connectionState}>{ existingConnection?.id ? "Save" : "Add repository" }</Button>}
{ canEdit && existingConnection?.id && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove repository</Button>}
</ButtonSet>
</form>;
};

View File

@ -10,7 +10,7 @@ import { DropItem } from "../elements/DropdownSearch";
import { ConnectionSearch } from "../elements/ConnectionSearch";
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 toggleEnabledHook = useCallback((evt: any) => {
@ -107,8 +107,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
</ul>
</InputField>
<ButtonSet>
{ canEdit && <Button type="submit" disabled={!existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Remove project</Button>}
{ canEdit && <Button type="submit" disabled={isUpdating || !existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>}
{ canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove project</Button>}
</ButtonSet>
</form>;
};

View File

@ -11,7 +11,7 @@ import { DropItem } from "../elements/DropdownSearch";
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 toggleEvent = useCallback((evt: Event) => {
@ -93,8 +93,8 @@ const ConnectionConfiguration: FunctionComponent<ConnectionConfigurationProps<ne
</ul>
</InputField>
<ButtonSet>
{ canEdit && <Button type="submit" disabled={!existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>}
{ canEdit && existingConnection && <Button intent="remove" onClick={onRemove}>Remove project</Button>}
{ canEdit && <Button type="submit" disabled={isUpdating || !existingConnection && !newConnectionState}>{ existingConnection ? "Save" : "Add project" }</Button>}
{ canEdit && existingConnection && <Button disabled={isUpdating} intent="remove" onClick={onRemove}>Remove project</Button>}
</ButtonSet>
</form>;
};

View File

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