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:
@ -364,6 +364,7 @@
|
||||
},
|
||||
"misc": {
|
||||
"offline": "你已離線,學習進度可能不會被保存",
|
||||
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
|
||||
"unsubscribed": "你已成功取消訂閱",
|
||||
"keep-coding": "無論你做什麼,都要繼續編程!",
|
||||
"email-signup": "郵件註冊",
|
||||
|
@ -364,6 +364,7 @@
|
||||
},
|
||||
"misc": {
|
||||
"offline": "你已离线,学习进度可能不会被保存",
|
||||
"server-offline": "The server could not be reached and your progress may not be saved. Please contact support if this message persists",
|
||||
"unsubscribed": "你已成功取消订阅",
|
||||
"keep-coding": "无论你做什么,都要继续编程!",
|
||||
"email-signup": "邮件注册",
|
||||
|
@ -318,7 +318,7 @@
|
||||
"nicely-done": "Nicely done. You just completed {{block}}.",
|
||||
"credit-card": "Credit Card",
|
||||
"credit-card-2": "Or donate with a credit card:",
|
||||
"or-card": "Or donate with card",
|
||||
"or-card": "Or donate with card",
|
||||
"paypal": "with PayPal:",
|
||||
"need-email": "We need a valid email address to which we can send your donation tax receipt.",
|
||||
"went-wrong": "Something went wrong processing your donation. Your card has not been charged.",
|
||||
@ -364,6 +364,7 @@
|
||||
},
|
||||
"misc": {
|
||||
"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",
|
||||
"keep-coding": "Whatever you go on to, keep coding!",
|
||||
"email-signup": "Email Sign Up",
|
||||
|
@ -364,6 +364,7 @@
|
||||
},
|
||||
"misc": {
|
||||
"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",
|
||||
"keep-coding": "Sea lo que sea que hagas, ¡sigue programando!",
|
||||
"email-signup": "Registrarse con Email",
|
||||
|
@ -364,6 +364,7 @@
|
||||
},
|
||||
"misc": {
|
||||
"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",
|
||||
"keep-coding": "Qualsiasi cosa tu abbia intenzione di fare, continua a programmare!",
|
||||
"email-signup": "Registrazione con email",
|
||||
|
@ -364,6 +364,7 @@
|
||||
},
|
||||
"misc": {
|
||||
"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",
|
||||
"keep-coding": "Seja o que for, continue programando!",
|
||||
"email-signup": "Inscrição via e-mail",
|
||||
|
@ -7,7 +7,7 @@
|
||||
padding-bottom: 3px;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.flash-message div {
|
||||
|
@ -3,4 +3,11 @@
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 150;
|
||||
}
|
||||
|
||||
.offline-warning a {
|
||||
margin: 0 1ch;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import './offline-warning.css';
|
||||
|
||||
@ -8,20 +8,30 @@ let id: ReturnType<typeof setTimeout>;
|
||||
|
||||
interface OfflineWarningProps {
|
||||
isOnline: boolean;
|
||||
isServerOnline: boolean;
|
||||
isSignedIn: boolean;
|
||||
}
|
||||
|
||||
function OfflineWarning({
|
||||
isOnline,
|
||||
isServerOnline,
|
||||
isSignedIn
|
||||
}: OfflineWarningProps): JSX.Element | null {
|
||||
const { t } = useTranslation();
|
||||
const [showWarning, setShowWarning] = React.useState(false);
|
||||
let message;
|
||||
|
||||
if (!isSignedIn || isOnline) {
|
||||
if (!isSignedIn || (isOnline && isServerOnline)) {
|
||||
clearTimeout(id);
|
||||
if (showWarning) setShowWarning(false);
|
||||
} else {
|
||||
message = !isOnline ? (
|
||||
t('misc.offline')
|
||||
) : (
|
||||
<Trans i18nKey='misc.server-offline'>
|
||||
<a href={'mailto:support@freecodecamp.org'}>placeholder</a>
|
||||
</Trans>
|
||||
);
|
||||
timeout();
|
||||
}
|
||||
|
||||
@ -32,7 +42,10 @@ function OfflineWarning({
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,9 @@ import {
|
||||
fetchUser,
|
||||
isSignedInSelector,
|
||||
onlineStatusChange,
|
||||
serverStatusChange,
|
||||
isOnlineSelector,
|
||||
isServerOnlineSelector,
|
||||
userFetchStateSelector,
|
||||
userSelector,
|
||||
usernameSelector,
|
||||
@ -55,10 +57,12 @@ const propTypes = {
|
||||
}),
|
||||
hasMessage: PropTypes.bool,
|
||||
isOnline: PropTypes.bool.isRequired,
|
||||
isServerOnline: PropTypes.bool.isRequired,
|
||||
isSignedIn: PropTypes.bool,
|
||||
onlineStatusChange: PropTypes.func.isRequired,
|
||||
pathname: PropTypes.string.isRequired,
|
||||
removeFlashMessage: PropTypes.func.isRequired,
|
||||
serverStatusChange: PropTypes.func.isRequired,
|
||||
showFooter: PropTypes.bool,
|
||||
signedInUserName: PropTypes.string,
|
||||
t: PropTypes.func.isRequired,
|
||||
@ -71,14 +75,16 @@ const mapStateToProps = createSelector(
|
||||
isSignedInSelector,
|
||||
flashMessageSelector,
|
||||
isOnlineSelector,
|
||||
isServerOnlineSelector,
|
||||
userFetchStateSelector,
|
||||
userSelector,
|
||||
usernameSelector,
|
||||
(isSignedIn, flashMessage, isOnline, fetchState, user) => ({
|
||||
(isSignedIn, flashMessage, isOnline, isServerOnline, fetchState, user) => ({
|
||||
isSignedIn,
|
||||
flashMessage,
|
||||
hasMessage: !!flashMessage.message,
|
||||
isOnline,
|
||||
isServerOnline,
|
||||
fetchState,
|
||||
theme: user.theme,
|
||||
user
|
||||
@ -87,7 +93,13 @@ const mapStateToProps = createSelector(
|
||||
|
||||
const mapDispatchToProps = dispatch =>
|
||||
bindActionCreators(
|
||||
{ fetchUser, removeFlashMessage, onlineStatusChange, executeGA },
|
||||
{
|
||||
fetchUser,
|
||||
removeFlashMessage,
|
||||
onlineStatusChange,
|
||||
serverStatusChange,
|
||||
executeGA
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
|
||||
@ -130,6 +142,7 @@ class DefaultLayout extends Component {
|
||||
fetchState,
|
||||
flashMessage,
|
||||
isOnline,
|
||||
isServerOnline,
|
||||
isSignedIn,
|
||||
removeFlashMessage,
|
||||
showFooter = true,
|
||||
@ -201,7 +214,11 @@ class DefaultLayout extends Component {
|
||||
</Helmet>
|
||||
<div className={`default-layout`}>
|
||||
<Header fetchState={fetchState} user={user} />
|
||||
<OfflineWarning isOnline={isOnline} isSignedIn={isSignedIn} />
|
||||
<OfflineWarning
|
||||
isOnline={isOnline}
|
||||
isServerOnline={isServerOnline}
|
||||
isSignedIn={isSignedIn}
|
||||
/>
|
||||
{hasMessage && flashMessage ? (
|
||||
<Flash flashMessage={flashMessage} onClose={removeFlashMessage} />
|
||||
) : null}
|
||||
|
@ -12,6 +12,7 @@ export const actionTypes = createTypes(
|
||||
'preventProgressDonationRequests',
|
||||
'openDonationModal',
|
||||
'onlineStatusChange',
|
||||
'serverStatusChange',
|
||||
'resetUserData',
|
||||
'tryToShowDonationModal',
|
||||
'executeGA',
|
||||
|
@ -15,7 +15,11 @@ import { backEndProject } from '../../utils/challenge-types';
|
||||
import { isGoodXHRStatus } from '../templates/Challenges/utils';
|
||||
import postUpdate$ from '../templates/Challenges/utils/postUpdate$';
|
||||
import { actionTypes } from './action-types';
|
||||
import { onlineStatusChange, isOnlineSelector, isSignedInSelector } from './';
|
||||
import {
|
||||
serverStatusChange,
|
||||
isServerOnlineSelector,
|
||||
isSignedInSelector
|
||||
} from './';
|
||||
|
||||
const key = 'fcc-failed-updates';
|
||||
|
||||
@ -37,14 +41,14 @@ function failedUpdateEpic(action$, state$) {
|
||||
store.set(key, [...failures, payload]);
|
||||
}
|
||||
}),
|
||||
map(() => onlineStatusChange(false))
|
||||
map(() => serverStatusChange(false))
|
||||
);
|
||||
|
||||
const flushUpdates = action$.pipe(
|
||||
ofType(actionTypes.fetchUserComplete, actionTypes.updateComplete),
|
||||
filter(() => isSignedInSelector(state$.value)),
|
||||
filter(() => store.get(key)),
|
||||
filter(() => isOnlineSelector(state$.value)),
|
||||
filter(() => isServerOnlineSelector(state$.value)),
|
||||
tap(() => {
|
||||
let failures = store.get(key) || [];
|
||||
|
||||
|
@ -27,6 +27,7 @@ describe('failed-updates-epic', () => {
|
||||
const initialState = {
|
||||
app: {
|
||||
isOnline: true,
|
||||
isServerOnline: true,
|
||||
appUsername: 'developmentuser'
|
||||
}
|
||||
};
|
||||
|
@ -60,6 +60,7 @@ const initialState = {
|
||||
sessionMeta: { activeDonations: 0 },
|
||||
showDonationModal: false,
|
||||
isOnline: true,
|
||||
isServerOnline: true,
|
||||
donationFormState: {
|
||||
...defaultDonationFormState
|
||||
}
|
||||
@ -102,6 +103,7 @@ export const updateDonationFormState = createAction(
|
||||
);
|
||||
|
||||
export const onlineStatusChange = createAction(actionTypes.onlineStatusChange);
|
||||
export const serverStatusChange = createAction(actionTypes.serverStatusChange);
|
||||
|
||||
// TODO: re-evaluate this since /internal is no longer used.
|
||||
// `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 isOnlineSelector = state => state[ns].isOnline;
|
||||
export const isServerOnlineSelector = state => state[ns].isServerOnline;
|
||||
export const isSignedInSelector = state => !!state[ns].appUsername;
|
||||
export const isDonationModalOpenSelector = state => state[ns].showDonationModal;
|
||||
export const recentlyClaimedBlockSelector = state =>
|
||||
@ -553,6 +556,10 @@ export const reducer = handleActions(
|
||||
...state,
|
||||
isOnline
|
||||
}),
|
||||
[actionTypes.serverStatusChange]: (state, { payload: isServerOnline }) => ({
|
||||
...state,
|
||||
isServerOnline
|
||||
}),
|
||||
[actionTypes.closeDonationModal]: state => ({
|
||||
...state,
|
||||
showDonationModal: false
|
||||
|
@ -2,12 +2,12 @@ import { ofType } from 'redux-observable';
|
||||
import { mapTo, filter } from 'rxjs/operators';
|
||||
|
||||
import { actionTypes as types } from './action-types';
|
||||
import { onlineStatusChange, isOnlineSelector } from './';
|
||||
import { serverStatusChange, isServerOnlineSelector } from './';
|
||||
|
||||
export default function updateCompleteEpic(action$, state$) {
|
||||
return action$.pipe(
|
||||
ofType(types.updateComplete),
|
||||
filter(() => !isOnlineSelector(state$.value)),
|
||||
mapTo(onlineStatusChange(true))
|
||||
filter(() => !isServerOnlineSelector(state$.value)),
|
||||
mapTo(serverStatusChange(true))
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user