486 lines
16 KiB
Plaintext
486 lines
16 KiB
Plaintext
@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.
|
|
*@
|
|
|
|
<h3 class="mt-3">Download</h3>
|
|
|
|
<style>
|
|
.meta {
|
|
color: gray;
|
|
font-style: italic;
|
|
}
|
|
.idx {
|
|
color: lightgreen;
|
|
margin-top: 5px;
|
|
}
|
|
</style>
|
|
|
|
<AuthorizedOnly Policy="AdminAccess" Resource="@UserSession.Database">
|
|
<div class="row mt-3">
|
|
<SplitPanel Orientation="horizontal" InitialSplit="0.3">
|
|
<First>
|
|
<div class="ml-1 mr-1">
|
|
<div class="form-row">
|
|
<FormButton Name="Select meta" OnClick="OnSelectAllMeta" Icon="icon-context-info-outline-sm" Class=""/>
|
|
<FormButton Name="Select all" OnClick="OnSelectAllCurrent" Icon="icon-view-sm" Class=""/>
|
|
<FormButton Name="Select none" OnClick="OnSelectNoneCurrent" Icon="icon-hide-sm" Class=""/>
|
|
</div>
|
|
<div class="mt-3">
|
|
<TreeComponent TItem="IndexTreeItem" RootNodes="@_currentNodes" ReadOnly="true">
|
|
<LabelTemplate Context="item">
|
|
<div class="row ml-1">
|
|
@if ( item.Data == null ) // index label
|
|
{
|
|
<div class="idx">@item.Label</div>
|
|
}
|
|
else if (item.Data.Collection != null ) // collection node
|
|
{
|
|
<FormItemCheckBox LabelOn="" LabelOff="" Value="@item.Data.IsSelected" ValueChanged="@(x => SetSourceItem(item, x))"/>
|
|
if (item.Data.Collection.Name.EndsWith("-Meta"))
|
|
{
|
|
<div class="meta ml-1">@item.Label</div>
|
|
}
|
|
else
|
|
{
|
|
<div class="ml-1">@item.Label</div>
|
|
}
|
|
}
|
|
</div>
|
|
</LabelTemplate>
|
|
</TreeComponent>
|
|
</div>
|
|
</div>
|
|
</First>
|
|
<Second>
|
|
<div class="ml-1 mr-1">
|
|
<div class="form-row">
|
|
<div class="form-group">
|
|
<FormButton Name="Download" OnClick="OnDownload" Icon="icon-download-sm" Class="mr-2"/>
|
|
<label> </label>
|
|
</div>
|
|
<FormItemSelect Enabled="@IsReady" Class="mr-2" Name="Running jobs:" @bind-Value="SelectedJob" Values="@RunningJobs" />
|
|
|
|
<div class="form-group">
|
|
<label> </label>
|
|
<FormButton Enabled="@IsReadyToCancel" Name="Cancel job" Icon="icon-trash-sm" OnClick="CancelJob" IsPrimary="false" Class="btn-outline-danger" />
|
|
</div>
|
|
|
|
</div>
|
|
<TableControl Items="@SelectedJob.Status" PagerSize="35" Class="table table-hover table-striped table-forge table-forge-striped">
|
|
<TableColumnControl Name="Collection" Field="DestinationCollection" />
|
|
<TableColumnControl Name="Count" Field="Count" Format="N0" />
|
|
<TableColumnControl Name="Copied" Field="Copied" Format="N0" />
|
|
<TableColumnControl Name="Progress" Field="Progress" Format="P2" />
|
|
<TableColumnControl Name="DPS" Field="DocsPerSecond" Format="N0" />
|
|
<TableColumnControl Name="Elapsed" Field="Elapsed" Format="hh\:mm\:ss" />
|
|
<TableColumnControl Name="Remaining" Field="Remaining" Format="hh\:mm\:ss" />
|
|
<TableColumnControl Name="Complete" Field="Complete" />
|
|
<TableColumnControl Name="Error" Field="Error" />
|
|
</TableControl>
|
|
|
|
@if (!string.IsNullOrWhiteSpace(Error))
|
|
{
|
|
<pre class="wf-converter-syntax-error">@Error</pre>
|
|
}
|
|
|
|
@if ( SelectedJob.Complete && !string.IsNullOrWhiteSpace(SelectedJob.DownloadUrl) )
|
|
{
|
|
<a class="mt-3" target="_blank" href="@SelectedJob.DownloadUrl">Download results</a>
|
|
}
|
|
</div>
|
|
</Second>
|
|
</SplitPanel>
|
|
</div>
|
|
|
|
</AuthorizedOnly>
|
|
|
|
@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<MigrationJob> RunningJobs { get; set; } = [];
|
|
|
|
private CancellationTokenSource _cts = new();
|
|
|
|
private readonly List<TreeNode<IndexTreeItem>> _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<string>("DashboardUtils.ChangeUrl", url);
|
|
}
|
|
|
|
public static async Task<(List<DatabaseStructureLoader.CollectionStructure>, TreeNode<IndexTreeItem>)> LoadStructure(IMongoDbDatabaseAdminService db, IMongoDbDatabaseAdminService admin, CancellationToken token)
|
|
{
|
|
var root = new TreeNode<IndexTreeItem>
|
|
{
|
|
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<IndexTreeItem>
|
|
{
|
|
Label = gr.Key,
|
|
IsExpanded = true,
|
|
Children = gr
|
|
.OrderBy(x => x.Value.Name)
|
|
.Select(coll => new TreeNode<IndexTreeItem>
|
|
{
|
|
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<IndexTreeItem> item, bool value, Func<TreeNode<IndexTreeItem>, 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<IndexTreeItem> item, bool b)
|
|
{
|
|
Set(item, b);
|
|
return InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
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 = _currentNodes[0].Children
|
|
.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<string, string> GetInfo(MigrationJob job)
|
|
{
|
|
var info = new Dictionary<string, string>
|
|
{
|
|
["from"] = job.SourceDatabase,
|
|
["batchSize"] = job.BatchSize.ToString(),
|
|
};
|
|
return info;
|
|
}
|
|
|
|
private Task<string?> 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);
|
|
}
|
|
|
|
}
|