@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.
- package/CHANGELOG.md +10 -1
- package/admin/AdminUtils.ts +5 -0
- package/admin/App.scss +6 -0
- package/admin/AppModel.ts +19 -7
- package/admin/{tabs/client/clients/ClientsColumns.ts → columns/Clients.ts} +20 -53
- package/admin/columns/Core.ts +34 -35
- package/admin/columns/Rest.ts +8 -0
- package/admin/columns/Tracking.ts +144 -42
- package/admin/columns/index.ts +1 -0
- package/admin/tabs/activity/tracking/ActivityTracking.scss +18 -0
- package/admin/tabs/activity/tracking/ActivityTrackingModel.ts +309 -210
- package/admin/tabs/activity/tracking/ActivityTrackingPanel.ts +81 -51
- package/admin/tabs/activity/tracking/chart/AggChartModel.ts +218 -0
- package/admin/tabs/activity/tracking/chart/AggChartPanel.ts +61 -0
- package/admin/tabs/activity/tracking/datafields/DataFieldsEditor.ts +147 -0
- package/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.ts +133 -0
- package/admin/tabs/activity/tracking/detail/ActivityDetailModel.ts +123 -59
- package/admin/tabs/activity/tracking/detail/ActivityDetailView.ts +110 -54
- package/admin/tabs/client/ClientTab.ts +2 -4
- package/admin/tabs/client/clients/ClientsModel.ts +10 -11
- package/admin/tabs/cluster/instances/memory/MemoryMonitorModel.ts +1 -2
- package/admin/tabs/general/GeneralTab.ts +2 -0
- package/build/types/admin/AdminUtils.d.ts +2 -0
- package/build/types/admin/AppModel.d.ts +4 -1
- package/build/types/admin/{tabs/client/clients/ClientsColumns.d.ts → columns/Clients.d.ts} +3 -7
- package/build/types/admin/columns/Core.d.ts +5 -5
- package/build/types/admin/columns/Rest.d.ts +1 -0
- package/build/types/admin/columns/Tracking.d.ts +13 -4
- package/build/types/admin/columns/index.d.ts +1 -0
- package/build/types/admin/tabs/activity/tracking/ActivityTrackingModel.d.ts +31 -28
- package/build/types/admin/tabs/activity/tracking/chart/AggChartModel.d.ts +33 -0
- package/build/types/admin/tabs/activity/tracking/chart/AggChartPanel.d.ts +2 -0
- package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditor.d.ts +2 -0
- package/build/types/admin/tabs/activity/tracking/datafields/DataFieldsEditorModel.d.ts +46 -0
- package/build/types/admin/tabs/activity/tracking/detail/ActivityDetailModel.d.ts +14 -1
- package/build/types/cmp/form/FormModel.d.ts +19 -30
- package/build/types/cmp/form/field/SubformsFieldModel.d.ts +25 -22
- package/build/types/core/HoistBase.d.ts +2 -2
- package/build/types/data/cube/CubeField.d.ts +4 -5
- package/build/types/desktop/cmp/appOption/AutoRefreshAppOption.d.ts +3 -3
- package/build/types/desktop/cmp/appOption/ThemeAppOption.d.ts +3 -3
- package/cmp/error/ErrorBoundaryModel.ts +1 -1
- package/cmp/form/FormModel.ts +20 -28
- package/cmp/form/field/SubformsFieldModel.ts +28 -22
- package/cmp/grid/columns/DatesTimes.ts +1 -2
- package/cmp/grid/impl/GridHScrollbar.ts +1 -2
- package/core/HoistBase.ts +12 -12
- package/data/cube/CubeField.ts +17 -18
- package/package.json +1 -1
- package/svc/TrackService.ts +2 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/admin/tabs/activity/tracking/charts/ChartsModel.ts +0 -218
- package/admin/tabs/activity/tracking/charts/ChartsPanel.ts +0 -76
- package/build/types/admin/tabs/activity/tracking/charts/ChartsModel.d.ts +0 -34
- package/build/types/admin/tabs/activity/tracking/charts/ChartsPanel.d.ts +0 -2
- /package/admin/tabs/{client → general}/feedback/FeedbackPanel.ts +0 -0
- /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 {
|
|
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
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
129
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
+
});
|