@page "/user/browse" @page "/user/browse/{DatabaseStr}/{CollectionStr}" @page "/user/browse/{DatabaseStr}/{DatabaseInstanceStr}/{CollectionStr}" @using Rms.Risk.Mango.Language @using Rms.Risk.Mango.Pivot.Core.MongoDb @implements IDisposable @attribute [Authorize] @inject NavigationManager NavigationManager @inject IUserSession UserSession @inject IJSRuntime JsRuntime @inject IAuthorizationService Auth @* * 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. *@

Browse: @SelectedCollection

@context
@if (Stats.TryGetValue(context, out var stat)) { if (stat.Sharded) {
Sharded
}
@NumbersUtils.ToHumanReadable(stat.Size)
}
@* *@ @if ( !IsReady ) { } else if (ShowAsJson) { } else { }
@code { [CascadingParameter] public IModalService Modal { get; set; } = null!; [Parameter] public string? DatabaseStr { get; set; } [Parameter] public string? DatabaseInstanceStr { get; set; } [Parameter] public string? CollectionStr { get; set; } [Parameter] [SupplyParameterFromQuery(Name = "q")] public string Text { get; set; } = ""; private List Collections { get; set; } = []; private ConcurrentDictionary Stats { get; set; } = new(StringComparer.OrdinalIgnoreCase); private static readonly List _rowValues = Enumerable.Range(0, 8).Select(x => x * 5 + 25).ToList(); private Dictionary> AllHistory { get; set; } = []; private int Rows { get; set { if (field == value) return; field = value; InvokeAsync(StateHasChanged); } } = 35; private List History { get { var key = $"{Database}|{DatabaseInstance}|{SelectedCollection}"; if ( AllHistory.TryGetValue(key, out var hist) ) { hist.Sort(StringComparer.OrdinalIgnoreCase); return hist; } hist = []; AllHistory[key] = hist; return hist; } } private string SelectedCollection { get => UserSession.Collection; set { if (UserSession.Collection == value) return; UserSession.Collection = value; ShowDropdown = false; Text = ""; SyncUrl(); InvokeAsync(StateHasChanged); } } private string Database { get => UserSession.Database; set { if (UserSession.Database == value) return; UserSession.Database = value; SyncUrl(); InvokeAsync(StateHasChanged); } } 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; } = "100"; private bool IsReady { get; set; } private bool ShowAsJson { get; set; } private bool ShowDropdown { 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!; private readonly CancellationTokenSource _globalCancellation = new(); private static string GetCollectionClass(string collectionName) => collectionName.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ? "meta" : ""; protected override void OnInitialized() { _editContext = new (this); } public void Dispose() { _globalCancellation.Cancel(); _globalCancellation.Dispose(); } private class BrowseState { public string Text { get; set; } = ""; public int Rows { get; set; } = 25; public Dictionary> History { get; set; } = []; public string? Collection { get; set; } } 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; var cts = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); var state = await GetState(cts.Token); if ( state != null ) { if ( state.History.Any() ) AllHistory = state.History; Rows = state.Rows; if ( string.IsNullOrWhiteSpace(Text) && !string.IsNullOrWhiteSpace(state.Text)) Text = state.Text; if (string.IsNullOrWhiteSpace(CollectionStr) && !string.IsNullOrWhiteSpace(state.Collection)) CollectionStr = state.Collection; } if (string.IsNullOrWhiteSpace(CollectionStr)) CollectionStr = SelectedCollection; else SelectedCollection = CollectionStr; SyncUrl(); if (Text is "null" or null) Text = ""; _ = Task.Run(async () => { try { var admin = UserSession.MongoDbAdmin; var cts2 = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); var grouped = (await admin.ListCollections(cts2.Token)) .GroupBy(x => x.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ? " Meta" : x.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase) ? " Cache" : " Data" ); Collections.Clear(); foreach( var group in grouped) { Collections.Add(group.Key); Collections.AddRange(group.OrderBy(x => x).Select(x => x)); } if (!Collections.Contains(SelectedCollection)) SelectedCollection = Collections.FirstOrDefault( x => !x.StartsWith(" ")) ?? ""; IsReady = true; await InvokeAsync(StateHasChanged); _ = Task.Run(LoadCollStats, _globalCancellation.Token); } catch (Exception e) { await Display(e); } }, _globalCancellation.Token); } private async Task LoadCollStats() { var admin = UserSession.MongoDbAdmin; var collections = Collections.ToList(); // make a copy to avoid modifying the original list during iteration foreach (var name in collections) { try { var cts = CancellationTokenSource.CreateLinkedTokenSource(_globalCancellation.Token, new CancellationTokenSource(TimeSpan.FromSeconds(5)).Token); var doc = await admin.CollStats(name, cts.Token); doc.Details = null; // Remove details to avoid large output Stats[name] = doc; await InvokeAsync(StateHasChanged); } catch (Exception) { // ignore } } } private async Task GetState(CancellationToken token) { try { var json = await JsRuntime.LoadFromLocalStorage("Browse", token); if (string.IsNullOrWhiteSpace(json)) return null; return JsonUtils.FromJson(json); } catch (Exception) { return null; } } enum ActionType { Find } private async Task Run(ActionType actionType) { var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); try { IsReady = false; ShowDropdown = false; await InvokeAsync(StateHasChanged); await SaveState(cts.Token); if (int.TryParse(FetchSize, out var limit) == false) limit = 0; var json = ""; if (!string.IsNullOrWhiteSpace(Text)) { var afh = $$"""FROM "{{SelectedCollection}}" PIPELINE { WHERE {{Text}} }"""; try { var ast = LanguageParser.ParseScriptToAST(afh); json = ast.AsJson()!.ToJsonString(new() { WriteIndented = true }); } catch (Exception e) { await Display(e); return; } } var res = actionType switch { ActionType.Find => await RunFind(json, limit, cts.Token), _ => throw new ArgumentOutOfRangeException(nameof(actionType), actionType, null) }; ResultPivot = res.Item1; ResultBson = res.Item2; if (!History.Contains(Text)) { History.Insert(0, Text); await SaveState(cts.Token); } SyncUrl(); await Display(ResultBson); } catch (Exception e) { await Display(e); } finally { IsReady = true; await InvokeAsync(StateHasChanged); } } private async Task SaveState(CancellationToken token) { try { foreach( var key in AllHistory.Keys.ToList()) { var hist = AllHistory[key]; AllHistory[key] = hist.Distinct(StringComparer.OrdinalIgnoreCase).Take(100).ToList(); } var state = new BrowseState { Text = Text, Collection = SelectedCollection, Rows = Rows, History = AllHistory }; var json = JsonUtils.ToJson(state); await JsRuntime.SaveToLocalStorage("Browse", json, token); } catch (Exception) { // Ignore errors during saving state // This is not critical, and we can continue without saving } } private Task Run() => Run(ActionType.Find); 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)> RunFind(string text, int limit, CancellationToken token) { var service = UserSession.MongoDb; var results = string.IsNullOrWhiteSpace(text) || text.Trim() == "{}" ? service.FindAsync("{}", limit: limit <= 0 ? -1 : limit, token: token) : service.AggregateAsyncRaw(text, limit <= 0 ? -1 : limit, token: token); var fieldMap = new ConcurrentDictionary { ["find"] = new(false) }; var (pd, list) = await MongoDbDataSource.FetchPivotData( "find", "find", fieldMap, results, null, true, int.Parse(FetchSize), true, 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/browse/{Database}"; if (!string.IsNullOrWhiteSpace(DatabaseInstance)) url += $"/{DatabaseInstance}"; url += $"/{SelectedCollection}"; if (!string.IsNullOrWhiteSpace(Text)) url += $"?q={Uri.EscapeDataString(Text)}"; JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); } private async Task OnCellClick(DynamicObject row, string columnName) { try { var id = TableControl.GetDynamicMember(row, "_id"); if (id == null) return true; var bson = ResultBson.FirstOrDefault(x => x["_id"].ToString() == id.ToString()); if (bson == null) return true; var oldId = bson["_id"]; var enableWrite = await Auth.AuthorizeAsync( UserSession.User.GetUser(), UserSession.Database, [new WriteAccessRequirement()]); var res = await Find.ShowBsonDialog(Modal, id.ToString()!, bson, enableWrite.Succeeded); if (res == null) return true; if (enableWrite.Succeeded) { if (res.Value.ShouldUpdate) { bson = BsonDocument.Parse(res.Value.Json); await OnUpdate(oldId, bson); } else await OnDelete(oldId); } } catch (Exception e) { await ModalDialogUtils.ShowExceptionDialog(Modal, "Error", e); } return true; } private async Task OnDelete(BsonValue id) { var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); if (string.IsNullOrWhiteSpace(ticket)) return; var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Delete {id}", "Are you sure to delete document?"); if (r.Cancelled) return; var command = BsonDocument.Parse($@"{{ ""delete"": ""{SelectedCollection}"", ""deletes"": [{{ ""q"": {{ }}, ""limit"": 1 }}] }}"); var idDoc = new BsonDocument { ["_id"] = id }; command["deletes"][0]["q"] = idDoc; var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); Shell.UpdateComment(command, ticket, UserSession.User.GetEmail()); var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token); var deleted = res["n"].ToInt32(); if (deleted != 1) { Result = res.ToJson(new() { Indent = true }) + "\n------------------------------------------------------------------\n"+ command.ToJson(new() { Indent = true }); ShowAsJson = true; await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Failure"); await InvokeAsync(StateHasChanged); } else await ModalDialogUtils.ShowInfoDialog(Modal, $"Delete {id}", "Success"); } private async Task OnUpdate(BsonValue id, BsonDocument newBson) { var ticket = await Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); if (string.IsNullOrWhiteSpace(ticket)) return; var r = await ModalDialogUtils.ShowConfirmationDialog(Modal, $"Update {id}", "Are you sure to update document?"); if (r.Cancelled) return; var command = BsonDocument.Parse($@"{{ update: ""{SelectedCollection}"", updates: [ {{ q: {{ }}, u: {{ }}, upsert: false, multi: false, }} ], ordered: false, bypassDocumentValidation: false }}"); var idDoc = new BsonDocument { ["_id"] = id }; command["updates"][0]["q"] = idDoc; command["updates"][0]["u"] = newBson; Shell.UpdateComment(command, ticket, UserSession.User.GetEmail()); var cts = new CancellationTokenSource(TimeSpan.FromSeconds(int.Parse(Timeout))); var res = await UserSession.MongoDbAdmin.RunCommand(command, cts.Token); var modified = res["nModified"].ToInt32(); if (modified != 1) { Result = res.ToJson(new() { Indent = true }) + "\n------------------------------------------------------------------\n"+ command.ToJson(new() { Indent = true }); ShowAsJson = true; await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Failure"); await InvokeAsync(StateHasChanged); } else await ModalDialogUtils.ShowInfoDialog(Modal, $"Update {id}", "Success"); } private bool IsSelectable(string arg) => !arg.StartsWith(" "); }