@xh/hoist 73.0.0-SNAPSHOT.1745976013413 → 73.0.0-SNAPSHOT.1746050068813

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +10 -1
  2. package/admin/AdminUtils.ts +5 -0
  3. package/admin/App.scss +6 -0
  4. package/admin/AppModel.ts +19 -7
  5. package/admin/{tabs/client/clients/ClientsColumns.ts → columns/Clients.ts} +20 -53
  6. package/admin/columns/Core.ts +34 -35
  7. package/admin/columns/Rest.ts +8 -0
  8. package/admin/columns/Tracking.ts +144 -42
  9. package/admin/columns/index.ts +1 -0
  10. package/admin/tabs/activity/tracking/ActivityTracking.scss +18 -0
  11. package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +309 -210
  12. package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +81 -51
  13. package/admin/tabs/activity/tracking/chart/AggChartModel.ts +218 -0
  14. package/admin/tabs/activity/tracking/chart/AggChartPanel.ts +61 -0
  15. package/admin/tabs/activity/tracking/datafields/DataFieldsEditor.ts +147 -0
  16. package/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.ts +133 -0
  17. package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +123 -59
  18. package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +110 -54
  19. package/admin/tabs/client/ClientTab.ts +2 -4
  20. package/admin/tabs/client/clients/ClientsModel.ts +10 -11
  21. package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -2
  22. package/admin/tabs/general/GeneralTab.ts +2 -0
  23. package/build/types/admin/AdminUtils.d.ts +2 -0
  24. package/build/types/admin/AppModel.d.ts +4 -1
  25. package/build/types/admin/{tabs/client/clients/ClientsColumns.d.ts → columns/Clients.d.ts} +3 -7
  26. package/build/types/admin/columns/Core.d.ts +5 -5
  27. package/build/types/admin/columns/Rest.d.ts +1 -0
  28. package/build/types/admin/columns/Tracking.d.ts +13 -4
  29. package/build/types/admin/columns/index.d.ts +1 -0
  30. package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +31 -28
  31. package/build/types/admin/tabs/activity/tracking/chart/AggChartModel.d.ts +33 -0
  32. package/build/types/admin/tabs/activity/tracking/chart/AggChartPanel.d.ts +2 -0
  33. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditor.d.ts +2 -0
  34. package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.d.ts +46 -0
  35. package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +14 -1
  36. package/build/types/cmp/form/FormModel.d.ts +19 -30
  37. package/build/types/cmp/form/field/SubformsFieldModel.d.ts +25 -22
  38. package/build/types/core/HoistBase.d.ts +2 -2
  39. package/build/types/data/cube/CubeField.d.ts +4 -5
  40. package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
  41. package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
  42. package/cmp/error/ErrorBoundaryModel.ts +1 -1
  43. package/cmp/form/FormModel.ts +20 -28
  44. package/cmp/form/field/SubformsFieldModel.ts +28 -22
  45. package/cmp/grid/columns/DatesTimes.ts +1 -2
  46. package/cmp/grid/impl/GridHScrollbar.ts +1 -2
  47. package/core/HoistBase.ts +12 -12
  48. package/data/cube/CubeField.ts +17 -18
  49. package/package.json +1 -1
  50. package/svc/TrackService.ts +2 -0
  51. package/tsconfig.tsbuildinfo +1 -1
  52. package/admin/tabs/activity/tracking/charts/ChartsModel.ts +0 -218
  53. package/admin/tabs/activity/tracking/charts/ChartsPanel.ts +0 -76
  54. package/build/types/admin/tabs/activity/tracking/charts/ChartsModel.d.ts +0 -34
  55. package/build/types/admin/tabs/activity/tracking/charts/ChartsPanel.d.ts +0 -2
  56. /package/admin/tabs/{client → general}/feedback/FeedbackPanel.ts +0 -0
  57. /package/build/types/admin/tabs/{client → general}/feedback/FeedbackPanel.d.ts +0 -0
@@ -4,22 +4,24 @@
4
4
  *
5
5
  * Copyright © 2025 Extremely Heavy Industries Inc.
6
6
  */
7
+ import {dataFieldsEditor} from '@xh/hoist/admin/tabs/activity/tracking/datafields/DataFieldsEditor';
8
+ import {errorMessage} from '@xh/hoist/cmp/error';
7
9
  import {form} from '@xh/hoist/cmp/form';
8
10
  import {grid} from '@xh/hoist/cmp/grid';
9
- import {div, hframe} from '@xh/hoist/cmp/layout';
11
+ import {div, filler, hframe} from '@xh/hoist/cmp/layout';
10
12
  import {creates, hoistCmp} from '@xh/hoist/core';
11
13
  import {button, buttonGroup, colChooserButton, exportButton} from '@xh/hoist/desktop/cmp/button';
12
- import {errorMessage} from '@xh/hoist/cmp/error';
13
14
  import {filterChooser} from '@xh/hoist/desktop/cmp/filter';
14
15
  import {formField} from '@xh/hoist/desktop/cmp/form';
15
16
  import {groupingChooser} from '@xh/hoist/desktop/cmp/grouping';
16
17
  import {dateInput, DateInputProps, select} from '@xh/hoist/desktop/cmp/input';
17
18
  import {panel} from '@xh/hoist/desktop/cmp/panel';
18
19
  import {toolbar, toolbarSep} from '@xh/hoist/desktop/cmp/toolbar';
20
+ import {viewManager} from '@xh/hoist/desktop/cmp/viewmanager';
19
21
  import {Icon} from '@xh/hoist/icon';
20
22
  import {LocalDate} from '@xh/hoist/utils/datetime';
21
23
  import {ActivityTrackingModel} from './ActivityTrackingModel';
22
- import {chartsPanel} from './charts/ChartsPanel';
24
+ import {aggChartPanel} from '@xh/hoist/admin/tabs/activity/tracking/chart/AggChartPanel';
23
25
  import {activityDetailView} from './detail/ActivityDetailView';
24
26
  import './ActivityTracking.scss';
25
27
 
@@ -36,14 +38,20 @@ export const activityTrackingPanel = hoistCmp.factory({
36
38
  return panel({
37
39
  className: 'xh-admin-activity-panel',
38
40
  tbar: tbar(),
39
- item: hframe(aggregateView(), activityDetailView({flex: 1})),
41
+ items: [filterBar(), hframe(aggregateView(), activityDetailView({flex: 1}))],
40
42
  mask: 'onLoad'
41
43
  });
42
44
  }
43
45
  });
44
46
 
45
47
  const tbar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
48
+ const dateBtn = {outlined: true, width: 40} as const;
46
49
  return toolbar(
50
+ viewManager({
51
+ model: model.viewManagerModel,
52
+ showSaveButton: 'always'
53
+ }),
54
+ '-',
47
55
  form({
48
56
  fieldDefaults: {label: null},
49
57
  items: [
@@ -65,74 +73,96 @@ const tbar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
65
73
  onClick: () => model.adjustDates('add'),
66
74
  disabled: model.endDay >= LocalDate.currentAppDay()
67
75
  }),
68
- buttonGroup(
69
- button({
70
- text: '6m',
71
- outlined: true,
72
- width: 40,
73
- onClick: () => model.adjustStartDate(6, 'months'),
74
- active: model.isInterval(6, 'months')
75
- }),
76
- button({
77
- text: '1m',
78
- outlined: true,
79
- width: 40,
80
- onClick: () => model.adjustStartDate(1, 'months'),
81
- active: model.isInterval(1, 'months')
82
- }),
83
- button({
84
- text: '7d',
85
- outlined: true,
86
- width: 40,
87
- onClick: () => model.adjustStartDate(7, 'days'),
88
- active: model.isInterval(7, 'days')
89
- }),
90
- button({
91
- text: '1d',
92
- outlined: true,
93
- width: 40,
94
- onClick: () => model.adjustStartDate(1, 'days'),
95
- active: model.isInterval(1, 'days')
96
- })
97
- ),
98
- toolbarSep(),
99
- filterChooser({
100
- flex: 1,
101
- enableClear: true
76
+ buttonGroup({
77
+ items: [
78
+ button({
79
+ text: '6m',
80
+ onClick: () => model.adjustStartDate(6, 'months'),
81
+ active: model.isInterval(6, 'months'),
82
+ ...dateBtn
83
+ }),
84
+ button({
85
+ text: '1m',
86
+ onClick: () => model.adjustStartDate(1, 'months'),
87
+ active: model.isInterval(1, 'months'),
88
+ ...dateBtn
89
+ }),
90
+ button({
91
+ text: '7d',
92
+ onClick: () => model.adjustStartDate(7, 'days'),
93
+ active: model.isInterval(7, 'days'),
94
+ ...dateBtn
95
+ }),
96
+ button({
97
+ text: '1d',
98
+ onClick: () => model.adjustStartDate(1, 'days'),
99
+ active: model.isInterval(1, 'days'),
100
+ ...dateBtn
101
+ })
102
+ ]
102
103
  }),
103
104
  toolbarSep(),
105
+ filterChooserToggleButton(),
106
+ toolbarSep(),
107
+ dataFieldsEditor(),
108
+ filler(),
104
109
  formField({
105
110
  field: 'maxRows',
106
- label: 'Max rows:',
111
+ label: 'Max rows',
107
112
  width: 140,
108
113
  item: select({
109
114
  enableFilter: false,
110
115
  hideDropdownIndicator: true,
111
116
  options: model.maxRowOptions
112
117
  })
113
- }),
114
- toolbarSep(),
115
- button({
116
- icon: Icon.reset(),
117
- intent: 'danger',
118
- title: 'Reset query to defaults',
119
- onClick: () => model.resetQuery()
120
118
  })
121
119
  ]
122
120
  })
123
121
  );
124
122
  });
125
123
 
124
+ const filterChooserToggleButton = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
125
+ const {hasFilter, showFilterChooser} = model;
126
+
127
+ return button({
128
+ text: 'Filter',
129
+ icon: Icon.filter({prefix: hasFilter ? 'fas' : 'far'}),
130
+ intent: hasFilter ? 'warning' : null,
131
+ outlined: showFilterChooser,
132
+ onClick: () => model.toggleFilterChooser()
133
+ });
134
+ });
135
+
136
+ const filterBar = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
137
+ return model.showFilterChooser
138
+ ? toolbar(
139
+ filterChooser({
140
+ flex: 1,
141
+ enableClear: true
142
+ })
143
+ )
144
+ : null;
145
+ });
146
+
126
147
  const aggregateView = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
127
148
  return panel({
128
- title: 'Aggregate Activity Report',
129
- icon: Icon.users(),
149
+ collapsedTitle: 'Aggregate Activity',
150
+ collapsedIcon: Icon.users(),
130
151
  compactHeader: true,
131
152
  modelConfig: {
132
153
  side: 'left',
133
- defaultSize: 500
154
+ defaultSize: 500,
155
+ persistWith: {...model.persistWith, path: 'aggPanel'}
134
156
  },
135
- tbar: [groupingChooser({flex: 1}), colChooserButton(), exportButton()],
157
+ tbar: toolbar({
158
+ // compact: true,
159
+ items: [
160
+ groupingChooser({flex: 10, maxWidth: 300}),
161
+ filler(),
162
+ colChooserButton(),
163
+ exportButton()
164
+ ]
165
+ }),
136
166
  items: [
137
167
  grid({
138
168
  flex: 1,
@@ -146,7 +176,7 @@ const aggregateView = hoistCmp.factory<ActivityTrackingModel>(({model}) => {
146
176
  ],
147
177
  omit: !model.maxRowsReached
148
178
  }),
149
- chartsPanel()
179
+ aggChartPanel()
150
180
  ]
151
181
  });
152
182
  });
@@ -0,0 +1,218 @@
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
+ import {ChartModel} from '@xh/hoist/cmp/chart';
8
+ import {HoistModel, lookup, managed, SelectOption} from '@xh/hoist/core';
9
+ import {Cube, StoreRecord} from '@xh/hoist/data';
10
+ import {bindable, computed, makeObservable} from '@xh/hoist/mobx';
11
+ import {LocalDate} from '@xh/hoist/utils/datetime';
12
+ import {pluralize} from '@xh/hoist/utils/js';
13
+ import {isEmpty, last, sortBy} from 'lodash';
14
+ import moment from 'moment';
15
+ import {ActivityTrackingModel} from '../ActivityTrackingModel';
16
+
17
+ export class AggChartModel extends HoistModel {
18
+ @lookup(ActivityTrackingModel)
19
+ activityTrackingModel: ActivityTrackingModel;
20
+
21
+ /**
22
+ * Metric to chart on Y axis - one of:
23
+ * - entryCount - count of total track log entries within the primary dim group.
24
+ * - count - count of unique secondary dim values within the primary dim group.
25
+ * - elapsed - avg elapsed time in ms for the primary dim group.
26
+ * - any other numeric, aggregated custom data field metrics, if so configured
27
+ */
28
+ @bindable metric: string = 'entryCount';
29
+
30
+ @computed
31
+ get metricLabel() {
32
+ return this.selectableMetrics.find(it => it.value === this.metric)?.label ?? this.metric;
33
+ }
34
+
35
+ @bindable incWeekends: boolean = true;
36
+
37
+ @managed chartModel: ChartModel;
38
+
39
+ get showAsTimeseries(): boolean {
40
+ return this.primaryDim === 'day';
41
+ }
42
+
43
+ get selectableMetrics(): SelectOption[] {
44
+ const {activityTrackingModel, secondaryDim, secondaryDimLabel} = this;
45
+ if (!activityTrackingModel) return [];
46
+
47
+ const ret: SelectOption[] = [
48
+ {
49
+ label: 'Entries [count]',
50
+ value: 'entryCount'
51
+ },
52
+ {
53
+ label: 'Elapsed ms [avg]',
54
+ value: 'elapsed'
55
+ }
56
+ ];
57
+
58
+ if (secondaryDim) {
59
+ ret.push({
60
+ label: `Unique ${pluralize(secondaryDimLabel)} [count]`,
61
+ value: 'count'
62
+ });
63
+ }
64
+
65
+ const dfMetrics = activityTrackingModel.dataFields.filter(
66
+ it => (it.type === 'int' || it.type === 'number') && it.aggregator
67
+ );
68
+
69
+ dfMetrics.forEach(it => {
70
+ ret.push({
71
+ label: `${it.displayName} [${it.aggregator.toLowerCase()}]`,
72
+ value: it.name
73
+ });
74
+ });
75
+
76
+ return sortBy(ret, 'label');
77
+ }
78
+
79
+ constructor() {
80
+ super();
81
+ makeObservable(this);
82
+ }
83
+
84
+ override onLinked() {
85
+ this.chartModel = this.createChartModel();
86
+
87
+ const {persistWith} = this.activityTrackingModel;
88
+ this.markPersist('metric', {...persistWith, path: 'chartMetric'});
89
+ this.markPersist('incWeekends', {...persistWith, path: 'chartIncWeekends'});
90
+
91
+ this.addReaction({
92
+ track: () => [this.data, this.metric, this.incWeekends],
93
+ run: () => this.loadChart()
94
+ });
95
+ }
96
+
97
+ //-----------------
98
+ // Implementation
99
+ //-----------------
100
+ private get cube(): Cube {
101
+ return this.activityTrackingModel?.cube;
102
+ }
103
+
104
+ private get dimensions() {
105
+ return this.activityTrackingModel.dimensions;
106
+ }
107
+
108
+ private get primaryDim(): string {
109
+ return this.dimensions[0];
110
+ }
111
+
112
+ private get primaryDimLabel(): string {
113
+ return this.getDisplayName(this.primaryDim);
114
+ }
115
+
116
+ private get secondaryDim(): string {
117
+ const {dimensions} = this;
118
+ return dimensions.length >= 2 ? dimensions[1] : null;
119
+ }
120
+
121
+ private get secondaryDimLabel(): string {
122
+ return this.getDisplayName(this.secondaryDim);
123
+ }
124
+
125
+ private get data() {
126
+ const roots = this.activityTrackingModel.gridModel.store.allRootRecords;
127
+ return roots.length ? roots[0].children : [];
128
+ }
129
+
130
+ private createChartModel(): ChartModel {
131
+ return new ChartModel({
132
+ highchartsConfig: {
133
+ chart: {type: 'column', animation: false},
134
+ plotOptions: {
135
+ column: {
136
+ animation: false,
137
+ borderWidth: 0,
138
+ events: {
139
+ click: e => this.selectRow(e)
140
+ }
141
+ }
142
+ },
143
+ legend: {enabled: false},
144
+ title: {text: null},
145
+ xAxis: {type: 'category', title: {}},
146
+ yAxis: [{title: {text: null}, allowDecimals: false}]
147
+ }
148
+ });
149
+ }
150
+
151
+ private selectRow(e) {
152
+ const id = `root>>${this.primaryDim}=[${e.point.name}]`;
153
+ this.activityTrackingModel.gridModel.selectAsync(id);
154
+ }
155
+
156
+ private loadChart() {
157
+ const {primaryDim, chartModel, primaryDimLabel} = this,
158
+ xAxisTitle = ['day', 'month'].includes(primaryDim) ? null : primaryDimLabel,
159
+ series = this.getSeriesData();
160
+
161
+ chartModel.setSeries(series);
162
+ chartModel.updateHighchartsConfig({
163
+ xAxis: {title: {text: xAxisTitle}}
164
+ });
165
+ }
166
+
167
+ private getSeriesData() {
168
+ const {data, metric, metricLabel, primaryDim, showAsTimeseries, incWeekends} = this,
169
+ sortedData = sortBy(data, aggRow => {
170
+ const {cubeLabel} = aggRow.data;
171
+ switch (primaryDim) {
172
+ case 'month':
173
+ return moment(cubeLabel, 'MMM YYYY').valueOf();
174
+ default:
175
+ return cubeLabel;
176
+ }
177
+ }),
178
+ chartData = [];
179
+
180
+ // Early out if no data.
181
+ if (isEmpty(sortedData)) {
182
+ return [{metric: metricLabel, data: chartData}];
183
+ }
184
+
185
+ // Special handling for timeseries - pad series internally so we can use a category axis
186
+ // with option to skip weekends, while retaining relative spacing between included days.
187
+ if (showAsTimeseries) {
188
+ // Index data we do have by day, for quick retrieval below.
189
+ const byDay: Record<string, StoreRecord> = {};
190
+ sortedData.forEach(it => {
191
+ byDay[it.data.cubeLabel] = it;
192
+ });
193
+
194
+ // Walk from first to last day, ensuring we have a point or placeholder for each one.
195
+ let dataDay = LocalDate.get(sortedData[0].data.cubeLabel);
196
+ const lastDay = LocalDate.get(last(sortedData).data.cubeLabel);
197
+ while (dataDay <= lastDay) {
198
+ if (incWeekends || dataDay.isWeekday) {
199
+ const xVal = dataDay.toString(),
200
+ yVal = byDay[xVal]?.data[metric] ?? null;
201
+ chartData.push([xVal, Math.round(yVal)]);
202
+ }
203
+
204
+ dataDay = dataDay.nextDay();
205
+ }
206
+ } else {
207
+ sortedData.forEach(it => {
208
+ chartData.push([it.data.cubeLabel, Math.round(it.data[metric])]);
209
+ });
210
+ }
211
+
212
+ return [{name: metricLabel, data: chartData}];
213
+ }
214
+
215
+ private getDisplayName(fieldName: string) {
216
+ return this.activityTrackingModel?.getDisplayName(fieldName) ?? fieldName;
217
+ }
218
+ }
@@ -0,0 +1,61 @@
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
+ import {chart} from '@xh/hoist/cmp/chart';
8
+ import {creates, hoistCmp} from '@xh/hoist/core';
9
+ import {modalToggleButton} from '@xh/hoist/desktop/cmp/button';
10
+ import {checkbox, select} from '@xh/hoist/desktop/cmp/input';
11
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
12
+ import {toolbar} from '@xh/hoist/desktop/cmp/toolbar';
13
+ import {Icon} from '@xh/hoist/icon/Icon';
14
+ import {AggChartModel} from './AggChartModel';
15
+
16
+ export const aggChartPanel = hoistCmp.factory({
17
+ model: creates(AggChartModel),
18
+
19
+ render({model, ...props}) {
20
+ return panel({
21
+ collapsedTitle: 'Aggregate Activity Chart',
22
+ collapsedIcon: Icon.chartBar(),
23
+ modelConfig: {
24
+ modalSupport: {width: '90vw', height: '60vh'},
25
+ side: 'bottom',
26
+ defaultSize: 400,
27
+ persistWith: {...model.activityTrackingModel.persistWith, path: 'aggChartPanel'}
28
+ },
29
+ compactHeader: true,
30
+ item: chart({model: model.chartModel}),
31
+ bbar: toolbar({
32
+ items: [
33
+ Icon.chartBar(),
34
+ metricSwitcher(),
35
+ incWeekendsCheckbox(),
36
+ '-',
37
+ modalToggleButton()
38
+ ]
39
+ }),
40
+ ...props
41
+ });
42
+ }
43
+ });
44
+
45
+ const metricSwitcher = hoistCmp.factory<AggChartModel>(({model}) => {
46
+ return select({
47
+ bind: 'metric',
48
+ options: model.selectableMetrics,
49
+ enableFilter: false,
50
+ flex: 1
51
+ });
52
+ });
53
+
54
+ const incWeekendsCheckbox = hoistCmp.factory<AggChartModel>(({model}) =>
55
+ checkbox({
56
+ omit: !model.showAsTimeseries,
57
+ bind: 'incWeekends',
58
+ label: 'Weekends',
59
+ style: {marginLeft: '10px'}
60
+ })
61
+ );
@@ -0,0 +1,147 @@
1
+ import {DataFieldsEditorModel} from '@xh/hoist/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel';
2
+ import {form, FormModel} from '@xh/hoist/cmp/form';
3
+ import {br, filler, hbox, hspacer, placeholder, span, vspacer} from '@xh/hoist/cmp/layout';
4
+ import {hoistCmp, uses} from '@xh/hoist/core';
5
+ import {FieldType} from '@xh/hoist/data';
6
+ import {button} from '@xh/hoist/desktop/cmp/button';
7
+ import {formField} from '@xh/hoist/desktop/cmp/form';
8
+ import {checkbox, select, textInput} from '@xh/hoist/desktop/cmp/input';
9
+ import {panel} from '@xh/hoist/desktop/cmp/panel';
10
+ import {Icon} from '@xh/hoist/icon';
11
+ import {popover} from '@xh/hoist/kit/blueprint';
12
+ import {isEmpty} from 'lodash';
13
+
14
+ export const dataFieldsEditor = hoistCmp.factory({
15
+ model: uses(DataFieldsEditorModel),
16
+
17
+ render({model}) {
18
+ const {showEditor, appliedDataFieldCount, hasAppliedDataFields} = model;
19
+
20
+ return popover({
21
+ isOpen: showEditor,
22
+ item: button({
23
+ text: `Extract Data Fields${appliedDataFieldCount ? ' (' + appliedDataFieldCount + ')' : ''}`,
24
+ icon: Icon.json({prefix: hasAppliedDataFields ? 'fas' : 'far'}),
25
+ intent: hasAppliedDataFields ? 'primary' : null,
26
+ outlined: showEditor,
27
+ onClick: () => model.show()
28
+ }),
29
+ content: formPanel(),
30
+ popoverClassName: 'xh-popup xh-popup--framed',
31
+ onClose: () => model.close()
32
+ });
33
+ }
34
+ });
35
+
36
+ const formPanel = hoistCmp.factory<DataFieldsEditorModel>(({model}) => {
37
+ const {formModel, dataFields} = model;
38
+ return panel({
39
+ className: 'xh-admin-activity-panel__data-fields-editor',
40
+ item: form({
41
+ model: formModel,
42
+ items: isEmpty(dataFields.value)
43
+ ? emptyPlaceholder()
44
+ : [
45
+ ...dataFields.value.map((dfModel: FormModel) => {
46
+ return form({
47
+ model: dfModel,
48
+ fieldDefaults: {label: null, commitOnChange: true},
49
+ item: hbox({
50
+ className: 'xh-admin-activity-panel__data-fields-editor__row',
51
+ alignItems: 'flex-start',
52
+ flex: 'none',
53
+ items: [
54
+ formField({
55
+ field: 'path',
56
+ flex: 1,
57
+ item: textInput({placeholder: 'Path (dot-delimited)'})
58
+ }),
59
+ formField({
60
+ field: 'displayName',
61
+ width: 180,
62
+ item: textInput({placeholder: 'Display Name'})
63
+ }),
64
+ formField({
65
+ field: 'type',
66
+ width: 120,
67
+ item: select({
68
+ placeholder: 'Data Type',
69
+ options: Object.values(FieldType).sort()
70
+ })
71
+ }),
72
+ formField({
73
+ field: 'aggregator',
74
+ width: 120,
75
+ item: select({
76
+ placeholder: 'Aggregator',
77
+ // TODO - cascade select with type?
78
+ options: model.aggTokens,
79
+ enableClear: true
80
+ })
81
+ }),
82
+ formField({
83
+ field: 'isDimension',
84
+ marginTop: 8,
85
+ item: checkbox({label: 'dimension'})
86
+ }),
87
+ button({
88
+ icon: Icon.copy(),
89
+ marginTop: 3,
90
+ marginRight: 4,
91
+ onClick: () => model.cloneField(dfModel)
92
+ }),
93
+ button({
94
+ icon: Icon.delete(),
95
+ intent: 'danger',
96
+ marginTop: 3,
97
+ marginRight: 4,
98
+ onClick: () => dataFields.remove(dfModel)
99
+ })
100
+ ]
101
+ })
102
+ });
103
+ }),
104
+ hbox(filler(), addButton(), filler()),
105
+ filler({minHeight: 10})
106
+ ]
107
+ }),
108
+ bbar: [
109
+ filler(),
110
+ button({
111
+ text: 'Cancel',
112
+ onClick: () => model.close()
113
+ }),
114
+ hspacer(5),
115
+ button({
116
+ text: 'Apply + Reload',
117
+ icon: Icon.check(),
118
+ outlined: true,
119
+ intent: 'success',
120
+ disabled: !formModel.isValid,
121
+ onClick: () => model.applyAndClose()
122
+ })
123
+ ]
124
+ });
125
+ });
126
+
127
+ const emptyPlaceholder = hoistCmp.factory<DataFieldsEditorModel>(({model}) => {
128
+ return placeholder(
129
+ span(
130
+ 'Define fields to extract from the optional data payload on each activity record.',
131
+ br(),
132
+ 'Extracted data can then be viewed on both aggregate and detail levels.'
133
+ ),
134
+ vspacer(),
135
+ addButton()
136
+ );
137
+ });
138
+
139
+ const addButton = hoistCmp.factory<DataFieldsEditorModel>(({model}) => {
140
+ return button({
141
+ text: 'Add field...',
142
+ icon: Icon.add(),
143
+ intent: 'primary',
144
+ outlined: true,
145
+ onClick: () => model.addField()
146
+ });
147
+ });