Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MTM-58884] UI as datapoint graph user i want to see configured events and alarms #28

Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9b67cf0
MTM-58884 Initial commit.
Apr 18, 2024
90abdd0
MTM-58884 Events are now displayed. You can show/hide them in legend
Apr 19, 2024
46eae2b
MTM-58884 added chart legend and event data on hover of markedLine
Apr 22, 2024
24debe8
MTM-58884 Added realtime support for events and general improvements.
Apr 24, 2024
0388885
[MTM-58885] UI as datapoint graph user i want to see configured alarm…
eniosultan May 13, 2024
034c3a3
MTM-58884 Legend re-work initial commit.
May 15, 2024
9fbab27
MTM-58884 Added new legend and all-around fixes.
May 16, 2024
2ee9e61
MTM-58884 Hover improvements + general fixes.
May 18, 2024
d5e10ed
feat: improve datapoints graph widget styling
carlosceia May 20, 2024
40d8f11
MTM-58884 Fixed issues with sticky tooltip when we enable marked area
May 21, 2024
c51cc1e
Merge branch 'feature/MTM-58884-ui-as-datapoint-graph-user-i-want-to-…
carlosceia May 21, 2024
aa3676d
Merge event-alarm selector
May 23, 2024
1b2d013
MTM-58884 Updates after merging event-alarm selector + refactoring
May 25, 2024
c5574ee
MTM-58884 Updates after review part 1
May 28, 2024
56d26a7
MTM-58884 Updates after review and refactor part 2.
May 29, 2024
ed28ead
MTM-58884 Refactoring and changes after review final part 3
May 31, 2024
616cbf7
Apply suggestions from code review
eniosultan Jun 3, 2024
dac1c7b
MTM-58884 Refactoring and improvements after 2nd round of review.
Jun 3, 2024
4e65513
MTM-58884 Changes after review.
Jun 5, 2024
bed401a
MTM-58884 Realtime fix and partial unit test updates...
Jun 11, 2024
c1160f3
MTM-58884 Initial commit with severity filters.
Jun 13, 2024
d13ceb5
Merge branch 'feature/MTM-58884-ui-as-datapoint-graph-user-i-want-to-…
Jun 13, 2024
f359212
MTM-58884 Changes after review.
Jun 14, 2024
224f1e5
MTM-58884 Realtime refactor and changes after review.
Jun 20, 2024
ddbdcaf
MTM-58884 FIxed issue with duplicated event id's
Jun 21, 2024
fea9991
MTM-58884 Fixes after review.
Jun 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ export type AlarmDetails = AlarmOrEventBase & {
filters: {
type: string;
};
__hidden?: boolean;
};

export type EventDetails = AlarmOrEventBase & {
timelineType: 'EVENT';
filters: {
type: string;
};
__hidden?: boolean;
};

export type AlarmOrEvent = AlarmDetails | EventDetails;
Expand Down
138 changes: 128 additions & 10 deletions src/datapoints-graph/charts/chart-realtime.service.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { Injectable } from '@angular/core';
import { interval, merge, Observable, Subscription } from 'rxjs';
import { IMeasurement } from '@c8y/client';
import { combineLatest, interval, merge, Observable, Subscription } from 'rxjs';
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
import { IAlarm, IEvent, IMeasurement } from '@c8y/client';
import { buffer, map, tap, throttleTime } from 'rxjs/operators';
import {
DatapointChartRenderType,
DatapointRealtimeMeasurements,
DatapointsGraphKPIDetails,
DatapointsGraphWidgetConfig,
DatapointWithValues,
SeriesDatapointInfo,
SeriesValue,
} from '../model';
import { MeasurementRealtimeService } from '@c8y/ngx-components';
import {
AlarmRealtimeService,
EventRealtimeService,
MeasurementRealtimeService,
RealtimeMessage,
} from '@c8y/ngx-components';
import type { ECharts, SeriesOption } from 'echarts';
import { EchartsOptionsService } from './echarts-options.service';
import { AlarmOrEvent } from '../alarm-event-selector';

type Milliseconds = number;

Expand All @@ -23,7 +32,12 @@ export class ChartRealtimeService {
private echartsInstance: ECharts;
private currentTimeRange: { dateFrom: Date; dateTo: Date };

constructor(private measurementRealtime: MeasurementRealtimeService) {}
constructor(
private measurementRealtime: MeasurementRealtimeService,
private alarmRealtimeService: AlarmRealtimeService,
private eventRealtimeService: EventRealtimeService,
private echartsOptionsService: EchartsOptionsService
) {}

startRealtime(
echartsInstance: ECharts,
Expand All @@ -32,14 +46,35 @@ export class ChartRealtimeService {
datapointOutOfSyncCallback: (dp: DatapointsGraphKPIDetails) => void,
timeRangeChangedCallback: (
timeRange: Pick<DatapointsGraphWidgetConfig, 'dateFrom' | 'dateTo'>
) => void
) => void,
alarmOrEventConfig: AlarmOrEvent[]
) {
this.echartsInstance = echartsInstance;
this.currentTimeRange = {
dateFrom: new Date(timeRange.dateFrom),
dateTo: new Date(timeRange.dateTo),
};

const activeAlarmsOrEvents = alarmOrEventConfig.filter(
(alarmOrEvent) => alarmOrEvent.__active
);
const uniqueAlarmOrEventTargets = Array.from(
new Set(activeAlarmsOrEvents.map((aOrE) => aOrE.__target.id))
);

const allAlarmsAndEvents: Observable<IAlarm | IEvent>[] =
uniqueAlarmOrEventTargets.map((targetId) => {
const alarmsRealtime$: Observable<RealtimeMessage<IAlarm>> =
this.alarmRealtimeService.onAll$(targetId);
const eventsRealtime$: Observable<RealtimeMessage<IEvent>> =
this.eventRealtimeService.onAll$(targetId);
return merge(alarmsRealtime$, eventsRealtime$).pipe(
map((realtimeMessage) => realtimeMessage.data as IAlarm | IEvent)
);
});

const allAlarmsAndEvents$ = merge(...allAlarmsAndEvents);
eniosultan marked this conversation as resolved.
Show resolved Hide resolved

const measurementsForDatapoints: Observable<DatapointRealtimeMeasurements>[] =
datapoints.map((dp) => {
const source$: Observable<IMeasurement> =
Expand All @@ -57,6 +92,7 @@ export class ChartRealtimeService {
const measurement$ = merge(...measurementsForDatapoints);
const bufferReset$ = merge(
measurement$.pipe(throttleTime(updateThrottleTime)),
allAlarmsAndEvents$.pipe(throttleTime(updateThrottleTime)),
interval(this.INTERVAL).pipe(
tap(() => {
this.currentTimeRange = {
Expand All @@ -73,11 +109,34 @@ export class ChartRealtimeService {
)
).pipe(throttleTime(this.MIN_REALTIME_TIMEOUT));

this.realtimeSubscription = measurement$
.pipe(buffer(bufferReset$))
.subscribe((measurements) => {
this.updateChartInstance(measurements, datapointOutOfSyncCallback);
});
this.realtimeSubscription = combineLatest([
measurement$.pipe(buffer(bufferReset$)),
allAlarmsAndEvents$.pipe(buffer(bufferReset$)),
]).subscribe(([measurements, alarmsAndEvents]) => {
const filteredAlarmsOrEvents = alarmsAndEvents.filter(
(alarmOrEvent: IAlarm | IEvent) => {
return alarmOrEventConfig.some((aOrE) => {
return aOrE.filters.type === alarmOrEvent.type;
});
}
);
const alarmsAndEventsWithColor = filteredAlarmsOrEvents.map(
(alarmOrEvent: IAlarm | IEvent) => {
const foundAlarmOrEvent = alarmOrEventConfig.find((aOrE) => {
return aOrE.filters.type === alarmOrEvent.type;
});
if (foundAlarmOrEvent) {
alarmOrEvent.color = foundAlarmOrEvent.color;
}
return alarmOrEvent;
}
);
this.updateChartInstance(
measurements,
alarmsAndEventsWithColor,
datapointOutOfSyncCallback
);
});
}

stopRealtime() {
Expand Down Expand Up @@ -117,12 +176,19 @@ export class ChartRealtimeService {

private updateChartInstance(
receivedMeasurements: DatapointRealtimeMeasurements[],
alarmsAndEvents: (IAlarm | IEvent | number)[],
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
datapointOutOfSyncCallback: (dp: DatapointsGraphKPIDetails) => void
) {
const isEvent = (item: IAlarm | IEvent | number): item is IEvent =>
!(item as IAlarm | IEvent).severity;
const isAlarm = (item: IAlarm | IEvent | number): item is IAlarm =>
!!(item as IAlarm | IEvent).severity;
eniosultan marked this conversation as resolved.
Show resolved Hide resolved

const seriesDataToUpdate = new Map<
DatapointsGraphKPIDetails,
IMeasurement[]
>();

receivedMeasurements.forEach(({ datapoint, measurement }) => {
if (!seriesDataToUpdate.has(datapoint)) {
seriesDataToUpdate.set(datapoint, []);
Expand All @@ -149,6 +215,58 @@ export class ChartRealtimeService {
seriesMatchingDatapoint.data = this.removeValuesBeforeTimeRange(
seriesMatchingDatapoint
);

alarmsAndEvents.forEach((item) => {
if (
typeof seriesMatchingDatapoint.data !== 'object' ||
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
seriesMatchingDatapoint.data === null
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
) {
return;
}
const renderType: DatapointChartRenderType =
datapoint.renderType || 'min';
const dp: DatapointWithValues = {
...datapoint,
values: seriesMatchingDatapoint.data as {
[date: string]: { min: number; max: number }[];
},
};

if (isEvent(item)) {
const newEventSeries =
this.echartsOptionsService.getAlarmOrEventSeries(
dp,
renderType,
false,
[item],
'event',
item.id
);
allDataSeries.push(...newEventSeries);
} else if (isAlarm(item)) {
const alarmExists = allDataSeries.some((series: { data: any[] }) =>
series.data.some((data) => data[0] === item.creationTime)
);
if (alarmExists) {
const alarmSeries = allDataSeries.find((series: { data: any[] }) =>
series.data.some((data) => data[0] === item.creationTime)
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
);
// Instead of updating the existing alarm series, it would be easier if we remove all existing series for the current time range and add them again.
allDataSeries.splice(allDataSeries.indexOf(alarmSeries), 1);
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
}
const newAlarmSeries =
this.echartsOptionsService.getAlarmOrEventSeries(
dp,
renderType,
false,
[item],
'alarm',
item.id
);
allDataSeries.push(...newAlarmSeries);
}
});

this.checkForValuesAfterTimeRange(
seriesMatchingDatapoint.data as SeriesValue[],
datapoint,
Expand Down
26 changes: 26 additions & 0 deletions src/datapoints-graph/charts/chart.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export interface CustomSeriesOptions extends echarts.EChartsOption {
// typeOfSeries is used for formatter to distinguish between events/alarms series and datapoints
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
typeOfSeries?: 'alarm' | 'event' | null;
id: string;
}

// Add the following info to a markdown file:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it should be moved to .md file? to be honest I don't know exactly what this whole explanation refers to, so some introduction needed or reference to place in code where this explanation makes more sense.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please ignore it for now. I will move this info to codex and will remove it from here.


/* Alarm properties related to time:
time --> Used for filtering alarms in the BE. So it could happen that the alarm is not displayed in the graph
eniosultan marked this conversation as resolved.
Show resolved Hide resolved
because lastUpdated might fit the timeframe while time does not. When a new occurrence of the alarm happens, the time property is
updated together with the lastUpdated property. On the other hand for severity changes (e.g. via smart rules) and
clearing the alarm, only the lastUpdated property is updated.

firstOccurrence ----> Time in which the alarm was first raised. So if a new occurrence of the alarm happens,
the count property is increased, but the firstOccurrence is not updated.


creationTime --> Time in which the alarm was created. Can be used to filter alarms in the BE using creationTimeTo and
creationTimeFrom.


lastUpdated --> Time in which the alarm was last updated. NOTE: Clearing an alarm updated the lastUpdated, but does
not update the time property! Can also be used to filter alarms in the BE using lastUpdatedTo and lastUpdatedFrom.
Note that using only that filter could also miss alarms that were created before the lastUpdatedFrom.
*/
Loading
Loading