@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. *@

Aggregate: @SelectedCollection

@for (var i = 0; i < Stages.Count; i++) { var index = i;
@if (index > 0) { } @if (index < Stages.Count - 1) { } @if (index > 0) { }
}
@if (ShowAsJson) { } else { }
@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 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 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 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(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 res) { Result = res.ToJson(new() { Indent = true }); return InvokeAsync(StateHasChanged); } private async Task<(ArrayBasedPivotData, List)> 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)> RunAggregate(string text, CancellationToken token) { var service = UserSession.MongoDb; var results = service.AggregateAsyncRaw(text, int.Parse(FetchSize), token: token); var fieldMap = new ConcurrentDictionary { ["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("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("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(text); if (arr == null || arr.Count == 0) return; var newStages = new List(); foreach (var stage in arr.OfType().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; } }