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:
@@ -378,4 +378,10 @@
|
|||||||
<data name="audit.noSites" xml:space="preserve">
|
<data name="audit.noSites" xml:space="preserve">
|
||||||
<value>Sélectionnez au moins un site.</value>
|
<value>Sé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écutez une analyse pour voir la répartition par type de fichier.</value></data>
|
||||||
</root>
|
</root>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
23
SharepointToolbox/Views/Converters/BytesLabelConverter.cs
Normal file
23
SharepointToolbox/Views/Converters/BytesLabelConverter.cs
Normal 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();
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user