@c8y/tutorial 1023.53.0 → 1023.57.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/cumulocity.config.ts +7 -0
- package/package.json +7 -7
- package/src/__mocks/global-mocks/measurements.interceptor.ts +70 -0
- package/src/c8y-charts/charts-example.component.html +206 -0
- package/src/c8y-charts/charts-example.component.ts +379 -0
- package/src/c8y-charts/charts-example.module.ts +20 -0
- package/src/hooks/service/counter/counter.model.ts +2 -0
- package/src/modal/confirm-modal/confirm-modal-example.component.ts +26 -0
package/cumulocity.config.ts
CHANGED
|
@@ -892,6 +892,13 @@ export default {
|
|
|
892
892
|
description: 'This is an example for the countdown component.',
|
|
893
893
|
scope: 'self'
|
|
894
894
|
},
|
|
895
|
+
{
|
|
896
|
+
name: 'Charts',
|
|
897
|
+
module: 'ChartsExampleModule',
|
|
898
|
+
path: './src/c8y-charts/charts-example.module.ts',
|
|
899
|
+
description: 'This is an example for the charts component.',
|
|
900
|
+
scope: 'self'
|
|
901
|
+
},
|
|
895
902
|
{
|
|
896
903
|
name: 'Bottom drawer',
|
|
897
904
|
module: 'bottomDrawerExampleModuleProviders',
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@c8y/tutorial",
|
|
3
|
-
"version": "1023.
|
|
3
|
+
"version": "1023.57.0",
|
|
4
4
|
"description": "This package is used to scaffold a tutorial for Cumulocity IoT Web SDK which explains a lot of concepts.",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@c8y/style": "1023.
|
|
7
|
-
"@c8y/ngx-components": "1023.
|
|
8
|
-
"@c8y/client": "1023.
|
|
9
|
-
"@c8y/bootstrap": "1023.
|
|
6
|
+
"@c8y/style": "1023.57.0",
|
|
7
|
+
"@c8y/ngx-components": "1023.57.0",
|
|
8
|
+
"@c8y/client": "1023.57.0",
|
|
9
|
+
"@c8y/bootstrap": "1023.57.0",
|
|
10
10
|
"@angular/cdk": "^20.2.14",
|
|
11
11
|
"monaco-editor": "~0.53.0",
|
|
12
12
|
"ngx-bootstrap": "20.0.2",
|
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
"rxjs": "7.8.2"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"@c8y/options": "1023.
|
|
18
|
-
"@c8y/devkit": "1023.
|
|
17
|
+
"@c8y/options": "1023.57.0",
|
|
18
|
+
"@c8y/devkit": "1023.57.0"
|
|
19
19
|
},
|
|
20
20
|
"peerDependencies": {
|
|
21
21
|
"@angular/common": ">=20 <21"
|
|
@@ -21,6 +21,10 @@ export class MeasurementsInterceptor implements HttpInterceptor {
|
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
private mockGET(_requestDescriptor: string) {
|
|
24
|
+
if (_requestDescriptor.includes('measurement/measurements/series')) {
|
|
25
|
+
return this.mockSeriesGET(_requestDescriptor);
|
|
26
|
+
}
|
|
27
|
+
|
|
24
28
|
const responseGenerators = this.getResponseGenerators();
|
|
25
29
|
|
|
26
30
|
for (const urlPart in responseGenerators) {
|
|
@@ -34,6 +38,72 @@ export class MeasurementsInterceptor implements HttpInterceptor {
|
|
|
34
38
|
return null;
|
|
35
39
|
}
|
|
36
40
|
|
|
41
|
+
private mockSeriesGET(_requestDescriptor: string) {
|
|
42
|
+
const jsonMatch = _requestDescriptor.match(/series(.*)$/);
|
|
43
|
+
|
|
44
|
+
if (!jsonMatch || !jsonMatch[1]) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let params: any = {};
|
|
49
|
+
try {
|
|
50
|
+
params = JSON.parse(jsonMatch[1]);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
console.error('Failed to parse series params', e);
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { dateFrom, dateTo, series } = params;
|
|
57
|
+
|
|
58
|
+
if (!dateFrom || !dateTo) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const start = new Date(dateFrom);
|
|
63
|
+
const end = new Date(dateTo);
|
|
64
|
+
const values: Record<string, { min: number; max: number }[]> = {};
|
|
65
|
+
|
|
66
|
+
const current = new Date(start);
|
|
67
|
+
current.setMinutes(0, 0, 0);
|
|
68
|
+
|
|
69
|
+
while (current <= end) {
|
|
70
|
+
const timestamp = current.toISOString();
|
|
71
|
+
|
|
72
|
+
const baseValue = 50 + Math.sin(current.getTime() / (1000 * 60 * 60 * 6)) * 30;
|
|
73
|
+
const variation = Math.random() * 20 - 10;
|
|
74
|
+
const value = Math.max(0, Math.min(100, baseValue + variation));
|
|
75
|
+
|
|
76
|
+
values[timestamp] = [
|
|
77
|
+
{
|
|
78
|
+
min: Number(value.toFixed(2)),
|
|
79
|
+
max: Number(value.toFixed(2))
|
|
80
|
+
}
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
current.setHours(current.getHours() + 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let seriesInfo = {
|
|
87
|
+
unit: '%',
|
|
88
|
+
name: 'Battery',
|
|
89
|
+
type: 'c8y_Battery'
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
if (series?.includes('c8y_Temperature')) {
|
|
93
|
+
seriesInfo = {
|
|
94
|
+
unit: '°C',
|
|
95
|
+
name: 'T',
|
|
96
|
+
type: 'c8y_Temperature'
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return generateResponse(() => ({
|
|
101
|
+
values,
|
|
102
|
+
series: [seriesInfo],
|
|
103
|
+
truncated: false
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
|
|
37
107
|
private getResponseGenerators() {
|
|
38
108
|
return {
|
|
39
109
|
c8y_Battery: () => ({
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
<div class="container-fluid">
|
|
2
|
+
<div class="row m-t-16">
|
|
3
|
+
<div class="col-lg-12">
|
|
4
|
+
<div class="card">
|
|
5
|
+
<div class="card-header">
|
|
6
|
+
<h3 class="card-title">Example Configurations</h3>
|
|
7
|
+
</div>
|
|
8
|
+
<div class="card-block">
|
|
9
|
+
<div
|
|
10
|
+
class="btn-group"
|
|
11
|
+
role="group"
|
|
12
|
+
>
|
|
13
|
+
@for (example of configExamples; track example.value) {
|
|
14
|
+
<button
|
|
15
|
+
class="btn"
|
|
16
|
+
[class.btn-primary]="selectedExample === example.value"
|
|
17
|
+
[class.btn-default]="selectedExample !== example.value"
|
|
18
|
+
type="button"
|
|
19
|
+
(click)="loadExampleConfiguration(example.value)"
|
|
20
|
+
>
|
|
21
|
+
{{ example.label }}
|
|
22
|
+
</button>
|
|
23
|
+
}
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div class="m-t-16">
|
|
27
|
+
@switch (selectedExample) {
|
|
28
|
+
@case ('basic') {
|
|
29
|
+
<div class="alert alert-info">
|
|
30
|
+
<strong>Basic Chart:</strong>
|
|
31
|
+
Simple chart with 2 datapoints showing temperature data over the last 24 hours.
|
|
32
|
+
Good starting point for understanding the component.
|
|
33
|
+
</div>
|
|
34
|
+
}
|
|
35
|
+
@case ('slider') {
|
|
36
|
+
<div class="alert alert-info">
|
|
37
|
+
<strong>With Slider:</strong>
|
|
38
|
+
Chart with navigation slider enabled for exploring 7 days of data. Supports data
|
|
39
|
+
aggregation and zoom capabilities.
|
|
40
|
+
</div>
|
|
41
|
+
}
|
|
42
|
+
@case ('multiaxis') {
|
|
43
|
+
<div class="alert alert-info">
|
|
44
|
+
<strong>Multiple Axes:</strong>
|
|
45
|
+
Chart displaying 4 datapoints with different units, each with its own Y-axis.
|
|
46
|
+
Shows how to handle heterogeneous data types.
|
|
47
|
+
</div>
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="row m-t-16">
|
|
57
|
+
<div class="col-lg-9">
|
|
58
|
+
<div class="card">
|
|
59
|
+
<div class="card-header">
|
|
60
|
+
<h3 class="card-title">Chart Visualization</h3>
|
|
61
|
+
<span class="card-header-actions">
|
|
62
|
+
<span class="badge">Active datapoints: {{ activeDatapoints.length }}</span>
|
|
63
|
+
</span>
|
|
64
|
+
</div>
|
|
65
|
+
<div
|
|
66
|
+
class="card-block"
|
|
67
|
+
style="height: 500px"
|
|
68
|
+
>
|
|
69
|
+
<c8y-charts
|
|
70
|
+
[config]="config"
|
|
71
|
+
[alerts]="alerts"
|
|
72
|
+
[chartViewContext]="chartViewContext"
|
|
73
|
+
(updateAlarmsAndEvents)="updateAlarmsAndEvents($event)"
|
|
74
|
+
(configChangeOnZoomOut)="onSliderZoom($event)"
|
|
75
|
+
(datapointOutOfSync)="handleDatapointOutOfSync($event)"
|
|
76
|
+
(timeRangeChangeOnRealtime)="updateTimeRangeOnRealtime($event)"
|
|
77
|
+
(isMarkedAreaEnabled)="onMarkedAreaEnabled($event)"
|
|
78
|
+
(updateActiveDatapoints)="updateActiveDatapoints($event)"
|
|
79
|
+
(updateAggregatedSliderDatapoint)="updateAggregatedSliderDatapoint($event)"
|
|
80
|
+
></c8y-charts>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<div class="col-lg-3">
|
|
86
|
+
<div class="card">
|
|
87
|
+
<div class="card-header">
|
|
88
|
+
<h3 class="card-title">Chart Options</h3>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="card-block">
|
|
91
|
+
<fieldset class="c8y-fieldset">
|
|
92
|
+
<legend>Display Options</legend>
|
|
93
|
+
|
|
94
|
+
<label class="c8y-checkbox">
|
|
95
|
+
<input
|
|
96
|
+
type="checkbox"
|
|
97
|
+
[checked]="config.yAxisSplitLines"
|
|
98
|
+
(change)="toggleOption('yAxisSplitLines')"
|
|
99
|
+
/>
|
|
100
|
+
<span></span>
|
|
101
|
+
<span>Y-axis helper lines</span>
|
|
102
|
+
</label>
|
|
103
|
+
|
|
104
|
+
<label class="c8y-checkbox">
|
|
105
|
+
<input
|
|
106
|
+
type="checkbox"
|
|
107
|
+
[checked]="config.xAxisSplitLines"
|
|
108
|
+
(change)="toggleOption('xAxisSplitLines')"
|
|
109
|
+
/>
|
|
110
|
+
<span></span>
|
|
111
|
+
<span>X-axis helper lines</span>
|
|
112
|
+
</label>
|
|
113
|
+
|
|
114
|
+
<label class="c8y-checkbox">
|
|
115
|
+
<input
|
|
116
|
+
type="checkbox"
|
|
117
|
+
[checked]="config.showSlider"
|
|
118
|
+
(change)="toggleOption('showSlider')"
|
|
119
|
+
/>
|
|
120
|
+
<span></span>
|
|
121
|
+
<span>Show slider</span>
|
|
122
|
+
</label>
|
|
123
|
+
|
|
124
|
+
<label class="c8y-checkbox">
|
|
125
|
+
<input
|
|
126
|
+
type="checkbox"
|
|
127
|
+
[checked]="config.smoothLines"
|
|
128
|
+
(change)="toggleOption('smoothLines')"
|
|
129
|
+
/>
|
|
130
|
+
<span></span>
|
|
131
|
+
<span>Smooth lines</span>
|
|
132
|
+
</label>
|
|
133
|
+
|
|
134
|
+
<label class="c8y-checkbox">
|
|
135
|
+
<input
|
|
136
|
+
type="checkbox"
|
|
137
|
+
[checked]="config.showLabelAndUnit"
|
|
138
|
+
(change)="toggleOption('showLabelAndUnit')"
|
|
139
|
+
/>
|
|
140
|
+
<span></span>
|
|
141
|
+
<span>Show labels & units</span>
|
|
142
|
+
</label>
|
|
143
|
+
</fieldset>
|
|
144
|
+
|
|
145
|
+
<fieldset class="c8y-fieldset m-t-16">
|
|
146
|
+
<legend>Axis Options</legend>
|
|
147
|
+
|
|
148
|
+
<label class="c8y-checkbox">
|
|
149
|
+
<input
|
|
150
|
+
type="checkbox"
|
|
151
|
+
[checked]="config.mergeMatchingDatapoints"
|
|
152
|
+
(change)="toggleOption('mergeMatchingDatapoints')"
|
|
153
|
+
/>
|
|
154
|
+
<span></span>
|
|
155
|
+
<span>Merge matching datapoints</span>
|
|
156
|
+
</label>
|
|
157
|
+
|
|
158
|
+
<label class="c8y-checkbox">
|
|
159
|
+
<input
|
|
160
|
+
type="checkbox"
|
|
161
|
+
[checked]="config.forceMergeDatapoints"
|
|
162
|
+
(change)="toggleOption('forceMergeDatapoints')"
|
|
163
|
+
/>
|
|
164
|
+
<span></span>
|
|
165
|
+
<span>Force merge all datapoints</span>
|
|
166
|
+
</label>
|
|
167
|
+
|
|
168
|
+
<label class="c8y-checkbox">
|
|
169
|
+
<input
|
|
170
|
+
type="checkbox"
|
|
171
|
+
[checked]="config.setYaxisStartToZero"
|
|
172
|
+
(change)="toggleOption('setYaxisStartToZero')"
|
|
173
|
+
/>
|
|
174
|
+
<span></span>
|
|
175
|
+
<span>Y-axis starts at 0</span>
|
|
176
|
+
</label>
|
|
177
|
+
</fieldset>
|
|
178
|
+
|
|
179
|
+
<fieldset class="c8y-fieldset m-t-16">
|
|
180
|
+
<legend>Alarms & Events</legend>
|
|
181
|
+
|
|
182
|
+
<label class="c8y-checkbox">
|
|
183
|
+
<input
|
|
184
|
+
type="checkbox"
|
|
185
|
+
[checked]="config.displayMarkedLine"
|
|
186
|
+
(change)="toggleOption('displayMarkedLine')"
|
|
187
|
+
/>
|
|
188
|
+
<span></span>
|
|
189
|
+
<span>Show vertical lines</span>
|
|
190
|
+
</label>
|
|
191
|
+
|
|
192
|
+
<label class="c8y-checkbox">
|
|
193
|
+
<input
|
|
194
|
+
type="checkbox"
|
|
195
|
+
[checked]="config.displayMarkedPoint"
|
|
196
|
+
(change)="toggleOption('displayMarkedPoint')"
|
|
197
|
+
/>
|
|
198
|
+
<span></span>
|
|
199
|
+
<span>Show icons</span>
|
|
200
|
+
</label>
|
|
201
|
+
</fieldset>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Component, OnInit, ViewChild } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import { FormsModule } from '@angular/forms';
|
|
4
|
+
import {
|
|
5
|
+
CoreModule,
|
|
6
|
+
DynamicComponentAlert,
|
|
7
|
+
DynamicComponentAlertAggregator,
|
|
8
|
+
WidgetTimeContextDateRangeService,
|
|
9
|
+
DateAndTimeOptions
|
|
10
|
+
} from '@c8y/ngx-components';
|
|
11
|
+
import { TimeContextComponent } from '@c8y/ngx-components/time-context';
|
|
12
|
+
import {
|
|
13
|
+
AlarmOrEventExtended,
|
|
14
|
+
CHART_VIEW_CONTEXT,
|
|
15
|
+
ChartAlarmsService,
|
|
16
|
+
ChartEventsService,
|
|
17
|
+
ChartHelpersService,
|
|
18
|
+
ChartsComponent,
|
|
19
|
+
DatapointsGraphKPIDetails,
|
|
20
|
+
DatapointsGraphWidgetConfig
|
|
21
|
+
} from '@c8y/ngx-components/echart';
|
|
22
|
+
import { aggregationType } from '@c8y/client';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* This example demonstrates how to use the c8y-charts component to display
|
|
26
|
+
* time-series data with datapoints, alarms, and events.
|
|
27
|
+
*
|
|
28
|
+
* The c8y-charts component is a powerful visualization tool that:
|
|
29
|
+
* - Displays multiple datapoint series on a chart
|
|
30
|
+
* - Supports real-time data updates
|
|
31
|
+
* - Shows alarms and events as markers
|
|
32
|
+
* - Provides zoom and pan capabilities
|
|
33
|
+
* - Supports data aggregation and slider navigation
|
|
34
|
+
*
|
|
35
|
+
* Key features demonstrated:
|
|
36
|
+
* - Basic chart configuration
|
|
37
|
+
* - Handling chart events (zoom, datapoint updates)
|
|
38
|
+
* - Working with alerts and notifications
|
|
39
|
+
* - Chart view context management
|
|
40
|
+
*/
|
|
41
|
+
@Component({
|
|
42
|
+
selector: 'app-charts-example',
|
|
43
|
+
templateUrl: './charts-example.component.html',
|
|
44
|
+
standalone: true,
|
|
45
|
+
imports: [CommonModule, CoreModule, FormsModule, ChartsComponent],
|
|
46
|
+
providers: [
|
|
47
|
+
ChartEventsService,
|
|
48
|
+
ChartAlarmsService,
|
|
49
|
+
ChartHelpersService,
|
|
50
|
+
WidgetTimeContextDateRangeService
|
|
51
|
+
]
|
|
52
|
+
})
|
|
53
|
+
export class ChartsExampleComponent implements OnInit {
|
|
54
|
+
// Chart configuration object
|
|
55
|
+
config: DatapointsGraphWidgetConfig = {
|
|
56
|
+
datapoints: [],
|
|
57
|
+
alarmsEventsConfigs: [],
|
|
58
|
+
dateFrom: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(), // 24 hours ago
|
|
59
|
+
dateTo: new Date().toISOString(),
|
|
60
|
+
interval: 'custom',
|
|
61
|
+
realtime: false,
|
|
62
|
+
yAxisSplitLines: true,
|
|
63
|
+
xAxisSplitLines: false,
|
|
64
|
+
showLabelAndUnit: true,
|
|
65
|
+
showSlider: true,
|
|
66
|
+
smoothLines: false,
|
|
67
|
+
displayMarkedLine: true,
|
|
68
|
+
displayMarkedPoint: true,
|
|
69
|
+
mergeMatchingDatapoints: false,
|
|
70
|
+
forceMergeDatapoints: false,
|
|
71
|
+
setYaxisStartToZero: false,
|
|
72
|
+
numberOfDecimalPlaces: 2
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Alert aggregator for chart notifications
|
|
76
|
+
alerts = new DynamicComponentAlertAggregator();
|
|
77
|
+
|
|
78
|
+
// Chart view context - determines the chart behavior and features
|
|
79
|
+
chartViewContext = CHART_VIEW_CONTEXT.DATAPOINT_EXPLORER;
|
|
80
|
+
|
|
81
|
+
// Active datapoints currently visible in the chart
|
|
82
|
+
activeDatapoints: DatapointsGraphKPIDetails[] = [];
|
|
83
|
+
|
|
84
|
+
// Time context reference
|
|
85
|
+
@ViewChild('timeContext') timeContext: TimeContextComponent;
|
|
86
|
+
|
|
87
|
+
// Time properties for binding to time context
|
|
88
|
+
timeProps: {
|
|
89
|
+
dateFrom: Date;
|
|
90
|
+
dateTo: Date;
|
|
91
|
+
interval?: string;
|
|
92
|
+
realtime?: boolean;
|
|
93
|
+
aggregation?: aggregationType | null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Time picker configuration
|
|
97
|
+
readonly TIME_PICKER_CONFIG: DateAndTimeOptions = {
|
|
98
|
+
showMinutes: true,
|
|
99
|
+
showSeconds: true,
|
|
100
|
+
showSpinners: false
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// Flag to skip onTimeContextChange during zoom handling
|
|
104
|
+
private isHandlingZoom = false;
|
|
105
|
+
|
|
106
|
+
// Configuration examples
|
|
107
|
+
configExamples = [
|
|
108
|
+
{ label: 'Basic Chart', value: 'basic' },
|
|
109
|
+
{ label: 'With Slider', value: 'slider' },
|
|
110
|
+
{ label: 'Multiple Axes', value: 'multiaxis' }
|
|
111
|
+
];
|
|
112
|
+
|
|
113
|
+
selectedExample = 'basic';
|
|
114
|
+
|
|
115
|
+
ngOnInit(): void {
|
|
116
|
+
// Initialize with a basic configuration
|
|
117
|
+
this.loadExampleConfiguration('basic');
|
|
118
|
+
|
|
119
|
+
// Initialize timeProps from config
|
|
120
|
+
if (this.config.dateFrom && this.config.dateTo) {
|
|
121
|
+
this.timeProps = {
|
|
122
|
+
dateFrom: new Date(this.config.dateFrom),
|
|
123
|
+
dateTo: new Date(this.config.dateTo),
|
|
124
|
+
interval: this.config.interval as any,
|
|
125
|
+
realtime: this.config.realtime,
|
|
126
|
+
aggregation: this.config.realtime ? null : this.config.aggregation
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Load different example configurations to demonstrate various chart capabilities
|
|
133
|
+
*/
|
|
134
|
+
loadExampleConfiguration(example: string): void {
|
|
135
|
+
this.selectedExample = example;
|
|
136
|
+
|
|
137
|
+
switch (example) {
|
|
138
|
+
case 'basic':
|
|
139
|
+
this.loadBasicExample();
|
|
140
|
+
break;
|
|
141
|
+
case 'realtime':
|
|
142
|
+
this.loadRealtimeExample();
|
|
143
|
+
break;
|
|
144
|
+
case 'slider':
|
|
145
|
+
this.loadSliderExample();
|
|
146
|
+
break;
|
|
147
|
+
case 'multiaxis':
|
|
148
|
+
this.loadMultiAxisExample();
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Handle zoom events from the chart
|
|
155
|
+
*/
|
|
156
|
+
onSliderZoom(timeProps: { dateFrom: Date; dateTo: Date; interval?: 'hours' | 'weeks' }): void {
|
|
157
|
+
this.isHandlingZoom = true;
|
|
158
|
+
|
|
159
|
+
const dateFrom = timeProps.dateFrom.toISOString();
|
|
160
|
+
const dateTo = timeProps.dateTo.toISOString();
|
|
161
|
+
const interval = timeProps.interval || 'custom';
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
this.config.dateFrom === dateFrom &&
|
|
165
|
+
this.config.dateTo === dateTo &&
|
|
166
|
+
this.config.interval === interval &&
|
|
167
|
+
this.config.realtime === false
|
|
168
|
+
) {
|
|
169
|
+
this.isHandlingZoom = false;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Update config
|
|
174
|
+
this.config = {
|
|
175
|
+
...this.config,
|
|
176
|
+
dateFrom,
|
|
177
|
+
dateTo,
|
|
178
|
+
interval,
|
|
179
|
+
realtime: false,
|
|
180
|
+
isAutoRefreshEnabled: false,
|
|
181
|
+
refreshInterval: 0
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
// Update timeProps binding
|
|
185
|
+
this.timeProps = {
|
|
186
|
+
dateFrom: new Date(timeProps.dateFrom.getTime()),
|
|
187
|
+
dateTo: new Date(timeProps.dateTo.getTime()),
|
|
188
|
+
interval: 'custom',
|
|
189
|
+
realtime: false
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
this.isHandlingZoom = false;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Handle real-time updates to the time range
|
|
197
|
+
*/
|
|
198
|
+
updateTimeRangeOnRealtime(
|
|
199
|
+
timeRange: Pick<DatapointsGraphWidgetConfig, 'dateFrom' | 'dateTo'>
|
|
200
|
+
): void {
|
|
201
|
+
if (this.config.dateFrom === timeRange.dateFrom && this.config.dateTo === timeRange.dateTo) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
this.config = {
|
|
206
|
+
...this.config,
|
|
207
|
+
...timeRange
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Update timeProps for time context
|
|
211
|
+
if (timeRange.dateFrom && timeRange.dateTo) {
|
|
212
|
+
this.timeProps = {
|
|
213
|
+
...this.timeProps,
|
|
214
|
+
dateFrom: new Date(timeRange.dateFrom),
|
|
215
|
+
dateTo: new Date(timeRange.dateTo)
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Handle datapoint synchronization issues
|
|
222
|
+
*/
|
|
223
|
+
handleDatapointOutOfSync(datapoint: DatapointsGraphKPIDetails): void {
|
|
224
|
+
console.warn('Datapoint out of sync:', datapoint);
|
|
225
|
+
|
|
226
|
+
const alert = new DynamicComponentAlert({
|
|
227
|
+
type: 'warning',
|
|
228
|
+
text: 'Datapoint out of sync: ' + datapoint.label
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
this.alerts?.addAlerts(alert);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update alarms and events displayed on the chart
|
|
236
|
+
*/
|
|
237
|
+
updateAlarmsAndEvents(alarmsEventsConfigs: AlarmOrEventExtended[]): void {
|
|
238
|
+
if (this.config.alarmsEventsConfigs === alarmsEventsConfigs) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.config = {
|
|
243
|
+
...this.config,
|
|
244
|
+
alarmsEventsConfigs
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Track which datapoints are currently active and visible
|
|
250
|
+
*/
|
|
251
|
+
updateActiveDatapoints(activeDatapoints: DatapointsGraphKPIDetails[]): void {
|
|
252
|
+
this.activeDatapoints = activeDatapoints;
|
|
253
|
+
console.log('Active datapoints:', activeDatapoints.length);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Handle marked area state changes (when alarms are clicked)
|
|
258
|
+
*/
|
|
259
|
+
onMarkedAreaEnabled(isEnabled: boolean): void {
|
|
260
|
+
console.log('Marked area enabled:', isEnabled);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Handle aggregated slider datapoint selection
|
|
265
|
+
*/
|
|
266
|
+
updateAggregatedSliderDatapoint(selectedDatapoint: any): void {
|
|
267
|
+
console.log('Aggregated slider datapoint updated:', selectedDatapoint);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Toggle a configuration option
|
|
272
|
+
*/
|
|
273
|
+
toggleOption(option: keyof DatapointsGraphWidgetConfig): void {
|
|
274
|
+
this.config = {
|
|
275
|
+
...this.config,
|
|
276
|
+
[option]: !this.config[option]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Basic chart example with simple configuration
|
|
282
|
+
*/
|
|
283
|
+
private loadBasicExample(): void {
|
|
284
|
+
this.config = {
|
|
285
|
+
...this.config,
|
|
286
|
+
datapoints: this.createSampleDatapoints(2),
|
|
287
|
+
dateFrom: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
|
288
|
+
dateTo: new Date().toISOString(),
|
|
289
|
+
interval: 'custom',
|
|
290
|
+
realtime: false,
|
|
291
|
+
showSlider: false,
|
|
292
|
+
yAxisSplitLines: true,
|
|
293
|
+
xAxisSplitLines: false
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Real-time chart example with auto-refresh
|
|
299
|
+
*/
|
|
300
|
+
private loadRealtimeExample(): void {
|
|
301
|
+
this.config = {
|
|
302
|
+
...this.config,
|
|
303
|
+
datapoints: this.createSampleDatapoints(1),
|
|
304
|
+
dateFrom: new Date(Date.now() - 60 * 60 * 1000).toISOString(), // 1 hour ago
|
|
305
|
+
dateTo: new Date().toISOString(),
|
|
306
|
+
interval: 'hours',
|
|
307
|
+
realtime: true,
|
|
308
|
+
isAutoRefreshEnabled: true,
|
|
309
|
+
refreshInterval: 5000,
|
|
310
|
+
showSlider: false
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Chart with slider for data exploration
|
|
316
|
+
*/
|
|
317
|
+
private loadSliderExample(): void {
|
|
318
|
+
this.config = {
|
|
319
|
+
...this.config,
|
|
320
|
+
datapoints: this.createSampleDatapoints(3),
|
|
321
|
+
dateFrom: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000).toISOString(), // 7 days ago
|
|
322
|
+
dateTo: new Date().toISOString(),
|
|
323
|
+
interval: 'weeks',
|
|
324
|
+
realtime: false,
|
|
325
|
+
showSlider: true,
|
|
326
|
+
aggregation: aggregationType.HOURLY
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Chart with multiple Y-axes for different data types
|
|
332
|
+
*/
|
|
333
|
+
private loadMultiAxisExample(): void {
|
|
334
|
+
this.config = {
|
|
335
|
+
...this.config,
|
|
336
|
+
datapoints: this.createSampleDatapoints(4, true),
|
|
337
|
+
dateFrom: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
|
|
338
|
+
dateTo: new Date().toISOString(),
|
|
339
|
+
interval: 'custom',
|
|
340
|
+
realtime: false,
|
|
341
|
+
showSlider: true,
|
|
342
|
+
mergeMatchingDatapoints: false,
|
|
343
|
+
showLabelAndUnit: true
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Create sample datapoints for demonstration
|
|
349
|
+
* In a real application, these would come from your device/asset data
|
|
350
|
+
*/
|
|
351
|
+
private createSampleDatapoints(count: number, variedUnits = false): DatapointsGraphKPIDetails[] {
|
|
352
|
+
const datapoints: DatapointsGraphKPIDetails[] = [];
|
|
353
|
+
const units = variedUnits ? ['°C', '%', 'kWh', 'bar'] : ['°C'];
|
|
354
|
+
|
|
355
|
+
for (let i = 0; i < count; i++) {
|
|
356
|
+
datapoints.push({
|
|
357
|
+
__target: { id: `device${i + 1}`, name: `Device ${i + 1}` },
|
|
358
|
+
fragment: 'c8y_Battery',
|
|
359
|
+
series: '%',
|
|
360
|
+
label: `Temperature ${i + 1}`,
|
|
361
|
+
unit: units[i % units.length],
|
|
362
|
+
color: this.getColorForIndex(i),
|
|
363
|
+
lineType: 'line',
|
|
364
|
+
renderType: 'min',
|
|
365
|
+
__active: true
|
|
366
|
+
} as any);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return datapoints;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get a color for a datapoint based on its index
|
|
374
|
+
*/
|
|
375
|
+
private getColorForIndex(index: number): string {
|
|
376
|
+
const colors = ['#1776BF', '#14A68E', '#F3B511', '#E66C37', '#9D5BBA'];
|
|
377
|
+
return colors[index % colors.length];
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { NgModule } from '@angular/core';
|
|
2
|
+
import { NavigatorNode, hookNavigator, hookRoute } from '@c8y/ngx-components';
|
|
3
|
+
|
|
4
|
+
@NgModule({
|
|
5
|
+
providers: [
|
|
6
|
+
hookRoute({
|
|
7
|
+
path: 'chart-example',
|
|
8
|
+
loadComponent: () => import('./charts-example.component').then(m => m.ChartsExampleComponent)
|
|
9
|
+
}),
|
|
10
|
+
hookNavigator(
|
|
11
|
+
new NavigatorNode({
|
|
12
|
+
path: '/chart-example',
|
|
13
|
+
label: 'Chart Example',
|
|
14
|
+
icon: 'combo-chart',
|
|
15
|
+
priority: 115
|
|
16
|
+
})
|
|
17
|
+
)
|
|
18
|
+
]
|
|
19
|
+
})
|
|
20
|
+
export class ChartsExampleModule {}
|
|
@@ -17,6 +17,11 @@ import { DeleteModalExampleComponent } from './delete-modal-example.component';
|
|
|
17
17
|
Confirm modal using <code>c8y-confirm-modal</code> component
|
|
18
18
|
</button>
|
|
19
19
|
</div>
|
|
20
|
+
<div class="p-b-24 text-center">
|
|
21
|
+
<button class="btn btn-default" (click)="deleteTenantWithCodeVerification()">
|
|
22
|
+
Confirm modal with code verification
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
20
25
|
</div>`,
|
|
21
26
|
standalone: true,
|
|
22
27
|
imports: [ModalModule, FormsModule, CoreModule]
|
|
@@ -69,4 +74,25 @@ export class ConfirmModalExampleComponent {
|
|
|
69
74
|
console.log('Cancel clicked');
|
|
70
75
|
}
|
|
71
76
|
}
|
|
77
|
+
|
|
78
|
+
async deleteTenantWithCodeVerification() {
|
|
79
|
+
try {
|
|
80
|
+
await this.modalService.confirm(
|
|
81
|
+
'Confirm delete?',
|
|
82
|
+
'You are about to delete the tenant. This action will immediately affect all users.<br><br>For security reasons, enter the following code to continue:',
|
|
83
|
+
Status.DANGER,
|
|
84
|
+
{
|
|
85
|
+
ok: 'Delete'
|
|
86
|
+
},
|
|
87
|
+
{},
|
|
88
|
+
undefined,
|
|
89
|
+
true // requireCodeVerification
|
|
90
|
+
);
|
|
91
|
+
// eslint-disable-next-line no-console
|
|
92
|
+
console.log('Delete confirmed with code verification');
|
|
93
|
+
} catch (e) {
|
|
94
|
+
// eslint-disable-next-line no-console
|
|
95
|
+
console.log('Cancel clicked or code verification failed');
|
|
96
|
+
}
|
|
97
|
+
}
|
|
72
98
|
}
|