diff --git a/src/MikrotikInstaller.App/App.xaml b/src/MikrotikInstaller.App/App.xaml index 6b1955b..ece6aa3 100644 --- a/src/MikrotikInstaller.App/App.xaml +++ b/src/MikrotikInstaller.App/App.xaml @@ -6,6 +6,7 @@ xmlns:steps="clr-namespace:MikrotikInstaller.App.ViewModels.Steps" xmlns:stepViews="clr-namespace:MikrotikInstaller.App.Views.Steps" xmlns:converters="clr-namespace:MikrotikInstaller.App.Converters" + xmlns:viewControls="clr-namespace:MikrotikInstaller.App.Views.Controls" StartupUri="MainWindow.xaml"> @@ -37,6 +38,9 @@ + + + diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/ExpertEditorViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/ExpertEditorViewModel.cs new file mode 100644 index 0000000..d3bf6e0 --- /dev/null +++ b/src/MikrotikInstaller.App/ViewModels/Steps/ExpertEditorViewModel.cs @@ -0,0 +1,175 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using MikrotikInstaller.Core.Connectivity; +using MikrotikInstaller.Core.Expert; + +namespace MikrotikInstaller.App.ViewModels.Steps; + +/// +/// Bearbeiten-Bereich für einen RouterOS-Eintrag — im Experte-Bereich und in der Übersicht (Issue #24) +/// derselbe: Feldhilfen aus dem Katalog, „Weitere Parameter“, Befehlsvorschau, Speichern erst bei Änderung, +/// Sicherung vor der ersten Änderung einer Sitzung. +/// +public partial class ExpertEditorViewModel : ObservableObject +{ + private readonly WizardSession _session; + private readonly MenuSchema? _schema; + private readonly IReadOnlyDictionary? _row; + private readonly Func _onSaved; + + public ExpertEditorViewModel( + WizardSession session, string path, MenuSchema? schema, IReadOnlyDictionary? row, + IReadOnlyList interfaceNames, Func onSaved) + { + _session = session; + Path = path; + _schema = schema; + _row = row; + _onSaved = onSaved; + Title = row is null ? "Neuer Eintrag" + : ExpertMenus.IsTrue(row, "dynamic") ? "Eintrag ansehen (dynamisch, von RouterOS verwaltet)" + : "Eintrag bearbeiten"; + BuildFields(interfaceNames); + } + + public string Path { get; } + + public string Title { get; } + + public ObservableCollection Fields { get; } = []; + + /// Zusätzliche Parameter als „name=wert“-Zeilen, z. B. für Felder ohne Katalog-Eintrag. + [ObservableProperty] + private string _extraParameters = string.Empty; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private string? _previewText; + + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private bool _isSaving; + + [ObservableProperty] + private string? _errorMessage; + + /// Wird ausgelöst, wenn der Editor geschlossen werden soll (nach dem Speichern oder per „Schließen“). + public event EventHandler? Closed; + + private void BuildFields(IReadOnlyList interfaceNames) + { + foreach (var field in _schema?.Fields ?? []) + { + var options = field.Kind switch + { + "enum" => field.Options ?? [], + "interfacePick" => interfaceNames, + _ => [], + }; + var item = new ExpertFieldViewModel(field.Key, field.Label, field.Help, field.Kind, options, _row?.GetValueOrDefault(field.Key) ?? string.Empty); + if (_row is null && field.Default is { } defaultValue) + { + // Neuer Eintrag: Katalog-Standardwert vorschlagen (zählt als Änderung, wird also mitgeschickt). + item.Value = defaultValue; + } + + Add(item); + } + + // Weitere Felder des Eintrags, die der Katalog nicht kennt (oder alle, bei einem eigenen Menüpfad). + foreach (var (key, value) in _row ?? new Dictionary()) + { + if (key.StartsWith('.') || Fields.Any(f => f.Key == key) || key is "dynamic" or "invalid" or "running" or "default") + { + continue; + } + + Add(new ExpertFieldViewModel(key, key, "Weiterer Parameter dieses Eintrags.", value is "true" or "false" ? "bool" : "text", [], value)); + } + + UpdatePreview(); + } + + private void Add(ExpertFieldViewModel field) + { + field.PropertyChanged += (_, _) => UpdatePreview(); + Fields.Add(field); + } + + partial void OnExtraParametersChanged(string value) => UpdatePreview(); + + private Dictionary CollectChanges() + { + var original = _row is null ? null : Fields.ToDictionary(f => f.Key, f => f.Original); + var edited = Fields.ToDictionary(f => f.Key, f => f.Value); + foreach (var line in ExtraParameters.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var separator = line.IndexOf('='); + if (separator > 0) + { + edited[line[..separator].Trim()] = line[(separator + 1)..].Trim(); + } + } + + return ExpertMenus.Changes(original, edited); + } + + private void UpdatePreview() + { + var changes = CollectChanges(); + PreviewText = changes.Count == 0 + ? null + : _schema is { Singleton: true } + ? ExpertMenus.Preview(Path, "set", null, changes) + : ExpertMenus.Preview(Path, _row is null ? "add" : "set", _row.GetValueOrDefault(".id"), changes); + } + + private bool CanSave => PreviewText is not null && !IsSaving; + + [RelayCommand(CanExecute = nameof(CanSave))] + private async Task SaveAsync() + { + if (_session.Client is not { } client) + { + return; + } + + if (_row is not null && _schema is not { Singleton: true } && !_row.ContainsKey(".id")) + { + // Ohne ID würde aus „ändern“ ein neuer Eintrag. + ErrorMessage = "Dieser Eintrag hat keine ID und kann hier nicht geändert werden."; + return; + } + + IsSaving = true; + ErrorMessage = null; + try + { + if (!_session.ExpertBackupDone) + { + await _session.CreateSafetyBackupAsync(client); + _session.ExpertBackupDone = true; + } + + await ExpertMenus.SaveAsync(client, _schema, Path, _row?.GetValueOrDefault(".id"), CollectChanges()); + LastSavedCommand = PreviewText; + Closed?.Invoke(this, EventArgs.Empty); + await _onSaved(); + } + catch (RouterOsException ex) + { + ErrorMessage = ex.Message; + } + finally + { + IsSaving = false; + } + } + + /// Der zuletzt ausgeführte Befehl, für die Erfolgsmeldung des Aufrufers. + public string? LastSavedCommand { get; private set; } + + [RelayCommand] + private void Close() => Closed?.Invoke(this, EventArgs.Empty); +} diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/ExpertStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/ExpertStepViewModel.cs index a7f11df..72607e8 100644 --- a/src/MikrotikInstaller.App/ViewModels/Steps/ExpertStepViewModel.cs +++ b/src/MikrotikInstaller.App/ViewModels/Steps/ExpertStepViewModel.cs @@ -88,6 +88,7 @@ public partial class ExpertFieldViewModel : ObservableObject }; } + /// /// Experte-Bereich (Issue #21, Mac „Experte“-Tab): kuratierte RouterOS-Menüs mit Feldhilfen und ein /// eigener Menüpfad für alles andere. Auflisten, bearbeiten, anlegen, löschen — jede Änderung zeigt vorher @@ -96,8 +97,6 @@ public partial class ExpertFieldViewModel : ObservableObject public partial class ExpertStepViewModel : WizardStepViewModelBase { private readonly WizardSession _session; - private IReadOnlyDictionary? _editingRow; - private bool _safetyBackupDone; public ExpertStepViewModel(WizardSession session) : base( @@ -118,8 +117,6 @@ public partial class ExpertStepViewModel : WizardStepViewModelBase public ObservableCollection Entries { get; } = []; - public ObservableCollection Fields { get; } = []; - [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsMenuSelected))] [NotifyPropertyChangedFor(nameof(IsToolSelected))] @@ -144,19 +141,9 @@ public partial class ExpertStepViewModel : WizardStepViewModelBase [ObservableProperty] private string? _statusMessage; + /// Der geöffnete Bearbeiten-Bereich, null wenn geschlossen (gemeinsam mit der Übersicht, Issue #24). [ObservableProperty] - [NotifyPropertyChangedFor(nameof(IsEditing))] - private string? _editorTitle; - - public bool IsEditing => EditorTitle is not null; - - /// Zusätzliche Parameter als „name=wert“-Zeilen, z. B. für Felder ohne Katalog-Eintrag. - [ObservableProperty] - private string _extraParameters = string.Empty; - - [ObservableProperty] - [NotifyCanExecuteChangedFor(nameof(SaveCommand))] - private string? _previewText; + private ExpertEditorViewModel? _editor; public override async Task OnActivatedAsync() { @@ -170,7 +157,7 @@ public partial class ExpertStepViewModel : WizardStepViewModelBase partial void OnSelectedMenuChanged(ExpertMenuItem? value) { Tools.ActiveTool = value?.Tool; - CloseEditor(); + Editor = null; Entries.Clear(); ErrorMessage = null; StatusMessage = null; @@ -237,151 +224,32 @@ public partial class ExpertStepViewModel : WizardStepViewModelBase : ex.Message; [RelayCommand] - private void EditEntry(ExpertEntryViewModel entry) - { - _editingRow = entry.Row; - EditorTitle = entry.IsDynamic ? "Eintrag ansehen (dynamisch, von RouterOS verwaltet)" : "Eintrag bearbeiten"; - BuildFields(entry.Row); - } + private void EditEntry(ExpertEntryViewModel entry) => OpenEditor(entry.Row); [RelayCommand] - private void NewEntry() + private void NewEntry() => OpenEditor(null); + + private void OpenEditor(IReadOnlyDictionary? row) { - _editingRow = null; - EditorTitle = "Neuer Eintrag"; - BuildFields(null); - } - - [RelayCommand] - private void CloseEditor() - { - EditorTitle = null; - Fields.Clear(); - ExtraParameters = string.Empty; - PreviewText = null; - } - - private void BuildFields(IReadOnlyDictionary? row) - { - Fields.Clear(); - ExtraParameters = string.Empty; - var schema = SelectedMenu?.Schema; - - foreach (var field in schema?.Fields ?? []) - { - var options = field.Kind switch - { - "enum" => field.Options ?? [], - "interfacePick" => Tools.InterfaceNames, - "bool" => [], - _ => [], - }; - var item = new ExpertFieldViewModel(field.Key, field.Label, field.Help, field.Kind, options, row?.GetValueOrDefault(field.Key) ?? string.Empty); - if (row is null && field.Default is { } defaultValue) - { - // Neuer Eintrag: Katalog-Standardwert vorschlagen (zählt als Änderung, wird also mitgeschickt). - item.Value = defaultValue; - } - - AddField(item); - } - - // Weitere Felder des Eintrags, die der Katalog nicht kennt (oder alle, bei einem eigenen Menüpfad). - foreach (var (key, value) in row ?? new Dictionary()) - { - if (key.StartsWith('.') || Fields.Any(f => f.Key == key) || key is "dynamic" or "invalid" or "running" or "default") - { - continue; - } - - AddField(new ExpertFieldViewModel(key, key, "Weiterer Parameter dieses Eintrags.", value is "true" or "false" ? "bool" : "text", [], value)); - } - - UpdatePreview(); - } - - private void AddField(ExpertFieldViewModel field) - { - field.PropertyChanged += (_, _) => UpdatePreview(); - Fields.Add(field); - } - - partial void OnExtraParametersChanged(string value) => UpdatePreview(); - - private Dictionary CollectChanges() - { - var original = _editingRow is null ? null : Fields.ToDictionary(f => f.Key, f => f.Original); - var edited = Fields.ToDictionary(f => f.Key, f => f.Value); - foreach (var line in ExtraParameters.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) - { - var separator = line.IndexOf('='); - if (separator > 0) - { - edited[line[..separator].Trim()] = line[(separator + 1)..].Trim(); - } - } - - return ExpertMenus.Changes(original, edited); - } - - private void UpdatePreview() - { - if (!IsEditing || SelectedMenu?.Path is not { } path) - { - PreviewText = null; - return; - } - - var changes = CollectChanges(); - PreviewText = changes.Count == 0 - ? null - : SelectedMenu.Schema is { Singleton: true } - ? ExpertMenus.Preview(path, "set", null, changes) - : ExpertMenus.Preview(path, _editingRow is null ? "add" : "set", _editingRow?.GetValueOrDefault(".id"), changes); - } - - private bool CanSave => PreviewText is not null && !IsLoading; - - [RelayCommand(CanExecute = nameof(CanSave))] - private async Task SaveAsync() - { - if (_session.Client is not { } client || SelectedMenu?.Path is not { } path) + if (SelectedMenu?.Path is not { } path) { return; } - var changes = CollectChanges(); - if (_editingRow is not null && SelectedMenu.Schema is not { Singleton: true } && !_editingRow.ContainsKey(".id")) - { - // Ohne ID würde aus „ändern“ ein neuer Eintrag. - ErrorMessage = "Dieser Eintrag hat keine ID und kann hier nicht geändert werden."; - return; - } - - IsLoading = true; - ErrorMessage = null; - try - { - await EnsureSafetyBackupAsync(client); - StatusMessage = $"Wird ausgeführt: {PreviewText}"; - await ExpertMenus.SaveAsync(client, SelectedMenu.Schema, path, _editingRow?.GetValueOrDefault(".id"), changes); - StatusMessage = $"Gespeichert: {PreviewText}"; - CloseEditor(); - } - catch (RouterOsException ex) - { - ErrorMessage = ex.Message; - StatusMessage = null; - } - finally - { - IsLoading = false; - } - - if (ErrorMessage is null) + ExpertEditorViewModel? editor = null; + editor = new ExpertEditorViewModel(_session, path, SelectedMenu.Schema, row, Tools.InterfaceNames, async () => { + StatusMessage = $"Gespeichert: {editor!.LastSavedCommand}"; await LoadEntriesAsync(); - } + }); + editor.Closed += (_, _) => + { + if (Editor == editor) + { + Editor = null; + } + }; + Editor = editor; } [RelayCommand] @@ -403,7 +271,13 @@ public partial class ExpertStepViewModel : WizardStepViewModelBase ErrorMessage = null; try { - await EnsureSafetyBackupAsync(client); + if (!_session.ExpertBackupDone) + { + StatusMessage = "Sicherung vor der ersten Änderung wird erstellt …"; + await _session.CreateSafetyBackupAsync(client); + _session.ExpertBackupDone = true; + } + await client.RemoveAsync(path, id); StatusMessage = $"Gelöscht: {preview}"; } @@ -418,17 +292,4 @@ public partial class ExpertStepViewModel : WizardStepViewModelBase await LoadEntriesAsync(); } - - /// Vor der ersten Änderung im Experte-Bereich eine Sicherung (eine Handsicherung der letzten 10 Minuten zählt). - private async Task EnsureSafetyBackupAsync(IRouterOsClient client) - { - if (_safetyBackupDone) - { - return; - } - - StatusMessage = "Sicherung vor der ersten Änderung wird erstellt …"; - await _session.CreateSafetyBackupAsync(client); - _safetyBackupDone = true; - } } diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/NetworkAnalysisStepViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/NetworkAnalysisStepViewModel.cs index 739c543..0712ce8 100644 --- a/src/MikrotikInstaller.App/ViewModels/Steps/NetworkAnalysisStepViewModel.cs +++ b/src/MikrotikInstaller.App/ViewModels/Steps/NetworkAnalysisStepViewModel.cs @@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Input; using MikrotikInstaller.Core.Analysis; using MikrotikInstaller.Core.Connectivity; using MikrotikInstaller.Core.Devices; +using MikrotikInstaller.Core.Expert; namespace MikrotikInstaller.App.ViewModels.Steps; @@ -96,11 +97,67 @@ public partial class NetworkAnalysisStepViewModel : WizardStepViewModelBase } /// Wird ausgelöst, wenn der Nutzer in der Grafik einen anderen Knoten anklickt (NodifyEditor.SelectedItem). - partial void OnSelectedTopologyItemChanged(object? value) => HighlightConnectedTo(value as TopologyNodeViewModel); + partial void OnSelectedTopologyItemChanged(object? value) + { + HighlightConnectedTo(value as TopologyNodeViewModel); + OnPropertyChanged(nameof(CanEditSelected)); + } [RelayCommand] private void ClearSelection() => SelectedTopologyItem = null; + /// Bearbeiten im Diagramm (Issue #24, Mac „Übersicht“): nur mit „Professioneller Netzwerker“. + public bool CanEditSelected => UserSettings.Current.ProfessionalMode && SelectedTopologyItem is TopologyNodeViewModel { EditPath: not null }; + + /// Derselbe Bearbeiten-Bereich wie im Experte-Bereich, null wenn geschlossen. + [ObservableProperty] + private ExpertEditorViewModel? _editor; + + [ObservableProperty] + private string? _editMessage; + + [RelayCommand] + private async Task EditSelectedNodeAsync() + { + if (_session.Client is not { } client || SelectedTopologyItem is not TopologyNodeViewModel { EditPath: { } path, EditMatch: { } match }) + { + return; + } + + EditMessage = null; + try + { + var row = (await client.GetAsync(path)).FirstOrDefault(r => match.All(kv => kv.Key == "@bound" + ? new[] { "to-addresses", "dst-address", "src-address" }.Any(k => r.GetValueOrDefault(k)?.Split('/')[0] == kv.Value.Split('/')[0]) + : r.GetValueOrDefault(kv.Key) == kv.Value)); + if (row is null) + { + EditMessage = "Den Eintrag gibt es auf dem Router so nicht mehr — bitte die Übersicht neu laden."; + return; + } + + var interfaces = (await client.GetAsync("/interface")).Select(i => i.GetValueOrDefault("name")).OfType().ToList(); + ExpertEditorViewModel? editor = null; + editor = new ExpertEditorViewModel(_session, path, MenuCatalog.Find(path), row, interfaces, async () => + { + EditMessage = $"Gespeichert: {editor!.LastSavedCommand}"; + await OnActivatedAsync(); + }); + editor.Closed += (_, _) => + { + if (Editor == editor) + { + Editor = null; + } + }; + Editor = editor; + } + catch (RouterOsException ex) + { + EditMessage = ex.Message; + } + } + /// /// Hebt den gesamten mit dem angeklickten Knoten verbundenen Teilbaum hervor: die Eltern-Kette /// bis zur Wurzel (der Weg zum Internet-Zugang, die "Hops") UND alle Nachfahren (alles, was von @@ -241,7 +298,18 @@ public partial class NetworkAnalysisStepViewModel : WizardStepViewModelBase ? $"{interfaceName} ({ssid})" : interfaceName; - nodesById[id] = new TopologyNodeViewModel(id, label, kind, parentId); + nodesById[id] = new TopologyNodeViewModel(id, label, kind, parentId) + { + EditPath = kind switch + { + TopologyNodeKind.Ethernet => "/interface/ethernet", + TopologyNodeKind.Bridge => "/interface/bridge", + TopologyNodeKind.Vlan => "/interface/vlan", + TopologyNodeKind.Wireless => "/interface/wireless", + _ => "/interface", + }, + EditMatch = new Dictionary { ["name"] = interfaceName }, + }; } foreach (var address in snapshot.IpAddresses) @@ -252,7 +320,11 @@ public partial class NetworkAnalysisStepViewModel : WizardStepViewModelBase } var id = $"ip:{address.InterfaceName}:{address.AddressCidr}"; - nodesById[id] = new TopologyNodeViewModel(id, address.AddressCidr, TopologyNodeKind.IpAddress, InterfaceNodeId(address.InterfaceName)); + nodesById[id] = new TopologyNodeViewModel(id, address.AddressCidr, TopologyNodeKind.IpAddress, InterfaceNodeId(address.InterfaceName)) + { + EditPath = "/ip/address", + EditMatch = new Dictionary { ["address"] = address.AddressCidr, ["interface"] = address.InterfaceName }, + }; } foreach (var dhcp in snapshot.DhcpServers) @@ -263,7 +335,11 @@ public partial class NetworkAnalysisStepViewModel : WizardStepViewModelBase } var id = $"dhcp:{dhcp.Name}"; - nodesById[id] = new TopologyNodeViewModel(id, dhcp.Name, TopologyNodeKind.DhcpServer, InterfaceNodeId(dhcp.InterfaceName), dhcp.AddressRange); + nodesById[id] = new TopologyNodeViewModel(id, dhcp.Name, TopologyNodeKind.DhcpServer, InterfaceNodeId(dhcp.InterfaceName), dhcp.AddressRange) + { + EditPath = "/ip/dhcp-server", + EditMatch = new Dictionary { ["name"] = dhcp.Name }, + }; } // Firewall-/NAT-Regeln unter der IP-Adresse einhängen, deren Subnetz die gebundene Adresse @@ -287,7 +363,11 @@ public partial class NetworkAnalysisStepViewModel : WizardStepViewModelBase var detail = rule.Table == "nat" ? "Portfreigabe" : rule.BoundAddress; var id = $"fw:{firewallIndex}"; - nodesById[id] = new TopologyNodeViewModel(id, label, TopologyNodeKind.Firewall, parentIpNode?.Id, detail); + nodesById[id] = new TopologyNodeViewModel(id, label, TopologyNodeKind.Firewall, parentIpNode?.Id, detail) + { + EditPath = $"/ip/firewall/{rule.Table}", + EditMatch = new Dictionary { ["chain"] = rule.Chain, ["action"] = rule.Action, ["@bound"] = rule.BoundAddress }, + }; } AssignLayout(nodesById.Values, DiagramItems); diff --git a/src/MikrotikInstaller.App/ViewModels/Steps/TopologyNodeViewModel.cs b/src/MikrotikInstaller.App/ViewModels/Steps/TopologyNodeViewModel.cs index 1afa311..f6247cb 100644 --- a/src/MikrotikInstaller.App/ViewModels/Steps/TopologyNodeViewModel.cs +++ b/src/MikrotikInstaller.App/ViewModels/Steps/TopologyNodeViewModel.cs @@ -18,6 +18,12 @@ public partial class TopologyNodeViewModel(string id, string label, TopologyNode /// Zusätzliche, laientaugliche Zusatzinfo für die Grafik (z. B. DHCP-Adressbereich, Gateway-IP). public string? Detail { get; } = detail; + /// RouterOS-Menüpfad des Eintrags hinter diesem Knoten, null wenn nicht bearbeitbar (Issue #24). + public string? EditPath { get; init; } + + /// Felder, an denen der Eintrag wiedergefunden wird; "@bound" = eine der Adressfelder einer Firewall-Regel. + public IReadOnlyDictionary? EditMatch { get; init; } + [ObservableProperty] private Point _location; diff --git a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs index 0e59324..23896ea 100644 --- a/src/MikrotikInstaller.App/ViewModels/WizardSession.cs +++ b/src/MikrotikInstaller.App/ViewModels/WizardSession.cs @@ -136,6 +136,9 @@ public sealed class WizardSession } } + /// Vor der ersten Änderung im Experte-Bereich/in der Übersicht wurde schon gesichert. + public bool ExpertBackupDone { get; set; } + /// Zuletzt von Hand erstelltes Backup (Name, Zeitpunkt). public (string Name, DateTimeOffset At)? LastManualBackup { get; set; } diff --git a/src/MikrotikInstaller.App/Views/Controls/ExpertEditorView.xaml b/src/MikrotikInstaller.App/Views/Controls/ExpertEditorView.xaml new file mode 100644 index 0000000..19e5e5a --- /dev/null +++ b/src/MikrotikInstaller.App/Views/Controls/ExpertEditorView.xaml @@ -0,0 +1,62 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/MikrotikInstaller.App/Views/Controls/ExpertEditorView.xaml.cs b/src/MikrotikInstaller.App/Views/Controls/ExpertEditorView.xaml.cs new file mode 100644 index 0000000..89338c9 --- /dev/null +++ b/src/MikrotikInstaller.App/Views/Controls/ExpertEditorView.xaml.cs @@ -0,0 +1,9 @@ +namespace MikrotikInstaller.App.Views.Controls; + +public partial class ExpertEditorView : System.Windows.Controls.UserControl +{ + public ExpertEditorView() + { + InitializeComponent(); + } +} diff --git a/src/MikrotikInstaller.App/Views/Steps/ExpertStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/ExpertStepView.xaml index c6ba33a..14914e3 100644 --- a/src/MikrotikInstaller.App/Views/Steps/ExpertStepView.xaml +++ b/src/MikrotikInstaller.App/Views/Steps/ExpertStepView.xaml @@ -144,55 +144,8 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + diff --git a/src/MikrotikInstaller.App/Views/Steps/NetworkAnalysisStepView.xaml b/src/MikrotikInstaller.App/Views/Steps/NetworkAnalysisStepView.xaml index 498bccf..3c02b77 100644 --- a/src/MikrotikInstaller.App/Views/Steps/NetworkAnalysisStepView.xaml +++ b/src/MikrotikInstaller.App/Views/Steps/NetworkAnalysisStepView.xaml @@ -75,9 +75,20 @@ - + + + + + + + + + +