From 1af741155a48947d4f0603965d94cc2b78d4e69c Mon Sep 17 00:00:00 2001 From: Andreas Billmann Date: Mon, 31 Jul 2023 16:54:59 +0200 Subject: [PATCH] ui improvements and async test when download a file --- K8sFileBrowser/App.axaml | 15 +++-- K8sFileBrowser/ApplicationHelper.cs | 5 +- K8sFileBrowser/Assets/Icons.axaml | 40 +++++++++++ K8sFileBrowser/Models/FileInformation.cs | 4 +- K8sFileBrowser/Services/KubernetesService.cs | 20 +++--- .../ViewModels/MainWindowViewModel.cs | 67 +++++++++++-------- K8sFileBrowser/Views/MainWindow.axaml | 59 ++++++++++++---- 7 files changed, 150 insertions(+), 60 deletions(-) create mode 100644 K8sFileBrowser/Assets/Icons.axaml diff --git a/K8sFileBrowser/App.axaml b/K8sFileBrowser/App.axaml index 2a7be7a..ee50a95 100644 --- a/K8sFileBrowser/App.axaml +++ b/K8sFileBrowser/App.axaml @@ -8,18 +8,18 @@ - + - @@ -38,9 +38,10 @@ - + + \ No newline at end of file diff --git a/K8sFileBrowser/ApplicationHelper.cs b/K8sFileBrowser/ApplicationHelper.cs index 1e02b71..67b28ab 100644 --- a/K8sFileBrowser/ApplicationHelper.cs +++ b/K8sFileBrowser/ApplicationHelper.cs @@ -14,9 +14,8 @@ public static class ApplicationHelper { public static async Task SaveFile(string? initialFolder, string? initialFile) { - Window? ret; - if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || - desktop.MainWindow is not { } wnd) return null; + if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || + desktop.MainWindow is not { } wnd) return null; try { var filter = new List { new("All files") { Patterns = new List { "*" } } }; diff --git a/K8sFileBrowser/Assets/Icons.axaml b/K8sFileBrowser/Assets/Icons.axaml new file mode 100644 index 0000000..6e4a286 --- /dev/null +++ b/K8sFileBrowser/Assets/Icons.axaml @@ -0,0 +1,40 @@ + + + + + + + + + + \ No newline at end of file diff --git a/K8sFileBrowser/Models/FileInformation.cs b/K8sFileBrowser/Models/FileInformation.cs index bf33e0f..ca9105a 100644 --- a/K8sFileBrowser/Models/FileInformation.cs +++ b/K8sFileBrowser/Models/FileInformation.cs @@ -9,8 +9,10 @@ public class FileInformation public string Name { get; set; } = string.Empty; public string Size { get; set; } = string.Empty; public DateTimeOffset Date { get; set; } = DateTimeOffset.MinValue; - + public bool IsFile => Type == FileType.File; + public bool IsDirectory => Type == FileType.Directory; + public bool IsUnknown => Type == FileType.Unknown; } public enum FileType diff --git a/K8sFileBrowser/Services/KubernetesService.cs b/K8sFileBrowser/Services/KubernetesService.cs index 7e80534..502ac2b 100644 --- a/K8sFileBrowser/Services/KubernetesService.cs +++ b/K8sFileBrowser/Services/KubernetesService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; +using System.Threading.Tasks; using k8s; using k8s.KubeConfigModels; using K8sFileBrowser.Models; @@ -48,7 +49,7 @@ public class KubernetesService { var pods = _kubernetesClient.CoreV1.ListNamespacedPod(namespaceName); var podList = pods != null - ? pods.Items.Select(n => + ? pods.Items.Select(n => new Pod { Name = n.Metadata.Name, @@ -57,7 +58,7 @@ public class KubernetesService : new List(); return podList; } - + public IList GetFiles(string namespaceName, string podName, string containerName, string path) { try @@ -65,14 +66,17 @@ public class KubernetesService var execResult = new KubernetesFileInformationResult(path); var resultCode = _kubernetesClient .NamespacedPodExecAsync( - podName, namespaceName, containerName, - new[] { "find", path, "-maxdepth", "1", "-exec", "stat", "-c", "%F|%n|%s|%Y", "{}", ";" }, + podName, namespaceName, containerName, + new[] { "find", path, "-maxdepth", "1", "-exec", "stat", "-c", "%F|%n|%s|%Y", "{}", ";" }, true, execResult.ParseFileInformationCallback, CancellationToken.None) .ConfigureAwait(false) .GetAwaiter() .GetResult(); - return execResult.FileInformations; + return execResult.FileInformations + .Where(f => f.Name != "." && f.Name != path) + .OrderBy(f => f.Type).ThenBy(f => f.Name) + .ToList(); } catch (Exception e) { @@ -91,10 +95,10 @@ public class KubernetesService } - public void DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile, string? saveFileName) + public async Task DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile, string? saveFileName) { Log.Information($"{selectedNamespace} - {selectedPod} - {selectedFile} - {saveFileName}"); - - // TODO: this is done with Tar + await Task.Delay(10000); + // TODO: this is done with Tar } } \ No newline at end of file diff --git a/K8sFileBrowser/ViewModels/MainWindowViewModel.cs b/K8sFileBrowser/ViewModels/MainWindowViewModel.cs index 6449cef..d0bbfc2 100644 --- a/K8sFileBrowser/ViewModels/MainWindowViewModel.cs +++ b/K8sFileBrowser/ViewModels/MainWindowViewModel.cs @@ -20,7 +20,7 @@ public class MainWindowViewModel : ViewModelBase get => _selectedClusterContext; set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value); } - + private readonly ObservableAsPropertyHelper> _namespaces; public IEnumerable Namespaces => _namespaces.Value; @@ -30,7 +30,7 @@ public class MainWindowViewModel : ViewModelBase get => _selectedNamespace; set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value); } - + private readonly ObservableAsPropertyHelper> _pods; public IEnumerable Pods => _pods.Value; @@ -40,24 +40,31 @@ public class MainWindowViewModel : ViewModelBase get => _selectedPod; set => this.RaiseAndSetIfChanged(ref _selectedPod, value); } - + private readonly ObservableAsPropertyHelper> _fileInformation; public IEnumerable FileInformation => _fileInformation.Value; - + private FileInformation? _selectedFile; public FileInformation? SelectedFile { get => _selectedFile; set => this.RaiseAndSetIfChanged(ref _selectedFile, value); } - + private string? _selectedPath; - public string? SelectedPath + public string? SelectedPath { get => _selectedPath; set => this.RaiseAndSetIfChanged(ref _selectedPath, value); } - + + private bool _isDownloadActive; + public bool IsDownloadActive + { + get => _isDownloadActive; + set => this.RaiseAndSetIfChanged(ref _isDownloadActive, value); + } + public ReactiveCommand DownloadCommand { get; } public ReactiveCommand ParentCommand { get; } public ReactiveCommand OpenCommand { get; } @@ -66,33 +73,39 @@ public class MainWindowViewModel : ViewModelBase public MainWindowViewModel() { var kubernetesService = new KubernetesService(); - + var isFile = this - .WhenAnyValue(x => x.SelectedFile) - .Select(x => x is { Type: FileType.File }); - + .WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive) + .Select(x => x is { Item1.Type: FileType.File, Item2: false }); + var isDirectory = this - .WhenAnyValue(x => x.SelectedFile) - .Select(x => x is { Type: FileType.Directory }); - + .WhenAnyValue(x => x.SelectedFile, x => x.IsDownloadActive) + .Select(x => x is { Item1.Type: FileType.Directory, Item2: false }); + var isNotRoot = this - .WhenAnyValue(x => x.SelectedPath) - .Select(x => x is not "/"); - + .WhenAnyValue(x => x.SelectedPath, x => x.IsDownloadActive) + .Select(x => x.Item1 is not "/" && !x.Item2); + OpenCommand = ReactiveCommand.Create(() => { SelectedPath = SelectedFile != null ? SelectedFile!.Name : "/"; }, isDirectory, RxApp.MainThreadScheduler); - - DownloadCommand = ReactiveCommand.CreateFromTask(async () => + + DownloadCommand = ReactiveCommand.CreateFromTask(async () => { + await Observable.StartAsync(async () => { var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1, SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1); var saveFileName = await ApplicationHelper.SaveFile(".", fileName); - kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName); + if (saveFileName != null) + { + IsDownloadActive = true; + await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedFile, saveFileName); + IsDownloadActive = false; + } + }, RxApp.TaskpoolScheduler); + }, isFile, RxApp.MainThreadScheduler); - }, isFile, RxApp.TaskpoolScheduler); - - ParentCommand = ReactiveCommand.Create(() => + ParentCommand = ReactiveCommand.Create(() => { SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')]; if (SelectedPath!.Length == 0) @@ -100,20 +113,20 @@ public class MainWindowViewModel : ViewModelBase SelectedPath = "/"; } }, isNotRoot, RxApp.MainThreadScheduler); - + // read the cluster contexts _namespaces = this .WhenAnyValue(c => c.SelectedClusterContext) .Throttle(TimeSpan.FromMilliseconds(10)) .Where(context => context != null) - .Select(context => + .Select(context => { kubernetesService.SwitchClusterContext(context!); return kubernetesService.GetNamespaces(); }) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.Namespaces); - + // read the pods when the namespace changes _pods = this .WhenAnyValue(c => c.SelectedNamespace) @@ -133,7 +146,7 @@ public class MainWindowViewModel : ViewModelBase x.Item1)) .ObserveOn(RxApp.MainThreadScheduler) .ToProperty(this, x => x.FileInformation); - + // reset the path when the pod or namespace changes this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace) .Subscribe(x => SelectedPath = "/"); diff --git a/K8sFileBrowser/Views/MainWindow.axaml b/K8sFileBrowser/Views/MainWindow.axaml index c7768e5..5af3685 100644 --- a/K8sFileBrowser/Views/MainWindow.axaml +++ b/K8sFileBrowser/Views/MainWindow.axaml @@ -3,18 +3,25 @@ xmlns:vm="using:K8sFileBrowser.ViewModels" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" + xmlns:models="clr-namespace:K8sFileBrowser.Models" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="K8sFileBrowser.Views.MainWindow" x:DataType="vm:MainWindowViewModel" Icon="/Assets/avalonia-logo.ico" Title="K8sFileBrowser"> - + - - + + + + + Download File... + + + - - - - - - - - + + + + + + + + + + + + + SelectedItem="{Binding SelectedFile}" + > - + + + + + + + + + + + + + @@ -99,5 +130,5 @@ - + \ No newline at end of file