mirror of
https://github.com/frosch95/K8sFileBrowser.git
synced 2026-04-11 04:48:22 +02:00
First draft of kubernetes file browser
This commit is contained in:
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
bin/
|
||||
obj/
|
||||
/packages/
|
||||
riderModule.iml
|
||||
/_ReSharper.Caches/
|
||||
.idea/
|
||||
*.user
|
||||
16
K8sFileBrowser.sln
Normal file
16
K8sFileBrowser.sln
Normal file
@@ -0,0 +1,16 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "K8sFileBrowser", "K8sFileBrowser\K8sFileBrowser.csproj", "{637A753B-3168-4C9C-8098-7A16024E1957}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{637A753B-3168-4C9C-8098-7A16024E1957}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{637A753B-3168-4C9C-8098-7A16024E1957}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{637A753B-3168-4C9C-8098-7A16024E1957}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{637A753B-3168-4C9C-8098-7A16024E1957}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
46
K8sFileBrowser/App.axaml
Normal file
46
K8sFileBrowser/App.axaml
Normal file
@@ -0,0 +1,46 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="K8sFileBrowser.App"
|
||||
xmlns:local="using:K8sFileBrowser"
|
||||
RequestedThemeVariant="Dark">
|
||||
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
|
||||
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator/>
|
||||
</Application.DataTemplates>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme>
|
||||
<FluentTheme.Palettes>
|
||||
<!-- Palette for Light theme variant -->
|
||||
<ColorPaletteResources x:Key="Light" Accent="Green" RegionColor="White" ErrorText="Red" />
|
||||
<!-- Palette for Dark theme variant -->
|
||||
<ColorPaletteResources x:Key="Dark"
|
||||
BaseHigh="#abb2bf"
|
||||
Accent="#677696"
|
||||
RegionColor="#282c34"
|
||||
ErrorText="Red"
|
||||
AltHigh="#282c34"
|
||||
AltMediumLow="#2c313c"
|
||||
ListLow="#21252b"
|
||||
ListMedium="#2c313c"
|
||||
BaseMediumLow="#343a45"
|
||||
AltMedium="#2c313c"
|
||||
ChromeMediumLow="#21252b"
|
||||
|
||||
/>
|
||||
<!-- AltHigh is used for the color of header in the DataGrid -->
|
||||
<!-- BaseHigh is used for the text color -->
|
||||
<!-- ListLow is used for the mouse over in lists and DataGrid -->
|
||||
<!-- Accent is used for selection in lists -->
|
||||
<!-- BaseMedium is used for border in comboboxes and Header Text in DataGrid-->
|
||||
<!-- AltMediumLow background for comboboxes -->
|
||||
<!-- BaseMediumLow lines in Datarid -->
|
||||
<!-- AltMedium on mouse over comboboxes -->
|
||||
<!-- ChromeMediumLow is used for the background of the combobox list -->
|
||||
|
||||
</FluentTheme.Palettes>
|
||||
</FluentTheme>
|
||||
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
28
K8sFileBrowser/App.axaml.cs
Normal file
28
K8sFileBrowser/App.axaml.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using K8sFileBrowser.ViewModels;
|
||||
using K8sFileBrowser.Views;
|
||||
|
||||
namespace K8sFileBrowser;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow
|
||||
{
|
||||
DataContext = new MainWindowViewModel(),
|
||||
};
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
50
K8sFileBrowser/ApplicationHelper.cs
Normal file
50
K8sFileBrowser/ApplicationHelper.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Serilog;
|
||||
|
||||
namespace K8sFileBrowser;
|
||||
|
||||
public static class ApplicationHelper
|
||||
{
|
||||
public static async Task<string?> SaveFile(string? initialFolder, string? initialFile)
|
||||
{
|
||||
Window? ret;
|
||||
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop ||
|
||||
desktop.MainWindow is not { } wnd) return null;
|
||||
try
|
||||
{
|
||||
var filter = new List<FilePickerFileType> { new("All files") { Patterns = new List<string> { "*" } } };
|
||||
|
||||
var startLocation = initialFolder != null
|
||||
? await wnd.StorageProvider.TryGetFolderFromPathAsync(initialFolder)
|
||||
: null;
|
||||
var extension = initialFile != null ? Path.GetExtension(initialFile).TrimStart('.') : null;
|
||||
var fileName = initialFile != null ? Path.GetFileNameWithoutExtension(initialFile) : null;
|
||||
|
||||
var file = await wnd.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
SuggestedStartLocation = startLocation,
|
||||
DefaultExtension = extension,
|
||||
ShowOverwritePrompt = true,
|
||||
SuggestedFileName = fileName,
|
||||
FileTypeChoices = filter
|
||||
});
|
||||
|
||||
if (file != null)
|
||||
{
|
||||
return file.Path.LocalPath;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error saving file");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
BIN
K8sFileBrowser/Assets/avalonia-logo.ico
Normal file
BIN
K8sFileBrowser/Assets/avalonia-logo.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 172 KiB |
31
K8sFileBrowser/K8sFileBrowser.csproj
Normal file
31
K8sFileBrowser/K8sFileBrowser.csproj
Normal file
@@ -0,0 +1,31 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" Version="11.0.1" />
|
||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.1" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.1" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.1" />
|
||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.1" />
|
||||
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.1" />
|
||||
<PackageReference Include="KubernetesClient" Version="11.0.44" />
|
||||
<PackageReference Include="Serilog" Version="3.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
10
K8sFileBrowser/Models/ClusterContext.cs
Normal file
10
K8sFileBrowser/Models/ClusterContext.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace K8sFileBrowser.Models;
|
||||
|
||||
public class ClusterContext
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
21
K8sFileBrowser/Models/FileInformation.cs
Normal file
21
K8sFileBrowser/Models/FileInformation.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace K8sFileBrowser.Models;
|
||||
|
||||
public class FileInformation
|
||||
{
|
||||
public string Parent { get; set; } = string.Empty;
|
||||
public FileType Type { get; set; } = FileType.File;
|
||||
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 enum FileType
|
||||
{
|
||||
Directory,
|
||||
File,
|
||||
Unknown
|
||||
}
|
||||
11
K8sFileBrowser/Models/Namespace.cs
Normal file
11
K8sFileBrowser/Models/Namespace.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
namespace K8sFileBrowser.Models;
|
||||
|
||||
public class Namespace
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
14
K8sFileBrowser/Models/Pod.cs
Normal file
14
K8sFileBrowser/Models/Pod.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace K8sFileBrowser.Models;
|
||||
|
||||
public class Pod
|
||||
{
|
||||
public string Name { get; init; } = string.Empty;
|
||||
public IList<string> Containers { get; set; } = new List<string>();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Name;
|
||||
}
|
||||
}
|
||||
46
K8sFileBrowser/Program.cs
Normal file
46
K8sFileBrowser/Program.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using Avalonia;
|
||||
using Avalonia.ReactiveUI;
|
||||
using System;
|
||||
using Serilog;
|
||||
|
||||
namespace K8sFileBrowser;
|
||||
|
||||
class Program
|
||||
{
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
[STAThread]
|
||||
public static void Main(string[] args) {
|
||||
try
|
||||
{
|
||||
Log.Logger = new LoggerConfiguration()
|
||||
//.Filter.ByIncludingOnly(Matching.WithProperty("Area", LogArea.Control))
|
||||
.MinimumLevel.Verbose()
|
||||
.WriteTo.Console()
|
||||
.CreateLogger();
|
||||
|
||||
BuildAvaloniaApp()
|
||||
.StartWithClassicDesktopLifetime(args);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
// here we can work with the exception, for example add it to our log file
|
||||
Log.Fatal(e, "Something very bad happened");
|
||||
}
|
||||
finally
|
||||
{
|
||||
// This block is optional.
|
||||
// Use the finally-block if you need to clean things up or similar
|
||||
Log.CloseAndFlush();
|
||||
}
|
||||
}
|
||||
|
||||
// Avalonia configuration, don't remove; also used by visual designer.
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace()
|
||||
.UseReactiveUI();
|
||||
}
|
||||
68
K8sFileBrowser/Services/KubernetesFileInformationResult.cs
Normal file
68
K8sFileBrowser/Services/KubernetesFileInformationResult.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using K8sFileBrowser.Models;
|
||||
using Serilog;
|
||||
|
||||
namespace K8sFileBrowser.Services;
|
||||
|
||||
public class KubernetesFileInformationResult
|
||||
{
|
||||
private readonly string _parent;
|
||||
public IList<FileInformation> FileInformations { get; set; } = new List<FileInformation>();
|
||||
|
||||
public KubernetesFileInformationResult(string parent)
|
||||
{
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public Task ParseFileInformationCallback(Stream stdIn, Stream stdOut, Stream stdErr)
|
||||
{
|
||||
using (var stdOutReader = new StreamReader(stdOut))
|
||||
{
|
||||
var output = stdOutReader.ReadToEnd();
|
||||
|
||||
output.Split("\n").ToList().ForEach(line =>
|
||||
{
|
||||
if (line.Length <= 0) return;
|
||||
line = line.TrimEnd('\n');
|
||||
|
||||
var fileInformation = line.Split("|").ToList();
|
||||
FileInformations.Add(new FileInformation
|
||||
{
|
||||
Parent = _parent,
|
||||
Type = GetFileType(fileInformation[0]),
|
||||
Name = fileInformation[1],
|
||||
Size = fileInformation[2],
|
||||
Date = GetDate(fileInformation[3])
|
||||
});
|
||||
});
|
||||
Log.Information(output);
|
||||
}
|
||||
|
||||
using (var stdErrReader = new StreamReader(stdErr))
|
||||
{
|
||||
var output = stdErrReader.ReadToEnd();
|
||||
Log.Warning(output);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static FileType GetFileType(string type)
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
"directory" => FileType.Directory,
|
||||
"regular file" => FileType.File,
|
||||
_ => FileType.Unknown
|
||||
};
|
||||
}
|
||||
|
||||
private static DateTimeOffset GetDate(string date)
|
||||
{
|
||||
var unixTime = long.Parse(date);
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unixTime);
|
||||
}
|
||||
}
|
||||
100
K8sFileBrowser/Services/KubernetesService.cs
Normal file
100
K8sFileBrowser/Services/KubernetesService.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using k8s;
|
||||
using k8s.KubeConfigModels;
|
||||
using K8sFileBrowser.Models;
|
||||
using Serilog;
|
||||
|
||||
namespace K8sFileBrowser.Services;
|
||||
|
||||
public class KubernetesService
|
||||
{
|
||||
private readonly K8SConfiguration _k8SConfiguration;
|
||||
private IKubernetes _kubernetesClient = null!;
|
||||
|
||||
public KubernetesService()
|
||||
{
|
||||
_k8SConfiguration = KubernetesClientConfiguration.LoadKubeConfig();
|
||||
CreateKubernetesClient();
|
||||
}
|
||||
|
||||
public IEnumerable<ClusterContext> GetClusterContexts()
|
||||
{
|
||||
return _k8SConfiguration.Contexts.Select(c => new ClusterContext { Name = c.Name }).ToList();
|
||||
}
|
||||
|
||||
public string GetCurrentContext()
|
||||
{
|
||||
return _k8SConfiguration.CurrentContext;
|
||||
}
|
||||
|
||||
public void SwitchClusterContext(ClusterContext clusterContext)
|
||||
{
|
||||
CreateKubernetesClient(clusterContext);
|
||||
}
|
||||
|
||||
public IEnumerable<Namespace> GetNamespaces()
|
||||
{
|
||||
var namespaces = _kubernetesClient.CoreV1.ListNamespace();
|
||||
var namespaceList = namespaces != null
|
||||
? namespaces.Items.Select(n => new Namespace { Name = n.Metadata.Name }).ToList()
|
||||
: new List<Namespace>();
|
||||
return namespaceList;
|
||||
}
|
||||
|
||||
public IEnumerable<Pod> GetPods(string namespaceName)
|
||||
{
|
||||
var pods = _kubernetesClient.CoreV1.ListNamespacedPod(namespaceName);
|
||||
var podList = pods != null
|
||||
? pods.Items.Select(n =>
|
||||
new Pod
|
||||
{
|
||||
Name = n.Metadata.Name,
|
||||
Containers = n.Spec.Containers.Select(c => c.Name).ToList()
|
||||
}).ToList()
|
||||
: new List<Pod>();
|
||||
return podList;
|
||||
}
|
||||
|
||||
public IList<FileInformation> GetFiles(string namespaceName, string podName, string containerName, string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
var execResult = new KubernetesFileInformationResult(path);
|
||||
var resultCode = _kubernetesClient
|
||||
.NamespacedPodExecAsync(
|
||||
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;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Log.Error(e, "exception while getting files");
|
||||
return new List<FileInformation>();
|
||||
}
|
||||
}
|
||||
|
||||
private void CreateKubernetesClient(ClusterContext? clusterContext = null)
|
||||
{
|
||||
var clusterContextName = clusterContext == null ? _k8SConfiguration.CurrentContext : clusterContext.Name;
|
||||
var kubernetesClientConfiguration =
|
||||
KubernetesClientConfiguration.BuildConfigFromConfigFile(currentContext: clusterContextName);
|
||||
var kubernetesClient = new Kubernetes(kubernetesClientConfiguration);
|
||||
_kubernetesClient = kubernetesClient;
|
||||
}
|
||||
|
||||
|
||||
public void DownloadFile(Namespace? selectedNamespace, Pod? selectedPod, FileInformation selectedFile, string? saveFileName)
|
||||
{
|
||||
Log.Information($"{selectedNamespace} - {selectedPod} - {selectedFile} - {saveFileName}");
|
||||
|
||||
// TODO: this is done with Tar
|
||||
}
|
||||
}
|
||||
27
K8sFileBrowser/ViewLocator.cs
Normal file
27
K8sFileBrowser/ViewLocator.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Templates;
|
||||
using K8sFileBrowser.ViewModels;
|
||||
|
||||
namespace K8sFileBrowser;
|
||||
|
||||
public class ViewLocator : IDataTemplate
|
||||
{
|
||||
public Control Build(object data)
|
||||
{
|
||||
var name = data.GetType().FullName!.Replace("ViewModel", "View");
|
||||
var type = Type.GetType(name);
|
||||
|
||||
if (type != null)
|
||||
{
|
||||
return (Control)Activator.CreateInstance(type)!;
|
||||
}
|
||||
|
||||
return new TextBlock { Text = "Not Found: " + name };
|
||||
}
|
||||
|
||||
public bool Match(object data)
|
||||
{
|
||||
return data is ViewModelBase;
|
||||
}
|
||||
}
|
||||
151
K8sFileBrowser/ViewModels/MainWindowViewModel.cs
Normal file
151
K8sFileBrowser/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,151 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Linq;
|
||||
using K8sFileBrowser.Models;
|
||||
using K8sFileBrowser.Services;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace K8sFileBrowser.ViewModels;
|
||||
|
||||
public class MainWindowViewModel : ViewModelBase
|
||||
{
|
||||
private readonly ObservableAsPropertyHelper<IEnumerable<ClusterContext>> _clusterContexts;
|
||||
public IEnumerable<ClusterContext> ClusterContexts => _clusterContexts.Value;
|
||||
|
||||
private ClusterContext? _selectedClusterContext;
|
||||
public ClusterContext? SelectedClusterContext
|
||||
{
|
||||
get => _selectedClusterContext;
|
||||
set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value);
|
||||
}
|
||||
|
||||
private readonly ObservableAsPropertyHelper<IEnumerable<Namespace>> _namespaces;
|
||||
public IEnumerable<Namespace> Namespaces => _namespaces.Value;
|
||||
|
||||
private Namespace? _selectedNamespace;
|
||||
public Namespace? SelectedNamespace
|
||||
{
|
||||
get => _selectedNamespace;
|
||||
set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value);
|
||||
}
|
||||
|
||||
private readonly ObservableAsPropertyHelper<IEnumerable<Pod>> _pods;
|
||||
public IEnumerable<Pod> Pods => _pods.Value;
|
||||
|
||||
private Pod? _selectedPod;
|
||||
public Pod? SelectedPod
|
||||
{
|
||||
get => _selectedPod;
|
||||
set => this.RaiseAndSetIfChanged(ref _selectedPod, value);
|
||||
}
|
||||
|
||||
private readonly ObservableAsPropertyHelper<IEnumerable<FileInformation>> _fileInformation;
|
||||
public IEnumerable<FileInformation> FileInformation => _fileInformation.Value;
|
||||
|
||||
private FileInformation? _selectedFile;
|
||||
public FileInformation? SelectedFile
|
||||
{
|
||||
get => _selectedFile;
|
||||
set => this.RaiseAndSetIfChanged(ref _selectedFile, value);
|
||||
}
|
||||
|
||||
private string? _selectedPath;
|
||||
public string? SelectedPath
|
||||
{
|
||||
get => _selectedPath;
|
||||
set => this.RaiseAndSetIfChanged(ref _selectedPath, value);
|
||||
}
|
||||
|
||||
public ReactiveCommand<Unit, Unit> DownloadCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> ParentCommand { get; }
|
||||
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
|
||||
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
var kubernetesService = new KubernetesService();
|
||||
|
||||
var isFile = this
|
||||
.WhenAnyValue(x => x.SelectedFile)
|
||||
.Select(x => x is { Type: FileType.File });
|
||||
|
||||
var isDirectory = this
|
||||
.WhenAnyValue(x => x.SelectedFile)
|
||||
.Select(x => x is { Type: FileType.Directory });
|
||||
|
||||
var isNotRoot = this
|
||||
.WhenAnyValue(x => x.SelectedPath)
|
||||
.Select(x => x is not "/");
|
||||
|
||||
OpenCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
SelectedPath = SelectedFile != null ? SelectedFile!.Name : "/";
|
||||
}, isDirectory, RxApp.MainThreadScheduler);
|
||||
|
||||
DownloadCommand = ReactiveCommand.CreateFromTask(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);
|
||||
|
||||
}, isFile, RxApp.TaskpoolScheduler);
|
||||
|
||||
ParentCommand = ReactiveCommand.Create(() =>
|
||||
{
|
||||
SelectedPath = SelectedPath![..SelectedPath!.LastIndexOf('/')];
|
||||
if (SelectedPath!.Length == 0)
|
||||
{
|
||||
SelectedPath = "/";
|
||||
}
|
||||
}, isNotRoot, RxApp.MainThreadScheduler);
|
||||
|
||||
// read the cluster contexts
|
||||
_namespaces = this
|
||||
.WhenAnyValue(c => c.SelectedClusterContext)
|
||||
.Throttle(TimeSpan.FromMilliseconds(10))
|
||||
.Where(context => context != null)
|
||||
.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)
|
||||
.Throttle(TimeSpan.FromMilliseconds(10))
|
||||
.Where(ns => ns != null)
|
||||
.Select(ns => kubernetesService.GetPods(ns!.Name))
|
||||
.ObserveOn(RxApp.MainThreadScheduler)
|
||||
.ToProperty(this, x => x.Pods);
|
||||
|
||||
// read the file information when the path changes
|
||||
_fileInformation = this
|
||||
.WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace)
|
||||
.Throttle(TimeSpan.FromMilliseconds(10))
|
||||
.Select(x => x.Item3 == null || x.Item2 == null
|
||||
? new List<FileInformation>()
|
||||
: kubernetesService.GetFiles(x.Item3!.Name, x.Item2!.Name, x.Item2!.Containers.First(),
|
||||
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 = "/");
|
||||
|
||||
// load the cluster contexts when the view model is created
|
||||
var loadContexts = ReactiveCommand
|
||||
.Create<Unit, IEnumerable<ClusterContext>>(_ => kubernetesService.GetClusterContexts());
|
||||
_clusterContexts = loadContexts.Execute().ToProperty(
|
||||
this, x => x.ClusterContexts, scheduler: RxApp.MainThreadScheduler);
|
||||
|
||||
// select the current cluster context
|
||||
SelectedClusterContext = ClusterContexts
|
||||
.FirstOrDefault(x => x.Name == kubernetesService.GetCurrentContext());
|
||||
}
|
||||
}
|
||||
7
K8sFileBrowser/ViewModels/ViewModelBase.cs
Normal file
7
K8sFileBrowser/ViewModels/ViewModelBase.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using ReactiveUI;
|
||||
|
||||
namespace K8sFileBrowser.ViewModels;
|
||||
|
||||
public class ViewModelBase : ReactiveObject
|
||||
{
|
||||
}
|
||||
103
K8sFileBrowser/Views/MainWindow.axaml
Normal file
103
K8sFileBrowser/Views/MainWindow.axaml
Normal file
@@ -0,0 +1,103 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:K8sFileBrowser.ViewModels"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
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">
|
||||
|
||||
<Design.DataContext>
|
||||
<vm:MainWindowViewModel />
|
||||
</Design.DataContext>
|
||||
|
||||
<Grid RowDefinitions="Auto, *">
|
||||
|
||||
<Border Padding="10 14" Background="#21252b">
|
||||
<Grid ColumnDefinitions="Auto,Auto,Auto,*">
|
||||
<Label Grid.Column="0"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0">
|
||||
Cluster:
|
||||
</Label>
|
||||
<ComboBox Grid.Column="1"
|
||||
ItemsSource="{Binding ClusterContexts}"
|
||||
SelectedItem="{Binding SelectedClusterContext}"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="200"
|
||||
Margin="0 0 10 0">
|
||||
</ComboBox>
|
||||
<Label Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 10 0">
|
||||
Namespace:
|
||||
</Label>
|
||||
<ComboBox Grid.Column="3"
|
||||
ItemsSource="{Binding Namespaces}"
|
||||
SelectedItem="{Binding SelectedNamespace}"
|
||||
VerticalAlignment="Center"
|
||||
MinWidth="200"
|
||||
Margin="0 0 10 0">
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
<Grid ColumnDefinitions="*, 1, 3*" Grid.Row="1">
|
||||
<ListBox
|
||||
ItemsSource="{Binding Pods}"
|
||||
SelectedItem="{Binding SelectedPod}" Background="Transparent">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
</Style>
|
||||
</ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<Border CornerRadius="0" Padding="0 4 0 4" BorderBrush="SlateGray"
|
||||
BorderThickness="0 0 0 1">
|
||||
<TextBlock Text="{Binding}" Padding="4" Margin="4" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
<GridSplitter Grid.Column="1" ResizeDirection="Columns" />
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto, *">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4" Margin="10">
|
||||
<Button Command="{Binding ParentCommand}" VerticalAlignment="Center">Parent Directory</Button>
|
||||
<Button Command="{Binding OpenCommand}" VerticalAlignment="Center">Open Directory</Button>
|
||||
<Button Command="{Binding DownloadCommand}" VerticalAlignment="Center">Download</Button>
|
||||
<TextBlock Text="{Binding SelectedPath}" VerticalAlignment="Center" Margin="10 0 0 0"></TextBlock>
|
||||
</StackPanel>
|
||||
<DataGrid Grid.Row="1"
|
||||
Name="FileInformationDataGrid"
|
||||
Margin="2 0 2 0"
|
||||
ItemsSource="{Binding FileInformation}"
|
||||
IsReadOnly="True"
|
||||
GridLinesVisibility="Horizontal"
|
||||
BorderThickness="1"
|
||||
SelectionMode="Single"
|
||||
SelectedItem="{Binding SelectedFile}">
|
||||
<DataGrid.Styles>
|
||||
<Style Selector="DataGridColumnHeader">
|
||||
<Setter Property="FontSize" Value="14"></Setter>
|
||||
<Setter Property="Padding" Value="10"></Setter>
|
||||
</Style>
|
||||
<Style Selector="DataGridCell">
|
||||
<Setter Property="FontSize" Value="12"></Setter>
|
||||
</Style>
|
||||
</DataGrid.Styles>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Type" Binding="{Binding Type}"/>
|
||||
<DataGridTextColumn Header="Name" Width="*" Binding="{Binding Name}" />
|
||||
<DataGridTextColumn Header="Size" Binding="{Binding Size}" />
|
||||
<DataGridTextColumn Header="Date" Binding="{Binding Date}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</Window>
|
||||
11
K8sFileBrowser/Views/MainWindow.axaml.cs
Normal file
11
K8sFileBrowser/Views/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace K8sFileBrowser.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
18
K8sFileBrowser/app.manifest
Normal file
18
K8sFileBrowser/app.manifest
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
|
||||
<!-- This manifest is used on Windows only.
|
||||
Don't remove it as it might cause problems with window transparency and embeded controls.
|
||||
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
|
||||
<assemblyIdentity version="1.0.0.0" name="K8sFileBrowser.Desktop"/>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- A list of the Windows versions that this application has been tested on
|
||||
and is designed to work with. Uncomment the appropriate elements
|
||||
and Windows will automatically select the most compatible environment. -->
|
||||
|
||||
<!-- Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
|
||||
</application>
|
||||
</compatibility>
|
||||
</assembly>
|
||||
Reference in New Issue
Block a user