diff --git a/CHANGELOG.md b/CHANGELOG.md index 49661b1a7..bfb5936f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 69.0.0-SNAPSHOT - unreleased +### ⚙️ Technical +* Misc. Improvements to Cluster Tab in Admin Panel. + ## 68.0.0 - 2024-09-18 ### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - Hoist Core update only) diff --git a/admin/tabs/cluster/hzobject/HzObjectModel.ts b/admin/tabs/cluster/hzobject/HzObjectModel.ts index 90aa8a5b6..bcea11d91 100644 --- a/admin/tabs/cluster/hzobject/HzObjectModel.ts +++ b/admin/tabs/cluster/hzobject/HzObjectModel.ts @@ -11,12 +11,15 @@ import {BaseInstanceModel} from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel' import {GridModel} from '@xh/hoist/cmp/grid'; import * as Col from '@xh/hoist/cmp/grid/columns'; import {br, fragment} from '@xh/hoist/cmp/layout'; -import {LoadSpec, managed, XH} from '@xh/hoist/core'; +import {LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core'; import {RecordActionSpec} from '@xh/hoist/data'; import {Icon} from '@xh/hoist/icon'; -import {isEmpty} from 'lodash'; +import {bindable, makeObservable} from '@xh/hoist/mobx'; +import {first, isEmpty, last} from 'lodash'; export class HzObjectModel extends BaseInstanceModel { + @bindable groupBy: 'type' | 'owner' = 'owner'; + clearAction: RecordActionSpec = { text: 'Clear Objects', icon: Icon.reset(), @@ -35,21 +38,25 @@ export class HzObjectModel extends BaseInstanceModel { selModel: 'multiple', enableExport: true, exportOptions: {filename: exportFilenameWithDate('distributed-objects'), columns: 'ALL'}, - sortBy: 'name', - groupBy: 'type', + sortBy: 'displayName', + groupBy: this.groupBy, store: { fields: [ {name: 'name', type: 'string'}, + {name: 'displayName', type: 'string'}, + {name: 'owner', type: 'string'}, {name: 'type', type: 'string', displayName: 'Type'}, {name: 'size', type: 'int'}, {name: 'lastUpdateTime', type: 'date'}, {name: 'lastAccessTime', type: 'date'} ], - idSpec: 'name' + idSpec: 'name', + processRawData: o => this.processRawData(o) }, columns: [ - {field: 'type', hidden: true}, - {field: 'name', flex: 1}, + {field: 'displayName', flex: 1}, + {field: 'owner'}, + {field: 'type'}, {field: 'size', displayName: 'Entry Count', ...Col.number, width: 130}, { ...timestampNoYear, @@ -65,6 +72,15 @@ export class HzObjectModel extends BaseInstanceModel { contextMenu: [this.clearAction, '-', ...GridModel.defaultContextMenu] }); + constructor() { + super(); + makeObservable(this); + this.addReaction({ + track: () => this.groupBy, + run: v => this.gridModel.setGroupBy(v) + }); + } + async clearAsync() { const {gridModel} = this; if ( @@ -151,4 +167,26 @@ export class HzObjectModel extends BaseInstanceModel { this.handleLoadException(e, loadSpec); } } + + //---------------------- + // Implementation + //---------------------- + private processRawData(obj: PlainObject): PlainObject { + const tail: string = last(obj.name.split('.')), + className = first(tail.split('[')); + + const owner = className.endsWith('Service') + ? className + : className.startsWith('xh') + ? 'Hoist' + : obj.type == 'Cache' + ? 'Hibernate' + : 'Other'; + + return { + displayName: tail, + owner: owner, + ...obj + }; + } } diff --git a/admin/tabs/cluster/hzobject/HzObjectPanel.ts b/admin/tabs/cluster/hzobject/HzObjectPanel.ts index ce12079c5..7266565a2 100644 --- a/admin/tabs/cluster/hzobject/HzObjectPanel.ts +++ b/admin/tabs/cluster/hzobject/HzObjectPanel.ts @@ -9,8 +9,9 @@ import {filler, hframe, placeholder} from '@xh/hoist/cmp/layout'; import {storeFilterField} from '@xh/hoist/cmp/store'; import {creates, hoistCmp, uses} from '@xh/hoist/core'; import {button, exportButton} from '@xh/hoist/desktop/cmp/button'; -import {jsonInput} from '@xh/hoist/desktop/cmp/input'; +import {jsonInput, select} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {toolbar} from '@xh/hoist/desktop/cmp/toolbar'; import {recordActionBar} from '@xh/hoist/desktop/cmp/record'; import {Icon} from '@xh/hoist/icon'; import {HzObjectModel} from './HzObjectModel'; @@ -20,26 +21,13 @@ export const hzObjectPanel = hoistCmp.factory({ render({model}) { return panel({ - bbar: [ - recordActionBar({ - selModel: model.gridModel.selModel, - actions: [model.clearAction] + item: hframe( + panel({ + item: grid({agOptions: {groupDefaultExpanded: 0}}), + bbar: bbar() }), - '-', - button({ - text: 'Clear Hibernate Caches', - icon: Icon.reset(), - intent: 'warning', - tooltip: 'Clear the Hibernate caches using the native Hibernate API', - onClick: () => model.clearHibernateCachesAsync() - }), - filler(), - gridCountLabel({unit: 'objects'}), - '-', - storeFilterField({matchMode: 'any'}), - exportButton() - ], - item: hframe(grid(), detailsPanel()), + detailsPanel() + ), mask: 'onLoad', ref: model.viewRef }); @@ -49,16 +37,16 @@ export const hzObjectPanel = hoistCmp.factory({ const detailsPanel = hoistCmp.factory({ model: uses(HzObjectModel), render({model}) { - const data = model.gridModel.selectedRecord?.raw; + const record = model.gridModel.selectedRecord; return panel({ - title: data ? `Stats: ${data.name}` : 'Stats', + title: record ? `Stats: ${record.data.displayName}` : 'Stats', icon: Icon.info(), compactHeader: true, modelConfig: { side: 'right', defaultSize: 450 }, - item: data + item: record ? panel({ item: jsonInput({ readonly: true, @@ -66,10 +54,45 @@ const detailsPanel = hoistCmp.factory({ height: '100%', showFullscreenButton: false, editorProps: {lineNumbers: false}, - value: model.fmtStats(data) + value: model.fmtStats(record.raw) }) }) : placeholder(Icon.grip(), 'Select an object') }); } }); + +const bbar = hoistCmp.factory({ + model: uses(HzObjectModel), + render({model}) { + return toolbar( + recordActionBar({ + selModel: model.gridModel.selModel, + actions: [model.clearAction] + }), + '-', + button({ + text: 'Clear Hibernate Caches', + icon: Icon.reset(), + intent: 'warning', + tooltip: 'Clear the Hibernate caches using the native Hibernate API', + onClick: () => model.clearHibernateCachesAsync() + }), + filler(), + gridCountLabel({unit: 'objects'}), + '-', + select({ + options: [ + {label: 'By Owner', value: 'owner'}, + {label: 'By Type', value: 'type'}, + {label: 'Ungrouped', value: null} + ], + width: 125, + bind: 'groupBy', + hideDropdownIndicator: true + }), + storeFilterField({matchMode: 'any'}), + exportButton() + ); + } +}); diff --git a/admin/tabs/cluster/services/ServiceModel.ts b/admin/tabs/cluster/services/ServiceModel.ts index fa63a16c5..f114e7437 100644 --- a/admin/tabs/cluster/services/ServiceModel.ts +++ b/admin/tabs/cluster/services/ServiceModel.ts @@ -11,12 +11,19 @@ import {BaseInstanceModel} from '@xh/hoist/admin/tabs/cluster/BaseInstanceModel' import {GridModel} from '@xh/hoist/cmp/grid'; import {br, fragment} from '@xh/hoist/cmp/layout'; import {LoadSpec, managed, XH} from '@xh/hoist/core'; -import {RecordActionSpec} from '@xh/hoist/data'; +import {FilterLike, FilterTestFn, RecordActionSpec} from '@xh/hoist/data'; import {Icon} from '@xh/hoist/icon'; +import {bindable, makeObservable} from '@xh/hoist/mobx'; import {pluralize} from '@xh/hoist/utils/js'; -import {isEmpty, lowerFirst} from 'lodash'; +import {capitalize, isEmpty, lowerFirst} from 'lodash'; export class ServiceModel extends BaseInstanceModel { + @bindable + typeFilter: 'hoist' | 'app' | 'all' = 'all'; + + @bindable.ref + textFilter: FilterTestFn = null; + clearCachesAction: RecordActionSpec = { text: 'Clear Caches', icon: Icon.reset(), @@ -57,10 +64,9 @@ export class ServiceModel extends BaseInstanceModel { ] }, sortBy: 'displayName', - groupBy: 'provider', columns: [ - {field: 'provider', hidden: true}, {field: 'displayName', flex: 1}, + {field: 'provider'}, {...timestampNoYear, field: 'lastCachesCleared'}, {...timestampNoYear, field: 'initializedDate'} ], @@ -72,6 +78,16 @@ export class ServiceModel extends BaseInstanceModel { ] }); + constructor() { + super(); + makeObservable(this); + this.addReaction({ + track: () => [this.textFilter, this.typeFilter], + run: this.applyFilters, + fireImmediately: true + }); + } + async clearCachesAsync(entireCluster: boolean) { const {gridModel, instanceName, loadModel} = this, {selectedRecords} = gridModel; @@ -132,4 +148,14 @@ export class ServiceModel extends BaseInstanceModel { const displayName = lowerFirst(r.name.replace('hoistCore', '')); return {provider, displayName, ...r}; } + + private applyFilters() { + const {typeFilter, textFilter} = this; + const filters: FilterLike[] = [textFilter]; + + if (typeFilter == 'hoist' || typeFilter == 'app') { + filters.push({field: 'provider', op: '=', value: capitalize(typeFilter)}); + } + this.gridModel.store.setFilter(filters); + } } diff --git a/admin/tabs/cluster/services/ServicePanel.ts b/admin/tabs/cluster/services/ServicePanel.ts index e7731d577..c4e6728f2 100644 --- a/admin/tabs/cluster/services/ServicePanel.ts +++ b/admin/tabs/cluster/services/ServicePanel.ts @@ -8,10 +8,12 @@ import {detailsPanel} from '@xh/hoist/admin/tabs/cluster/services/DetailsPanel'; import {grid, gridCountLabel} from '@xh/hoist/cmp/grid'; import {filler, hframe} from '@xh/hoist/cmp/layout'; import {storeFilterField} from '@xh/hoist/cmp/store'; -import {creates, hoistCmp} from '@xh/hoist/core'; +import {creates, hoistCmp, uses} from '@xh/hoist/core'; import {exportButton} from '@xh/hoist/desktop/cmp/button'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {recordActionBar} from '@xh/hoist/desktop/cmp/record'; +import {toolbar} from '@xh/hoist/desktop/cmp/toolbar'; +import {select} from '@xh/hoist/desktop/cmp/input'; import {ServiceModel} from './ServiceModel'; export const servicePanel = hoistCmp.factory({ @@ -19,25 +21,17 @@ export const servicePanel = hoistCmp.factory({ render({model}) { return panel({ - bbar: [ - recordActionBar({ - selModel: model.gridModel.selModel, - actions: [model.clearCachesAction, model.clearClusterCachesAction] - }), - filler(), - gridCountLabel({unit: 'service'}), - '-', - storeFilterField({matchMode: 'any'}), - exportButton() - ], item: hframe( - grid({ - flex: 1, - agOptions: { - groupRowRendererParams: { - innerRenderer: params => params.value + ' Services' + panel({ + item: grid({ + flex: 1, + agOptions: { + groupRowRendererParams: { + innerRenderer: params => params.value + ' Services' + } } - } + }), + bbar: bbar() }), detailsPanel() ), @@ -46,3 +40,34 @@ export const servicePanel = hoistCmp.factory({ }); } }); + +const bbar = hoistCmp.factory({ + model: uses(ServiceModel), + render({model}) { + return toolbar( + recordActionBar({ + selModel: model.gridModel.selModel, + actions: [model.clearCachesAction, model.clearClusterCachesAction] + }), + filler(), + gridCountLabel({unit: 'service'}), + '-', + select({ + options: [ + {value: 'all', label: 'All'}, + {value: 'app', label: 'App Only'}, + {value: 'hoist', label: 'Hoist Only'} + ], + width: 125, + bind: 'typeFilter', + hideDropdownIndicator: true + }), + storeFilterField({ + matchMode: 'any', + autoApply: false, + onFilterChange: f => (model.textFilter = f) + }), + exportButton() + ); + } +});