/* * This file belongs to Hoist, an application development toolkit * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) * * Copyright © 2026 Extremely Heavy Industries Inc. */ import {AppModel} from '@xh/hoist/admin/AppModel'; import {timestampNoYear} from '@xh/hoist/admin/columns'; import {connPoolMonitorPanel} from '@xh/hoist/admin/tabs/cluster/instances/connpool/ConnPoolMonitorPanel'; import {serverEnvPanel} from '@xh/hoist/admin/tabs/cluster/instances/environment/ServerEnvPanel'; import {logViewer} from '@xh/hoist/admin/tabs/cluster/instances/logs/LogViewer'; import { usedHeapMb, usedPctMax } from '@xh/hoist/admin/tabs/cluster/instances/memory/MemoryMonitorModel'; import {memoryMonitorPanel} from '@xh/hoist/admin/tabs/cluster/instances/memory/MemoryMonitorPanel'; import {servicePanel} from '@xh/hoist/admin/tabs/cluster/instances/services/ServicePanel'; import {badge} from '@xh/hoist/cmp/badge'; import {GridContextMenuSpec, GridModel, numberCol} from '@xh/hoist/cmp/grid'; import {hbox} from '@xh/hoist/cmp/layout'; import {getRelativeTimestamp} from '@xh/hoist/cmp/relativetimestamp'; import {TabContainerModel, TabModel} from '@xh/hoist/cmp/tab'; import {HoistModel, LoadSpec, lookup, managed, PlainObject, XH} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {makeObservable} from '@xh/hoist/mobx'; import {Timer} from '@xh/hoist/utils/async'; import {SECONDS} from '@xh/hoist/utils/datetime'; import {ReactNode} from 'react'; export class InstancesTabModel extends HoistModel { override telemetryPrefix = 'xh.client.admin.instances'; override persistWith = {localStorageKey: 'xhAdminClusterTabState'}; @lookup(TabModel) private tabModel: TabModel; @managed readonly gridModel: GridModel = this.createGridModel(); @managed readonly tabContainerModel: TabContainerModel = this.createTabContainerModel(); @managed readonly timer: Timer; get instance(): PlainObject { return this.gridModel.selectedRecord?.data; } get instanceName(): string { return this.instance?.name; } get isMultiInstance(): boolean { return this.gridModel.store.allCount > 1; } get instanceNames(): string[] { return this.gridModel.store.records.map(r => r.data.name); } override async doLoadAsync(loadSpec: LoadSpec) { const {gridModel} = this; await this.runner({loadSpec}) .span('load') .run(async ctx => { let data = await XH.fetchJson( { url: 'clusterAdmin/allInstances', // Tighter default timeout for background auto-refresh, to ensure we report connectivity // issues promptly. This call should be quick, but still allow full default timeout for // a manual refresh. timeout: loadSpec.isAutoRefresh ? this.autoRefreshTimeout : undefined }, ctx ); data = data.map(row => ({ ...row, isLocal: row.name == XH.environmentService.serverInstance, usedHeapMb: row.memory?.usedHeapMb, usedPctMax: row.memory?.usedPctMax })); gridModel.loadData(data); if (!gridModel.hasSelection) { const primary = gridModel.store.records.find(r => r.data.isPrimary); await (primary ? gridModel.selectAsync(primary) : gridModel.selectFirstAsync()); } }); } constructor() { super(); makeObservable(this); this.timer = Timer.create({ runFn: () => { if (this.tabModel?.isActive) this.autoRefreshAsync(); }, interval: this.autoRefreshInterval, delay: true }); this.addReaction( { track: () => this.instanceName, run: instName => { if (instName) this.tabContainerModel.refreshContextModel.refreshAsync(); } }, { track: () => XH.environmentService.serverInstance, run: this.refreshAsync } ); } formatInstance(instance: PlainObject): ReactNode { const content = [instance.name]; if (instance.isPrimary) content.push(badge({item: 'primary', intent: 'primary'})); if (instance.isLocal) content.push(badge('local')); return hbox(content); } //------------------ // Implementation //------------------ private createGridModel() { return new GridModel({ store: { idSpec: 'name', fields: [ {name: 'name', type: 'string'}, {name: 'isLocal', type: 'bool'}, {name: 'isPrimary', type: 'bool'}, {name: 'isReady', type: 'bool'}, {name: 'wsConnections', type: 'int'}, {name: 'startupTime', type: 'date'}, {name: 'address', type: 'string'} ] }, columns: [ { field: 'isReady', headerName: '', align: 'center', width: 40, renderer: v => v ? Icon.circle({prefix: 'fas', className: 'xh-green'}) : Icon.circle({prefix: 'fal', className: 'xh-white'}), tooltip: v => (v ? 'Ready' : 'Not Ready') }, { field: 'name', rendererIsComplex: true, renderer: (v, {record}) => { return this.formatInstance(record.data); } }, { field: 'address' }, { ...usedHeapMb, headerName: 'Heap (mb)' }, { ...usedPctMax, headerName: 'Heap (% Max)' }, { field: { name: 'wsConnections', type: 'int', description: 'Active Websocket Connections' }, headerName: Icon.bolt(), ...numberCol }, { ...timestampNoYear, field: 'startupTime' }, { colId: 'uptime', field: 'startupTime', renderer: v => getRelativeTimestamp(v, {pastSuffix: ''}), rendererIsComplex: true } ], contextMenu: this.createContextMenu() }); } private createContextMenu(): GridContextMenuSpec { return [ { icon: Icon.skull(), text: 'Shutdown Instance', intent: 'danger', actionFn: ({record}) => this.shutdownInstanceAsync(record.data), displayFn: () => ({hidden: AppModel.readonly}), recordsRequired: 1 }, '-', ...GridModel.defaults.contextMenu ]; } private createTabContainerModel() { return new TabContainerModel({ route: 'default.servers.instances', tabs: [ {id: 'logs', icon: Icon.fileText(), content: logViewer}, {id: 'memory', icon: Icon.memory(), content: memoryMonitorPanel}, { id: 'jdbcPool', title: 'JDBC Pool', icon: Icon.database(), content: connPoolMonitorPanel }, {id: 'environment', icon: Icon.globe(), content: serverEnvPanel}, {id: 'services', icon: Icon.gears(), content: servicePanel} ] }); } private async shutdownInstanceAsync(instance: PlainObject) { if ( !(await XH.confirm({ message: `Are you sure you want to immediately terminate instance ${instance.name}?`, extraConfirmText: instance.name, confirmProps: { icon: Icon.skull(), text: 'Yes, kill the instance', intent: 'danger', outlined: true, autoFocus: false } })) ) return; await this.runner() .span('shutdown') .fetchJson({ url: 'clusterAdmin/shutdownInstance', params: {instance: instance.name} }) .finally(() => this.loadAsync()) .linkTo({observer: this.loadObserver, message: 'Attempting instance shutdown'}) .catchDefault(); } private get autoRefreshInterval(): number { return XH.getConf('xhAdminAppConfig', {}).clusterTabAutoRefreshInterval ?? 4 * SECONDS; } private get autoRefreshTimeout(): number { return XH.getConf('xhAdminAppConfig', {}).clusterTabAutoRefreshTimeout ?? 2 * SECONDS; } }