diff --git a/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumb.razor b/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumb.razor index ca0d08b79..4cdc74354 100644 --- a/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumb.razor +++ b/src/dashboard/Synapse.Dashboard/Components/Breadcrumb/Breadcrumb.razor @@ -17,24 +17,35 @@ @namespace Synapse.Dashboard.Components @inject IBreadcrumbManager BreadcrumbService @implements IDisposable - +
@Body
diff --git a/src/dashboard/Synapse.Dashboard/Components/WorkflowVersionSelector/WorkflowVersionSelector.razor b/src/dashboard/Synapse.Dashboard/Components/WorkflowVersionSelector/WorkflowVersionSelector.razor new file mode 100644 index 000000000..2cab2e422 --- /dev/null +++ b/src/dashboard/Synapse.Dashboard/Components/WorkflowVersionSelector/WorkflowVersionSelector.razor @@ -0,0 +1,46 @@ +@* + Copyright © 2024-Present The Synapse Authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*@ + +@namespace Synapse.Dashboard.Components + +@if (Workflow != null) +{ + +} +@code { + + protected string? ClassNames => Class; + [Parameter] public Workflow? Workflow { get; set; } + [Parameter] public string? Version { get; set; } + [Parameter] public EventCallback OnChange { get; set; } + [Parameter] public string? Class { get; set; } + + async Task OnVersionChangedAsync(ChangeEventArgs e) + { + if (OnChange.HasDelegate) + { + await this.OnChange.InvokeAsync(e); + } + } +} diff --git a/src/dashboard/Synapse.Dashboard/Pages/About/View.razor b/src/dashboard/Synapse.Dashboard/Pages/About/View.razor index 900a932c2..182c4b2d9 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/About/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/About/View.razor @@ -16,6 +16,7 @@ @namespace Synapse.Dashboard.Components @page "/about" +@inject IBreadcrumbManager BreadcrumbManager Synapse - About @@ -65,4 +66,13 @@

- \ No newline at end of file + + +@code { + /// + protected override void OnInitialized() + { + base.OnInitialized(); + BreadcrumbManager.Use(Breadcrumbs.About); + } +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Correlations/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Correlations/List/View.razor index 7651b7340..9c6ce2f06 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Correlations/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Correlations/List/View.razor @@ -2,6 +2,7 @@ @attribute [Authorize] @namespace Synapse.Dashboard.Pages.Correlations.List @inherits NamespacedResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager Correlations @@ -165,4 +166,10 @@ else return $"/workflow-instances/{correlation.Spec.Outcome.Correlate!.Instance}"; } + /// + protected override void OnInitialized() + { + base.OnInitialized(); + BreadcrumbManager.Use(Breadcrumbs.Correlations); + } } \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Correlators/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Correlators/List/View.razor index 56f91868e..0723c01ca 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Correlators/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Correlators/List/View.razor @@ -2,6 +2,7 @@ @attribute [Authorize] @namespace Synapse.Dashboard.Pages.Correlators.List @inherits NamespacedResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager Correlators @@ -67,7 +68,13 @@ -@{ +@code { + /// + protected override void OnInitialized() + { + base.OnInitialized(); + BreadcrumbManager.Use(Breadcrumbs.Correlators); + } string GetStatusClass(Correlator correlator) { diff --git a/src/dashboard/Synapse.Dashboard/Pages/Namespaces/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Namespaces/List/View.razor index 3cb79a350..2049f441d 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Namespaces/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Namespaces/List/View.razor @@ -2,6 +2,7 @@ @attribute [Authorize] @namespace Synapse.Dashboard.Pages.Namespaces.List @inherits ClusterResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager Namespaces @@ -51,4 +52,13 @@ - \ No newline at end of file + + +@code { + /// + protected override void OnInitialized() + { + base.OnInitialized(); + BreadcrumbManager.Use(Breadcrumbs.Namespaces); + } +} \ No newline at end of file diff --git a/src/dashboard/Synapse.Dashboard/Pages/Operators/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Operators/List/View.razor index 85483255a..a05e9513d 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Operators/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Operators/List/View.razor @@ -2,6 +2,7 @@ @attribute [Authorize] @namespace Synapse.Dashboard.Pages.Operators.List @inherits NamespacedResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager Operators @@ -67,7 +68,13 @@ -@{ +@code { + /// + protected override void OnInitialized() + { + base.OnInitialized(); + BreadcrumbManager.Use(Breadcrumbs.Operators); + } string GetStatusClass(Operator @operator) { diff --git a/src/dashboard/Synapse.Dashboard/Pages/Users/Profile.razor b/src/dashboard/Synapse.Dashboard/Pages/Users/Profile.razor index 270684f8d..fe15c51ed 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Users/Profile.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Users/Profile.razor @@ -1,6 +1,7 @@ @page "/users/profile" @attribute [Authorize] @inject ApplicationAuthenticationStateProvider AuthenticationStateProvider +@inject IBreadcrumbManager BreadcrumbManager User Profile @@ -31,6 +32,7 @@ protected override async Task OnInitializedAsync() { + BreadcrumbManager.Use(Breadcrumbs.UserProfile); user = (await AuthenticationStateProvider.GetAuthenticationStateAsync()).User; AuthenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged; await base.OnInitializedAsync(); diff --git a/src/dashboard/Synapse.Dashboard/Pages/WorkflowInstances/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/WorkflowInstances/List/View.razor index aa0237840..2fadf3155 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/WorkflowInstances/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/WorkflowInstances/List/View.razor @@ -3,6 +3,7 @@ @namespace Synapse.Dashboard.Pages.WorkflowInstances.List @using BlazorBootstrap @inherits NamespacedResourceManagementComponent +@inject IBreadcrumbManager BreadcrumbManager Workflow Instances @@ -27,6 +28,11 @@ @code{ + RenderFragment Title() => __builder => + { +

Workflow Instances

+ }; + /// /// Gets the list of available s /// @@ -36,15 +42,11 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); + BreadcrumbManager.Use(Breadcrumbs.WorkflowInstances); this.Store.Workflows.Subscribe(workflows => this.OnStateChanged(cmp => cmp.Workflows = workflows), token: this.CancellationTokenSource.Token); await this.Store.ListWorkflowsAsync().ConfigureAwait(false); } - RenderFragment Title() => __builder => - { -

Workflow Instances

- }; - /// /// Handles changes of the workflow selector /// diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs index a65f9fd9a..7dcf7022e 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/State.cs @@ -22,21 +22,30 @@ namespace Synapse.Dashboard.Pages.Workflows.Create; [Feature] public record CreateWorkflowViewState { - /// - /// Gets/sets the used to code the workflow definition to create + /// Gets/sets the 's /// - public StandaloneCodeEditor TextEditor { get; set; } = null!; + public string? Namespace { get; set; } /// - /// Gets/sets the workflow that defines the created workflow definition + /// Gets/sets the 's name /// - public Workflow? Workflow { get; set; } + public string? Name { get; set; } /// /// Gets/sets the definition of the workflow to create /// - public WorkflowDefinition? WorkflowDefinition { get; set; } + public WorkflowDefinition WorkflowDefinition { get; set; } = new WorkflowDefinition() + { + Document = new() + { + Dsl = "1.0.0", + Namespace = Neuroglia.Data.Infrastructure.ResourceOriented.Namespace.DefaultNamespaceName, + Name = "new-workflow", + Version = "0.1.0" + }, + Do = [] + }; /// /// Gets/sets the workflow definition text representation @@ -46,16 +55,41 @@ public record CreateWorkflowViewState /// /// Gets/sets a boolean indicating whether or not the state is being loaded /// - public bool Loading { get; set; } + public bool Loading { get; set; } = false; /// /// Defines if the workflow definition is being updated /// - public bool Updating { get; set; } + public bool Updating { get; set; } = false; /// /// Defines if the workflow definition is being saved /// - public bool Saving { get; set; } + public bool Saving { get; set; } = false; + + /// + /// Gets/sets the type that occurred when trying to save the resource, if any + /// + public Uri? ProblemType { get; set; } = null; + + /// + /// Gets/sets the title that occurred when trying to save the resource, if any + /// + public string ProblemTitle { get; set; } = string.Empty; + + /// + /// Gets/sets the details that occurred when trying to save the resource, if any + /// + public string ProblemDetail { get; set; } = string.Empty; + + /// + /// Gets/sets the status that occurred when trying to save the resource, if any + /// + public int ProblemStatus { get; set; } = 0; + + /// + /// Gets/sets the list of errors that occurred when trying to save the resource, if any + /// + public IDictionary ProblemErrors { get; set; } = new Dictionary(); } diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/Store.cs b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/Store.cs index d0608f22d..cbcb346ed 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/Store.cs +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/Store.cs @@ -11,11 +11,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -using Microsoft.JSInterop; +using JsonCons.Utilities; +using Neuroglia.Data; using Semver; using ServerlessWorkflow.Sdk.Models; using Synapse.Api.Client.Services; -using Synapse.Dashboard.Components.ReferenceDetailsStateManagement; +using Synapse.Dashboard.Components.ResourceEditorStateManagement; using Synapse.Resources; namespace Synapse.Dashboard.Pages.Workflows.Create; @@ -27,12 +28,21 @@ namespace Synapse.Dashboard.Pages.Workflows.Create; /// The service used to help handling Monaco editors /// The service used to serialize/deserialize data to/from JSON /// The service used to serialize/deserialize data to/from YAML -public class CreateWorkflowViewStore(ISynapseApiClient api, IMonacoEditorHelper monacoEditorHelper, IJsonSerializer jsonSerializer, IYamlSerializer yamlSerializer) +/// The service used for JS interop +/// The service used to provides an abstraction for querying and managing URI navigation +public class CreateWorkflowViewStore( + ISynapseApiClient api, + IMonacoEditorHelper monacoEditorHelper, + IJsonSerializer jsonSerializer, + IYamlSerializer yamlSerializer, + IJSRuntime jsRuntime, + NavigationManager navigationManager +) : ComponentStore(new()) { - Workflow? _workflow; - WorkflowDefinition? _workflowDefinition; + private TextModel? _textModel = null; + private bool _disposed; /// /// Gets the service used to interact with the Synapse API @@ -55,19 +65,40 @@ public class CreateWorkflowViewStore(ISynapseApiClient api, IMonacoEditorHelper protected IYamlSerializer YamlSerializer { get; } = yamlSerializer; /// - /// Gets an used to observe changes to the state's property + /// Gets the service used for JS interop /// - public IObservable Workflow => this.Select(state => state.Workflow).DistinctUntilChanged(); + protected IJSRuntime JSRuntime { get; } = jsRuntime; /// - /// Gets an used to observe changes to the state's property + /// Gets the service used to provides an abstraction for querying and managing URI navigation /// - public IObservable WorkflowDefinition => this.Select(state => state.WorkflowDefinition).DistinctUntilChanged(); + protected NavigationManager NavigationManager { get; } = navigationManager; + + /// + /// The provider function + /// + public Func StandaloneEditorConstructionOptions = monacoEditorHelper.GetStandaloneEditorConstructionOptions(string.Empty, false, monacoEditorHelper.PreferredLanguage); + + /// + /// The reference + /// + public StandaloneCodeEditor? TextEditor { get; set; } + + #region Selectors + /// + /// Gets an used to observe changes + /// + public IObservable Namespace => this.Select(state => state.Namespace).DistinctUntilChanged(); /// - /// Gets an used to observe changes to the state's property + /// Gets an used to observe changes /// - public IObservable WorkflowDefinitionText => this.Select(state => state.WorkflowDefinitionText).DistinctUntilChanged(); + public IObservable Name => this.Select(state => state.Name).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes to the state's property + /// + public IObservable WorkflowDefinition => this.Select(state => state.WorkflowDefinition).DistinctUntilChanged(); /// /// Gets an used to observe changes to the state's property @@ -80,84 +111,200 @@ public class CreateWorkflowViewStore(ISynapseApiClient api, IMonacoEditorHelper public IObservable Saving => this.Select(state => state.Saving).DistinctUntilChanged(); /// - /// Creates a new from scratch + /// Gets an used to observe changes /// - /// A new awaitable - public Task CreateWorkflowDefinitionAsync() - { - var workflowDefinition = new WorkflowDefinition() + public IObservable ProblemType => this.Select(state => state.ProblemType).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemTitle => this.Select(state => state.ProblemTitle).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemDetail => this.Select(state => state.ProblemDetail).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable ProblemStatus => this.Select(state => state.ProblemStatus).DistinctUntilChanged(); + + /// + /// Gets an used to observe changes + /// + public IObservable> ProblemErrors => this.Select(state => state.ProblemErrors).DistinctUntilChanged(); + + /// + /// Gets an used to observe computed + /// + public IObservable ProblemDetails => Observable.CombineLatest( + this.ProblemType, + this.ProblemTitle, + this.ProblemStatus, + this.ProblemDetail, + this.ProblemErrors, + (type, title, status, details, errors) => { - Document = new() + if (string.IsNullOrWhiteSpace(title)) { - Dsl = "1.0.0", - Namespace = Namespace.DefaultNamespaceName, - Name = "new-workflow", - Version = "0.1.0" - }, - Do = [] - }; - return this.SetWorkflowDefinitionAsync(workflowDefinition); + return null; + } + return new ProblemDetails(type ?? new Uri("unknown://"), title, status, details, null, errors, null); + } + ); + #endregion + + #region Setters + /// + /// Sets the state's + /// + /// The new value + public void SetNamespace(string? ns) + { + this.Reduce(state => state with + { + Namespace = ns, + Loading = true + }); + } + + /// + /// Sets the state's + /// + /// The new value + public void SetName(string? name) + { + this.Reduce(state => state with + { + Name = name, + Loading = true + }); } /// - /// Creates a new from the specified one + /// Sets the state's 's related data + /// + /// The to populate the data with + public void SetProblemDetails(ProblemDetails? problem) + { + this.Reduce(state => state with + { + ProblemType = problem?.Type, + ProblemTitle = problem?.Title ?? string.Empty, + ProblemStatus = problem?.Status ?? 0, + ProblemDetail = problem?.Detail ?? string.Empty, + ProblemErrors = problem?.Errors?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value) ?? [] + }); + } + #endregion + + #region Actions + /// + /// Gets the for the specified namespace and name /// /// The namespace the to create a new version of belongs to /// The name of the to create a new version of /// A new awaitable - public async Task CreateWorkflowDefinitionAsync(string @namespace, string name) + public async Task GetWorkflowDefinitionAsync(string @namespace, string name) { ArgumentException.ThrowIfNullOrWhiteSpace(@namespace); ArgumentException.ThrowIfNullOrWhiteSpace(name); - this._workflow = await this.Api.Workflows.GetAsync(name, @namespace) ?? throw new NullReferenceException($"Failed to find the specified workflow '{name}.{@namespace}'"); + var workflow = await this.Api.Workflows.GetAsync(name, @namespace) ?? throw new NullReferenceException($"Failed to find the specified workflow '{name}.{@namespace}'"); + var definition = workflow.Spec.Versions.GetLatest(); this.Reduce(s => s with { - Workflow = this._workflow + WorkflowDefinition = definition, + Loading = false }); - var workflowDefinition = this._workflow.Spec.Versions.GetLatest(); - await this.SetWorkflowDefinitionAsync(workflowDefinition); } /// - /// Sets the to create + /// Handles changed of the text editor's language /// - /// The base - /// A new awaitable - protected Task SetWorkflowDefinitionAsync(WorkflowDefinition workflowDefinition) + /// + /// + public async Task ToggleTextBasedEditorLanguageAsync(string _) { - ArgumentNullException.ThrowIfNull(workflowDefinition); - this.Reduce(s => s with + if (this.TextEditor == null) { - Loading = true - }); - var serializer = this.MonacoEditorHelper.PreferredLanguage switch - { - PreferredLanguage.JSON => (ITextSerializer)this.JsonSerializer, - PreferredLanguage.YAML => this.YamlSerializer, - _ => throw new NotSupportedException($"The specified language '{this.MonacoEditorHelper.PreferredLanguage}' is not supported") - }; - var text = serializer.SerializeToText(workflowDefinition); - this._workflowDefinition = workflowDefinition; - this.Reduce(s => s with + return; + } + var language = this.MonacoEditorHelper.PreferredLanguage; + try { - WorkflowDefinition = workflowDefinition, - WorkflowDefinitionText = text, - Loading = false - }); - return Task.CompletedTask; + var document = await this.TextEditor.GetValue(); + if (document == null) + { + return; + } + document = language == PreferredLanguage.YAML ? + this.YamlSerializer.ConvertFromJson(document) : + this.YamlSerializer.ConvertToJson(document); + this.Reduce(state => state with + { + WorkflowDefinitionText = document + }); + await this.OnTextBasedEditorInitAsync(); + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + await this.MonacoEditorHelper.ChangePreferredLanguageAsync(language == PreferredLanguage.YAML ? PreferredLanguage.JSON : PreferredLanguage.YAML); + } } /// - /// Sets the state's + /// Handles initialization of the text editor /// - /// The new value - public void SetTextEditor(StandaloneCodeEditor? textEditor) + /// + public async Task OnTextBasedEditorInitAsync() { - ArgumentNullException.ThrowIfNull(textEditor); - this.Reduce(state => state with + await this.SetTextBasedEditorLanguageAsync(); + await this.SetTextEditorValueAsync(); + } + + /// + /// Sets the language of the text editor + /// + /// + public async Task SetTextBasedEditorLanguageAsync() + { + try { - TextEditor = textEditor - }); + var language = this.MonacoEditorHelper.PreferredLanguage; + if (this.TextEditor != null) + { + if (this._textModel != null) + { + await Global.SetModelLanguage(this.JSRuntime, this._textModel, language); + } + else + { + var resourceUri = $"inmemory://workflow-definition"; + this._textModel = await Global.CreateModel(this.JSRuntime, "", language, resourceUri); + } + await this.TextEditor!.SetModel(this._textModel); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.ToString()); + // todo: handle exception + } + } + + /// + /// Changes the value of the text editor + /// + /// + async Task SetTextEditorValueAsync() + { + var document = this.Get(state => state.WorkflowDefinitionText); + if (this.TextEditor != null && !string.IsNullOrWhiteSpace(document)) + { + await this.TextEditor.SetValue(document); + } } /// @@ -166,39 +313,156 @@ public void SetTextEditor(StandaloneCodeEditor? textEditor) /// A new awaitable public async Task SaveWorkflowDefinitionAsync() { - if (this._workflowDefinition == null) throw new NullReferenceException("The workflow definition cannot be null"); - this.Reduce(s => s with + if (this.TextEditor == null) { - Saving = true - }); - if(this._workflow == null) + this.Reduce(state => state with + { + ProblemTitle = "Text editor", + ProblemDetail = "The text editor must be initialized." + }); + return; + } + var workflowDefinitionText = await this.TextEditor.GetValue(); + if (string.IsNullOrWhiteSpace(workflowDefinitionText)) + { + this.Reduce(state => state with + { + ProblemTitle = "Invalid definition", + ProblemDetail = "The workflow definition cannot be empty." + }); + return; + } + try { - this._workflow = await this.Api.Workflows.CreateAsync(new() + var workflowDefinition = this.MonacoEditorHelper.PreferredLanguage == PreferredLanguage.JSON ? + this.JsonSerializer.Deserialize(workflowDefinitionText) : + this.YamlSerializer.Deserialize(workflowDefinitionText); + var @namespace = workflowDefinition!.Document.Namespace; + var name = workflowDefinition.Document.Name; + var version = workflowDefinition.Document.Version; + this.Reduce(s => s with { - Metadata = new() + Saving = true + }); + Workflow? workflow = null; + try + { + workflow = await this.Api.Workflows.GetAsync(name, @namespace); + } + catch + { + // Assume 404, might need actual handling + } + if (workflow == null) + { + workflow = await this.Api.Workflows.CreateAsync(new() { - Namespace = this._workflowDefinition.Document.Namespace, - Name = this._workflowDefinition.Document.Name - }, - Spec = new() + Metadata = new() + { + Namespace = workflowDefinition!.Document.Namespace, + Name = workflowDefinition.Document.Name + }, + Spec = new() + { + Versions = [workflowDefinition] + } + }); + } + else + { + var updatedResource = workflow.Clone()!; + var documentVersion = SemVersion.Parse(version, SemVersionStyles.Strict)!; + var latestVersion = SemVersion.Parse(updatedResource.Spec.Versions.GetLatest().Document.Version, SemVersionStyles.Strict)!; + if (updatedResource.Spec.Versions.Any(v => SemVersion.Parse(v.Document.Version, SemVersionStyles.Strict).CompareSortOrderTo(documentVersion) >= 0)) { - Versions = [this._workflowDefinition] + this.Reduce(state => state with + { + ProblemTitle = "Invalid version", + ProblemDetail = $"The specified version '{documentVersion}' must be strictly superior to the latest version '{latestVersion}'." + }); + return; } - }); + updatedResource.Spec.Versions.Add(workflowDefinition!); + var jsonPatch = JsonPatch.FromDiff(this.JsonSerializer.SerializeToElement(workflow)!.Value, this.JsonSerializer.SerializeToElement(updatedResource)!.Value); + var patch = this.JsonSerializer.Deserialize(jsonPatch.RootElement); + if (patch != null) + { + var resourcePatch = new Patch(PatchType.JsonPatch, jsonPatch); + await this.Api.ManageNamespaced().PatchAsync(name, @namespace, resourcePatch, null, this.CancellationTokenSource.Token); + } + } + this.NavigationManager.NavigateTo($"/workflows/details/{@namespace}/{name}/{version}"); } - else + catch (ProblemDetailsException ex) { - var originalResource = await this.Api.Workflows.GetAsync(this._workflowDefinition.Document.Name, this._workflowDefinition.Document.Namespace); - var updatedResource = originalResource.Clone()!; - var latestVersion = SemVersion.Parse(updatedResource.Spec.Versions.GetLatest().Document.Version, SemVersionStyles.Strict)!; - if (updatedResource.Spec.Versions.Any(v => SemVersion.Parse(v.Document.Version, SemVersionStyles.Strict).CompareSortOrderTo(latestVersion) >= 0)) throw new Exception($"The specified version '{this._workflowDefinition.Document.Version}' must be strictly superior to the latest version '{latestVersion}'"); - updatedResource.Spec.Versions.Add(this._workflowDefinition); + this.SetProblemDetails(ex.Problem); } - this.Reduce(s => s with + catch (Exception ex) { - Workflow = this._workflow, - Saving = false - }); + Console.WriteLine(ex.ToString()); + // todo: handle exception + } + finally + { + this.Reduce(s => s with + { + Saving = false + }); + } + } + #endregion + + /// + public override async Task InitializeAsync() + { + this.WorkflowDefinition.SubscribeAsync(async definition => { + string document = ""; + if (definition != null) + { + document = this.MonacoEditorHelper.PreferredLanguage == PreferredLanguage.JSON ? + this.JsonSerializer.SerializeToText(definition) : + this.YamlSerializer.SerializeToText(definition); + } + this.Reduce(state => state with + { + WorkflowDefinitionText = document + }); + await this.SetTextEditorValueAsync(); + }, cancellationToken: this.CancellationTokenSource.Token); + Observable.CombineLatest( + this.Namespace.Where(ns => !string.IsNullOrWhiteSpace(ns)), + this.Name.Where(name => !string.IsNullOrWhiteSpace(name)), + (ns, name) => (ns!, name!) + ).SubscribeAsync(async ((string ns, string name) workflow) => + { + await this.GetWorkflowDefinitionAsync(workflow.ns, workflow.name); + }, cancellationToken: this.CancellationTokenSource.Token); + await base.InitializeAsync(); + } + + /// + /// Disposes of the store + /// + /// A boolean indicating whether or not the dispose of the store + protected override void Dispose(bool disposing) + { + if (!this._disposed) + { + if (disposing) + { + if (this._textModel != null) + { + this._textModel.DisposeModel(); + this._textModel = null; + } + if (this.TextEditor != null) + { + this.TextEditor.Dispose(); + this.TextEditor = null; + } + } + this._disposed = true; + } } } diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/View.razor index d32635224..6f2a23cea 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Create/View.razor @@ -15,7 +15,7 @@ *@ @page "/workflows/new" -@page "/workflows/{namespace}/{name}/new" +@page "/workflows/new/{namespace}/{name}" @using ServerlessWorkflow.Sdk.Models @using Synapse.Api.Client.Services @inherits StatefulComponent @@ -25,73 +25,87 @@ New workflow

New Workflow

-@if (saving) +@if (loading) { -
+
} -else if (workflowDefinition != null) +else { - - + - -} -else if(!string.IsNullOrWhiteSpace(ns)) -{ - +
+ @if (problemDetails != null) + { + + @problemDetails.Detail + + @if (problemDetails.Errors != null && problemDetails.Errors.Any()) + { + foreach (KeyValuePair errorContainer in problemDetails.Errors) + { + +
    + @foreach (string error in errorContainer.Value) + { +
  • @error
  • + } +
+
+ } + } + } +
+ } @code { - StandaloneCodeEditor? textBasedEditor; - string textEditorValue = string.Empty; string? ns; string? name; - Workflow? workflow; - WorkflowDefinition? workflowDefinition; - bool initialized; bool loading; bool saving; - StandaloneCodeEditor? TextBasedEditor - { - get => this.textBasedEditor; - set => this.Store.SetTextEditor(value); - } + private ProblemDetails? problemDetails = null; [Parameter] public string? Namespace { get; set; } [Parameter] public string? Name { get; set; } + /// protected override async Task OnInitializedAsync() { await base.OnInitializedAsync(); - this.Store.Loading.Subscribe(value => this.OnStateChanged(_ => loading = value), token: this.CancellationTokenSource.Token); - this.Store.Saving.Subscribe(value => this.OnStateChanged(_ => saving = value), token: this.CancellationTokenSource.Token); - this.Store.WorkflowDefinition.Subscribe(value => this.OnStateChanged(_ => workflowDefinition = value), token: this.CancellationTokenSource.Token); + BreadcrumbManager.Use(Breadcrumbs.Workflows); + BreadcrumbManager.Add(new($"New", $"/workflows/new")); + Store.Namespace.Subscribe(value => OnStateChanged(_ => ns = value), token: CancellationTokenSource.Token); + Store.Name.Subscribe(value => OnStateChanged(_ => name = value), token: CancellationTokenSource.Token); + Store.Loading.Subscribe(value => OnStateChanged(_ => loading = value), token: CancellationTokenSource.Token); + Store.Saving.Subscribe(value => OnStateChanged(_ => saving = value), token: CancellationTokenSource.Token); + Store.ProblemDetails.Subscribe(problemDetails => OnStateChanged(cmp => cmp.problemDetails = problemDetails), token: CancellationTokenSource.Token); } - protected override async Task OnParametersSetAsync() + /// + protected override void OnParametersSet() { - var updated = false; if (Namespace != ns) { - ns = Namespace; - updated = true; + Store.SetNamespace(Namespace); } if (Name != name) { - name = Name; - updated = true; - } - if (updated || !initialized) - { - if (string.IsNullOrWhiteSpace(ns) || string.IsNullOrWhiteSpace(name)) await this.Store.CreateWorkflowDefinitionAsync(); - else await this.Store.CreateWorkflowDefinitionAsync(ns, name); - BreadcrumbManager.Use(Breadcrumbs.Workflows); - BreadcrumbManager.Add(new($"New", $"/workflows/new")); - initialized = true; - StateHasChanged(); + Store.SetName(Name); } } diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/State.cs b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/State.cs index ae23e691a..e878de587 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/State.cs +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/State.cs @@ -28,12 +28,12 @@ public record WorkflowDetailsState public Workflow? Workflow { get; set; } /// - /// The displayed 's name + /// Gets/sets the displayed 's name /// public string? WorkflowDefinitionName { get; set; } /// - /// The displayed 's version + /// Gets/sets the displayed 's version /// public string? WorkflowDefinitionVersion { get; set; } @@ -45,6 +45,6 @@ public record WorkflowDetailsState /// /// Gets/sets the parsed /// - public string JsonWorkflowDefinition { get; set; } = string.Empty; + public string WorkflowDefinitionJson { get; set; } = string.Empty; } diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/Store.cs b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/Store.cs index 53289442f..dc6e89157 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/Store.cs +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/Store.cs @@ -22,7 +22,7 @@ namespace Synapse.Dashboard.Pages.Workflows.Details; ///
/// The service used to interact with the Synapse API /// The hub used to watch resource events -/// The service used from JS interop +/// The service used for JS interop /// The service used ease Monaco Editor interactions /// The service used to serialize and deserialize JSON /// The service used to serialize and deserialize YAML @@ -38,6 +38,7 @@ IYamlSerializer yamlSerializer { private TextModel? _textModel = null; + private bool _disposed; /// /// The provider function @@ -96,9 +97,9 @@ IYamlSerializer yamlSerializer public IObservable?> Workflows => this.Select(state => state.Workflows).DistinctUntilChanged(); /// - /// Gets an used to observe changes + /// Gets an used to observe changes /// - public IObservable Document => this.Select(state => state.JsonWorkflowDefinition).DistinctUntilChanged(); + public IObservable Document => this.Select(state => state.WorkflowDefinitionJson).DistinctUntilChanged(); #endregion #region Setters @@ -113,8 +114,8 @@ public void SetWorkflowDefinitionName(string? workflowDefinitionName) Workflow = null, WorkflowDefinitionName = workflowDefinitionName }); - var ns = this.Get(state => state.Namespace); } + /// /// Sets the state's /// @@ -210,7 +211,7 @@ public async Task SetTextBasedEditorLanguageAsync() /// async Task SetTextEditorValueAsync() { - var document = this.Get(state => state.JsonWorkflowDefinition); + var document = this.Get(state => state.WorkflowDefinitionJson); var language = monacoEditorHelper.PreferredLanguage; if (this.TextEditor != null && !string.IsNullOrWhiteSpace(document)) { @@ -240,7 +241,7 @@ public override async Task InitializeAsync() var document = jsonSerializer.SerializeToText(definition); this.Reduce(state => state with { - JsonWorkflowDefinition = document + WorkflowDefinitionJson = document }); await this.SetTextEditorValueAsync(); if (monacoEditorHelper.PreferredLanguage != PreferredLanguage.YAML) @@ -266,14 +267,13 @@ public override async Task InitializeAsync() await base.InitializeAsync(); } - private bool disposed; /// /// Disposes of the store /// /// A boolean indicating whether or not the dispose of the store protected override void Dispose(bool disposing) { - if (!this.disposed) + if (!this._disposed) { if (disposing) { @@ -288,7 +288,7 @@ protected override void Dispose(bool disposing) this.TextEditor = null; } } - this.disposed = true; + this._disposed = true; } } } diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/View.razor index 8e1c147fa..a5b9606a6 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/View.razor @@ -14,16 +14,12 @@ limitations under the License. *@ -@page "/workflows/{namespace}/{name}/latest" -@page "/workflows/{namespace}/{name}/{version}" +@page "/workflows/details/{namespace}/{name}/latest" +@page "/workflows/details/{namespace}/{name}/{version}" @using ServerlessWorkflow.Sdk.Models @using Synapse.Api.Client.Services @inherits NamespacedResourceManagementComponent -@inject ISynapseApiClient Api @inject IBreadcrumbManager BreadcrumbManager -@inject IJSRuntime JSRuntime -@inject IMonacoEditorHelper MonacoEditorHelper -@inject IYamlSerializer YamlSerializer Workflow @($"{name}.{ns}:{version}") @@ -33,7 +29,6 @@
@@ -66,11 +61,16 @@ } else { - - +
+ +
+ +
+
+ } @@ -86,10 +86,10 @@ WorkflowDefinition workflowDefinition = null!; readonly IEnumerable columns = [ "Name", - "Namespace", - "Status", - "Started At", - "Ended At" + "Namespace", + "Status", + "Started At", + "Ended At" ]; [Parameter] public new string? Namespace { get; set; } @@ -100,50 +100,63 @@ protected override async Task OnInitializedAsync() { await base.OnInitializedAsync().ConfigureAwait(false); - this.Store.Namespace.Where(newNamespace => !string.IsNullOrWhiteSpace(newNamespace)).Subscribe(newNamespace => this.OnStateChanged(cmp => cmp.ns = newNamespace!), token: this.CancellationTokenSource.Token); - this.Store.WorkflowDefinitionName.Where(newName => !string.IsNullOrWhiteSpace(newName)).Subscribe(newName => this.OnStateChanged(cmp => cmp.name = newName!), token: this.CancellationTokenSource.Token); - this.Store.WorkflowDefinitionVersion.Where(newVersion => !string.IsNullOrWhiteSpace(newVersion)).Subscribe(newVersion => this.OnStateChanged(cmp => cmp.version = newVersion!), token: this.CancellationTokenSource.Token); - this.Store.Workflow.Where(newWorkflow => newWorkflow != null).Subscribe(newWorkflow => this.OnStateChanged(cmp => cmp.workflow = newWorkflow!), token: this.CancellationTokenSource.Token); - this.Store.WorkflowDefinition.Where(newWorkflowDefinition => newWorkflowDefinition != null).Subscribe(newWorkflowDefinition => + UpdateBreadcrumb(); + Store.Namespace.Where(value => !string.IsNullOrWhiteSpace(value)).Subscribe(value => OnStateChanged(_ => ns = value!), token: CancellationTokenSource.Token); + Store.WorkflowDefinitionName.Where(value => !string.IsNullOrWhiteSpace(value)).Subscribe(value => OnStateChanged(_ => name = value!), token: CancellationTokenSource.Token); + Store.WorkflowDefinitionVersion.Where(value => !string.IsNullOrWhiteSpace(value)).Subscribe(value => OnStateChanged(_ => version = value!), token: CancellationTokenSource.Token); + Store.WorkflowDefinition.Where(value => value != null).Subscribe(value => OnStateChanged(_ => workflowDefinition = value!), token: CancellationTokenSource.Token); + Store.Workflow.Where(value => value != null).Subscribe(value => { - this.OnStateChanged(cmp => cmp.workflowDefinition = newWorkflowDefinition!); - BreadcrumbManager.Use(Breadcrumbs.Workflows); - BreadcrumbManager.Add(new($"{newWorkflowDefinition!.Document.Name}.{newWorkflowDefinition!.Document.Namespace}", $"/workflows/{newWorkflowDefinition!.Document.Namespace}/{newWorkflowDefinition!.Document.Name}/latest")); - - }, token: this.CancellationTokenSource.Token); + OnStateChanged(_ => workflow = value!); + UpdateBreadcrumb(); + }, token: CancellationTokenSource.Token); } + /// protected override void OnParametersSet() { - if (this.Namespace != this.ns) + if (Namespace != ns) + { + Store.SetNamespace(Namespace); + } + if (Name != name) { - this.Store.SetNamespace(this.Namespace); + Store.SetWorkflowDefinitionName(Name); } - if (this.Name != this.name) + if (Version != version) { - this.Store.SetWorkflowDefinitionName(this.Name); + Store.SetWorkflowDefinitionVersion(Version); } - if (this.Version != this.version) + } + + /// + /// Updates the breadcrumb + /// + protected void UpdateBreadcrumb() + { + BreadcrumbManager.Use(Breadcrumbs.Workflows); + BreadcrumbManager.Add(new($"{Name}.{Namespace}", $"/workflows/{Namespace}/{Name}/latest")); + if (workflow != null) { - this.Store.SetWorkflowDefinitionVersion(this.Version); + BreadcrumbManager.Add(new(VersionSelector())); } } - RenderFragment Title() => __builder => + /// + /// Renders the workflows instances table's title + /// + /// + protected RenderFragment VersionSelector() => __builder => { -
- - / - + @if (workflow != null) + { + foreach (var definitionVersion in workflow.Spec.Versions) { - foreach (var definitionVersion in workflow.Spec.Versions) - { - - } + } - -
+ } + }; } diff --git a/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor b/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor index c4cef5f1a..884799bd4 100644 --- a/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor +++ b/src/dashboard/Synapse.Dashboard/Pages/Workflows/List/View.razor @@ -124,10 +124,13 @@ else throw new NotSupportedException("The specified schedule type is not supported"); } - void OnViewWorkflow(Workflow workflow) => this.NavigationManager.NavigateTo($"workflows/{workflow.GetNamespace()}/{workflow.GetName()}/{workflow.Spec.Versions.GetLatest().Document.Version}"); + void OnViewWorkflow(Workflow workflow) => this.NavigationManager.NavigateTo($"workflows/details/{workflow.GetNamespace()}/{workflow.GetName()}/{workflow.Spec.Versions.GetLatest().Document.Version}"); void OnCreateNewWorkflow() => this.NavigationManager.NavigateTo("/workflows/new"); - void OnCreateNewWorkflowVersion(string ns, string name) => this.NavigationManager.NavigateTo($"/workflows/{ns}/{name}/new"); + void OnCreateNewWorkflowVersion(string ns, string name) => this.NavigationManager.NavigateTo($"/workflows/new/{ns}/{name}"); + + //void OnNavigateToVersion(string ns, string name, string version) => NavigationManager.NavigateTo($"/workflows/details/{ns}/{name}/{version}"); + } \ No newline at end of file