mirror of
https://github.com/frosch95/K8sFileBrowser.git
synced 2026-04-11 12:58: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