Skip to content

Commit 74089da

Browse files
committed
Merge
2 parents 18955b7 + 69f6d36 commit 74089da

File tree

12 files changed

+198
-165
lines changed

12 files changed

+198
-165
lines changed

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

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,16 +36,19 @@
3636
</svg>
3737
<DagreGraph @ref="Store.DagreGraph" OnMouseUp="OnMouseUp" Options="options" Graph="graph">
3838
<ExtraControls>
39-
<button class="btn" type="button" title="legend" @onclick="async (_) => await Store.ShowLegendAsync()">
39+
<button class="btn" type="button" title="legend" @onclick="Store.ToggleLegendAsync">
4040
<svg>
4141
<use href="#legend" />
4242
</svg>
4343
</button>
4444
</ExtraControls>
4545
</DagreGraph>
46-
}
4746

48-
<Modal @ref="Store.LegendModal" />
47+
@if (isLegendVisible)
48+
{
49+
<WorkflowDiagramLegend />
50+
}
51+
}
4952

5053
@code {
5154
[Parameter] public EventCallback<GraphEventArgs<MouseEventArgs>> OnMouseUp { get; set; }
@@ -58,6 +61,7 @@
5861

5962
IGraphViewModel? graph;
6063
IDagreGraphOptions? options = null;
64+
bool isLegendVisible = false;
6165
bool isDirty = true; // moving isDirty to the State/Store seems to have unwanted behavior, the `ShouldRender` method doesn't seem to behave properly.
6266
6367
/// <inheritdoc/>
@@ -67,18 +71,25 @@
6771
Store.WorkflowDefinition.Subscribe(value => OnStateChanged(_ => workflowDefinition = value), token: CancellationTokenSource.Token);
6872
Store.WorkflowInstances.Subscribe(value => OnStateChanged(_ => workflowInstances = value), token: CancellationTokenSource.Token);
6973
Store.Orientation.Subscribe(value => OnStateChanged(_ => orientation = value), token: CancellationTokenSource.Token);
70-
Store.Graph.SubscribeAsync(async value =>
74+
Store.IsLegendVisible.Subscribe(value => OnStateChanged(_ =>
7175
{
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 =>
76+
isLegendVisible = value;
77+
isDirty = true;
78+
}), token: CancellationTokenSource.Token);
79+
Observable.CombineLatest(
80+
Store.Graph.Where(g => g != null),
81+
Store.Options.Where(o => o != null),
82+
(graph, options) => (graph, options)
83+
).Subscribe((values) =>
7784
{
78-
this.isDirty = true;
79-
OnStateChanged(_ => options = value);
80-
if (this.Store.DagreGraph != null) await this.Store.DagreGraph.RefreshAsync();
81-
}, cancellationToken: CancellationTokenSource.Token);
85+
var (newGraph, newOptions) = values;
86+
OnStateChanged(_ =>
87+
{
88+
graph = newGraph;
89+
options = newOptions;
90+
isDirty = true;
91+
});
92+
}, token: CancellationTokenSource.Token);
8293
}
8394

8495
protected override void OnParametersSet()

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

Lines changed: 32 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -16,124 +16,40 @@
1616

1717
@namespace Synapse.Dashboard
1818

19-
<div class="d-flex align-items-center justify-content-center">
20-
<svg style="height: @(height * 14)">
21-
<g class="node call-task-node legend" transform="translate(0, @(height * 0))">
22-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
23-
<foreignObject x="0" y="0" width="@width" height="50">
24-
<div>
25-
CALL
26-
</div>
27-
</foreignObject>
28-
</g>
29-
<g class="node do-task-node legend" transform="translate(0, @(height * 1))">
30-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
31-
<foreignObject x="0" y="0" width="@width" height="50">
32-
<div>
33-
DO
34-
</div>
35-
</foreignObject>
36-
</g>
37-
<g class="node fork-task-node legend" transform="translate(0, @(height * 2))">
38-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
39-
<foreignObject x="0" y="0" width="@width" height="50">
40-
<div>
41-
FORK
42-
</div>
43-
</foreignObject>
44-
</g>
45-
<g class="node for-task-node legend" transform="translate(0, @(height * 3))">
46-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
47-
<foreignObject x="0" y="0" width="@width" height="50">
48-
<div>
49-
FOR
50-
</div>
51-
</foreignObject>
52-
</g>
53-
<g class="node listen-task-node legend" transform="translate(0, @(height * 4))">
54-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
55-
<foreignObject x="0" y="0" width="@width" height="50">
56-
<div>
57-
LISTEN
58-
</div>
59-
</foreignObject>
60-
</g>
61-
<g class="node run-task-node legend" transform="translate(0, @(height * 5))">
62-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
63-
<foreignObject x="0" y="0" width="@width" height="50">
64-
<div>
65-
RUN
66-
</div>
67-
</foreignObject>
68-
</g>
69-
<g class="node set-task-node legend" transform="translate(0, @(height * 6))">
70-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
71-
<foreignObject x="0" y="0" width="@width" height="50">
72-
<div>
73-
SET
74-
</div>
75-
</foreignObject>
76-
</g>
77-
<g class="node switch-task-node legend" transform="translate(0, @(height * 7))">
78-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
79-
<foreignObject x="0" y="0" width="@width" height="50">
80-
<div>
81-
SWITCH
82-
</div>
83-
</foreignObject>
84-
</g>
85-
<g class="node try-catch-task-node legend" transform="translate(0, @(height * 8))">
86-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
87-
<foreignObject x="0" y="0" width="@width" height="50">
88-
<div>
89-
TRY..CATCH
90-
</div>
91-
</foreignObject>
92-
</g>
93-
<g class="node try-task-node legend" transform="translate(0, @(height * 9))">
94-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
95-
<foreignObject x="0" y="0" width="@width" height="50">
96-
<div>
97-
TRY
98-
</div>
99-
</foreignObject>
100-
</g>
101-
<g class="node emit-task-node legend" transform="translate(0, @(height * 10))">
102-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
103-
<foreignObject x="0" y="0" width="@width" height="50">
104-
<div>
105-
EMIT
106-
</div>
107-
</foreignObject>
108-
</g>
109-
<g class="node wait-task-node legend" transform="translate(0, @(height * 11))">
110-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
111-
<foreignObject x="0" y="0" width="@width" height="50">
112-
<div>
113-
WAIT
114-
</div>
115-
</foreignObject>
116-
</g>
117-
<g class="node catch-task-node legend" transform="translate(0, @(height * 12))">
118-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
119-
<foreignObject x="0" y="0" width="@width" height="50">
120-
<div>
121-
CATCH
122-
</div>
123-
</foreignObject>
124-
</g>
125-
<g class="node raise-task-node legend" transform="translate(0, @(height * 13))">
126-
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
127-
<foreignObject x="0" y="0" width="@width" height="50">
128-
<div>
129-
RAISE
130-
</div>
131-
</foreignObject>
132-
</g>
19+
<div class="d-flex align-items-center justify-content-center position-absolute" style="bottom: @(height)px; left: @(height)px;">
20+
<svg style="height: @(height * nodeTypes.Count())px">
21+
@for(int i = 0, c = nodeTypes.Count(); i<c; i++)
22+
{
23+
var nodeType = nodeTypes.ElementAt(i);
24+
<g class="node @(nodeType)-task-node legend" transform="translate(0, @(height * i))">
25+
<rect class="node-rectangle" x="0" y="0" width="@width" height="@height"></rect>
26+
<foreignObject x="0" y="0" width="@width" height="@height">
27+
<div>
28+
@nodeType.ToUpper().Replace("-", "..")
29+
</div>
30+
</foreignObject>
31+
</g>
32+
}
13333
</svg>
13434
</div>
13535

13636
@code {
137-
int width = 300;
138-
int height = 50;
37+
int width = 100;
38+
int height = 25;
39+
IEnumerable<string> nodeTypes = [
40+
"call",
41+
"do",
42+
"fork",
43+
"for",
44+
"listen",
45+
"run",
46+
"set",
47+
"switch",
48+
"try-catch",
49+
"try",
50+
"emit",
51+
"wait",
52+
"catch",
53+
"raise"
54+
];
13955
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,9 @@ public record WorkflowDiagramState
3535
/// Gets/sets the <see cref="WorkflowInstance"/>s to get the activity counts from
3636
/// </summary>
3737
public EquatableList<WorkflowInstance> WorkflowInstances { get; set; } = [];
38+
39+
/// <summary>
40+
/// Gets/sets a boolean indicating if the legend is visible
41+
/// </summary>
42+
public bool IsLegendVisible { get; set; } = false;
3843
}

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

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
using ServerlessWorkflow.Sdk.Models;
1717
using Synapse.Resources;
1818
using System.Text.RegularExpressions;
19-
using System.Threading.Tasks;
2019

2120
namespace Synapse.Dashboard.Components.WorkflowDiagramStateManagement;
2221

@@ -61,6 +60,11 @@ IWorkflowGraphBuilder workflowGraphBuilder
6160
/// </summary>
6261
public IObservable<EquatableList<WorkflowInstance>> WorkflowInstances => this.Select(state => state.WorkflowInstances).DistinctUntilChanged();
6362

63+
/// <summary>
64+
/// Gets an <see cref="IObservable{T}"/> used to observe <see cref="WorkflowDiagramState.IsLegendVisible"/> changes
65+
/// </summary>
66+
public IObservable<bool> IsLegendVisible => this.Select(state => state.IsLegendVisible).DistinctUntilChanged();
67+
6468
/// <summary>
6569
/// Gets an <see cref="IObservable{T}"/> used to observe <see cref="DagreGraphOptions"/> changes
6670
/// </summary>
@@ -115,7 +119,8 @@ IWorkflowGraphBuilder workflowGraphBuilder
115119
((IWorkflowNodeViewModel)node).FaultedInstancesCount = faultedCount;
116120
}
117121
return graph;
118-
});
122+
})
123+
.DistinctUntilChanged();
119124
#endregion
120125

121126
#region Setters
@@ -158,15 +163,15 @@ public void SetWorkflowInstances(EquatableList<WorkflowInstance> workflowInstanc
158163

159164
#region Actions
160165
/// <summary>
161-
/// Shows the legend modal
166+
/// Toggles the legend visibily
162167
/// </summary>
163-
/// <returns></returns>
164-
public async Task ShowLegendAsync()
168+
public void ToggleLegendAsync()
165169
{
166-
if (this.LegendModal != null)
170+
var isLegendVisible = this.Get(state => state.IsLegendVisible);
171+
this.Reduce(state => state with
167172
{
168-
await this.LegendModal.ShowAsync<WorkflowDiagramLegend>(title: "Legend");
169-
}
173+
IsLegendVisible = !isLegendVisible
174+
});
170175
}
171176
#endregion
172177
}

src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/Store.cs

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

14-
using Neuroglia.Collections;
15-
using Neuroglia.Data.Infrastructure.ResourceOriented;
14+
using Neuroglia.Blazor.Dagre.Models;
1615
using ServerlessWorkflow.Sdk.Models;
1716
using Synapse.Api.Client.Services;
1817
using Synapse.Resources;
@@ -28,13 +27,15 @@ namespace Synapse.Dashboard.Pages.Workflows.Details;
2827
/// <param name="monacoEditorHelper">The service used ease Monaco Editor interactions</param>
2928
/// <param name="jsonSerializer">The service used to serialize and deserialize JSON</param>
3029
/// <param name="yamlSerializer">The service used to serialize and deserialize YAML</param>
30+
/// <param name="monacoInterop">The service used to build a bridge with the monaco interop extension</param>
3131
public class WorkflowDetailsStore(
3232
ISynapseApiClient apiClient,
3333
ResourceWatchEventHubClient resourceEventHub,
3434
IJSRuntime jsRuntime,
3535
IMonacoEditorHelper monacoEditorHelper,
3636
IJsonSerializer jsonSerializer,
37-
IYamlSerializer yamlSerializer
37+
IYamlSerializer yamlSerializer,
38+
MonacoInterop monacoInterop
3839
)
3940
: NamespacedResourceManagementComponentStore<WorkflowDetailsState, WorkflowInstance>(apiClient, resourceEventHub)
4041
{
@@ -62,6 +63,11 @@ IYamlSerializer yamlSerializer
6263
/// </summary>
6364
protected IYamlSerializer YamlSerializer { get; } = yamlSerializer;
6465

66+
/// <summary>
67+
/// Gets the service used to build a bridge with the monaco interop extension
68+
/// </summary>
69+
protected MonacoInterop MonacoInterop { get; } = monacoInterop;
70+
6571
/// <summary>
6672
/// Gets/sets the <see cref="BlazorMonaco.Editor.StandaloneEditorConstructionOptions"/> provider function
6773
/// </summary>
@@ -247,7 +253,6 @@ public async Task ToggleTextBasedEditorLanguageAsync(string _)
247253
/// <returns></returns>
248254
public async Task OnTextBasedEditorInitAsync()
249255
{
250-
await Task.Delay(1);
251256
await this.SetTextBasedEditorLanguageAsync();
252257
await this.SetTextEditorValueAsync();
253258
}
@@ -358,25 +363,27 @@ public async Task DeleteWorkflowInstanceAsync(WorkflowInstance workflowInstance)
358363
{
359364
await this.ApiClient.ManageNamespaced<WorkflowInstance>().DeleteAsync(workflowInstance.GetName(), workflowInstance.GetNamespace()!).ConfigureAwait(false);
360365
}
366+
367+
/// <summary>
368+
/// Selects the target node in the code editor
369+
/// </summary>
370+
/// <param name="e">The source of the event</param>
371+
public async Task SelectNodeInEditor(GraphEventArgs<MouseEventArgs> e)
372+
{
373+
if (e.GraphElement == null) return;
374+
if (this.TextEditor == null) return;
375+
var source = await this.TextEditor.GetValue();
376+
var pointer = e.GraphElement.Id;
377+
var language = this.MonacoEditorHelper.PreferredLanguage;
378+
var range = await this.MonacoInterop.GetJsonPointerRange(source, pointer, language);
379+
await this.TextEditor.SetSelection(range, string.Empty);
380+
await this.TextEditor.RevealRangeInCenter(range);
381+
}
361382
#endregion
362383

363384
/// <inheritdoc/>
364385
public override async Task InitializeAsync()
365386
{
366-
this.WorkflowDefinition.Where(definition => definition != null).SubscribeAsync(async (definition) =>
367-
{
368-
await Task.Delay(1);
369-
var document = this.JsonSerializer.SerializeToText(definition.Clone());
370-
this.Reduce(state => state with
371-
{
372-
WorkflowDefinitionJson = document
373-
});
374-
await this.SetTextEditorValueAsync();
375-
if (this.MonacoEditorHelper.PreferredLanguage != PreferredLanguage.YAML)
376-
{
377-
await this.MonacoEditorHelper.ChangePreferredLanguageAsync(PreferredLanguage.YAML);
378-
}
379-
}, cancellationToken: this.CancellationTokenSource.Token);
380387
Observable.CombineLatest(
381388
this.Namespace.Where(ns => !string.IsNullOrWhiteSpace(ns)),
382389
this.ActiveResourceName.Where(name => !string.IsNullOrWhiteSpace(name)),
@@ -392,6 +399,20 @@ public override async Task InitializeAsync()
392399
this.RemoveLabelSelector(SynapseDefaults.Resources.Labels.WorkflowVersion);
393400
this.AddLabelSelector(new(SynapseDefaults.Resources.Labels.WorkflowVersion, LabelSelectionOperator.Equals, version!));
394401
});
402+
this.WorkflowDefinition.Where(definition => definition != null).SubscribeAsync(async (definition) =>
403+
{
404+
await Task.Delay(1);
405+
var document = this.JsonSerializer.SerializeToText(definition.Clone());
406+
this.Reduce(state => state with
407+
{
408+
WorkflowDefinitionJson = document
409+
});
410+
await this.SetTextEditorValueAsync();
411+
if (this.MonacoEditorHelper.PreferredLanguage != PreferredLanguage.YAML)
412+
{
413+
await this.MonacoEditorHelper.ChangePreferredLanguageAsync(PreferredLanguage.YAML);
414+
}
415+
}, cancellationToken: this.CancellationTokenSource.Token);
395416
await base.InitializeAsync();
396417
}
397418

0 commit comments

Comments
 (0)