Skip to content

Commit c654349

Browse files
committed
feat(Dashboard): enable label management when creating or editing workflows
Signed-off-by: Jean-Baptiste Bianchi <[email protected]>
1 parent b20f4de commit c654349

File tree

11 files changed

+363
-299
lines changed

11 files changed

+363
-299
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
</Content>
3939
</Tab>
4040
}
41-
<Tab Title="Text">
41+
<Tab Title="Raw">
4242
<Content>
4343
<div class="pt-3">
4444
<MonacoEditor OnTextChanged="OnTextChanged" ModelName="@modelName" Document="input" />
@@ -52,7 +52,7 @@
5252
<Content>
5353
@if (Operators != null && Operators.Count() > 0)
5454
{
55-
<label for="operator">Select an Operator to run the Workflow:</label>
55+
<label for="operator">Run on:</label>
5656
<select id="operator" class="form-select" @onchange="OnSelectOperatorChanged">
5757
<option value="">Any Operator</option>
5858
@foreach (var op in Operators)

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,9 +103,9 @@ public record CreateWorkflowViewState
103103
public EquatableList<Operator>? Operators { get; set; }
104104

105105
/// <summary>
106-
/// Gets/sets the active operator filter
106+
/// Gets/sets the workflow's labels
107107
/// </summary>
108-
public string? Operator { get; set; } = null;
108+
public EquatableDictionary<string, string> Labels { get; set; } = [];
109109

110110
/// <summary>
111111
/// Gets/sets the list of <see cref="ProblemDetails"/> errors that occurred when trying to save the resource, if any

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

Lines changed: 67 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@
1919
using Synapse.Api.Client.Services;
2020
using Synapse.Dashboard.Pages.Workflows.List;
2121
using Synapse.Resources;
22+
using System.Reflection.Emit;
2223
using System.Text.RegularExpressions;
24+
using static Synapse.SynapseDefaults.Resources;
2325

2426
namespace Synapse.Dashboard.Pages.Workflows.Create;
2527

@@ -177,10 +179,15 @@ IWorkflowDefinitionValidator workflowDefinitionValidator
177179
/// </summary>
178180
public IObservable<EquatableList<Operator>?> Operators => this.Select(s => s.Operators).DistinctUntilChanged();
179181

182+
/// <summary>
183+
/// Gets an <see cref="IObservable{T}"/> used to observe <see cref="CreateWorkflowViewState.Labels"/> changes
184+
/// </summary>
185+
public IObservable<EquatableDictionary<string, string>> Labels => this.Select(state => state.Labels).DistinctUntilChanged();
186+
180187
/// <summary>
181188
/// Gets an <see cref="IObservable{T}"/> used to observe <see cref="WorkflowListState.Operator"/> changes
182189
/// </summary>
183-
public IObservable<string?> Operator => this.Select(s => s.Operator).DistinctUntilChanged();
190+
public IObservable<string?> Operator => this.Select(state => state.Labels.TryGetValue(SynapseDefaults.Resources.Labels.Operator, out var label) ? label : null).DistinctUntilChanged();
184191

185192
/// <summary>
186193
/// Gets an <see cref="IObservable{T}"/> used to observe computed <see cref="Neuroglia.ProblemDetails"/>
@@ -270,15 +277,15 @@ public async Task GetWorkflowDefinitionAsync(string @namespace, string name)
270277
ArgumentException.ThrowIfNullOrWhiteSpace(@namespace);
271278
ArgumentException.ThrowIfNullOrWhiteSpace(name);
272279
var workflow = await Api.Workflows.GetAsync(name, @namespace) ?? throw new NullReferenceException($"Failed to find the specified workflow '{name}.{@namespace}'");
273-
var operatorName = workflow.Metadata.Labels?.Get(SynapseDefaults.Resources.Labels.Operator);
280+
//var operatorName = workflow.Metadata.Labels?.Get(SynapseDefaults.Resources.Labels.Operator);
274281
var definition = workflow.Spec.Versions.GetLatest();
275282
var nextVersion = SemVersion.Parse(definition.Document.Version, SemVersionStyles.Strict);
276283
nextVersion = nextVersion.WithPatch(nextVersion.Patch + 1);
277284
definition.Document.Version = nextVersion.ToString();
278285
Reduce(s => s with
279286
{
280287
WorkflowDefinition = definition,
281-
Operator = operatorName,
288+
Labels = workflow.Metadata.Labels != null ? [..workflow.Metadata.Labels] : [],
282289
Loading = false
283290
});
284291
}
@@ -439,7 +446,7 @@ public async Task SaveWorkflowDefinitionAsync()
439446
var name = workflowDefinition.Document.Name;
440447
var version = workflowDefinition.Document.Version;
441448
var isNew = Get(state => state.IsNew);
442-
var operatorName = Get(state => state.Operator);
449+
var labels = Get(state => state.Labels);
443450
Reduce(s => s with
444451
{
445452
Saving = true
@@ -460,11 +467,7 @@ public async Task SaveWorkflowDefinitionAsync()
460467
Versions = [workflowDefinition]
461468
}
462469
};
463-
if (!string.IsNullOrWhiteSpace(operatorName))
464-
{
465-
workflow.Metadata.Labels = workflow.Metadata.Labels ?? new EquatableDictionary<string, string>();
466-
workflow.Metadata.Labels.Add(SynapseDefaults.Resources.Labels.Operator, operatorName);
467-
}
470+
if (labels.Count > 0) workflow.Metadata.Labels = labels;
468471
workflow = await Api.Workflows.CreateAsync(workflow);
469472
NavigationManager.NavigateTo($"/workflows/details/{@namespace}/{name}/{version}");
470473
return;
@@ -487,11 +490,7 @@ public async Task SaveWorkflowDefinitionAsync()
487490
});
488491
return;
489492
}
490-
if (!string.IsNullOrWhiteSpace(operatorName) && updatedResource.Metadata.Labels?.Get(SynapseDefaults.Resources.Labels.Operator) != operatorName)
491-
{
492-
updatedResource.Metadata.Labels ??= new EquatableDictionary<string, string>();
493-
updatedResource.Metadata.Labels[SynapseDefaults.Resources.Labels.Operator] = operatorName;
494-
}
493+
updatedResource.Metadata.Labels = labels;
495494
updatedResource.Spec.Versions.Add(workflowDefinition!);
496495
var jsonPatch = JsonPatch.FromDiff(JsonSerializer.SerializeToElement(workflow)!.Value, JsonSerializer.SerializeToElement(updatedResource)!.Value);
497496
var patch = JsonSerializer.Deserialize<Json.Patch.JsonPatch>(jsonPatch.RootElement);
@@ -567,16 +566,66 @@ public async Task ListOperatorsAsync()
567566
});
568567
}
569568

569+
/// <summary>
570+
/// Sets the workflow labels
571+
/// </summary>
572+
/// <param name="labels">The new labels</param>
573+
public virtual void SetLabels(EquatableDictionary<string, string>? labels)
574+
{
575+
this.Reduce(state => state with
576+
{
577+
Labels = labels != null ? [..labels] : []
578+
});
579+
}
580+
581+
/// <summary>
582+
/// Adds a single label
583+
/// </summary>
584+
/// <param name="key">The key of the label</param>
585+
/// <param name="value">The value of the label</param>
586+
public virtual void AddLabel(string key, string value)
587+
{
588+
if (string.IsNullOrEmpty(key) || string.IsNullOrEmpty(value))
589+
{
590+
return;
591+
}
592+
var labels = new EquatableDictionary<string, string>(this.Get(state => state.Labels));
593+
if (labels.ContainsKey(key))
594+
{
595+
labels.Remove(key);
596+
}
597+
labels.Add(key, value);
598+
this.SetLabels(labels);
599+
}
600+
601+
/// <summary>
602+
/// Removes a single label using it's key
603+
/// </summary>
604+
/// <param name="key">The label selector key to remove</param>
605+
public void RemoveLabel(string key)
606+
{
607+
if (string.IsNullOrWhiteSpace(key))
608+
{
609+
return;
610+
}
611+
var labels = new EquatableDictionary<string, string>(this.Get(state => state.Labels));
612+
if (labels.ContainsKey(key))
613+
{
614+
labels.Remove(key);
615+
}
616+
this.SetLabels(labels);
617+
}
618+
570619
/// <summary>
571620
/// Sets the <see cref="WorkflowListState.Operator"/>
572621
/// </summary>
573622
/// <param name="operatorName">The new value</param>
574623
public void SetOperator(string? operatorName)
575624
{
576-
Reduce(state => state with
577-
{
578-
Operator = operatorName
579-
});
625+
if (string.IsNullOrEmpty(operatorName))
626+
RemoveLabel(SynapseDefaults.Resources.Labels.Operator);
627+
else
628+
AddLabel(SynapseDefaults.Resources.Labels.Operator, operatorName);
580629
}
581630
#endregion
582631

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

Lines changed: 79 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,23 +40,84 @@ else
4040
ConstructionOptions="Store.StandaloneEditorConstructionOptions"
4141
OnDidInit="Store.OnTextBasedEditorInitAsync"
4242
OnDidChangeModelContent="Store.OnDidChangeModelContent"
43-
CssClass="h-100" />
43+
CssClass="h-100 min-h-0" />
44+
45+
<Accordion class="py-3 flex-shrink-0">
46+
<AccordionItem Title="Advanced Settings">
47+
<Content>
48+
<div>
49+
@if (operators != null && operators.Count() > 0)
50+
{
51+
<label for="operator" class="fw-bolder mb-2">Run on:</label>
52+
<select id="operator" class="form-select" @onchange="(e) => Store.SetOperator(e.Value?.ToString())">
53+
<option value="">Any Operator</option>
54+
@foreach (var op in operators)
55+
{
56+
var name = op.GetName() + "." + op.GetNamespace();
57+
<option value="@name" selected="@(name == operatorName)">@name</option>
58+
}
59+
</select>
60+
}
61+
</div>
62+
<div class="mt-4">
63+
<div class="fw-bolder mb-2">Labels:</div>
64+
<div class="border rounded p-3">
65+
<table class="table">
66+
<tbody>
67+
<tr>
68+
<td class="ps-0 py-0 pe-2">
69+
<input name="label-key" type="text" class="form-control border-0" placeholder="Enter label key" @bind="labelKey" />
70+
</td>
71+
<td class="ps-0 py-0 pe-2">
72+
<input name="label-value" type="text" class="form-control border-0" placeholder="Enter label value" @bind="labelValue" />
73+
</td>
74+
<td class="text-end pt-2 fit">
75+
<Button Color="ButtonColor.Primary" Size="ButtonSize.Small" Outline="true" class="m-1" @onclick="_ => AddLabel()">
76+
<Icon Color="IconColor.Primary" Name="IconName.Plus" />
77+
</Button>
78+
</td>
79+
</tr>
80+
@foreach (KeyValuePair<string, string> label in labels)
81+
{
82+
<tr>
83+
<td class="pt-2 ps-2">@label.Key</td>
84+
<td class="pt-2 ps-2">@label.Value</td>
85+
<td class="text-end pt-2 fit">
86+
<Button Color="ButtonColor.Primary" Size="ButtonSize.Small" Outline="true" class="m-1" @onclick="() => Store.RemoveLabel(label.Key)">
87+
<Icon Color="IconColor.Primary" Name="IconName.Trash" />
88+
</Button>
89+
</td>
90+
</tr>
91+
}
92+
</tbody>
93+
</table>
94+
</div>
95+
</div>
96+
</Content>
97+
</AccordionItem>
98+
</Accordion>
99+
44100
@if (problemDetails != null)
45101
{
46-
<div class="problems px-3">
102+
<div class="problems mt-2">
47103
<Callout Color="CalloutColor.Danger" Heading="@problemDetails.Title" Class="position-relative">
48104
<Icon Name="IconName.X" Class="position-absolute" @onclick="() => Store.SetProblemDetails(null)" />
49-
<p>@problemDetails.Detail</p>
105+
<p>@problemDetails.Detail?.Trim()</p>
50106

51107
@if (problemDetails.Errors != null && problemDetails.Errors.Any())
52108
{
53109
foreach (KeyValuePair<string, string[]> errorContainer in problemDetails.Errors)
54110
{
55-
<strong>@errorContainer.Key:</strong>
111+
@if (!string.IsNullOrWhiteSpace(errorContainer.Key)) {
112+
<strong>@errorContainer.Key:</strong>
113+
}
56114
<ul>
57115
@foreach (string error in errorContainer.Value)
58116
{
59-
<li>@error</li>
117+
@if (!string.IsNullOrWhiteSpace(error))
118+
{
119+
<li>@error</li>
120+
}
60121
}
61122
</ul>
62123
}
@@ -65,25 +126,6 @@ else
65126
</div>
66127
}
67128

68-
<Accordion class="py-3">
69-
<AccordionItem Title="Advanced Settings">
70-
<Content>
71-
@if (operators != null && operators.Count() > 0)
72-
{
73-
<label for="operator">Select an Operator to run the Workflow:</label>
74-
<select id="operator" class="form-select" @onchange="(e) => Store.SetOperator(e.Value?.ToString())">
75-
<option value="">Any Operator</option>
76-
@foreach (var op in operators)
77-
{
78-
var name = op.GetName() + "." + op.GetNamespace();
79-
<option value="@name" selected="@(name == operatorName)">@name</option>
80-
}
81-
</select>
82-
}
83-
</Content>
84-
</AccordionItem>
85-
</Accordion>
86-
87129
<Button class="mt-3" Color="ButtonColor.Primary" Outline="true" Disabled="saving" @onclick="async (_) => await Store.SaveWorkflowDefinitionAsync()">
88130
@if(!saving)
89131
{
@@ -105,7 +147,10 @@ else
105147
private ProblemDetails? problemDetails = null;
106148
protected IEnumerable<Operator>? operators { get; set; }
107149
protected string? operatorName { get; set; }
108-
150+
protected EquatableDictionary<string, string> labels { get; set; } = [];
151+
string labelKey = string.Empty;
152+
string labelValue = string.Empty;
153+
109154
[Parameter] public string? Namespace { get; set; }
110155
[Parameter] public string? Name { get; set; }
111156

@@ -117,6 +162,7 @@ else
117162
BreadcrumbManager.Add(new($"New", $"/workflows/new"));
118163
Store.SetIsNew(true);
119164
Store.Operators.Subscribe(value => OnStateChanged(_ => operators = value), token: CancellationTokenSource.Token);
165+
Store.Labels.Subscribe(value => OnStateChanged(_ => labels = value), token: CancellationTokenSource.Token);
120166
Store.Operator.Subscribe(value => OnStateChanged(_ => operatorName = value), token: CancellationTokenSource.Token);
121167
Store.Namespace.Subscribe(value => OnStateChanged(_ => ns = value), token: CancellationTokenSource.Token);
122168
Store.Name.Subscribe(value => OnStateChanged(_ => name = value), token: CancellationTokenSource.Token);
@@ -139,4 +185,12 @@ else
139185
}
140186
}
141187

188+
protected void AddLabel()
189+
{
190+
Store.AddLabel(labelKey, labelValue);
191+
labelKey = string.Empty;
192+
labelValue = string.Empty;
193+
OnStateChanged();
194+
}
195+
142196
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ public async Task OnShowCreateInstanceAsync(WorkflowDefinition workflowDefinitio
384384
{ nameof(CreateWorkflowInstanceDialog.OnCreate), EventCallback.Factory.Create<CreateWorkflowInstanceParameters>(this, CreateInstanceAsync) },
385385
{ nameof(CreateWorkflowInstanceDialog.OnProblem), EventCallback.Factory.Create<ProblemDetails>(this, SetProblemDetails) }
386386
};
387-
await Modal.ShowAsync<CreateWorkflowInstanceDialog>(title: "Start a new workflow", parameters: parameters);
387+
await Modal.ShowAsync<CreateWorkflowInstanceDialog>(title: "", parameters: parameters);
388388
}
389389
}
390390

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@
170170
"Status",
171171
"Start Time",
172172
"End Time",
173+
"Operator",
173174
..WorkflowInstancesList.DirectActions
174175
];
175176
protected EquatableList<Operator>? operators { get; set; }

src/dashboard/Synapse.Dashboard/wwwroot/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{
1+
{
22
"Authentication": {
33
"Oidc": {
44
"Authority": "https://google.com",

0 commit comments

Comments
 (0)