feat(09-03): extend StorageViewModel with chart data properties and toggle
- Add IsDonutChart toggle, FileTypeMetrics collection, PieChartSeries, BarChartSeries, BarXAxes, BarYAxes - Add UpdateChartSeries method with top-10 + Other aggregation - Call CollectFileTypeMetricsAsync after storage scan in RunOperationAsync - Clear chart data on tenant switch Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,11 +3,15 @@ using System.Diagnostics;
|
|||||||
using System.Windows;
|
using System.Windows;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using LiveChartsCore;
|
||||||
|
using LiveChartsCore.SkiaSharpView;
|
||||||
|
using LiveChartsCore.SkiaSharpView.Painting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using SharepointToolbox.Core.Models;
|
using SharepointToolbox.Core.Models;
|
||||||
using SharepointToolbox.Services;
|
using SharepointToolbox.Services;
|
||||||
using SharepointToolbox.Services.Export;
|
using SharepointToolbox.Services.Export;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
namespace SharepointToolbox.ViewModels.Tabs;
|
namespace SharepointToolbox.ViewModels.Tabs;
|
||||||
|
|
||||||
@@ -33,6 +37,51 @@ public partial class StorageViewModel : FeatureViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private int _folderDepth;
|
private int _folderDepth;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isDonutChart = true;
|
||||||
|
|
||||||
|
private ObservableCollection<FileTypeMetric> _fileTypeMetrics = new();
|
||||||
|
public ObservableCollection<FileTypeMetric> FileTypeMetrics
|
||||||
|
{
|
||||||
|
get => _fileTypeMetrics;
|
||||||
|
private set
|
||||||
|
{
|
||||||
|
_fileTypeMetrics = value;
|
||||||
|
OnPropertyChanged();
|
||||||
|
UpdateChartSeries();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasChartData => FileTypeMetrics.Count > 0;
|
||||||
|
|
||||||
|
private IEnumerable<ISeries> _pieChartSeries = Enumerable.Empty<ISeries>();
|
||||||
|
public IEnumerable<ISeries> PieChartSeries
|
||||||
|
{
|
||||||
|
get => _pieChartSeries;
|
||||||
|
private set { _pieChartSeries = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<ISeries> _barChartSeries = Enumerable.Empty<ISeries>();
|
||||||
|
public IEnumerable<ISeries> BarChartSeries
|
||||||
|
{
|
||||||
|
get => _barChartSeries;
|
||||||
|
private set { _barChartSeries = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Axis[] _barXAxes = Array.Empty<Axis>();
|
||||||
|
public Axis[] BarXAxes
|
||||||
|
{
|
||||||
|
get => _barXAxes;
|
||||||
|
private set { _barXAxes = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private Axis[] _barYAxes = Array.Empty<Axis>();
|
||||||
|
public Axis[] BarYAxes
|
||||||
|
{
|
||||||
|
get => _barYAxes;
|
||||||
|
private set { _barYAxes = value; OnPropertyChanged(); }
|
||||||
|
}
|
||||||
|
|
||||||
public bool IsMaxDepth
|
public bool IsMaxDepth
|
||||||
{
|
{
|
||||||
get => FolderDepth >= 999;
|
get => FolderDepth >= 999;
|
||||||
@@ -163,6 +212,22 @@ public partial class StorageViewModel : FeatureViewModelBase
|
|||||||
{
|
{
|
||||||
Results = new ObservableCollection<StorageNode>(flat);
|
Results = new ObservableCollection<StorageNode>(flat);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Collect file-type metrics for chart visualization
|
||||||
|
progress.Report(OperationProgress.Indeterminate("Scanning file types for chart..."));
|
||||||
|
var typeMetrics = await _storageService.CollectFileTypeMetricsAsync(ctx, progress, ct);
|
||||||
|
|
||||||
|
if (Application.Current?.Dispatcher is { } chartDispatcher)
|
||||||
|
{
|
||||||
|
await chartDispatcher.InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
FileTypeMetrics = new ObservableCollection<FileTypeMetric>(typeMetrics);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FileTypeMetrics = new ObservableCollection<FileTypeMetric>(typeMetrics);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnTenantSwitched(TenantProfile profile)
|
protected override void OnTenantSwitched(TenantProfile profile)
|
||||||
@@ -170,6 +235,7 @@ public partial class StorageViewModel : FeatureViewModelBase
|
|||||||
_currentProfile = profile;
|
_currentProfile = profile;
|
||||||
_hasLocalSiteOverride = false;
|
_hasLocalSiteOverride = false;
|
||||||
Results = new ObservableCollection<StorageNode>();
|
Results = new ObservableCollection<StorageNode>();
|
||||||
|
FileTypeMetrics = new ObservableCollection<FileTypeMetric>();
|
||||||
SiteUrl = string.Empty;
|
SiteUrl = string.Empty;
|
||||||
OnPropertyChanged(nameof(CurrentProfile));
|
OnPropertyChanged(nameof(CurrentProfile));
|
||||||
ExportCsvCommand.NotifyCanExecuteChanged();
|
ExportCsvCommand.NotifyCanExecuteChanged();
|
||||||
@@ -229,6 +295,88 @@ public partial class StorageViewModel : FeatureViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnIsDonutChartChanged(bool value)
|
||||||
|
{
|
||||||
|
UpdateChartSeries();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateChartSeries()
|
||||||
|
{
|
||||||
|
var metrics = FileTypeMetrics.ToList();
|
||||||
|
OnPropertyChanged(nameof(HasChartData));
|
||||||
|
|
||||||
|
if (metrics.Count == 0)
|
||||||
|
{
|
||||||
|
PieChartSeries = Enumerable.Empty<ISeries>();
|
||||||
|
BarChartSeries = Enumerable.Empty<ISeries>();
|
||||||
|
BarXAxes = Array.Empty<Axis>();
|
||||||
|
BarYAxes = Array.Empty<Axis>();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Take top 10 by size, aggregate the rest as "Other"
|
||||||
|
var top = metrics.Take(10).ToList();
|
||||||
|
long otherSize = metrics.Skip(10).Sum(m => m.TotalSizeBytes);
|
||||||
|
int otherCount = metrics.Skip(10).Sum(m => m.FileCount);
|
||||||
|
|
||||||
|
var chartItems = new List<FileTypeMetric>(top);
|
||||||
|
if (otherSize > 0)
|
||||||
|
chartItems.Add(new FileTypeMetric("Other", otherSize, otherCount));
|
||||||
|
|
||||||
|
// Pie/Donut series
|
||||||
|
double innerRadius = IsDonutChart ? 50 : 0;
|
||||||
|
PieChartSeries = chartItems.Select(m => new PieSeries<long>
|
||||||
|
{
|
||||||
|
Values = new[] { m.TotalSizeBytes },
|
||||||
|
Name = m.DisplayLabel,
|
||||||
|
InnerRadius = innerRadius,
|
||||||
|
DataLabelsPosition = LiveChartsCore.Measure.PolarLabelsPosition.Outer,
|
||||||
|
DataLabelsFormatter = point => m.DisplayLabel,
|
||||||
|
ToolTipLabelFormatter = point =>
|
||||||
|
$"{m.DisplayLabel}: {FormatBytes(m.TotalSizeBytes)} ({m.FileCount} files)"
|
||||||
|
}).ToList();
|
||||||
|
|
||||||
|
// Bar chart series
|
||||||
|
BarChartSeries = new ISeries[]
|
||||||
|
{
|
||||||
|
new ColumnSeries<long>
|
||||||
|
{
|
||||||
|
Values = chartItems.Select(m => m.TotalSizeBytes).ToArray(),
|
||||||
|
Name = "Size",
|
||||||
|
DataLabelsFormatter = point =>
|
||||||
|
{
|
||||||
|
int idx = (int)point.Index;
|
||||||
|
return idx < chartItems.Count ? FormatBytes(chartItems[idx].TotalSizeBytes) : "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BarXAxes = new Axis[]
|
||||||
|
{
|
||||||
|
new Axis
|
||||||
|
{
|
||||||
|
Labels = chartItems.Select(m => m.DisplayLabel).ToArray(),
|
||||||
|
LabelsRotation = -45
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
BarYAxes = new Axis[]
|
||||||
|
{
|
||||||
|
new Axis
|
||||||
|
{
|
||||||
|
Labeler = value => FormatBytes((long)value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatBytes(long bytes)
|
||||||
|
{
|
||||||
|
if (bytes < 1024) return $"{bytes} B";
|
||||||
|
if (bytes < 1024 * 1024) return $"{bytes / 1024.0:F1} KB";
|
||||||
|
if (bytes < 1024 * 1024 * 1024) return $"{bytes / (1024.0 * 1024):F1} MB";
|
||||||
|
return $"{bytes / (1024.0 * 1024 * 1024):F2} GB";
|
||||||
|
}
|
||||||
|
|
||||||
private static void FlattenNode(StorageNode node, int level, List<StorageNode> result)
|
private static void FlattenNode(StorageNode node, int level, List<StorageNode> result)
|
||||||
{
|
{
|
||||||
node.IndentLevel = level;
|
node.IndentLevel = level;
|
||||||
|
|||||||
Reference in New Issue
Block a user