|
|
@@ -4,6 +4,7 @@ using System.ComponentModel;
|
|
|
using System.Runtime.CompilerServices;
|
|
|
using System.Windows;
|
|
|
using System.Windows.Controls;
|
|
|
+using System.Windows.Input;
|
|
|
using System.Windows.Media;
|
|
|
using System.Windows.Media.Animation;
|
|
|
using NetworkTool.Client.Models;
|
|
|
@@ -15,9 +16,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
private const int ApplyConfirmationTimeoutSeconds = 20;
|
|
|
private readonly ServerApiService _serverApiService = new();
|
|
|
- private readonly ObservableCollection<EditableAddress> _addresses = [];
|
|
|
- private readonly ObservableCollection<EditableRoute> _routes = [];
|
|
|
- private readonly ObservableCollection<EditableDns> _dns = [];
|
|
|
+ private readonly ObservableCollection<InterfaceEditor> _interfaces = [];
|
|
|
private readonly string _baseAddress;
|
|
|
private readonly string _remoteHost;
|
|
|
private readonly string _localIPv4;
|
|
|
@@ -25,23 +24,20 @@ public partial class DeviceDetailsWindow : Window
|
|
|
private bool _configValidated;
|
|
|
private bool _configDirty;
|
|
|
private bool _isBusy;
|
|
|
- private bool _isRestoringInterfaceSelection;
|
|
|
private bool _suppressConfigChangeHandling;
|
|
|
- private RemoteInterfaceInfo? _currentSelectedInterface;
|
|
|
private CancellationTokenSource? _statusMessageCts;
|
|
|
|
|
|
public DeviceDetailsWindow(string baseAddress, string localIPv4, string password)
|
|
|
{
|
|
|
InitializeComponent();
|
|
|
- AddressesDataGrid.ItemsSource = _addresses;
|
|
|
- RoutesDataGrid.ItemsSource = _routes;
|
|
|
- DnsDataGrid.ItemsSource = _dns;
|
|
|
+ InterfacesItemsControl.ItemsSource = _interfaces;
|
|
|
_baseAddress = baseAddress;
|
|
|
_remoteHost = GetRemoteHost(baseAddress);
|
|
|
_localIPv4 = localIPv4;
|
|
|
_password = password;
|
|
|
UpdateWindowTitle();
|
|
|
Loaded += DeviceDetailsWindow_OnLoaded;
|
|
|
+ Closing += DeviceDetailsWindow_OnClosing;
|
|
|
}
|
|
|
|
|
|
private async void DeviceDetailsWindow_OnLoaded(object sender, RoutedEventArgs e)
|
|
|
@@ -64,7 +60,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
var device = await _serverApiService.GetDeviceInfoAsync(_baseAddress, _password, _localIPv4);
|
|
|
if (device is not null)
|
|
|
{
|
|
|
- UpdateWindowTitle(device.Hostname);
|
|
|
+ UpdateWindowTitle(device.Hostname, device.ServerVersion);
|
|
|
}
|
|
|
|
|
|
var interfaces = await _serverApiService.GetInterfacesAsync(_baseAddress, _password, _localIPv4);
|
|
|
@@ -74,38 +70,32 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface}。请选择需要配置的目标接口。");
|
|
|
- var suggested = interfaces.Interfaces.FirstOrDefault(item => item.SystemName == interfaces.SuggestedTargetInterface)
|
|
|
- ?? interfaces.Interfaces.FirstOrDefault(item => item.IsSuggestedTarget)
|
|
|
- ?? interfaces.Interfaces.FirstOrDefault(item => !item.IsManagementInterface);
|
|
|
- RemoteTargetInterfaceTabControl.ItemsSource = interfaces.Interfaces;
|
|
|
- if (suggested is not null)
|
|
|
+ ShowStatusMessage($"当前管理接口:{interfaces.ManagementInterface}。正在读取全部接口配置。");
|
|
|
+ foreach (var info in interfaces.Interfaces)
|
|
|
{
|
|
|
- RemoteTargetInterfaceTabControl.SelectedItem = suggested;
|
|
|
- await LoadRemoteInterfaceConfigAsync(suggested.SystemName);
|
|
|
- _currentSelectedInterface = suggested;
|
|
|
+ var editor = new InterfaceEditor(info);
|
|
|
+ _interfaces.Add(editor);
|
|
|
+ await LoadRemoteInterfaceConfigAsync(editor);
|
|
|
}
|
|
|
+
|
|
|
+ _configValidated = false;
|
|
|
+ _configDirty = false;
|
|
|
+ ShowStatusMessage("已读取全部接口配置。");
|
|
|
}
|
|
|
|
|
|
private void ClearDetails()
|
|
|
{
|
|
|
UpdateWindowTitle();
|
|
|
- RemoteTargetInterfaceTabControl.ItemsSource = null;
|
|
|
- _addresses.Clear();
|
|
|
- _routes.Clear();
|
|
|
- _dns.Clear();
|
|
|
- DefaultGatewayCheckBox.IsChecked = false;
|
|
|
- DefaultGatewayTextBox.Text = string.Empty;
|
|
|
- CustomRoutesCheckBox.IsChecked = false;
|
|
|
+ _interfaces.Clear();
|
|
|
_configValidated = false;
|
|
|
_configDirty = false;
|
|
|
- _currentSelectedInterface = null;
|
|
|
}
|
|
|
|
|
|
- private void UpdateWindowTitle(string? hostname = null)
|
|
|
+ private void UpdateWindowTitle(string? hostname = null, string? serverVersion = null)
|
|
|
{
|
|
|
var hostPart = string.IsNullOrWhiteSpace(hostname) ? _remoteHost : $"{hostname} ({_remoteHost})";
|
|
|
- Title = string.IsNullOrWhiteSpace(hostPart) ? "设备信息与接口配置" : $"设备信息与接口配置 - {hostPart}";
|
|
|
+ var versionPart = string.IsNullOrWhiteSpace(serverVersion) ? string.Empty : $" - Server {serverVersion}";
|
|
|
+ Title = string.IsNullOrWhiteSpace(hostPart) ? $"设备信息与接口配置{versionPart}" : $"设备信息与接口配置 - {hostPart}{versionPart}";
|
|
|
}
|
|
|
|
|
|
private static string GetRemoteHost(string baseAddress)
|
|
|
@@ -113,49 +103,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return Uri.TryCreate(baseAddress, UriKind.Absolute, out var uri) ? uri.Host : baseAddress;
|
|
|
}
|
|
|
|
|
|
- private async void RemoteTargetInterfaceTabControl_OnSelectionChanged(object sender, SelectionChangedEventArgs e)
|
|
|
- {
|
|
|
- try
|
|
|
- {
|
|
|
- if (RemoteTargetInterfaceTabControl.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
- {
|
|
|
- UpdateButtonStates();
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (_isRestoringInterfaceSelection)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (_configDirty && _currentSelectedInterface is not null && selected.SystemName != _currentSelectedInterface.SystemName)
|
|
|
- {
|
|
|
- var result = MessageBox.Show(
|
|
|
- this,
|
|
|
- "当前配置已修改,切换接口会丢失未应用内容。是否继续?",
|
|
|
- "确认切换接口",
|
|
|
- MessageBoxButton.OKCancel,
|
|
|
- MessageBoxImage.Warning);
|
|
|
- if (result != MessageBoxResult.OK)
|
|
|
- {
|
|
|
- _isRestoringInterfaceSelection = true;
|
|
|
- RemoteTargetInterfaceTabControl.SelectedItem = _currentSelectedInterface;
|
|
|
- _isRestoringInterfaceSelection = false;
|
|
|
- return;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- await LoadRemoteInterfaceConfigAsync(selected.SystemName, useBusyState: true);
|
|
|
- _currentSelectedInterface = selected;
|
|
|
- }
|
|
|
- catch (Exception ex)
|
|
|
- {
|
|
|
- ShowStatusMessage($"读取目标接口配置失败:{ex.Message}");
|
|
|
- SetBusyState(false);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private async Task LoadRemoteInterfaceConfigAsync(string interfaceName, bool useBusyState = false)
|
|
|
+ private async Task LoadRemoteInterfaceConfigAsync(InterfaceEditor editor, bool useBusyState = false)
|
|
|
{
|
|
|
if (useBusyState)
|
|
|
{
|
|
|
@@ -164,53 +112,51 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
try
|
|
|
{
|
|
|
- var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, interfaceName);
|
|
|
+ var result = await _serverApiService.GetInterfaceConfigAsync(_baseAddress, _password, _localIPv4, editor.SystemName);
|
|
|
if (!result.Success || result.Data is null)
|
|
|
{
|
|
|
- ShowStatusMessage($"读取目标接口 {interfaceName} 配置失败:{result.Message}");
|
|
|
+ ShowStatusMessage($"读取目标接口 {editor.SystemName} 配置失败:{result.Message}");
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
var config = result.Data;
|
|
|
_suppressConfigChangeHandling = true;
|
|
|
- Dhcp4CheckBox.IsChecked = false;
|
|
|
- _addresses.Clear();
|
|
|
+ editor.Dhcp4 = config.Dhcp4;
|
|
|
+ editor.Addresses.Clear();
|
|
|
foreach (var address in config.EffectiveAddresses)
|
|
|
{
|
|
|
- _addresses.Add(new EditableAddress { IP = address.IP, Mask = PrefixToMask(address.Prefix) });
|
|
|
+ editor.Addresses.Add(new EditableAddress(editor) { IP = address.IP, Mask = PrefixToMask(address.Prefix) });
|
|
|
}
|
|
|
- _routes.Clear();
|
|
|
- DefaultGatewayCheckBox.IsChecked = false;
|
|
|
- DefaultGatewayTextBox.Text = string.Empty;
|
|
|
+ editor.Routes.Clear();
|
|
|
+ editor.DefaultGatewayEnabled = false;
|
|
|
+ editor.DefaultGateway = string.Empty;
|
|
|
foreach (var route in config.EffectiveRoutes)
|
|
|
{
|
|
|
if (route.To.Equals("default", StringComparison.OrdinalIgnoreCase))
|
|
|
{
|
|
|
- DefaultGatewayCheckBox.IsChecked = true;
|
|
|
- DefaultGatewayTextBox.Text = route.Via;
|
|
|
+ editor.DefaultGatewayEnabled = true;
|
|
|
+ editor.DefaultGateway = route.Via;
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
- _routes.Add(CreateEditableRoute(route));
|
|
|
+ editor.Routes.Add(CreateEditableRoute(editor, route));
|
|
|
}
|
|
|
}
|
|
|
- CustomRoutesCheckBox.IsChecked = _routes.Count > 0;
|
|
|
- _dns.Clear();
|
|
|
+ editor.CustomRoutesEnabled = editor.Routes.Count > 0;
|
|
|
+ editor.Dns.Clear();
|
|
|
if (config.Dns is not null)
|
|
|
{
|
|
|
foreach (var dns in config.Dns)
|
|
|
{
|
|
|
- _dns.Add(new EditableDns { Address = dns });
|
|
|
+ editor.Dns.Add(new EditableDns(editor) { Address = dns });
|
|
|
}
|
|
|
}
|
|
|
_suppressConfigChangeHandling = false;
|
|
|
- _configValidated = false;
|
|
|
- _configDirty = false;
|
|
|
- ShowStatusMessage("已读取Linux端IP配置。");
|
|
|
UpdateButtonStates();
|
|
|
}
|
|
|
finally
|
|
|
{
|
|
|
+ _suppressConfigChangeHandling = false;
|
|
|
if (useBusyState)
|
|
|
{
|
|
|
SetBusyState(false);
|
|
|
@@ -220,20 +166,50 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
- if (RemoteTargetInterfaceTabControl.SelectedItem is RemoteInterfaceInfo selected)
|
|
|
+ if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用,刷新会丢失未应用内容。是否继续刷新?", "确认刷新配置"))
|
|
|
{
|
|
|
- await LoadRemoteInterfaceConfigAsync(selected.SystemName);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ SetBusyState(true, "正在刷新全部接口配置...");
|
|
|
+ try
|
|
|
+ {
|
|
|
+ foreach (var editor in _interfaces)
|
|
|
+ {
|
|
|
+ await LoadRemoteInterfaceConfigAsync(editor);
|
|
|
+ }
|
|
|
+
|
|
|
+ _configValidated = false;
|
|
|
+ _configDirty = false;
|
|
|
+ ShowStatusMessage("已刷新全部接口配置。");
|
|
|
+ }
|
|
|
+ finally
|
|
|
+ {
|
|
|
+ SetBusyState(false);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
+ private void DeviceDetailsWindow_OnClosing(object? sender, CancelEventArgs e)
|
|
|
{
|
|
|
- if (RemoteTargetInterfaceTabControl.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
+ if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用。是否关闭窗口?", "确认关闭窗口"))
|
|
|
{
|
|
|
- return;
|
|
|
+ e.Cancel = true;
|
|
|
}
|
|
|
+ }
|
|
|
|
|
|
- var request = BuildConfigRequest(selected.SystemName);
|
|
|
+ private bool ConfirmDiscardPendingChanges(string message, string title)
|
|
|
+ {
|
|
|
+ if (!_configDirty)
|
|
|
+ {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return MessageBox.Show(this, message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning) == MessageBoxResult.OK;
|
|
|
+ }
|
|
|
+
|
|
|
+ private async void ValidateConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
+ {
|
|
|
+ var request = BuildConfigRequests();
|
|
|
if (request is null)
|
|
|
{
|
|
|
return;
|
|
|
@@ -242,13 +218,13 @@ public partial class DeviceDetailsWindow : Window
|
|
|
SetBusyState(true, "正在校验配置,请稍候...");
|
|
|
try
|
|
|
{
|
|
|
- var result = await _serverApiService.ValidateInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
|
|
|
+ var result = await _serverApiService.ValidateInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
|
|
|
_configValidated = result.Success && result.Data?.Valid == true;
|
|
|
if (result.Data is not null)
|
|
|
{
|
|
|
var warnings = result.Data.Warnings.Count > 0 ? $" 警告:{string.Join(";", result.Data.Warnings)}" : string.Empty;
|
|
|
var errors = result.Data.Errors.Count > 0 ? $" 错误:{string.Join(";", result.Data.Errors)}" : string.Empty;
|
|
|
- ShowStatusMessage(_configValidated ? $"校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}");
|
|
|
+ ShowStatusMessage(_configValidated ? $"全部接口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}");
|
|
|
}
|
|
|
else
|
|
|
{
|
|
|
@@ -265,23 +241,13 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private async void ApplyConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
- if (RemoteTargetInterfaceTabControl.SelectedItem is not RemoteInterfaceInfo selected)
|
|
|
- {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- var request = BuildConfigRequest(selected.SystemName);
|
|
|
+ var request = BuildConfigRequests();
|
|
|
if (request is null)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- var confirmMessage = $"将要把以下配置应用到接口 {selected.SystemName}:\n\n" +
|
|
|
- $"模式:{(request.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
|
|
|
- $"IP:{(request.Dhcp4 ? "自动获取" : FormatAddresses(request.Addresses))}\n" +
|
|
|
- $"路由:{(request.Dhcp4 ? "自动获取" : FormatRoutes(request.Routes))}\n" +
|
|
|
- $"DNS:{(request.Dns.Count == 0 ? "无" : string.Join(", ", request.Dns))}\n\n" +
|
|
|
- "请确认是否继续。";
|
|
|
+ var confirmMessage = "将要一次性应用以下接口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。";
|
|
|
if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
|
|
|
{
|
|
|
return;
|
|
|
@@ -290,7 +256,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
SetBusyState(true, "正在提交并应用配置,请稍候...");
|
|
|
try
|
|
|
{
|
|
|
- var applyResult = await _serverApiService.ApplyInterfaceConfigAsync(_baseAddress, _password, _localIPv4, request);
|
|
|
+ var applyResult = await _serverApiService.ApplyInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
|
|
|
if (!applyResult.Success || applyResult.Data is null)
|
|
|
{
|
|
|
ShowStatusMessage($"提交配置任务失败:{applyResult.Message}");
|
|
|
@@ -349,9 +315,9 @@ public partial class DeviceDetailsWindow : Window
|
|
|
if (task.Status is "success" or "failed" or "rolled_back")
|
|
|
{
|
|
|
ShowTaskCompletionDialog(task);
|
|
|
- if (RemoteTargetInterfaceTabControl.SelectedItem is RemoteInterfaceInfo selected)
|
|
|
+ foreach (var editor in _interfaces)
|
|
|
{
|
|
|
- await LoadRemoteInterfaceConfigAsync(selected.SystemName);
|
|
|
+ await LoadRemoteInterfaceConfigAsync(editor);
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
@@ -481,37 +447,60 @@ public partial class DeviceDetailsWindow : Window
|
|
|
ShowStatusMessage($"{title}任务已提交:{result.Data.TaskId}。命令已发出,设备可能立即断开。");
|
|
|
}
|
|
|
|
|
|
- private RemoteInterfaceConfig? BuildConfigRequest(string interfaceName)
|
|
|
+ private RemoteInterfaceConfig[]? BuildConfigRequests()
|
|
|
{
|
|
|
CommitConfigEdits();
|
|
|
- var dhcp4 = Dhcp4CheckBox.IsChecked == true;
|
|
|
+ var result = new List<RemoteInterfaceConfig>();
|
|
|
+ foreach (var editor in _interfaces)
|
|
|
+ {
|
|
|
+ var request = BuildConfigRequest(editor);
|
|
|
+ if (request is null)
|
|
|
+ {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ result.Add(request);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (result.Count == 0)
|
|
|
+ {
|
|
|
+ ShowStatusMessage("接口配置不能为空。");
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return result.ToArray();
|
|
|
+ }
|
|
|
+
|
|
|
+ private RemoteInterfaceConfig? BuildConfigRequest(InterfaceEditor editor)
|
|
|
+ {
|
|
|
+ var dhcp4 = editor.Dhcp4;
|
|
|
var addresses = Array.Empty<RemoteInterfaceAddressConfig>();
|
|
|
var routes = Array.Empty<RemoteInterfaceRouteConfig>();
|
|
|
if (!dhcp4)
|
|
|
{
|
|
|
- if (_addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
|
|
|
+ if (editor.Addresses.All(item => string.IsNullOrWhiteSpace(item.IP) && string.IsNullOrWhiteSpace(item.Mask)))
|
|
|
{
|
|
|
- ShowStatusMessage("IP 地址不能为空,至少需要填写一行地址。");
|
|
|
+ ShowStatusMessage($"{editor.SystemName}:IP 地址不能为空,至少需要填写一行地址。");
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- if (!TryBuildAddresses(out addresses, out var addressError))
|
|
|
+ if (!TryBuildAddresses(editor, out addresses, out var addressError))
|
|
|
{
|
|
|
- ShowStatusMessage(addressError);
|
|
|
+ ShowStatusMessage($"{editor.SystemName}:{addressError}");
|
|
|
return null;
|
|
|
}
|
|
|
|
|
|
- if (!TryBuildRoutes(out routes, out var routeError))
|
|
|
+ if (!TryBuildRoutes(editor, out routes, out var routeError))
|
|
|
{
|
|
|
- ShowStatusMessage(routeError);
|
|
|
+ ShowStatusMessage($"{editor.SystemName}:{routeError}");
|
|
|
return null;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- var dns = _dns.Select(item => item.Address.Trim()).Where(item => item != string.Empty).ToArray();
|
|
|
+ var dns = editor.Dns.Select(item => item.Address.Trim()).Where(item => item != string.Empty).ToArray();
|
|
|
return new RemoteInterfaceConfig
|
|
|
{
|
|
|
- Interface = interfaceName,
|
|
|
+ Interface = editor.SystemName,
|
|
|
Dhcp4 = dhcp4,
|
|
|
Addresses = dhcp4 ? Array.Empty<RemoteInterfaceAddressConfig>() : addresses,
|
|
|
Routes = dhcp4 ? Array.Empty<RemoteInterfaceRouteConfig>() : routes,
|
|
|
@@ -521,12 +510,21 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private void CommitConfigEdits()
|
|
|
{
|
|
|
- AddressesDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
- AddressesDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
- RoutesDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
- RoutesDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
- DnsDataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
- DnsDataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
+ CommitDataGridEdits(InterfacesItemsControl);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void CommitDataGridEdits(DependencyObject root)
|
|
|
+ {
|
|
|
+ for (var i = 0; i < VisualTreeHelper.GetChildrenCount(root); i++)
|
|
|
+ {
|
|
|
+ var child = VisualTreeHelper.GetChild(root, i);
|
|
|
+ if (child is DataGrid dataGrid)
|
|
|
+ {
|
|
|
+ dataGrid.CommitEdit(DataGridEditingUnit.Cell, true);
|
|
|
+ dataGrid.CommitEdit(DataGridEditingUnit.Row, true);
|
|
|
+ }
|
|
|
+ CommitDataGridEdits(child);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
private static string FormatCurrentIp(RemoteInterfaceConfig config)
|
|
|
@@ -550,7 +548,17 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return routes.Count == 0 ? "无" : string.Join(Environment.NewLine, routes.Select(item => $"{item.To} via {item.Via}"));
|
|
|
}
|
|
|
|
|
|
- private static EditableRoute CreateEditableRoute(RemoteInterfaceRouteConfig route)
|
|
|
+ private static string FormatConfigSummary(IReadOnlyList<RemoteInterfaceConfig> configs)
|
|
|
+ {
|
|
|
+ return string.Join(Environment.NewLine + Environment.NewLine, configs.Select(item =>
|
|
|
+ $"接口:{item.Interface}\n" +
|
|
|
+ $"模式:{(item.Dhcp4 ? "DHCP 自动获取" : "静态 IPv4")}\n" +
|
|
|
+ $"IP:{(item.Dhcp4 ? "自动获取" : FormatAddresses(item.Addresses))}\n" +
|
|
|
+ $"路由:{(item.Dhcp4 ? "自动获取" : FormatRoutes(item.Routes))}\n" +
|
|
|
+ $"DNS:{(item.Dns.Count == 0 ? "无" : string.Join(", ", item.Dns))}"));
|
|
|
+ }
|
|
|
+
|
|
|
+ private static EditableRoute CreateEditableRoute(InterfaceEditor owner, RemoteInterfaceRouteConfig route)
|
|
|
{
|
|
|
var to = route.To.Trim();
|
|
|
var mask = string.Empty;
|
|
|
@@ -564,13 +572,13 @@ public partial class DeviceDetailsWindow : Window
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return new EditableRoute { To = to, Mask = mask, Via = route.Via };
|
|
|
+ return new EditableRoute(owner) { To = to, Mask = mask, Via = route.Via };
|
|
|
}
|
|
|
|
|
|
- private bool TryBuildAddresses(out RemoteInterfaceAddressConfig[] addresses, out string error)
|
|
|
+ private bool TryBuildAddresses(InterfaceEditor editor, out RemoteInterfaceAddressConfig[] addresses, out string error)
|
|
|
{
|
|
|
var result = new List<RemoteInterfaceAddressConfig>();
|
|
|
- foreach (var row in _addresses)
|
|
|
+ foreach (var row in editor.Addresses)
|
|
|
{
|
|
|
var ip = row.IP.Trim();
|
|
|
var maskText = row.Mask.Trim();
|
|
|
@@ -613,12 +621,12 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return addresses.Length > 0;
|
|
|
}
|
|
|
|
|
|
- private bool TryBuildRoutes(out RemoteInterfaceRouteConfig[] routes, out string error)
|
|
|
+ private bool TryBuildRoutes(InterfaceEditor editor, out RemoteInterfaceRouteConfig[] routes, out string error)
|
|
|
{
|
|
|
var result = new List<RemoteInterfaceRouteConfig>();
|
|
|
- if (DefaultGatewayCheckBox.IsChecked == true)
|
|
|
+ if (editor.DefaultGatewayEnabled)
|
|
|
{
|
|
|
- var gateway = DefaultGatewayTextBox.Text.Trim();
|
|
|
+ var gateway = editor.DefaultGateway.Trim();
|
|
|
if (gateway == string.Empty)
|
|
|
{
|
|
|
routes = [];
|
|
|
@@ -627,9 +635,9 @@ public partial class DeviceDetailsWindow : Window
|
|
|
}
|
|
|
result.Add(new RemoteInterfaceRouteConfig { To = "default", Via = gateway });
|
|
|
}
|
|
|
- if (CustomRoutesCheckBox.IsChecked == true)
|
|
|
+ if (editor.CustomRoutesEnabled)
|
|
|
{
|
|
|
- foreach (var row in _routes)
|
|
|
+ foreach (var row in editor.Routes)
|
|
|
{
|
|
|
var to = row.To.Trim();
|
|
|
var maskText = row.Mask.Trim();
|
|
|
@@ -865,21 +873,49 @@ public partial class DeviceDetailsWindow : Window
|
|
|
MarkConfigChanged("配置内容已变更,请重新点击“2. 校验配置”。");
|
|
|
}
|
|
|
|
|
|
+ private void DataGrid_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
|
|
+ {
|
|
|
+ if (sender is not DataGrid dataGrid)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ e.Handled = true;
|
|
|
+ var eventArg = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
|
|
|
+ {
|
|
|
+ RoutedEvent = MouseWheelEvent,
|
|
|
+ Source = dataGrid,
|
|
|
+ };
|
|
|
+ ContentScrollViewer.RaiseEvent(eventArg);
|
|
|
+ }
|
|
|
+
|
|
|
private void AddAddressButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
- _addresses.Add(new EditableAddress { Mask = "255.255.255.0" });
|
|
|
+ if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ editor.Addresses.Add(new EditableAddress(editor) { Mask = "255.255.255.0" });
|
|
|
MarkConfigChanged("已添加 IP 地址,请填写后重新校验配置。");
|
|
|
}
|
|
|
|
|
|
private void AddRouteButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
- _routes.Add(new EditableRoute());
|
|
|
+ if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ editor.Routes.Add(new EditableRoute(editor));
|
|
|
MarkConfigChanged("已添加路由,请填写后重新校验配置。");
|
|
|
}
|
|
|
|
|
|
private void AddDnsButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
- _dns.Add(new EditableDns());
|
|
|
+ if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ editor.Dns.Add(new EditableDns(editor));
|
|
|
MarkConfigChanged("已添加 DNS,请填写后重新校验配置。");
|
|
|
}
|
|
|
|
|
|
@@ -889,7 +925,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
- _addresses.Remove(address);
|
|
|
+ address.Owner.Addresses.Remove(address);
|
|
|
MarkConfigChanged("已删除 IP 地址,请重新校验配置。");
|
|
|
}
|
|
|
|
|
|
@@ -897,7 +933,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
if ((sender as FrameworkElement)?.DataContext is EditableRoute route)
|
|
|
{
|
|
|
- _routes.Remove(route);
|
|
|
+ route.Owner.Routes.Remove(route);
|
|
|
MarkConfigChanged("已删除路由,请重新校验配置。");
|
|
|
}
|
|
|
}
|
|
|
@@ -906,7 +942,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
if ((sender as FrameworkElement)?.DataContext is EditableDns dns)
|
|
|
{
|
|
|
- _dns.Remove(dns);
|
|
|
+ dns.Owner.Dns.Remove(dns);
|
|
|
MarkConfigChanged("已删除 DNS,请重新校验配置。");
|
|
|
}
|
|
|
}
|
|
|
@@ -1050,26 +1086,11 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private void UpdateButtonStates()
|
|
|
{
|
|
|
- var hasSelectedInterface = RemoteTargetInterfaceTabControl.SelectedItem is RemoteInterfaceInfo;
|
|
|
- var canEdit = !_isBusy && hasSelectedInterface;
|
|
|
- var canEditStatic = canEdit && Dhcp4CheckBox.IsChecked != true;
|
|
|
- var canEditGateway = canEditStatic && DefaultGatewayCheckBox.IsChecked == true;
|
|
|
- var canEditCustomRoutes = canEditStatic && CustomRoutesCheckBox.IsChecked == true;
|
|
|
-
|
|
|
- RemoteTargetInterfaceTabControl.IsEnabled = !_isBusy && RemoteTargetInterfaceTabControl.Items.Count > 0;
|
|
|
- ReloadInterfaceConfigButton.IsEnabled = canEdit;
|
|
|
- ValidateConfigButton.IsEnabled = canEdit;
|
|
|
- ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasSelectedInterface;
|
|
|
- Dhcp4CheckBox.IsEnabled = canEdit;
|
|
|
- AddressesDataGrid.IsEnabled = canEditStatic;
|
|
|
- DefaultGatewayCheckBox.IsEnabled = canEditStatic;
|
|
|
- DefaultGatewayTextBox.IsEnabled = canEditGateway;
|
|
|
- CustomRoutesCheckBox.IsEnabled = canEditStatic;
|
|
|
- RoutesDataGrid.IsEnabled = canEditCustomRoutes;
|
|
|
- DnsDataGrid.IsEnabled = canEdit;
|
|
|
- AddAddressButton.IsEnabled = canEditStatic;
|
|
|
- AddRouteButton.IsEnabled = canEditCustomRoutes;
|
|
|
- AddDnsButton.IsEnabled = canEdit;
|
|
|
+ var hasInterfaces = _interfaces.Count > 0;
|
|
|
+ InterfacesItemsControl.IsEnabled = !_isBusy && hasInterfaces;
|
|
|
+ ReloadInterfaceConfigButton.IsEnabled = !_isBusy && hasInterfaces;
|
|
|
+ ValidateConfigButton.IsEnabled = !_isBusy && hasInterfaces;
|
|
|
+ ApplyConfigButton.IsEnabled = !_isBusy && _configValidated && hasInterfaces;
|
|
|
RebootButton.IsEnabled = !_isBusy;
|
|
|
ShutdownButton.IsEnabled = !_isBusy;
|
|
|
}
|
|
|
@@ -1082,11 +1103,74 @@ public partial class DeviceDetailsWindow : Window
|
|
|
UpdateButtonStates();
|
|
|
}
|
|
|
|
|
|
+ private sealed class InterfaceEditor : INotifyPropertyChanged
|
|
|
+ {
|
|
|
+ private bool _dhcp4;
|
|
|
+ private bool _defaultGatewayEnabled;
|
|
|
+ private bool _customRoutesEnabled;
|
|
|
+ private string _defaultGateway = string.Empty;
|
|
|
+
|
|
|
+ public InterfaceEditor(RemoteInterfaceInfo info)
|
|
|
+ {
|
|
|
+ SystemName = info.SystemName;
|
|
|
+ StatusSummary = info.StatusSummary;
|
|
|
+ }
|
|
|
+
|
|
|
+ public string SystemName { get; }
|
|
|
+ public string StatusSummary { get; }
|
|
|
+ public ObservableCollection<EditableAddress> Addresses { get; } = [];
|
|
|
+ public ObservableCollection<EditableRoute> Routes { get; } = [];
|
|
|
+ public ObservableCollection<EditableDns> Dns { get; } = [];
|
|
|
+
|
|
|
+ public bool Dhcp4
|
|
|
+ {
|
|
|
+ get => _dhcp4;
|
|
|
+ set => SetField(ref _dhcp4, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool DefaultGatewayEnabled
|
|
|
+ {
|
|
|
+ get => _defaultGatewayEnabled;
|
|
|
+ set => SetField(ref _defaultGatewayEnabled, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public bool CustomRoutesEnabled
|
|
|
+ {
|
|
|
+ get => _customRoutesEnabled;
|
|
|
+ set => SetField(ref _customRoutesEnabled, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public string DefaultGateway
|
|
|
+ {
|
|
|
+ get => _defaultGateway;
|
|
|
+ set => SetField(ref _defaultGateway, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
+
|
|
|
+ private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
|
|
+ {
|
|
|
+ if (EqualityComparer<T>.Default.Equals(field, value))
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ field = value;
|
|
|
+ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
private sealed class EditableAddress : INotifyPropertyChanged
|
|
|
{
|
|
|
private string _ip = string.Empty;
|
|
|
private string _mask = string.Empty;
|
|
|
|
|
|
+ public EditableAddress(InterfaceEditor owner)
|
|
|
+ {
|
|
|
+ Owner = owner;
|
|
|
+ }
|
|
|
+
|
|
|
+ public InterfaceEditor Owner { get; }
|
|
|
+
|
|
|
public string IP
|
|
|
{
|
|
|
get => _ip;
|
|
|
@@ -1114,6 +1198,12 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private sealed class EditableRoute
|
|
|
{
|
|
|
+ public EditableRoute(InterfaceEditor owner)
|
|
|
+ {
|
|
|
+ Owner = owner;
|
|
|
+ }
|
|
|
+
|
|
|
+ public InterfaceEditor Owner { get; }
|
|
|
public string To { get; set; } = string.Empty;
|
|
|
public string Mask { get; set; } = string.Empty;
|
|
|
public string Via { get; set; } = string.Empty;
|
|
|
@@ -1121,6 +1211,12 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private sealed class EditableDns
|
|
|
{
|
|
|
+ public EditableDns(InterfaceEditor owner)
|
|
|
+ {
|
|
|
+ Owner = owner;
|
|
|
+ }
|
|
|
+
|
|
|
+ public InterfaceEditor Owner { get; }
|
|
|
public string Address { get; set; } = string.Empty;
|
|
|
}
|
|
|
}
|