|
|
@@ -14,7 +14,6 @@ namespace NetworkTool.Client;
|
|
|
|
|
|
public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
- private const int ApplyConfirmationTimeoutSeconds = 20;
|
|
|
private readonly ServerApiService _serverApiService = new();
|
|
|
private readonly ObservableCollection<InterfaceEditor> _interfaces = [];
|
|
|
private readonly string _baseAddress;
|
|
|
@@ -168,7 +167,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private async void ReloadInterfaceConfigButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
- if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用,重新获取会丢失未应用内容。是否继续重新获取?", "确认重新获取配置"))
|
|
|
+ if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未保存,重新获取会丢失未保存内容。是否继续重新获取?", "确认重新获取配置"))
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
@@ -194,7 +193,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private void DeviceDetailsWindow_OnClosing(object? sender, CancelEventArgs e)
|
|
|
{
|
|
|
- if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未应用。是否关闭窗口?", "确认关闭窗口"))
|
|
|
+ if (!ConfirmDiscardPendingChanges("当前配置已修改但尚未保存。是否关闭窗口?", "确认关闭窗口"))
|
|
|
{
|
|
|
e.Cancel = true;
|
|
|
}
|
|
|
@@ -227,9 +226,9 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
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;
|
|
|
- SetConfigStateMessage(_configValidated ? "配置已校验通过,可以应用。" : "配置校验未通过,请修正后重新校验。", !_configValidated);
|
|
|
+ SetConfigStateMessage(_configValidated ? "配置已校验通过,可以保存。" : "配置校验未通过,请修正后重新校验。", !_configValidated);
|
|
|
ShowStatusMessage(
|
|
|
- _configValidated ? $"全部网口校验通过,可应用配置。{warnings}" : $"校验失败。{errors}{warnings}",
|
|
|
+ _configValidated ? $"全部网口校验通过,可保存配置。{warnings}" : $"校验失败。{errors}{warnings}",
|
|
|
_configValidated ? StatusMessageType.Success : StatusMessageType.Error);
|
|
|
}
|
|
|
else
|
|
|
@@ -256,14 +255,14 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
var changeSummary = FormatChangeSummary();
|
|
|
var confirmMessage = string.IsNullOrWhiteSpace(changeSummary)
|
|
|
- ? "将要一次性应用以下网口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。"
|
|
|
- : "将要一次性应用以下已修改配置:\n\n" + changeSummary + "\n\n完整目标配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。";
|
|
|
- if (MessageBox.Show(this, confirmMessage, "确认应用配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
|
|
|
+ ? "将要一次性保存以下网口配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。"
|
|
|
+ : "将要一次性保存以下已修改配置:\n\n" + changeSummary + "\n\n完整目标配置:\n\n" + FormatConfigSummary(request) + "\n\n请确认是否继续。";
|
|
|
+ if (MessageBox.Show(this, confirmMessage, "确认保存配置", MessageBoxButton.OKCancel, MessageBoxImage.Question) != MessageBoxResult.OK)
|
|
|
{
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- SetBusyState(true, "正在提交并应用配置,请稍候...");
|
|
|
+ SetBusyState(true, "正在提交并保存配置,请稍候...");
|
|
|
try
|
|
|
{
|
|
|
var applyResult = await _serverApiService.ApplyInterfaceConfigsAsync(_baseAddress, _password, _localIPv4, request);
|
|
|
@@ -273,7 +272,8 @@ public partial class DeviceDetailsWindow : Window
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- ShowStatusMessage("配置任务已提交,正在应用并等待连通确认...", StatusMessageType.Info);
|
|
|
+ SetBusyState(true, "已提交,等待完成...");
|
|
|
+ ShowStatusMessage("配置任务已提交,正在保存并等待连通确认...", StatusMessageType.Info);
|
|
|
await PollTaskAsync(applyResult.Data.TaskId);
|
|
|
}
|
|
|
finally
|
|
|
@@ -315,37 +315,31 @@ public partial class DeviceDetailsWindow : Window
|
|
|
if (task.Status == "running" && task.Step == "confirming" && !confirmationRequested)
|
|
|
{
|
|
|
confirmationRequested = true;
|
|
|
- var confirm = ShowApplyConfirmationDialog(ApplyConfirmationTimeoutSeconds);
|
|
|
- if (confirm)
|
|
|
- {
|
|
|
- var confirmResult = await _serverApiService.ConfirmApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
|
|
|
- ShowStatusMessage(
|
|
|
- confirmResult.Success ? "已发送保留配置确认。" : $"发送确认失败:{confirmResult.Message}",
|
|
|
- confirmResult.Success ? StatusMessageType.Success : StatusMessageType.Error);
|
|
|
- }
|
|
|
- else
|
|
|
- {
|
|
|
- var cancelResult = await _serverApiService.CancelApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
|
|
|
- ShowStatusMessage(
|
|
|
- cancelResult.Success ? "已取消保留配置,正在回滚。" : $"发送取消失败:{cancelResult.Message}",
|
|
|
- cancelResult.Success ? StatusMessageType.Warning : StatusMessageType.Error);
|
|
|
- }
|
|
|
+ var confirmResult = await _serverApiService.ConfirmApplyTaskAsync(_baseAddress, _password, _localIPv4, taskId);
|
|
|
+ ShowStatusMessage(
|
|
|
+ confirmResult.Success ? "设备连接已恢复,已自动确认保留配置。" : $"自动确认保留配置失败:{confirmResult.Message}",
|
|
|
+ confirmResult.Success ? StatusMessageType.Success : StatusMessageType.Error);
|
|
|
}
|
|
|
|
|
|
if (task.Status is "success" or "failed" or "rolled_back")
|
|
|
{
|
|
|
+ if (task.Status is "success" or "rolled_back")
|
|
|
+ {
|
|
|
+ SetBusyState(true, string.Empty);
|
|
|
+ }
|
|
|
+
|
|
|
if (task.Status == "success")
|
|
|
{
|
|
|
_configValidated = false;
|
|
|
_configDirty = false;
|
|
|
- SetConfigStateMessage("配置已应用,当前显示为设备最新配置。", false);
|
|
|
+ SetConfigStateMessage("配置已保存,当前显示为设备最新配置。", false);
|
|
|
+ foreach (var editor in _interfaces)
|
|
|
+ {
|
|
|
+ await LoadRemoteInterfaceConfigAsync(editor);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
ShowTaskCompletionDialog(task);
|
|
|
- foreach (var editor in _interfaces)
|
|
|
- {
|
|
|
- await LoadRemoteInterfaceConfigAsync(editor);
|
|
|
- }
|
|
|
|
|
|
return;
|
|
|
}
|
|
|
@@ -354,95 +348,6 @@ public partial class DeviceDetailsWindow : Window
|
|
|
ShowStatusMessage($"任务 {taskId} 轮询超时,请稍后手动刷新。", StatusMessageType.Warning);
|
|
|
}
|
|
|
|
|
|
- private bool ShowApplyConfirmationDialog(int timeoutSeconds)
|
|
|
- {
|
|
|
- var remaining = timeoutSeconds;
|
|
|
- var result = false;
|
|
|
- var messageTextBlock = new TextBlock
|
|
|
- {
|
|
|
- Width = 420,
|
|
|
- TextWrapping = TextWrapping.Wrap,
|
|
|
- FontSize = 13,
|
|
|
- Foreground = Brushes.Black,
|
|
|
- };
|
|
|
- var confirmButton = new Button
|
|
|
- {
|
|
|
- MinWidth = 88,
|
|
|
- MinHeight = 32,
|
|
|
- Margin = new Thickness(0, 0, 10, 0),
|
|
|
- Content = "保留",
|
|
|
- IsDefault = true,
|
|
|
- };
|
|
|
- confirmButton.Style = (Style)FindResource("PrimaryButtonStyle");
|
|
|
- var cancelButton = new Button
|
|
|
- {
|
|
|
- MinWidth = 88,
|
|
|
- MinHeight = 32,
|
|
|
- Content = "回滚",
|
|
|
- IsCancel = true,
|
|
|
- };
|
|
|
- var dialog = new Window
|
|
|
- {
|
|
|
- Title = "确认保留网络配置",
|
|
|
- Owner = this,
|
|
|
- WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
|
- ResizeMode = ResizeMode.NoResize,
|
|
|
- SizeToContent = SizeToContent.WidthAndHeight,
|
|
|
- Content = new StackPanel
|
|
|
- {
|
|
|
- Margin = new Thickness(18),
|
|
|
- Children =
|
|
|
- {
|
|
|
- messageTextBlock,
|
|
|
- new StackPanel
|
|
|
- {
|
|
|
- Margin = new Thickness(0, 18, 0, 0),
|
|
|
- HorizontalAlignment = HorizontalAlignment.Right,
|
|
|
- Orientation = Orientation.Horizontal,
|
|
|
- Children = { confirmButton, cancelButton },
|
|
|
- },
|
|
|
- },
|
|
|
- },
|
|
|
- };
|
|
|
-
|
|
|
- void UpdateMessage()
|
|
|
- {
|
|
|
- messageTextBlock.Text = "当前客户端仍可连接到设备。是否确认保留这次网络配置?\n\n超时或取消时,Linux 端会自动回滚。";
|
|
|
- confirmButton.Content = $"保留({remaining}秒)";
|
|
|
- }
|
|
|
-
|
|
|
- var timer = new System.Windows.Threading.DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
|
|
- timer.Tick += (_, _) =>
|
|
|
- {
|
|
|
- remaining--;
|
|
|
- if (remaining <= 0)
|
|
|
- {
|
|
|
- timer.Stop();
|
|
|
- dialog.DialogResult = false;
|
|
|
- dialog.Close();
|
|
|
- return;
|
|
|
- }
|
|
|
- UpdateMessage();
|
|
|
- };
|
|
|
- confirmButton.Click += (_, _) =>
|
|
|
- {
|
|
|
- result = true;
|
|
|
- dialog.DialogResult = true;
|
|
|
- dialog.Close();
|
|
|
- };
|
|
|
- cancelButton.Click += (_, _) =>
|
|
|
- {
|
|
|
- dialog.DialogResult = false;
|
|
|
- dialog.Close();
|
|
|
- };
|
|
|
- dialog.Closed += (_, _) => timer.Stop();
|
|
|
-
|
|
|
- UpdateMessage();
|
|
|
- timer.Start();
|
|
|
- dialog.ShowDialog();
|
|
|
- return result;
|
|
|
- }
|
|
|
-
|
|
|
private async void RebootButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
await ExecuteSystemActionAsync(
|
|
|
@@ -949,6 +854,23 @@ public partial class DeviceDetailsWindow : Window
|
|
|
ContentScrollViewer.RaiseEvent(eventArg);
|
|
|
}
|
|
|
|
|
|
+ private void ConfigGrid_OnLostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (sender is not DataGrid dataGrid)
|
|
|
+ {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ Dispatcher.BeginInvoke(() =>
|
|
|
+ {
|
|
|
+ if (!dataGrid.IsKeyboardFocusWithin)
|
|
|
+ {
|
|
|
+ dataGrid.UnselectAllCells();
|
|
|
+ dataGrid.UnselectAll();
|
|
|
+ }
|
|
|
+ }, System.Windows.Threading.DispatcherPriority.Input);
|
|
|
+ }
|
|
|
+
|
|
|
private void AddAddressButton_OnClick(object sender, RoutedEventArgs e)
|
|
|
{
|
|
|
if ((sender as FrameworkElement)?.DataContext is not InterfaceEditor editor)
|
|
|
@@ -1017,7 +939,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
_configValidated = false;
|
|
|
RefreshChangeState();
|
|
|
SetConfigStateMessage(
|
|
|
- _configDirty ? $"配置已修改:{FormatChangedFields()}。需重新校验后才能应用。" : "配置未修改。",
|
|
|
+ _configDirty ? "配置已修改,需重新校验后才能保存。" : "配置未修改。",
|
|
|
_configDirty);
|
|
|
UpdateButtonStates();
|
|
|
}
|
|
|
@@ -1134,16 +1056,16 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
return task.Status switch
|
|
|
{
|
|
|
- "success" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功应用。" : task.Detail, StatusMessageType.Success),
|
|
|
- "failed" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败。" : task.Detail, StatusMessageType.Error),
|
|
|
- "rolled_back" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置应用失败,已自动回滚。" : task.Detail, StatusMessageType.Error),
|
|
|
+ "success" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置已成功保存。" : task.Detail, StatusMessageType.Success),
|
|
|
+ "failed" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置保存失败。" : task.Detail, StatusMessageType.Error),
|
|
|
+ "rolled_back" => (string.IsNullOrWhiteSpace(task.Detail) ? "配置保存失败,已自动回滚。" : task.Detail, StatusMessageType.Error),
|
|
|
_ => task.Step switch
|
|
|
{
|
|
|
"validating" => ("正在校验配置...", StatusMessageType.Info),
|
|
|
"writing_netplan" => ("正在写入 Linux 网络配置...", StatusMessageType.Info),
|
|
|
- "applying" => ("正在应用 Linux 网络配置...", StatusMessageType.Info),
|
|
|
- "confirming" => (string.IsNullOrWhiteSpace(task.Detail) ? "等待确认保留配置..." : task.Detail, StatusMessageType.Warning),
|
|
|
- "rolling_back" => ("配置应用失败,正在自动回滚...", StatusMessageType.Warning),
|
|
|
+ "applying" => ("正在保存 Linux 网络配置...", StatusMessageType.Info),
|
|
|
+ "confirming" => ("设备连接已恢复,正在自动确认保留配置...", StatusMessageType.Info),
|
|
|
+ "rolling_back" => ("配置保存失败,正在自动回滚...", StatusMessageType.Warning),
|
|
|
_ => (string.IsNullOrWhiteSpace(task.Detail) ? "正在处理,请稍候..." : task.Detail, StatusMessageType.Info),
|
|
|
}
|
|
|
};
|
|
|
@@ -1152,7 +1074,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
private void ShowTaskCompletionDialog(RemoteTaskResult task)
|
|
|
{
|
|
|
var (message, _) = FormatTaskStatusMessage(task);
|
|
|
- var title = task.Status == "success" ? "应用配置成功" : "应用配置失败";
|
|
|
+ var title = task.Status == "success" ? "保存配置成功" : "保存配置失败";
|
|
|
var image = task.Status == "success" ? MessageBoxImage.Information : MessageBoxImage.Warning;
|
|
|
MessageBox.Show(this, message, title, MessageBoxButton.OK, image);
|
|
|
}
|
|
|
@@ -1172,6 +1094,7 @@ public partial class DeviceDetailsWindow : Window
|
|
|
{
|
|
|
_isBusy = isBusy;
|
|
|
BusyOverlay.Visibility = isBusy ? Visibility.Visible : Visibility.Collapsed;
|
|
|
+ BusyMessageTextBlock.Visibility = isBusy && message == string.Empty ? Visibility.Collapsed : Visibility.Visible;
|
|
|
BusyMessageTextBlock.Text = string.IsNullOrWhiteSpace(message) ? "正在处理,请稍候..." : message;
|
|
|
UpdateButtonStates();
|
|
|
}
|
|
|
@@ -1287,10 +1210,12 @@ public partial class DeviceDetailsWindow : Window
|
|
|
private string[] GetGatewayKeys()
|
|
|
{
|
|
|
var keys = new List<string>();
|
|
|
- if (DefaultGatewayEnabled || !string.IsNullOrWhiteSpace(DefaultGateway))
|
|
|
+ keys.Add($"default enabled {DefaultGatewayEnabled}");
|
|
|
+ if (!string.IsNullOrWhiteSpace(DefaultGateway))
|
|
|
{
|
|
|
keys.Add($"default via {DefaultGateway.Trim()}");
|
|
|
}
|
|
|
+ keys.Add($"custom routes enabled {CustomRoutesEnabled}");
|
|
|
if (CustomRoutesEnabled)
|
|
|
{
|
|
|
keys.AddRange(Routes
|
|
|
@@ -1318,7 +1243,25 @@ public partial class DeviceDetailsWindow : Window
|
|
|
|
|
|
private static string FormatGatewaySummary(bool dhcp4, IReadOnlyList<string> keys)
|
|
|
{
|
|
|
- return dhcp4 ? "自动获取" : FormatKeys(keys);
|
|
|
+ if (dhcp4)
|
|
|
+ {
|
|
|
+ return "自动获取";
|
|
|
+ }
|
|
|
+
|
|
|
+ var defaultGatewayEnabled = keys.Contains("default enabled True");
|
|
|
+ var customRoutesEnabled = keys.Contains("custom routes enabled True");
|
|
|
+ var displayItems = new List<string>();
|
|
|
+ if (defaultGatewayEnabled)
|
|
|
+ {
|
|
|
+ var defaultGateway = keys.FirstOrDefault(item => item.StartsWith("default via ", StringComparison.Ordinal));
|
|
|
+ displayItems.Add(defaultGateway is null ? "默认网关已启用(未填写)" : $"默认网关:{defaultGateway[12..]}");
|
|
|
+ }
|
|
|
+ if (customRoutesEnabled)
|
|
|
+ {
|
|
|
+ displayItems.AddRange(keys.Where(item => !item.StartsWith("default ", StringComparison.Ordinal) && !item.StartsWith("custom routes enabled ", StringComparison.Ordinal)));
|
|
|
+ }
|
|
|
+
|
|
|
+ return FormatKeys(displayItems);
|
|
|
}
|
|
|
|
|
|
private void SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = "")
|
|
|
@@ -1447,3 +1390,18 @@ public partial class DeviceDetailsWindow : Window
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+internal static class ApplyConfirmationDialogElementExtensions
|
|
|
+{
|
|
|
+ public static T SetGridRow<T>(this T element, int row) where T : UIElement
|
|
|
+ {
|
|
|
+ Grid.SetRow(element, row);
|
|
|
+ return element;
|
|
|
+ }
|
|
|
+
|
|
|
+ public static T SetMargin<T>(this T element, Thickness margin) where T : FrameworkElement
|
|
|
+ {
|
|
|
+ element.Margin = margin;
|
|
|
+ return element;
|
|
|
+ }
|
|
|
+}
|