@using System.Text @using Newtonsoft.Json @using Rms.Risk.Mango.Pivot.Core @using Rms.Risk.Mango.Pivot.Core.Models @inject IPivotSharingService PivotSharingService @inject IJSRuntime Js @* * 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. *@
@if (LastRefresh != default || LastRefreshElapsed != TimeSpan.Zero) {

Last refresh @LastRefresh.ToLongTimeString() took @LastRefreshElapsed.ToString("g")

} @code { private const string SavePivotDialogHeader = "Save Pivot"; private const string SharePivotDialogHeader = "Share Pivot"; [CascadingParameter] public IModalService Modal { get; set; } = null!; [Inject] public IUserService UserSession { get; set; } = null!; #region Parameters // ================================================= PARAMETERS ======================================================== // = = // = = // = = // ================================================= PARAMETERS ======================================================== [Parameter] public IPivotTableDataSource.PivotType PivotType { get; set; } = IPivotTableDataSource.PivotType.Predefined; [Parameter] public RenderFragment ExtraFilter { get; set; } = null!; [Parameter] public IPivotedData? PivotData { get; set { if (field == value) return; field = value; PivotDataChanged.InvokeAsync(field); InvokeAsync(StateHasChanged); } } [Parameter] public EventCallback PivotDataChanged { get; set; } /// /// Unlike Pivot which contains pivot that would be executed CurrentPivot holds definition /// that is already shown and corresponding to PivotData. /// Always set PivotData and CurrentPivot at the same time. /// [Parameter] public PivotDefinition? CurrentPivot { get; set { if (value == null || field == value) return; field = value; CurrentPivotChanged.InvokeAsync(field); } } [Parameter] public EventCallback CurrentPivotChanged { get; set; } [Parameter] public IPivotTableDataSource PivotService { get; set; } = null!; [Parameter] public bool ShowCollection { get; set; } = true; [Parameter] public string? Collection { get; set { if ( field == value ) return; field = value; if (!string.IsNullOrWhiteSpace(field)) { CollectionChanged.InvokeAsync(field); if (Collections.Count > 0 && SelectedCollectionNode != null) SelectedPivotNode = SelectedCollectionNode.Pivots.FirstOrDefault(x => x.Text == Pivot); } } } [Parameter] public EventCallback CollectionChanged { get; set; } [Parameter] public string? Pivot { get; set { if (field == value) return; field = value; if (!string.IsNullOrWhiteSpace(field)) PivotChanged.InvokeAsync(field); if (Collections.Count > 0 && SelectedCollectionNode != null) SelectedPivotNode = SelectedCollectionNode.Pivots.FirstOrDefault(x => x.Text == Pivot); } } [Parameter] public EventCallback PivotChanged { get; set; } [Parameter] public DateTime LastRefresh { get; set { if (field == value) return; field = value; LastRefreshChanged.InvokeAsync(field); InvokeAsync(StateHasChanged); } } [Parameter] public EventCallback LastRefreshChanged { get; set; } [Parameter] public TimeSpan LastRefreshElapsed { get; set { if (field == value) return; field = value; LastRefreshElapsedChanged.InvokeAsync(field); InvokeAsync(StateHasChanged); } } [Parameter] public EventCallback LastRefreshElapsedChanged { get; set; } [Parameter] public Navigation? Navigation { get; set { if (field == value) return; field = value; NavigationChanged.InvokeAsync(field); } } [Parameter] public EventCallback> NavigationChanged { get; set; } [Parameter] public string? AllPivotsAccessPolicyName { get; set; } [Parameter] public Func GetExtraFilter { get; set; } = () => null; // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedMember.Local [Parameter] public bool UseCache { get => _useCache; set { if (_useCache == value) return; _useCache = value; UseCacheChanged.InvokeAsync(_useCache); } } [Parameter] public EventCallback UseCacheChanged { get; set; } [Parameter] public int Rows { get; set { if (field == value) return; field = value; RowsChanged.InvokeAsync(field); InvokeAsync(StateHasChanged); } } = 40; [Parameter] public EventCallback RowsChanged { get; set; } [Parameter, EditorRequired] public List Collections { get; set; } = []; // ReSharper restore UnusedMember.Local // ReSharper restore UnusedAutoPropertyAccessor.Local // ================================================= END OF PARAMETERS ================================================= // = = // = = // = = // ================================================= END OF PARAMETERS ================================================= #endregion public void NavigateTo(string collection, GroupedPivot pivot) { if (string.IsNullOrWhiteSpace(collection) || pivot.IsGroup) return; Collection = collection; Pivot = pivot.Pivot.Name; SelectedPivotNode = pivot; } private GroupedCollection? SelectedCollectionNode => Collections.FirstOrDefault(x => x.CollectionNameWithPrefix == Collection); private GroupedPivot? SelectedPivotNode { get { if (field != null) return field; if (string.IsNullOrWhiteSpace(Pivot)) return null; field = SelectedCollectionNode?.Pivots.FirstOrDefault(x => x.Text == Pivot); return field; } set { if ( field == value ) return; field = value; InvokeAsync(StateHasChanged); } } private bool IsExportEnabled { get; set; } private bool IsShareDisabled => CurrentPivot == null; private bool _isFilterHidden = true; private bool _useCache = true; public PivotTableComponent PivotTable { get; private set; } = null!; private HashSet AllDataFields => SelectedCollectionNode?.DataFields ?? []; private HashSet AllKeyFields => SelectedCollectionNode?.KeyFields ?? []; private Task OnCopyCsv() => PivotTable.CopyCsv(); private Task OnExportCsv() => PivotTable.ExportCsv(Uri.EscapeDataString($"{SelectedPivotNode?.Pivot.Name}.csv")); private string FilterHiddenClass => _isFilterHidden ? "hidden" : ""; private Task ShowHideFilter() { _isFilterHidden = !_isFilterHidden; return InvokeAsync(StateHasChanged); } private Dictionary GetAllFields() { if (SelectedCollectionNode?.FieldTypes != null) return SelectedCollectionNode.FieldTypes .ToDictionary( x => x.Key, x => x.Value.Type ); var res = new Dictionary(StringComparer.OrdinalIgnoreCase); if (AllKeyFields.Count > 0) { foreach (var k in AllKeyFields.Where(k => !res.ContainsKey(k))) { res[k] = typeof(string); } } if (AllDataFields.Count > 0) { foreach (var k in AllDataFields.Where(k => !res.ContainsKey(k))) { res[k] = typeof(double); } } return res; } protected override void OnInitialized() { if (SelectedPivotNode == null) { SelectedPivotNode = SelectedCollectionNode?.Pivots.FirstOrDefault(x => x.Text == Pivot); StateHasChanged(); } } protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); if (!firstRender) return; Navigation ??= new(PivotTable.Navigate); await InvokeAsync(StateHasChanged); } private Task OnRefreshPivot() => PivotTable.RunPivot(); private bool IsSaveDisabled => Collection == null || SelectedPivotNode == null || SelectedPivotNode.IsGroup || SelectedCollectionNode?.Pivots == null; private bool IsDeleteDisabled => IsSaveDisabled || SelectedPivotNode?.Pivot.IsPredefined == true; private static string Base64Encode(string plainText) { var plainTextBytes = Encoding.UTF8.GetBytes(plainText); return Convert.ToBase64String(plainTextBytes); } private Task GetUserName() => Task.FromResult(UserSession.GetEmail()); protected async Task OnSave() { if (IsSaveDisabled) return; var user = await GetUserName(); if (string.IsNullOrWhiteSpace(user)) return; var pivotDef = SelectedPivotNode!.Pivot; if (pivotDef.IsPredefined) { var answer = await ModalDialogUtils.ShowConfirmationDialogWithInput( Modal, SavePivotDialogHeader, $"Do you want to save Pivot \"{pivotDef.Name}\" to group \"{pivotDef.Group}\"?" + "
If so, what's the magic word?" + "

Note that you can always make your own copy by pressing Save As button.

", "Magic word" ); if ( string.IsNullOrWhiteSpace(answer) ) return; var magic = Base64Encode(DateTime.Now.ToString( "yyyy-MM-dd" )); if ( answer != magic ) { await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, "Nope!" ); return; } } else { var res = await ModalDialogUtils.ShowConfirmationDialog( Modal, SavePivotDialogHeader, $"Do you want to save Pivot \"{pivotDef.Name}\" to group \"{pivotDef.Group}\"?" ); if ( res.Cancelled ) return; } // update filter pivotDef.DrilldownFilter = ""; await PivotService.UpdatePivotAsync(Collection!, pivotDef, user, new CancellationTokenSource(5000).Token ); await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, $"Pivot \"{pivotDef.Name}\" updated." ); } protected async Task OnShare() { if (Collection == null || CurrentPivot == null) { await ModalDialogUtils.ShowInfoDialog(Modal, SharePivotDialogHeader, "Please select collection and pivot to share."); return; } var def = new SharedPivotDef { PivotDef = CurrentPivot!, Collection = Collection!, ExtraFilter = GetExtraFilter()?.ToJson(GetAllFields()) ?? "", SharedBy = await GetUserName(), SharedAtUTC = DateTime.UtcNow }; await SharePivot(def); } protected async Task OnDelete() { if (IsDeleteDisabled) return; var myself = await GetUserName(); if (string.IsNullOrWhiteSpace(myself)) return; var pivotDef = SelectedPivotNode!.Pivot; var user = SelectedPivotNode.Pivot.Owner; if (!user.Equals(myself, StringComparison.OrdinalIgnoreCase)) { await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, $"Pivot \"{pivotDef.Name}\" can only be deleted by user \"{user}\"." ); return; } var res = await ModalDialogUtils.ShowConfirmationDialog( Modal, SavePivotDialogHeader, $"Do you want to delete Pivot \"{pivotDef.Name}\" from group \"{pivotDef.Group}\" for user \"{user}\"?" ); if ( res.Cancelled ) return; await PivotService.DeletePivotAsync(Collection!, pivotDef.Name, pivotDef.Group, user, new CancellationTokenSource(5000).Token ); Pivot = SelectedCollectionNode!.Pivots.FirstOrDefault(x => x is { IsGroup: false, Pivot.Name: "Summary" })?.Text ?? SelectedCollectionNode.Pivots.FirstOrDefault(x => !x.IsGroup)?.Text ; await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, $"Pivot \"{pivotDef.Name}\" deleted." ); } protected async Task OnSaveAs() { if (IsSaveDisabled) return; var user = await GetUserName(); if (string.IsNullOrWhiteSpace(user)) return; var pivotDef = SelectedPivotNode!.Pivot; var groups = new[] {PivotDefinition.UserPivotsGroup} .Concat(SelectedCollectionNode!.Pivots .Where(x => x is { IsGroup: false, Pivot.IsPredefined: true }) .Select(x => x.Pivot.Group) .Distinct() ) .ToArray() ; var res = await PivotSaveAsComponent.ShowDialog(Modal, pivotDef, groups); if (res == null) return; var (name, group, answer) = res; if (string.IsNullOrWhiteSpace(name)) return; var needMagic = group != PivotDefinition.UserPivotsGroup; var magic = Base64Encode(DateTime.Now.ToString( "yyyy-MM-dd" )); if ( needMagic && answer != magic) { await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, "Nope!" ); return; } pivotDef = SelectedPivotNode.Pivot.Clone(); pivotDef.Name = name; pivotDef.Group = group; pivotDef.Owner = user; // update filter pivotDef.DrilldownFilter = ""; await PivotService.UpdatePivotAsync(Collection!, pivotDef, user, new CancellationTokenSource(5000).Token); Pivot = SelectedCollectionNode.Pivots.FirstOrDefault(x => !x.IsGroup && x.Pivot.Group == pivotDef.Group && x.Pivot.Name == pivotDef.Name)?.Text ?? SelectedCollectionNode.Pivots.FirstOrDefault(x => !x.IsGroup)?.Text ; await ModalDialogUtils.ShowInfoDialog( Modal, SavePivotDialogHeader, $"Pivot \"{pivotDef.Name}\" (group \"{pivotDef.Group}\") updated." ); } private Task SharePivot(SharedPivotDef data) => ModalDialogUtils.SafeCall(Modal, "Share Pivot", () => SharePivotUnsafe(data)); private ValueTask GetCurrentUrlViaJs() => Js.InvokeAsync( "eval", "window.location.href"); private async Task SharePivotUnsafe(SharedPivotDef data) { var json = JsonConvert.SerializeObject(data, Formatting.None); var bytes = Encoding.UTF8.GetBytes(json); await using var mem = new MemoryStream(); mem.Write(bytes); mem.Position = 0; var guid = await PivotSharingService.SharePivot(data); var url = await MakeSharedUrl(guid); await ModalDialogUtils.ShowInfoDialog( Modal, SharePivotDialogHeader, "

This function allows you to share the exact pivot you've just ran even if you made any modification to it.

" + $"

Pivot \"{data.PivotDef.Name}\" with all modifications applied will be available using this URL:
{url}.

" + "

Please copy this URL and send it using instant messaging or by E-Mail." + "This URL will be valid for 7 days.

" ); } private async Task MakeSharedUrl(string guid) { var url = await GetCurrentUrlViaJs(); var idx = url.IndexOf('?'); if (idx >= 0) url = url[..idx]; url += $"?shared={guid}"; return url; } public Task NavigateToSharedPivot(string guidStr) => ModalDialogUtils.SafeCall(Modal, "Error running shared pivot", () => NavigateToSharedPivotUnsafe(guidStr)); private async Task NavigateToSharedPivotUnsafe(string guidStr) { var sharedPivot = await PivotSharingService.GetSharedPivot(guidStr); if (sharedPivot == null) { await InvokeAsync(StateHasChanged); return; } Collection = sharedPivot.Collection; var extraFilter = FilterExpressionTree.ParseJson(sharedPivot.ExtraFilter ?? "{}"); await InvokeAsync(() => PivotTable.RunPivot(sharedPivot.PivotDef, extraFilter)); } }