feat: themed + iconed UI polish (v0.3.1)
Fluent light/dark theme dictionaries (accent/surface/card/border/danger), OS-following variant, App-level text/button/card styles. Per-protocol vector icons via ProtocolIdToIconConverter (mirrors StatusKindToBrushConverter, view-layer, safe fallback, no external assets). Curated layout across dashboard/detail/config: consistent spacing scale, card grouping, theme brushes, accent/danger buttons. App-only; no VM contract change, no new NuGet, no fw bump. Projects: Junction.App (+ Junction.Tests: +10 converter tests, 148 pass). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
53ce8ab4da
commit
f4abdc0039
8 changed files with 403 additions and 142 deletions
|
|
@ -2,15 +2,113 @@
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:local="clr-namespace:Junction.App"
|
xmlns:local="clr-namespace:Junction.App"
|
||||||
xmlns:conv="clr-namespace:Junction.App.Converters"
|
xmlns:conv="clr-namespace:Junction.App.Converters"
|
||||||
x:Class="Junction.App.App">
|
x:Class="Junction.App.App"
|
||||||
|
RequestedThemeVariant="Default">
|
||||||
|
<!-- RequestedThemeVariant="Default" = follow the OS light/dark preference. -->
|
||||||
|
|
||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
|
<ResourceDictionary>
|
||||||
|
<!-- Per-variant palette. Fluent supplies the base chrome; these are Junction's
|
||||||
|
accent + surface + status tokens, adapted for Light and Dark. -->
|
||||||
|
<ResourceDictionary.ThemeDictionaries>
|
||||||
|
<ResourceDictionary x:Key="Light">
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#2563EB" />
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#1D4FD7" />
|
||||||
|
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#F4F6F9" />
|
||||||
|
<SolidColorBrush x:Key="CardBackgroundBrush" Color="#FFFFFF" />
|
||||||
|
<SolidColorBrush x:Key="CardBorderBrush" Color="#E2E6EC" />
|
||||||
|
<SolidColorBrush x:Key="HeaderForegroundBrush" Color="#161A1F" />
|
||||||
|
<SolidColorBrush x:Key="SubtleForegroundBrush" Color="#697586" />
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#C62828" />
|
||||||
|
<SolidColorBrush x:Key="OnAccentBrush" Color="#FFFFFF" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
<ResourceDictionary x:Key="Dark">
|
||||||
|
<SolidColorBrush x:Key="AccentBrush" Color="#4C8DF6" />
|
||||||
|
<SolidColorBrush x:Key="AccentHoverBrush" Color="#6BA1F8" />
|
||||||
|
<SolidColorBrush x:Key="AppBackgroundBrush" Color="#1B1E23" />
|
||||||
|
<SolidColorBrush x:Key="CardBackgroundBrush" Color="#24282E" />
|
||||||
|
<SolidColorBrush x:Key="CardBorderBrush" Color="#3A3F46" />
|
||||||
|
<SolidColorBrush x:Key="HeaderForegroundBrush" Color="#F2F4F7" />
|
||||||
|
<SolidColorBrush x:Key="SubtleForegroundBrush" Color="#9AA3AF" />
|
||||||
|
<SolidColorBrush x:Key="DangerBrush" Color="#EF5350" />
|
||||||
|
<SolidColorBrush x:Key="OnAccentBrush" Color="#FFFFFF" />
|
||||||
|
</ResourceDictionary>
|
||||||
|
</ResourceDictionary.ThemeDictionaries>
|
||||||
|
|
||||||
|
<!-- Spacing scale (variant-invariant). -->
|
||||||
|
<Thickness x:Key="PageMargin">20</Thickness>
|
||||||
|
|
||||||
|
<!-- Variant-invariant converters. -->
|
||||||
<conv:StatusKindToBrushConverter x:Key="StatusKindToBrush" />
|
<conv:StatusKindToBrushConverter x:Key="StatusKindToBrush" />
|
||||||
|
<conv:ProtocolIdToIconConverter x:Key="ProtocolIdToIcon" />
|
||||||
|
</ResourceDictionary>
|
||||||
</Application.Resources>
|
</Application.Resources>
|
||||||
|
|
||||||
<Application.DataTemplates>
|
<Application.DataTemplates>
|
||||||
<local:ViewLocator />
|
<local:ViewLocator />
|
||||||
</Application.DataTemplates>
|
</Application.DataTemplates>
|
||||||
|
|
||||||
<Application.Styles>
|
<Application.Styles>
|
||||||
<FluentTheme />
|
<FluentTheme />
|
||||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml" />
|
||||||
|
|
||||||
|
<!-- Inter (bundled via WithInterFont) as the shell default for a consistent, crisp face. -->
|
||||||
|
<Style Selector="Window">
|
||||||
|
<Setter Property="FontFamily" Value="avares://Avalonia.Fonts.Inter/Assets#Inter" />
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AppBackgroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Reusable text roles. -->
|
||||||
|
<Style Selector="TextBlock.h1">
|
||||||
|
<Setter Property="FontSize" Value="22" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource HeaderForegroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.h2">
|
||||||
|
<Setter Property="FontSize" Value="16" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource HeaderForegroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.subtle">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SubtleForegroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.fieldLabel">
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource SubtleForegroundBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Card / section container. -->
|
||||||
|
<Style Selector="Border.card">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource CardBackgroundBrush}" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource CardBorderBrush}" />
|
||||||
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="Padding" Value="16" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Primary (accent) button. -->
|
||||||
|
<Style Selector="Button.accent">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.accent:pointerover /template/ ContentPresenter">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentHoverBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource OnAccentBrush}" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Destructive button. -->
|
||||||
|
<Style Selector="Button.danger">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource DangerBrush}" />
|
||||||
|
<Setter Property="Foreground" Value="#FFFFFF" />
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<!-- Protocol glyph. -->
|
||||||
|
<Style Selector="PathIcon.protocol">
|
||||||
|
<Setter Property="Width" Value="16" />
|
||||||
|
<Setter Property="Height" Value="16" />
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentBrush}" />
|
||||||
|
</Style>
|
||||||
</Application.Styles>
|
</Application.Styles>
|
||||||
</Application>
|
</Application>
|
||||||
|
|
|
||||||
75
src/Junction.App/Converters/ProtocolIdToIconConverter.cs
Normal file
75
src/Junction.App/Converters/ProtocolIdToIconConverter.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Avalonia.Media;
|
||||||
|
|
||||||
|
namespace Junction.App.Converters
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maps a machine's <c>ProtocolId</c> ("mtconnect", future "opcua"/"fanuc", …) to a vector
|
||||||
|
/// glyph (<see cref="Geometry"/>) for a <c>PathIcon</c>. Keeps view-models framework-agnostic:
|
||||||
|
/// the icon decision lives entirely in the view layer, mirroring
|
||||||
|
/// <see cref="StatusKindToBrushConverter"/>. Unknown/empty ids fall back to a generic glyph.
|
||||||
|
/// <para>Glyphs are line-only 24×24 path data (no arcs/curves) so they always parse and read
|
||||||
|
/// as distinct filled shapes. No external image assets — net48 packaging stays a single exe set.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed class ProtocolIdToIconConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public static readonly ProtocolIdToIconConverter Instance = new ProtocolIdToIconConverter();
|
||||||
|
|
||||||
|
// Signal-strength bars — a live telemetry feed.
|
||||||
|
private const string MtconnectData = "M4,14 H7 V20 H4 Z M10,10 H13 V20 H10 Z M16,5 H19 V20 H16 Z";
|
||||||
|
|
||||||
|
// Stacked diamonds — the OPC UA layered address space.
|
||||||
|
private const string OpcuaData = "M12,2 L20,7 L12,12 L4,7 Z M12,13 L20,18 L12,23 L4,18 Z";
|
||||||
|
|
||||||
|
// Plus/cross — an industrial CNC controller marker.
|
||||||
|
private const string FanucData = "M10,3 H14 V10 H21 V14 H14 V21 H10 V14 H3 V10 H10 Z";
|
||||||
|
|
||||||
|
// Octagon node — generic / unknown protocol.
|
||||||
|
private const string FallbackData = "M8,4 H16 L20,8 V16 L16,20 H8 L4,16 V8 Z";
|
||||||
|
|
||||||
|
private static Geometry? _mtconnect;
|
||||||
|
private static Geometry? _opcua;
|
||||||
|
private static Geometry? _fanuc;
|
||||||
|
private static Geometry? _fallback;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Pure lookup of the raw path-data string for a protocol id. View-framework-free and
|
||||||
|
/// side-effect-free so it is unit-testable without an Avalonia platform. Known ids return a
|
||||||
|
/// distinct non-empty string; anything else returns the generic fallback. Never throws.
|
||||||
|
/// </summary>
|
||||||
|
public static string PathDataFor(string? protocolId)
|
||||||
|
{
|
||||||
|
switch (protocolId == null ? null : protocolId.Trim().ToLowerInvariant())
|
||||||
|
{
|
||||||
|
case "mtconnect":
|
||||||
|
return MtconnectData;
|
||||||
|
case "opcua":
|
||||||
|
return OpcuaData;
|
||||||
|
case "fanuc":
|
||||||
|
return FanucData;
|
||||||
|
default:
|
||||||
|
return FallbackData;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
switch (PathDataFor(value as string))
|
||||||
|
{
|
||||||
|
case MtconnectData:
|
||||||
|
return _mtconnect ?? (_mtconnect = Geometry.Parse(MtconnectData));
|
||||||
|
case OpcuaData:
|
||||||
|
return _opcua ?? (_opcua = Geometry.Parse(OpcuaData));
|
||||||
|
case FanucData:
|
||||||
|
return _fanuc ?? (_fanuc = Geometry.Parse(FanucData));
|
||||||
|
default:
|
||||||
|
return _fallback ?? (_fallback = Geometry.Parse(FallbackData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public object ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
=> throw new NotSupportedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
x:DataType="vm:MainWindowViewModel"
|
x:DataType="vm:MainWindowViewModel"
|
||||||
x:CompileBindings="True"
|
x:CompileBindings="True"
|
||||||
Title="Junction"
|
Title="Junction"
|
||||||
Width="800" Height="600">
|
Width="900" Height="640"
|
||||||
|
Background="{DynamicResource AppBackgroundBrush}">
|
||||||
<!-- Shell: hosts the current page; ViewLocator maps the VM to its View. -->
|
<!-- Shell: hosts the current page; ViewLocator maps the VM to its View. -->
|
||||||
<ContentControl Content="{Binding CurrentPage}" />
|
<ContentControl Content="{Binding CurrentPage}" />
|
||||||
</Window>
|
</Window>
|
||||||
|
|
|
||||||
|
|
@ -8,36 +8,49 @@
|
||||||
x:DataType="vm:DashboardViewModel"
|
x:DataType="vm:DashboardViewModel"
|
||||||
x:CompileBindings="True">
|
x:CompileBindings="True">
|
||||||
|
|
||||||
<DockPanel Margin="16">
|
<DockPanel Margin="20">
|
||||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto" Margin="0,0,0,12">
|
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,Auto" Margin="0,0,0,16">
|
||||||
<TextBlock Grid.Column="0"
|
<TextBlock Grid.Column="0"
|
||||||
|
Classes="h1"
|
||||||
Text="{Binding Title}"
|
Text="{Binding Title}"
|
||||||
FontSize="22" FontWeight="SemiBold"
|
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<Button Grid.Column="1"
|
<Button Grid.Column="1"
|
||||||
|
Classes="accent"
|
||||||
Content="+ Add machine"
|
Content="+ Add machine"
|
||||||
Command="{Binding AddMachineCommand}"
|
Command="{Binding AddMachineCommand}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Border Classes="card" Padding="0">
|
||||||
<DataGrid ItemsSource="{Binding Machines}"
|
<DataGrid ItemsSource="{Binding Machines}"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
CanUserReorderColumns="False"
|
CanUserReorderColumns="False"
|
||||||
CanUserResizeColumns="True"
|
CanUserResizeColumns="True"
|
||||||
GridLinesVisibility="Horizontal"
|
GridLinesVisibility="Horizontal"
|
||||||
HeadersVisibility="Column">
|
HeadersVisibility="Column"
|
||||||
|
Background="Transparent">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="Machine"
|
<DataGridTemplateColumn Header="Machine" Width="2*">
|
||||||
Binding="{Binding Name}"
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
Width="2*"
|
<DataTemplate x:DataType="vm:MachineRowViewModel">
|
||||||
x:DataType="vm:MachineRowViewModel" />
|
<StackPanel Orientation="Horizontal" Spacing="10"
|
||||||
|
VerticalAlignment="Center" Margin="12,0">
|
||||||
|
<PathIcon Classes="protocol"
|
||||||
|
Data="{Binding ProtocolId, Converter={StaticResource ProtocolIdToIcon}}"
|
||||||
|
ToolTip.Tip="{Binding ProtocolId}"
|
||||||
|
VerticalAlignment="Center" />
|
||||||
|
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
|
</DataGridTemplateColumn>
|
||||||
|
|
||||||
<DataGridTemplateColumn Header="Status" Width="Auto">
|
<DataGridTemplateColumn Header="Status" Width="Auto">
|
||||||
<DataGridTemplateColumn.CellTemplate>
|
<DataGridTemplateColumn.CellTemplate>
|
||||||
<DataTemplate x:DataType="vm:MachineRowViewModel">
|
<DataTemplate x:DataType="vm:MachineRowViewModel">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6"
|
<StackPanel Orientation="Horizontal" Spacing="6"
|
||||||
VerticalAlignment="Center" Margin="8,0">
|
VerticalAlignment="Center" Margin="12,0">
|
||||||
<Ellipse Width="10" Height="10" VerticalAlignment="Center"
|
<Ellipse Width="10" Height="10" VerticalAlignment="Center"
|
||||||
Fill="{Binding StatusKind, Converter={StaticResource StatusKindToBrush}}" />
|
Fill="{Binding StatusKind, Converter={StaticResource StatusKindToBrush}}" />
|
||||||
<TextBlock Text="{Binding ConnectionState}"
|
<TextBlock Text="{Binding ConnectionState}"
|
||||||
|
|
@ -63,11 +76,12 @@
|
||||||
<DataTemplate x:DataType="vm:MachineRowViewModel">
|
<DataTemplate x:DataType="vm:MachineRowViewModel">
|
||||||
<Button Content="Details"
|
<Button Content="Details"
|
||||||
Command="{Binding OpenDetailCommand}"
|
Command="{Binding OpenDetailCommand}"
|
||||||
VerticalAlignment="Center" Margin="8,2" />
|
VerticalAlignment="Center" Margin="12,4" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</DataGridTemplateColumn.CellTemplate>
|
</DataGridTemplateColumn.CellTemplate>
|
||||||
</DataGridTemplateColumn>
|
</DataGridTemplateColumn>
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
</Border>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,16 @@
|
||||||
x:DataType="vm:MachineConfigViewModel"
|
x:DataType="vm:MachineConfigViewModel"
|
||||||
x:CompileBindings="True">
|
x:CompileBindings="True">
|
||||||
|
|
||||||
<DockPanel Margin="16">
|
<DockPanel Margin="20">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<TextBlock DockPanel.Dock="Top"
|
<TextBlock DockPanel.Dock="Top" Classes="h1"
|
||||||
Text="{Binding Title}"
|
Text="{Binding Title}"
|
||||||
FontSize="22" FontWeight="SemiBold"
|
|
||||||
Margin="0,0,0,16" />
|
Margin="0,0,0,16" />
|
||||||
|
|
||||||
<!-- Action bar -->
|
<!-- Action bar -->
|
||||||
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="12" Margin="0,16,0,0">
|
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" Spacing="12" Margin="0,16,0,0">
|
||||||
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" />
|
<Button Classes="accent" Content="Save" Command="{Binding SaveCommand}" IsDefault="True" />
|
||||||
<Button Content="Cancel" Command="{Binding CancelCommand}" />
|
<Button Content="Cancel" Command="{Binding CancelCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
|
@ -26,20 +25,21 @@
|
||||||
<TextBlock DockPanel.Dock="Bottom"
|
<TextBlock DockPanel.Dock="Bottom"
|
||||||
Text="{Binding ValidationError}"
|
Text="{Binding ValidationError}"
|
||||||
IsVisible="{Binding HasValidationError}"
|
IsVisible="{Binding HasValidationError}"
|
||||||
Foreground="#C0392B"
|
Foreground="{DynamicResource DangerBrush}"
|
||||||
TextWrapping="Wrap"
|
TextWrapping="Wrap"
|
||||||
Margin="0,12,0,0" />
|
Margin="0,12,0,0" />
|
||||||
|
|
||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<StackPanel DockPanel.Dock="Top" Spacing="12" MaxWidth="480" HorizontalAlignment="Left">
|
<Border DockPanel.Dock="Top" Classes="card" MaxWidth="480" HorizontalAlignment="Left">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Name" FontWeight="SemiBold" />
|
<TextBlock Classes="fieldLabel" Text="Name" />
|
||||||
<TextBox Text="{Binding Name}" Watermark="Machine name" />
|
<TextBox Text="{Binding Name}" Watermark="Machine name" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Protocol" FontWeight="SemiBold" />
|
<TextBlock Classes="fieldLabel" Text="Protocol" />
|
||||||
<ComboBox ItemsSource="{Binding AvailableProtocols}"
|
<ComboBox ItemsSource="{Binding AvailableProtocols}"
|
||||||
SelectedItem="{Binding SelectedProtocol}"
|
SelectedItem="{Binding SelectedProtocol}"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
|
|
@ -47,26 +47,24 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Agent URL" FontWeight="SemiBold" />
|
<TextBlock Classes="fieldLabel" Text="Agent URL" />
|
||||||
<TextBox Text="{Binding AgentUrl}" Watermark="http://host:5000" />
|
<TextBox Text="{Binding AgentUrl}" Watermark="http://host:5000" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Text="Poll Interval (seconds)" FontWeight="SemiBold" />
|
<TextBlock Classes="fieldLabel" Text="Poll Interval (seconds)" />
|
||||||
<TextBox Text="{Binding PollIntervalSeconds}" Width="120" HorizontalAlignment="Left" />
|
<TextBox Text="{Binding PollIntervalSeconds}" Width="120" HorizontalAlignment="Left" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Data items to monitor (fills remaining space) -->
|
<!-- Data items to monitor (fills remaining space) -->
|
||||||
<Border BorderThickness="1"
|
<Border Classes="card" Margin="0,16,0,0" MaxWidth="640" HorizontalAlignment="Left">
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="12" Margin="0,16,0,0" MaxWidth="640" HorizontalAlignment="Left">
|
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|
||||||
<TextBlock DockPanel.Dock="Top" Text="Data items to monitor"
|
<TextBlock DockPanel.Dock="Top" Classes="h2" Text="Data items to monitor"
|
||||||
FontSize="16" FontWeight="SemiBold" Margin="0,0,0,8" />
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
<!-- Controls row -->
|
<!-- Controls row -->
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="0,0,0,8">
|
||||||
|
|
@ -82,22 +80,22 @@
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Status message -->
|
<!-- Status message -->
|
||||||
<TextBlock DockPanel.Dock="Top"
|
<TextBlock DockPanel.Dock="Top" Classes="subtle"
|
||||||
Text="{Binding ItemsStatus}"
|
Text="{Binding ItemsStatus}"
|
||||||
IsVisible="{Binding HasItemsStatus}"
|
IsVisible="{Binding HasItemsStatus}"
|
||||||
Opacity="0.85" TextWrapping="Wrap" Margin="0,0,0,8" />
|
TextWrapping="Wrap" Margin="0,0,0,8" />
|
||||||
|
|
||||||
<!-- Column header -->
|
<!-- Column header -->
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
BorderThickness="0,0,0,1"
|
BorderThickness="0,0,0,1"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||||
Padding="0,0,0,4" Margin="0,0,0,4">
|
Padding="0,0,0,4" Margin="0,0,0,4">
|
||||||
<Grid ColumnDefinitions="Auto,2*,1.2*,1.2*,1*" Margin="0,0,8,0">
|
<Grid ColumnDefinitions="Auto,2*,1.2*,1.2*,1*" Margin="0,0,8,0">
|
||||||
<TextBlock Grid.Column="0" Text="" Width="28" />
|
<TextBlock Grid.Column="0" Text="" Width="28" />
|
||||||
<TextBlock Grid.Column="1" Text="Name" FontWeight="Bold" />
|
<TextBlock Grid.Column="1" Classes="fieldLabel" Text="Name" />
|
||||||
<TextBlock Grid.Column="2" Text="Type" FontWeight="Bold" />
|
<TextBlock Grid.Column="2" Classes="fieldLabel" Text="Type" />
|
||||||
<TextBlock Grid.Column="3" Text="Category" FontWeight="Bold" />
|
<TextBlock Grid.Column="3" Classes="fieldLabel" Text="Category" />
|
||||||
<TextBlock Grid.Column="4" Text="Units" FontWeight="Bold" />
|
<TextBlock Grid.Column="4" Classes="fieldLabel" Text="Units" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|
@ -112,7 +110,7 @@
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
<StackPanel Grid.Column="1" VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="{Binding Id}" FontSize="11" Opacity="0.6" TextWrapping="Wrap" />
|
<TextBlock Text="{Binding Id}" Classes="subtle" FontSize="11" TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="2" Text="{Binding Type}" VerticalAlignment="Center" TextWrapping="Wrap" />
|
<TextBlock Grid.Column="2" Text="{Binding Type}" VerticalAlignment="Center" TextWrapping="Wrap" />
|
||||||
<TextBlock Grid.Column="3" Text="{Binding Category}" VerticalAlignment="Center" TextWrapping="Wrap" />
|
<TextBlock Grid.Column="3" Text="{Binding Category}" VerticalAlignment="Center" TextWrapping="Wrap" />
|
||||||
|
|
|
||||||
|
|
@ -9,71 +9,70 @@
|
||||||
x:CompileBindings="True">
|
x:CompileBindings="True">
|
||||||
|
|
||||||
<Grid>
|
<Grid>
|
||||||
<DockPanel Margin="16">
|
<DockPanel Margin="20">
|
||||||
|
|
||||||
<!-- Header bar: Back + machine name + status badge + Edit/Delete -->
|
<!-- Header bar: Back + protocol glyph + machine name + status badge + Edit/Delete -->
|
||||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,Auto,*,Auto,Auto" Margin="0,0,0,12">
|
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto" Margin="0,0,0,16">
|
||||||
<Button Grid.Column="0" Content="← Back" Command="{Binding BackCommand}" VerticalAlignment="Center" />
|
<Button Grid.Column="0" Content="← Back" Command="{Binding BackCommand}" VerticalAlignment="Center" />
|
||||||
<TextBlock Grid.Column="1" Text="{Binding MachineName}"
|
<PathIcon Grid.Column="1" Classes="protocol" Width="20" Height="20"
|
||||||
FontSize="22" FontWeight="SemiBold"
|
Data="{Binding ProtocolId, Converter={StaticResource ProtocolIdToIcon}}"
|
||||||
VerticalAlignment="Center" Margin="12,0" />
|
ToolTip.Tip="{Binding ProtocolId}"
|
||||||
<Border Grid.Column="2"
|
VerticalAlignment="Center" Margin="14,0,0,0" />
|
||||||
|
<TextBlock Grid.Column="2" Classes="h1" Text="{Binding MachineName}"
|
||||||
|
VerticalAlignment="Center" Margin="10,0" />
|
||||||
|
<Border Grid.Column="3"
|
||||||
Background="{Binding StatusKind, Converter={StaticResource StatusKindToBrush}}"
|
Background="{Binding StatusKind, Converter={StaticResource StatusKindToBrush}}"
|
||||||
CornerRadius="4" Padding="8,3" HorizontalAlignment="Left" VerticalAlignment="Center">
|
CornerRadius="4" Padding="8,3" HorizontalAlignment="Left" VerticalAlignment="Center">
|
||||||
<TextBlock Text="{Binding ConnectionState}" Foreground="White" FontWeight="SemiBold" FontSize="12" />
|
<TextBlock Text="{Binding ConnectionState}" Foreground="White" FontWeight="SemiBold" FontSize="12" />
|
||||||
</Border>
|
</Border>
|
||||||
<Button Grid.Column="3" Content="Edit" Command="{Binding EditCommand}"
|
<Button Grid.Column="4" Content="Edit" Command="{Binding EditCommand}"
|
||||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||||
<Button Grid.Column="4" Content="Delete" Command="{Binding RequestDeleteCommand}"
|
<Button Grid.Column="5" Classes="danger" Content="Delete" Command="{Binding RequestDeleteCommand}"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Machine info panel -->
|
<!-- Machine info panel -->
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top" Classes="card" Margin="0,0,0,16">
|
||||||
BorderThickness="1"
|
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
|
||||||
CornerRadius="4"
|
|
||||||
Padding="12" Margin="0,0,0,12">
|
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" >
|
<Grid ColumnDefinitions="Auto,*,Auto,*" RowDefinitions="Auto,Auto,Auto,Auto" >
|
||||||
<TextBlock Grid.Row="0" Grid.Column="0" Text="Protocol" FontWeight="Bold" Margin="0,0,12,4" />
|
<TextBlock Grid.Row="0" Grid.Column="0" Classes="fieldLabel" Text="Protocol" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ProtocolId}" Margin="0,0,24,4" />
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding ProtocolId}" Margin="0,0,24,6" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="2" Text="Poll Interval" FontWeight="Bold" Margin="0,0,12,4" />
|
<TextBlock Grid.Row="0" Grid.Column="2" Classes="fieldLabel" Text="Poll Interval" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="3" Text="{Binding PollInterval}" Margin="0,0,0,4" />
|
<TextBlock Grid.Row="0" Grid.Column="3" Text="{Binding PollInterval}" Margin="0,0,0,6" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="1" Grid.Column="0" Text="Connection" FontWeight="Bold" Margin="0,0,12,4" />
|
<TextBlock Grid.Row="1" Grid.Column="0" Classes="fieldLabel" Text="Connection" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ConnectionState}"
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="{Binding ConnectionState}"
|
||||||
Foreground="{Binding StatusKind, Converter={StaticResource StatusKindToBrush}}"
|
Foreground="{Binding StatusKind, Converter={StaticResource StatusKindToBrush}}"
|
||||||
FontWeight="SemiBold" Margin="0,0,24,4" />
|
FontWeight="SemiBold" Margin="0,0,24,6" />
|
||||||
<TextBlock Grid.Row="1" Grid.Column="2" Text="Captured At" FontWeight="Bold" Margin="0,0,12,4" />
|
<TextBlock Grid.Row="1" Grid.Column="2" Classes="fieldLabel" Text="Captured At" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Row="1" Grid.Column="3" Text="{Binding CapturedAt}" Margin="0,0,0,4" />
|
<TextBlock Grid.Row="1" Grid.Column="3" Text="{Binding CapturedAt}" Margin="0,0,0,6" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="2" Grid.Column="0" Text="Last Seen" FontWeight="Bold" Margin="0,0,12,4" />
|
<TextBlock Grid.Row="2" Grid.Column="0" Classes="fieldLabel" Text="Last Seen" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding LastSeen}" Margin="0,0,24,4" />
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="{Binding LastSeen}" Margin="0,0,24,6" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="2" Text="Machine Id" FontWeight="Bold" Margin="0,0,12,4" />
|
<TextBlock Grid.Row="2" Grid.Column="2" Classes="fieldLabel" Text="Machine Id" Margin="0,0,12,6" />
|
||||||
<TextBlock Grid.Row="2" Grid.Column="3" Text="{Binding MachineIdText}" Margin="0,0,0,4"
|
<TextBlock Grid.Row="2" Grid.Column="3" Text="{Binding MachineIdText}" Margin="0,0,0,6"
|
||||||
FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
|
FontSize="11" Opacity="0.7" TextWrapping="Wrap" />
|
||||||
|
|
||||||
<TextBlock Grid.Row="3" Grid.Column="0" Text="Agent URL" FontWeight="Bold" Margin="0,0,12,0" />
|
<TextBlock Grid.Row="3" Grid.Column="0" Classes="fieldLabel" Text="Agent URL" Margin="0,0,12,0" />
|
||||||
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding AgentUrl}"
|
<TextBlock Grid.Row="3" Grid.Column="1" Grid.ColumnSpan="3" Text="{Binding AgentUrl}"
|
||||||
Margin="0,0,0,0" TextWrapping="Wrap" />
|
Margin="0,0,0,0" TextWrapping="Wrap" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Data-items header -->
|
<!-- Data-items header -->
|
||||||
<TextBlock DockPanel.Dock="Top"
|
<TextBlock DockPanel.Dock="Top" Classes="h2"
|
||||||
Text="{Binding ItemCount, StringFormat='Data Items ({0})'}"
|
Text="{Binding ItemCount, StringFormat='Data Items ({0})'}"
|
||||||
FontSize="16" FontWeight="SemiBold" Margin="0,0,0,6" />
|
Margin="0,0,0,8" />
|
||||||
|
|
||||||
<Border DockPanel.Dock="Top"
|
<Border DockPanel.Dock="Top"
|
||||||
BorderThickness="0,0,0,1"
|
BorderThickness="0,0,0,1"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
BorderBrush="{DynamicResource CardBorderBrush}"
|
||||||
Padding="0,0,0,6" Margin="0,0,0,4"
|
Padding="0,0,0,6" Margin="0,0,0,4"
|
||||||
IsVisible="{Binding HasItems}">
|
IsVisible="{Binding HasItems}">
|
||||||
<Grid ColumnDefinitions="2*,2*,1.2*,1.2*">
|
<Grid ColumnDefinitions="2*,2*,1.2*,1.2*">
|
||||||
<TextBlock Grid.Column="0" Text="Name" FontWeight="Bold" />
|
<TextBlock Grid.Column="0" Classes="fieldLabel" Text="Name" />
|
||||||
<TextBlock Grid.Column="1" Text="Value" FontWeight="Bold" />
|
<TextBlock Grid.Column="1" Classes="fieldLabel" Text="Value" />
|
||||||
<TextBlock Grid.Column="2" Text="Category" FontWeight="Bold" />
|
<TextBlock Grid.Column="2" Classes="fieldLabel" Text="Category" />
|
||||||
<TextBlock Grid.Column="3" Text="Timestamp" FontWeight="Bold" />
|
<TextBlock Grid.Column="3" Classes="fieldLabel" Text="Timestamp" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|
@ -86,7 +85,7 @@
|
||||||
<Grid ColumnDefinitions="2*,2*,1.2*,1.2*" Margin="0,5">
|
<Grid ColumnDefinitions="2*,2*,1.2*,1.2*" Margin="0,5">
|
||||||
<StackPanel Grid.Column="0">
|
<StackPanel Grid.Column="0">
|
||||||
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
<TextBlock Text="{Binding Name}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="{Binding Id}" FontSize="11" Opacity="0.6" TextWrapping="Wrap" />
|
<TextBlock Text="{Binding Id}" Classes="subtle" FontSize="11" TextWrapping="Wrap" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Value}" VerticalAlignment="Center" TextWrapping="Wrap" />
|
<TextBlock Grid.Column="1" Text="{Binding Value}" VerticalAlignment="Center" TextWrapping="Wrap" />
|
||||||
<TextBlock Grid.Column="2" Text="{Binding Category}" VerticalAlignment="Center" />
|
<TextBlock Grid.Column="2" Text="{Binding Category}" VerticalAlignment="Center" />
|
||||||
|
|
@ -101,11 +100,10 @@
|
||||||
<StackPanel IsVisible="{Binding HasNoItems}"
|
<StackPanel IsVisible="{Binding HasNoItems}"
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||||
Spacing="6" MaxWidth="420">
|
Spacing="6" MaxWidth="420">
|
||||||
<TextBlock Text="No monitored items"
|
<TextBlock Classes="h2" Text="No monitored items"
|
||||||
FontSize="16" FontWeight="SemiBold"
|
|
||||||
HorizontalAlignment="Center" />
|
HorizontalAlignment="Center" />
|
||||||
<TextBlock Text="Edit this machine to select which data items to monitor."
|
<TextBlock Classes="subtle" Text="Edit this machine to select which data items to monitor."
|
||||||
Opacity="0.75" TextWrapping="Wrap" TextAlignment="Center" />
|
TextWrapping="Wrap" TextAlignment="Center" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
|
|
@ -113,19 +111,16 @@
|
||||||
<!-- Delete-confirmation overlay: dims the screen, names the machine, requires an explicit choice. -->
|
<!-- Delete-confirmation overlay: dims the screen, names the machine, requires an explicit choice. -->
|
||||||
<Border IsVisible="{Binding IsDeleteConfirmVisible}"
|
<Border IsVisible="{Binding IsDeleteConfirmVisible}"
|
||||||
Background="#99000000">
|
Background="#99000000">
|
||||||
<Border Background="{DynamicResource SystemControlBackgroundAltHighBrush}"
|
<Border Classes="card" MaxWidth="420"
|
||||||
BorderBrush="{DynamicResource SystemControlForegroundBaseMediumLowBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="6"
|
|
||||||
Padding="20" MaxWidth="420"
|
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
<StackPanel Spacing="14">
|
<StackPanel Spacing="14">
|
||||||
<TextBlock Text="{Binding DeleteConfirmPrompt}" FontSize="16" FontWeight="SemiBold" TextWrapping="Wrap" />
|
<TextBlock Classes="h2" Text="{Binding DeleteConfirmPrompt}" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="This permanently removes the machine and stops monitoring it. This cannot be undone."
|
<TextBlock Classes="subtle"
|
||||||
Opacity="0.75" TextWrapping="Wrap" />
|
Text="This permanently removes the machine and stops monitoring it. This cannot be undone."
|
||||||
|
TextWrapping="Wrap" />
|
||||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="8">
|
||||||
<Button Content="Cancel" Command="{Binding CancelDeleteCommand}" />
|
<Button Content="Cancel" Command="{Binding CancelDeleteCommand}" />
|
||||||
<Button Content="Delete" Command="{Binding ConfirmDeleteCommand}"
|
<Button Classes="danger" Content="Delete" Command="{Binding ConfirmDeleteCommand}" />
|
||||||
Background="#C62828" Foreground="White" FontWeight="SemiBold" />
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,8 @@
|
||||||
<ProjectReference Include="..\..\src\Junction.Core\Junction.Core.csproj" />
|
<ProjectReference Include="..\..\src\Junction.Core\Junction.Core.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Junction.Persistence\Junction.Persistence.csproj" />
|
<ProjectReference Include="..\..\src\Junction.Persistence\Junction.Persistence.csproj" />
|
||||||
<ProjectReference Include="..\..\src\Junction.Protocols.MTConnect\Junction.Protocols.MTConnect.csproj" />
|
<ProjectReference Include="..\..\src\Junction.Protocols.MTConnect\Junction.Protocols.MTConnect.csproj" />
|
||||||
|
<!-- App reference resolves its net8.0 target (tests are net8.0-only); covers view-layer converters. -->
|
||||||
|
<ProjectReference Include="..\..\src\Junction.App\Junction.App.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
78
tests/Junction.Tests/Unit/ProtocolIdToIconConverterTests.cs
Normal file
78
tests/Junction.Tests/Unit/ProtocolIdToIconConverterTests.cs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
using Junction.App.Converters;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Junction.Tests.Unit
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Data-level tests for the protocol → glyph mapping. Targets the pure
|
||||||
|
/// <see cref="ProtocolIdToIconConverter.PathDataFor"/> so no Avalonia platform is required
|
||||||
|
/// (the geometry parse in Convert needs a render backend). Mirrors the intent of the
|
||||||
|
/// StatusKind converter: known ids → distinct non-empty data, unknown → fallback, never throws.
|
||||||
|
/// </summary>
|
||||||
|
public class ProtocolIdToIconConverterTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("mtconnect")]
|
||||||
|
[InlineData("opcua")]
|
||||||
|
[InlineData("fanuc")]
|
||||||
|
public void KnownProtocol_ReturnsNonEmptyData(string protocolId)
|
||||||
|
{
|
||||||
|
var data = ProtocolIdToIconConverter.PathDataFor(protocolId);
|
||||||
|
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KnownProtocols_AreMutuallyDistinct()
|
||||||
|
{
|
||||||
|
var mtc = ProtocolIdToIconConverter.PathDataFor("mtconnect");
|
||||||
|
var opc = ProtocolIdToIconConverter.PathDataFor("opcua");
|
||||||
|
var fan = ProtocolIdToIconConverter.PathDataFor("fanuc");
|
||||||
|
|
||||||
|
Assert.NotEqual(mtc, opc);
|
||||||
|
Assert.NotEqual(mtc, fan);
|
||||||
|
Assert.NotEqual(opc, fan);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Lookup_IsCaseAndWhitespaceInsensitive()
|
||||||
|
{
|
||||||
|
var canonical = ProtocolIdToIconConverter.PathDataFor("mtconnect");
|
||||||
|
|
||||||
|
Assert.Equal(canonical, ProtocolIdToIconConverter.PathDataFor(" MtConnect "));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("unknown")]
|
||||||
|
[InlineData("modbus")]
|
||||||
|
[InlineData("")]
|
||||||
|
public void UnknownOrEmpty_ReturnsFallback(string protocolId)
|
||||||
|
{
|
||||||
|
var fallback = ProtocolIdToIconConverter.PathDataFor("definitely-not-a-protocol");
|
||||||
|
var data = ProtocolIdToIconConverter.PathDataFor(protocolId);
|
||||||
|
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(data));
|
||||||
|
Assert.Equal(fallback, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Null_ReturnsFallback_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var fallback = ProtocolIdToIconConverter.PathDataFor("definitely-not-a-protocol");
|
||||||
|
var data = ProtocolIdToIconConverter.PathDataFor(null);
|
||||||
|
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(data));
|
||||||
|
Assert.Equal(fallback, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Fallback_DiffersFromEveryKnownProtocol()
|
||||||
|
{
|
||||||
|
var fallback = ProtocolIdToIconConverter.PathDataFor("unknown");
|
||||||
|
|
||||||
|
Assert.NotEqual(fallback, ProtocolIdToIconConverter.PathDataFor("mtconnect"));
|
||||||
|
Assert.NotEqual(fallback, ProtocolIdToIconConverter.PathDataFor("opcua"));
|
||||||
|
Assert.NotEqual(fallback, ProtocolIdToIconConverter.PathDataFor("fanuc"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue