mirror of
https://github.com/finos/SymphonyElectron.git
synced 2025-02-25 18:55:29 -06:00
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:
parent
284ec984e6
commit
8ffed54d7b
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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": "キャンセル",
|
||||
|
@ -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": "キャンセル",
|
||||
|
145
src/renderer/components/message-banner.tsx
Normal file
145
src/renderer/components/message-banner.tsx
Normal 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>
|
||||
`;
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
105
src/renderer/styles/message-banner.less
Normal file
105
src/renderer/styles/message-banner.less
Normal 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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user