Skip to content

Commit 2b5b01a

Browse files
committed
feat(Dashboard): Integrated SDK validation
Closes #410 Signed-off-by: Jean-Baptiste Bianchi <[email protected]>
1 parent 685a728 commit 2b5b01a

File tree

9 files changed

+130
-69
lines changed

9 files changed

+130
-69
lines changed

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,24 @@
5353
}
5454
@if (problemDetails != null)
5555
{
56-
<div class="problems">
57-
<Callout Type="CalloutType.Danger" Heading="@problemDetails.Title">
58-
@problemDetails.Detail
59-
</Callout>
60-
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
61-
{
62-
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
56+
<div class="problems p-3">
57+
<Callout Color="CalloutColor.Danger" Heading="@problemDetails.Title">
58+
<p>@problemDetails.Detail</p>
59+
60+
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
6361
{
64-
<Callout Type="CalloutType.Danger" Heading="@errorContainer.Key">
62+
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
63+
{
64+
<strong>@errorContainer.Key:</strong>
6565
<ul>
6666
@foreach (string error in errorContainer.Value)
6767
{
6868
<li>@error</li>
6969
}
7070
</ul>
71-
</Callout>
71+
}
7272
}
73-
}
73+
</Callout>
7474
</div>
7575
}
7676
}

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

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,28 @@
3030
ConstructionOptions="MonacoEditorHelper.GetStandaloneEditorConstructionOptions(this.textEditorValue, false, this.MonacoEditorHelper.PreferredLanguage)"
3131
OnDidInit="OnTextBasedEditorInit"
3232
OnDidChangeModelContent="OnTextBasedValueChanged" />
33-
<div class="problems">
34-
@if (problemDetails != null)
35-
{
36-
<Callout Type="CalloutType.Danger" Heading="@problemDetails.Title">
37-
@problemDetails.Detail
38-
</Callout>
39-
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
40-
{
41-
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
33+
@if (problemDetails != null)
34+
{
35+
<div class="problems p-3">
36+
<Callout Color="CalloutColor.Danger" Heading="@problemDetails.Title">
37+
<p>@problemDetails.Detail</p>
38+
39+
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
4240
{
43-
<Callout Type="CalloutType.Danger" Heading="@errorContainer.Key">
41+
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
42+
{
43+
<strong>@errorContainer.Key:</strong>
4444
<ul>
4545
@foreach (string error in errorContainer.Value)
4646
{
4747
<li>@error</li>
4848
}
4949
</ul>
50-
</Callout>
50+
}
5151
}
52-
}
53-
}
54-
</div>
52+
</Callout>
53+
</div>
54+
}
5555
<Button Color="ButtonColor.Primary" Outline="true" Disabled="isUpdating || isSaving" Loading="isSaving" @onclick="async _ => await Store.SubmitResourceAsync()">
5656
@if (resource?.Metadata?.Generation == null || resource?.Metadata?.Generation == 0)
5757
{

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

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@
1212
// limitations under the License.
1313

1414
using JsonCons.Utilities;
15+
using Neuroglia.Collections;
1516
using Neuroglia.Data;
1617
using Semver;
1718
using ServerlessWorkflow.Sdk.Models;
19+
using ServerlessWorkflow.Sdk.Validation;
1820
using Synapse.Api.Client.Services;
1921
using Synapse.Resources;
2022
using System.Text.RegularExpressions;
@@ -33,6 +35,7 @@ namespace Synapse.Dashboard.Pages.Workflows.Create;
3335
/// <param name="navigationManager">The service used to provides an abstraction for querying and managing URI navigation</param>
3436
/// <param name="specificationSchemaManager">The service used to download the specification schemas</param>
3537
/// <param name="monacoInterop">The service to build a bridge with the monaco interop extension</param>
38+
/// <param name="workflowDefinitionValidator">The service to validate workflow defintions</param>
3639
public class CreateWorkflowViewStore(
3740
ILogger<CreateWorkflowViewStore> logger,
3841
ISynapseApiClient api,
@@ -42,7 +45,8 @@ public class CreateWorkflowViewStore(
4245
IJSRuntime jsRuntime,
4346
NavigationManager navigationManager,
4447
SpecificationSchemaManager specificationSchemaManager,
45-
MonacoInterop monacoInterop
48+
MonacoInterop monacoInterop,
49+
IWorkflowDefinitionValidator workflowDefinitionValidator
4650
)
4751
: ComponentStore<CreateWorkflowViewState>(new())
4852
{
@@ -97,6 +101,11 @@ MonacoInterop monacoInterop
97101
/// </summary>
98102
protected MonacoInterop MonacoInterop { get; } = monacoInterop;
99103

104+
/// <summary>
105+
/// Gets the service to to validate workflow defintions
106+
/// </summary>
107+
protected IWorkflowDefinitionValidator WorkflowDefinitionValidator { get; } = workflowDefinitionValidator;
108+
100109
/// <summary>
101110
/// The <see cref="BlazorMonaco.Editor.StandaloneEditorConstructionOptions"/> provider function
102111
/// </summary>
@@ -210,7 +219,7 @@ public void SetName(string? name)
210219
}
211220

212221
/// <summary>
213-
/// Sets the state's <see cref="ResourceEditorState{TResource}" /> <see cref="ProblemDetails"/>'s related data
222+
/// Sets the state's <see cref="CreateWorkflowViewState" /> <see cref="ProblemDetails"/>'s related data
214223
/// </summary>
215224
/// <param name="problem">The <see cref="ProblemDetails"/> to populate the data with</param>
216225
public void SetProblemDetails(ProblemDetails? problem)
@@ -349,7 +358,8 @@ public async Task OnDidChangeModelContent(ModelContentChangedEvent e)
349358
{
350359
if (this.TextEditor == null) return;
351360
var document = await this.TextEditor.GetValue();
352-
this.Reduce(state => state with {
361+
this.Reduce(state => state with
362+
{
353363
WorkflowDefinitionText = document
354364
});
355365
}
@@ -382,8 +392,24 @@ public async Task SaveWorkflowDefinitionAsync()
382392
try
383393
{
384394
var workflowDefinition = this.MonacoEditorHelper.PreferredLanguage == PreferredLanguage.JSON ?
385-
this.JsonSerializer.Deserialize<WorkflowDefinition>(workflowDefinitionText) :
386-
this.YamlSerializer.Deserialize<WorkflowDefinition>(workflowDefinitionText);
395+
this.JsonSerializer.Deserialize<WorkflowDefinition>(workflowDefinitionText)! :
396+
this.YamlSerializer.Deserialize<WorkflowDefinition>(workflowDefinitionText)!;
397+
var validationResult = await this.WorkflowDefinitionValidator.ValidateAsync(workflowDefinition);
398+
if (!validationResult.IsValid)
399+
{
400+
var errors = new Dictionary<string, string[]>();
401+
validationResult.Errors?.Select(e => e.Reference ?? "")?.Distinct()?.ToList()?.ForEach(reference =>
402+
{
403+
errors.Add(reference, validationResult.Errors!.Where(e => e.Reference == reference).Select(e => e.Details ?? "").ToArray());
404+
});
405+
this.Reduce(state => state with
406+
{
407+
ProblemTitle = "Invalid definition",
408+
ProblemDetail = "The workflow definition is not valid.",
409+
ProblemErrors = errors
410+
});
411+
return;
412+
}
387413
var @namespace = workflowDefinition!.Document.Namespace;
388414
var name = workflowDefinition.Document.Name;
389415
var version = workflowDefinition.Document.Version;
@@ -444,6 +470,33 @@ public async Task SaveWorkflowDefinitionAsync()
444470
{
445471
this.SetProblemDetails(ex.Problem);
446472
}
473+
catch (YamlDotNet.Core.YamlException ex)
474+
{
475+
this.Reduce(state => state with
476+
{
477+
ProblemTitle = "Serialization error",
478+
ProblemDetail = "The workflow definition cannot be serialized.",
479+
ProblemErrors = new Dictionary<string, string[]>()
480+
{
481+
{"Message", [ex.Message] },
482+
{"Start", [ex.Start.ToString()] },
483+
{"End", [ex.End.ToString()] }
484+
}
485+
});
486+
}
487+
catch (System.Text.Json.JsonException ex)
488+
{
489+
this.Reduce(state => state with
490+
{
491+
ProblemTitle = "Serialization error",
492+
ProblemDetail = "The workflow definition cannot be serialized.",
493+
ProblemErrors = new Dictionary<string, string[]>()
494+
{
495+
{"Message", [ex.Message] },
496+
{"LineNumber", [ex.LineNumber?.ToString()??""] }
497+
}
498+
});
499+
}
447500
catch (Exception ex)
448501
{
449502
this.Logger.LogError("Unable to save workflow definition: {exception}", ex.ToString());

src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/View.razor

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,28 +41,28 @@ else
4141
OnDidInit="Store.OnTextBasedEditorInitAsync"
4242
OnDidChangeModelContent="Store.OnDidChangeModelContent"
4343
CssClass="h-100" />
44-
<div class="problems">
45-
@if (problemDetails != null)
46-
{
47-
<Callout Type="CalloutType.Danger" Heading="@problemDetails.Title">
48-
@problemDetails.Detail
49-
</Callout>
50-
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
51-
{
52-
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
44+
@if (problemDetails != null)
45+
{
46+
<div class="problems p-3">
47+
<Callout Color="CalloutColor.Danger" Heading="@problemDetails.Title">
48+
<p>@problemDetails.Detail</p>
49+
50+
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
5351
{
54-
<Callout Type="CalloutType.Danger" Heading="@errorContainer.Key">
52+
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
53+
{
54+
<strong>@errorContainer.Key:</strong>
5555
<ul>
5656
@foreach (string error in errorContainer.Value)
5757
{
5858
<li>@error</li>
5959
}
6060
</ul>
61-
</Callout>
61+
}
6262
}
63-
}
64-
}
65-
</div>
63+
</Callout>
64+
</div>
65+
}
6666
<Button class="mt-3" Color="ButtonColor.Primary" Outline="true" Disabled="saving" @onclick="async (_) => await Store.SaveWorkflowDefinitionAsync()">
6767
@if(!saving)
6868
{

src/dashboard/Synapse.Dashboard/Program.cs

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

14+
using FluentValidation;
1415
using Microsoft.AspNetCore.Components.Authorization;
1516
using Neuroglia.Blazor.Dagre;
17+
using ServerlessWorkflow.Sdk.Models;
18+
using ServerlessWorkflow.Sdk.Validation;
1619
using System.Text.Json;
1720

1821
var builder = WebAssemblyHostBuilder.CreateDefault(args);
@@ -45,7 +48,17 @@
4548
{
4649
builder.Configuration.Bind("Authentication:OIDC", options.ProviderOptions);
4750
});
48-
51+
// Seems to bring conflict between scope & singleton services
52+
//builder.Services.AddServerlessWorkflowValidation();
53+
/* From AddServerlessWorkflowValidation */
54+
builder.Services.AddScoped<IWorkflowDefinitionValidator, WorkflowDefinitionValidator>();
55+
builder.Services.AddValidatorsFromAssemblyContaining<WorkflowDefinition>();
56+
var defaultPropertyNameResolver = ValidatorOptions.Global.PropertyNameResolver;
57+
ValidatorOptions.Global.PropertyNameResolver = (type, member, lambda) =>
58+
{
59+
return member == null ? defaultPropertyNameResolver(type, member, lambda) : member.Name.ToCamelCase();
60+
};
61+
/* End of AddServerlessWorkflowValidation */
4962
builder.Services.AddSingleton<IMonacoEditorHelper, MonacoEditorHelper>();
5063
builder.Services.AddScoped<IApplicationLayout, ApplicationLayout>();
5164
builder.Services.AddScoped<SpecificationSchemaManager>();

src/dashboard/Synapse.Dashboard/Synapse.Dashboard.csproj

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,9 @@
66
<ImplicitUsings>enable</ImplicitUsings>
77
<NeutralLanguage>en</NeutralLanguage>
88
<GenerateDocumentationFile>True</GenerateDocumentationFile>
9-
<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>
9+
<!--<StaticWebAssetProjectMode>Default</StaticWebAssetProjectMode>-->
1010
</PropertyGroup>
1111

12-
<ItemGroup>
13-
<Content Remove="wwwroot\css\horizontal-collapse.scss" />
14-
<Content Remove="wwwroot\css\workflow-instance-details.scss" />
15-
</ItemGroup>
16-
17-
<ItemGroup>
18-
<_ContentIncludedByDefault Remove="wwwroot\css\horizontal-collapse.scss" />
19-
<_ContentIncludedByDefault Remove="wwwroot\css\workflow-instance-details.scss" />
20-
</ItemGroup>
21-
22-
<ItemGroup>
23-
<None Include="Components\WorkflowInstanceDetails\CorrelationContextRow.razor" />
24-
<None Include="Components\WorkflowInstanceDetails\TaskInstanceRow.razor" />
25-
<None Include="wwwroot\css\horizontal-collapse.scss" />
26-
<None Include="wwwroot\css\workflow-instance-details.scss" />
27-
</ItemGroup>
28-
2912
<ItemGroup>
3013
<PackageReference Include="Blazor.Bootstrap" Version="3.0.0" />
3114
<PackageReference Include="BlazorMonaco" Version="3.2.0" />
@@ -42,10 +25,4 @@
4225
<ProjectReference Include="..\Synapse.Dashboard.StateManagement\Synapse.Dashboard.StateManagement.csproj" />
4326
</ItemGroup>
4427

45-
<ItemGroup>
46-
<Content Update="Components\DocumentDetails\DocumentDetails.razor">
47-
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
48-
</Content>
49-
</ItemGroup>
50-
5128
</Project>

src/dashboard/Synapse.Dashboard/wwwroot/css/app.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21295,3 +21295,12 @@ input[type=search]::-webkit-search-cancel-button {
2129521295
background-color: var(--vscode-editor-background);
2129621296
color: var(--vscode-editor-foreground);
2129721297
}
21298+
21299+
.problems {
21300+
position: absolute;
21301+
bottom: 65px;
21302+
left: 0;
21303+
max-height: 300px;
21304+
overflow: scroll;
21305+
width: 100%;
21306+
}

src/dashboard/Synapse.Dashboard/wwwroot/css/app.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/dashboard/Synapse.Dashboard/wwwroot/css/app.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,4 +169,13 @@ input[type="search"]::-webkit-search-cancel-button {
169169
overflow: scroll;
170170
background-color: var(--vscode-editor-background);
171171
color: var(--vscode-editor-foreground);
172+
}
173+
174+
.problems {
175+
position: absolute;
176+
bottom: 65px;
177+
left: 0;
178+
max-height: 300px;
179+
overflow: scroll;
180+
width: 100%;
172181
}

0 commit comments

Comments
 (0)