feat(09-03): add chart panel to StorageView with toggle and localization

- Update StorageView.xaml: DataGrid top, GridSplitter, chart panel bottom
- Add PieChart and CartesianChart with MultiDataTrigger visibility
- Add radio buttons for donut/bar chart toggle in left panel
- Create BytesLabelConverter for chart tooltip formatting
- Add stor.chart.* localization keys in EN and FR resx files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dev
2026-04-07 15:35:35 +02:00
parent 70048ddcdf
commit a8d79a8241
4 changed files with 166 additions and 36 deletions

View File

@@ -378,4 +378,10 @@
<data name="audit.noSites" xml:space="preserve"> <data name="audit.noSites" xml:space="preserve">
<value>S&#233;lectionnez au moins un site.</value> <value>S&#233;lectionnez au moins un site.</value>
</data> </data>
<!-- Phase 9: Storage Visualization Charts -->
<data name="stor.chart.title" xml:space="preserve"><value>Stockage par type de fichier</value></data>
<data name="stor.chart.donut" xml:space="preserve"><value>Graphique en anneau</value></data>
<data name="stor.chart.bar" xml:space="preserve"><value>Graphique en barres</value></data>
<data name="stor.chart.toggle" xml:space="preserve"><value>Type de graphique :</value></data>
<data name="stor.chart.nodata" xml:space="preserve"><value>Ex&#233;cutez une analyse pour voir la r&#233;partition par type de fichier.</value></data>
</root> </root>

View File

@@ -378,4 +378,10 @@
<data name="audit.noSites" xml:space="preserve"> <data name="audit.noSites" xml:space="preserve">
<value>Select at least one site to scan.</value> <value>Select at least one site to scan.</value>
</data> </data>
<!-- Phase 9: Storage Visualization Charts -->
<data name="stor.chart.title" xml:space="preserve"><value>Storage by File Type</value></data>
<data name="stor.chart.donut" xml:space="preserve"><value>Donut Chart</value></data>
<data name="stor.chart.bar" xml:space="preserve"><value>Bar Chart</value></data>
<data name="stor.chart.toggle" xml:space="preserve"><value>Chart View:</value></data>
<data name="stor.chart.nodata" xml:space="preserve"><value>Run a storage scan to see file type breakdown.</value></data>
</root> </root>

View File

@@ -0,0 +1,23 @@
using System.Globalization;
using System.Windows.Data;
namespace SharepointToolbox.Views.Converters;
/// <summary>
/// Converts a long byte value to a human-readable label for chart axes and tooltips.
/// Similar to BytesConverter but implements IValueConverter for XAML binding.
/// </summary>
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();
}

View File

@@ -2,7 +2,8 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:loc="clr-namespace:SharepointToolbox.Localization" xmlns:loc="clr-namespace:SharepointToolbox.Localization"
xmlns:conv="clr-namespace:SharepointToolbox.Views.Converters"> xmlns:conv="clr-namespace:SharepointToolbox.Views.Converters"
xmlns:lvc="clr-namespace:LiveChartsCore.SkiaSharpView.WPF;assembly=LiveChartsCore.SkiaSharpView.WPF">
<DockPanel LastChildFill="True"> <DockPanel LastChildFill="True">
<!-- Options panel --> <!-- Options panel -->
<ScrollViewer DockPanel.Dock="Left" Width="240" VerticalScrollBarVisibility="Auto" <ScrollViewer DockPanel.Dock="Left" Width="240" VerticalScrollBarVisibility="Auto"
@@ -59,46 +60,140 @@
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<!-- Chart view toggle -->
<GroupBox Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.toggle]}"
Margin="0,0,0,8">
<StackPanel Margin="4">
<RadioButton Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.donut]}"
IsChecked="{Binding IsDonutChart}" Margin="0,2" />
<RadioButton Content="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.bar]}"
IsChecked="{Binding IsDonutChart, Converter={StaticResource InverseBoolConverter}}"
Margin="0,2" />
</StackPanel>
</GroupBox>
<!-- Status --> <!-- Status -->
<TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap" <TextBlock Text="{Binding StatusMessage}" TextWrapping="Wrap"
FontSize="11" Foreground="#555" Margin="0,4" /> FontSize="11" Foreground="#555" Margin="0,4" />
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
<!-- Results DataGrid --> <!-- Right content area: DataGrid on top, Chart on bottom -->
<DataGrid x:Name="ResultsGrid" <Grid Margin="4,8,8,8">
ItemsSource="{Binding Results}" <Grid.RowDefinitions>
IsReadOnly="True" <RowDefinition Height="*" MinHeight="150" />
AutoGenerateColumns="False" <RowDefinition Height="Auto" />
VirtualizingPanel.IsVirtualizing="True" <RowDefinition Height="300" MinHeight="200" />
VirtualizingPanel.VirtualizationMode="Recycling" </Grid.RowDefinitions>
Margin="4,8,8,8">
<DataGrid.Columns> <!-- Results DataGrid -->
<DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.library]}" <DataGrid x:Name="ResultsGrid"
Width="*" MinWidth="160"> Grid.Row="0"
<DataGridTemplateColumn.CellTemplate> ItemsSource="{Binding Results}"
<DataTemplate> IsReadOnly="True"
<TextBlock Text="{Binding Name}" AutoGenerateColumns="False"
Margin="{Binding IndentLevel, Converter={StaticResource IndentConverter}}" VirtualizingPanel.IsVirtualizing="True"
VerticalAlignment="Center" /> VirtualizingPanel.VirtualizationMode="Recycling">
</DataTemplate> <DataGrid.Columns>
</DataGridTemplateColumn.CellTemplate> <DataGridTemplateColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.library]}"
</DataGridTemplateColumn> Width="*" MinWidth="160">
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.site]}" <DataGridTemplateColumn.CellTemplate>
Binding="{Binding SiteTitle}" Width="140" /> <DataTemplate>
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.files]}" <TextBlock Text="{Binding Name}"
Binding="{Binding TotalFileCount, StringFormat=N0}" Margin="{Binding IndentLevel, Converter={StaticResource IndentConverter}}"
Width="70" ElementStyle="{StaticResource RightAlignStyle}" /> VerticalAlignment="Center" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.size]}" </DataTemplate>
Binding="{Binding TotalSizeBytes, Converter={StaticResource BytesConverter}}" </DataGridTemplateColumn.CellTemplate>
Width="100" ElementStyle="{StaticResource RightAlignStyle}" /> </DataGridTemplateColumn>
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.versions]}" <DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.site]}"
Binding="{Binding VersionSizeBytes, Converter={StaticResource BytesConverter}}" Binding="{Binding SiteTitle}" Width="140" />
Width="110" ElementStyle="{StaticResource RightAlignStyle}" /> <DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.files]}"
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.lastmod]}" Binding="{Binding TotalFileCount, StringFormat=N0}"
Binding="{Binding LastModified, StringFormat=yyyy-MM-dd}" Width="70" ElementStyle="{StaticResource RightAlignStyle}" />
Width="110" /> <DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.size]}"
</DataGrid.Columns> Binding="{Binding TotalSizeBytes, Converter={StaticResource BytesConverter}}"
</DataGrid> Width="100" ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.versions]}"
Binding="{Binding VersionSizeBytes, Converter={StaticResource BytesConverter}}"
Width="110" ElementStyle="{StaticResource RightAlignStyle}" />
<DataGridTextColumn Header="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.col.lastmod]}"
Binding="{Binding LastModified, StringFormat=yyyy-MM-dd}"
Width="110" />
</DataGrid.Columns>
</DataGrid>
<!-- Splitter between DataGrid and Chart -->
<GridSplitter Grid.Row="1" Height="5" HorizontalAlignment="Stretch"
Background="#DDD" ResizeDirection="Rows" />
<!-- Chart panel -->
<Border Grid.Row="2" BorderBrush="#DDD" BorderThickness="1" CornerRadius="4"
Padding="8" Background="White">
<Grid>
<!-- Chart title -->
<TextBlock Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.title]}"
FontWeight="SemiBold" FontSize="14" VerticalAlignment="Top"
HorizontalAlignment="Left" Margin="4,0,0,0" />
<!-- No data placeholder -->
<TextBlock HorizontalAlignment="Center" VerticalAlignment="Center"
Foreground="#888" FontSize="12"
Text="{Binding Source={x:Static loc:TranslationSource.Instance}, Path=[stor.chart.nodata]}">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<DataTrigger Binding="{Binding HasChartData}" Value="False">
<Setter Property="Visibility" Value="Visible" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<!-- Pie/Donut chart wrapper (visible when IsDonutChart=true AND HasChartData=true) -->
<Grid Margin="4,24,4,4">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsDonutChart}" Value="True" />
<Condition Binding="{Binding HasChartData}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<lvc:PieChart Series="{Binding PieChartSeries}"
LegendPosition="Right" />
</Grid>
<!-- Bar chart wrapper (visible when IsDonutChart=false AND HasChartData=true) -->
<Grid Margin="4,24,4,4">
<Grid.Style>
<Style TargetType="Grid">
<Setter Property="Visibility" Value="Collapsed" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsDonutChart}" Value="False" />
<Condition Binding="{Binding HasChartData}" Value="True" />
</MultiDataTrigger.Conditions>
<Setter Property="Visibility" Value="Visible" />
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
<lvc:CartesianChart Series="{Binding BarChartSeries}"
XAxes="{Binding BarXAxes}"
YAxes="{Binding BarYAxes}"
LegendPosition="Hidden" />
</Grid>
</Grid>
</Border>
</Grid>
</DockPanel> </DockPanel>
</UserControl> </UserControl>