14 Commits

Author SHA1 Message Date
249e74eb7b .gitignore 2025-03-13 03:22:11 +01:00
9926c26cc3 fixed nuke build 2025-03-13 03:21:23 +01:00
f12b12a82c Build for Windows, MacOs and Linux (#11)
* macosx build
* update to .net8 and newest nugets
* added context menu to file list
* update version number
2025-03-13 03:04:42 +01:00
d405677420 Merge pull request #10 from frosch95/pr-9
Refresh button for the file list
2023-10-09 20:30:31 +02:00
0e979fad5f Closes #9 : Refresh button for the file list 2023-10-09 20:29:40 +02:00
6754210e9b Merge pull request #7 from frosch95/pr-6
Sort properties in alphabatically order
2023-10-05 14:37:30 +02:00
0d11fdd000 Closes #6 Sort properties in alphabatically order 2023-10-05 14:22:18 +02:00
b33552531a remember last chosen local directory 2023-08-28 20:26:41 +02:00
7f7471d47b show message also in ui thread 2023-08-14 20:30:25 +02:00
6d03c88261 reworked the whole reactive property handling 2023-08-12 00:31:37 +02:00
a696152667 fix broken build 2023-08-11 14:31:07 +02:00
491127d460 fix broken path selection 2023-08-11 14:30:20 +02:00
551b77f5a1 fix reactive resets 2023-08-11 13:09:27 +02:00
7932583d9d better description of the tool 2023-08-10 22:26:18 +02:00
33 changed files with 440 additions and 203 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

3
.gitignore vendored
View File

@@ -4,4 +4,5 @@ obj/
riderModule.iml
/_ReSharper.Caches/
.idea/
*.user
*.user
.nuke/temp

View File

@@ -1,19 +1,48 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/build",
"title": "Build Schema",
"definitions": {
"build": {
"type": "object",
"Host": {
"type": "string",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
},
"ExecutableTarget": {
"type": "string",
"enum": [
"Clean",
"Publish",
"PublishLinux",
"PublishOsx",
"PublishWin"
]
},
"Verbosity": {
"type": "string",
"description": "",
"enum": [
"Verbose",
"Normal",
"Minimal",
"Quiet"
]
},
"NukeBuild": {
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Debug",
"Release"
]
},
"Continue": {
"type": "boolean",
"description": "Indicates to continue a previously failed build attempt"
@@ -23,25 +52,8 @@
"description": "Shows the help text for this build assembly"
},
"Host": {
"type": "string",
"description": "Host for execution. Default is 'automatic'",
"enum": [
"AppVeyor",
"AzurePipelines",
"Bamboo",
"Bitbucket",
"Bitrise",
"GitHubActions",
"GitLab",
"Jenkins",
"Rider",
"SpaceAutomation",
"TeamCity",
"Terminal",
"TravisCI",
"VisualStudio",
"VSCode"
]
"$ref": "#/definitions/Host"
},
"NoLogo": {
"type": "boolean",
@@ -70,44 +82,41 @@
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"Clean",
"Publish",
"PublishLinux",
"PublishWin",
"Restore"
]
"$ref": "#/definitions/ExecutableTarget"
}
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"Clean",
"Publish",
"PublishLinux",
"PublishWin",
"Restore"
]
"$ref": "#/definitions/ExecutableTarget"
}
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"$ref": "#/definitions/Verbosity"
}
}
}
},
"allOf": [
{
"properties": {
"Configuration": {
"type": "string",
"description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
"Debug",
"Release"
]
},
"Version": {
"type": "string"
}
}
},
{
"$ref": "#/definitions/NukeBuild"
}
}
]
}

BIN
Assets/.DS_Store vendored Normal file

Binary file not shown.

BIN
Assets/favicon_io.zip Normal file

Binary file not shown.

BIN
Assets/favicon_io/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -0,0 +1,6 @@
This favicon was generated using the following font:
- Font Title: Genos
- Font Author: Copyright 2011 The Genos Project Authors (https://github.com/googlefonts/genos)
- Font Source: http://fonts.gstatic.com/s/genos/v12/SlGNmQqPqpUOYTYjacb0Hc91fTwVA0_orUK6K7ZsAg.ttf
- Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL))

View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

View File

@@ -12,6 +12,7 @@ Global
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{C0C29CAE-A5C1-43B8-BFF8-BFE718FE04E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{C0C29CAE-A5C1-43B8-BFF8-BFE718FE04E8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C0C29CAE-A5C1-43B8-BFF8-BFE718FE04E8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{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

BIN
K8sFileBrowser/.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -10,7 +10,7 @@
</Application.DataTemplates>
<Application.Styles>
<FluentTheme>
<FluentTheme DensityStyle="Compact">
<FluentTheme.Palettes>
<!-- Palette for Light theme variant -->
<ColorPaletteResources x:Key="Light" Accent="Green" RegionColor="White" ErrorText="Red" />
@@ -20,7 +20,7 @@
Accent="#677696"
RegionColor="#282c34"
ErrorText="Red"
AltHigh="#282c34"
AltHigh="#343a45"
AltMediumLow="#2c313c"
ListLow="#21252b"
ListMedium="#2c313c"
@@ -29,6 +29,7 @@
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 -->

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 708 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
@@ -9,8 +9,8 @@
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU</Platforms>
<ApplicationIcon>Assets/app.ico</ApplicationIcon>
<Version>0.0.9</Version>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
<Version>0.1.4</Version>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>TRACE</DefineConstants>
@@ -22,21 +22,21 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.0.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.2" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.2" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.2" />
<PackageReference Include="Avalonia" Version="11.2.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.5" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.5" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.5" />
<!--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.2" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.2" />
<PackageReference Include="Avalonia.Xaml.Interactions" Version="11.0.2" />
<PackageReference Include="Avalonia.Xaml.Interactivity" Version="11.0.2" />
<PackageReference Include="KubernetesClient" Version="11.0.44" />
<PackageReference Include="ReactiveUI.Fody" Version="19.4.1" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Async" Version="1.5.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.2.5" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.5" />
<PackageReference Include="Avalonia.Xaml.Interactions" Version="11.2.0.14" />
<PackageReference Include="Avalonia.Xaml.Interactivity" Version="11.2.0.14" />
<PackageReference Include="KubernetesClient" Version="16.0.2" />
<PackageReference Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageReference Include="Serilog" Version="4.2.0" />
<PackageReference Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageReference Include="SharpZipLib" Version="1.4.2" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,7 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
using Avalonia.Logging;
using Serilog;
namespace K8sFileBrowser;
@@ -18,6 +19,7 @@ class Program
//.Filter.ByIncludingOnly(Matching.WithProperty("Area", LogArea.Control))
.MinimumLevel.Information()
.WriteTo.Async(a => a.File("app.log"))
//.WriteTo.Console()
.CreateLogger();
BuildAvaloniaApp()
@@ -42,5 +44,6 @@ class Program
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
//.LogToTrace(LogEventLevel.Verbose)
.UseReactiveUI();
}

View File

@@ -1,33 +1,43 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reflection;
using System.Threading.Tasks;
using K8sFileBrowser.Models;
using K8sFileBrowser.Services;
using Microsoft.IdentityModel.Tokens;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Serilog;
namespace K8sFileBrowser.ViewModels;
public class MainWindowViewModel : ViewModelBase
{
private ObservableAsPropertyHelper<IEnumerable<ClusterContext>> _clusterContexts = null!;
public IEnumerable<ClusterContext> ClusterContexts => _clusterContexts.Value;
#region Properties
[Reactive]
public string? Version { get; set; }
[Reactive]
public IEnumerable<ClusterContext> ClusterContexts { get; set; } = null!;
[Reactive]
public ClusterContext? SelectedClusterContext { get; set; }
[Reactive]
public IEnumerable<Namespace> Namespaces { get; set; }
public IEnumerable<Namespace> Namespaces { get; set; } = null!;
[Reactive]
public Namespace? SelectedNamespace { get; set; }
private ObservableAsPropertyHelper<IEnumerable<Pod>> _pods = null!;
public IEnumerable<Pod> Pods => _pods.Value;
[Reactive]
public IEnumerable<Pod> Pods { get; set; } = null!;
[Reactive]
public Pod? SelectedPod { get; set; }
@@ -38,8 +48,8 @@ public class MainWindowViewModel : ViewModelBase
[Reactive]
public Container? SelectedContainer { get; set; }
private ObservableAsPropertyHelper<IEnumerable<FileInformation>> _fileInformation = null!;
public IEnumerable<FileInformation> FileInformation => _fileInformation.Value;
[Reactive]
public IEnumerable<FileInformation> FileInformation { get; set; } = null!;
[Reactive]
public FileInformation? SelectedFile { get; set; }
@@ -48,24 +58,38 @@ public class MainWindowViewModel : ViewModelBase
public string? SelectedPath { get; set; }
[Reactive]
public Message Message { get; set; }
public Message Message { get; set; } = null!;
private string _lastDirectory = ".";
#endregion Properties
#region Commands
public ReactiveCommand<Unit, Unit> DownloadCommand { get; private set; } = null!;
public ReactiveCommand<Unit, Unit> DownloadLogCommand { get; private set; } = null!;
public ReactiveCommand<Unit, Unit> RefreshCommand { get; private set; } = null!;
public ReactiveCommand<Unit, Unit> ParentCommand { get; private set; } = null!;
public ReactiveCommand<Unit, Unit> OpenCommand { get; private set; } = null!;
public ReactiveCommand<FileInformation, Unit> OpenContextCommand { get; private set; } = null!;
public ReactiveCommand<FileInformation, Unit> DownloadContextCommand { get; private set; } = null!;
private ReactiveCommand<Namespace, IEnumerable<Pod>> GetPodsForNamespace { get; set; } = null!;
#endregion Commands
public MainWindowViewModel()
{
//TODO: use dependency injection to get the kubernetes service
IKubernetesService kubernetesService = new KubernetesService();
Version = Assembly.GetExecutingAssembly().GetName().Version?.ToString();
// commands
ConfigureOpenDirectoryCommand();
ConfigureOpenDirectoryContextCommand();
ConfigureDownloadFileCommand(kubernetesService);
ConfigureDownloadFileContextCommand(kubernetesService);
ConfigureRefreshCommand(kubernetesService);
ConfigureDownloadLogCommand(kubernetesService);
ConfigureParentDirectoryCommand();
ConfigureGetPodsForNamespaceCommand(kubernetesService);
@@ -81,68 +105,25 @@ public class MainWindowViewModel : ViewModelBase
InitiallyLoadContexts(kubernetesService);
}
#region Property Subscriptions
private void InitiallyLoadContexts(IKubernetesService kubernetesService)
{
// 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());
}
private void RegisterResetPath()
{
// reset the path when the pod or namespace changes
this.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace)
.Throttle(new TimeSpan(10))
.ObserveOn(RxApp.TaskpoolScheduler)
.Subscribe(_ => SelectedPath = "/");
}
private void RegisterReadContainers()
{
// read the file information when the path changes
this
.WhenAnyValue(c => c.SelectedPod, c => c.SelectedNamespace)
.Throttle(new TimeSpan(10))
.Select(x => x.Item2 == null || x.Item1 == null
? new List<Container>()
: x.Item1.Containers.Select(c => new Container {Name = c}))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe( x => Containers = x);
this.WhenAnyValue(x => x.Containers)
loadContexts.Execute()
.Throttle(new TimeSpan(10))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => SelectedContainer = x?.FirstOrDefault());
}
.Subscribe(x =>
{
ResetNamespaces();
ClusterContexts = x.OrderBy(c => c.Name);
private void RegisterReadFiles(IKubernetesService kubernetesService)
{
// read the file information when the path changes
_fileInformation = this
.WhenAnyValue(c => c.SelectedPath, c => c.SelectedPod, c => c.SelectedNamespace, c => c.SelectedContainer)
.Throttle(new TimeSpan(10))
.Select(x => x.Item3 == null || x.Item2 == null || x.Item1 == null || x.Item4 == null
? new List<FileInformation>()
: GetFileInformation(kubernetesService, x.Item1, x.Item2, x.Item3, x.Item4))
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.FileInformation);
}
private void RegisterReadPods()
{
// read the pods when the namespace changes
_pods = this
.WhenAnyValue(c => c.SelectedNamespace)
.Throttle(new TimeSpan(10))
.SelectMany(ns => GetPodsForNamespace.Execute(ns!))
.ObserveOn(RxApp.MainThreadScheduler)
.ToProperty(this, x => x.Pods);
// select the current cluster context
SelectedClusterContext = ClusterContexts
.FirstOrDefault(c => c.Name == kubernetesService.GetCurrentContext());
});
}
private void RegisterReadNamespaces(IKubernetesService kubernetesService)
@@ -153,16 +134,82 @@ public class MainWindowViewModel : ViewModelBase
.Throttle(new TimeSpan(10))
.SelectMany(context => GetClusterContextAsync(context, kubernetesService))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ns => Namespaces = ns);
.Subscribe(ns =>
{
ResetPods();
Namespaces = ns.OrderBy(n => n.Name);
});
}
private void RegisterReadPods()
{
// read the pods when the namespace changes
this
.WhenAnyValue(c => c.SelectedNamespace)
.Throttle(new TimeSpan(10))
.Where(x => x != null)
.SelectMany(ns => GetPodsForNamespace.Execute(ns!))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x =>
{
ResetContainers();
Pods = x.OrderBy(p => p.Name);
});
}
private void RegisterReadContainers()
{
// read the file information when the path changes
this
.WhenAnyValue(c => c.SelectedPod)
.Throttle(new TimeSpan(10))
.Where(x => x != null)
.Select(x => x!.Containers.Select(c => new Container {Name = c}))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe( x =>
{
ResetPath();
Containers = x;
});
this.WhenAnyValue(x => x.Containers)
.Throttle(new TimeSpan(10))
.Where(x => x != null && x.Any())
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => SelectedContainer = x?.FirstOrDefault());
}
private void RegisterResetPath()
{
// reset the path when the pod or namespace changes
this.WhenAnyValue(c => c.SelectedContainer)
.Throttle(new TimeSpan(10))
.Where(x => x != null)
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(_ => SelectedPath = "/");
}
private void RegisterReadFiles(IKubernetesService kubernetesService)
{
// read the file information when the path changes
this
.WhenAnyValue(c => c.SelectedContainer, c => c.SelectedPath)
.Throttle(new TimeSpan(10))
.Where(x => x is { Item1: not null, Item2: not null })
.Select(x => GetFileInformation(kubernetesService, x.Item2!, SelectedPod!, SelectedNamespace!, x.Item1!))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => FileInformation = x);
}
#endregion Property Subscriptions
#region Configure Commands
private void ConfigureGetPodsForNamespaceCommand(IKubernetesService kubernetesService)
{
GetPodsForNamespace = ReactiveCommand.CreateFromObservable<Namespace, IEnumerable<Pod>>(ns =>
Observable.StartAsync(_ => PodsAsync(ns, kubernetesService), RxApp.TaskpoolScheduler));
GetPodsForNamespace.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex => ShowErrorMessage(ex.Message).ConfigureAwait(false).GetAwaiter().GetResult());
.Subscribe(ShowErrorMessage);
}
private void ConfigureParentDirectoryCommand()
@@ -181,7 +228,7 @@ public class MainWindowViewModel : ViewModelBase
}, isNotRoot, RxApp.MainThreadScheduler);
ParentCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex => ShowErrorMessage(ex.Message).ConfigureAwait(false).GetAwaiter().GetResult());
.Subscribe(ShowErrorMessage);
}
private void ConfigureDownloadLogCommand(IKubernetesService kubernetesService)
@@ -195,9 +242,10 @@ public class MainWindowViewModel : ViewModelBase
await Observable.StartAsync(async () =>
{
var fileName = SelectedPod?.Name + ".log";
var saveFileName = await ApplicationHelper.SaveFile(".", fileName);
var saveFileName = await ApplicationHelper.SaveFile(_lastDirectory, fileName);
if (saveFileName != null)
{
SetLastDirectory(saveFileName);
ShowWorkingMessage("Downloading Log...");
await kubernetesService.DownloadLog(SelectedNamespace, SelectedPod, SelectedContainer, saveFileName);
HideWorkingMessage();
@@ -206,7 +254,25 @@ public class MainWindowViewModel : ViewModelBase
}, isSelectedPod, RxApp.MainThreadScheduler);
DownloadLogCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex => ShowErrorMessage(ex.Message).ConfigureAwait(false).GetAwaiter().GetResult());
.Subscribe(ShowErrorMessage);
}
private void ConfigureRefreshCommand(IKubernetesService kubernetesService)
{
var isSelectedContainer = this
.WhenAnyValue(x => x.SelectedContainer)
.Select(x => x != null);
RefreshCommand = ReactiveCommand.CreateFromTask(async () =>
{
await Observable.Start(() =>
{
FileInformation = GetFileInformation(kubernetesService, SelectedPath!, SelectedPod!, SelectedNamespace!, SelectedContainer!);
}, RxApp.TaskpoolScheduler);
}, isSelectedContainer, RxApp.MainThreadScheduler);
RefreshCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ShowErrorMessage);
}
private void ConfigureDownloadFileCommand(IKubernetesService kubernetesService)
@@ -221,9 +287,10 @@ public class MainWindowViewModel : ViewModelBase
{
var fileName = SelectedFile!.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1,
SelectedFile!.Name.Length - SelectedFile!.Name.LastIndexOf('/') - 1);
var saveFileName = await ApplicationHelper.SaveFile(".", fileName);
var saveFileName = await ApplicationHelper.SaveFile(_lastDirectory, fileName);
if (saveFileName != null)
{
SetLastDirectory(saveFileName);
ShowWorkingMessage("Downloading File...");
await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedContainer, SelectedFile, saveFileName);
HideWorkingMessage();
@@ -232,7 +299,36 @@ public class MainWindowViewModel : ViewModelBase
}, isFile, RxApp.MainThreadScheduler);
DownloadCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex => ShowErrorMessage(ex.Message).ConfigureAwait(false).GetAwaiter().GetResult());
.Subscribe(ShowErrorMessage);
}
private void ConfigureDownloadFileContextCommand(IKubernetesService kubernetesService)
{
DownloadContextCommand = ReactiveCommand.CreateFromTask<FileInformation>(async (file) =>
{
await Observable.StartAsync(async () =>
{
var fileName = file.Name.Substring(SelectedFile!.Name.LastIndexOf('/') + 1,
file.Name.Length - file.Name.LastIndexOf('/') - 1);
var saveFileName = await ApplicationHelper.SaveFile(_lastDirectory, fileName);
if (saveFileName != null)
{
SetLastDirectory(saveFileName);
ShowWorkingMessage("Downloading File...");
await kubernetesService.DownloadFile(SelectedNamespace, SelectedPod, SelectedContainer, file, saveFileName);
HideWorkingMessage();
}
}, RxApp.TaskpoolScheduler);
}, outputScheduler: RxApp.MainThreadScheduler);
DownloadCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ShowErrorMessage);
}
private void SetLastDirectory(string saveFileName)
{
_lastDirectory = saveFileName.Substring(0, saveFileName.LastIndexOf(Path.DirectorySeparatorChar));
}
private void ConfigureOpenDirectoryCommand()
@@ -251,8 +347,23 @@ public class MainWindowViewModel : ViewModelBase
isDirectory, RxApp.MainThreadScheduler);
OpenCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex => ShowErrorMessage(ex.Message).ConfigureAwait(false).GetAwaiter().GetResult());
.Subscribe(ShowErrorMessage);
}
private void ConfigureOpenDirectoryContextCommand()
{
OpenContextCommand = ReactiveCommand.Create<FileInformation>((file) =>
{
SelectedPath = ".." == file.Name ? file.Parent : file.Name;
}, outputScheduler: RxApp.MainThreadScheduler);
OpenCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ShowErrorMessage);
}
#endregion Configure Commands
#region Get Data
private static async Task<IEnumerable<Pod>> PodsAsync(Namespace? ns, IKubernetesService kubernetesService)
{
@@ -277,10 +388,8 @@ public class MainWindowViewModel : ViewModelBase
}
catch (Exception e)
{
RxApp.MainThreadScheduler.Schedule(Action);
ShowErrorMessage(e);
return new List<Namespace>();
async void Action() => await ShowErrorMessage(e.Message);
}
}
@@ -308,35 +417,89 @@ public class MainWindowViewModel : ViewModelBase
}).ToList();
}
private void ShowWorkingMessage(string message)
#endregion Get Data
#region Reset Data
private void ResetPath()
{
Message = new Message
{
IsVisible = true,
Text = message,
IsError = false
};
FileInformation = new List<FileInformation>();
SelectedPath = null;
SelectedContainer = null;
}
private async Task ShowErrorMessage(string message)
private void ResetContainers()
{
Message = new Message
ResetPath();
Containers = new List<Container>();
SelectedPod = null;
}
private void ResetPods()
{
ResetContainers();
SelectedNamespace = null;
Pods = new List<Pod>();
}
private void ResetNamespaces()
{
ResetPods();
Namespaces = new List<Namespace>();
SelectedClusterContext = null;
}
#endregion Reset Data
#region show messages
private void ShowWorkingMessage(string message)
{
RxApp.MainThreadScheduler.Schedule(Action);
return;
void Action()
{
IsVisible = true,
Text = message,
IsError = true
};
await Task.Delay(7000);
HideWorkingMessage();
Message = new Message
{
IsVisible = true,
Text = message,
IsError = false
};
}
}
private void ShowErrorMessage(string message)
{
RxApp.MainThreadScheduler.Schedule(Action);
return;
async void Action()
{
Message = new Message { IsVisible = true, Text = message, IsError = true };
await Task.Delay(7000);
HideWorkingMessage();
}
}
private void ShowErrorMessage(Exception exception)
{
// ReSharper disable once TemplateIsNotCompileTimeConstantProblem
Log.Error(exception, exception.Message);
ShowErrorMessage(exception.Message);
}
private void HideWorkingMessage()
{
Message = new Message
RxApp.MainThreadScheduler.Schedule(() => Message = new Message
{
IsVisible = false,
Text = "",
IsError = false
};
});
}
#endregion show messages
}

View File

@@ -24,7 +24,7 @@
</Border>
<Grid RowDefinitions="Auto, *">
<Border Padding="10 14" Background="#21252b">
<Grid ColumnDefinitions="Auto,Auto,Auto,*">
<Grid ColumnDefinitions="Auto,Auto,Auto,*,Auto">
<Label Grid.Column="0"
VerticalAlignment="Center"
Margin="0 0 10 0">
@@ -49,6 +49,7 @@
MinWidth="200"
Margin="0 0 10 0">
</ComboBox>
<TextBlock Grid.Column="4" HorizontalAlignment="Right" VerticalAlignment="Center" Text="{Binding Version}"/>
</Grid>
</Border>
@@ -93,6 +94,9 @@
<Button Command="{Binding DownloadLogCommand}" VerticalAlignment="Center" ToolTip.Tip="Download Container Log" Margin="0 0 48 0 ">
<PathIcon Data="{StaticResource document_one_page_regular}"></PathIcon>
</Button>
<Button Command="{Binding RefreshCommand}" VerticalAlignment="Center" ToolTip.Tip="Refresh Directory">
<PathIcon Data="{StaticResource arrow_sync_circle_regular}"></PathIcon>
</Button>
<Button Command="{Binding ParentCommand}" VerticalAlignment="Center" ToolTip.Tip="Go To Parent Directory">
<PathIcon Data="{StaticResource arrow_curve_up_left_regular}"></PathIcon>
</Button>
@@ -147,7 +151,19 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="models:FileInformation">
<Border Background="Transparent">
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center"/>
<Border.ContextFlyout>
<MenuFlyout>
<MenuItem Header="Open"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).OpenContextCommand}"
CommandParameter="{Binding}"
IsEnabled="{Binding IsDirectory}"/>
<MenuItem Header="Download"
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).DownloadContextCommand}"
CommandParameter="{Binding}"
IsEnabled="{Binding IsFile}"/>
</MenuFlyout>
</Border.ContextFlyout>
<TextBlock Text="{Binding DisplayName}" VerticalAlignment="Center" />
<Interaction.Behaviors>
<EventTriggerBehavior EventName="DoubleTapped">
<InvokeCommandAction Command="{Binding ((vm:MainWindowViewModel)DataContext).OpenCommand, RelativeSource={RelativeSource AncestorType=Window }}" />

View File

@@ -3,7 +3,7 @@
<!-- 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"/>
<assemblyIdentity version="1.4.0.0" name="K8sFileBrowser.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>

View File

@@ -3,4 +3,14 @@
A UI tool for downloading files from a Pod.
The application is also the first Avalonia UI and C# UI app I have written.
## Usage
Just start the executable and select the cluster, namespace and pod you want to download files from.
Then select the files you want to download and click the download button.
The available clusters are read from the `~/.kube/config` file.
## Limitations
It only works on linux containers and the container must support `find` and `tar`.
![screenshot of K8sFileBrowser](https://github.com/frosch95/K8sFileBrowser/blob/master/screenshot.png?raw=true)

View File

@@ -11,40 +11,33 @@ class Build : NukeBuild
{
[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;
[Parameter] readonly string Version = "1.0.0";
[Parameter] readonly string Version = "1.4.0";
AbsolutePath SourceDirectory => RootDirectory / "K8sFileBrowser";
AbsolutePath OutputDirectory => RootDirectory / "output";
AbsolutePath WinOutputDirectory => OutputDirectory / "win";
AbsolutePath LinuxOutputDirectory => OutputDirectory / "linux";
AbsolutePath OsxOutputDirectory => OutputDirectory / "osx";
AbsolutePath WinZip => OutputDirectory / $"K8sFileBrowser_{Version}.zip";
AbsolutePath LinuxGz => OutputDirectory / $"K8sFileBrowser_{Version}.tgz";
AbsolutePath OsxGz => OutputDirectory / $"K8sFileBrowser_OSX_{Version}.tgz";
AbsolutePath ProjectFile => SourceDirectory / "K8sFileBrowser.csproj";
readonly string ExcludedExtensions = "pdb";
public static int Main () => Execute<Build>(x => x.Publish);
Target Clean => _ => _
.Before(Restore)
.Executes(() =>
{
OutputDirectory.DeleteDirectory();
});
Target Restore => _ => _
.Executes(() =>
{
DotNet($"restore {ProjectFile}");
//DotNetTasks.DotNetRestore(new DotNetRestoreSettings());
});
Target PublishWin => _ => _
.DependsOn(Clean)
.Executes(() =>
@@ -53,55 +46,78 @@ class Build : NukeBuild
.SetProject(ProjectFile)
.SetConfiguration(Configuration)
.SetOutput(WinOutputDirectory)
.SetSelfContained(true)
.SetFramework("net7.0")
.EnableSelfContained()
.SetFramework("net8.0")
.SetRuntime("win-x64")
.SetPublishSingleFile(true)
.SetPublishReadyToRun(true)
.EnablePublishSingleFile()
.EnablePublishReadyToRun()
.SetAuthors("Andreas Billmann")
.SetCopyright("Copyright (c) 2023")
.SetVersion(Version)
.SetProcessArgumentConfigurator(_ => _
.Add("-p:IncludeNativeLibrariesForSelfExtract=true"))
.EnableNoRestore());
.Add("-p:IncludeNativeLibrariesForSelfExtract=true")));
WinOutputDirectory.ZipTo(
WinZip,
filter: x => !x.HasExtension(ExcludedExtensions),
compressionLevel: CompressionLevel.SmallestSize,
fileMode: FileMode.CreateNew);
});
Target PublishLinux => _ => _
.DependsOn(Clean)
.DependsOn(Clean)
.Executes(() =>
{
DotNetPublish(s => s
.SetProject(ProjectFile)
.SetConfiguration(Configuration)
.SetOutput(LinuxOutputDirectory)
.SetSelfContained(true)
.SetFramework("net7.0")
.EnableSelfContained()
.SetFramework("net8.0")
.SetRuntime("linux-x64")
.SetPublishSingleFile(true)
.SetPublishReadyToRun(true)
.EnablePublishSingleFile()
.EnablePublishReadyToRun()
.SetAuthors("Andreas Billmann")
.SetCopyright("Copyright (c) 2023")
.SetVersion(Version)
.SetProcessArgumentConfigurator(_ => _
.Add("-p:IncludeNativeLibrariesForSelfExtract=true"))
.EnableNoRestore());
.Add("-p:IncludeNativeLibrariesForSelfExtract=true")));
LinuxOutputDirectory.TarGZipTo(
LinuxGz,
filter: x => !x.HasExtension(ExcludedExtensions),
fileMode: FileMode.CreateNew);
});
Target PublishOsx => _ => _
.DependsOn(Clean)
.Executes(() =>
{
DotNetPublish(s => s
.SetProject(ProjectFile)
.SetConfiguration(Configuration)
.SetOutput(OsxOutputDirectory)
.EnableSelfContained()
.SetFramework("net8.0")
.SetRuntime("osx-arm64")
.EnablePublishSingleFile()
.EnablePublishReadyToRun()
.SetAuthors("Andreas Billmann")
.SetCopyright("Copyright (c) 2023")
.SetVersion(Version)
.SetProcessArgumentConfigurator(_ => _
.Add("-p:IncludeNativeLibrariesForSelfExtract=true")));
OsxOutputDirectory.TarGZipTo(
OsxGz,
filter: x => !x.HasExtension(ExcludedExtensions),
fileMode: FileMode.CreateNew);
});
Target Publish => _ => _
.DependsOn(PublishWin, PublishLinux)
.DependsOn(PublishWin, PublishLinux, PublishOsx)
.Executes(() =>
{
});
}
}

View File

@@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace></RootNamespace>
<NoWarn>CS0649;CS0169</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
@@ -11,7 +11,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="7.0.2" />
<PackageReference Include="Nuke.Common" Version="8.1.4" />
</ItemGroup>
</Project>

View File

@@ -17,6 +17,8 @@
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_SIMPLE_ANONYMOUSMETHOD_ON_SINGLE_LINE/@EntryValue">False</s:Boolean>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=4a98fdf6_002D7d98_002D4f5a_002Dafeb_002Dea44ad98c70c/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;Kind Name="READONLY_FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/UserRules/=f9fce829_002De6f4_002D4cb2_002D80f1_002D5497c44f51df/@EntryIndexedValue">&lt;Policy&gt;&lt;Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"&gt;&lt;ElementKinds&gt;&lt;Kind Name="FIELD" /&gt;&lt;/ElementKinds&gt;&lt;/Descriptor&gt;&lt;Policy Inspect="True" WarnAboutPrefixesAndSuffixes="False" Prefix="" Suffix="" Style="AaBb" /&gt;&lt;/Policy&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpAttributeForSingleLineMethodUpgrade/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
@@ -25,4 +27,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAddAccessorOwnerDeclarationBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateThisQualifierSettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EPredefinedNamingRulesToUserRulesUpgrade/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

7
global.json Normal file
View File

@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 150 KiB