Skip to content

Commit

Permalink
Deliver Alert Banner data with environment polling (#3786)
Browse files Browse the repository at this point in the history
+ Requires corresponding hoist-core change xh/hoist-core#404
  • Loading branch information
lbwexler authored Sep 17, 2024
1 parent 7d3d89f commit 645cc60
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 80 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
47 changes: 23 additions & 24 deletions admin/tabs/general/alertBanner/AlertBannerModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
//----------------
Expand All @@ -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 &&
Expand Down Expand Up @@ -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);
}
}
}
8 changes: 3 additions & 5 deletions admin/tabs/general/alertBanner/AlertBannerPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -72,15 +72,13 @@ const formPanel = hoistCmp.factory<AlertBannerModel>(({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.'
)
]
})
Expand Down
36 changes: 6 additions & 30 deletions svc/AlertBannerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
50 changes: 29 additions & 21 deletions svc/EnvironmentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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();
}
});
}

Expand Down Expand Up @@ -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'});
Expand All @@ -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')) {
Expand All @@ -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;
Expand Down

0 comments on commit 645cc60

Please sign in to comment.