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 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
|
// Implementation wrapper class
|
||||||
export default class BitbucketBackend implements Implementation {
|
export default class BitbucketBackend implements Implementation {
|
||||||
lock: AsyncLock;
|
lock: AsyncLock;
|
||||||
@ -114,16 +123,36 @@ export default class BitbucketBackend implements Implementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async status() {
|
async status() {
|
||||||
const auth =
|
const api = await fetch(BITBUCKET_STATUS_ENDPOINT)
|
||||||
(await this.api
|
.then(res => res.json())
|
||||||
?.user()
|
.then(res => {
|
||||||
.then(user => !!user)
|
return res['components']
|
||||||
.catch(e => {
|
.filter((statusComponent: BitbucketStatusComponent) =>
|
||||||
console.warn('Failed getting Bitbucket user', e);
|
BITBUCKET_OPERATIONAL_UNITS.includes(statusComponent.name),
|
||||||
return false;
|
)
|
||||||
})) || false;
|
.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() {
|
authComponent() {
|
||||||
|
@ -35,6 +35,15 @@ import GitLabAPI from './GitLabAPI';
|
|||||||
import AuthenticationPage from './AuthenticationPage';
|
import AuthenticationPage from './AuthenticationPage';
|
||||||
import { getClient, Client } from './netlify-lfs-client';
|
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 = {
|
type NetlifyIdentity = {
|
||||||
logout: () => void;
|
logout: () => void;
|
||||||
currentUser: () => User;
|
currentUser: () => User;
|
||||||
@ -179,15 +188,33 @@ export default class GitGateway implements Implementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async status() {
|
async status() {
|
||||||
const auth =
|
const api = await fetch(GIT_GATEWAY_STATUS_ENDPOINT)
|
||||||
(await this.tokenPromise?.()
|
.then(res => res.json())
|
||||||
.then(token => !!token)
|
.then(res => {
|
||||||
.catch(e => {
|
return res['components']
|
||||||
console.warn('Failed getting Identity token', e);
|
.filter((statusComponent: GitGatewayStatus) =>
|
||||||
return false;
|
GIT_GATEWAY_OPERATIONAL_UNITS.includes(statusComponent.name),
|
||||||
})) || false;
|
)
|
||||||
|
.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() {
|
async getAuthClient() {
|
||||||
|
@ -43,6 +43,15 @@ type ApiFile = { id: string; type: string; name: string; path: string; size: num
|
|||||||
|
|
||||||
const { fetchWithTimeout: fetch } = unsentRequest;
|
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 {
|
export default class GitHub implements Implementation {
|
||||||
lock: AsyncLock;
|
lock: AsyncLock;
|
||||||
api: API | null;
|
api: API | null;
|
||||||
@ -112,16 +121,36 @@ export default class GitHub implements Implementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async status() {
|
async status() {
|
||||||
const auth =
|
const api = await fetch(GITHUB_STATUS_ENDPOINT)
|
||||||
(await this.api
|
.then(res => res.json())
|
||||||
?.getUser()
|
.then(res => {
|
||||||
.then(user => !!user)
|
return res['components']
|
||||||
.catch(e => {
|
.filter((statusComponent: GitHubStatusComponent) =>
|
||||||
console.warn('Failed getting GitHub user', e);
|
GITHUB_OPERATIONAL_UNITS.includes(statusComponent.name),
|
||||||
return false;
|
)
|
||||||
})) || false;
|
.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() {
|
authComponent() {
|
||||||
|
@ -97,7 +97,7 @@ export default class GitLab implements Implementation {
|
|||||||
return false;
|
return false;
|
||||||
})) || false;
|
})) || false;
|
||||||
|
|
||||||
return { auth };
|
return { auth: { status: auth }, api: { status: true, statusPage: '' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
authComponent() {
|
authComponent() {
|
||||||
|
@ -64,7 +64,7 @@ export default class ProxyBackend implements Implementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status() {
|
status() {
|
||||||
return Promise.resolve({ auth: true });
|
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
authComponent() {
|
authComponent() {
|
||||||
|
@ -102,7 +102,7 @@ export default class TestBackend implements Implementation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
status() {
|
status() {
|
||||||
return Promise.resolve({ auth: true });
|
return Promise.resolve({ auth: { status: true }, api: { status: true, statusPage: '' } });
|
||||||
}
|
}
|
||||||
|
|
||||||
authComponent() {
|
authComponent() {
|
||||||
|
@ -4,7 +4,7 @@ import { ThunkDispatch } from 'redux-thunk';
|
|||||||
import { AnyAction } from 'redux';
|
import { AnyAction } from 'redux';
|
||||||
import { actions as notifActions } from 'redux-notifications';
|
import { actions as notifActions } from 'redux-notifications';
|
||||||
|
|
||||||
const { notifSend } = notifActions;
|
const { notifSend, notifDismiss } = notifActions;
|
||||||
|
|
||||||
export const STATUS_REQUEST = 'STATUS_REQUEST';
|
export const STATUS_REQUEST = 'STATUS_REQUEST';
|
||||||
export const STATUS_SUCCESS = 'STATUS_SUCCESS';
|
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 {
|
return {
|
||||||
type: STATUS_SUCCESS,
|
type: STATUS_SUCCESS,
|
||||||
payload: { status },
|
payload: { status },
|
||||||
@ -26,7 +29,7 @@ export function statusSuccess(status: { auth: boolean }) {
|
|||||||
export function statusFailure(error: Error) {
|
export function statusFailure(error: Error) {
|
||||||
return {
|
return {
|
||||||
type: STATUS_FAILURE,
|
type: STATUS_FAILURE,
|
||||||
error,
|
payload: { error },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +45,30 @@ export function checkBackendStatus() {
|
|||||||
const backend = currentBackend(state.config);
|
const backend = currentBackend(state.config);
|
||||||
const status = await backend.status();
|
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) {
|
if (authError) {
|
||||||
const key = 'ui.toast.onLoggedOut';
|
const key = 'ui.toast.onLoggedOut';
|
||||||
const existingNotification = state.notifs.find(n => n.message?.key === key);
|
const existingNotification = state.notifs.find(n => n.message?.key === key);
|
||||||
|
@ -181,11 +181,17 @@ export class Backend {
|
|||||||
|
|
||||||
async status() {
|
async status() {
|
||||||
const attempts = 3;
|
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++) {
|
for (let i = 1; i <= attempts; i++) {
|
||||||
status = await this.implementation!.status();
|
status = await this.implementation!.status();
|
||||||
// return on first success
|
// return on first success
|
||||||
if (Object.values(status).every(s => s === true)) {
|
if (Object.values(status).every(s => s.status === true)) {
|
||||||
return status;
|
return status;
|
||||||
} else {
|
} else {
|
||||||
await new Promise(resolve => setTimeout(resolve, i * 1000));
|
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_REQUEST, STATUS_SUCCESS, STATUS_FAILURE } from '../actions/status';
|
||||||
import { Status } from '../types/redux';
|
import { Status } from '../types/redux';
|
||||||
|
|
||||||
export interface EntriesAction extends AnyAction {
|
interface StatusAction extends AnyAction {
|
||||||
payload: { status: { auth: boolean }; error?: Error };
|
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) {
|
switch (action.type) {
|
||||||
case STATUS_REQUEST:
|
case STATUS_REQUEST:
|
||||||
return state.set('isFetching', true);
|
return state.set('isFetching', true);
|
||||||
|
@ -141,7 +141,10 @@ export interface Implementation {
|
|||||||
) => Promise<{ entries: ImplementationEntry[]; cursor: Cursor }>;
|
) => Promise<{ entries: ImplementationEntry[]; cursor: Cursor }>;
|
||||||
|
|
||||||
isGitBackend?: () => boolean;
|
isGitBackend?: () => boolean;
|
||||||
status: () => Promise<{ auth: boolean }>;
|
status: () => Promise<{
|
||||||
|
auth: { status: boolean };
|
||||||
|
api: { status: boolean; statusPage: string };
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_CONCURRENT_DOWNLOADS = 10;
|
const MAX_CONCURRENT_DOWNLOADS = 10;
|
||||||
|
@ -12,7 +12,7 @@ const isAbortControllerSupported = () => {
|
|||||||
|
|
||||||
const timeout = 60;
|
const timeout = 60;
|
||||||
const fetchWithTimeout = (input, init) => {
|
const fetchWithTimeout = (input, init) => {
|
||||||
if (init.signal || !isAbortControllerSupported()) {
|
if ((init && init.signal) || !isAbortControllerSupported()) {
|
||||||
return fetch(input, init);
|
return fetch(input, init);
|
||||||
}
|
}
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
@ -224,6 +224,8 @@ const en = {
|
|||||||
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
onDeleteUnpublishedChanges: 'Unpublished changes deleted',
|
||||||
onFailToAuth: '%{details}',
|
onFailToAuth: '%{details}',
|
||||||
onLoggedOut: 'You have been logged out, please back up any data and login again',
|
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: {
|
workflow: {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user