From 167b03d779ad92f5d084b3ed8e9bc063648b625d Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Wed, 22 Jan 2025 19:54:17 -0500 Subject: [PATCH 01/19] First working impl of Json Path Search component --- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 56 ++++++++++-- desktop/cmp/jsonsearch/JsonSearchPanel.ts | 89 +++++++++++++++++++ .../impl/JsonSearchPanelImplModel.ts | 85 ++++++++++++++++++ 3 files changed, 221 insertions(+), 9 deletions(-) create mode 100644 desktop/cmp/jsonsearch/JsonSearchPanel.ts create mode 100644 desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index 3f3fc4ae2..a5e405e9e 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -5,9 +5,13 @@ * Copyright © 2025 Extremely Heavy Industries Inc. */ -import {fragment} from '@xh/hoist/cmp/layout'; +import * as Col from '@xh/hoist/admin/columns/Rest'; +import * as AdminCol from '@xh/hoist/admin/columns'; +import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; +import {jsonSearchPanel} from '@xh/hoist/desktop/cmp/jsonsearch/JsonSearchPanel'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; import {differ} from '../../../differ/Differ'; @@ -17,14 +21,48 @@ export const jsonBlobPanel = hoistCmp.factory({ model: creates(JsonBlobModel), render({model}) { - return fragment( - restGrid({ - extraToolbarItems: () => { - return button({ - icon: Icon.diff(), - text: 'Compare w/ Remote', - onClick: () => model.openDiffer() - }); + return hframe( + panel({ + item: restGrid({ + extraToolbarItems: () => { + return button({ + icon: Icon.diff(), + text: 'Compare w/ Remote', + onClick: () => model.openDiffer() + }); + } + }) + }), + jsonSearchPanel({ + docSearchUrl: 'jsonBlobSearchAdmin/searchByJsonPath', + matchingNodesUrl: 'jsonBlobSearchAdmin/getMatchingNodes', + gridModelConfig: { + sortBy: ['owner', 'name'], + store: { + idSpec: 'token' + }, + groupBy: 'type', + columns: [ + { + field: {name: 'token', type: 'string'}, + hidden: true, + width: 100 + }, + { + field: {name: 'type', type: 'string'}, + width: 200 + }, + { + field: {name: 'owner', type: 'string'}, + width: 200 + }, + {...Col.lastUpdated}, + { + field: {name: 'json', type: 'string'}, + hidden: true + }, + {...AdminCol.name} + ] } }), differ({omit: !model.differModel}) diff --git a/desktop/cmp/jsonsearch/JsonSearchPanel.ts b/desktop/cmp/jsonsearch/JsonSearchPanel.ts new file mode 100644 index 000000000..357e78b4f --- /dev/null +++ b/desktop/cmp/jsonsearch/JsonSearchPanel.ts @@ -0,0 +1,89 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2025 Extremely Heavy Industries Inc. + */ + +import {JsonBlobModel} from '@xh/hoist/admin/tabs/userData/jsonblob/JsonBlobModel'; +import {grid} from '@xh/hoist/cmp/grid'; +import {a, box, hframe} from '@xh/hoist/cmp/layout'; +import {hoistCmp, useLocalModel} from '@xh/hoist/core'; +import {button} from '@xh/hoist/desktop/cmp/button'; +import {jsonInput, textInput} from '@xh/hoist/desktop/cmp/input'; +import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {Icon} from '@xh/hoist/icon'; +import {popover} from '@xh/hoist/kit/blueprint'; + +import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel'; + +export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ + render() { + const impl = useLocalModel(JsonSearchPanelImplModel); + return panel({ + title: 'JSON Path Search', + icon: Icon.json(), + modelConfig: { + side: 'right', + defaultSize: '75%', + collapsible: true, + defaultCollapsed: true + }, + compactHeader: true, + tbar: [pathField({model: impl}), helpButton()], + flex: 1, + item: panel({ + item: hframe({ + items: [ + panel({ + item: grid({model: impl.gridModel}) + }), + panel({ + item: jsonInput({ + model: impl, + bind: 'matchingNodes', + flex: 1, + width: '100%', + readonly: true + }) + }) + ] + }) + }) + }); + } +}); + +const pathField = hoistCmp.factory({ + render({model}) { + return textInput({ + bind: 'path', + commitOnChange: true, + leftIcon: Icon.search(), + enableClear: true, + placeholder: 'JSON Path', + width: null, + flex: 1 + }); + } +}); + +const helpButton = hoistCmp.factory({ + model: false, + render() { + return popover({ + item: button({ + icon: Icon.questionCircle(), + outlined: true + }), + content: box({ + style: {padding: 5}, + item: a({ + href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators', + target: '_blank', + item: 'Path Syntax Docs' + }) + }) + }); + } +}); diff --git a/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts b/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts new file mode 100644 index 000000000..fade6522e --- /dev/null +++ b/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -0,0 +1,85 @@ +/* + * This file belongs to Hoist, an application development toolkit + * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) + * + * Copyright © 2025 Extremely Heavy Industries Inc. + */ +import {GridModel} from '@xh/hoist/cmp/grid'; +import {HoistModel, XH} from '@xh/hoist/core'; +import {bindable, makeObservable} from '@xh/hoist/mobx'; + +/** + * @internal + */ +export class JsonSearchPanelImplModel extends HoistModel { + override xhImpl = true; + + gridModel: GridModel; + + @bindable.ref path; + @bindable matchingNodes: string; + + get queryBuffer(): number { + return this.componentProps.queryBuffer ?? 200; + } + + get docSearchUrl(): string { + return this.componentProps.docSearchUrl; + } + + get matchingNodesUrl(): string { + return this.componentProps.matchingNodesUrl; + } + + get gridModelConfig() { + return this.componentProps.gridModelConfig; + } + + constructor() { + super(); + makeObservable(this); + } + + override onLinked() { + this.gridModel = new GridModel({ + ...this.gridModelConfig, + selModel: 'single' + }); + + this.addReaction( + { + track: () => this.path, + run: () => this.loadJsonDocsAsync(), + debounce: this.queryBuffer + }, + { + track: () => [this.gridModel.selectedRecord, this.path], + run: () => this.loadJsonNodesAsync(), + debounce: 300 + } + ); + } + + private async loadJsonDocsAsync() { + let data = await XH.fetchJson({ + url: this.docSearchUrl, + params: {path: this.path} + }); + + this.gridModel.loadData(data); + } + + private async loadJsonNodesAsync() { + if (!this.gridModel.selectedRecord) { + this.matchingNodes = ''; + return; + } + + const nodes = await XH.fetchJson({ + url: this.matchingNodesUrl, + params: {path: this.path, json: this.gridModel.selectedRecord.data.json} + }); + + this.matchingNodes = JSON.stringify(nodes, null, 2); + } +} From 2826b330989d1b36958d70ab6cd8500c11ed3481 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Thu, 23 Jan 2025 11:28:38 -0500 Subject: [PATCH 02/19] support path/value toggle support XPath/JsonPath toggle --- desktop/cmp/jsonsearch/JsonSearchPanel.ts | 79 +++++++++++++++++-- .../impl/JsonSearchPanelImplModel.ts | 46 +++++++++-- 2 files changed, 113 insertions(+), 12 deletions(-) diff --git a/desktop/cmp/jsonsearch/JsonSearchPanel.ts b/desktop/cmp/jsonsearch/JsonSearchPanel.ts index 357e78b4f..618a2355d 100644 --- a/desktop/cmp/jsonsearch/JsonSearchPanel.ts +++ b/desktop/cmp/jsonsearch/JsonSearchPanel.ts @@ -5,12 +5,13 @@ * Copyright © 2025 Extremely Heavy Industries Inc. */ +import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {JsonBlobModel} from '@xh/hoist/admin/tabs/userData/jsonblob/JsonBlobModel'; -import {grid} from '@xh/hoist/cmp/grid'; -import {a, box, hframe} from '@xh/hoist/cmp/layout'; +import {grid, gridCountLabel} from '@xh/hoist/cmp/grid'; +import {a, box, filler, hframe, label} from '@xh/hoist/cmp/layout'; import {hoistCmp, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonInput, textInput} from '@xh/hoist/desktop/cmp/input'; +import {buttonGroupInput, jsonInput, textInput} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {Icon} from '@xh/hoist/icon'; import {popover} from '@xh/hoist/kit/blueprint'; @@ -30,7 +31,7 @@ export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory(({model}) => { + return toolbar( + pathField({model}), + helpButton(), + toolbarSep(), + gridCountLabel({ + gridModel: model.gridModel, + unit: 'document' + }) + ); +}); + const pathField = hoistCmp.factory({ - render({model}) { + render() { return textInput({ bind: 'path', commitOnChange: true, @@ -87,3 +106,51 @@ const helpButton = hoistCmp.factory({ }); } }); + +const nodeTbar = hoistCmp.factory(({model}) => { + return toolbar( + buttonGroupInput({ + model, + bind: 'pathOrValue', + minimal: true, + outlined: true, + items: [ + button({ + text: 'Path', + value: 'path' + }), + button({ + text: 'Value', + value: 'value' + }) + ] + }), + filler(), + box({ + omit: !model.matchingNodeCount, + item: `${model.matchingNodeCount} ${model.matchingNodeCount === 1 ? 'match' : 'matches'}` + }) + ); +}); + +const nodeBbar = hoistCmp.factory(({model}) => { + return toolbar( + label('Path Format:'), + buttonGroupInput({ + model, + bind: 'pathFormat', + minimal: true, + outlined: true, + items: [ + button({ + text: 'XPath', + value: 'XPath' + }), + button({ + text: 'JSONPath', + value: 'JSONPath' + }) + ] + }) + ); +}); diff --git a/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts b/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts index fade6522e..333dba275 100644 --- a/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -4,6 +4,7 @@ * * Copyright © 2025 Extremely Heavy Industries Inc. */ + import {GridModel} from '@xh/hoist/cmp/grid'; import {HoistModel, XH} from '@xh/hoist/core'; import {bindable, makeObservable} from '@xh/hoist/mobx'; @@ -16,8 +17,15 @@ export class JsonSearchPanelImplModel extends HoistModel { gridModel: GridModel; - @bindable.ref path; - @bindable matchingNodes: string; + @bindable path: string = ''; + @bindable pathOrValue: 'path' | 'value' = 'value'; + @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath'; + @bindable matchingNodes: string = ''; + @bindable matchingNodeCount: number = 0; + + get asPathList(): boolean { + return this.pathOrValue === 'path'; + } get queryBuffer(): number { return this.componentProps.queryBuffer ?? 200; @@ -53,7 +61,12 @@ export class JsonSearchPanelImplModel extends HoistModel { debounce: this.queryBuffer }, { - track: () => [this.gridModel.selectedRecord, this.path], + track: () => [ + this.gridModel.selectedRecord, + this.path, + this.pathOrValue, + this.pathFormat + ], run: () => this.loadJsonNodesAsync(), debounce: 300 } @@ -61,7 +74,12 @@ export class JsonSearchPanelImplModel extends HoistModel { } private async loadJsonDocsAsync() { - let data = await XH.fetchJson({ + if (this.path.endsWith('.') || this.path === '$') { + this.gridModel.clear(); + return; + } + + const data = await XH.fetchJson({ url: this.docSearchUrl, params: {path: this.path} }); @@ -71,15 +89,31 @@ export class JsonSearchPanelImplModel extends HoistModel { private async loadJsonNodesAsync() { if (!this.gridModel.selectedRecord) { + this.matchingNodeCount = 0; this.matchingNodes = ''; return; } - const nodes = await XH.fetchJson({ + let nodes = await XH.fetchJson({ url: this.matchingNodesUrl, - params: {path: this.path, json: this.gridModel.selectedRecord.data.json} + params: { + path: this.path, + asPathList: this.pathOrValue === 'path', + json: this.gridModel.selectedRecord.data.json + } }); + this.matchingNodeCount = nodes.length; + if (this.asPathList && this.pathFormat === 'XPath') { + nodes = nodes.map(it => this.convertToPath(it)); + } this.matchingNodes = JSON.stringify(nodes, null, 2); } + + private convertToPath(JSONPath: string): string { + return JSONPath.replaceAll(/^\$\['?/g, '/') + .replaceAll(/^\$/g, '') + .replaceAll(/'?]\['?/g, '/') + .replaceAll(/'?]$/g, ''); + } } From 95eaf5d4de1b65abc6fa60a11acb81a61f137d8f Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Thu, 23 Jan 2025 14:16:41 -0500 Subject: [PATCH 03/19] Add JsonSearchPanel to Preference Panel --- .../userData/prefs/UserPreferencePanel.ts | 61 ++++++++++++++----- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index b5b9a7213..b378defe1 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -4,10 +4,14 @@ * * Copyright © 2025 Extremely Heavy Industries Inc. */ +import * as Col from '@xh/hoist/admin/columns/Rest'; +import * as AdminCol from '@xh/hoist/admin/columns'; import {prefEditorDialog} from '@xh/hoist/admin/tabs/userData/prefs/editor/PrefEditorDialog'; import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPreferenceModel'; +import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; +import {jsonSearchPanel} from '@xh/hoist/desktop/cmp/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; @@ -16,20 +20,47 @@ export const userPreferencePanel = hoistCmp.factory({ model: creates(UserPreferenceModel), render({model}) { - return panel({ - items: [ - restGrid({ - extraToolbarItems: () => { - return button({ - icon: Icon.gear(), - text: 'Configure', - onClick: () => (model.showEditorDialog = true) - }); - } - }), - prefEditorDialog() - ], - mask: 'onLoad' - }); + return hframe( + panel({ + items: [ + restGrid({ + extraToolbarItems: () => { + return button({ + icon: Icon.gear(), + text: 'Configure', + onClick: () => (model.showEditorDialog = true) + }); + } + }), + prefEditorDialog() + ], + mask: 'onLoad' + }), + jsonSearchPanel({ + docSearchUrl: 'preferenceJsonSearchAdmin/searchByJsonPath', + matchingNodesUrl: 'preferenceJsonSearchAdmin/getMatchingNodes', + gridModelConfig: { + sortBy: ['name'], + groupBy: 'groupName', + columns: [ + { + field: {name: 'type', type: 'string'}, + width: 200 + }, + { + field: {name: 'owner', type: 'string'}, + width: 200 + }, + {...Col.lastUpdated}, + { + field: {name: 'json', type: 'string'}, + hidden: true + }, + {...AdminCol.name}, + {...AdminCol.groupName} + ] + } + }) + ); } }); From b034faf52cf328fadf812a9aa7ba15a3c96ceb20 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Thu, 23 Jan 2025 17:17:33 -0500 Subject: [PATCH 04/19] Refine input, show errors, add examples --- desktop/cmp/jsonsearch/JsonSearchPanel.ts | 117 +++++++++++++----- .../impl/JsonSearchPanelImplModel.ts | 47 ++++--- 2 files changed, 114 insertions(+), 50 deletions(-) diff --git a/desktop/cmp/jsonsearch/JsonSearchPanel.ts b/desktop/cmp/jsonsearch/JsonSearchPanel.ts index 618a2355d..f7da5d817 100644 --- a/desktop/cmp/jsonsearch/JsonSearchPanel.ts +++ b/desktop/cmp/jsonsearch/JsonSearchPanel.ts @@ -6,21 +6,26 @@ */ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; +import {errorMessage} from '@xh/hoist/cmp/error'; import {JsonBlobModel} from '@xh/hoist/admin/tabs/userData/jsonblob/JsonBlobModel'; import {grid, gridCountLabel} from '@xh/hoist/cmp/grid'; -import {a, box, filler, hframe, label} from '@xh/hoist/cmp/layout'; +import {a, box, filler, h4, hframe, label, li, span, ul, vbox} from '@xh/hoist/cmp/layout'; import {hoistCmp, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {buttonGroupInput, jsonInput, textInput} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {Icon} from '@xh/hoist/icon'; import {popover} from '@xh/hoist/kit/blueprint'; +import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; +import {startCase} from 'lodash'; import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel'; export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ render() { - const impl = useLocalModel(JsonSearchPanelImplModel); + const impl = useLocalModel(JsonSearchPanelImplModel), + {error} = impl; + return panel({ title: 'JSON Path Search', icon: Icon.json(), @@ -34,28 +39,37 @@ export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory(({model}) => { }); const pathField = hoistCmp.factory({ - render() { + render({model}) { return textInput({ bind: 'path', + autoFocus: true, commitOnChange: true, leftIcon: Icon.search(), enableClear: true, - placeholder: 'JSON Path', + placeholder: + "JSON Path - e.g. $..[?(@.colId == 'trader')] - type a path and hit ENTER to search", width: null, - flex: 1 + flex: 1, + onKeyDown: e => { + if (e.key === 'Enter') model.loadJsonDocsAsync(); + } }); } }); @@ -95,13 +114,47 @@ const helpButton = hoistCmp.factory({ icon: Icon.questionCircle(), outlined: true }), - content: box({ - style: {padding: 5}, - item: a({ - href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators', - target: '_blank', - item: 'Path Syntax Docs' - }) + content: vbox({ + style: { + padding: '0px 20px 10px 20px' + }, + items: [ + h4('Sample Queries'), + ul({ + style: {listStyleType: 'none'}, + items: [ + { + query: "$..[?(@.colId == 'trader')]", + explanation: + 'Find all nodes with a property "colId" equal to "trader"' + } + ].map(({query, explanation}) => + li({ + key: query, + items: [ + span({ + className: 'xh-bg-alt xh-gray-dark xh-font-family-mono', + item: query + }), + ' ', + clipboardButton({ + text: null, + icon: Icon.copy(), + getCopyText: () => query, + successMessage: 'Query copied to clipboard.' + }), + ' ', + explanation + ] + }) + ) + }), + a({ + href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators', + target: '_blank', + item: 'Path Syntax Docs & More Examples' + }) + ] }) }); } diff --git a/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts b/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts index 333dba275..161e9c292 100644 --- a/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -6,8 +6,9 @@ */ import {GridModel} from '@xh/hoist/cmp/grid'; -import {HoistModel, XH} from '@xh/hoist/core'; +import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; import {bindable, makeObservable} from '@xh/hoist/mobx'; +import {isEmpty} from 'lodash'; /** * @internal @@ -15,8 +16,11 @@ import {bindable, makeObservable} from '@xh/hoist/mobx'; export class JsonSearchPanelImplModel extends HoistModel { override xhImpl = true; - gridModel: GridModel; + @managed gridModel: GridModel; + @managed docLoadTask: TaskObserver = TaskObserver.trackLast(); + @managed nodeLoadTask: TaskObserver = TaskObserver.trackLast(); + @bindable.ref error = null; @bindable path: string = ''; @bindable pathOrValue: 'path' | 'value' = 'value'; @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath'; @@ -57,34 +61,41 @@ export class JsonSearchPanelImplModel extends HoistModel { this.addReaction( { track: () => this.path, - run: () => this.loadJsonDocsAsync(), - debounce: this.queryBuffer + run: path => { + if (isEmpty(path)) { + this.error = null; + this.gridModel.clear(); + } + } }, { - track: () => [ - this.gridModel.selectedRecord, - this.path, - this.pathOrValue, - this.pathFormat - ], + track: () => [this.gridModel.selectedRecord, this.pathOrValue, this.pathFormat], run: () => this.loadJsonNodesAsync(), debounce: 300 } ); } - private async loadJsonDocsAsync() { - if (this.path.endsWith('.') || this.path === '$') { + async loadJsonDocsAsync() { + if (isEmpty(this.path)) { + this.error = null; this.gridModel.clear(); return; } - const data = await XH.fetchJson({ - url: this.docSearchUrl, - params: {path: this.path} - }); + try { + const data = await XH.fetchJson({ + url: this.docSearchUrl, + params: {path: this.path} + }).linkTo(this.docLoadTask); - this.gridModel.loadData(data); + this.error = null; + this.gridModel.loadData(data); + this.gridModel.selectFirstAsync(); + } catch (e) { + this.gridModel.clear(); + this.error = e; + } } private async loadJsonNodesAsync() { @@ -101,7 +112,7 @@ export class JsonSearchPanelImplModel extends HoistModel { asPathList: this.pathOrValue === 'path', json: this.gridModel.selectedRecord.data.json } - }); + }).linkTo(this.nodeLoadTask); this.matchingNodeCount = nodes.length; if (this.asPathList && this.pathFormat === 'XPath') { From 8967663e500775713a319dfb847cd8d3d30d4ab0 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Thu, 23 Jan 2025 17:19:39 -0500 Subject: [PATCH 05/19] Move JsonSearch cmp into admin --- {desktop/cmp => admin}/jsonsearch/JsonSearchPanel.ts | 0 .../cmp => admin}/jsonsearch/impl/JsonSearchPanelImplModel.ts | 0 admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 2 +- admin/tabs/userData/prefs/UserPreferencePanel.ts | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename {desktop/cmp => admin}/jsonsearch/JsonSearchPanel.ts (100%) rename {desktop/cmp => admin}/jsonsearch/impl/JsonSearchPanelImplModel.ts (100%) diff --git a/desktop/cmp/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts similarity index 100% rename from desktop/cmp/jsonsearch/JsonSearchPanel.ts rename to admin/jsonsearch/JsonSearchPanel.ts diff --git a/desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts similarity index 100% rename from desktop/cmp/jsonsearch/impl/JsonSearchPanelImplModel.ts rename to admin/jsonsearch/impl/JsonSearchPanelImplModel.ts diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index a5e405e9e..e957b1ec1 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -10,7 +10,7 @@ import * as AdminCol from '@xh/hoist/admin/columns'; import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchPanel} from '@xh/hoist/desktop/cmp/jsonsearch/JsonSearchPanel'; +import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index b378defe1..dd5f7ae2d 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -11,7 +11,7 @@ import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPrefe import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchPanel} from '@xh/hoist/desktop/cmp/jsonsearch/JsonSearchPanel'; +import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; From 79b3dbddde175ca12771c1220c457e2051f2136a Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Thu, 23 Jan 2025 17:24:26 -0500 Subject: [PATCH 06/19] Fix column orders --- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 4 ++-- admin/tabs/userData/prefs/UserPreferencePanel.ts | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index e957b1ec1..3364a8789 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -56,12 +56,12 @@ export const jsonBlobPanel = hoistCmp.factory({ field: {name: 'owner', type: 'string'}, width: 200 }, - {...Col.lastUpdated}, + {...AdminCol.name}, { field: {name: 'json', type: 'string'}, hidden: true }, - {...AdminCol.name} + {...Col.lastUpdated} ] } }), diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index dd5f7ae2d..bd1078460 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -43,21 +43,17 @@ export const userPreferencePanel = hoistCmp.factory({ sortBy: ['name'], groupBy: 'groupName', columns: [ - { - field: {name: 'type', type: 'string'}, - width: 200 - }, { field: {name: 'owner', type: 'string'}, width: 200 }, - {...Col.lastUpdated}, + {...AdminCol.groupName}, + {...AdminCol.name}, { field: {name: 'json', type: 'string'}, hidden: true }, - {...AdminCol.name}, - {...AdminCol.groupName} + {...Col.lastUpdated} ] } }) From 8da1200f676162138bcfbc38325a6d6f1878ae43 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Thu, 23 Jan 2025 18:45:59 -0500 Subject: [PATCH 07/19] fix coloring on help code --- admin/jsonsearch/JsonSearchPanel.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index f7da5d817..a08b3ab13 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -133,7 +133,8 @@ const helpButton = hoistCmp.factory({ key: query, items: [ span({ - className: 'xh-bg-alt xh-gray-dark xh-font-family-mono', + className: + 'xh-border xh-pad-half xh-bg-alt xh-font-family-mono', item: query }), ' ', From be43fc2127ab66d21a24e157c6f143a66956eb9f Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Fri, 24 Jan 2025 15:05:15 -0500 Subject: [PATCH 08/19] Show whole document in json reader. --- admin/jsonsearch/JsonSearchPanel.ts | 59 +++++++++++-------- .../impl/JsonSearchPanelImplModel.ts | 33 +++++++---- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index a08b3ab13..99f20f3d9 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -53,14 +53,14 @@ export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory(({model}) => { - return toolbar( - buttonGroupInput({ - model, - bind: 'pathOrValue', - minimal: true, - outlined: true, - items: [ - button({ - text: 'Path', - value: 'path' - }), - button({ - text: 'Value', - value: 'value' - }) - ] - }), - filler(), - box({ - omit: !model.matchingNodeCount, - item: `${model.matchingNodeCount} ${model.matchingNodeCount === 1 ? 'match' : 'matches'}` - }) - ); +const readerTbar = hoistCmp.factory(({model}) => { + return toolbar({ + items: [ + buttonGroupInput({ + model, + bind: 'readerContentType', + minimal: true, + outlined: true, + disabled: !model.selectedRecord, + items: [ + button({ + text: 'Document', + value: 'document' + }), + button({ + text: 'Matching Paths', + value: 'paths' + }), + button({ + text: 'Matching Values', + value: 'values' + }) + ] + }), + filler(), + box({ + omit: !model.matchingNodeCount, + item: `${model.matchingNodeCount} ${model.matchingNodeCount === 1 ? 'match' : 'matches'}` + }) + ] + }); }); const nodeBbar = hoistCmp.factory(({model}) => { diff --git a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts index 161e9c292..ff541ed9b 100644 --- a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -22,13 +22,13 @@ export class JsonSearchPanelImplModel extends HoistModel { @bindable.ref error = null; @bindable path: string = ''; - @bindable pathOrValue: 'path' | 'value' = 'value'; + @bindable readerContentType: 'document' | 'paths' | 'values' = 'values'; @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath'; - @bindable matchingNodes: string = ''; + @bindable readerContent: string = ''; @bindable matchingNodeCount: number = 0; get asPathList(): boolean { - return this.pathOrValue === 'path'; + return this.readerContentType === 'paths'; } get queryBuffer(): number { @@ -43,6 +43,10 @@ export class JsonSearchPanelImplModel extends HoistModel { return this.componentProps.matchingNodesUrl; } + get selectedRecord() { + return this.gridModel.selectedRecord; + } + get gridModelConfig() { return this.componentProps.gridModelConfig; } @@ -69,8 +73,8 @@ export class JsonSearchPanelImplModel extends HoistModel { } }, { - track: () => [this.gridModel.selectedRecord, this.pathOrValue, this.pathFormat], - run: () => this.loadJsonNodesAsync(), + track: () => [this.selectedRecord, this.readerContentType, this.pathFormat], + run: () => this.loadreaderContentTypeAsync(), debounce: 300 } ); @@ -98,10 +102,17 @@ export class JsonSearchPanelImplModel extends HoistModel { } } - private async loadJsonNodesAsync() { - if (!this.gridModel.selectedRecord) { + private async loadreaderContentTypeAsync() { + if (!this.selectedRecord) { this.matchingNodeCount = 0; - this.matchingNodes = ''; + this.readerContent = ''; + return; + } + + const {json} = this.selectedRecord.data; + + if (this.readerContentType === 'document') { + this.readerContent = JSON.stringify(JSON.parse(json), null, 2); return; } @@ -109,8 +120,8 @@ export class JsonSearchPanelImplModel extends HoistModel { url: this.matchingNodesUrl, params: { path: this.path, - asPathList: this.pathOrValue === 'path', - json: this.gridModel.selectedRecord.data.json + asPathList: this.readerContentType === 'paths', + json } }).linkTo(this.nodeLoadTask); @@ -118,7 +129,7 @@ export class JsonSearchPanelImplModel extends HoistModel { if (this.asPathList && this.pathFormat === 'XPath') { nodes = nodes.map(it => this.convertToPath(it)); } - this.matchingNodes = JSON.stringify(nodes, null, 2); + this.readerContent = JSON.stringify(nodes, null, 2); } private convertToPath(JSONPath: string): string { From 7eb8658600d7228ecdafdd02c55613eb52a052bf Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Mon, 27 Jan 2025 18:44:54 -0500 Subject: [PATCH 09/19] refactor into JsonSearch service --- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 4 ++-- admin/tabs/userData/prefs/UserPreferencePanel.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index 3364a8789..0c4562dc3 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -34,8 +34,8 @@ export const jsonBlobPanel = hoistCmp.factory({ }) }), jsonSearchPanel({ - docSearchUrl: 'jsonBlobSearchAdmin/searchByJsonPath', - matchingNodesUrl: 'jsonBlobSearchAdmin/getMatchingNodes', + docSearchUrl: 'jsonSearch/searchBlobs', + matchingNodesUrl: 'jsonSearch/getMatchingNodes', gridModelConfig: { sortBy: ['owner', 'name'], store: { diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index bd1078460..3e0daad15 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -37,8 +37,8 @@ export const userPreferencePanel = hoistCmp.factory({ mask: 'onLoad' }), jsonSearchPanel({ - docSearchUrl: 'preferenceJsonSearchAdmin/searchByJsonPath', - matchingNodesUrl: 'preferenceJsonSearchAdmin/getMatchingNodes', + docSearchUrl: 'jsonSearch/searchUserPreferences', + matchingNodesUrl: 'jsonSearch/getMatchingNodes', gridModelConfig: { sortBy: ['name'], groupBy: 'groupName', From 0b37b0aca464141f3ee0d34c2d31d32d0c8efd10 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 Jan 2025 01:27:54 -0500 Subject: [PATCH 10/19] support groupby --- admin/jsonsearch/JsonSearchPanel.ts | 40 +++++++++++++++---- .../impl/JsonSearchPanelImplModel.ts | 20 +++++++++- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 4 +- .../userData/prefs/UserPreferencePanel.ts | 4 +- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index 99f20f3d9..f066dc416 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -5,23 +5,41 @@ * Copyright © 2025 Extremely Heavy Industries Inc. */ +import {startCase} from 'lodash'; import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {errorMessage} from '@xh/hoist/cmp/error'; -import {JsonBlobModel} from '@xh/hoist/admin/tabs/userData/jsonblob/JsonBlobModel'; -import {grid, gridCountLabel} from '@xh/hoist/cmp/grid'; +import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid'; import {a, box, filler, h4, hframe, label, li, span, ul, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, useLocalModel} from '@xh/hoist/core'; +import {hoistCmp, SelectOption, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {buttonGroupInput, jsonInput, textInput} from '@xh/hoist/desktop/cmp/input'; +import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {Icon} from '@xh/hoist/icon'; import {popover} from '@xh/hoist/kit/blueprint'; import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; -import {startCase} from 'lodash'; - import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel'; -export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ +export interface JsonSearchPanelProps { + /** Url to endpoint for searching for matching JSON documents */ + docSearchUrl: string; + + /** Url to endpoint for listing matching JSON nodes */ + matchingNodesUrl: string; + + /** + * Config for GridModel used to display search results. + */ + gridModelConfig: GridConfig; + + /** + * Names of field(s) that can be used to group by. + */ + groupByOptions: SelectOption[]; +} + +export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ + displayName: 'JsonSearchPanel', + render() { const impl = useLocalModel(JsonSearchPanelImplModel), {error} = impl; @@ -80,6 +98,14 @@ const searchTbar = hoistCmp.factory(({model}) => { pathField({model}), helpButton(), toolbarSep(), + span('Group by:'), + select({ + bind: 'groupBy', + options: model.groupByOptions, + width: 160, + enableFilter: false + }), + toolbarSep(), gridCountLabel({ gridModel: model.gridModel, unit: 'document' diff --git a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts index ff541ed9b..fe956e637 100644 --- a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -6,8 +6,9 @@ */ import {GridModel} from '@xh/hoist/cmp/grid'; +import {GroupingChooserModel} from '@xh/hoist/cmp/grouping'; import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; -import {bindable, makeObservable} from '@xh/hoist/mobx'; +import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx'; import {isEmpty} from 'lodash'; /** @@ -17,9 +18,12 @@ export class JsonSearchPanelImplModel extends HoistModel { override xhImpl = true; @managed gridModel: GridModel; + @managed groupingChooserModel: GroupingChooserModel; @managed docLoadTask: TaskObserver = TaskObserver.trackLast(); @managed nodeLoadTask: TaskObserver = TaskObserver.trackLast(); + @observable groupBy: string = null; + @bindable.ref error = null; @bindable path: string = ''; @bindable readerContentType: 'document' | 'paths' | 'values' = 'values'; @@ -51,6 +55,10 @@ export class JsonSearchPanelImplModel extends HoistModel { return this.componentProps.gridModelConfig; } + get groupByOptions() { + return [...this.componentProps.groupByOptions, {value: null, label: 'None'}]; + } + constructor() { super(); makeObservable(this); @@ -138,4 +146,14 @@ export class JsonSearchPanelImplModel extends HoistModel { .replaceAll(/'?]\['?/g, '/') .replaceAll(/'?]$/g, ''); } + + @action + private setGroupBy(groupBy: string) { + this.groupBy = groupBy; + + // Always select first when regrouping. + const groupByArr = groupBy ? groupBy.split(',') : []; + this.gridModel.setGroupBy(groupByArr); + this.gridModel.preSelectFirstAsync(); + } } diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index 0c4562dc3..fc065752a 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -41,7 +41,6 @@ export const jsonBlobPanel = hoistCmp.factory({ store: { idSpec: 'token' }, - groupBy: 'type', columns: [ { field: {name: 'token', type: 'string'}, @@ -63,7 +62,8 @@ export const jsonBlobPanel = hoistCmp.factory({ }, {...Col.lastUpdated} ] - } + }, + groupByOptions: ['owner', 'type', 'name'] }), differ({omit: !model.differModel}) ); diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index 3e0daad15..a621727fd 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -41,7 +41,6 @@ export const userPreferencePanel = hoistCmp.factory({ matchingNodesUrl: 'jsonSearch/getMatchingNodes', gridModelConfig: { sortBy: ['name'], - groupBy: 'groupName', columns: [ { field: {name: 'owner', type: 'string'}, @@ -55,7 +54,8 @@ export const userPreferencePanel = hoistCmp.factory({ }, {...Col.lastUpdated} ] - } + }, + groupByOptions: ['owner', 'groupName', 'name'] }) ); } From 8075732164431dfc8d5197fe16d3b1ef187b22e9 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 Jan 2025 09:13:53 -0500 Subject: [PATCH 11/19] fix ts error --- admin/jsonsearch/JsonSearchPanel.ts | 6 +++--- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 7 +++++-- admin/tabs/userData/prefs/UserPreferencePanel.ts | 7 +++++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index f066dc416..99e109e71 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -10,7 +10,7 @@ import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {errorMessage} from '@xh/hoist/cmp/error'; import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid'; import {a, box, filler, h4, hframe, label, li, span, ul, vbox} from '@xh/hoist/cmp/layout'; -import {hoistCmp, SelectOption, useLocalModel} from '@xh/hoist/core'; +import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; @@ -19,7 +19,7 @@ import {popover} from '@xh/hoist/kit/blueprint'; import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel'; -export interface JsonSearchPanelProps { +export interface JsonSearchPanelProps extends HoistProps { /** Url to endpoint for searching for matching JSON documents */ docSearchUrl: string; @@ -37,7 +37,7 @@ export interface JsonSearchPanelProps { groupByOptions: SelectOption[]; } -export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ +export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ displayName: 'JsonSearchPanel', render() { diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index fc065752a..8d3810823 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -10,7 +10,10 @@ import * as AdminCol from '@xh/hoist/admin/columns'; import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import { + jsonSearchPanel, + type JsonSearchPanelProps +} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; @@ -64,7 +67,7 @@ export const jsonBlobPanel = hoistCmp.factory({ ] }, groupByOptions: ['owner', 'type', 'name'] - }), + } as JsonSearchPanelProps), differ({omit: !model.differModel}) ); } diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index a621727fd..446216308 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -11,7 +11,10 @@ import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPrefe import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import { + jsonSearchPanel, + type JsonSearchPanelProps +} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; @@ -56,7 +59,7 @@ export const userPreferencePanel = hoistCmp.factory({ ] }, groupByOptions: ['owner', 'groupName', 'name'] - }) + } as JsonSearchPanelProps) ); } }); From 94d812caf7d7a8499f9e6dc275bdb1abdd63bf54 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 Jan 2025 15:56:31 -0500 Subject: [PATCH 12/19] fix ts error --- admin/jsonsearch/JsonSearchPanel.ts | 2 +- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 7 ++----- admin/tabs/userData/prefs/UserPreferencePanel.ts | 8 +++----- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index 99e109e71..f203be490 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -34,7 +34,7 @@ export interface JsonSearchPanelProps extends HoistProps { /** * Names of field(s) that can be used to group by. */ - groupByOptions: SelectOption[]; + groupByOptions: Array; } export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index 8d3810823..fc065752a 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -10,10 +10,7 @@ import * as AdminCol from '@xh/hoist/admin/columns'; import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import { - jsonSearchPanel, - type JsonSearchPanelProps -} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; @@ -67,7 +64,7 @@ export const jsonBlobPanel = hoistCmp.factory({ ] }, groupByOptions: ['owner', 'type', 'name'] - } as JsonSearchPanelProps), + }), differ({omit: !model.differModel}) ); } diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index 446216308..3588e046b 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -4,6 +4,7 @@ * * Copyright © 2025 Extremely Heavy Industries Inc. */ + import * as Col from '@xh/hoist/admin/columns/Rest'; import * as AdminCol from '@xh/hoist/admin/columns'; import {prefEditorDialog} from '@xh/hoist/admin/tabs/userData/prefs/editor/PrefEditorDialog'; @@ -11,10 +12,7 @@ import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPrefe import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import { - jsonSearchPanel, - type JsonSearchPanelProps -} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {Icon} from '@xh/hoist/icon'; @@ -59,7 +57,7 @@ export const userPreferencePanel = hoistCmp.factory({ ] }, groupByOptions: ['owner', 'groupName', 'name'] - } as JsonSearchPanelProps) + }) ); } }); From fda66ac65f78b6dbd631402b5edbc2ebfae89670 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Tue, 28 Jan 2025 16:35:51 -0500 Subject: [PATCH 13/19] fix comment --- admin/jsonsearch/JsonSearchPanel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index f203be490..a9d25d6d3 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -32,7 +32,7 @@ export interface JsonSearchPanelProps extends HoistProps { gridModelConfig: GridConfig; /** - * Names of field(s) that can be used to group by. + * Names of fields that can be used to group by. */ groupByOptions: Array; } From ebcff00323689c5feb14383b914d74db8e16449f Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Wed, 29 Jan 2025 10:02:26 -0500 Subject: [PATCH 14/19] rework json search cmp into dialog --- admin/jsonsearch/JsonSearchPanel.ts | 134 ++++++++++++------ .../impl/JsonSearchPanelImplModel.ts | 10 +- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 77 +++++----- .../userData/prefs/UserPreferencePanel.ts | 55 +++---- 4 files changed, 165 insertions(+), 111 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index a9d25d6d3..e0ce5e576 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -9,17 +9,32 @@ import {startCase} from 'lodash'; import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {errorMessage} from '@xh/hoist/cmp/error'; import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid'; -import {a, box, filler, h4, hframe, label, li, span, ul, vbox} from '@xh/hoist/cmp/layout'; +import { + a, + box, + filler, + fragment, + h4, + hframe, + label, + li, + span, + ul, + vbox +} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {Icon} from '@xh/hoist/icon'; -import {popover} from '@xh/hoist/kit/blueprint'; +import {dialog, popover} from '@xh/hoist/kit/blueprint'; import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel'; -export interface JsonSearchPanelProps extends HoistProps { +export interface JsonSearchButtonProps extends HoistProps { + /** Name of the type of Json Documents being searched. This appears in the dialog title. */ + subjectName: string; + /** Url to endpoint for searching for matching JSON documents */ docSearchUrl: string; @@ -37,57 +52,84 @@ export interface JsonSearchPanelProps extends HoistProps { groupByOptions: Array; } -export const [JsonSearchPanel, jsonSearchPanel] = hoistCmp.withFactory({ +export const [JsonSearchButton, jsonSearchButton] = hoistCmp.withFactory({ displayName: 'JsonSearchPanel', render() { - const impl = useLocalModel(JsonSearchPanelImplModel), - {error} = impl; + const impl = useLocalModel(JsonSearchPanelImplModel); - return panel({ - title: 'JSON Path Search', - icon: Icon.json(), - modelConfig: { - side: 'right', - defaultSize: '75%', - collapsible: true, - defaultCollapsed: true + return fragment( + button({ + icon: Icon.json(), + text: 'JSON Search', + onClick: () => impl.toggleSearchIsOpen() + }), + jsonSearchDialog({ + omit: !impl.isOpen, + model: impl + }) + ); + } +}); + +const jsonSearchDialog = hoistCmp.factory({ + displayName: 'JsonSearchPanel', + + render({model}) { + const {error, subjectName} = model; + + return dialog({ + title: `${subjectName} Json Search`, + style: { + width: '90vw', + height: '90vh' }, - compactHeader: true, - tbar: searchTbar({model: impl}), - flex: 1, + icon: Icon.json(), + isOpen: true, + className: 'xh-admin-diff-detail', + onClose: () => model.toggleSearchIsOpen(), item: panel({ - mask: impl.docLoadTask, - items: [ - errorMessage({ - error, - title: error?.name ? startCase(error.name) : undefined - }), - hframe({ - omit: impl.error, - items: [ - panel({ - item: grid({model: impl.gridModel}) - }), - panel({ - mask: impl.nodeLoadTask, - tbar: readerTbar({model: impl}), - bbar: nodeBbar({ - omit: !impl.asPathList, - model: impl + tbar: searchTbar(), + item: panel({ + mask: model.docLoadTask, + items: [ + errorMessage({ + error, + title: error?.name ? startCase(error.name) : undefined + }), + hframe({ + omit: error, + items: [ + panel({ + item: grid({model: model.gridModel}), + modelConfig: { + side: 'left', + defaultSize: '30%', + collapsible: true, + defaultCollapsed: false, + resizable: true + } }), - item: jsonInput({ - model: impl, - bind: 'readerContent', - flex: 1, - width: '100%', - readonly: true, - showCopyButton: true + panel({ + mask: model.nodeLoadTask, + tbar: readerTbar(), + bbar: nodeBbar({ + omit: !model.asPathList, + model + }), + item: jsonInput({ + model, + bind: 'readerContent', + flex: 1, + width: '100%', + readonly: true, + showCopyButton: true + }) }) - }) - ] - }) - ] + ] + }) + ] + }) }) }); } diff --git a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts index fe956e637..d612a0d3b 100644 --- a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -23,6 +23,7 @@ export class JsonSearchPanelImplModel extends HoistModel { @managed nodeLoadTask: TaskObserver = TaskObserver.trackLast(); @observable groupBy: string = null; + @observable isOpen: boolean = false; @bindable.ref error = null; @bindable path: string = ''; @@ -35,8 +36,8 @@ export class JsonSearchPanelImplModel extends HoistModel { return this.readerContentType === 'paths'; } - get queryBuffer(): number { - return this.componentProps.queryBuffer ?? 200; + get subjectName(): string { + return this.componentProps.subjectName; } get docSearchUrl(): string { @@ -59,6 +60,11 @@ export class JsonSearchPanelImplModel extends HoistModel { return [...this.componentProps.groupByOptions, {value: null, label: 'None'}]; } + @action + toggleSearchIsOpen() { + this.isOpen = !this.isOpen; + } + constructor() { super(); makeObservable(this); diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index fc065752a..184e4fbc2 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -10,9 +10,10 @@ import * as AdminCol from '@xh/hoist/admin/columns'; import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; +import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {Icon} from '@xh/hoist/icon'; import {differ} from '../../../differ/Differ'; import {JsonBlobModel} from './JsonBlobModel'; @@ -24,46 +25,48 @@ export const jsonBlobPanel = hoistCmp.factory({ return hframe( panel({ item: restGrid({ - extraToolbarItems: () => { - return button({ + extraToolbarItems: () => [ + button({ icon: Icon.diff(), text: 'Compare w/ Remote', onClick: () => model.openDiffer() - }); - } - }) - }), - jsonSearchPanel({ - docSearchUrl: 'jsonSearch/searchBlobs', - matchingNodesUrl: 'jsonSearch/getMatchingNodes', - gridModelConfig: { - sortBy: ['owner', 'name'], - store: { - idSpec: 'token' - }, - columns: [ - { - field: {name: 'token', type: 'string'}, - hidden: true, - width: 100 - }, - { - field: {name: 'type', type: 'string'}, - width: 200 - }, - { - field: {name: 'owner', type: 'string'}, - width: 200 - }, - {...AdminCol.name}, - { - field: {name: 'json', type: 'string'}, - hidden: true - }, - {...Col.lastUpdated} + }), + toolbarSep(), + jsonSearchButton({ + subjectName: 'JSON Blob', + docSearchUrl: 'jsonSearch/searchBlobs', + matchingNodesUrl: 'jsonSearch/getMatchingNodes', + gridModelConfig: { + sortBy: ['type', 'name', 'owner'], + store: { + idSpec: 'token' + }, + columns: [ + { + field: {name: 'token', type: 'string'}, + hidden: true, + width: 100 + }, + { + field: {name: 'type', type: 'string'}, + width: 200 + }, + { + field: {name: 'owner', type: 'string'}, + width: 200 + }, + {...AdminCol.name}, + { + field: {name: 'json', type: 'string'}, + hidden: true + }, + {...Col.lastUpdated} + ] + }, + groupByOptions: ['owner', 'type', 'name'] + }) ] - }, - groupByOptions: ['owner', 'type', 'name'] + }) }), differ({omit: !model.differModel}) ); diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index 3588e046b..26d961ee9 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -12,9 +12,10 @@ import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPrefe import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchPanel} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; +import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {Icon} from '@xh/hoist/icon'; export const userPreferencePanel = hoistCmp.factory({ @@ -25,38 +26,40 @@ export const userPreferencePanel = hoistCmp.factory({ panel({ items: [ restGrid({ - extraToolbarItems: () => { - return button({ + extraToolbarItems: () => [ + button({ icon: Icon.gear(), text: 'Configure', onClick: () => (model.showEditorDialog = true) - }); - } + }), + toolbarSep(), + jsonSearchButton({ + subjectName: 'User Preference', + docSearchUrl: 'jsonSearch/searchUserPreferences', + matchingNodesUrl: 'jsonSearch/getMatchingNodes', + gridModelConfig: { + sortBy: ['groupName', 'name', 'owner'], + columns: [ + { + field: {name: 'owner', type: 'string'}, + width: 200 + }, + {...AdminCol.groupName}, + {...AdminCol.name}, + { + field: {name: 'json', type: 'string'}, + hidden: true + }, + {...Col.lastUpdated} + ] + }, + groupByOptions: ['owner', 'groupName', 'name'] + }) + ] }), prefEditorDialog() ], mask: 'onLoad' - }), - jsonSearchPanel({ - docSearchUrl: 'jsonSearch/searchUserPreferences', - matchingNodesUrl: 'jsonSearch/getMatchingNodes', - gridModelConfig: { - sortBy: ['name'], - columns: [ - { - field: {name: 'owner', type: 'string'}, - width: 200 - }, - {...AdminCol.groupName}, - {...AdminCol.name}, - { - field: {name: 'json', type: 'string'}, - hidden: true - }, - {...Col.lastUpdated} - ] - }, - groupByOptions: ['owner', 'groupName', 'name'] }) ); } From 66adfb8c0edc6e0fe42e23f764b18d572c6e9839 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Wed, 29 Jan 2025 10:44:02 -0500 Subject: [PATCH 15/19] add json search to configs. other cleanups --- admin/jsonsearch/JsonSearchPanel.ts | 3 -- .../impl/JsonSearchPanelImplModel.ts | 6 ++-- admin/tabs/general/config/ConfigPanel.ts | 34 ++++++++++++++++--- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 4 --- .../userData/prefs/UserPreferencePanel.ts | 1 - 5 files changed, 32 insertions(+), 16 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index e0ce5e576..edad8f2ee 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -38,9 +38,6 @@ export interface JsonSearchButtonProps extends HoistProps { /** Url to endpoint for searching for matching JSON documents */ docSearchUrl: string; - /** Url to endpoint for listing matching JSON nodes */ - matchingNodesUrl: string; - /** * Config for GridModel used to display search results. */ diff --git a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts index d612a0d3b..18a4aa339 100644 --- a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -17,6 +17,8 @@ import {isEmpty} from 'lodash'; export class JsonSearchPanelImplModel extends HoistModel { override xhImpl = true; + private matchingNodesUrl = 'jsonSearch/getMatchingNodes'; + @managed gridModel: GridModel; @managed groupingChooserModel: GroupingChooserModel; @managed docLoadTask: TaskObserver = TaskObserver.trackLast(); @@ -44,10 +46,6 @@ export class JsonSearchPanelImplModel extends HoistModel { return this.componentProps.docSearchUrl; } - get matchingNodesUrl(): string { - return this.componentProps.matchingNodesUrl; - } - get selectedRecord() { return this.gridModel.selectedRecord; } diff --git a/admin/tabs/general/config/ConfigPanel.ts b/admin/tabs/general/config/ConfigPanel.ts index d7f660f52..797e118c9 100644 --- a/admin/tabs/general/config/ConfigPanel.ts +++ b/admin/tabs/general/config/ConfigPanel.ts @@ -4,10 +4,14 @@ * * Copyright © 2025 Extremely Heavy Industries Inc. */ +import * as AdminCol from '@xh/hoist/admin/columns'; +import * as Col from '@xh/hoist/admin/columns/Rest'; +import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; import {fragment} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; +import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {Icon} from '@xh/hoist/icon'; import {differ} from '../../../differ/Differ'; import {regroupDialog} from '../../../regroup/RegroupDialog'; @@ -20,13 +24,35 @@ export const configPanel = hoistCmp.factory({ return fragment( restGrid({ testId: 'config', - extraToolbarItems: () => { - return button({ + extraToolbarItems: () => [ + button({ icon: Icon.diff(), text: 'Compare w/ Remote', onClick: () => model.openDiffer() - }); - } + }), + toolbarSep(), + jsonSearchButton({ + subjectName: 'Config', + docSearchUrl: 'jsonSearch/searchConfigs', + gridModelConfig: { + sortBy: ['groupName', 'name', 'owner'], + columns: [ + { + field: {name: 'owner', type: 'string'}, + width: 200 + }, + {...AdminCol.groupName}, + {...AdminCol.name}, + { + field: {name: 'json', type: 'string'}, + hidden: true + }, + {...Col.lastUpdated} + ] + }, + groupByOptions: ['owner', 'groupName', 'name'] + }) + ] }), differ({omit: !model.differModel}), regroupDialog() diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index 184e4fbc2..b81b3caa5 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -35,12 +35,8 @@ export const jsonBlobPanel = hoistCmp.factory({ jsonSearchButton({ subjectName: 'JSON Blob', docSearchUrl: 'jsonSearch/searchBlobs', - matchingNodesUrl: 'jsonSearch/getMatchingNodes', gridModelConfig: { sortBy: ['type', 'name', 'owner'], - store: { - idSpec: 'token' - }, columns: [ { field: {name: 'token', type: 'string'}, diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index 26d961ee9..2d405c481 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -36,7 +36,6 @@ export const userPreferencePanel = hoistCmp.factory({ jsonSearchButton({ subjectName: 'User Preference', docSearchUrl: 'jsonSearch/searchUserPreferences', - matchingNodesUrl: 'jsonSearch/getMatchingNodes', gridModelConfig: { sortBy: ['groupName', 'name', 'owner'], columns: [ From a614d189f05bc60978df47611c86ceddff951427 Mon Sep 17 00:00:00 2001 From: Colin Rudd Date: Wed, 29 Jan 2025 11:20:26 -0500 Subject: [PATCH 16/19] show path and value in same panel, add more examples --- admin/jsonsearch/JsonSearchPanel.ts | 48 +++++++++++++------ .../impl/JsonSearchPanelImplModel.ts | 26 +++++----- 2 files changed, 45 insertions(+), 29 deletions(-) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearchPanel.ts index edad8f2ee..10f97f308 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearchPanel.ts @@ -111,7 +111,7 @@ const jsonSearchDialog = hoistCmp.factory({ mask: model.nodeLoadTask, tbar: readerTbar(), bbar: nodeBbar({ - omit: !model.asPathList, + omit: model.readerContentType !== 'matches', model }), item: jsonInput({ @@ -186,14 +186,7 @@ const helpButton = hoistCmp.factory({ items: [ h4('Sample Queries'), ul({ - style: {listStyleType: 'none'}, - items: [ - { - query: "$..[?(@.colId == 'trader')]", - explanation: - 'Find all nodes with a property "colId" equal to "trader"' - } - ].map(({query, explanation}) => + items: queryExamples.map(({query, explanation}) => li({ key: query, items: [ @@ -241,12 +234,8 @@ const readerTbar = hoistCmp.factory(({model}) => { value: 'document' }), button({ - text: 'Matching Paths', - value: 'paths' - }), - button({ - text: 'Matching Values', - value: 'values' + text: 'Matches', + value: 'matches' }) ] }), @@ -267,6 +256,7 @@ const nodeBbar = hoistCmp.factory(({model}) => { bind: 'pathFormat', minimal: true, outlined: true, + disabled: !model.selectedRecord, items: [ button({ text: 'XPath', @@ -280,3 +270,31 @@ const nodeBbar = hoistCmp.factory(({model}) => { }) ); }); + +const queryExamples = [ + { + query: '$', + explanation: 'Return the root object' + }, + { + query: '$..*', + explanation: 'Return all nodes, recursively' + }, + { + query: '$..[?(@.colId && @.width && @.hidden != true)]', + explanation: + 'Find all nodes with a property "colId" and a property "width" and a property "hidden" not equal to true' + }, + { + query: '$..[?(@.colId && @.width)]', + explanation: 'Find all nodes with a property "colId" and a property "width"' + }, + { + query: "$..[?(@.colId == 'trader')]", + explanation: 'Find all nodes with a property "colId" equal to "trader"' + }, + { + query: '$..grid[?(@.version == 1)]', + explanation: 'Find all grid nodes with a property "version" equal to 1' + } +]; diff --git a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts index 18a4aa339..72f38e7b4 100644 --- a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts @@ -9,7 +9,7 @@ import {GridModel} from '@xh/hoist/cmp/grid'; import {GroupingChooserModel} from '@xh/hoist/cmp/grouping'; import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx'; -import {isEmpty} from 'lodash'; +import {isEmpty, zipWith} from 'lodash'; /** * @internal @@ -29,15 +29,11 @@ export class JsonSearchPanelImplModel extends HoistModel { @bindable.ref error = null; @bindable path: string = ''; - @bindable readerContentType: 'document' | 'paths' | 'values' = 'values'; + @bindable readerContentType: 'document' | 'matches' = 'matches'; @bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath'; @bindable readerContent: string = ''; @bindable matchingNodeCount: number = 0; - get asPathList(): boolean { - return this.readerContentType === 'paths'; - } - get subjectName(): string { return this.componentProps.subjectName; } @@ -86,7 +82,7 @@ export class JsonSearchPanelImplModel extends HoistModel { }, { track: () => [this.selectedRecord, this.readerContentType, this.pathFormat], - run: () => this.loadreaderContentTypeAsync(), + run: () => this.loadreaderContentAsync(), debounce: 300 } ); @@ -114,7 +110,7 @@ export class JsonSearchPanelImplModel extends HoistModel { } } - private async loadreaderContentTypeAsync() { + private async loadreaderContentAsync() { if (!this.selectedRecord) { this.matchingNodeCount = 0; this.readerContent = ''; @@ -132,19 +128,21 @@ export class JsonSearchPanelImplModel extends HoistModel { url: this.matchingNodesUrl, params: { path: this.path, - asPathList: this.readerContentType === 'paths', json } }).linkTo(this.nodeLoadTask); - this.matchingNodeCount = nodes.length; - if (this.asPathList && this.pathFormat === 'XPath') { - nodes = nodes.map(it => this.convertToPath(it)); - } + this.matchingNodeCount = nodes.paths.length; + nodes = zipWith(nodes.paths, nodes.values, (path: string, value) => { + return { + path: this.pathFormat === 'XPath' ? this.convertToXPath(path) : path, + value + }; + }); this.readerContent = JSON.stringify(nodes, null, 2); } - private convertToPath(JSONPath: string): string { + private convertToXPath(JSONPath: string): string { return JSONPath.replaceAll(/^\$\['?/g, '/') .replaceAll(/^\$/g, '') .replaceAll(/'?]\['?/g, '/') From 7131a1dd092f6886017ccef8b9e071997ce69235 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Wed, 29 Jan 2025 11:13:29 -0800 Subject: [PATCH 17/19] Misc polish --- .../{JsonSearchPanel.ts => JsonSearch.ts} | 95 +++++++++---------- ...nelImplModel.ts => JsonSearchImplModel.ts} | 22 +++-- admin/tabs/general/config/ConfigPanel.ts | 10 +- admin/tabs/userData/jsonblob/JsonBlobPanel.ts | 2 +- .../userData/prefs/UserPreferencePanel.ts | 2 +- 5 files changed, 65 insertions(+), 66 deletions(-) rename admin/jsonsearch/{JsonSearchPanel.ts => JsonSearch.ts} (78%) rename admin/jsonsearch/impl/{JsonSearchPanelImplModel.ts => JsonSearchImplModel.ts} (87%) diff --git a/admin/jsonsearch/JsonSearchPanel.ts b/admin/jsonsearch/JsonSearch.ts similarity index 78% rename from admin/jsonsearch/JsonSearchPanel.ts rename to admin/jsonsearch/JsonSearch.ts index 10f97f308..e524df0aa 100644 --- a/admin/jsonsearch/JsonSearchPanel.ts +++ b/admin/jsonsearch/JsonSearch.ts @@ -5,8 +5,6 @@ * Copyright © 2025 Extremely Heavy Industries Inc. */ -import {startCase} from 'lodash'; -import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {errorMessage} from '@xh/hoist/cmp/error'; import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid'; import { @@ -24,36 +22,39 @@ import { } from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; +import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input'; import {panel} from '@xh/hoist/desktop/cmp/panel'; +import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; import {Icon} from '@xh/hoist/icon'; import {dialog, popover} from '@xh/hoist/kit/blueprint'; -import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; -import {JsonSearchPanelImplModel} from './impl/JsonSearchPanelImplModel'; +import {pluralize} from '@xh/hoist/utils/js'; +import {startCase} from 'lodash'; +import {JsonSearchImplModel} from './impl/JsonSearchImplModel'; export interface JsonSearchButtonProps extends HoistProps { - /** Name of the type of Json Documents being searched. This appears in the dialog title. */ + /** Descriptive label for the type of records being searched - appears in the dialog title. */ subjectName: string; - /** Url to endpoint for searching for matching JSON documents */ + /** Endpoint to search and return matches - Hoist `JsonSearchController` action expected. */ docSearchUrl: string; - /** - * Config for GridModel used to display search results. - */ + /** Config for GridModel used to display search results. */ gridModelConfig: GridConfig; - /** - * Names of fields that can be used to group by. - */ + /** Field names on returned results to enable for grouping in the search results grid. */ groupByOptions: Array; } -export const [JsonSearchButton, jsonSearchButton] = hoistCmp.withFactory({ - displayName: 'JsonSearchPanel', +/** + * Main entry point component for the JSON search feature. Supported out-of-the-box for a limited + * set of Hoist artifacts that hold JSON values: JSONBlob, Configs, and User Preferences. + */ +export const jsonSearchButton = hoistCmp.factory({ + displayName: 'JsonSearchButton', render() { - const impl = useLocalModel(JsonSearchPanelImplModel); + const impl = useLocalModel(JsonSearchImplModel); return fragment( button({ @@ -69,14 +70,14 @@ export const [JsonSearchButton, jsonSearchButton] = hoistCmp.withFactory({ - displayName: 'JsonSearchPanel', +const jsonSearchDialog = hoistCmp.factory({ + displayName: 'JsonSearchDialog', render({model}) { const {error, subjectName} = model; return dialog({ - title: `${subjectName} Json Search`, + title: `JSON Search: ${subjectName}`, style: { width: '90vw', height: '90vh' @@ -110,10 +111,6 @@ const jsonSearchDialog = hoistCmp.factory({ panel({ mask: model.nodeLoadTask, tbar: readerTbar(), - bbar: nodeBbar({ - omit: model.readerContentType !== 'matches', - model - }), item: jsonInput({ model, bind: 'readerContent', @@ -132,7 +129,7 @@ const jsonSearchDialog = hoistCmp.factory({ } }); -const searchTbar = hoistCmp.factory(({model}) => { +const searchTbar = hoistCmp.factory(({model}) => { return toolbar( pathField({model}), helpButton(), @@ -152,7 +149,7 @@ const searchTbar = hoistCmp.factory(({model}) => { ); }); -const pathField = hoistCmp.factory({ +const pathField = hoistCmp.factory({ render({model}) { return textInput({ bind: 'path', @@ -219,7 +216,7 @@ const helpButton = hoistCmp.factory({ } }); -const readerTbar = hoistCmp.factory(({model}) => { +const readerTbar = hoistCmp.factory(({model}) => { return toolbar({ items: [ buttonGroupInput({ @@ -239,38 +236,38 @@ const readerTbar = hoistCmp.factory(({model}) => { }) ] }), + fragment({ + omit: model.readerContentType !== 'matches' || !model.selectedRecord, + items: [ + toolbarSep(), + label('View path as'), + buttonGroupInput({ + model, + bind: 'pathFormat', + minimal: true, + outlined: true, + items: [ + button({ + text: 'XPath', + value: 'XPath' + }), + button({ + text: 'JSONPath', + value: 'JSONPath' + }) + ] + }) + ] + }), filler(), box({ omit: !model.matchingNodeCount, - item: `${model.matchingNodeCount} ${model.matchingNodeCount === 1 ? 'match' : 'matches'}` + item: `${pluralize('match', model.matchingNodeCount, true)} within this document` }) ] }); }); -const nodeBbar = hoistCmp.factory(({model}) => { - return toolbar( - label('Path Format:'), - buttonGroupInput({ - model, - bind: 'pathFormat', - minimal: true, - outlined: true, - disabled: !model.selectedRecord, - items: [ - button({ - text: 'XPath', - value: 'XPath' - }), - button({ - text: 'JSONPath', - value: 'JSONPath' - }) - ] - }) - ); -}); - const queryExamples = [ { query: '$', diff --git a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts b/admin/jsonsearch/impl/JsonSearchImplModel.ts similarity index 87% rename from admin/jsonsearch/impl/JsonSearchPanelImplModel.ts rename to admin/jsonsearch/impl/JsonSearchImplModel.ts index 72f38e7b4..d64935a15 100644 --- a/admin/jsonsearch/impl/JsonSearchPanelImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchImplModel.ts @@ -5,8 +5,7 @@ * Copyright © 2025 Extremely Heavy Industries Inc. */ -import {GridModel} from '@xh/hoist/cmp/grid'; -import {GroupingChooserModel} from '@xh/hoist/cmp/grouping'; +import {GridConfig, GridModel} from '@xh/hoist/cmp/grid'; import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx'; import {isEmpty, zipWith} from 'lodash'; @@ -14,13 +13,12 @@ import {isEmpty, zipWith} from 'lodash'; /** * @internal */ -export class JsonSearchPanelImplModel extends HoistModel { +export class JsonSearchImplModel extends HoistModel { override xhImpl = true; private matchingNodesUrl = 'jsonSearch/getMatchingNodes'; @managed gridModel: GridModel; - @managed groupingChooserModel: GroupingChooserModel; @managed docLoadTask: TaskObserver = TaskObserver.trackLast(); @managed nodeLoadTask: TaskObserver = TaskObserver.trackLast(); @@ -46,12 +44,19 @@ export class JsonSearchPanelImplModel extends HoistModel { return this.gridModel.selectedRecord; } - get gridModelConfig() { + get gridModelConfig(): GridConfig { return this.componentProps.gridModelConfig; } get groupByOptions() { - return [...this.componentProps.groupByOptions, {value: null, label: 'None'}]; + const cols = this.gridModel.getLeafColumns(); + return [ + ...this.componentProps.groupByOptions.map(it => ({ + value: it, + label: cols.find(col => col.colId === it)?.displayName ?? it + })), + {value: null, label: 'None'} + ]; } @action @@ -67,6 +72,7 @@ export class JsonSearchPanelImplModel extends HoistModel { override onLinked() { this.gridModel = new GridModel({ ...this.gridModelConfig, + emptyText: 'No matches found...', selModel: 'single' }); @@ -82,7 +88,7 @@ export class JsonSearchPanelImplModel extends HoistModel { }, { track: () => [this.selectedRecord, this.readerContentType, this.pathFormat], - run: () => this.loadreaderContentAsync(), + run: () => this.loadReaderContentAsync(), debounce: 300 } ); @@ -110,7 +116,7 @@ export class JsonSearchPanelImplModel extends HoistModel { } } - private async loadreaderContentAsync() { + private async loadReaderContentAsync() { if (!this.selectedRecord) { this.matchingNodeCount = 0; this.readerContent = ''; diff --git a/admin/tabs/general/config/ConfigPanel.ts b/admin/tabs/general/config/ConfigPanel.ts index 797e118c9..bddc31910 100644 --- a/admin/tabs/general/config/ConfigPanel.ts +++ b/admin/tabs/general/config/ConfigPanel.ts @@ -6,7 +6,7 @@ */ import * as AdminCol from '@xh/hoist/admin/columns'; import * as Col from '@xh/hoist/admin/columns/Rest'; -import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch'; import {fragment} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; @@ -35,12 +35,8 @@ export const configPanel = hoistCmp.factory({ subjectName: 'Config', docSearchUrl: 'jsonSearch/searchConfigs', gridModelConfig: { - sortBy: ['groupName', 'name', 'owner'], + sortBy: ['groupName', 'name'], columns: [ - { - field: {name: 'owner', type: 'string'}, - width: 200 - }, {...AdminCol.groupName}, {...AdminCol.name}, { @@ -50,7 +46,7 @@ export const configPanel = hoistCmp.factory({ {...Col.lastUpdated} ] }, - groupByOptions: ['owner', 'groupName', 'name'] + groupByOptions: ['groupName'] }) ] }), diff --git a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts index b81b3caa5..bc9bb812e 100644 --- a/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +++ b/admin/tabs/userData/jsonblob/JsonBlobPanel.ts @@ -10,7 +10,7 @@ import * as AdminCol from '@xh/hoist/admin/columns'; import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; diff --git a/admin/tabs/userData/prefs/UserPreferencePanel.ts b/admin/tabs/userData/prefs/UserPreferencePanel.ts index 2d405c481..a615cf045 100644 --- a/admin/tabs/userData/prefs/UserPreferencePanel.ts +++ b/admin/tabs/userData/prefs/UserPreferencePanel.ts @@ -12,7 +12,7 @@ import {UserPreferenceModel} from '@xh/hoist/admin/tabs/userData/prefs/UserPrefe import {hframe} from '@xh/hoist/cmp/layout'; import {creates, hoistCmp} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; -import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearchPanel'; +import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch'; import {panel} from '@xh/hoist/desktop/cmp/panel'; import {restGrid} from '@xh/hoist/desktop/cmp/rest'; import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar'; From 79c3e74e21a83d7df5bf9e8b9ff0b611b79a3f97 Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Wed, 29 Jan 2025 16:33:26 -0800 Subject: [PATCH 18/19] Misc polish --- admin/jsonsearch/JsonSearch.ts | 105 +++++++++---------- admin/jsonsearch/impl/JsonSearchImplModel.ts | 36 ++++--- 2 files changed, 73 insertions(+), 68 deletions(-) diff --git a/admin/jsonsearch/JsonSearch.ts b/admin/jsonsearch/JsonSearch.ts index e524df0aa..f3e86b554 100644 --- a/admin/jsonsearch/JsonSearch.ts +++ b/admin/jsonsearch/JsonSearch.ts @@ -7,19 +7,7 @@ import {errorMessage} from '@xh/hoist/cmp/error'; import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid'; -import { - a, - box, - filler, - fragment, - h4, - hframe, - label, - li, - span, - ul, - vbox -} from '@xh/hoist/cmp/layout'; +import {a, box, filler, fragment, hframe, label, li, p, span, ul, vbox} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core'; import {button} from '@xh/hoist/desktop/cmp/button'; import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard'; @@ -33,7 +21,7 @@ import {startCase} from 'lodash'; import {JsonSearchImplModel} from './impl/JsonSearchImplModel'; export interface JsonSearchButtonProps extends HoistProps { - /** Descriptive label for the type of records being searched - appears in the dialog title. */ + /** Descriptive label for the type of records being searched - will be auto-pluralized. */ subjectName: string; /** Endpoint to search and return matches - Hoist `JsonSearchController` action expected. */ @@ -129,24 +117,34 @@ const jsonSearchDialog = hoistCmp.factory({ } }); -const searchTbar = hoistCmp.factory(({model}) => { - return toolbar( - pathField({model}), - helpButton(), - toolbarSep(), - span('Group by:'), - select({ - bind: 'groupBy', - options: model.groupByOptions, - width: 160, - enableFilter: false - }), - toolbarSep(), - gridCountLabel({ - gridModel: model.gridModel, - unit: 'document' - }) - ); +const searchTbar = hoistCmp.factory({ + render({model}) { + return toolbar( + pathField({model}), + button({ + text: `Search ${model.subjectName}`, + intent: 'success', + outlined: true, + disabled: !model.path, + onClick: () => model.loadMatchingDocsAsync() + }), + '-', + helpButton({model}), + '-', + span('Group by:'), + select({ + bind: 'groupBy', + options: model.groupByOptions, + width: 160, + enableFilter: false + }), + '-', + gridCountLabel({ + gridModel: model.gridModel, + unit: 'match' + }) + ); + } }); const pathField = hoistCmp.factory({ @@ -157,31 +155,32 @@ const pathField = hoistCmp.factory({ commitOnChange: true, leftIcon: Icon.search(), enableClear: true, - placeholder: - "JSON Path - e.g. $..[?(@.colId == 'trader')] - type a path and hit ENTER to search", + placeholder: 'Provide a JSON Path expression to evaluate', width: null, flex: 1, onKeyDown: e => { - if (e.key === 'Enter') model.loadJsonDocsAsync(); + if (e.key === 'Enter') model.loadMatchingDocsAsync(); } }); } }); -const helpButton = hoistCmp.factory({ - model: false, - render() { +const helpButton = hoistCmp.factory({ + render({model}) { return popover({ item: button({ icon: Icon.questionCircle(), outlined: true }), content: vbox({ - style: { - padding: '0px 20px 10px 20px' - }, + className: 'xh-pad', items: [ - h4('Sample Queries'), + p( + `JSON Path expressions allow you to recursively query JSON documents, matching nodes based on their path, properties, and values.` + ), + p( + `Enter a path and press [Enter] to search for matches within the JSON content of ${model.subjectName}.` + ), ul({ items: queryExamples.map(({query, explanation}) => li({ @@ -203,7 +202,8 @@ const helpButton = hoistCmp.factory({ explanation ] }) - ) + ), + style: {marginTop: 0} }), a({ href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators', @@ -270,28 +270,25 @@ const readerTbar = hoistCmp.factory(({model}) => { const queryExamples = [ { - query: '$', - explanation: 'Return the root object' - }, - { - query: '$..*', - explanation: 'Return all nodes, recursively' + query: '$.displayMode', + explanation: 'Return documents with a top-level property "displayMode"' }, { - query: '$..[?(@.colId && @.width && @.hidden != true)]', + query: "$..[?(@.colId == 'trader')]", explanation: - 'Find all nodes with a property "colId" and a property "width" and a property "hidden" not equal to true' + 'Find all nodes (anywhere in the document) with a property "colId" equal to "trader"' }, { query: '$..[?(@.colId && @.width)]', explanation: 'Find all nodes with a property "colId" and a property "width"' }, { - query: "$..[?(@.colId == 'trader')]", - explanation: 'Find all nodes with a property "colId" equal to "trader"' + query: '$..[?(@.colId && @.hidden != true)]', + explanation: + 'Find all nodes with a property "colId" and a property "hidden" not equal to true' }, { query: '$..grid[?(@.version == 1)]', - explanation: 'Find all grid nodes with a property "version" equal to 1' + explanation: 'Find all nodes with a key of "grid" and a property "version" equal to 1' } ]; diff --git a/admin/jsonsearch/impl/JsonSearchImplModel.ts b/admin/jsonsearch/impl/JsonSearchImplModel.ts index d64935a15..50b1db898 100644 --- a/admin/jsonsearch/impl/JsonSearchImplModel.ts +++ b/admin/jsonsearch/impl/JsonSearchImplModel.ts @@ -8,7 +8,8 @@ import {GridConfig, GridModel} from '@xh/hoist/cmp/grid'; import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core'; import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx'; -import {isEmpty, zipWith} from 'lodash'; +import {pluralize} from '@xh/hoist/utils/js'; +import {camelCase, isEmpty, zipWith} from 'lodash'; /** * @internal @@ -33,21 +34,21 @@ export class JsonSearchImplModel extends HoistModel { @bindable matchingNodeCount: number = 0; get subjectName(): string { - return this.componentProps.subjectName; + return pluralize(this.componentProps.subjectName); } get docSearchUrl(): string { return this.componentProps.docSearchUrl; } - get selectedRecord() { - return this.gridModel.selectedRecord; - } - get gridModelConfig(): GridConfig { return this.componentProps.gridModelConfig; } + get selectedRecord() { + return this.gridModel.selectedRecord; + } + get groupByOptions() { const cols = this.gridModel.getLeafColumns(); return [ @@ -76,6 +77,8 @@ export class JsonSearchImplModel extends HoistModel { selModel: 'single' }); + this.markPersist('path', {localStorageKey: `xhJsonSearch${camelCase(this.subjectName)}`}); + this.addReaction( { track: () => this.path, @@ -92,26 +95,31 @@ export class JsonSearchImplModel extends HoistModel { debounce: 300 } ); + + // We might have a persisted path - go ahead and load if so. + this.loadMatchingDocsAsync(); } - async loadJsonDocsAsync() { - if (isEmpty(this.path)) { + async loadMatchingDocsAsync() { + const {path, gridModel, docLoadTask} = this; + + if (isEmpty(path)) { this.error = null; - this.gridModel.clear(); + gridModel.clear(); return; } try { const data = await XH.fetchJson({ url: this.docSearchUrl, - params: {path: this.path} - }).linkTo(this.docLoadTask); + params: {path} + }).linkTo(docLoadTask); this.error = null; - this.gridModel.loadData(data); - this.gridModel.selectFirstAsync(); + gridModel.loadData(data); + gridModel.preSelectFirstAsync(); } catch (e) { - this.gridModel.clear(); + gridModel.clear(); this.error = e; } } From 4140cd622ac4ce8ca992b3bd37fa90871e7c08fa Mon Sep 17 00:00:00 2001 From: Anselm McClain Date: Wed, 29 Jan 2025 16:37:47 -0800 Subject: [PATCH 19/19] Changelog entry --- CHANGELOG.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1115868f..4c9ad9684 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,19 @@ ## v73.0.0-SNAPSHOT - unreleased +### 💥 Breaking Changes (upgrade difficulty: 🟢 LOW - Hoist core update) + +* Requires `hoist-core >= 28.1` with new APIs to support JSON searching in the Admin Console. + +### 🎁 New Features + +* Introduced a new "JSON Search" feature to the Hoist Admin Console, accessible from the Config, + User Preference, and JSON Blob tabs. Supports searching JSON values stored within these objects + to filter and match data using JSON Path expressions. + ## v72.0.0 - 2025-01-27 -### 💥 Breaking Changes +### 💥 Breaking Changes (upgrade difficulty: 🟢 TRIVIAL - minor changes to mobile nav) * Mobile `Navigator` no longer supports `animation` prop, and `NavigatorModel` no longer supports `swipeToGoBack`. Both of these properties are now managed internally by the `Navigator` component. @@ -17,7 +27,8 @@ ### 🐞 Bug Fixes * Fixed `ViewManagerModel` unique name validation. -* Fixed `GridModel.restoreDefaultsAsync()` to restore any default filter, rather than simply clearing it. +* Fixed `GridModel.restoreDefaultsAsync()` to restore any default filter, rather than simply + clearing it. * Improved suboptimal column state synchronization between `GridModel` and AG Grid. ### ⚙️ Technical