Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
f992bb0
feat: graph
fdewas-aneo Jun 30, 2025
19f629b
chore: update graph component
fdewas-aneo Jul 1, 2025
d317b26
feat: graph basic components
fdewas-aneo Jul 2, 2025
1573d57
feat: session graph
fdewas-aneo Jul 2, 2025
667ce72
test: graph legend component
fdewas-aneo Jul 2, 2025
e741722
chore: tests and doc for graph component
fdewas-aneo Jul 3, 2025
5c159d0
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Jul 3, 2025
47ff13f
chore: added payload link
fdewas-aneo Jul 3, 2025
3aa9c02
chore: added payload link
fdewas-aneo Jul 3, 2025
639faa3
Merge branch 'main' into feat/graph
fdewas-aneo Jul 15, 2025
0b72765
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Jul 15, 2025
89ec3cb
chore: updated code
fdewas-aneo Jul 15, 2025
0d98fda
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Jul 17, 2025
2aab2df
chore: stored links colors
fdewas-aneo Jul 17, 2025
724d586
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Jul 17, 2025
ee5eba2
chore: updated highlight code
fdewas-aneo Jul 17, 2025
fc6d386
test: graph component
fdewas-aneo Jul 23, 2025
b566365
Merge branch 'main' into feat/graph
fdewas-aneo Jul 23, 2025
0bb350f
chore: tests and rework for graph legend component
fdewas-aneo Jul 23, 2025
d06eea2
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Jul 23, 2025
0860c54
test: graph data service, grpc event and graph session component
fdewas-aneo Jul 23, 2025
b6812f1
chore: add on destroy method to session graph component
fdewas-aneo Jul 23, 2025
2aefb17
chore: address sonarqube comments
fdewas-aneo Jul 23, 2025
6baf1c3
chore: address sonarqube comments
fdewas-aneo Jul 23, 2025
41eab89
fix: "flying" results in the graph
fdewas-aneo Jul 28, 2025
4625c6c
chore: add highlight detail
fdewas-aneo Jul 28, 2025
3c61b4a
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Jul 31, 2025
9bda881
fix: huge lags when loading children and parents nodes
fdewas-aneo Jul 31, 2025
56713cf
chore: address comments
fdewas-aneo Sep 1, 2025
600bafe
Merge branch 'feat/graph' into feat/session-graph
fdewas-aneo Sep 3, 2025
c8836ff
feat: graph node inspection
fdewas-aneo Sep 3, 2025
098a8d3
docs: inspect components
fdewas-aneo Sep 8, 2025
e4a291f
Merge branch 'main' into feat/graph-node-inspect
fdewas-aneo Sep 30, 2025
42106f4
chore: update data.ts
fdewas-aneo Sep 30, 2025
d130c86
chore: undo some merge changes
fdewas-aneo Sep 30, 2025
69ef2ce
Merge branch 'main' into feat/graph-node-inspect
fdewas-aneo Oct 21, 2025
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
6 changes: 5 additions & 1 deletion src/app/components/graph/graph.component.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<mat-card appearance="outlined">
<mat-card id="graph-session-header" appearance="outlined">
<mat-card-content>
<section>
<h2>
Expand Down Expand Up @@ -35,5 +35,9 @@ <h2>
</mat-checkbox>
</mat-card-actions>
</mat-card>
@let node = selectedNode();
@if (node) {
<app-graph-inspect-node [node]="node" (closePanel)="closeNodePanel()" />
}
<app-graph-legend [links]="colorMap" />
<div class="graph" #graph (window:resize)="onResize($event)"></div>
12 changes: 6 additions & 6 deletions src/app/components/graph/graph.component.scss
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
@use 'styles' as *;

mat-card {
#graph-session-header {
position: absolute;
z-index: 2;
top: 1rem;
left: 3rem;
width: fit-content;
}

mat-card-content {
@include flex-row();
min-width: 60vw;
padding: 0.5rem !important;
mat-card-content {
@include flex-row();
min-width: 60vw;
padding: 0.5rem !important;
}
}

section {
Expand Down
90 changes: 17 additions & 73 deletions src/app/components/graph/graph.component.spec.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
import { ResultStatus, SessionStatus, TaskStatus } from '@aneoconsultingfr/armonik.api.angular';
import { Clipboard } from '@angular/cdk/clipboard';
import { ElementRef } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ResultsStatusesService } from '@app/results/services/results-statuses.service';
import { SessionsStatusesService } from '@app/sessions/services/sessions-statuses.service';
import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service';
import { ArmoniKGraphNode, GraphData, GraphLink, LinkType } from '@app/types/graph.types';
import { DefaultConfigService } from '@services/default-config.service';
import { IconsService } from '@services/icons.service';
import { StorageService } from '@services/storage.service';
import { Subject } from 'rxjs';
import { GraphComponent } from './graph.component';
import { NodeStatusService } from './services/node-status.service';

describe('GraphComponent', () => {
let component: GraphComponent<ArmoniKGraphNode, GraphLink<ArmoniKGraphNode>>;
Expand All @@ -22,27 +19,6 @@ describe('GraphComponent', () => {
setItem: jest.fn(),
};

const mockSessionsStatuses = {
statusToLabel: jest.fn(() => ({
label: 'Running',
color: 'green'
})),
};

const mockTasksStatuses = {
statusToLabel: jest.fn(() => ({
label: 'Completed',
color: 'green'
})),
};

const mockResultsStatuses = {
statusToLabel: jest.fn(() => ({
label: 'Completed',
color: 'green'
})),
};

const mockClipboard = {
copy: jest.fn(),
};
Expand All @@ -55,16 +31,18 @@ describe('GraphComponent', () => {
nativeElement: {}
} as ElementRef;

const mockNodeStatusService = {
getNodeStatusData: jest.fn(() => ({ label: 'status', color: 'green' })),
};

beforeEach(() => {
component = TestBed.configureTestingModule({
providers: [
GraphComponent,
DefaultConfigService,
{ provide: StorageService, useValue: mockStorageService },
{ provide: IconsService, useValue: mockIconsService },
{ provide: SessionsStatusesService, useValue: mockSessionsStatuses },
{ provide: TasksStatusesService, useValue: mockTasksStatuses },
{ provide: ResultsStatusesService, useValue: mockResultsStatuses },
{ provide: NodeStatusService, useValue: mockNodeStatusService },
{ provide: Clipboard, useValue: mockClipboard },
]
}).inject(GraphComponent<ArmoniKGraphNode, GraphLink<ArmoniKGraphNode>>);
Expand Down Expand Up @@ -249,51 +227,6 @@ describe('GraphComponent', () => {
});
});

describe('getNodeStatusData', () => {
it('should return the running session label', () => {
const node = {
type: 'session',
status: SessionStatus.SESSION_STATUS_RUNNING,
} as ArmoniKGraphNode;
expect(component['getNodeStatusData'](node)).toEqual(
mockSessionsStatuses.statusToLabel()
);
});

it('should return the running task label', () => {
const node = {
type: 'task',
status: TaskStatus.TASK_STATUS_COMPLETED,
} as ArmoniKGraphNode;
expect(component['getNodeStatusData'](node)).toEqual(
mockTasksStatuses.statusToLabel()
);
});

it('should return the running result label', () => {
const node = {
type: 'result',
status: ResultStatus.RESULT_STATUS_COMPLETED,
} as ArmoniKGraphNode;
expect(component['getNodeStatusData'](node)).toEqual(
mockResultsStatuses.statusToLabel()
);
});


it('should get the default color', () => {
const node = {
type: 'unknown',
status: ResultStatus.RESULT_STATUS_COMPLETED,
} as unknown as ArmoniKGraphNode;

expect(component['getNodeStatusData'](node)).toEqual({
label: 'Unknown',
color: 'grey'
});
});
});

describe('getLinkColor', () => {
it('should get all kind of link colors', () => {
const types: LinkType[] = ['dependency', 'output', 'parent', 'payload'];
Expand Down Expand Up @@ -403,4 +336,15 @@ describe('GraphComponent', () => {
expect(spy).toHaveBeenCalled();
});
});

it('should close node panel', () => {
component.selectedNode.set({ id: 'id' } as ArmoniKGraphNode);
component.closeNodePanel();
expect(component.selectedNode()).toBeNull();
});

it('should unsubscribe on destroy', () => {
component.ngOnDestroy();
expect(grpcObservable.observed).toBeFalsy();
});
});
75 changes: 44 additions & 31 deletions src/app/components/graph/graph.component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ResultStatus, SessionStatus, TaskStatus } from '@aneoconsultingfr/armonik.api.angular';
import { Clipboard } from '@angular/cdk/clipboard';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild, inject } from '@angular/core';
import { AfterViewInit, ChangeDetectionStrategy, Component, ElementRef, Input, OnDestroy, OnInit, ViewChild, inject, signal } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatCheckboxModule } from '@angular/material/checkbox';
Expand All @@ -9,19 +8,31 @@ import { MatIconModule } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatTooltipModule } from '@angular/material/tooltip';
import { RouterModule } from '@angular/router';
import { ResultsFiltersService } from '@app/results/services/results-filters.service';
import { ResultsGrpcService } from '@app/results/services/results-grpc.service';
import { ResultsStatusesService } from '@app/results/services/results-statuses.service';
import { SessionsStatusesService } from '@app/sessions/services/sessions-statuses.service';
import { TasksFiltersService } from '@app/tasks/services/tasks-filters.service';
import { TasksGrpcService } from '@app/tasks/services/tasks-grpc.service';
import { TasksStatusesService } from '@app/tasks/services/tasks-statuses.service';
import { ArmoniKGraphNode, GraphData, GraphLink, LinkType } from '@app/types/graph.types';
import { StatusLabelColor } from '@app/types/status';
import { StatusService } from '@app/types/status';
import { DefaultConfigService } from '@services/default-config.service';
import { FiltersService } from '@services/filters.service';
import { GrpcSortFieldService } from '@services/grpc-sort-field.service';
import { IconsService } from '@services/icons.service';
import { StorageService } from '@services/storage.service';
import { TableStorageService } from '@services/table-storage.service';
import { TableURLService } from '@services/table-url.service';
import { TableService } from '@services/table.service';
import { UtilsService } from '@services/utils.service';
import { forceLink, forceManyBody } from 'd3';
import ForceGraph from 'force-graph';
import { Observable, Subject, Subscription, switchMap } from 'rxjs';
import { AutoCompleteComponent } from '../auto-complete.component';
import { GraphLegendComponent } from './graph-legend.component';
import { InspectNodeComponent } from './inspect-node.component';
import { NodeStatusService } from './services/node-status.service';

@Component({
selector: 'app-graph',
Expand All @@ -38,12 +49,28 @@ import { GraphLegendComponent } from './graph-legend.component';
RouterModule,
GraphLegendComponent,
MatTooltipModule,
AutoCompleteComponent
AutoCompleteComponent,
InspectNodeComponent,
],
providers: [
SessionsStatusesService,
TasksStatusesService,
ResultsStatusesService,
TasksGrpcService,
TasksFiltersService,
ResultsGrpcService,
ResultsFiltersService,
FiltersService,
UtilsService,
TableService,
TableURLService,
TableStorageService,
{
provide: StatusService,
useClass: TasksStatusesService,
},
GrpcSortFieldService,
NodeStatusService,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
Expand All @@ -65,12 +92,10 @@ export class GraphComponent<N extends ArmoniKGraphNode, L extends GraphLink<N>>
colorMap: Record<LinkType, string>;

private readonly iconsService = inject(IconsService);
private readonly sessionsStatusesService = inject(SessionsStatusesService);
private readonly tasksStatusesService = inject(TasksStatusesService);
private readonly resultsStatusesService = inject(ResultsStatusesService);
private readonly storageService = inject(StorageService);
private readonly defaultConfigService = inject(DefaultConfigService);
private readonly clipboard = inject(Clipboard);
private readonly nodeStatusService = inject(NodeStatusService);

private readonly redrawGraph$ = new Subject<void>();

Expand All @@ -82,6 +107,8 @@ export class GraphComponent<N extends ArmoniKGraphNode, L extends GraphLink<N>>
nodesIds: string[] = [];
readonly highlightLabel = $localize`Highlight a task`;

readonly selectedNode = signal<N | null>(null);

ngOnInit(): void {
const storedColorMap = this.storageService.getItem<Record<LinkType, string>>('graph-links-colors', true) as Record<LinkType, string> | null;
this.colorMap = storedColorMap ?? this.defaultConfigService.defaultGraphLinksColors;
Expand Down Expand Up @@ -112,8 +139,11 @@ export class GraphComponent<N extends ArmoniKGraphNode, L extends GraphLink<N>>
.linkWidth(4)
.nodeLabel('id')
.onNodeClick((node: N) => {
this.graph.centerAt(node.x, node.y);
this.graph.zoom(4, 2000);
if (node.type !== 'session') {
this.selectedNode.set(node);
}
this.graph.centerAt(node.x, node.y, 2000);
this.graph.zoom(1.2, 2000);
});

this.subscription.add(this.grpcObservable.subscribe((result) => this.subscribeToData(result)));
Expand Down Expand Up @@ -320,7 +350,7 @@ export class GraphComponent<N extends ArmoniKGraphNode, L extends GraphLink<N>>
*/
private drawNode(node: N, ctx: CanvasRenderingContext2D) {
if (node.x !== undefined && node.y !== undefined) {
const label = this.getNodeStatusData(node);
const label = this.nodeStatusService.getNodeStatusData(node);
if (this.nodesToHighlight.has(node.id)) {
ctx.font = '70px Material Icons';
const complementary = this.getComplementaryColor(label.color);
Expand All @@ -333,27 +363,6 @@ export class GraphComponent<N extends ArmoniKGraphNode, L extends GraphLink<N>>
}
}

/**
* Get the data associated to the node status.
* @param node N
* @returns StatusLabelColor, contains label, color and icon.
*/
private getNodeStatusData(node: N): StatusLabelColor {
switch (node.type) {
case 'session':
return this.sessionsStatusesService.statusToLabel(node.status as SessionStatus);
case 'task':
return this.tasksStatusesService.statusToLabel(node.status as TaskStatus);
case 'result':
return this.resultsStatusesService.statusToLabel(node.status as ResultStatus);
default:
return {
label: $localize`Unknown`,
color: 'grey',
};
}
}

/**
* Returns the associated link color.
* @param link L
Expand All @@ -362,4 +371,8 @@ export class GraphComponent<N extends ArmoniKGraphNode, L extends GraphLink<N>>
private getLinkColor(link: L): string {
return this.colorMap[link.type];
}

closeNodePanel() {
this.selectedNode.set(null);
}
}
30 changes: 30 additions & 0 deletions src/app/components/graph/inspect-node.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
mat-card {
z-index: 2;
position: absolute;
bottom: 0;
right: 0;
width: 30%;
}

mat-card-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}

article {
display: flex;
justify-content: space-between;
align-items: center;
}

section {
display: flex;
align-items: center;
gap: 0.5rem;
}

h3 {
padding: 0;
margin: 0;
}
24 changes: 24 additions & 0 deletions src/app/components/graph/inspect-node.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<mat-card appearance="outlined">
<mat-card-content>
<article>
<section>
<h3>{{ node.id }}</h3>
<button mat-icon-button (click)="copy()" i18n-matTooltip matTooltip="Copy">
<mat-icon [fontIcon]="getIcon(copyIcon())" />
</button>
</section>
<button mat-icon-button (click)="onClose()" i18n-matTooltip matTooltip="Close">
<mat-icon [fontIcon]="getIcon('cancelling')" />
</button>
</article>
<app-status-chip [status]="nodeStatusService.getNodeStatusData(node)" />
</mat-card-content>
@switch (node.type) {
@case ('task') {
<app-graph-inspect-task-actions [node]="node" />
}
@case ('result') {
<app-graph-inspect-result-actions [node]="node" />
}
}
</mat-card>
Loading
Loading