feat: add backend status down indicator (#3889)
This commit is contained in:
parent
2b01e009c6
commit
a50edc7055
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -97,7 +97,7 @@ export default class GitLab implements Implementation {
|
||||
return false;
|
||||
})) || false;
|
||||
|
||||
return { auth };
|
||||
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
|
||||
}
|
||||
|
||||
authComponent() {
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
|
@ -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: {
|
||||
|
Loading…
x
Reference in New Issue
Block a user