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": {
|
"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": "郵件註冊",
|
||||||
|
@ -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": "邮件注册",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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}
|
||||||
|
@ -12,6 +12,7 @@ export const actionTypes = createTypes(
|
|||||||
'preventProgressDonationRequests',
|
'preventProgressDonationRequests',
|
||||||
'openDonationModal',
|
'openDonationModal',
|
||||||
'onlineStatusChange',
|
'onlineStatusChange',
|
||||||
|
'serverStatusChange',
|
||||||
'resetUserData',
|
'resetUserData',
|
||||||
'tryToShowDonationModal',
|
'tryToShowDonationModal',
|
||||||
'executeGA',
|
'executeGA',
|
||||||
|
@ -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) || [];
|
||||||
|
|
||||||
|
@ -27,6 +27,7 @@ describe('failed-updates-epic', () => {
|
|||||||
const initialState = {
|
const initialState = {
|
||||||
app: {
|
app: {
|
||||||
isOnline: true,
|
isOnline: true,
|
||||||
|
isServerOnline: true,
|
||||||
appUsername: 'developmentuser'
|
appUsername: 'developmentuser'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user