From a63a69828270bbd2551ea6dc0c9dbf17718e8261 Mon Sep 17 00:00:00 2001 From: Dev Date: Tue, 7 Apr 2026 15:16:16 +0200 Subject: [PATCH] =?UTF-8?q?docs(09-storage-visualization):=20create=20phas?= =?UTF-8?q?e=20plan=20=E2=80=94=204=20plans=20in=204=20waves?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wave 1: LiveCharts2 NuGet + FileTypeMetric model + IStorageService extension Wave 2: StorageService file-type enumeration implementation Wave 3: ViewModel chart properties + View XAML + localization Wave 4: Unit tests for chart ViewModel behavior Co-Authored-By: Claude Opus 4.6 (1M context) --- .planning/ROADMAP.md | 15 +- .../09-storage-visualization/09-01-PLAN.md | 209 ++++++ .../09-storage-visualization/09-02-PLAN.md | 242 +++++++ .../09-storage-visualization/09-03-PLAN.md | 634 ++++++++++++++++++ .../09-storage-visualization/09-04-PLAN.md | 195 ++++++ 5 files changed, 1290 insertions(+), 5 deletions(-) create mode 100644 .planning/phases/09-storage-visualization/09-01-PLAN.md create mode 100644 .planning/phases/09-storage-visualization/09-02-PLAN.md create mode 100644 .planning/phases/09-storage-visualization/09-03-PLAN.md create mode 100644 .planning/phases/09-storage-visualization/09-04-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 321558d..d7ef013 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -93,7 +93,12 @@ Plans: 2. After a storage scan completes, a chart appears in the Storage Metrics tab showing space broken down by file type 3. A toggle control switches the chart between pie/donut and bar chart representations without re-running the scan 4. The chart updates automatically whenever a new storage scan finishes, without requiring manual refresh -**Plans**: TBD +**Plans:** 4 plans +Plans: +- [ ] 09-01-PLAN.md — LiveCharts2 NuGet + FileTypeMetric model + IStorageService extension (Wave 1) +- [ ] 09-02-PLAN.md — StorageService file-type enumeration implementation (Wave 2) +- [ ] 09-03-PLAN.md — ViewModel chart properties + View XAML + localization (Wave 3) +- [ ] 09-04-PLAN.md — Unit tests for chart ViewModel behavior (Wave 4) ## Progress @@ -104,7 +109,7 @@ Plans: | 3. Storage and File Operations | v1.0 | 8/8 | Complete | 2026-04-02 | | 4. Bulk Operations and Provisioning | v1.0 | 10/10 | Complete | 2026-04-03 | | 5. Distribution and Hardening | v1.0 | 3/3 | Complete | 2026-04-03 | -| 6. Global Site Selection | 5/5 | Complete | 2026-04-07 | - | -| 7. User Access Audit | 10/10 | Complete | 2026-04-07 | - | -| 8. Simplified Permissions | 6/6 | Complete | 2026-04-07 | - | -| 9. Storage Visualization | v1.1 | 0/? | Not started | - | +| 6. Global Site Selection | v1.1 | 5/5 | Complete | 2026-04-07 | +| 7. User Access Audit | v1.1 | 10/10 | Complete | 2026-04-07 | +| 8. Simplified Permissions | v1.1 | 6/6 | Complete | 2026-04-07 | +| 9. Storage Visualization | v1.1 | 0/4 | Not started | - | diff --git a/.planning/phases/09-storage-visualization/09-01-PLAN.md b/.planning/phases/09-storage-visualization/09-01-PLAN.md new file mode 100644 index 0000000..14d5a77 --- /dev/null +++ b/.planning/phases/09-storage-visualization/09-01-PLAN.md @@ -0,0 +1,209 @@ +--- +phase: 09-storage-visualization +plan: 01 +type: execute +wave: 1 +depends_on: [] +files_modified: + - SharepointToolbox/SharepointToolbox.csproj + - SharepointToolbox/Core/Models/FileTypeMetric.cs + - SharepointToolbox/Services/IStorageService.cs +autonomous: true +requirements: + - VIZZ-01 +must_haves: + truths: + - "LiveCharts2 SkiaSharp WPF package is a NuGet dependency and the project compiles" + - "FileTypeMetric record models file extension, total size, and file count" + - "IStorageService declares CollectFileTypeMetricsAsync without breaking existing CollectStorageAsync" + artifacts: + - path: "SharepointToolbox/SharepointToolbox.csproj" + provides: "LiveChartsCore.SkiaSharpView.WPF package reference" + contains: "LiveChartsCore.SkiaSharpView.WPF" + - path: "SharepointToolbox/Core/Models/FileTypeMetric.cs" + provides: "Data model for file type breakdown" + contains: "record FileTypeMetric" + - path: "SharepointToolbox/Services/IStorageService.cs" + provides: "Extended interface with file type metrics method" + contains: "CollectFileTypeMetricsAsync" + key_links: + - from: "SharepointToolbox/Services/IStorageService.cs" + to: "SharepointToolbox/Core/Models/FileTypeMetric.cs" + via: "Return type of CollectFileTypeMetricsAsync" + pattern: "IReadOnlyList" +--- + + +Add LiveCharts2 NuGet dependency, create the FileTypeMetric data model, and extend IStorageService with a file-type metrics collection method signature. + +Purpose: Establishes the charting library dependency (VIZZ-01) and the data contracts that all subsequent plans depend on. No implementation yet -- just the NuGet, the model, and the interface. +Output: Updated csproj, FileTypeMetric.cs, updated IStorageService.cs + + + +@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/dev/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md + + + +From SharepointToolbox/Services/IStorageService.cs: +```csharp +using Microsoft.SharePoint.Client; +using SharepointToolbox.Core.Models; + +namespace SharepointToolbox.Services; + +public interface IStorageService +{ + Task> CollectStorageAsync( + ClientContext ctx, + StorageScanOptions options, + IProgress progress, + CancellationToken ct); +} +``` + +From SharepointToolbox/Core/Models/StorageScanOptions.cs: +```csharp +public record StorageScanOptions(bool PerLibrary = true, bool IncludeSubsites = false, int FolderDepth = 0); +``` + +From SharepointToolbox/Core/Models/OperationProgress.cs: +```csharp +public record OperationProgress(int Current, int Total, string Message) +{ + public static OperationProgress Indeterminate(string message) => new(0, 0, message); +} +``` + + + + + + + Task 1: Add LiveCharts2 NuGet and create FileTypeMetric model + SharepointToolbox/SharepointToolbox.csproj, SharepointToolbox/Core/Models/FileTypeMetric.cs + + **Step 1:** Add LiveCharts2 WPF NuGet package: + + ```bash + cd "C:\Users\dev\Documents\projets\Sharepoint" + dotnet add SharepointToolbox/SharepointToolbox.csproj package LiveChartsCore.SkiaSharpView.WPF --version 2.0.0-rc5.4 + ``` + + This will add the package reference to the csproj. The `--version 2.0.0-rc5.4` is a pre-release RC, so the command may need `--prerelease` flag if it fails. Try with explicit version first; if that fails, use: + ```bash + dotnet add SharepointToolbox/SharepointToolbox.csproj package LiveChartsCore.SkiaSharpView.WPF --prerelease + ``` + + **Step 2:** Create `SharepointToolbox/Core/Models/FileTypeMetric.cs`: + + ```csharp + namespace SharepointToolbox.Core.Models; + + /// + /// Represents storage consumption for a single file extension across all scanned libraries. + /// Produced by IStorageService.CollectFileTypeMetricsAsync and consumed by chart bindings. + /// + public record FileTypeMetric( + /// File extension including dot, e.g. ".docx", ".pdf". Empty string for extensionless files. + string Extension, + /// Total size in bytes of all files with this extension. + long TotalSizeBytes, + /// Number of files with this extension. + int FileCount) + { + /// + /// Human-friendly display label: ".docx" becomes "DOCX", empty becomes "No Extension". + /// + public string DisplayLabel => string.IsNullOrEmpty(Extension) + ? "No Extension" + : Extension.TrimStart('.').ToUpperInvariant(); + } + ``` + + Design notes: + - Record type for value semantics (same as StorageScanOptions, PermissionSummary patterns) + - Extension stored with dot prefix for consistency with Path.GetExtension + - DisplayLabel computed property for chart label binding + - TotalSizeBytes is long to match StorageNode.TotalSizeBytes type + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 + + LiveChartsCore.SkiaSharpView.WPF appears in csproj PackageReference. FileTypeMetric.cs exists with Extension, TotalSizeBytes, FileCount properties and DisplayLabel computed property. Project compiles with 0 errors. + + + + Task 2: Extend IStorageService with CollectFileTypeMetricsAsync + SharepointToolbox/Services/IStorageService.cs + + Update `SharepointToolbox/Services/IStorageService.cs` to add a second method for file-type metrics collection. Do NOT modify the existing CollectStorageAsync signature. + + Replace the file contents with: + + ```csharp + using Microsoft.SharePoint.Client; + using SharepointToolbox.Core.Models; + + namespace SharepointToolbox.Services; + + public interface IStorageService + { + /// + /// Collects storage metrics per library/folder using SharePoint StorageMetrics API. + /// Returns a tree of StorageNode objects with aggregate size data. + /// + Task> CollectStorageAsync( + ClientContext ctx, + StorageScanOptions options, + IProgress progress, + CancellationToken ct); + + /// + /// Enumerates files across all non-hidden document libraries in the site + /// and aggregates storage consumption grouped by file extension. + /// Uses CSOM CamlQuery to retrieve FileLeafRef and File_x0020_Size per file. + /// This is a separate operation from CollectStorageAsync -- it provides + /// file-type breakdown data for chart visualization. + /// + Task> CollectFileTypeMetricsAsync( + ClientContext ctx, + IProgress progress, + CancellationToken ct); + } + ``` + + Design notes: + - CollectFileTypeMetricsAsync does NOT take StorageScanOptions because file-type enumeration scans ALL non-hidden doc libraries (no per-library/subfolder filtering needed for chart aggregation) + - Returns IReadOnlyList sorted by TotalSizeBytes descending (convention -- implementation will handle sorting) + - Separate from CollectStorageAsync so existing storage scan flow is untouched + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 + + IStorageService.cs declares both CollectStorageAsync (unchanged) and CollectFileTypeMetricsAsync (new). Build fails with CS0535 in StorageService.cs (expected -- Plan 09-02 implements the method). If build succeeds, even better. Interface contract is established. + + + + + +- `dotnet restore SharepointToolbox/SharepointToolbox.csproj` succeeds and LiveChartsCore.SkiaSharpView.WPF is resolved +- FileTypeMetric.cs exists in Core/Models with record definition +- IStorageService.cs has both method signatures +- Existing CollectStorageAsync signature is byte-identical to original + + + +LiveCharts2 is a project dependency. FileTypeMetric data model is defined. IStorageService has the new CollectFileTypeMetricsAsync method signature. The project compiles (or fails only because StorageService doesn't implement the new method yet -- that is acceptable and expected). + + + +After completion, create `.planning/phases/09-storage-visualization/09-01-SUMMARY.md` + diff --git a/.planning/phases/09-storage-visualization/09-02-PLAN.md b/.planning/phases/09-storage-visualization/09-02-PLAN.md new file mode 100644 index 0000000..27add06 --- /dev/null +++ b/.planning/phases/09-storage-visualization/09-02-PLAN.md @@ -0,0 +1,242 @@ +--- +phase: 09-storage-visualization +plan: 02 +type: execute +wave: 2 +depends_on: + - "09-01" +files_modified: + - SharepointToolbox/Services/StorageService.cs +autonomous: true +requirements: + - VIZZ-02 +must_haves: + truths: + - "CollectFileTypeMetricsAsync enumerates files from all non-hidden document libraries" + - "Files are grouped by extension with summed size and count" + - "Results are sorted by TotalSizeBytes descending" + - "Existing CollectStorageAsync method is not modified" + artifacts: + - path: "SharepointToolbox/Services/StorageService.cs" + provides: "Implementation of CollectFileTypeMetricsAsync" + contains: "CollectFileTypeMetricsAsync" + key_links: + - from: "SharepointToolbox/Services/StorageService.cs" + to: "SharepointToolbox/Core/Models/FileTypeMetric.cs" + via: "Groups CSOM file data into FileTypeMetric records" + pattern: "new FileTypeMetric" + - from: "SharepointToolbox/Services/StorageService.cs" + to: "SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs" + via: "Throttle-safe query execution" + pattern: "ExecuteQueryRetryHelper\\.ExecuteQueryRetryAsync" +--- + + +Implement CollectFileTypeMetricsAsync in StorageService -- enumerate files across all non-hidden document libraries using CSOM CamlQuery, aggregate by file extension, and return sorted FileTypeMetric list. + +Purpose: Provides the data layer for chart visualization (VIZZ-02). The ViewModel will call this after the main storage scan completes. +Output: Updated StorageService.cs with CollectFileTypeMetricsAsync implementation + + + +@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/dev/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/09-storage-visualization/09-01-SUMMARY.md + + + +From SharepointToolbox/Core/Models/FileTypeMetric.cs: +```csharp +public record FileTypeMetric(string Extension, long TotalSizeBytes, int FileCount) +{ + public string DisplayLabel => string.IsNullOrEmpty(Extension) + ? "No Extension" + : Extension.TrimStart('.').ToUpperInvariant(); +} +``` + +From SharepointToolbox/Services/IStorageService.cs: +```csharp +public interface IStorageService +{ + Task> CollectStorageAsync( + ClientContext ctx, StorageScanOptions options, + IProgress progress, CancellationToken ct); + + Task> CollectFileTypeMetricsAsync( + ClientContext ctx, + IProgress progress, + CancellationToken ct); +} +``` + +From SharepointToolbox/Core/Helpers/ExecuteQueryRetryHelper.cs: +```csharp +public static class ExecuteQueryRetryHelper +{ + public static async Task ExecuteQueryRetryAsync( + ClientContext ctx, + IProgress? progress = null, + CancellationToken ct = default); +} +``` + + +From SharepointToolbox/Services/StorageService.cs: +```csharp +public class StorageService : IStorageService +{ + public async Task> CollectStorageAsync(...) { ... } + private static async Task LoadFolderNodeAsync(...) { ... } + private static async Task CollectSubfoldersAsync(...) { ... } +} +``` + + + + + + + Task 1: Implement CollectFileTypeMetricsAsync in StorageService + SharepointToolbox/Services/StorageService.cs + + Add the `CollectFileTypeMetricsAsync` method to the existing `StorageService` class. Do NOT modify any existing methods (CollectStorageAsync, LoadFolderNodeAsync, CollectSubfoldersAsync). Add the new method after the existing `CollectStorageAsync` method. + + Add this method to the `StorageService` class: + + ```csharp + public async Task> CollectFileTypeMetricsAsync( + ClientContext ctx, + IProgress progress, + CancellationToken ct) + { + ct.ThrowIfCancellationRequested(); + + // Load all non-hidden document libraries + ctx.Load(ctx.Web, + w => w.Lists.Include( + l => l.Title, + l => l.Hidden, + l => l.BaseType, + l => l.ItemCount)); + await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct); + + var libs = ctx.Web.Lists + .Where(l => !l.Hidden && l.BaseType == BaseType.DocumentLibrary) + .ToList(); + + // Accumulate file sizes by extension across all libraries + var extensionMap = new Dictionary(StringComparer.OrdinalIgnoreCase); + + int libIdx = 0; + foreach (var lib in libs) + { + ct.ThrowIfCancellationRequested(); + libIdx++; + progress.Report(new OperationProgress(libIdx, libs.Count, + $"Scanning files by type: {lib.Title} ({libIdx}/{libs.Count})")); + + // Use CamlQuery to enumerate all files in the library + // Paginate with 500 items per batch to avoid list view threshold issues + var query = new CamlQuery + { + ViewXml = @" + + + + + 0 + + + + + + + + 500 + " + }; + + ListItemCollection items; + do + { + ct.ThrowIfCancellationRequested(); + items = lib.GetItems(query); + ctx.Load(items, ic => ic.ListItemCollectionPosition, + ic => ic.Include( + i => i["FileLeafRef"], + i => i["File_x0020_Size"])); + await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, progress, ct); + + foreach (var item in items) + { + string fileName = item["FileLeafRef"]?.ToString() ?? string.Empty; + string sizeStr = item["File_x0020_Size"]?.ToString() ?? "0"; + + if (!long.TryParse(sizeStr, out long fileSize)) + fileSize = 0; + + string ext = Path.GetExtension(fileName).ToLowerInvariant(); + // ext is "" for extensionless files, ".docx" etc. for others + + if (extensionMap.TryGetValue(ext, out var existing)) + extensionMap[ext] = (existing.totalSize + fileSize, existing.count + 1); + else + extensionMap[ext] = (fileSize, 1); + } + + // Move to next page + query.ListItemCollectionPosition = items.ListItemCollectionPosition; + } + while (items.ListItemCollectionPosition != null); + } + + // Convert to FileTypeMetric list, sorted by size descending + return extensionMap + .Select(kvp => new FileTypeMetric(kvp.Key, kvp.Value.totalSize, kvp.Value.count)) + .OrderByDescending(m => m.TotalSizeBytes) + .ToList(); + } + ``` + + Make sure to add `using System.IO;` at the top of the file if not already present (for `Path.GetExtension`). + + Design notes: + - Uses `Scope='RecursiveAll'` in CamlQuery to get files from all subfolders without explicit recursion + - `FSObjType=0` filter ensures only files (not folders) are returned + - Paged query with 500-item batches avoids list view threshold (5000 default) issues + - File_x0020_Size is the internal name for file size in SharePoint + - Extensions normalized to lowercase for consistent grouping (".DOCX" and ".docx" merge) + - Dictionary uses OrdinalIgnoreCase comparer as extra safety + - Existing methods (CollectStorageAsync, LoadFolderNodeAsync, CollectSubfoldersAsync) are NOT touched + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 + + StorageService.cs implements CollectFileTypeMetricsAsync. Method enumerates files via CamlQuery with paging, groups by extension, returns IReadOnlyList<FileTypeMetric> sorted by TotalSizeBytes descending. Existing CollectStorageAsync is unchanged. Project compiles with 0 errors. + + + + + +- `dotnet build SharepointToolbox/SharepointToolbox.csproj` succeeds with 0 errors +- StorageService now implements both IStorageService methods +- CollectFileTypeMetricsAsync uses paginated CamlQuery (RowLimit 500, Paged=TRUE) +- Extensions normalized to lowercase +- Results sorted by TotalSizeBytes descending +- No modifications to CollectStorageAsync, LoadFolderNodeAsync, or CollectSubfoldersAsync + + + +StorageService fully implements IStorageService. CollectFileTypeMetricsAsync can enumerate files by extension from any SharePoint site. The project compiles cleanly and existing storage scan behavior is unaffected. + + + +After completion, create `.planning/phases/09-storage-visualization/09-02-SUMMARY.md` + diff --git a/.planning/phases/09-storage-visualization/09-03-PLAN.md b/.planning/phases/09-storage-visualization/09-03-PLAN.md new file mode 100644 index 0000000..31cee6a --- /dev/null +++ b/.planning/phases/09-storage-visualization/09-03-PLAN.md @@ -0,0 +1,634 @@ +--- +phase: 09-storage-visualization +plan: 03 +type: execute +wave: 3 +depends_on: + - "09-01" + - "09-02" +files_modified: + - SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs + - SharepointToolbox/Views/Tabs/StorageView.xaml + - SharepointToolbox/Localization/Strings.resx + - SharepointToolbox/Localization/Strings.fr.resx + - SharepointToolbox/Views/Converters/BytesLabelConverter.cs +autonomous: true +requirements: + - VIZZ-02 + - VIZZ-03 +must_haves: + truths: + - "After a storage scan completes, a chart appears showing space broken down by file type" + - "A toggle control switches between pie/donut and bar chart views without re-running the scan" + - "The chart updates automatically when a new storage scan finishes" + - "Chart labels show file extension and human-readable size" + artifacts: + - path: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs" + provides: "FileTypeMetrics collection, IsDonutChart toggle, chart series computation" + contains: "FileTypeMetrics" + - path: "SharepointToolbox/Views/Tabs/StorageView.xaml" + provides: "Chart panel with PieChart and CartesianChart, toggle button" + contains: "lvc:PieChart" + - path: "SharepointToolbox/Views/Converters/BytesLabelConverter.cs" + provides: "Converter for chart tooltip bytes formatting" + contains: "class BytesLabelConverter" + - path: "SharepointToolbox/Localization/Strings.resx" + provides: "EN localization keys for chart UI" + contains: "stor.chart" + - path: "SharepointToolbox/Localization/Strings.fr.resx" + provides: "FR localization keys for chart UI" + contains: "stor.chart" + key_links: + - from: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs" + to: "SharepointToolbox/Services/IStorageService.cs" + via: "Calls CollectFileTypeMetricsAsync after CollectStorageAsync" + pattern: "_storageService\\.CollectFileTypeMetricsAsync" + - from: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs" + to: "SharepointToolbox/Core/Models/FileTypeMetric.cs" + via: "ObservableCollection property" + pattern: "ObservableCollection" + - from: "SharepointToolbox/Views/Tabs/StorageView.xaml" + to: "SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs" + via: "Binds PieSeries to PieChartSeries, ColumnSeries to BarChartSeries" + pattern: "Binding.*ChartSeries" +--- + + +Extend StorageViewModel with chart data properties and toggle, update StorageView.xaml with LiveCharts2 chart controls (pie/donut + bar), add localization keys, and create a bytes label converter for chart tooltips. + +Purpose: Delivers the complete UI for VIZZ-02 (chart showing file type breakdown) and VIZZ-03 (toggle between pie/donut and bar). This is the plan that makes the feature visible to users. +Output: Updated ViewModel, updated View XAML, localization keys, BytesLabelConverter + + + +@C:/Users/dev/.claude/get-shit-done/workflows/execute-plan.md +@C:/Users/dev/.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/09-storage-visualization/09-01-SUMMARY.md +@.planning/phases/09-storage-visualization/09-02-SUMMARY.md + + + +From SharepointToolbox/Core/Models/FileTypeMetric.cs: +```csharp +public record FileTypeMetric(string Extension, long TotalSizeBytes, int FileCount) +{ + public string DisplayLabel => string.IsNullOrEmpty(Extension) + ? "No Extension" + : Extension.TrimStart('.').ToUpperInvariant(); +} +``` + +From SharepointToolbox/Services/IStorageService.cs: +```csharp +public interface IStorageService +{ + Task> CollectStorageAsync( + ClientContext ctx, StorageScanOptions options, + IProgress progress, CancellationToken ct); + + Task> CollectFileTypeMetricsAsync( + ClientContext ctx, + IProgress progress, + CancellationToken ct); +} +``` + + +From SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs: +```csharp +public partial class StorageViewModel : FeatureViewModelBase +{ + // Fields: _storageService, _sessionManager, _csvExportService, _htmlExportService, _logger, _currentProfile, _hasLocalSiteOverride + // Properties: SiteUrl, PerLibrary, IncludeSubsites, FolderDepth, IsMaxDepth, Results + // Commands: RunCommand (base), CancelCommand (base), ExportCsvCommand, ExportHtmlCommand + // RunOperationAsync: calls CollectStorageAsync, flattens tree, sets Results + // Test constructor: internal StorageViewModel(IStorageService, ISessionManager, ILogger) +} +``` + + +From SharepointToolbox/Views/Tabs/StorageView.xaml: +- DockPanel with left ScrollViewer (options) and right DataGrid (results) +- Uses loc:TranslationSource.Instance for all labels +- Uses StaticResource: InverseBoolConverter, IndentConverter, BytesConverter, RightAlignStyle + + +From SharepointToolbox/Views/Converters/BytesConverter.cs: +```csharp +// IValueConverter: long bytes -> "1.23 GB" human-readable string +// Used in DataGrid column bindings +``` + + +LiveChartsCore.SkiaSharpView.WPF: +- PieChart control: Series property (IEnumerable) +- CartesianChart control: Series, XAxes, YAxes properties +- PieSeries: Values, Name, InnerRadius, DataLabelsPosition, DataLabelsFormatter +- ColumnSeries: Values, Name, DataLabelsFormatter +- Axis: Labels, LabelsRotation, Name +- SolidColorPaint: for axis/label paint + + + + + + + Task 1: Extend StorageViewModel with chart data and toggle + SharepointToolbox/ViewModels/Tabs/StorageViewModel.cs + + Add chart-related properties and logic to StorageViewModel. Read the current file first, then make these additions: + + **1. Add using statements** at the top (add to existing usings): + ```csharp + using LiveChartsCore; + using LiveChartsCore.SkiaSharpView; + using LiveChartsCore.SkiaSharpView.Painting; + using SkiaSharp; + ``` + + **2. Add new observable properties** (after the existing `_folderDepth` field): + ```csharp + [ObservableProperty] + private bool _isDonutChart = true; + + private ObservableCollection _fileTypeMetrics = new(); + public ObservableCollection FileTypeMetrics + { + get => _fileTypeMetrics; + private set + { + _fileTypeMetrics = value; + OnPropertyChanged(); + UpdateChartSeries(); + } + } + + public bool HasChartData => FileTypeMetrics.Count > 0; + ``` + + **3. Add chart series properties** (after HasChartData): + ```csharp + private IEnumerable _pieChartSeries = Enumerable.Empty(); + public IEnumerable PieChartSeries + { + get => _pieChartSeries; + private set { _pieChartSeries = value; OnPropertyChanged(); } + } + + private IEnumerable _barChartSeries = Enumerable.Empty(); + public IEnumerable BarChartSeries + { + get => _barChartSeries; + private set { _barChartSeries = value; OnPropertyChanged(); } + } + + private Axis[] _barXAxes = Array.Empty(); + public Axis[] BarXAxes + { + get => _barXAxes; + private set { _barXAxes = value; OnPropertyChanged(); } + } + + private Axis[] _barYAxes = Array.Empty(); + public Axis[] BarYAxes + { + get => _barYAxes; + private set { _barYAxes = value; OnPropertyChanged(); } + } + ``` + + **4. Add partial method** to react to IsDonutChart changes: + ```csharp + partial void OnIsDonutChartChanged(bool value) + { + UpdateChartSeries(); + } + ``` + + **5. Add UpdateChartSeries private method** (before the existing FlattenNode method): + ```csharp + private void UpdateChartSeries() + { + var metrics = FileTypeMetrics.ToList(); + OnPropertyChanged(nameof(HasChartData)); + + if (metrics.Count == 0) + { + PieChartSeries = Enumerable.Empty(); + BarChartSeries = Enumerable.Empty(); + BarXAxes = Array.Empty(); + BarYAxes = Array.Empty(); + 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(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 + { + 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 + { + Values = chartItems.Select(m => m.TotalSizeBytes).ToArray(), + Name = "Size", + DataLabelsFormatter = point => + { + int idx = (int)point.Index; + return idx < chartItems.Count ? FormatBytes(chartItems[idx].TotalSizeBytes) : ""; + }, + ToolTipLabelFormatter = point => + { + int idx = (int)point.Index; + if (idx >= chartItems.Count) return ""; + var m = chartItems[idx]; + return $"{m.DisplayLabel}: {FormatBytes(m.TotalSizeBytes)} ({m.FileCount} files)"; + } + } + }; + + 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"; + } + ``` + + **6. Update RunOperationAsync** to call CollectFileTypeMetricsAsync AFTER the existing storage scan. After the existing `Results = new ObservableCollection(flat);` block (both dispatcher and else branches), add: + + ```csharp + // 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(typeMetrics); + }); + } + else + { + FileTypeMetrics = new ObservableCollection(typeMetrics); + } + ``` + + **7. Update OnTenantSwitched** to clear chart data. Add after `Results = new ObservableCollection();`: + ```csharp + FileTypeMetrics = new ObservableCollection(); + ``` + + **Important:** The `ctx` variable used by the new CollectFileTypeMetricsAsync call is the same `ctx` already obtained earlier in RunOperationAsync. The call goes after the Results assignment but BEFORE the method returns. + + + cd "C:\Users\dev\Documents\projets\Sharepoint" && dotnet build SharepointToolbox/SharepointToolbox.csproj --no-incremental 2>&1 | tail -5 + + StorageViewModel has IsDonutChart toggle, FileTypeMetrics collection, PieChartSeries, BarChartSeries, BarXAxes, BarYAxes properties. RunOperationAsync calls CollectFileTypeMetricsAsync after storage scan. UpdateChartSeries builds top-10 + Other aggregation. OnTenantSwitched clears chart data. Project compiles. + + + + Task 2: Update StorageView.xaml with chart panel, toggle, and localization + SharepointToolbox/Views/Tabs/StorageView.xaml, SharepointToolbox/Views/Converters/BytesLabelConverter.cs, SharepointToolbox/Localization/Strings.resx, SharepointToolbox/Localization/Strings.fr.resx + + **Step 1: Add localization keys** to `SharepointToolbox/Localization/Strings.resx`. Add these entries before the closing `` tag (follow existing `stor.*` naming convention): + + ```xml + Storage by File Type + Donut Chart + Bar Chart + Chart View: + Run a storage scan to see file type breakdown. + ``` + + Add corresponding FR translations to `SharepointToolbox/Localization/Strings.fr.resx`: + + ```xml + Stockage par type de fichier + Graphique en anneau + Graphique en barres + Type de graphique : + Exécutez une analyse pour voir la répartition par type de fichier. + ``` + + Note: Use XML entities for accented chars (`é` for e-acute) matching existing resx convention per Phase 08 decision. + + **Step 2: Create BytesLabelConverter** at `SharepointToolbox/Views/Converters/BytesLabelConverter.cs`: + + ```csharp + using System.Globalization; + using System.Windows.Data; + + namespace SharepointToolbox.Views.Converters; + + /// + /// Converts a long byte value to a human-readable label for chart axes and tooltips. + /// Similar to BytesConverter but implements IValueConverter for XAML binding. + /// + public class BytesLabelConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + if (value is not long bytes) return value?.ToString() ?? ""; + 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"; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); + } + ``` + + **Step 3: Update StorageView.xaml** to add the chart panel. Replace the entire file content with the updated layout: + + The key structural change: Replace the simple `DockPanel` with left options + right content split. The right content area becomes a `Grid` with two rows -- top row for DataGrid, bottom row for chart panel. The chart panel has a toggle and two chart controls (one visible based on IsDonutChart). + + Read the current StorageView.xaml first, then replace with: + + ```xml + + + + + + +