From 645cc609e3788c0218af66b3535a10da40ce7749 Mon Sep 17 00:00:00 2001 From: Lee Wexler Date: Tue, 17 Sep 2024 14:27:40 -0400 Subject: [PATCH] Deliver Alert Banner data with environment polling (#3786) + Requires corresponding hoist-core change https://github.com/xh/hoist-core/pull/404 --- CHANGELOG.md | 6 +++ .../general/alertBanner/AlertBannerModel.ts | 47 +++++++++-------- .../general/alertBanner/AlertBannerPanel.ts | 8 ++- svc/AlertBannerService.ts | 36 +++---------- svc/EnvironmentService.ts | 50 +++++++++++-------- 5 files changed, 67 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a6c46b606..096570b016 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,15 @@ ## 68.0.0-SNAPSHOT - unreleased +### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - Hoist Core update only) + +* Requires `hoist-core >= 21.1` for consolidated polling of Alert Banner updates (see below). + ### ⚙️ Technical * Updated Admin Console's Cluster tab to refresh more frequently. +* Consolidated the polling check for Alert Banner updates into existing `EnvironmentService` + polling, avoiding an extra request and improving alert banner responsiveness. ### ⚙️ Typescript API Adjustments diff --git a/admin/tabs/general/alertBanner/AlertBannerModel.ts b/admin/tabs/general/alertBanner/AlertBannerModel.ts index 1ae01fe443..4fe8aebd41 100644 --- a/admin/tabs/general/alertBanner/AlertBannerModel.ts +++ b/admin/tabs/general/alertBanner/AlertBannerModel.ts @@ -10,13 +10,13 @@ import {FormModel} from '@xh/hoist/cmp/form'; import {fragment, p} from '@xh/hoist/cmp/layout'; import {HoistModel, Intent, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core'; import {dateIs, required} from '@xh/hoist/data'; -import {action, computed, makeObservable, observable} from '@xh/hoist/mobx'; +import {action, bindable, computed, makeObservable, observable} from '@xh/hoist/mobx'; import {AlertBannerSpec} from '@xh/hoist/svc'; import {isEqual, isMatch, sortBy, without} from 'lodash'; export class AlertBannerModel extends HoistModel { - savedValue; - @observable.ref savedPresets: PlainObject[] = []; + savedValue: AlertBannerSpec; + @bindable.ref savedPresets: PlainObject[] = []; @managed formModel = new FormModel({ @@ -118,7 +118,6 @@ export class AlertBannerModel extends HoistModel { this.formModel.setValues({...preset, expires: null}); } - @action addPreset() { const {message, intent, iconName, enableClose, clientApps} = this.formModel.values, dateCreated = Date.now(), @@ -188,24 +187,6 @@ export class AlertBannerModel extends HoistModel { } } - async saveBannerSpecAsync(spec: AlertBannerSpec) { - const {active, message, intent, iconName, enableClose, clientApps} = spec; - try { - await XH.fetchService.postJson({ - url: 'alertBannerAdmin/setAlertSpec', - body: spec, - track: { - category: 'Audit', - message: 'Updated Alert Banner', - data: {active, message, intent, iconName, enableClose, clientApps}, - logData: ['active'] - } - }); - } catch (e) { - XH.handleException(e); - } - } - //---------------- // Implementation //---------------- @@ -232,7 +213,7 @@ export class AlertBannerModel extends HoistModel { let preservedPublishDate = null; // Ask some questions if we are dealing with live stuff - if (XH.alertBannerService.enabled && (active || savedValue?.active)) { + if (active || savedValue?.active) { // Question 1. Reshow when modifying an active && already active, closable banner? if ( active && @@ -299,7 +280,25 @@ export class AlertBannerModel extends HoistModel { }; await this.saveBannerSpecAsync(value); - await XH.alertBannerService.checkForBannerAsync(); + await XH.environmentService.pollServerAsync(); await this.refreshAsync(); } + + private async saveBannerSpecAsync(spec: AlertBannerSpec) { + const {active, message, intent, iconName, enableClose, clientApps} = spec; + try { + await XH.fetchService.postJson({ + url: 'alertBannerAdmin/setAlertSpec', + body: spec, + track: { + category: 'Audit', + message: 'Updated Alert Banner', + data: {active, message, intent, iconName, enableClose, clientApps}, + logData: ['active'] + } + }); + } catch (e) { + XH.handleException(e); + } + } } diff --git a/admin/tabs/general/alertBanner/AlertBannerPanel.ts b/admin/tabs/general/alertBanner/AlertBannerPanel.ts index 8589449f82..e9a4341ee0 100644 --- a/admin/tabs/general/alertBanner/AlertBannerPanel.ts +++ b/admin/tabs/general/alertBanner/AlertBannerPanel.ts @@ -37,7 +37,7 @@ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar'; import {dateTimeRenderer} from '@xh/hoist/format'; import {Icon} from '@xh/hoist/icon'; import {menu, menuItem, popover} from '@xh/hoist/kit/blueprint'; -import {LocalDate, SECONDS} from '@xh/hoist/utils/datetime'; +import {LocalDate} from '@xh/hoist/utils/datetime'; import {isEmpty} from 'lodash'; import {ReactNode} from 'react'; import {AlertBannerModel} from './AlertBannerModel'; @@ -72,15 +72,13 @@ const formPanel = hoistCmp.factory(({model}) => { labelWidth: 100 }, items: [ - XH.alertBannerService.enabled + XH.getConf('xhAlertBannerConfig', {}).enabled ? div({ className: `${baseClassName}__intro`, items: [ p(`Show an alert banner to all ${XH.appName} users.`), p( - `Configure and preview below. Presets can be saved and loaded via bottom bar menu. Banner will appear to all users within ${ - XH.alertBannerService.interval / SECONDS - }s once marked Active and saved.` + 'Configure and preview below. Presets can be saved and loaded via bottom bar menu. Banner will appear to all users once marked Active and saved.' ) ] }) diff --git a/svc/AlertBannerService.ts b/svc/AlertBannerService.ts index 58a16ce69b..3a2808bf30 100644 --- a/svc/AlertBannerService.ts +++ b/svc/AlertBannerService.ts @@ -6,52 +6,28 @@ */ import {BannerModel} from '@xh/hoist/appcontainer/BannerModel'; import {markdown} from '@xh/hoist/cmp/markdown'; -import {BannerSpec, HoistService, Intent, managed, XH} from '@xh/hoist/core'; +import {BannerSpec, HoistService, Intent, XH} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; -import {Timer} from '@xh/hoist/utils/async'; -import {SECONDS} from '@xh/hoist/utils/datetime'; import {compact, isEmpty, map, trim} from 'lodash'; /** * Service to display an app-wide alert banner, as configured via the Hoist Admin console. * - * For this service to be active, a client-visible `xhAlertBannerConfig` config must be specified - * as `{enabled:true, interval: x}`, where `x` sets this service's polling frequency in seconds. + * Note that the client is provided with updated banner data from the server via + * EnvironmentService, and its regular polling. See 'xhEnvPollConfig' for more information. */ export class AlertBannerService extends HoistService { override xhImpl = true; static instance: AlertBannerService; - @managed - private timer: Timer; - - get interval(): number { - const conf = XH.getConf('xhAlertBannerConfig', {}); - return conf.enabled && conf.interval ? conf.interval * SECONDS : -1; - } - - get enabled(): boolean { - return this.interval > 0; - } - get lastDismissed(): number { return XH.localStorageService.get('xhAlertBanner.lastDismissed'); } - override async initAsync() { - this.timer = Timer.create({ - runFn: () => this.checkForBannerAsync(), - interval: this.interval - }); - } - - async checkForBannerAsync() { - if (!this.enabled) return; - - const data: AlertBannerSpec = await XH.fetchJson({url: 'xh/alertBanner'}), - {active, expires, publishDate, message, intent, iconName, enableClose, clientApps} = - data, + async updateBanner(spec: AlertBannerSpec) { + const {active, expires, publishDate, message, intent, iconName, enableClose, clientApps} = + spec, {lastDismissed, onClose} = this; if ( diff --git a/svc/EnvironmentService.ts b/svc/EnvironmentService.ts index 017e86eff1..7050c19575 100644 --- a/svc/EnvironmentService.ts +++ b/svc/EnvironmentService.ts @@ -18,7 +18,7 @@ import {version as reactVersion} from 'react'; /** * Load and report on the client and server environment, including software versions, timezones, and - * and other technical information. + * other technical information. */ export class EnvironmentService extends HoistService { static instance: EnvironmentService; @@ -49,7 +49,7 @@ export class EnvironmentService extends HoistService { private pollTimer: Timer; override async initAsync() { - const {pollConfig, instanceName, ...serverEnv} = await XH.fetchJson({ + const {pollConfig, instanceName, alertBanner, ...serverEnv} = await XH.fetchJson({ url: 'xh/environment' }), clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? 'Unknown', @@ -81,7 +81,10 @@ export class EnvironmentService extends HoistService { this.pollConfig = pollConfig; this.addReaction({ when: () => XH.appIsRunning, - run: this.startPolling + run: () => { + XH.alertBannerService.updateBanner(alertBanner); + this.startPolling(); + } }); } @@ -109,23 +112,11 @@ export class EnvironmentService extends HoistService { return checkMaxVersion(this.get('hoistCoreVersion'), version); } - //------------------------------ - // Implementation - //------------------------------ - constructor() { - super(); - makeObservable(this); - } - - private startPolling() { - this.pollTimer = Timer.create({ - runFn: () => this.pollServerAsync(), - interval: this.pollIntervalMs, - delay: true - }); - } - - private async pollServerAsync() { + /** + * Update critical environment information from server. + * @internal - not for app use. Called by `pollTimer` and as needed by Hoist code. + */ + async pollServerAsync() { let data; try { data = await XH.fetchJson({url: 'xh/environmentPoll'}); @@ -135,10 +126,11 @@ export class EnvironmentService extends HoistService { } // Update config/interval, and server info - const {pollConfig, instanceName, appVersion, appBuild} = data; + const {pollConfig, instanceName, alertBanner, appVersion, appBuild} = data; this.pollConfig = pollConfig; this.pollTimer.setInterval(this.pollIntervalMs); this.setServerInfo(instanceName, appVersion, appBuild); + XH.alertBannerService.updateBanner(alertBanner); // Handle version change if (appVersion != XH.getEnv('appVersion') || appBuild != XH.getEnv('appBuild')) { @@ -163,6 +155,22 @@ export class EnvironmentService extends HoistService { } } + //------------------------------ + // Implementation + //------------------------------ + constructor() { + super(); + makeObservable(this); + } + + private startPolling() { + this.pollTimer = Timer.create({ + runFn: () => this.pollServerAsync(), + interval: this.pollIntervalMs, + delay: true + }); + } + @action private setServerInfo(serverInstance: string, serverVersion: string, serverBuild: string) { this.serverInstance = serverInstance;