Skip to content

Commit aad8c3b

Browse files
committed
feat(Dashboard): added multi row selection
Signed-off-by: Jean-Baptiste Bianchi <[email protected]>
1 parent b3113c0 commit aad8c3b

File tree

20 files changed

+440
-28
lines changed

20 files changed

+440
-28
lines changed

src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponent.cs

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,47 +36,63 @@ public abstract class ResourceManagementComponent<TComponent, TStore, TState, TR
3636
[Inject]
3737
protected MonacoInterop? MonacoInterop { get; set; }
3838

39+
/// <summary>
40+
/// Gets/sets the service used for JS interop
41+
/// </summary>
42+
[Inject]
43+
protected JSInterop jsInterop { get; set; } = default!;
44+
3945
/// <summary>
4046
/// Gets the service used to serialize/deserialize objects to/from JSON
4147
/// </summary>
4248
[Inject]
4349
protected IJsonSerializer Serializer { get; set; } = null!;
4450

4551
/// <summary>
46-
/// The list of displayed <see cref="Resource"/>s
52+
/// Gets/sets the list of displayed <see cref="Resource"/>s
4753
/// </summary>
4854
protected EquatableList<TResource>? Resources { get; set; }
4955

5056
/// <summary>
51-
/// The <see cref="Offcanvas"/> used to show the <see cref="Resource"/>'s details
57+
/// Gets/sets the list of selected <see cref="Resource"/>s
58+
/// </summary>
59+
protected EquatableList<string> SelectedResourceNames { get; set; } = [];
60+
61+
/// <summary>
62+
/// Gets/sets the <see cref="Offcanvas"/> used to show the <see cref="Resource"/>'s details
5263
/// </summary>
5364
protected Offcanvas? DetailsOffCanvas { get; set; }
5465

5566
/// <summary>
56-
/// The <see cref="Offcanvas"/> used to edit the <see cref="Resource"/>
67+
/// Gets/sets the <see cref="Offcanvas"/> used to edit the <see cref="Resource"/>
5768
/// </summary>
5869
protected Offcanvas? EditorOffCanvas { get; set; }
5970

6071
/// <summary>
61-
/// The <see cref="ConfirmDialog"/> used to confirm the <see cref="Resource"/>'s deletion
72+
/// Gets/sets the <see cref="ConfirmDialog"/> used to confirm the <see cref="Resource"/>'s deletion
6273
/// </summary>
6374
protected ConfirmDialog? Dialog { get; set; }
6475

6576
/// <summary>
66-
/// The <see cref="Resource"/>'s <see cref="ResourceDefinition"/>
77+
/// Gets/sets the <see cref="Resource"/>'s <see cref="ResourceDefinition"/>
6778
/// </summary>
6879
protected ResourceDefinition? Definition { get; set; }
6980

7081
/// <summary>
71-
/// The search term to filter the resources with
82+
/// Gets/sets the search term to filter the resources with
7283
/// </summary>
7384
protected string? SearchTerm { get; set; }
7485

7586
/// <summary>
76-
/// A boolean value that indicates whether data is currently being gathered
87+
/// Gets/sets a boolean value that indicates whether data is currently being gathered
7788
/// </summary>
7889
protected bool Loading { get; set; } = false;
7990

91+
/// <summary>
92+
/// Gets/sets the checkbox used to (un)select all resources
93+
/// </summary>
94+
protected ElementReference? CheckboxAll { get; set; } = null;
95+
8096
string activeResourcesName = null!;
8197
/// <summary>
8298
/// Gets/sets the name of the active resource
@@ -87,7 +103,34 @@ public abstract class ResourceManagementComponent<TComponent, TStore, TState, TR
87103
protected override async Task OnInitializedAsync()
88104
{
89105
await base.OnInitializedAsync().ConfigureAwait(false);
90-
this.Store.Resources.Subscribe(value => this.OnStateChanged(_ => this.Resources = value), token: this.CancellationTokenSource.Token);
106+
Observable.CombineLatest(
107+
this.Store.Resources,
108+
this.Store.SelectedResourceNames,
109+
(resources, selectedResourceNames) => (resources, selectedResourceNames)
110+
).SubscribeAsync(async (values) => {
111+
var (resources, selectedResourceNames) = values;
112+
this.OnStateChanged(_ =>
113+
{
114+
this.Resources = resources;
115+
this.SelectedResourceNames = selectedResourceNames;
116+
});
117+
if (this.CheckboxAll.HasValue)
118+
{
119+
120+
if (selectedResourceNames.Count == 0)
121+
{
122+
await this.jsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Unchecked);
123+
}
124+
else if (selectedResourceNames.Count == (resources?.Count ?? 0))
125+
{
126+
await this.jsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Checked);
127+
}
128+
else
129+
{
130+
await this.jsInterop.SetCheckboxStateAsync(this.CheckboxAll.Value, CheckboxState.Indeterminate);
131+
}
132+
}
133+
}, cancellationToken: this.CancellationTokenSource.Token);
91134
this.Store.ActiveResourceName.Subscribe(value => this.OnStateChanged(_ => this.activeResourcesName = value), token: this.CancellationTokenSource.Token);
92135
this.Store.SearchTerm.Subscribe(value => this.OnStateChanged(_ => this.SearchTerm = value), token: this.CancellationTokenSource.Token);
93136
this.Store.Loading.Subscribe(value => this.OnStateChanged(_ => this.Loading = value), token: this.CancellationTokenSource.Token);
@@ -153,6 +196,28 @@ protected async Task OnDeleteResourceAsync(TResource resource)
153196
await this.Store.DeleteResourceAsync(resource);
154197
}
155198

199+
/// <summary>
200+
/// Handles the deletion of the selected <see cref="Resource"/>s
201+
/// </summary>
202+
protected async Task OnDeleteSelectedResourcesAsync()
203+
{
204+
if (this.Dialog == null) return;
205+
if (this.SelectedResourceNames.Count == 0) return;
206+
var confirmation = await this.Dialog.ShowAsync(
207+
title: $"Are you sure you want to delete {SelectedResourceNames.Count} resource{(SelectedResourceNames.Count > 1 ? "s" : "")}?",
208+
message1: $"The resource{(SelectedResourceNames.Count > 1 ? "s" : "")} will be permanently deleted. Are you sure you want to proceed ?",
209+
confirmDialogOptions: new ConfirmDialogOptions()
210+
{
211+
YesButtonColor = ButtonColor.Danger,
212+
YesButtonText = "Delete",
213+
NoButtonText = "Abort",
214+
IsVerticallyCentered = true
215+
}
216+
);
217+
if (!confirmation) return;
218+
await this.Store.DeleteSelectedResourcesAsync();
219+
}
220+
156221
/// <summary>
157222
/// Opens the targeted <see cref="Resource"/>'s details
158223
/// </summary>

src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponentState.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,15 @@ public record ResourceManagementComponentState<TResource>
2727
public ResourceDefinition? Definition { get; set; }
2828

2929
/// <summary>
30-
/// Gets/sets a <see cref="List{T}"/> that contains all cached <see cref="IResource"/>s
30+
/// Gets/sets a <see cref="List{T}"/> that contains all <see cref="IResource"/>s
3131
/// </summary>
3232
public EquatableList<TResource>? Resources { get; set; } = [];
3333

34+
/// <summary>
35+
/// Gets/sets a <see cref="List{T}"/> that contains all selected <see cref="IResource"/>s
36+
/// </summary>
37+
public EquatableList<string> SelectedResourceNames { get; set; } = [];
38+
3439
/// <summary>
3540
/// Gets/sets a list that contains the label selectors, if any, used to filter the resources to list
3641
/// </summary>
@@ -44,7 +49,7 @@ public record ResourceManagementComponentState<TResource>
4449
/// <summary>
4550
/// Gets/sets a boolean value that indicates whether data is currently being gathered
4651
/// </summary>
47-
public bool Loading { get; set; } = false;
52+
public bool Loading { get; set; } = true;
4853

4954
/// <summary>
5055
/// Gets/sets the name of the selected resource

src/dashboard/Synapse.Dashboard/Components/ResourceManagement/ResourceManagementComponentStoreBase.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ public abstract class ResourceManagementComponentStoreBase<TState, TResource>(IS
3838
/// </summary>
3939
protected IObservable<EquatableList<TResource>?> InternalResources => this.Select(s => s.Resources).DistinctUntilChanged();
4040

41+
/// <summary>
42+
/// Gets an <see cref="IObservable{T}"/> used to observe the <see cref="ResourceManagementComponentState{TResource}.SelectedResourceNames"/> changes
43+
/// </summary>
44+
public IObservable<EquatableList<string>> SelectedResourceNames => this.Select(s => s.SelectedResourceNames).DistinctUntilChanged();
45+
4146
/// <summary>
4247
/// Gets an <see cref="IObservable{T}"/> used to observe the <see cref="ResourceManagementComponentState{TResource}.SearchTerm"/> changes
4348
/// </summary>
@@ -212,13 +217,67 @@ public void RemoveLabelSelector(string labelSelectorKey)
212217
this.SetLabelSelectors(labelSelectors);
213218
}
214219

220+
/// <summary>
221+
/// Toggles the resources selection
222+
/// </summary>
223+
/// <param name="name">The name of the resource to select, or all if none is provided</param>
224+
public virtual void ToggleResourceSelection(string? name = null)
225+
{
226+
this.Reduce(state =>
227+
{
228+
if (string.IsNullOrWhiteSpace(name))
229+
{
230+
if (state.SelectedResourceNames.Any())
231+
{
232+
return state with
233+
{
234+
SelectedResourceNames = []
235+
};
236+
}
237+
return state with
238+
{
239+
SelectedResourceNames = [.. state.Resources?.Select(resource => resource.GetName()) ?? []]
240+
};
241+
}
242+
if (state.SelectedResourceNames.Contains(name))
243+
{
244+
return state with
245+
{
246+
SelectedResourceNames = [.. state.SelectedResourceNames.Where(n => n != name)]
247+
};
248+
}
249+
return state with
250+
{
251+
SelectedResourceNames = [.. state.SelectedResourceNames, name]
252+
};
253+
});
254+
}
255+
215256
/// <summary>
216257
/// Deletes the specified <see cref="IResource"/>
217258
/// </summary>
218259
/// <param name="resource">The <see cref="IResource"/> to delete</param>
219260
/// <returns>A new awaitable <see cref="Task"/></returns>
220261
public abstract Task DeleteResourceAsync(TResource resource);
221262

263+
/// <summary>
264+
/// Deletes the selected <see cref="IResource"/>s
265+
/// </summary>
266+
/// <returns>A new awaitable <see cref="Task"/></returns>
267+
public async Task DeleteSelectedResourcesAsync()
268+
{
269+
var selectedResourcesNames = this.Get(state => state.SelectedResourceNames);
270+
var resources = (this.Get(state => state.Resources) ?? []).Where(resource => selectedResourcesNames.Contains(resource.GetName()));
271+
foreach(var resource in resources)
272+
{
273+
await this.DeleteResourceAsync(resource);
274+
}
275+
this.Reduce(state => state with
276+
{
277+
SelectedResourceNames = []
278+
});
279+
}
280+
222281
/// <summary>
223282
/// Fetches the definition of the managed <see cref="IResource"/> type
224283
/// </summary>

src/dashboard/Synapse.Dashboard/Components/WorkflowInstanceLogs/Store.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@ namespace Synapse.Dashboard.Components.WorkflowInstanceLogsStateManagement;
2222
/// Represents the <see cref="ComponentStore{TState}" /> of a <see cref="WorkflowInstanceLogs"/> component
2323
/// </summary>
2424
/// <param name="apiClient">The service used interact with Synapse API</param>
25+
/// <param name="jsInterop">The service used to build a bridge with JS</param>
2526
public class WorkflowInstanceLogsStore(
26-
ISynapseApiClient apiClient
27+
ISynapseApiClient apiClient,
28+
JSInterop jsInterop
2729
)
2830
: ComponentStore<WorkflowInstanceLogsState>(new())
2931
{
@@ -35,11 +37,21 @@ ISynapseApiClient apiClient
3537
/// </summary>
3638
protected ISynapseApiClient ApiClient { get; } = apiClient;
3739

40+
/// <summary>
41+
/// Gets the service used to build a bridge with JS
42+
/// </summary>
43+
protected JSInterop JSInterop { get; } = jsInterop;
44+
3845
/// <summary>
3946
/// Gets/sets the logs <see cref="Collapse"/> panel
4047
/// </summary>
4148
public Collapse? Collapse { get; set; }
4249

50+
/// <summary>
51+
/// The <see cref="ElementReference"/> containing the logs
52+
/// </summary>
53+
public ElementReference? LogsContainer { get; set; } = null;
54+
4355
#region Selectors
4456
/// <summary>
4557
/// Gets an <see cref="IObservable{T}"/> used to observe <see cref="WorkflowInstanceLogsState.Name"/> changes
@@ -101,6 +113,7 @@ public void SetNamespace(string @namespace)
101113
/// <summary>
102114
/// Toggles the <see cref="Collapse"/> panel
103115
/// </summary>
116+
/// <returns>An awaitable task</returns>
104117
public async Task ToggleAsync()
105118
{
106119
if (this.Collapse != null)
@@ -117,6 +130,7 @@ public async Task ToggleAsync()
117130
/// <summary>
118131
/// Toggles the <see cref="Collapse"/> panel
119132
/// </summary>
133+
/// <returns>An awaitable task</returns>
120134
public async Task HideAsync()
121135
{
122136
if (this.Collapse != null)
@@ -132,6 +146,7 @@ public async Task HideAsync()
132146
/// <summary>
133147
/// Reads and watches the logs
134148
/// </summary>
149+
/// <returns>An awaitable task</returns>
135150
public async Task LoadLogsAsync()
136151
{
137152
this.Reduce(state => state with
@@ -142,9 +157,19 @@ public async Task LoadLogsAsync()
142157
await this.WatchLogsAsync(); //fire and forget, otherwise the subscription is blocked
143158
}
144159

160+
/// <summary>
161+
/// Scrolls down the logs
162+
/// </summary>
163+
/// <returns>An awaitable task</returns>
164+
public async Task ScrollDown()
165+
{
166+
if (this.LogsContainer.HasValue) await this.JSInterop.ScrollDownAsync(this.LogsContainer.Value);
167+
}
168+
145169
/// <summary>
146170
/// Reads the logs form the API
147171
/// </summary>
172+
/// <returns>An awaitable task</returns>
148173
protected async Task ReadLogsAsync()
149174
{
150175
var name = this.Get(state => state.Name);
@@ -160,6 +185,7 @@ protected async Task ReadLogsAsync()
160185
/// <summary>
161186
/// Watches the logs
162187
/// </summary>
188+
/// <returns>An awaitable task</returns>
163189
protected async Task WatchLogsAsync()
164190
{
165191
this._watchCancellationTokenSource.Cancel();
@@ -169,7 +195,6 @@ protected async Task WatchLogsAsync()
169195
var @namespace = this.Get(state => state.Namespace);
170196
await foreach (ITextDocumentWatchEvent evt in await this.ApiClient.WorkflowInstances.WatchLogsAsync(name, @namespace, this._watchCancellationTokenSource.Token).ConfigureAwait(false))
171197
{
172-
Console.WriteLine(evt?.Content);
173198
if (evt != null && !string.IsNullOrEmpty(evt.Content))
174199
{
175200
if (evt.Type == TextDocumentWatchEventType.Appended)

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
<Spinner Class="me-3" Color="SpinnerColor.Primary" Size="SpinnerSize.Small" />
3030
}
3131
else {
32-
<pre class="monaco-editor logs-container p-2">
32+
<pre @ref="Store.LogsContainer" class="monaco-editor logs-container p-2">
3333
@foreach(string line in logs)
3434
{
3535
var match = Regex.Match(line, timestampLevelPattern);
@@ -94,4 +94,10 @@
9494
Store.SetNamespace(Namespace);
9595
}
9696
}
97+
98+
protected override async Task OnAfterRenderAsync(bool firstRender)
99+
{
100+
await base.OnAfterRenderAsync(firstRender);
101+
await Store.ScrollDown();
102+
}
97103
}

0 commit comments

Comments
 (0)