@page "/admin/download" @page "/admin/download/{DatabaseStr}" @page "/admin/download/{DatabaseStr}/{DatabaseInstanceStr}" @implements IDisposable @using Rms.Risk.Mango.Pivot.Core.MongoDb @attribute [Authorize] @inject NavigationManager NavigationManager @inject IUserSession UserSession @inject IJSRuntime JsRuntime @inject IMigrationEngine MigrationEngine @* * 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. *@

Download

@if ( item.Data == null ) // index label {
@item.Label
} else if (item.Data.Collection != null ) // collection node { if (item.Data.Collection.Name.EndsWith("-Meta")) {
@item.Label
} else {
@item.Label
} }
@if (!string.IsNullOrWhiteSpace(Error)) {
@Error
} @if ( SelectedJob.Complete && !string.IsNullOrWhiteSpace(SelectedJob.DownloadUrl) ) { Download results }
@code { [CascadingParameter] public IModalService Modal { get; set; } = null!; [Parameter] public string? DatabaseStr { get; set; } [Parameter] public string? DatabaseInstanceStr { get; set; } public class IndexTreeItem { public DatabaseStructureLoader.CollectionStructure? Collection { get; set; } public bool IsSelected { get; set; } } private bool IsReady { get; set; } private bool IsReadyToCancel => IsReady && !SelectedJob.Complete; private string Error { get; set; } = ""; private List RunningJobs { get; set; } = []; private CancellationTokenSource _cts = new(); private readonly List> _currentNodes = [ new () { Label = "Collections", Children = [] } ]; 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 MigrationJob SelectedJob { get; set { if (field.JobId == value.JobId) return; field = value; StateHasChanged(); } } = new() { Complete = true }; 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; SyncUrl(); try { RunningJobs = MigrationEngine.List().Where(x => x.Type == MigrationJob.JobType.Download).ToList(); if (RunningJobs.Count > 0) SelectedJob = RunningJobs[0]; _ = Task.Run(() => RefreshLoop(_cts.Token), _cts.Token); var cts2 = new CancellationTokenSource(TimeSpan.FromMinutes(2)); var (_, root) = await LoadStructure(UserSession.MongoDbAdmin, UserSession.MongoDbAdminForAdminDatabase, cts2.Token); _currentNodes.Clear(); _currentNodes.Add(root); } catch (Exception ex) { await ModalDialogUtils.ShowExceptionDialog(Modal, $"Error loading structure for {Database}", ex); return; } IsReady = true; StateHasChanged(); } public void Dispose() { _cts.Cancel(); _cts.Dispose(); } private async Task RefreshLoop(CancellationToken token) { // percents are always increasing, so it safe to compare sum() var prevState = -1.0; var prevJobId = SelectedJob.JobId; var oldComplete = false; while (!token.IsCancellationRequested) { if (prevJobId != SelectedJob.JobId) { prevJobId = SelectedJob.JobId; prevState = -1.0; oldComplete = false; await InvokeAsync(StateHasChanged); } else { var newState = SelectedJob.Status.Sum(x => x.Count + x.Copied + (x.Cleared ?? 0)); if (SelectedJob.Complete != oldComplete || Math.Abs(newState - prevState) > 0.01) { prevState = newState; oldComplete = SelectedJob.Complete; await InvokeAsync(StateHasChanged); } } await Task.Delay(TimeSpan.FromSeconds(1), token); } } private void SyncUrl() { var url = NavigationManager.BaseUri + $"admin/download/{Database}"; if (!string.IsNullOrWhiteSpace(DatabaseInstance)) url += $"/{DatabaseInstance}"; JsRuntime.InvokeAsync("DashboardUtils.ChangeUrl", url); } public static async Task<(List, TreeNode)> LoadStructure(IMongoDbDatabaseAdminService db, IMongoDbDatabaseAdminService admin, CancellationToken token) { var root = new TreeNode { Label = "Collections" }; var collStructure = await DatabaseStructureLoader.LoadCollections(db, admin, token); var collections = (collStructure) .Select(x => new { Group = x.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ? " Meta" : x.Name.EndsWith("-Cache", StringComparison.OrdinalIgnoreCase) ? " Cache" : " Data", Value = x } ) .ToList(); var allCollections = collections .OrderBy(x => x.Value.Name) .Select( x => x.Value ) .ToList(); // Add collections to the root node var collectionNodes = collections .GroupBy(x => x.Group) .OrderBy(x => x.Key) .Select(gr => new TreeNode { Label = gr.Key, IsExpanded = true, Children = gr .OrderBy(x => x.Value.Name) .Select(coll => new TreeNode { Label = coll.Value.Name, IsExpanded = false, Data = new() { Collection = coll.Value, } }) .ToList() }) .ToList(); root.Children = collectionNodes; root.IsExpanded = true; Set(root, false); foreach( var node in root.Children) Set(node, true, x => x.Data?.Collection?.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ?? false); return (collStructure, root); } private static void Set(TreeNode item, bool value, Func, bool>? filter = null ) { filter ??= _ => true; if (item.Data != null && filter(item)) { item.Data.IsSelected = value; } foreach (var child in item.Children) { Set(child, value, filter); } } private Task OnSelectAllCurrent() { foreach( var node in _currentNodes) Set(node, true); return InvokeAsync(StateHasChanged); } private Task OnSelectAllMeta() { foreach( var node in _currentNodes) Set(node, true, x => x.Data?.Collection?.Name.EndsWith("-Meta", StringComparison.OrdinalIgnoreCase) ?? false); return InvokeAsync(StateHasChanged); } private Task OnSelectNoneCurrent() { foreach( var node in _currentNodes) Set(node, false); return InvokeAsync(StateHasChanged); } private Task SetSourceItem(TreeNode item, bool b) { Set(item, b); return InvokeAsync(StateHasChanged); } private static IEnumerable> GetAllNodes(TreeNode node) { yield return node; foreach (var child in node.Children) { foreach (var descendant in GetAllNodes(child)) { yield return descendant; } } } private async Task OnDownload() { var newJob = new MigrationJob { Type = MigrationJob.JobType.Download, SourceDatabase = UserSession.Database, SourceDatabaseInstance = UserSession.DatabaseInstance, DestinationDatabase = UserSession.Database, DestinationDatabaseInstance = UserSession.DatabaseInstance!, Email = UserSession.User.GetEmail(), Upsert = false, ClearDestinationBefore = false, BatchSize = 1, Status = GetAllNodes(_currentNodes[0]) .Where(x => (x.Data?.IsSelected ?? false) && x.Data.Collection != null) .Select(x => new MigrationJob.CollectionJob { SourceCollection = x.Data!.Collection!.Name, DestinationCollection = x.Data!.Collection!.Name, }) .ToList() }; if (newJob.Status.Count == 0) { await ModalDialogUtils.ShowInfoDialog(Modal, "Download data", "Please, select at least one collection."); return; } var info = GetInfo(newJob); var res = await ModalDialogUtils.ShowConfirmationDialog( Modal, "Download data", $"Are you sure to start downloading of {newJob.Status.Count} collection(s) from {newJob.SourceDatabase}?", info ); if (res.Cancelled) return; await RegisterNewJob(newJob); } private async Task CancelJob() { if (SelectedJob.Complete) return; var info = GetInfo(SelectedJob); var res = await ModalDialogUtils.ShowConfirmationDialog( Modal, "Migration", $"Are you sure to cancel migration of {SelectedJob.Status.Count} collection(s) from {SelectedJob.SourceDatabase} to {SelectedJob.DestinationDatabase}?", info ); if (res.Cancelled) return; await MigrationEngine.Cancel(SelectedJob, UserSession.User.GetUser()); await InvokeAsync(StateHasChanged); } private Dictionary GetInfo(MigrationJob job) { var info = new Dictionary { ["from"] = job.SourceDatabase, ["batchSize"] = job.BatchSize.ToString(), }; return info; } private Task CanExecuteCommand() => Shell.CanExecuteCommand(UserSession, InvokeAsync, Modal); private async Task RegisterNewJob(MigrationJob newJob) { try { var ticket = await CanExecuteCommand(); if (string.IsNullOrWhiteSpace(ticket)) return; newJob.Ticket = ticket; await MigrationEngine.Add(newJob, UserSession.User.GetUser()); SelectedJob = newJob; RunningJobs = MigrationEngine.List(); } catch (Exception e) { Error = e.ToString(); } await InvokeAsync(StateHasChanged); } }