First draft of kubernetes file browser

This commit is contained in:
2023-07-30 04:52:53 +02:00
commit 9fd5f5cbd8
20 changed files with 765 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
bin/
obj/
/packages/
riderModule.iml
/_ReSharper.Caches/
.idea/
*.user

16
K8sFileBrowser.sln Normal file
View 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
View 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>

View 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();
}
}

View 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;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View 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>

View File

@@ -0,0 +1,10 @@
namespace K8sFileBrowser.Models;
public class ClusterContext
{
public string Name { get; set; } = string.Empty;
public override string ToString()
{
return Name;
}
}

View 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
}

View File

@@ -0,0 +1,11 @@
namespace K8sFileBrowser.Models;
public class Namespace
{
public string Name { get; set; } = string.Empty;
public override string ToString()
{
return Name;
}
}

View 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
View 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();
}

View 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);
}
}

View 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
}
}

View 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;
}
}

View 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());
}
}

View File

@@ -0,0 +1,7 @@
using ReactiveUI;
namespace K8sFileBrowser.ViewModels;
public class ViewModelBase : ReactiveObject
{
}

View 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>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace K8sFileBrowser.Views;
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}

View 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>