feat: added warning for unreachable server (#43576)

* feat: added warning for unreachable server

* fix: update initial state in test file

* fix: make offline warning scroll with page

* adjust z-indexes for warning banners

* add hyperlink for offline warning
This commit is contained in:
Budbreaker
2021-10-06 15:18:02 +02:00
committed by GitHub
parent 83354c5632
commit bc802cbbbd
15 changed files with 70 additions and 14 deletions

View File

@ -364,6 +364,7 @@
}, },
"misc": { "misc": {
"offline": "你已離線,學習進度可能不會被保存", "offline": "你已離線,學習進度可能不會被保存",
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
"unsubscribed": "你已成功取消訂閱", "unsubscribed": "你已成功取消訂閱",
"keep-coding": "無論你做什麼,都要繼續編程!", "keep-coding": "無論你做什麼,都要繼續編程!",
"email-signup": "郵件註冊", "email-signup": "郵件註冊",

View File

@ -364,6 +364,7 @@
}, },
"misc": { "misc": {
"offline": "你已离线,学习进度可能不会被保存", "offline": "你已离线,学习进度可能不会被保存",
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
"unsubscribed": "你已成功取消订阅", "unsubscribed": "你已成功取消订阅",
"keep-coding": "无论你做什么,都要继续编程!", "keep-coding": "无论你做什么,都要继续编程!",
"email-signup": "邮件注册", "email-signup": "邮件注册",

View File

@ -364,6 +364,7 @@
}, },
"misc": { "misc": {
"offline": "You appear to be offline, your progress may not be saved", "offline": "You appear to be offline, your progress may not be saved",
"server-offline": "The server could not be reached and your progress may not be saved. Please contact <0>support</0> if this message persists",
"unsubscribed": "You have successfully been unsubscribed", "unsubscribed": "You have successfully been unsubscribed",
"keep-coding": "Whatever you go on to, keep coding!", "keep-coding": "Whatever you go on to, keep coding!",
"email-signup": "Email Sign Up", "email-signup": "Email Sign Up",

View File

@ -364,6 +364,7 @@
}, },
"misc": { "misc": {
"offline": "Parece que no estás conectado, es posible que tu progreso no se guarde", "offline": "Parece que no estás conectado, es posible que tu progreso no se guarde",
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
"unsubscribed": "Haz cancelado tu subscripción exitosamente", "unsubscribed": "Haz cancelado tu subscripción exitosamente",
"keep-coding": "Sea lo que sea que hagas, ¡sigue programando!", "keep-coding": "Sea lo que sea que hagas, ¡sigue programando!",
"email-signup": "Registrarse con Email", "email-signup": "Registrarse con Email",

View File

@ -364,6 +364,7 @@
}, },
"misc": { "misc": {
"offline": "Sembra che tu sia offline, i tuoi progressi potrebbero non essere salvati", "offline": "Sembra che tu sia offline, i tuoi progressi potrebbero non essere salvati",
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
"unsubscribed": "Hai annullato correttamente l'iscrizione", "unsubscribed": "Hai annullato correttamente l'iscrizione",
"keep-coding": "Qualsiasi cosa tu abbia intenzione di fare, continua a programmare!", "keep-coding": "Qualsiasi cosa tu abbia intenzione di fare, continua a programmare!",
"email-signup": "Registrazione con email", "email-signup": "Registrazione con email",

View File

@ -364,6 +364,7 @@
}, },
"misc": { "misc": {
"offline": "Você parece estar off-line, seu progresso pode não ser salvo", "offline": "Você parece estar off-line, seu progresso pode não ser salvo",
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
"unsubscribed": "Sua assinatura foi cancelada com sucesso", "unsubscribed": "Sua assinatura foi cancelada com sucesso",
"keep-coding": "Seja o que for, continue programando!", "keep-coding": "Seja o que for, continue programando!",
"email-signup": "Inscrição via e-mail", "email-signup": "Inscrição via e-mail",

View File

@ -7,7 +7,7 @@
padding-bottom: 3px; padding-bottom: 3px;
position: fixed; position: fixed;
width: 100%; width: 100%;
z-index: 100; z-index: 150;
} }
.flash-message div { .flash-message div {

View File

@ -3,4 +3,11 @@
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
position: fixed;
width: 100%;
z-index: 150;
}
.offline-warning a {
margin: 0 1ch;
} }

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
import './offline-warning.css'; import './offline-warning.css';
@ -8,20 +8,30 @@ let id: ReturnType<typeof setTimeout>;
interface OfflineWarningProps { interface OfflineWarningProps {
isOnline: boolean; isOnline: boolean;
isServerOnline: boolean;
isSignedIn: boolean; isSignedIn: boolean;
} }
function OfflineWarning({ function OfflineWarning({
isOnline, isOnline,
isServerOnline,
isSignedIn isSignedIn
}: OfflineWarningProps): JSX.Element | null { }: OfflineWarningProps): JSX.Element | null {
const { t } = useTranslation(); const { t } = useTranslation();
const [showWarning, setShowWarning] = React.useState(false); const [showWarning, setShowWarning] = React.useState(false);
let message;
if (!isSignedIn || isOnline) { if (!isSignedIn || (isOnline && isServerOnline)) {
clearTimeout(id); clearTimeout(id);
if (showWarning) setShowWarning(false); if (showWarning) setShowWarning(false);
} else { } else {
message = !isOnline ? (
t('misc.offline')
) : (
<Trans i18nKey='misc.server-offline'>
<a href={'mailto:support@freecodecamp.org'}>placeholder</a>
</Trans>
);
timeout(); timeout();
} }
@ -32,7 +42,10 @@ function OfflineWarning({
} }
return showWarning ? ( return showWarning ? (
<div className='offline-warning alert-info'>{t('misc.offline')}</div> <>
<div className='offline-warning alert-info'>{message}</div>
<div style={{ height: `38px` }} />
</>
) : null; ) : null;
} }

View File

@ -18,7 +18,9 @@ import {
fetchUser, fetchUser,
isSignedInSelector, isSignedInSelector,
onlineStatusChange, onlineStatusChange,
serverStatusChange,
isOnlineSelector, isOnlineSelector,
isServerOnlineSelector,
userFetchStateSelector, userFetchStateSelector,
userSelector, userSelector,
usernameSelector, usernameSelector,
@ -55,10 +57,12 @@ const propTypes = {
}), }),
hasMessage: PropTypes.bool, hasMessage: PropTypes.bool,
isOnline: PropTypes.bool.isRequired, isOnline: PropTypes.bool.isRequired,
isServerOnline: PropTypes.bool.isRequired,
isSignedIn: PropTypes.bool, isSignedIn: PropTypes.bool,
onlineStatusChange: PropTypes.func.isRequired, onlineStatusChange: PropTypes.func.isRequired,
pathname: PropTypes.string.isRequired, pathname: PropTypes.string.isRequired,
removeFlashMessage: PropTypes.func.isRequired, removeFlashMessage: PropTypes.func.isRequired,
serverStatusChange: PropTypes.func.isRequired,
showFooter: PropTypes.bool, showFooter: PropTypes.bool,
signedInUserName: PropTypes.string, signedInUserName: PropTypes.string,
t: PropTypes.func.isRequired, t: PropTypes.func.isRequired,
@ -71,14 +75,16 @@ const mapStateToProps = createSelector(
isSignedInSelector, isSignedInSelector,
flashMessageSelector, flashMessageSelector,
isOnlineSelector, isOnlineSelector,
isServerOnlineSelector,
userFetchStateSelector, userFetchStateSelector,
userSelector, userSelector,
usernameSelector, usernameSelector,
(isSignedIn, flashMessage, isOnline, fetchState, user) => ({ (isSignedIn, flashMessage, isOnline, isServerOnline, fetchState, user) => ({
isSignedIn, isSignedIn,
flashMessage, flashMessage,
hasMessage: !!flashMessage.message, hasMessage: !!flashMessage.message,
isOnline, isOnline,
isServerOnline,
fetchState, fetchState,
theme: user.theme, theme: user.theme,
user user
@ -87,7 +93,13 @@ const mapStateToProps = createSelector(
const mapDispatchToProps = dispatch => const mapDispatchToProps = dispatch =>
bindActionCreators( bindActionCreators(
{ fetchUser, removeFlashMessage, onlineStatusChange, executeGA }, {
fetchUser,
removeFlashMessage,
onlineStatusChange,
serverStatusChange,
executeGA
},
dispatch dispatch
); );
@ -130,6 +142,7 @@ class DefaultLayout extends Component {
fetchState, fetchState,
flashMessage, flashMessage,
isOnline, isOnline,
isServerOnline,
isSignedIn, isSignedIn,
removeFlashMessage, removeFlashMessage,
showFooter = true, showFooter = true,
@ -201,7 +214,11 @@ class DefaultLayout extends Component {
</Helmet> </Helmet>
<div className={`default-layout`}> <div className={`default-layout`}>
<Header fetchState={fetchState} user={user} /> <Header fetchState={fetchState} user={user} />
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} /> <OfflineWarning
isOnline={isOnline}
isServerOnline={isServerOnline}
isSignedIn={isSignedIn}
/>
{hasMessage && flashMessage ? ( {hasMessage && flashMessage ? (
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} /> <Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
) : null} ) : null}

View File

@ -12,6 +12,7 @@ export const actionTypes = createTypes(
'preventProgressDonationRequests', 'preventProgressDonationRequests',
'openDonationModal', 'openDonationModal',
'onlineStatusChange', 'onlineStatusChange',
'serverStatusChange',
'resetUserData', 'resetUserData',
'tryToShowDonationModal', 'tryToShowDonationModal',
'executeGA', 'executeGA',

View File

@ -15,7 +15,11 @@ import { backEndProject } from '../../utils/challenge-types';
import { isGoodXHRStatus } from '../templates/Challenges/utils'; import { isGoodXHRStatus } from '../templates/Challenges/utils';
import postUpdate$ from '../templates/Challenges/utils/postUpdate$'; import postUpdate$ from '../templates/Challenges/utils/postUpdate$';
import { actionTypes } from './action-types'; import { actionTypes } from './action-types';
import { onlineStatusChange, isOnlineSelector, isSignedInSelector } from './'; import {
serverStatusChange,
isServerOnlineSelector,
isSignedInSelector
} from './';
const key = 'fcc-failed-updates'; const key = 'fcc-failed-updates';
@ -37,14 +41,14 @@ function failedUpdateEpic(action$, state$) {
store.set(key, [...failures, payload]); store.set(key, [...failures, payload]);
} }
}), }),
map(() => onlineStatusChange(false)) map(() => serverStatusChange(false))
); );
const flushUpdates = action$.pipe( const flushUpdates = action$.pipe(
ofType(actionTypes.fetchUserComplete, actionTypes.updateComplete), ofType(actionTypes.fetchUserComplete, actionTypes.updateComplete),
filter(() => isSignedInSelector(state$.value)), filter(() => isSignedInSelector(state$.value)),
filter(() => store.get(key)), filter(() => store.get(key)),
filter(() => isOnlineSelector(state$.value)), filter(() => isServerOnlineSelector(state$.value)),
tap(() => { tap(() => {
let failures = store.get(key) || []; let failures = store.get(key) || [];

View File

@ -27,6 +27,7 @@ describe('failed-updates-epic', () => {
const initialState = { const initialState = {
app: { app: {
isOnline: true, isOnline: true,
isServerOnline: true,
appUsername: 'developmentuser' appUsername: 'developmentuser'
} }
}; };

View File

@ -60,6 +60,7 @@ const initialState = {
sessionMeta: { activeDonations: 0 }, sessionMeta: { activeDonations: 0 },
showDonationModal: false, showDonationModal: false,
isOnline: true, isOnline: true,
isServerOnline: true,
donationFormState: { donationFormState: {
...defaultDonationFormState ...defaultDonationFormState
} }
@ -102,6 +103,7 @@ export const updateDonationFormState = createAction(
); );
export const onlineStatusChange = createAction(actionTypes.onlineStatusChange); export const onlineStatusChange = createAction(actionTypes.onlineStatusChange);
export const serverStatusChange = createAction(actionTypes.serverStatusChange);
// TODO: re-evaluate this since /internal is no longer used. // TODO: re-evaluate this since /internal is no longer used.
// `hardGoTo` is used to hit the API server directly // `hardGoTo` is used to hit the API server directly
@ -189,6 +191,7 @@ export const stepsToClaimSelector = state => {
}; };
export const isDonatingSelector = state => userSelector(state).isDonating; export const isDonatingSelector = state => userSelector(state).isDonating;
export const isOnlineSelector = state => state[ns].isOnline; export const isOnlineSelector = state => state[ns].isOnline;
export const isServerOnlineSelector = state => state[ns].isServerOnline;
export const isSignedInSelector = state => !!state[ns].appUsername; export const isSignedInSelector = state => !!state[ns].appUsername;
export const isDonationModalOpenSelector = state => state[ns].showDonationModal; export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
export const recentlyClaimedBlockSelector = state => export const recentlyClaimedBlockSelector = state =>
@ -553,6 +556,10 @@ export const reducer = handleActions(
...state, ...state,
isOnline isOnline
}), }),
[actionTypes.serverStatusChange]: (state, { payload: isServerOnline }) => ({
...state,
isServerOnline
}),
[actionTypes.closeDonationModal]: state => ({ [actionTypes.closeDonationModal]: state => ({
...state, ...state,
showDonationModal: false showDonationModal: false

View File

@ -2,12 +2,12 @@ import { ofType } from 'redux-observable';
import { mapTo, filter } from 'rxjs/operators'; import { mapTo, filter } from 'rxjs/operators';
import { actionTypes as types } from './action-types'; import { actionTypes as types } from './action-types';
import { onlineStatusChange, isOnlineSelector } from './'; import { serverStatusChange, isServerOnlineSelector } from './';
export default function updateCompleteEpic(action$, state$) { export default function updateCompleteEpic(action$, state$) {
return action$.pipe( return action$.pipe(
ofType(types.updateComplete), ofType(types.updateComplete),
filter(() => !isOnlineSelector(state$.value)), filter(() => !isServerOnlineSelector(state$.value)),
mapTo(onlineStatusChange(true)) mapTo(serverStatusChange(true))
); );
} }