@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(" ");
}