@xh/hoist 72.0.0 β 72.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -2
- package/admin/jsonsearch/JsonSearch.ts +294 -0
- package/admin/jsonsearch/impl/JsonSearchImplModel.ts +175 -0
- package/admin/tabs/activity/clienterrors/ClientErrorsModel.ts +23 -4
- package/admin/tabs/general/config/ConfigPanel.ts +26 -4
- package/admin/tabs/userData/jsonblob/JsonBlobPanel.ts +47 -10
- package/admin/tabs/userData/prefs/UserPreferencePanel.ts +45 -15
- package/admin/tabs/userData/roles/RoleModel.ts +3 -3
- package/admin/tabs/userData/roles/details/RoleDetailsModel.ts +2 -1
- package/admin/tabs/userData/roles/editor/form/RoleFormModel.ts +3 -3
- package/admin/tabs/userData/roles/recategorize/RecategorizeDialogModel.ts +1 -1
- package/build/types/admin/jsonsearch/JsonSearch.d.ts +17 -0
- package/build/types/admin/jsonsearch/impl/JsonSearchImplModel.d.ts +32 -0
- package/build/types/cmp/tab/TabContainerModel.d.ts +5 -5
- package/build/types/core/HoistProps.d.ts +1 -0
- package/build/types/core/XH.d.ts +5 -5
- package/build/types/core/persist/PersistenceProvider.d.ts +4 -0
- package/build/types/core/types/Interfaces.d.ts +9 -0
- package/build/types/data/Store.d.ts +4 -0
- package/build/types/data/StoreRecord.d.ts +2 -0
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +1 -0
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +1 -0
- package/build/types/kit/blueprint/Wrappers.d.ts +1 -1
- package/build/types/kit/swiper/index.d.ts +4 -4
- package/build/types/security/BaseOAuthClient.d.ts +19 -21
- package/build/types/security/Token.d.ts +0 -1
- package/build/types/security/Types.d.ts +12 -0
- package/build/types/security/authzero/AuthZeroClient.d.ts +3 -4
- package/build/types/security/msal/MsalClient.d.ts +3 -4
- package/cmp/filter/FilterChooserModel.ts +6 -2
- package/cmp/grid/impl/InitPersist.ts +9 -3
- package/cmp/grouping/GroupingChooserModel.ts +6 -2
- package/cmp/tab/TabContainerModel.ts +5 -5
- package/cmp/zoneGrid/impl/InitPersist.ts +9 -3
- package/core/HoistBase.ts +1 -2
- package/core/HoistBaseDecorators.ts +4 -1
- package/core/HoistProps.ts +1 -0
- package/core/XH.ts +13 -5
- package/core/exception/Exception.ts +19 -12
- package/core/persist/PersistenceProvider.ts +31 -0
- package/core/types/Interfaces.ts +11 -0
- package/data/Store.ts +13 -3
- package/data/StoreRecord.ts +6 -1
- package/data/cube/row/BaseRow.ts +3 -2
- package/desktop/appcontainer/ExceptionDialog.ts +1 -1
- package/desktop/cmp/dash/canvas/DashCanvas.ts +2 -1
- package/mobile/cmp/navigator/NavigatorModel.ts +7 -0
- package/package.json +1 -1
- package/security/BaseOAuthClient.ts +41 -36
- package/security/Token.ts +0 -2
- package/security/Types.ts +22 -0
- package/security/authzero/AuthZeroClient.ts +6 -8
- package/security/msal/MsalClient.ts +6 -8
- package/tsconfig.tsbuildinfo +1 -1
- package/utils/react/LayoutPropUtils.ts +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v72.2.0 - 2025-03-13
|
|
4
|
+
|
|
5
|
+
### π New Features
|
|
6
|
+
* Modified `TabContainerModel` to make more methods `protected`, improving extensibility for
|
|
7
|
+
advanced use-cases.
|
|
8
|
+
* Enhanced `XH.reloadApp` with new argument to clear query parameters before loading.
|
|
9
|
+
* Enhanced exception handling in `FetchService` to capture messages returned as raw strings, or
|
|
10
|
+
without explicit names.
|
|
11
|
+
* Added dedicated columns to the Admin Console "Client Errors" tab for error names and messages.
|
|
12
|
+
* `BaseOAuthClient` has been enhanced to allow `lazy` loading of Access Tokens, and also made more
|
|
13
|
+
robust such that Access Tokens that fail to load will never prevent the client from
|
|
14
|
+
initialization.
|
|
15
|
+
|
|
16
|
+
### π Bug Fixes
|
|
17
|
+
|
|
18
|
+
* Prevent native browser context menu on Dash Canvas surfaces. It can hide the Dash Canvas custom
|
|
19
|
+
context menu when an app's `showBrowserContextMenu` flag is `true`.
|
|
20
|
+
|
|
21
|
+
## v72.1.0 - 2025-02-13
|
|
22
|
+
|
|
23
|
+
### π New Features
|
|
24
|
+
|
|
25
|
+
* Introduced a new "JSON Search" feature to the Hoist Admin Console, accessible from the Config,
|
|
26
|
+
User Preference, and JSON Blob tabs. Supports searching JSON values stored within these objects
|
|
27
|
+
to filter and match data using JSON Path expressions.
|
|
28
|
+
* β οΈRequires `hoist-core >= 28.1` with new APIs for this (optional) feature to function.
|
|
29
|
+
* Added new getters `StoreRecord.isDirty`, `Store.dirtyRecords`, and `Store.isDirty` to provide a
|
|
30
|
+
more consistent API in the data package. The pre-existing `isModified` getters are retained as
|
|
31
|
+
aliases, with the same semantics.
|
|
32
|
+
|
|
33
|
+
### π Bug Fixes
|
|
34
|
+
|
|
35
|
+
* Tuned mobile swipe handling to prevent horizontal swipes on a scrolling grid view from triggering
|
|
36
|
+
the Navigator's back gesture.
|
|
37
|
+
* Prevented the Admin Console Roles grid from losing its expand/collapse/scroll state on refresh.
|
|
38
|
+
* Fixed bug when merging `PersistOptions` with conflicting implicit provider types.
|
|
39
|
+
* Fixed bug where explicit `persistGrouping` options were not being respected by `GridModel`.
|
|
40
|
+
|
|
3
41
|
## v72.0.0 - 2025-01-27
|
|
4
42
|
|
|
5
|
-
### π₯ Breaking Changes
|
|
43
|
+
### π₯ Breaking Changes (upgrade difficulty: π’ TRIVIAL - minor changes to mobile nav)
|
|
6
44
|
|
|
7
45
|
* Mobile `Navigator` no longer supports `animation` prop, and `NavigatorModel` no longer supports
|
|
8
46
|
`swipeToGoBack`. Both of these properties are now managed internally by the `Navigator` component.
|
|
@@ -15,7 +53,8 @@
|
|
|
15
53
|
### π Bug Fixes
|
|
16
54
|
|
|
17
55
|
* Fixed `ViewManagerModel` unique name validation.
|
|
18
|
-
* Fixed `GridModel.restoreDefaultsAsync()` to restore any default filter, rather than simply
|
|
56
|
+
* Fixed `GridModel.restoreDefaultsAsync()` to restore any default filter, rather than simply
|
|
57
|
+
clearing it.
|
|
19
58
|
* Improved suboptimal column state synchronization between `GridModel` and AG Grid.
|
|
20
59
|
|
|
21
60
|
### βοΈ Technical
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright Β© 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {errorMessage} from '@xh/hoist/cmp/error';
|
|
9
|
+
import {grid, GridConfig, gridCountLabel} from '@xh/hoist/cmp/grid';
|
|
10
|
+
import {a, box, filler, fragment, hframe, label, li, p, span, ul, vbox} from '@xh/hoist/cmp/layout';
|
|
11
|
+
import {hoistCmp, HoistProps, SelectOption, useLocalModel} from '@xh/hoist/core';
|
|
12
|
+
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
13
|
+
import {clipboardButton} from '@xh/hoist/desktop/cmp/clipboard';
|
|
14
|
+
import {buttonGroupInput, jsonInput, select, textInput} from '@xh/hoist/desktop/cmp/input';
|
|
15
|
+
import {panel} from '@xh/hoist/desktop/cmp/panel';
|
|
16
|
+
import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
17
|
+
import {Icon} from '@xh/hoist/icon';
|
|
18
|
+
import {dialog, popover} from '@xh/hoist/kit/blueprint';
|
|
19
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
20
|
+
import {startCase} from 'lodash';
|
|
21
|
+
import {JsonSearchImplModel} from './impl/JsonSearchImplModel';
|
|
22
|
+
|
|
23
|
+
export interface JsonSearchButtonProps extends HoistProps {
|
|
24
|
+
/** Descriptive label for the type of records being searched - will be auto-pluralized. */
|
|
25
|
+
subjectName: string;
|
|
26
|
+
|
|
27
|
+
/** Endpoint to search and return matches - Hoist `JsonSearchController` action expected. */
|
|
28
|
+
docSearchUrl: string;
|
|
29
|
+
|
|
30
|
+
/** Config for GridModel used to display search results. */
|
|
31
|
+
gridModelConfig: GridConfig;
|
|
32
|
+
|
|
33
|
+
/** Field names on returned results to enable for grouping in the search results grid. */
|
|
34
|
+
groupByOptions: Array<SelectOption | any>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Main entry point component for the JSON search feature. Supported out-of-the-box for a limited
|
|
39
|
+
* set of Hoist artifacts that hold JSON values: JSONBlob, Configs, and User Preferences.
|
|
40
|
+
*/
|
|
41
|
+
export const jsonSearchButton = hoistCmp.factory<JsonSearchButtonProps>({
|
|
42
|
+
displayName: 'JsonSearchButton',
|
|
43
|
+
|
|
44
|
+
render() {
|
|
45
|
+
const impl = useLocalModel(JsonSearchImplModel);
|
|
46
|
+
|
|
47
|
+
return fragment(
|
|
48
|
+
button({
|
|
49
|
+
icon: Icon.json(),
|
|
50
|
+
text: 'JSON Search',
|
|
51
|
+
onClick: () => impl.toggleSearchIsOpen()
|
|
52
|
+
}),
|
|
53
|
+
jsonSearchDialog({
|
|
54
|
+
omit: !impl.isOpen,
|
|
55
|
+
model: impl
|
|
56
|
+
})
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const jsonSearchDialog = hoistCmp.factory<JsonSearchImplModel>({
|
|
62
|
+
displayName: 'JsonSearchDialog',
|
|
63
|
+
|
|
64
|
+
render({model}) {
|
|
65
|
+
const {error, subjectName} = model;
|
|
66
|
+
|
|
67
|
+
return dialog({
|
|
68
|
+
title: `JSON Search: ${subjectName}`,
|
|
69
|
+
style: {
|
|
70
|
+
width: '90vw',
|
|
71
|
+
height: '90vh'
|
|
72
|
+
},
|
|
73
|
+
icon: Icon.json(),
|
|
74
|
+
isOpen: true,
|
|
75
|
+
className: 'xh-admin-diff-detail',
|
|
76
|
+
onClose: () => model.toggleSearchIsOpen(),
|
|
77
|
+
item: panel({
|
|
78
|
+
tbar: searchTbar(),
|
|
79
|
+
item: panel({
|
|
80
|
+
mask: model.docLoadTask,
|
|
81
|
+
items: [
|
|
82
|
+
errorMessage({
|
|
83
|
+
error,
|
|
84
|
+
title: error?.name ? startCase(error.name) : undefined
|
|
85
|
+
}),
|
|
86
|
+
hframe({
|
|
87
|
+
omit: error,
|
|
88
|
+
items: [
|
|
89
|
+
panel({
|
|
90
|
+
item: grid({model: model.gridModel}),
|
|
91
|
+
modelConfig: {
|
|
92
|
+
side: 'left',
|
|
93
|
+
defaultSize: '30%',
|
|
94
|
+
collapsible: true,
|
|
95
|
+
defaultCollapsed: false,
|
|
96
|
+
resizable: true
|
|
97
|
+
}
|
|
98
|
+
}),
|
|
99
|
+
panel({
|
|
100
|
+
mask: model.nodeLoadTask,
|
|
101
|
+
tbar: readerTbar(),
|
|
102
|
+
item: jsonInput({
|
|
103
|
+
model,
|
|
104
|
+
bind: 'readerContent',
|
|
105
|
+
flex: 1,
|
|
106
|
+
width: '100%',
|
|
107
|
+
readonly: true,
|
|
108
|
+
showCopyButton: true
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
})
|
|
113
|
+
]
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const searchTbar = hoistCmp.factory<JsonSearchImplModel>({
|
|
121
|
+
render({model}) {
|
|
122
|
+
return toolbar(
|
|
123
|
+
pathField({model}),
|
|
124
|
+
button({
|
|
125
|
+
text: `Search ${model.subjectName}`,
|
|
126
|
+
intent: 'success',
|
|
127
|
+
outlined: true,
|
|
128
|
+
disabled: !model.path,
|
|
129
|
+
onClick: () => model.loadMatchingDocsAsync()
|
|
130
|
+
}),
|
|
131
|
+
'-',
|
|
132
|
+
helpButton({model}),
|
|
133
|
+
'-',
|
|
134
|
+
span('Group by:'),
|
|
135
|
+
select({
|
|
136
|
+
bind: 'groupBy',
|
|
137
|
+
options: model.groupByOptions,
|
|
138
|
+
width: 160,
|
|
139
|
+
enableFilter: false
|
|
140
|
+
}),
|
|
141
|
+
'-',
|
|
142
|
+
gridCountLabel({
|
|
143
|
+
gridModel: model.gridModel,
|
|
144
|
+
unit: 'match'
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const pathField = hoistCmp.factory<JsonSearchImplModel>({
|
|
151
|
+
render({model}) {
|
|
152
|
+
return textInput({
|
|
153
|
+
bind: 'path',
|
|
154
|
+
autoFocus: true,
|
|
155
|
+
commitOnChange: true,
|
|
156
|
+
leftIcon: Icon.search(),
|
|
157
|
+
enableClear: true,
|
|
158
|
+
placeholder: 'Provide a JSON Path expression to evaluate',
|
|
159
|
+
width: null,
|
|
160
|
+
flex: 1,
|
|
161
|
+
onKeyDown: e => {
|
|
162
|
+
if (e.key === 'Enter') model.loadMatchingDocsAsync();
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const helpButton = hoistCmp.factory<JsonSearchImplModel>({
|
|
169
|
+
render({model}) {
|
|
170
|
+
return popover({
|
|
171
|
+
item: button({
|
|
172
|
+
icon: Icon.questionCircle(),
|
|
173
|
+
outlined: true
|
|
174
|
+
}),
|
|
175
|
+
content: vbox({
|
|
176
|
+
className: 'xh-pad',
|
|
177
|
+
items: [
|
|
178
|
+
p(
|
|
179
|
+
`JSON Path expressions allow you to recursively query JSON documents, matching nodes based on their path, properties, and values.`
|
|
180
|
+
),
|
|
181
|
+
p(
|
|
182
|
+
`Enter a path and press [Enter] to search for matches within the JSON content of ${model.subjectName}.`
|
|
183
|
+
),
|
|
184
|
+
ul({
|
|
185
|
+
items: queryExamples.map(({query, explanation}) =>
|
|
186
|
+
li({
|
|
187
|
+
key: query,
|
|
188
|
+
items: [
|
|
189
|
+
span({
|
|
190
|
+
className:
|
|
191
|
+
'xh-border xh-pad-half xh-bg-alt xh-font-family-mono',
|
|
192
|
+
item: query
|
|
193
|
+
}),
|
|
194
|
+
' ',
|
|
195
|
+
clipboardButton({
|
|
196
|
+
text: null,
|
|
197
|
+
icon: Icon.copy(),
|
|
198
|
+
getCopyText: () => query,
|
|
199
|
+
successMessage: 'Query copied to clipboard.'
|
|
200
|
+
}),
|
|
201
|
+
' ',
|
|
202
|
+
explanation
|
|
203
|
+
]
|
|
204
|
+
})
|
|
205
|
+
),
|
|
206
|
+
style: {marginTop: 0}
|
|
207
|
+
}),
|
|
208
|
+
a({
|
|
209
|
+
href: 'https://github.com/json-path/JsonPath?tab=readme-ov-file#operators',
|
|
210
|
+
target: '_blank',
|
|
211
|
+
item: 'Path Syntax Docs & More Examples'
|
|
212
|
+
})
|
|
213
|
+
]
|
|
214
|
+
})
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const readerTbar = hoistCmp.factory<JsonSearchImplModel>(({model}) => {
|
|
220
|
+
return toolbar({
|
|
221
|
+
items: [
|
|
222
|
+
buttonGroupInput({
|
|
223
|
+
model,
|
|
224
|
+
bind: 'readerContentType',
|
|
225
|
+
minimal: true,
|
|
226
|
+
outlined: true,
|
|
227
|
+
disabled: !model.selectedRecord,
|
|
228
|
+
items: [
|
|
229
|
+
button({
|
|
230
|
+
text: 'Document',
|
|
231
|
+
value: 'document'
|
|
232
|
+
}),
|
|
233
|
+
button({
|
|
234
|
+
text: 'Matches',
|
|
235
|
+
value: 'matches'
|
|
236
|
+
})
|
|
237
|
+
]
|
|
238
|
+
}),
|
|
239
|
+
fragment({
|
|
240
|
+
omit: model.readerContentType !== 'matches' || !model.selectedRecord,
|
|
241
|
+
items: [
|
|
242
|
+
toolbarSep(),
|
|
243
|
+
label('View path as'),
|
|
244
|
+
buttonGroupInput({
|
|
245
|
+
model,
|
|
246
|
+
bind: 'pathFormat',
|
|
247
|
+
minimal: true,
|
|
248
|
+
outlined: true,
|
|
249
|
+
items: [
|
|
250
|
+
button({
|
|
251
|
+
text: 'XPath',
|
|
252
|
+
value: 'XPath'
|
|
253
|
+
}),
|
|
254
|
+
button({
|
|
255
|
+
text: 'JSONPath',
|
|
256
|
+
value: 'JSONPath'
|
|
257
|
+
})
|
|
258
|
+
]
|
|
259
|
+
})
|
|
260
|
+
]
|
|
261
|
+
}),
|
|
262
|
+
filler(),
|
|
263
|
+
box({
|
|
264
|
+
omit: !model.matchingNodeCount,
|
|
265
|
+
item: `${pluralize('match', model.matchingNodeCount, true)} within this document`
|
|
266
|
+
})
|
|
267
|
+
]
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const queryExamples = [
|
|
272
|
+
{
|
|
273
|
+
query: '$.displayMode',
|
|
274
|
+
explanation: 'Return documents with a top-level property "displayMode"'
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
query: "$..[?(@.colId == 'trader')]",
|
|
278
|
+
explanation:
|
|
279
|
+
'Find all nodes (anywhere in the document) with a property "colId" equal to "trader"'
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
query: '$..[?(@.colId && @.width)]',
|
|
283
|
+
explanation: 'Find all nodes with a property "colId" and a property "width"'
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
query: '$..[?(@.colId && @.hidden != true)]',
|
|
287
|
+
explanation:
|
|
288
|
+
'Find all nodes with a property "colId" and a property "hidden" not equal to true'
|
|
289
|
+
},
|
|
290
|
+
{
|
|
291
|
+
query: '$..grid[?(@.version == 1)]',
|
|
292
|
+
explanation: 'Find all nodes with a key of "grid" and a property "version" equal to 1'
|
|
293
|
+
}
|
|
294
|
+
];
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This file belongs to Hoist, an application development toolkit
|
|
3
|
+
* developed by Extremely Heavy Industries (www.xh.io | info@xh.io)
|
|
4
|
+
*
|
|
5
|
+
* Copyright Β© 2025 Extremely Heavy Industries Inc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {GridConfig, GridModel} from '@xh/hoist/cmp/grid';
|
|
9
|
+
import {HoistModel, managed, TaskObserver, XH} from '@xh/hoist/core';
|
|
10
|
+
import {action, bindable, makeObservable, observable} from '@xh/hoist/mobx';
|
|
11
|
+
import {pluralize} from '@xh/hoist/utils/js';
|
|
12
|
+
import {camelCase, isEmpty, zipWith} from 'lodash';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export class JsonSearchImplModel extends HoistModel {
|
|
18
|
+
override xhImpl = true;
|
|
19
|
+
|
|
20
|
+
private matchingNodesUrl = 'jsonSearch/getMatchingNodes';
|
|
21
|
+
|
|
22
|
+
@managed gridModel: GridModel;
|
|
23
|
+
@managed docLoadTask: TaskObserver = TaskObserver.trackLast();
|
|
24
|
+
@managed nodeLoadTask: TaskObserver = TaskObserver.trackLast();
|
|
25
|
+
|
|
26
|
+
@observable groupBy: string = null;
|
|
27
|
+
@observable isOpen: boolean = false;
|
|
28
|
+
|
|
29
|
+
@bindable.ref error = null;
|
|
30
|
+
@bindable path: string = '';
|
|
31
|
+
@bindable readerContentType: 'document' | 'matches' = 'matches';
|
|
32
|
+
@bindable pathFormat: 'XPath' | 'JSONPath' = 'XPath';
|
|
33
|
+
@bindable readerContent: string = '';
|
|
34
|
+
@bindable matchingNodeCount: number = 0;
|
|
35
|
+
|
|
36
|
+
get subjectName(): string {
|
|
37
|
+
return pluralize(this.componentProps.subjectName);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get docSearchUrl(): string {
|
|
41
|
+
return this.componentProps.docSearchUrl;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get gridModelConfig(): GridConfig {
|
|
45
|
+
return this.componentProps.gridModelConfig;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get selectedRecord() {
|
|
49
|
+
return this.gridModel.selectedRecord;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get groupByOptions() {
|
|
53
|
+
const cols = this.gridModel.getLeafColumns();
|
|
54
|
+
return [
|
|
55
|
+
...this.componentProps.groupByOptions.map(it => ({
|
|
56
|
+
value: it,
|
|
57
|
+
label: cols.find(col => col.colId === it)?.displayName ?? it
|
|
58
|
+
})),
|
|
59
|
+
{value: null, label: 'None'}
|
|
60
|
+
];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@action
|
|
64
|
+
toggleSearchIsOpen() {
|
|
65
|
+
this.isOpen = !this.isOpen;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
constructor() {
|
|
69
|
+
super();
|
|
70
|
+
makeObservable(this);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
override onLinked() {
|
|
74
|
+
this.gridModel = new GridModel({
|
|
75
|
+
...this.gridModelConfig,
|
|
76
|
+
emptyText: 'No matches found...',
|
|
77
|
+
selModel: 'single'
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
this.markPersist('path', {localStorageKey: `xhJsonSearch${camelCase(this.subjectName)}`});
|
|
81
|
+
|
|
82
|
+
this.addReaction(
|
|
83
|
+
{
|
|
84
|
+
track: () => this.path,
|
|
85
|
+
run: path => {
|
|
86
|
+
if (isEmpty(path)) {
|
|
87
|
+
this.error = null;
|
|
88
|
+
this.gridModel.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
track: () => [this.selectedRecord, this.readerContentType, this.pathFormat],
|
|
94
|
+
run: () => this.loadReaderContentAsync(),
|
|
95
|
+
debounce: 300
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
// We might have a persisted path - go ahead and load if so.
|
|
100
|
+
this.loadMatchingDocsAsync();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async loadMatchingDocsAsync() {
|
|
104
|
+
const {path, gridModel, docLoadTask} = this;
|
|
105
|
+
|
|
106
|
+
if (isEmpty(path)) {
|
|
107
|
+
this.error = null;
|
|
108
|
+
gridModel.clear();
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const data = await XH.fetchJson({
|
|
114
|
+
url: this.docSearchUrl,
|
|
115
|
+
params: {path}
|
|
116
|
+
}).linkTo(docLoadTask);
|
|
117
|
+
|
|
118
|
+
this.error = null;
|
|
119
|
+
gridModel.loadData(data);
|
|
120
|
+
gridModel.preSelectFirstAsync();
|
|
121
|
+
} catch (e) {
|
|
122
|
+
gridModel.clear();
|
|
123
|
+
this.error = e;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async loadReaderContentAsync() {
|
|
128
|
+
if (!this.selectedRecord) {
|
|
129
|
+
this.matchingNodeCount = 0;
|
|
130
|
+
this.readerContent = '';
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const {json} = this.selectedRecord.data;
|
|
135
|
+
|
|
136
|
+
if (this.readerContentType === 'document') {
|
|
137
|
+
this.readerContent = JSON.stringify(JSON.parse(json), null, 2);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
let nodes = await XH.fetchJson({
|
|
142
|
+
url: this.matchingNodesUrl,
|
|
143
|
+
params: {
|
|
144
|
+
path: this.path,
|
|
145
|
+
json
|
|
146
|
+
}
|
|
147
|
+
}).linkTo(this.nodeLoadTask);
|
|
148
|
+
|
|
149
|
+
this.matchingNodeCount = nodes.paths.length;
|
|
150
|
+
nodes = zipWith(nodes.paths, nodes.values, (path: string, value) => {
|
|
151
|
+
return {
|
|
152
|
+
path: this.pathFormat === 'XPath' ? this.convertToXPath(path) : path,
|
|
153
|
+
value
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
this.readerContent = JSON.stringify(nodes, null, 2);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private convertToXPath(JSONPath: string): string {
|
|
160
|
+
return JSONPath.replaceAll(/^\$\['?/g, '/')
|
|
161
|
+
.replaceAll(/^\$/g, '')
|
|
162
|
+
.replaceAll(/'?]\['?/g, '/')
|
|
163
|
+
.replaceAll(/'?]$/g, '');
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@action
|
|
167
|
+
private setGroupBy(groupBy: string) {
|
|
168
|
+
this.groupBy = groupBy;
|
|
169
|
+
|
|
170
|
+
// Always select first when regrouping.
|
|
171
|
+
const groupByArr = groupBy ? groupBy.split(',') : [];
|
|
172
|
+
this.gridModel.setGroupBy(groupByArr);
|
|
173
|
+
this.gridModel.preSelectFirstAsync();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -9,7 +9,7 @@ import * as Col from '@xh/hoist/admin/columns';
|
|
|
9
9
|
import {FilterChooserModel} from '@xh/hoist/cmp/filter';
|
|
10
10
|
import {FormModel} from '@xh/hoist/cmp/form';
|
|
11
11
|
import {GridModel} from '@xh/hoist/cmp/grid';
|
|
12
|
-
import {HoistModel, LoadSpec, managed, XH} from '@xh/hoist/core';
|
|
12
|
+
import {HoistModel, LoadSpec, managed, PlainObject, XH} from '@xh/hoist/core';
|
|
13
13
|
import {StoreRecord} from '@xh/hoist/data';
|
|
14
14
|
import {fmtJson} from '@xh/hoist/format';
|
|
15
15
|
import {action, bindable, comparer, computed, makeObservable, observable} from '@xh/hoist/mobx';
|
|
@@ -62,6 +62,14 @@ export class ClientErrorsModel extends HoistModel {
|
|
|
62
62
|
{...Col.appVersion},
|
|
63
63
|
{...Col.appEnvironment},
|
|
64
64
|
{...Col.msg, displayName: 'User Message', hidden},
|
|
65
|
+
{
|
|
66
|
+
field: {name: 'errorName', type: 'string'},
|
|
67
|
+
autosizeMaxWidth: 400
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
field: {name: 'errorMessage', type: 'string'},
|
|
71
|
+
autosizeMaxWidth: 400
|
|
72
|
+
},
|
|
65
73
|
{...Col.error, hidden},
|
|
66
74
|
{...Col.url},
|
|
67
75
|
{...Col.correlationId},
|
|
@@ -119,18 +127,29 @@ export class ClientErrorsModel extends HoistModel {
|
|
|
119
127
|
}
|
|
120
128
|
|
|
121
129
|
override async doLoadAsync(loadSpec: LoadSpec) {
|
|
122
|
-
const {gridModel} = this;
|
|
130
|
+
const {query, gridModel} = this;
|
|
123
131
|
|
|
124
132
|
try {
|
|
125
|
-
const data = await XH.fetchService.postJson({
|
|
133
|
+
const data: PlainObject[] = await XH.fetchService.postJson({
|
|
126
134
|
url: 'clientErrorAdmin',
|
|
127
|
-
body:
|
|
135
|
+
body: query,
|
|
128
136
|
loadSpec
|
|
129
137
|
});
|
|
130
138
|
|
|
139
|
+
// Parse name + message from JSON-serialized error object out to top-level properties.
|
|
140
|
+
data.forEach(it => {
|
|
141
|
+
try {
|
|
142
|
+
const error = JSON.parse(it.error);
|
|
143
|
+
it.errorName = error?.name;
|
|
144
|
+
it.errorMessage = error?.message;
|
|
145
|
+
} catch (ignored) {}
|
|
146
|
+
});
|
|
147
|
+
|
|
131
148
|
gridModel.loadData(data);
|
|
132
149
|
await gridModel.preSelectFirstAsync();
|
|
133
150
|
} catch (e) {
|
|
151
|
+
if (loadSpec.isStale || loadSpec.isAutoRefresh) return;
|
|
152
|
+
|
|
134
153
|
gridModel.clear();
|
|
135
154
|
XH.handleException(e);
|
|
136
155
|
}
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Copyright Β© 2025 Extremely Heavy Industries Inc.
|
|
6
6
|
*/
|
|
7
|
+
import * as AdminCol from '@xh/hoist/admin/columns';
|
|
8
|
+
import * as Col from '@xh/hoist/admin/columns/Rest';
|
|
9
|
+
import {jsonSearchButton} from '@xh/hoist/admin/jsonsearch/JsonSearch';
|
|
7
10
|
import {fragment} from '@xh/hoist/cmp/layout';
|
|
8
11
|
import {creates, hoistCmp} from '@xh/hoist/core';
|
|
9
12
|
import {button} from '@xh/hoist/desktop/cmp/button';
|
|
10
13
|
import {restGrid} from '@xh/hoist/desktop/cmp/rest';
|
|
14
|
+
import {toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
|
|
11
15
|
import {Icon} from '@xh/hoist/icon';
|
|
12
16
|
import {differ} from '../../../differ/Differ';
|
|
13
17
|
import {regroupDialog} from '../../../regroup/RegroupDialog';
|
|
@@ -20,13 +24,31 @@ export const configPanel = hoistCmp.factory({
|
|
|
20
24
|
return fragment(
|
|
21
25
|
restGrid({
|
|
22
26
|
testId: 'config',
|
|
23
|
-
extraToolbarItems: () =>
|
|
24
|
-
|
|
27
|
+
extraToolbarItems: () => [
|
|
28
|
+
button({
|
|
25
29
|
icon: Icon.diff(),
|
|
26
30
|
text: 'Compare w/ Remote',
|
|
27
31
|
onClick: () => model.openDiffer()
|
|
28
|
-
})
|
|
29
|
-
|
|
32
|
+
}),
|
|
33
|
+
toolbarSep(),
|
|
34
|
+
jsonSearchButton({
|
|
35
|
+
subjectName: 'Config',
|
|
36
|
+
docSearchUrl: 'jsonSearch/searchConfigs',
|
|
37
|
+
gridModelConfig: {
|
|
38
|
+
sortBy: ['groupName', 'name'],
|
|
39
|
+
columns: [
|
|
40
|
+
{...AdminCol.groupName},
|
|
41
|
+
{...AdminCol.name},
|
|
42
|
+
{
|
|
43
|
+
field: {name: 'json', type: 'string'},
|
|
44
|
+
hidden: true
|
|
45
|
+
},
|
|
46
|
+
{...Col.lastUpdated}
|
|
47
|
+
]
|
|
48
|
+
},
|
|
49
|
+
groupByOptions: ['groupName']
|
|
50
|
+
})
|
|
51
|
+
]
|
|
30
52
|
}),
|
|
31
53
|
differ({omit: !model.differModel}),
|
|
32
54
|
regroupDialog()
|