feat: SDA-1713 (Add logic to monitor network request and display banners) (#873)

* SDA-1713 - Add logic to monitor network request and display banners

* SDA-1713 - Add logic to monitor POD URL fetch to prevent VPN issues

Co-authored-by: Vishwas Shashidhar <VishwasShashidhar@users.noreply.github.com>
This commit is contained in:
Kiran Niranjan 2020-02-13 23:48:31 +05:30 committed by GitHub
parent 284ec984e6
commit 8ffed54d7b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 346 additions and 0 deletions

View File

@ -53,6 +53,10 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.IpcMainEvent, arg: IApiArgs) =>
// Since we register the prococol handler window upon login,
// we make use of it and update the pod version info on SDA
windowHandler.updateVersionInfo();
// Set this to false once the SFE is completely loaded
// so, we can prevent from showing error banners
windowHandler.isWebPageLoading = false;
break;
case apiCmds.registerLogRetriever:
registerLogRetriever(event.sender, arg.logName);
@ -89,6 +93,7 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.IpcMainEvent, arg: IApiArgs) =>
if (typeof arg.windowName === 'string') {
sanitize(arg.windowName);
}
windowHandler.isWebPageLoading = true;
break;
case apiCmds.bringToFront:
// validates the user bring to front config and activates the wrapper

View File

@ -27,6 +27,7 @@ import {
handleDownloadManager,
injectStyles,
isSymphonyReachable,
monitorNetworkInterception,
preventWindowNavigation,
reloadWindow,
windowExists,
@ -73,6 +74,7 @@ export class WindowHandler {
public willQuitApp: boolean = false;
public spellchecker: SpellChecker | undefined;
public isCustomTitleBar: boolean;
public isWebPageLoading: boolean = true;
private readonly contextIsolation: boolean;
private readonly backgroundThrottling: boolean;
@ -178,6 +180,9 @@ export class WindowHandler {
if ((this.config.isCustomTitleBar && isWindowsOS) && this.mainWindow && windowExists(this.mainWindow)) {
this.mainWindow.setMenuBarVisibility(false);
}
// monitors network connection and
// displays error banner on failure
monitorNetworkInterception();
});
this.url = WindowHandler.getValidUrl(this.globalConfig.url);

View File

@ -24,6 +24,7 @@ interface IStyles {
enum styleNames {
titleBar = 'title-bar',
snackBar = 'snack-bar',
messageBanner = 'message-banner',
}
const checkValidWindow = true;
@ -32,6 +33,7 @@ const { url: configUrl, ctWhitelist } = config.getGlobalConfigFields([ 'url', 'c
// Network status check variables
const networkStatusCheckInterval = 10 * 1000;
let networkStatusCheckIntervalId;
let isNetworkMonitorInitialized = false;
const styles: IStyles[] = [];
const DOWNLOAD_MANAGER_NAMESPACE = 'DownloadManager';
@ -451,6 +453,14 @@ export const injectStyles = async (mainWindow: BrowserWindow, isCustomTitleBar:
});
}
// Banner styles
if (styles.findIndex(({ name }) => name === styleNames.messageBanner) === -1) {
styles.push({
name: styleNames.messageBanner,
content: fs.readFileSync(path.join(__dirname, '..', '/renderer/styles/message-banner.css'), 'utf8').toString(),
});
}
return await readAndInsertCSS(mainWindow);
};
@ -570,3 +580,42 @@ export const getWindowByName = (windowName: string): BrowserWindow | undefined =
return (window as ICustomBrowserWindow).winName === windowName;
});
};
/**
* Monitors network requests and displays red banner on failure
*/
export const monitorNetworkInterception = () => {
if (isNetworkMonitorInitialized) {
return;
}
const { url } = config.getGlobalConfigFields( [ 'url' ] );
const { hostname, protocol } = parse(url);
if (!hostname || !protocol) {
return;
}
const mainWindow = windowHandler.getMainWindow();
const podUrl = `${protocol}//${hostname}/`;
logger.info('window-utils: monitoring network interception for url', podUrl);
// Filter applied w.r.t pod url
const filter = { urls: [ podUrl + '*' ] };
if (mainWindow && windowExists(mainWindow)) {
isNetworkMonitorInitialized = true;
mainWindow.webContents.session.webRequest.onErrorOccurred(filter,(details) => {
if (!mainWindow || !windowExists(mainWindow)) {
return;
}
if (windowHandler.isWebPageLoading
&& details.error === 'net::ERR_INTERNET_DISCONNECTED'
|| details.error === 'net::ERR_NETWORK_CHANGED'
|| details.error === 'net::ERR_NAME_NOT_RESOLVED') {
logger.error(`window-utils: URL failed to load`, details);
mainWindow.webContents.send('show-banner', { show: true, bannerType: 'error', url: podUrl });
}
});
}
};

View File

@ -12,6 +12,10 @@
"Actual Size": "Actual Size",
"Always on Top": "Always on Top",
"Auto Launch On Startup": "Auto Launch On Startup",
"Banner": {
"Connection lost. This message will disappear once the connection is restored.": "Connection lost. This message will disappear once the connection is restored.",
"Retry Now": "Retry Now"
},
"BasicAuth": {
"Authentication Request": "Authentication Request",
"Cancel": "Cancel",

View File

@ -12,6 +12,10 @@
"Actual Size": "Actual Size",
"Always on Top": "Always on Top",
"Auto Launch On Startup": "Auto Launch On Startup",
"Banner": {
"Connection lost. This message will disappear once the connection is restored.": "Connection lost. This message will disappear once the connection is restored.",
"Retry Now": "Retry Now"
},
"BasicAuth": {
"Authentication Request": "Authentication Request",
"Cancel": "Cancel",

View File

@ -12,6 +12,10 @@
"Actual Size": "Taille actuelle",
"Always on Top": "Garder Symphony au premier plan",
"Auto Launch On Startup": "Lancement automatique au démarrage",
"Banner": {
"Connection lost. This message will disappear once the connection is restored.": "Connexion perdue. Ce message disparaîtra une fois la connexion rétablie.",
"Retry Now": "Réessayer maintenant"
},
"BasicAuth": {
"Authentication Request": "Demande d'authentification",
"Cancel": "Annuler",

View File

@ -12,6 +12,10 @@
"Actual Size": "Taille actuelle",
"Always on Top": "Garder Symphony au premier plan",
"Auto Launch On Startup": "Lancement automatique au démarrage",
"Banner": {
"Connection lost. This message will disappear once the connection is restored.": "Connexion perdue. Ce message disparaîtra une fois la connexion rétablie.",
"Retry Now": "Réessayer maintenant"
},
"BasicAuth": {
"Authentication Request": "Demande d'authentification",
"Cancel": "Annuler",

View File

@ -12,6 +12,10 @@
"Actual Size": "実際のサイズ",
"Always on Top": "つねに前面に表示",
"Auto Launch On Startup": "スタートアップ時に自動起動",
"Banner": {
"Connection lost. This message will disappear once the connection is restored.": "接続が失われました。接続が回復すると、このメッセージは消えます。",
"Retry Now": "今すぐ再試行"
},
"BasicAuth": {
"Authentication Request": "認証要求",
"Cancel": "キャンセル",

View File

@ -12,6 +12,10 @@
"Actual Size": "実際のサイズ",
"Always on Top": "つねに前面に表示",
"Auto Launch On Startup": "スタートアップ時に自動起動",
"Banner": {
"Connection lost. This message will disappear once the connection is restored.": "接続が失われました。接続が回復すると、このメッセージは消えます。",
"Retry Now": "今すぐ再試行"
},
"BasicAuth": {
"Authentication Request": "認証要求",
"Cancel": "キャンセル",

View File

@ -0,0 +1,145 @@
import { i18n } from '../../common/i18n-preload';
export type bannerTypes = 'error' | 'warning';
const BANNER_NAME_SPACE = 'Banner';
// Network status check variables
const onlineStateInterval = 5 * 1000;
let onlineStateIntervalId;
export default class MessageBanner {
private readonly body: HTMLCollectionOf<Element> | undefined;
private banner: HTMLElement | null = null;
private closeButton: HTMLElement | null = null;
private retryButton: HTMLElement | null = null;
private domParser: DOMParser | undefined;
private url: string | undefined;
constructor() {
this.body = document.getElementsByTagName('body');
window.addEventListener('beforeunload', () => {
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
}, false);
}
/**
* initializes red banner
*/
public initBanner(): void {
this.domParser = new DOMParser();
const banner = this.domParser.parseFromString(this.render(), 'text/html');
this.closeButton = banner.getElementById('banner-close');
if (this.closeButton) {
this.closeButton.addEventListener('click', this.removeBanner);
}
this.retryButton = banner.getElementById('banner-retry');
if (this.retryButton) {
this.retryButton.addEventListener('click', this.reload);
}
this.banner = banner.getElementById('sda-banner');
}
/**
* Injects SDA banner into DOM
*
* @param show {boolean}
* @param type {bannerTypes}
* @param url {string} - POD URL from global config file
*/
public showBanner(show: boolean, type: bannerTypes, url?: string): void {
this.url = url;
if (this.body && this.body.length > 0 && this.banner) {
this.body[ 0 ].appendChild(this.banner);
if (show) {
this.banner.classList.add('sda-banner-show');
this.monitorOnlineState();
}
switch (type) {
case 'error':
this.banner.classList.add('sda-banner-error');
break;
case 'warning':
this.banner.classList.add('sda-banner-warning');
default:
break;
}
}
}
/**
* removes the message banner
*/
public removeBanner(): void {
const banner = document.getElementById('sda-banner');
if (banner) {
banner.classList.remove('sda-banner-show');
}
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
}
/**
* Monitors network online status and updates the banner
*/
public monitorOnlineState(): void {
if (onlineStateIntervalId) {
return;
}
onlineStateIntervalId = setInterval(async () => {
try {
const response = await window.fetch(this.url || window.location.href, { cache: 'no-cache', keepalive: false });
if (window.navigator.onLine && (response.status === 200 || response.ok)) {
if (this.banner) {
this.banner.classList.remove('sda-banner-show');
}
if (onlineStateIntervalId) {
clearInterval(onlineStateIntervalId);
onlineStateIntervalId = null;
}
this.reload();
}
// tslint:disable-next-line:no-empty
} catch (e) {}
}, onlineStateInterval);
}
/**
* reloads the web page
*/
public reload(): void {
if (document.location) {
document.location.reload();
}
}
/**
* Renders a message banner
*/
public render(): string {
return `
<div id='sda-banner' class='sda-banner'>
<span class='sda-banner-icon'></span>
<span class='sda-banner-message'>
${i18n.t('Connection lost. This message will disappear once the connection is restored.', BANNER_NAME_SPACE)()}
</span>
<span id='banner-retry' class='sda-banner-retry-button' title='${i18n.t('Retry Now', BANNER_NAME_SPACE)()}'>
${i18n.t('Retry Now', BANNER_NAME_SPACE)()}
</span>
<span id='banner-close' class='sda-banner-close-icon' title='${i18n.t('Close')()}'></span>
</div>
`;
}
}

View File

@ -6,6 +6,7 @@ import { apiCmds, apiName } from '../common/api-interface';
import { i18n } from '../common/i18n-preload';
import './app-bridge';
import DownloadManager from './components/download-manager';
import MessageBanner from './components/message-banner';
import NetworkError from './components/network-error';
import SnackBar from './components/snack-bar';
import WindowsTitleBar from './components/windows-title-bar';
@ -19,6 +20,7 @@ const ssfWindow: ISSFWindow = window;
const minMemoryFetchInterval = 4 * 60 * 60 * 1000;
const maxMemoryFetchInterval = 12 * 60 * 60 * 1000;
const snackBar = new SnackBar();
const banner = new MessageBanner();
/**
* creates API exposed from electron.
@ -108,6 +110,10 @@ ipcRenderer.on('page-load', (_event, { locale, resources, enableCustomTitleBar,
const downloadManager = new DownloadManager();
downloadManager.initDownloadManager();
// initialize red banner
banner.initBanner();
banner.showBanner(false, 'error');
if (isMainWindow) {
monitorMemory(getRandomTime(minMemoryFetchInterval, maxMemoryFetchInterval));
}
@ -127,3 +133,10 @@ ipcRenderer.on('network-error', (_event, { error }) => {
const networkError = React.createElement(NetworkError, { error });
ReactDOM.render(networkError, networkErrorContainer);
});
ipcRenderer.on('show-banner', (_event, { show, bannerType, url }) => {
if (!!document.getElementsByClassName('sda-banner-show').length) {
return;
}
banner.showBanner(show, bannerType, url);
});

View File

@ -0,0 +1,105 @@
@import "theme";
.sda-banner-error {
--info: #FFC2C2;
--info-border: #D4172D;
--info-icon: #E57373;
--info-text: #B0072C;
--info-link: #0962F1;
}
.sda-banner-warning {
--info: #F1E7CA;
--info-icon: #FDCD3B;
--info-text: #000000;
--info-link: #0962F1;
}
.sda-banner {
top: 0;
z-index: 2001 !important;
height: 34px;
display: none;
position: fixed;
left: 0;
width: 100%;
font-size: 12px;
background-color: var(--info);
font-family: @font-family;
font-weight: 400;
&-focus-active {
border-color: transparent !important;
box-shadow: none !important;
}
&-message {
margin-left: 18px;
margin-top: 4px;
display: flex;
flex: auto;
color: var(--info-text);
}
&-close-icon {
opacity: 0.8;
}
&-icon {
margin: 0;
height: 100%;
width: 38px;
opacity: 1;
background-image: url("data:image/svg+xml,%3Csvg id='fill' xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Eset2%3C/title%3E%3Cpath class='cls-1' d='M24,0A24,24,0,1,0,48,24,24,24,0,0,0,24,0Zm4,42a2,2,0,0,1-2,2H22a2,2,0,0,1-2-2V38a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2ZM28,9.88V30a2,2,0,0,1-2,2H22a2,2,0,0,1-2-2V6a2,2,0,0,1,2-2h4a2,2,0,0,1,2,2Z'/%3E%3C/svg%3E%0A");;
background-repeat: no-repeat;
background-size: 16px;
background-position: center;
}
&-error {
background-color: var(--info);
}
&-icon {
background-color: var(--info-icon) !important;
}
&-warning {
background-color: var(--info);
}
&-show {
display: flex;
align-items: center;
}
&-retry-button, &-action-button {
color: var(--info-link);
flex: 0 0 auto;
margin-left: 4px;
margin-right: 4px;
margin-top: 0;
&:hover, &:focus {
cursor: pointer;
}
&:active {
cursor: pointer;
}
}
&-close-icon {
flex: 0 0 auto;
margin-left: auto;
cursor: pointer;
margin-right: 8px;
width: 16px;
height: 16px;
opacity: 0.5;
background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --%3E%3Csvg version='1.1' id='fill' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' viewBox='0 0 48 48' style='enable-background:new 0 0 48 48;' xml:space='preserve'%3E%3Ctitle%3Eset1%3C/title%3E%3Cpath d='M39.4,33.8L31,25.4c-0.4-0.4-0.9-0.9-1.4-1.4c0.5-0.5,1-1,1.4-1.4l8.4-8.4c0.8-0.8,0.8-2,0-2.8l-2.8-2.8 c-0.8-0.8-2-0.8-2.8,0L25.4,17c-0.4,0.4-0.9,0.9-1.4,1.4c-0.5-0.5-1-1-1.4-1.4l-8.4-8.4c-0.8-0.8-2-0.8-2.8,0l-2.8,2.8 c-0.8,0.8-0.8,2,0,2.8l8.4,8.4c0.4,0.4,0.9,0.9,1.4,1.4c-0.5,0.5-1,1-1.4,1.4l-8.4,8.4c-0.8,0.8-0.8,2,0,2.8l2.8,2.8 c0.8,0.8,2,0.8,2.8,0l8.4-8.4c0.4-0.4,0.9-0.9,1.4-1.4c0.5,0.5,1,1,1.4,1.4l8.4,8.4c0.8,0.8,2,0.8,2.8,0l2.8-2.8 C40.2,35.8,40.2,34.6,39.4,33.8z'/%3E%3C/svg%3E%0A");
background-repeat: no-repeat;
background-size: 8px;
background-position: center;
}
}