Skip to content

Commit 05d0a43

Browse files
authored
Merge pull request #469 from serverlessworkflow/fix-457-workflow-existence-check
Improved workflow creation vs update process in the Dashboard
2 parents c4181aa + dc4d9f8 commit 05d0a43

File tree

6 files changed

+139
-81
lines changed

6 files changed

+139
-81
lines changed

src/dashboard/Synapse.Dashboard/Pages/Functions/Create/State.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
namespace Synapse.Dashboard.Pages.Functions.Create;
1919

2020
/// <summary>
21-
/// The <see cref="State{TState}"/> of the workflow editor
21+
/// The <see cref="State{TState}"/> of the function editor
2222
/// </summary>
2323
[Feature]
2424
public record CreateFunctionViewState
@@ -40,7 +40,12 @@ public record CreateFunctionViewState
4040
public SemVersion Version { get; set; } = new SemVersion(1, 0, 0);
4141

4242
/// <summary>
43-
/// Gets/sets the definition of the workflow to create
43+
/// Gets/sets a boolean determining if the function is new or if it's an update
44+
/// </summary>
45+
public bool IsNew { get; set; } = true;
46+
47+
/// <summary>
48+
/// Gets/sets the definition of the function to create
4449
/// </summary>
4550
public TaskDefinition? Function { get; set; } = null;
4651

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

Lines changed: 52 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using Semver;
1717
using ServerlessWorkflow.Sdk.Models;
1818
using Synapse.Api.Client.Services;
19+
using Synapse.Dashboard.Pages.Workflows.Create;
1920
using Synapse.Resources;
2021

2122
namespace Synapse.Dashboard.Pages.Functions.Create;
@@ -189,7 +190,6 @@ MonacoInterop monacoInterop
189190
#endregion
190191

191192
#region Setters
192-
193193
/// <summary>
194194
/// Sets the state's <see cref="CreateFunctionViewState.Name"/>
195195
/// </summary>
@@ -215,6 +215,18 @@ public void SetChosenName(string? name)
215215
});
216216
}
217217

218+
/// <summary>
219+
/// Sets the state's <see cref="CreateFunctionViewState.IsNew"/>
220+
/// </summary>
221+
/// <param name="isNew">The new <see cref="CreateFunctionViewState.IsNew"/> value</param>
222+
public void SetIsNew(bool isNew)
223+
{
224+
this.Reduce(state => state with
225+
{
226+
IsNew = isNew
227+
});
228+
}
229+
218230
/// <summary>
219231
/// Sets the state's <see cref="CreateFunctionViewState" /> <see cref="ProblemDetails"/>'s related data
220232
/// </summary>
@@ -393,6 +405,7 @@ public async Task SaveCustomFunctionAsync()
393405
this.YamlSerializer.Deserialize<TaskDefinition>(functionText)!;
394406
var name = this.Get(state => state.Name) ?? this.Get(state => state.ChosenName);
395407
var version = this.Get(state => state.Version).ToString();
408+
var isNew = this.Get(state => state.IsNew);
396409
if (string.IsNullOrEmpty(name))
397410
{
398411
this.Reduce(state => state with
@@ -407,41 +420,41 @@ public async Task SaveCustomFunctionAsync()
407420
Saving = true
408421
});
409422
CustomFunction? resource = null;
410-
try
411-
{
412-
resource = await this.ApiClient.CustomFunctions.GetAsync(name);
413-
}
414-
catch
423+
if (isNew)
415424
{
416-
// Assume 404, might need actual handling
417-
}
418-
if (resource == null)
419-
{
420-
resource = await this.ApiClient.CustomFunctions.CreateAsync(new()
425+
426+
try
421427
{
422-
Metadata = new()
428+
resource = await this.ApiClient.CustomFunctions.CreateAsync(new()
423429
{
424-
Name = name
425-
},
426-
Spec = new()
427-
{
428-
Versions = [new(version, function)]
429-
}
430-
});
431-
}
432-
else
433-
{
434-
var updatedResource = resource.Clone()!;
435-
updatedResource.Spec.Versions.Add(new(version, function));
436-
var jsonPatch = JsonPatch.FromDiff(this.JsonSerializer.SerializeToElement(resource)!.Value, this.JsonSerializer.SerializeToElement(updatedResource)!.Value);
437-
var patch = this.JsonSerializer.Deserialize<Json.Patch.JsonPatch>(jsonPatch.RootElement);
438-
if (patch != null)
430+
Metadata = new()
431+
{
432+
Name = name
433+
},
434+
Spec = new()
435+
{
436+
Versions = [new(version, function)]
437+
}
438+
});
439+
this.NavigationManager.NavigateTo($"/functions/{name}");
440+
return;
441+
}
442+
catch (ProblemDetailsException ex) when (ex.Problem.Title == "Conflict" && ex.Problem.Detail != null && ex.Problem.Detail.EndsWith("already exists"))
439443
{
440-
var resourcePatch = new Patch(PatchType.JsonPatch, jsonPatch);
441-
await this.ApiClient.ManageCluster<CustomFunction>().PatchAsync(name, resourcePatch, null, this.CancellationTokenSource.Token);
444+
// the function exists, try to update it instead
442445
}
443446
}
444-
this.NavigationManager.NavigateTo($"/functions/{name}");
447+
resource = await this.ApiClient.CustomFunctions.GetAsync(name);
448+
var updatedResource = resource.Clone()!;
449+
updatedResource.Spec.Versions.Add(new(version, function));
450+
var jsonPatch = JsonPatch.FromDiff(this.JsonSerializer.SerializeToElement(resource)!.Value, this.JsonSerializer.SerializeToElement(updatedResource)!.Value);
451+
var patch = this.JsonSerializer.Deserialize<Json.Patch.JsonPatch>(jsonPatch.RootElement);
452+
if (patch != null)
453+
{
454+
var resourcePatch = new Patch(PatchType.JsonPatch, jsonPatch);
455+
await this.ApiClient.ManageCluster<CustomFunction>().PatchAsync(name, resourcePatch, null, this.CancellationTokenSource.Token);
456+
}
457+
445458
}
446459
catch (ProblemDetailsException ex)
447460
{
@@ -477,6 +490,15 @@ public async Task SaveCustomFunctionAsync()
477490
catch (Exception ex)
478491
{
479492
this.Logger.LogError("Unable to save function definition: {exception}", ex.ToString());
493+
this.Reduce(state => state with
494+
{
495+
ProblemTitle = "Error",
496+
ProblemDetail = "An error occurred while saving the function.",
497+
ProblemErrors = new Dictionary<string, string[]>()
498+
{
499+
{"Message", [ex.ToString()] }
500+
}
501+
});
480502
}
481503
finally
482504
{

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ else
4747
<PreferredLanguageSelector PreferredLanguageChange="Store.ToggleTextBasedEditorLanguageAsync" />
4848
</div>
4949
<StandaloneCodeEditor @ref="Store.TextEditor"
50-
ConstructionOptions="Store.StandaloneEditorConstructionOptions"
51-
OnDidInit="Store.OnTextBasedEditorInitAsync"
52-
OnDidChangeModelContent="Store.OnDidChangeModelContent"
53-
CssClass="h-100" />
50+
ConstructionOptions="Store.StandaloneEditorConstructionOptions"
51+
OnDidInit="Store.OnTextBasedEditorInitAsync"
52+
OnDidChangeModelContent="Store.OnDidChangeModelContent"
53+
CssClass="h-100" />
5454
@if (problemDetails != null)
5555
{
5656
<div class="problems px-3">
@@ -104,6 +104,7 @@ else
104104
await base.OnInitializedAsync();
105105
BreadcrumbManager.Use(Breadcrumbs.Functions);
106106
BreadcrumbManager.Add(new("New", "/functions/new"));
107+
Store.SetIsNew(true);
107108
Store.Name.Subscribe(value => OnStateChanged(_ => name = value), token: CancellationTokenSource.Token);
108109
Store.ChosenName.Subscribe(value => OnStateChanged(_ => chosenName = value), token: CancellationTokenSource.Token);
109110
Store.Version.Subscribe(value => OnStateChanged(_ => version = value?.ToString()), token: CancellationTokenSource.Token);
@@ -115,7 +116,11 @@ else
115116
/// <inheritdoc/>
116117
protected override void OnParametersSet()
117118
{
118-
if (Name != name) Store.SetName(Name);
119+
if (Name != name)
120+
{
121+
Store.SetName(Name);
122+
Store.SetIsNew(false);
123+
}
119124
}
120125

121126
protected void SetName()

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ public record CreateWorkflowViewState
3636
/// </summary>
3737
public string? DslVersion { get; set; }
3838

39+
/// <summary>
40+
/// Gets/sets a boolean determining if the workflow is new or if it's an update
41+
/// </summary>
42+
public bool IsNew { get; set; } = true;
43+
3944
/// <summary>
4045
/// Gets/sets the definition of the workflow to create
4146
/// </summary>

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

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

1414
using JsonCons.Utilities;
15-
using Neuroglia.Collections;
1615
using Neuroglia.Data;
1716
using Semver;
1817
using ServerlessWorkflow.Sdk.Models;
@@ -102,7 +101,7 @@ IWorkflowDefinitionValidator workflowDefinitionValidator
102101
protected MonacoInterop MonacoInterop { get; } = monacoInterop;
103102

104103
/// <summary>
105-
/// Gets the service to to validate workflow defintions
104+
/// Gets the service to to validate workflow definitions
106105
/// </summary>
107106
protected IWorkflowDefinitionValidator WorkflowDefinitionValidator { get; } = workflowDefinitionValidator;
108107

@@ -218,6 +217,18 @@ public void SetName(string? name)
218217
});
219218
}
220219

220+
/// <summary>
221+
/// Sets the state's <see cref="CreateWorkflowViewState.IsNew"/>
222+
/// </summary>
223+
/// <param name="isNew">The new <see cref="CreateWorkflowViewState.IsNew"/> value</param>
224+
public void SetIsNew(bool isNew)
225+
{
226+
this.Reduce(state => state with
227+
{
228+
IsNew = isNew
229+
});
230+
}
231+
221232
/// <summary>
222233
/// Sets the state's <see cref="CreateWorkflowViewState" /> <see cref="ProblemDetails"/>'s related data
223234
/// </summary>
@@ -415,57 +426,56 @@ public async Task SaveWorkflowDefinitionAsync()
415426
var @namespace = workflowDefinition!.Document.Namespace;
416427
var name = workflowDefinition.Document.Name;
417428
var version = workflowDefinition.Document.Version;
429+
var isNew = this.Get(state => state.IsNew);
418430
this.Reduce(s => s with
419431
{
420432
Saving = true
421433
});
422434
Workflow? workflow = null;
423-
try
435+
if (isNew)
424436
{
425-
workflow = await this.Api.Workflows.GetAsync(name, @namespace);
426-
}
427-
catch
428-
{
429-
// Assume 404, might need actual handling
430-
}
431-
if (workflow == null)
432-
{
433-
workflow = await this.Api.Workflows.CreateAsync(new()
434-
{
435-
Metadata = new()
437+
try {
438+
workflow = await this.Api.Workflows.CreateAsync(new()
436439
{
437-
Namespace = workflowDefinition!.Document.Namespace,
438-
Name = workflowDefinition.Document.Name
439-
},
440-
Spec = new()
441-
{
442-
Versions = [workflowDefinition]
443-
}
444-
});
445-
}
446-
else
447-
{
448-
var updatedResource = workflow.Clone()!;
449-
var documentVersion = SemVersion.Parse(version, SemVersionStyles.Strict)!;
450-
var latestVersion = SemVersion.Parse(updatedResource.Spec.Versions.GetLatest().Document.Version, SemVersionStyles.Strict)!;
451-
if (updatedResource.Spec.Versions.Any(v => SemVersion.Parse(v.Document.Version, SemVersionStyles.Strict).CompareSortOrderTo(documentVersion) >= 0))
452-
{
453-
this.Reduce(state => state with
454-
{
455-
ProblemTitle = "Invalid version",
456-
ProblemDetail = $"The specified version '{documentVersion}' must be strictly superior to the latest version '{latestVersion}'."
440+
Metadata = new()
441+
{
442+
Namespace = workflowDefinition!.Document.Namespace,
443+
Name = workflowDefinition.Document.Name
444+
},
445+
Spec = new()
446+
{
447+
Versions = [workflowDefinition]
448+
}
457449
});
450+
this.NavigationManager.NavigateTo($"/workflows/details/{@namespace}/{name}/{version}");
458451
return;
459452
}
460-
updatedResource.Spec.Versions.Add(workflowDefinition!);
461-
var jsonPatch = JsonPatch.FromDiff(this.JsonSerializer.SerializeToElement(workflow)!.Value, this.JsonSerializer.SerializeToElement(updatedResource)!.Value);
462-
var patch = this.JsonSerializer.Deserialize<Json.Patch.JsonPatch>(jsonPatch.RootElement);
463-
if (patch != null)
453+
catch (ProblemDetailsException ex) when (ex.Problem.Title == "Conflict" && ex.Problem.Detail != null && ex.Problem.Detail.EndsWith("already exists"))
464454
{
465-
var resourcePatch = new Patch(PatchType.JsonPatch, jsonPatch);
466-
await this.Api.ManageNamespaced<Workflow>().PatchAsync(name, @namespace, resourcePatch, null, this.CancellationTokenSource.Token);
455+
// the workflow exists, try to update it instead
467456
}
468457
}
458+
workflow = await this.Api.Workflows.GetAsync(name, @namespace);
459+
var updatedResource = workflow.Clone()!;
460+
var documentVersion = SemVersion.Parse(version, SemVersionStyles.Strict)!;
461+
var latestVersion = SemVersion.Parse(updatedResource.Spec.Versions.GetLatest().Document.Version, SemVersionStyles.Strict)!;
462+
if (updatedResource.Spec.Versions.Any(v => SemVersion.Parse(v.Document.Version, SemVersionStyles.Strict).CompareSortOrderTo(documentVersion) >= 0))
463+
{
464+
this.Reduce(state => state with
465+
{
466+
ProblemTitle = "Invalid version",
467+
ProblemDetail = $"The specified version '{documentVersion}' must be strictly superior to the latest version '{latestVersion}'."
468+
});
469+
return;
470+
}
471+
updatedResource.Spec.Versions.Add(workflowDefinition!);
472+
var jsonPatch = JsonPatch.FromDiff(this.JsonSerializer.SerializeToElement(workflow)!.Value, this.JsonSerializer.SerializeToElement(updatedResource)!.Value);
473+
var patch = this.JsonSerializer.Deserialize<Json.Patch.JsonPatch>(jsonPatch.RootElement);
474+
if (patch != null)
475+
{
476+
var resourcePatch = new Patch(PatchType.JsonPatch, jsonPatch);
477+
await this.Api.ManageNamespaced<Workflow>().PatchAsync(name, @namespace, resourcePatch, null, this.CancellationTokenSource.Token);
478+
}
469479
this.NavigationManager.NavigateTo($"/workflows/details/{@namespace}/{name}/{version}");
470480
}
471481
catch (ProblemDetailsException ex)
@@ -502,6 +512,15 @@ public async Task SaveWorkflowDefinitionAsync()
502512
catch (Exception ex)
503513
{
504514
this.Logger.LogError("Unable to save workflow definition: {exception}", ex.ToString());
515+
this.Reduce(state => state with
516+
{
517+
ProblemTitle = "Error",
518+
ProblemDetail = "An error occurred while saving the workflow.",
519+
ProblemErrors = new Dictionary<string, string[]>()
520+
{
521+
{"Message", [ex.ToString()] }
522+
}
523+
});
505524
}
506525
finally
507526
{

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ else
3737
<PreferredLanguageSelector PreferredLanguageChange="Store.ToggleTextBasedEditorLanguageAsync" />
3838
</div>
3939
<StandaloneCodeEditor @ref="Store.TextEditor"
40-
ConstructionOptions="Store.StandaloneEditorConstructionOptions"
41-
OnDidInit="Store.OnTextBasedEditorInitAsync"
42-
OnDidChangeModelContent="Store.OnDidChangeModelContent"
43-
CssClass="h-100" />
40+
ConstructionOptions="Store.StandaloneEditorConstructionOptions"
41+
OnDidInit="Store.OnTextBasedEditorInitAsync"
42+
OnDidChangeModelContent="Store.OnDidChangeModelContent"
43+
CssClass="h-100" />
4444
@if (problemDetails != null)
4545
{
4646
<div class="problems px-3">
@@ -93,6 +93,7 @@ else
9393
await base.OnInitializedAsync();
9494
BreadcrumbManager.Use(Breadcrumbs.Workflows);
9595
BreadcrumbManager.Add(new($"New", $"/workflows/new"));
96+
Store.SetIsNew(true);
9697
Store.Namespace.Subscribe(value => OnStateChanged(_ => ns = value), token: CancellationTokenSource.Token);
9798
Store.Name.Subscribe(value => OnStateChanged(_ => name = value), token: CancellationTokenSource.Token);
9899
Store.Loading.Subscribe(value => OnStateChanged(_ => loading = value), token: CancellationTokenSource.Token);
@@ -110,6 +111,7 @@ else
110111
if (Name != name)
111112
{
112113
Store.SetName(Name);
114+
Store.SetIsNew(false);
113115
}
114116
}
115117

0 commit comments

Comments
 (0)