Skip to content

Commit b7d2505

Browse files
committed
feat(Dashboard): added activity counters on the graph
Signed-off-by: Jean-Baptiste Bianchi <[email protected]>
1 parent 2b8bb18 commit b7d2505

File tree

16 files changed

+581
-76
lines changed

16 files changed

+581
-76
lines changed

src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/StartNodeViewModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
// limitations under the License.
1313

1414
using Neuroglia.Blazor.Dagre;
15-
using System.Xml.Linq;
1615

1716
namespace Synapse.Dashboard.Components;
1817

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
@*
2+
Copyright © 2022-Present The Synapse Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*@
16+
17+
@namespace Synapse.Dashboard
18+
@using System.Collections.ObjectModel
19+
20+
@if (ActiveInstances > 0)
21+
{
22+
<g class="badge activity-badge activity-badge--active" transform="translate(@ActiveX, @ActiveY)">
23+
<circle cx="0" cy="0" r="@ActivityRadius" />
24+
<foreignObject class="activity-badge__label"
25+
x="@CountXY"
26+
y="@CountXY"
27+
width="@CountSize"
28+
height="@CountSize">
29+
<div style="width: @(CountSize)px;height: @(CountSize)px;">
30+
<span>@ActiveInstances</span>
31+
</div>
32+
</foreignObject>
33+
</g>
34+
}
35+
@if (CompensatedInstances > 0)
36+
{
37+
<g class="badge activity-badge activity-badge--compensated" transform="translate(@CompensatedX, @CompensatedY)">
38+
>
39+
<circle cx="0" cy="0" r="@ActivityRadius" />
40+
<foreignObject class="activity-badge__label"
41+
x="@CountXY"
42+
y="@CountXY"
43+
width="@CountSize"
44+
height="@CountSize">
45+
<div style="width: @(CountSize)px;height: @(CountSize)px;">
46+
<span>@CompensatedInstances</span>
47+
</div>
48+
</foreignObject>
49+
</g>
50+
}
51+
@if (FaultedInstances > 0)
52+
{
53+
<g class="badge activity-badge activity-badge--faulted" transform="translate(@FaultedX, @FaultedY)">
54+
>
55+
<circle cx="0" cy="0" r="@ActivityRadius" />
56+
<foreignObject class="activity-badge__label"
57+
x="@CountXY"
58+
y="@CountXY"
59+
width="@CountSize"
60+
height="@CountSize">
61+
<div style="width: @(CountSize)px;height: @(CountSize)px;">
62+
<span>@FaultedInstances</span>
63+
</div>
64+
</foreignObject>
65+
</g>
66+
}
67+
68+
@code {
69+
[CascadingParameter(Name = "ActiveInstances")] public int ActiveInstances { get; set; } = 0;
70+
71+
[CascadingParameter(Name = "FaultedInstances")] public int FaultedInstances { get; set; } = 0;
72+
73+
[CascadingParameter(Name = "CompensatedInstances")] public int CompensatedInstances { get; set; } = 0;
74+
75+
[CascadingParameter(Name = "Node")] public INodeViewModel Node { get; set; } = null!;
76+
77+
protected virtual string ActivityRadius { get; set; } = "";
78+
protected virtual string ActiveX { get; set; } = "";
79+
protected virtual string ActiveY { get; set; } = "";
80+
protected virtual string FaultedX { get; set; } = "";
81+
protected virtual string FaultedY { get; set; } = "";
82+
protected virtual string CompensatedX { get; set; } = "";
83+
protected virtual string CompensatedY { get; set; } = "";
84+
protected virtual string CountSize { get; set; } = "";
85+
protected virtual string CountXY { get; set; } = "";
86+
double radius = 15;
87+
88+
protected override void OnInitialized()
89+
{
90+
base.OnInitialized();
91+
this.ActivityRadius = radius.ToInvariantString();
92+
this.CountSize = (radius * 2).ToInvariantString();
93+
this.CountXY = (0 - radius).ToInvariantString();
94+
}
95+
96+
protected override void OnParametersSet()
97+
{
98+
double divider = this.Node.Shape == NodeShape.Circle || this.Node.Shape == NodeShape.Ellipse ? 1 : 2;
99+
double offset = radius * 4;
100+
double activeOffset = 0;
101+
double compensatedOffset = 0;
102+
double faultedOffset = 0;
103+
if (this.ActiveInstances > 0)
104+
{
105+
compensatedOffset += 1;
106+
faultedOffset += 1;
107+
if (this.CompensatedInstances > 0)
108+
{
109+
faultedOffset += 1;
110+
}
111+
}
112+
else if (this.CompensatedInstances > 0)
113+
{
114+
faultedOffset += 1;
115+
}
116+
this.ActiveX = (((this.Node.Bounds?.Width ?? Neuroglia.Blazor.Dagre.Constants.ClusterWidth) - (offset * activeOffset)) / divider).ToInvariantString();
117+
this.CompensatedX = (((this.Node.Bounds?.Width ?? Neuroglia.Blazor.Dagre.Constants.ClusterWidth) - (offset * compensatedOffset)) / divider).ToInvariantString();
118+
this.FaultedX = (((this.Node.Bounds?.Width ?? Neuroglia.Blazor.Dagre.Constants.ClusterWidth) - (offset * faultedOffset)) / divider).ToInvariantString();
119+
this.ActiveY = ((this.Node.Bounds?.Height ?? Neuroglia.Blazor.Dagre.Constants.ClusterHeight) / divider).ToInvariantString();
120+
this.CompensatedY = ((this.Node.Bounds?.Height ?? Neuroglia.Blazor.Dagre.Constants.ClusterHeight) / divider).ToInvariantString();
121+
this.FaultedY = ((this.Node.Bounds?.Height ?? Neuroglia.Blazor.Dagre.Constants.ClusterHeight) / divider).ToInvariantString();
122+
}
123+
}

src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/Templates/NodeTemplate.razor

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
@*
2+
Copyright © 2024-Present The Synapse Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*@
16+
117
@namespace Synapse.Dashboard
218
@inherits Neuroglia.Blazor.Dagre.Templates.NodeTemplate
319

@@ -12,7 +28,7 @@
1228
</CascadingValue>
1329
<CascadingValue Name="ActiveInstances" Value="WorkflowNode.OperativeInstancesCount">
1430
<CascadingValue Name="FaultedInstances" Value="WorkflowNode.FaultedInstancesCount">
15-
31+
<NodeActivityBadge />
1632
</CascadingValue>
1733
</CascadingValue>
1834
@if (Graph.EnableProfiling) {

src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowClusterViewModel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14-
using Neuroglia.Blazor.Dagre;
1514
using Neuroglia.Blazor.Dagre.Models;
1615
using ServerlessWorkflow.Sdk.Models;
1716
using System.Text.Json.Serialization;
Lines changed: 60 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,23 @@
1-
@namespace Synapse.Dashboard
1+
@*
2+
Copyright © 2024-Present The Synapse Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*@
16+
217
@using ServerlessWorkflow.Sdk.Models
3-
@using System.Diagnostics
4-
@inject IWorkflowGraphBuilder WorkflowGraphBuilder
18+
@using Synapse.Dashboard.Components.WorkflowDiagramStateManagement
19+
@namespace Synapse.Dashboard
20+
@inherits StatefulComponent<WorkflowDiagram,WorkflowDiagramStore, WorkflowDiagramState>
521

622
@if (graph != null)
723
{
@@ -18,9 +34,9 @@
1834
</svg>
1935
</defs>
2036
</svg>
21-
<DagreGraph @ref="dagre" Graph="graph" OnMouseUp="OnMouseUp" Options="options">
37+
<DagreGraph @ref="Store.DagreGraph" OnMouseUp="OnMouseUp" Options="options" Graph="graph">
2238
<ExtraControls>
23-
<button class="btn" type="button" title="legend" @onclick="ShowLegendAsync">
39+
<button class="btn" type="button" title="legend" @onclick="async (_) => await Store.ShowLegendAsync()">
2440
<svg>
2541
<use href="#legend" />
2642
</svg>
@@ -29,35 +45,56 @@
2945
</DagreGraph>
3046
}
3147

32-
<Modal @ref="modal" />
48+
<Modal @ref="Store.LegendModal" />
3349

34-
@code{
50+
@code {
51+
[Parameter] public EventCallback<GraphEventArgs<MouseEventArgs>> OnMouseUp { get; set; }
52+
WorkflowDefinition? workflowDefinition = null;
53+
[Parameter] public WorkflowDefinition WorkflowDefinition { get; set; } = null!;
54+
WorkflowDiagramOrientation orientation = default!;
55+
[Parameter] public WorkflowDiagramOrientation Orientation { get; set; } = WorkflowDiagramOrientation.TopToBottom;
56+
EquatableList<WorkflowInstance> workflowInstances = [];
57+
[Parameter] public EquatableList<WorkflowInstance> WorkflowInstances { get; set; } = [];
3558

36-
WorkflowDefinition workflowDefinition = null!;
37-
DagreGraph? dagre;
3859
IGraphViewModel? graph;
3960
IDagreGraphOptions? options = null;
40-
bool isDirty = true;
41-
private Modal modal = default!;
61+
bool isDirty = true; // moving isDirty to the State/Store seems to have unwanted behavior, the `ShouldRender` method doesn't seem to behave properly.
4262
43-
[Parameter] public WorkflowDefinition WorkflowDefinition { get; set; } = null!;
44-
45-
[Parameter] public WorkflowDiagramOrientation Orientation { get; set; } = WorkflowDiagramOrientation.TopToBottom;
46-
47-
[Parameter] public EventCallback<GraphEventArgs<MouseEventArgs>> OnMouseUp { get; set; }
63+
/// <inheritdoc/>
64+
protected override async Task OnInitializedAsync()
65+
{
66+
await base.OnInitializedAsync().ConfigureAwait(false);
67+
Store.WorkflowDefinition.Subscribe(value => OnStateChanged(_ => workflowDefinition = value), token: CancellationTokenSource.Token);
68+
Store.WorkflowInstances.Subscribe(value => OnStateChanged(_ => workflowInstances = value), token: CancellationTokenSource.Token);
69+
Store.Orientation.Subscribe(value => OnStateChanged(_ => orientation = value), token: CancellationTokenSource.Token);
70+
Store.Graph.SubscribeAsync(async value =>
71+
{
72+
this.isDirty = true;
73+
OnStateChanged(_ => graph = value);
74+
if (this.Store.DagreGraph != null) await this.Store.DagreGraph.RefreshAsync();
75+
}, cancellationToken: CancellationTokenSource.Token);
76+
Store.Options.SubscribeAsync(async value =>
77+
{
78+
this.isDirty = true;
79+
OnStateChanged(_ => options = value);
80+
if (this.Store.DagreGraph != null) await this.Store.DagreGraph.RefreshAsync();
81+
}, cancellationToken: CancellationTokenSource.Token);
82+
}
4883

4984
protected override void OnParametersSet()
5085
{
5186
base.OnParametersSet();
87+
if (this.Orientation != this.orientation)
88+
{
89+
this.Store.SetOrientation(this.Orientation);
90+
}
5291
if (this.WorkflowDefinition != null && this.WorkflowDefinition != this.workflowDefinition)
5392
{
54-
this.workflowDefinition = this.WorkflowDefinition;
55-
this.options = new DagreGraphOptions()
56-
{
57-
Direction = this.Orientation == WorkflowDiagramOrientation.LeftToRight ? DagreGraphDirection.LeftToRight : DagreGraphDirection.TopToBottom
58-
};
59-
this.graph = this.WorkflowGraphBuilder.Build(this.workflowDefinition);
60-
this.isDirty = true;
93+
this.Store.SetWorkflowDefinition(this.WorkflowDefinition);
94+
}
95+
if (this.WorkflowInstances != this.workflowInstances)
96+
{
97+
this.Store.SetWorkflowInstances(this.WorkflowInstances);
6198
}
6299
}
63100

@@ -68,9 +105,4 @@
68105
return true;
69106
}
70107

71-
private async Task ShowLegendAsync()
72-
{
73-
await modal.ShowAsync<WorkflowDiagramLegend>(title: "Legend");
74-
}
75-
76108
}

src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagramLegend.razor

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,20 @@
1-
@namespace Synapse.Dashboard
1+
@*
2+
Copyright © 2024-Present The Synapse Authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*@
16+
17+
@namespace Synapse.Dashboard
218

319
<div class="d-flex align-items-center justify-content-center">
420
<svg style="height: @(height * 14)">
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Copyright © 2024-Present The Synapse Authors
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License"),
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
using ServerlessWorkflow.Sdk.Models;
15+
using Synapse.Resources;
16+
17+
namespace Synapse.Dashboard.Components.WorkflowDiagramStateManagement;
18+
19+
/// <summary>
20+
/// Represents the state of a <see cref="WorkflowDiagram"/>
21+
/// </summary>
22+
public record WorkflowDiagramState
23+
{
24+
/// <summary>
25+
/// Gets/sets the <see cref="ServerlessWorkflow.Sdk.Models.WorkflowDefinition"/> to build a diagram for
26+
/// </summary>
27+
public WorkflowDefinition? WorkflowDefinition { get; set; } = null;
28+
29+
/// <summary>
30+
/// Gets/sets the <see cref="WorkflowDiagramOrientation"/> of the diagram
31+
/// </summary>
32+
public WorkflowDiagramOrientation Orientation { get; set; } = default!;
33+
34+
/// <summary>
35+
/// Gets/sets the <see cref="WorkflowInstance"/>s to get the activity counts from
36+
/// </summary>
37+
public EquatableList<WorkflowInstance> WorkflowInstances { get; set; } = [];
38+
}

0 commit comments

Comments
 (0)