dbMango/Rms.Risk.Mango/Pages/User/Aggregate.razor
Alexander Shabarshov 2a7a24c9e7 Initial contribution
2025-11-03 14:43:26 +00:00

547 lines
16 KiB
Plaintext

@page "/user/aggregate"
@page "/user/aggregate/{DatabaseStr}/{CollectionStr}"
@page "/user/aggregate/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}"
@attribute [Authorize]
@using MongoDB.Bson.Serialization
@inject NavigationManager NavigationManager
@inject IUserSession UserSession
@inject IJSRuntime JsRuntime
@*
* dbMango
*
* Copyright 2025 Deutsche Bank AG
* SPDX-License-Identifier: Apache-2.0
*
* 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.
*@
<style>
.js-field {
/* height: 100px; */
position: relative;
width: 100%;
}
.res-field {
width: 100%;
height: calc(100vh - 150px);
overflow: auto;
background-color: #1d2a3b;
}
pre {
width: 100%;
height: 100%;
font-size: 10pt;
background-color: #1d2a3b;
color: #FFFFFF;
}
.CodeMirror {
height: 150px;
}
</style>
<h3 class="mt-3">Aggregate: @SelectedCollection</h3>
<AuthorizedOnly Policy="ReadAccess" Resource="@UserSession.Database">
<EditForm EditContext="EditContext">
<div class="form-row">
<FormItemSelect Enabled="@IsReady" Class="mr-2" Name="Collection" @bind-Value="SelectedCollection" Values="@Collections" Icon="icon-folder-outline-sm" />
<FormItemText Enabled="@IsReady" Class="mr-2" Name="Timeout (sec)" @bind-Value="Timeout" InputType="number" Icon="icon-clock-sm"/>
<FormItemText Enabled="@IsReady" Class="mr-2" Name="Max fetch size" @bind-Value="FetchSize" InputType="number" Icon="icon-download-selected-sm" />
<FormItemCheckBox Enabled="@IsReady" Class="mr-2" Name="Show as Json" @bind-Value="ShowAsJson" Icon="icon-tos-sm" />
<div class="flex-stack-vertical">
<div class="flex-stack-horizontal">
<div class="form-group">
<label>&nbsp;</label>
<FormButton Enabled="@IsReady" Name="Run" Icon="icon-running-man-sm" OnClick="Run" IsPrimary="true" />
</div>
<div class="form-group">
<label>&nbsp;</label>
<FormButton Enabled="@IsReady" Name="Explain" Icon="icon-layers-sm" OnClick="Explain" IsPrimary="false" />
</div>
<div class="form-group">
<label>&nbsp;</label>
<FormButton Enabled="@IsReady" Name="Format" Icon="icon-pencil-sm" OnClick="Format" IsPrimary="false" />
</div>
<div class="form-group">
<label>&nbsp;</label>
<FormButton Enabled="@IsReady" Name="Copy" Icon="icon-duplicate-document-sm" OnClick="Copy" IsPrimary="false" />
</div>
<div class="form-group">
<label>&nbsp;</label>
<FormButton Enabled="@IsReady" Name="Paste" Icon="icon-paste" OnClick="Paste" IsPrimary="false" />
</div>
</div>
<div class="flex-stack-horizontal">
<a href="https://www.mongodb.com/docs/manual/reference/command/aggregate/" target="_blank" class="mr-1">See documentation...</a>
<a href="https://www.mongodb.com/docs/manual/reference/operator/aggregation-pipeline/#std-label-aggregation-pipeline-operator-reference" target="_blank">Stages</a>
</div>
</div>
</div>
</EditForm>
<div class="row">
<div class="col-6">
@for (var i = 0; i < Stages.Count; i++)
{
var index = i;
<div class="flex-stack-horizontal flex-align-end">
<FormItemSelect Enabled="@IsReady" @bind-Value:get="@GetStageType(index)" @bind-Value:set="@(x => SetStageType(index, x))" Values="@_availableStages" />
<button type="button" class="btn btn-secondary" @onclick="() => AddStageAfter(index)" title="Add row">
<span class="ui-icon-font icon-plus-sm"></span>
</button>
@if (index > 0)
{
<button type="button" class="btn btn-secondary" @onclick="() => MoveStageUp(index)" title="Up">
<span class="ui-icon-font icon-arrow-up-sm"></span>
</button>
}
@if (index < Stages.Count - 1)
{
<button type="button" class="btn btn-secondary" @onclick="() => MoveStageDown(index)" title="Down">
<span class="ui-icon-font icon-arrow-down-sm"></span>
</button>
}
@if (index > 0)
{
<button type="button" class="btn btn-secondary" @onclick="() => RemoveStage(index)" title="Remove row">
<span class="ui-icon-font icon-trash-sm"></span>
</button>
}
<FormItemCheckBox @bind-Value="Stages[index].Use"/>
</div>
<div class="js-field">
<FormCodeEditor @bind-Text="Stages[index].Text" MediaType="application/x-json" Class=""/>
</div>
}
</div>
<div class="col-6">
@if (ShowAsJson)
{
<FormJson Class="res-field row" Value="@Result"/>
}
else
{
<SimplePivotComponent PivotName="find"
CollectionName="find"
PivotData="@ResultPivot"
/>
}
</div>
</div>
</AuthorizedOnly>
@code {
[Parameter] public string? DatabaseStr { get; set; }
[Parameter] public string? DatabaseInstanceStr { get; set; }
[Parameter] public string? CollectionStr { get; set; }
private class StageRec
{
public string Text { get; set; } = @"{
""_id"": {
""$ne"": """"
}
}";
public bool Use { get; set; } = true;
public string Type { get; set; } = "match";
}
private List<StageRec> Stages { get; set; } = [new ()];
private static readonly string[] _availableStages =
new[]{
"addFields",
"bucket",
"bucketAuto",
"changeStream",
"changeStreamSplitLargeEvent",
"collStats",
"count",
"currentOp",
"densify",
"documents",
"facet",
"fill",
"geoNear",
"graphLookup",
"group",
"indexStats",
"limit",
"listLocalSessions",
"listSampledQueries",
"listSearchIndexes",
"listSessions",
"lookup",
"match",
"merge",
"out",
"planCacheStats",
"project",
"querySettings",
"queryStats",
"redact",
"replaceRoot",
"replaceWith",
"sample",
"search",
"searchMeta",
"set",
"setWindowFields",
"shardedDataDistribution",
"skip",
"sort",
"sortByCount",
"unionWith",
"unset",
"unwind",
"vectorSearch",
}.OrderBy(x => x).ToArray();
private IReadOnlyCollection<string> Collections { get; set; } = [];
private string SelectedCollection
{
get => UserSession.Collection;
set
{
if (UserSession.Collection == value)
return;
UserSession.Collection = value;
SyncUrl();
}
}
private string Database
{
get => UserSession.Database;
set
{
if (UserSession.Database == value)
return;
UserSession.Database = value;
SyncUrl();
}
}
private string DatabaseInstance
{
get => UserSession.DatabaseInstance;
set
{
if (UserSession.DatabaseInstance == value)
return;
UserSession.DatabaseInstance = value;
SyncUrl();
}
}
private string Timeout { get; set; } = "20";
private string FetchSize { get; set; } = "10";
private bool IsReady { get; set; }
private bool ShowAsJson { get; set; }
private string Result { get; set; } = "{}";
private List<BsonDocument> ResultBson { get; set; } = [];
public ArrayBasedPivotData ResultPivot { get; set; } = new([]);
private EditContext? _editContext;
private EditContext EditContext => _editContext!;
protected override void OnInitialized()
{
_editContext = new (this);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
return;
if (string.IsNullOrWhiteSpace(DatabaseStr))
DatabaseStr = Database;
else
Database = DatabaseStr;
if (string.IsNullOrWhiteSpace(DatabaseInstanceStr))
DatabaseInstanceStr = DatabaseInstance;
else
DatabaseInstance = DatabaseInstanceStr;
if (string.IsNullOrWhiteSpace(CollectionStr))
CollectionStr = SelectedCollection;
else
SelectedCollection = CollectionStr;
SyncUrl();
var text = await JsRuntime.LoadFromLocalStorage("Aggregate");
if ( !string.IsNullOrWhiteSpace(text))
UpdateStages(text);
_ = Task.Run(async () =>
{
try
{
var admin = UserSession.MongoDbAdmin;
Collections = await admin.ListCollections();
if (!Collections.Contains(SelectedCollection))
SelectedCollection = Collections.FirstOrDefault() ?? "";
IsReady = true;
await InvokeAsync(StateHasChanged);
}
catch (Exception e)
{
await Display(e);
}
});
}
private async Task Format()
{
foreach (var stage in Stages)
{
if (!BsonDocument.TryParse(stage.Text, out var doc))
continue;
stage.Text = doc.ToJson(new() { Indent = true });
}
await InvokeAsync(StateHasChanged);
}
private Task Run() => Run(false);
private Task Explain() => Run(true);
private async Task Run(bool explain)
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout)));
try
{
IsReady = false;
await InvokeAsync(StateHasChanged);
var text = GetCombinedPipelineText();
await JsRuntime.SaveToLocalStorage("Aggregate", text, cts.Token);
var res = explain
? await RunExplain(text, cts.Token)
: await RunAggregate(text, cts.Token)
;
ResultPivot = res.Item1;
ResultBson = res.Item2;
await Display(ResultBson);
}
catch (Exception e)
{
await Display(e);
}
finally
{
IsReady = true;
await InvokeAsync(StateHasChanged);
}
}
private string GetCombinedPipelineText()
{
var text = "[" + string.Join(",", Stages.Where(x => x.Use && !string.IsNullOrWhiteSpace(x.Text)).Select(CombineStage)) + "]";
var arr = BsonSerializer.Deserialize<BsonArray>(text);
return arr.ToJson(new() { Indent = true });
}
private string CombineStage(StageRec arg) => $"{{ ${arg.Type} : {arg.Text} }}";
private Task Display(Exception e)
{
ShowAsJson = true;
Result = e.ToString();
return InvokeAsync(StateHasChanged);
}
private Task Display(IEnumerable<BsonDocument> res)
{
Result = res.ToJson(new() { Indent = true });
return InvokeAsync(StateHasChanged);
}
private async Task<(ArrayBasedPivotData, List<BsonDocument>)> RunExplain(string text, CancellationToken token)
{
var service = UserSession.MongoDb;
var command = $@"{{
""aggregate"" : ""{SelectedCollection}"",
""pipeline"" : {text},
""cursor"" : {{}}
}}";
var res = await service.ExplainAsync(command, token);
ShowAsJson = true;
return (ArrayBasedPivotData.NoData, [res]);
}
private async Task<(ArrayBasedPivotData, List<BsonDocument>)> RunAggregate(string text, CancellationToken token)
{
var service = UserSession.MongoDb;
var results = service.AggregateAsyncRaw(text, int.Parse(FetchSize), token: token);
var fieldMap = new ConcurrentDictionary<string, FieldMapping>
{
["find"] = new(false)
};
var (pd, list) = await MongoDbDataSource.FetchPivotData(
"find",
"find",
fieldMap,
results,
null,
true,
int.Parse(FetchSize),
false,
token);
if (list.Count == 0)
list.Add(BsonDocument.Parse(@$"{{""Result"": ""No data fetched from {SelectedCollection}""}}"));
return (pd, list);
}
private void SyncUrl()
{
var url = NavigationManager.BaseUri + $"user/aggregate/{Database}";
if (!string.IsNullOrWhiteSpace(DatabaseInstance))
url += $"/{DatabaseInstance}";
url += $"/{SelectedCollection}";
JsRuntime.InvokeAsync<string>("DashboardUtils.ChangeUrl", url);
}
private async Task AddStageAfter(int i)
{
Stages.Insert(i+1,new());
await InvokeAsync(StateHasChanged);
}
private async Task RemoveStage(int i)
{
if (i == 0)
return;
Stages.RemoveAt(i);
await InvokeAsync(StateHasChanged);
}
private async Task MoveStageUp(int i)
{
if (i == 0)
return;
var item = Stages[i];
Stages.RemoveAt(i);
Stages.Insert(i - 1, item);
await InvokeAsync(StateHasChanged);
}
private async Task MoveStageDown(int i)
{
if (i >= Stages.Count-1)
return;
var item = Stages[i];
Stages.RemoveAt(i);
Stages.Insert(i+1, item);
await InvokeAsync(StateHasChanged);
}
private async Task Copy()
{
var text = GetCombinedPipelineText();
await JsRuntime.InvokeVoidAsync("DashboardUtils.CopyToClipboard", text);
}
private async Task Paste()
{
try
{
Result = "";
await InvokeAsync(StateHasChanged);
var text = await JsRuntime.InvokeAsync<string>("DashboardUtils.PasteFromClipboard");
UpdateStages(text);
await InvokeAsync(StateHasChanged);
}
catch (Exception e)
{
await Display(e);
}
}
private void UpdateStages(string text)
{
try
{
text = string.IsNullOrWhiteSpace(text)
? ""
: text.Trim()
;
if (!text.StartsWith("[") || text.StartsWith("]"))
return;
var arr = BsonSerializer.Deserialize<BsonArray>(text);
if (arr == null || arr.Count == 0)
return;
var newStages = new List<StageRec>();
foreach (var stage in arr.OfType<BsonDocument>().Where(x => x.ElementCount == 1))
{
var type = stage.ElementAt(0).Name;
var txt = stage.ElementAt(0).Value.ToJson(new(){Indent = true});
newStages.Add(new() { Type = type[1..], Text = txt, Use = true });
}
Stages.Clear();
Stages.AddRange(newStages);
}
catch (Exception)
{
// ignore
}
if ( Stages.Count == 0 )
Stages.Add(new());
}
private string GetStageType(int index) => Stages[index].Type;
private void SetStageType(int index, string value)
{
Stages[index].Type = value;
}
}