Skip to content

Commit 927a695

Browse files
authored
Merge pull request #1354 from utmstack/backlog/alert_echoes_timeline
Backlog/alert echoes timeline
2 parents 1ad9e2a + c686a10 commit 927a695

9 files changed

Lines changed: 235 additions & 34 deletions

File tree

frontend/src/app/data-management/alert-management/alert-view/alert-view.component.html

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -239,38 +239,11 @@ <h6 class="card-title text-blue-800 font-weight-light">
239239

240240
<ng-container *ngIf="alert.expanded">
241241

242-
<tr *ngFor="let child of childrenAlerts; let isFirst = first" class="bg-light">
243-
<!-- <td [ngClass]="{'pt-4': isFirst}" class="text-center pl-3">
244-
<ng-container *ngTemplateOutlet="actionContent; context: { $implicit: child }"></ng-container>
245-
</td>-->
246-
<td [ngClass]="{'pt-4': isFirst}" class="text-center pl-3">
247-
248-
</td>
249-
<ng-container *ngFor="let td of childrenFields">
250-
<td *ngIf="td.visible"
251-
[ngClass]="{'min-width': td.field === ALERT_ADVERSARY_FIELD || td.field === ALERT_TARGET_FIELD, 'pt-4': isFirst}"
252-
(click)="viewDetailAlert(child, td)">
253-
<app-data-field-render [data]="child"
254-
[field]="td"
255-
[dataType]="dataType"
256-
[showStatusChange]="true"
257-
(refreshData)="onRefreshData($event)">
258-
</app-data-field-render>
259-
</td>
260-
</ng-container>
261-
262-
<td></td>
242+
<tr >
243+
<td [attr.colspan]="9" >
244+
<utm-timeline [items]="getChildTimeSeries(childrenAlerts)" (itemClick)="viewDetailAlert($event, {field:0})" ></utm-timeline>
245+
</td>
263246
</tr>
264-
265-
266-
<tr *ngIf="totalChildren > 0" class="bg-light">
267-
<td [attr.colspan]="childrenFields.length + 2" class="text-center border-0 pt-2 pb-3">
268-
<button class="font-weight-semibold" (click)="prevPage(alert)" [disabled]="currentChildrenPage === 1">Back</button>
269-
<span class="px-2">Page {{ currentChildrenPage }} of {{ Math.round(totalChildren / pageSizeChildren) }}</span>
270-
<button class="font-weight-semibold" (click)="nextPage(alert)" [disabled]="currentChildrenPage * pageSizeChildren >= totalChildren">Next</button>
271-
</td>
272-
</tr>
273-
274247
</ng-container>
275248
</ng-template>
276249

frontend/src/app/data-management/alert-management/alert-view/alert-view.component.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ import {AlertTagService} from '../shared/services/alert-tag.service';
5454
import {OPEN_ALERTS_KEY, OpenAlertsService} from '../shared/services/open-alerts.service';
5555
import {getCurrentAlertStatus, getStatusName} from '../shared/util/alert-util-function';
5656
import {AlertActionRefreshService} from "../shared/services/alert-action-refresh.service";
57+
import {TimelineItem} from 'src/app/shared/types/utm-timeline-item'
5758

5859
@Component({
5960
selector: 'app-alert-view',
@@ -604,7 +605,7 @@ export class AlertViewComponent implements OnInit, OnDestroy {
604605

605606
if (alert.expanded) {
606607
this.loadingChildren = true;
607-
this.elasticDataService.search(this.currentChildrenPage, this.pageSizeChildren,
608+
this.elasticDataService.search(this.currentChildrenPage, alert.echoes,
608609
100000000, this.dataNature,
609610
sanitizeFilters(this.filtersChildren), this.sortBy, true).subscribe(
610611
(res: HttpResponse<any>) => {
@@ -635,4 +636,15 @@ export class AlertViewComponent implements OnInit, OnDestroy {
635636
this.destroy$.complete();
636637
this.alertActionRefreshService.clearValues();
637638
}
639+
640+
641+
getChildTimeSeries(childrenAlerts: UtmAlertType[]): TimelineItem[] {
642+
return childrenAlerts.map(cha => ({
643+
startDate: cha['@timestamp'],
644+
name: cha.name,
645+
metadata: cha,
646+
iconUrl: 'assets/icons/echoes/echoes_default.png'
647+
}));
648+
}
649+
638650
}

frontend/src/app/data-management/data-management.module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ import {CommonModule} from '@angular/common';
22
import {NgModule} from '@angular/core';
33
import {NewAlertBehavior} from '../shared/behaviors/new-alert.behavior';
44
import {DataManagementRouting} from './data-management-routing.module';
5+
import {UtmSharedModule} from 'src/app/shared/utm-shared.module'
56

67
@NgModule({
78
declarations: [],
89
imports: [
910
CommonModule,
10-
DataManagementRouting
11+
DataManagementRouting,
12+
UtmSharedModule
1113
],
1214
providers: [NewAlertBehavior]
1315
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
<div echarts [options]="chartOption" (chartClick)="onChartClick($event)" class="timeline-chart"></div>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
.timeline{
2+
min-width: 50dvh
3+
}
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { Component, Input, OnChanges, SimpleChanges, Output, EventEmitter } from '@angular/core';
2+
import {TimelineItem} from '../../types/utm-timeline-item'
3+
4+
5+
@Component({
6+
selector: 'utm-timeline',
7+
templateUrl: './utm-timeline.component.html',
8+
styleUrls: ['./utm-timeline.component.scss']
9+
})
10+
export class UtmTimeLineComponent implements OnChanges {
11+
@Input() items: TimelineItem[] = [];
12+
@Input() title: string = '';
13+
@Output() itemClick = new EventEmitter<TimelineItem>();
14+
15+
chartOption: any = {};
16+
17+
ngOnChanges(changes: SimpleChanges): void {
18+
if ((this.items.length || 0) > 0) {
19+
this.buildChart();
20+
}
21+
}
22+
23+
private buildChart() {
24+
const groups: Record<number, TimelineItem[]> = {};
25+
this.items.forEach(item => {
26+
const ts = new Date(item.startDate).getTime();
27+
if (!groups[ts]) groups[ts] = [];
28+
groups[ts].push(item);
29+
});
30+
31+
const seriesData: any[] = [];
32+
Object.keys(groups).forEach(tsStr => {
33+
const ts = Number(tsStr);
34+
const group = groups[ts];
35+
36+
group.forEach((item, idx) => {
37+
seriesData.push({
38+
value: [
39+
ts, // index 0: timestamp
40+
0, // index 1: Y coordinate
41+
item.name, // index 2: name
42+
new Date(item.startDate).toLocaleString(), // index 3:formatted date
43+
!!item.iconUrl?item.iconUrl:'', // index 4: offsetIndex
44+
idx, // index 5: offsetIndex
45+
group.length // index 6: groupSize
46+
],
47+
itemData: item
48+
});
49+
});
50+
});
51+
52+
this.chartOption = {
53+
title: {
54+
text: this.title,
55+
left: 'center',
56+
textStyle: {
57+
fontSize: 16,
58+
fontWeight: 'bold'
59+
}
60+
},
61+
grid:{
62+
left:0,
63+
right:0
64+
},
65+
tooltip: {
66+
trigger: 'item',
67+
formatter: (params: any) =>
68+
`<b>${params.data.value[2]}</b><br/>${params.data.value[3]}`
69+
},
70+
xAxis: {
71+
type: 'time',
72+
//let a space between init of the timeline and the first/last element so they can be showed
73+
min: (value: any) => {
74+
return value.min - 5 * 1000;
75+
},
76+
max: (value: any) => {
77+
return value.max + 10 * 1000;
78+
},
79+
axisLabel: {
80+
formatter: (val: number) => {
81+
const date = new Date(val)
82+
return `${date.getHours()%12}:${date.getMinutes() < 10 ? '0' : ''}${date.getMinutes()}:${date.getSeconds() < 10 ? '0' : ''}${date.getSeconds()} \n ${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} `
83+
}
84+
},
85+
},
86+
yAxis: { show: false, type: 'value'},
87+
series: [
88+
{
89+
type: 'custom',
90+
data: seriesData,
91+
renderItem: (params: any, api: any) => {
92+
const timestamp = api.value(0);
93+
const coord = api.coord([timestamp, 0]);
94+
95+
const cardHeight = 60;
96+
const spacing = 10;
97+
const offsetIndex = api.value(5) || 0;
98+
const totalOffset = (cardHeight + spacing) * offsetIndex + 80;
99+
100+
const name = api.value(2);
101+
const date = api.value(3);
102+
const iconUrl = api.value(4);
103+
104+
const iconPadding = 0;
105+
106+
return {
107+
type: 'group',
108+
children: [
109+
{
110+
type: 'rect',
111+
shape: {
112+
x: coord[0] - 75,
113+
y: coord[1] - totalOffset - cardHeight,
114+
width: 250,
115+
height: cardHeight,
116+
r: 8
117+
},
118+
style: {
119+
fill: '#fefefe',
120+
stroke: '#0277bd',
121+
lineWidth: 1,
122+
shadowBlur: 8,
123+
shadowColor: 'rgba(0,0,0,0.2)',
124+
cursor: 'pointer',
125+
overflow:'hidden'
126+
}
127+
},
128+
{
129+
type: 'image',
130+
style: {
131+
image: iconUrl,
132+
x: coord[0] - 75 + iconPadding +1.5,
133+
y: coord[1] - totalOffset - cardHeight+1.5,
134+
width: cardHeight-2.5,
135+
height:cardHeight-2.5,
136+
r: 8
137+
}
138+
},
139+
{
140+
type: 'text',
141+
style: {
142+
x: coord[0] - 75 + iconPadding + cardHeight + 10,
143+
y: coord[1] - totalOffset - cardHeight + 20,
144+
text:this.truncateText(name,150),
145+
textAlign: 'left',
146+
fill: '#000',
147+
fontSize: 13,
148+
fontWeight: 600,
149+
width: 150 - cardHeight - iconPadding * 2 - 10,
150+
overflow: 'break',
151+
ellipsis: '...'
152+
}
153+
},
154+
{
155+
type: 'text',
156+
style: {
157+
x: coord[0] - 75 + iconPadding + cardHeight + 10,
158+
y: coord[1] - totalOffset - cardHeight + 40,
159+
text: date,
160+
textAlign: 'left',
161+
fill: '#666',
162+
fontSize: 10
163+
}
164+
},
165+
{
166+
type: 'line',
167+
shape: {
168+
x1: coord[0],
169+
y1: coord[1] - totalOffset,
170+
x2: coord[0],
171+
y2: coord[1]
172+
},
173+
style: { stroke: '#0277bd', lineWidth: 1.5 }
174+
}
175+
]
176+
};
177+
},
178+
encode: { x: 0, y: 1 }
179+
}
180+
],
181+
dataZoom: [
182+
{ type: 'slider', xAxisIndex: 0 },
183+
{ type: 'inside', xAxisIndex: 0 }
184+
]
185+
};
186+
}
187+
188+
onChartClick(event: any): void {
189+
if (event.data && event.data.itemData) {
190+
this.itemClick.emit(event.data.itemData.metadata);
191+
}
192+
}
193+
194+
truncateText(text: string, maxWidth: number) {
195+
const avgCharWidth = 7;
196+
const maxChars = Math.floor(maxWidth / avgCharWidth);
197+
return text.length > maxChars ? text.substring(0, maxChars - 3) + '...' : text;
198+
};
199+
200+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface TimelineItem {
2+
startDate: string | Date;
3+
name: string;
4+
metadata: any;
5+
iconUrl: string | undefined | null;
6+
}

frontend/src/app/shared/utm-shared.module.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {PasswordStrengthBarComponent} from './components/auth/password-strength/
2828
import { TfaSetupComponent } from './components/auth/tfa-setup/tfa-setup.component';
2929
import {TotpComponent} from './components/auth/totp/totp.component';
3030
import {ContactUsComponent} from './components/contact-us/contact-us.component';
31+
import {UtmTimeLineComponent} from './components/utm-timeline/utm-timeline.component';
3132
import {
3233
EmailSettingNotificactionComponent
3334
} from './components/email-setting-notification/email-setting-notificaction.component';
@@ -238,7 +239,7 @@ import {HighlightPipe} from './pipes/text/highlight.pipe';
238239
import {TimePeriodPipe} from './pipes/time-period.pipe';
239240
import {TimezoneOffsetPipe} from './pipes/timezone-offset.pipe';
240241
import {UtmNotifier} from './websocket/utm-notifier';
241-
242+
import { NgxEchartsModule } from 'ngx-echarts';
242243

243244

244245
@NgModule({
@@ -258,6 +259,7 @@ import {UtmNotifier} from './websocket/utm-notifier';
258259
AssetsApplyNoteModule,
259260
AssetsGroupAddModule,
260261
InfiniteScrollModule,
262+
NgxEchartsModule
261263
],
262264
declarations: [
263265
ElasticFilterComponent,
@@ -268,6 +270,7 @@ import {UtmNotifier} from './websocket/utm-notifier';
268270
HasAnyAuthorityDirective,
269271
NotFoundComponent,
270272
SidebarComponent,
273+
UtmTimeLineComponent,
271274
PasswordResetInitComponent,
272275
PasswordResetFinishComponent,
273276
PasswordStrengthBarComponent,
@@ -407,6 +410,7 @@ import {UtmNotifier} from './websocket/utm-notifier';
407410
SidebarComponent,
408411
PasswordResetInitComponent,
409412
PasswordResetFinishComponent,
413+
UtmTimeLineComponent,
410414
PasswordStrengthBarComponent,
411415
SortByComponent,
412416
UtmSpinnerComponent,
34.5 KB
Loading

0 commit comments

Comments
 (0)