feat: add backend status down indicator (#3889)

This commit is contained in:
Nigel Huang 2020-06-15 10:59:28 -04:00 committed by GitHub
parent 2b01e009c6
commit a50edc7055
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 165 additions and 40 deletions

View File

@ -46,6 +46,15 @@ import { GitLfsClient } from './git-lfs-client';
const MAX_CONCURRENT_DOWNLOADS = 10;
const STATUS_PAGE = 'https://bitbucket.status.atlassian.com';
const BITBUCKET_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
const BITBUCKET_OPERATIONAL_UNITS = ['API', 'Authentication and user management', 'Git LFS'];
type BitbucketStatusComponent = {
id: string;
name: string;
status: string;
};
// Implementation wrapper class
export default class BitbucketBackend implements Implementation {
lock: AsyncLock;
@ -114,16 +123,36 @@ export default class BitbucketBackend implements Implementation {
}
async status() {
const auth =
(await this.api
?.user()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting Bitbucket user', e);
return false;
})) || false;
const api = await fetch(BITBUCKET_STATUS_ENDPOINT)
.then(res => res.json())
.then(res => {
return res['components']
.filter((statusComponent: BitbucketStatusComponent) =>
BITBUCKET_OPERATIONAL_UNITS.includes(statusComponent.name),
)
.every(
(statusComponent: BitbucketStatusComponent) => statusComponent.status === 'operational',
);
})
.catch(e => {
console.warn('Failed getting BitBucket status', e);
return true;
});
return { auth };
let auth = false;
// no need to check auth if api is down
if (api) {
auth =
(await this.api
?.user()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting Bitbucket user', e);
return false;
})) || false;
}
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
}
authComponent() {

View File

@ -35,6 +35,15 @@ import GitLabAPI from './GitLabAPI';
import AuthenticationPage from './AuthenticationPage';
import { getClient, Client } from './netlify-lfs-client';
const STATUS_PAGE = 'https://www.netlifystatus.com';
const GIT_GATEWAY_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
const GIT_GATEWAY_OPERATIONAL_UNITS = ['Git Gateway'];
type GitGatewayStatus = {
id: string;
name: string;
status: string;
};
type NetlifyIdentity = {
logout: () => void;
currentUser: () => User;
@ -179,15 +188,33 @@ export default class GitGateway implements Implementation {
}
async status() {
const auth =
(await this.tokenPromise?.()
.then(token => !!token)
.catch(e => {
console.warn('Failed getting Identity token', e);
return false;
})) || false;
const api = await fetch(GIT_GATEWAY_STATUS_ENDPOINT)
.then(res => res.json())
.then(res => {
return res['components']
.filter((statusComponent: GitGatewayStatus) =>
GIT_GATEWAY_OPERATIONAL_UNITS.includes(statusComponent.name),
)
.every((statusComponent: GitGatewayStatus) => statusComponent.status === 'operational');
})
.catch(e => {
console.warn('Failed getting Git Gateway status', e);
return true;
});
return { auth };
let auth = false;
// no need to check auth if api is down
if (api) {
auth =
(await this.tokenPromise?.()
.then(token => !!token)
.catch(e => {
console.warn('Failed getting Identity token', e);
return false;
})) || false;
}
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
}
async getAuthClient() {

View File

@ -43,6 +43,15 @@ type ApiFile = { id: string; type: string; name: string; path: string; size: num
const { fetchWithTimeout: fetch } = unsentRequest;
const STATUS_PAGE = 'https://www.githubstatus.com';
const GITHUB_STATUS_ENDPOINT = `${STATUS_PAGE}/api/v2/components.json`;
const GITHUB_OPERATIONAL_UNITS = ['API Requests', 'Issues, Pull Requests, Projects'];
type GitHubStatusComponent = {
id: string;
name: string;
status: string;
};
export default class GitHub implements Implementation {
lock: AsyncLock;
api: API | null;
@ -112,16 +121,36 @@ export default class GitHub implements Implementation {
}
async status() {
const auth =
(await this.api
?.getUser()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting GitHub user', e);
return false;
})) || false;
const api = await fetch(GITHUB_STATUS_ENDPOINT)
.then(res => res.json())
.then(res => {
return res['components']
.filter((statusComponent: GitHubStatusComponent) =>
GITHUB_OPERATIONAL_UNITS.includes(statusComponent.name),
)
.every(
(statusComponent: GitHubStatusComponent) => statusComponent.status === 'operational',
);
})
.catch(e => {
console.warn('Failed getting GitHub status', e);
return true;
});
return { auth };
let auth = false;
// no need to check auth if api is down
if (api) {
auth =
(await this.api
?.getUser()
.then(user => !!user)
.catch(e => {
console.warn('Failed getting GitHub user', e);
return false;
})) || false;
}
return { auth: { status: auth }, api: { status: api, statusPage: STATUS_PAGE } };
}
authComponent() {

View File

@ -97,7 +97,7 @@ export default class GitLab implements Implementation {
return false;
})) || false;
return { auth };
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
}
authComponent() {

View File

@ -64,7 +64,7 @@ export default class ProxyBackend implements Implementation {
}
status() {
return Promise.resolve({ auth: true });
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
}
authComponent() {

View File

@ -102,7 +102,7 @@ export default class TestBackend implements Implementation {
}
status() {
return Promise.resolve({ auth: true });
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
}
authComponent() {

View File

@ -4,7 +4,7 @@ import { ThunkDispatch } from 'redux-thunk';
import { AnyAction } from 'redux';
import { actions as notifActions } from 'redux-notifications';
const { notifSend } = notifActions;
const { notifSend, notifDismiss } = notifActions;
export const STATUS_REQUEST = 'STATUS_REQUEST';
export const STATUS_SUCCESS = 'STATUS_SUCCESS';
@ -16,7 +16,10 @@ export function statusRequest() {
};
}
export function statusSuccess(status: { auth: boolean }) {
export function statusSuccess(status: {
auth: { status: boolean };
api: { status: boolean; statusPage: string };
}) {
return {
type: STATUS_SUCCESS,
payload: { status },
@ -26,7 +29,7 @@ export function statusSuccess(status: { auth: boolean }) {
export function statusFailure(error: Error) {
return {
type: STATUS_FAILURE,
error,
payload: { error },
};
}
@ -42,7 +45,30 @@ export function checkBackendStatus() {
const backend = currentBackend(state.config);
const status = await backend.status();
const authError = status.auth === false;
const backendDownKey = 'ui.toast.onBackendDown';
const previousBackendDownNotifs = state.notifs.filter(n => n.message?.key === backendDownKey);
if (status.api.status === false) {
if (previousBackendDownNotifs.length === 0) {
dispatch(
notifSend({
message: {
details: status.api.statusPage,
key: 'ui.toast.onBackendDown',
},
kind: 'danger',
}),
);
}
return dispatch(statusSuccess(status));
} else if (status.api.status === true && previousBackendDownNotifs.length > 0) {
// If backend is up, clear all the danger messages
previousBackendDownNotifs.forEach(notif => {
dispatch(notifDismiss(notif.id));
});
}
const authError = status.auth.status === false;
if (authError) {
const key = 'ui.toast.onLoggedOut';
const existingNotification = state.notifs.find(n => n.message?.key === key);

View File

@ -181,11 +181,17 @@ export class Backend {
async status() {
const attempts = 3;
let status: { auth: boolean } = { auth: false };
let status: {
auth: { status: boolean };
api: { status: boolean; statusPage: string };
} = {
auth: { status: false },
api: { status: false, statusPage: '' },
};
for (let i = 1; i <= attempts; i++) {
status = await this.implementation!.status();
// return on first success
if (Object.values(status).every(s => s === true)) {
if (Object.values(status).every(s => s.status === true)) {
return status;
} else {
await new Promise(resolve => setTimeout(resolve, i * 1000));

View File

@ -3,11 +3,14 @@ import { AnyAction } from 'redux';
import { STATUS_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../actions/status';
import { Status } from '../types/redux';
export interface EntriesAction extends AnyAction {
payload: { status: { auth: boolean }; error?: Error };
interface StatusAction extends AnyAction {
payload: {
status: { auth: { status: boolean }; api: { status: boolean; statusPage: string } };
error?: Error;
};
}
const status = (state = Map(), action: EntriesAction) => {
const status = (state = Map(), action: StatusAction) => {
switch (action.type) {
case STATUS_REQUEST:
return state.set('isFetching', true);

View File

@ -141,7 +141,10 @@ export interface Implementation {
) => Promise<{ entries: ImplementationEntry[]; cursor: Cursor }>;
isGitBackend?: () => boolean;
status: () => Promise<{ auth: boolean }>;
status: () => Promise<{
auth: { status: boolean };
api: { status: boolean; statusPage: string };
}>;
}
const MAX_CONCURRENT_DOWNLOADS = 10;

View File

@ -12,7 +12,7 @@ const isAbortControllerSupported = () => {
const timeout = 60;
const fetchWithTimeout = (input, init) => {
if (init.signal || !isAbortControllerSupported()) {
if ((init && init.signal) || !isAbortControllerSupported()) {
return fetch(input, init);
}
const controller = new AbortController();

View File

@ -224,6 +224,8 @@ const en = {
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
onFailToAuth: '%{details}',
onLoggedOut: 'You have been logged out, please back up any data and login again',
onBackendDown:
'The backend service is experiencing an outage. See %{details} for more information',
},
},
workflow: {