16 Commits

22 changed files with 748 additions and 168 deletions

111
.nuke/build.schema.json Normal file
View File

@@ -0,0 +1,111 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/build",
"title": "Build Schema",
"definitions": {
"build": {
"type": "object",
"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"
},
"Help": {
"type": "boolean",
"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"
]
},
"NoLogo": {
"type": "boolean",
"description": "Disables displaying the NUKE logo"
},
"Partition": {
"type": "string",
"description": "Partition to use on CI"
},
"Plan": {
"type": "boolean",
"description": "Shows the execution plan (HTML)"
},
"Profile": {
"type": "array",
"description": "Defines the profiles to load",
"items": {
"type": "string"
}
},
"Root": {
"type": "string",
"description": "Root directory during build execution"
},
"Skip": {
"type": "array",
"description": "List of targets to be skipped. Empty list skips all dependencies",
"items": {
"type": "string",
"enum": [
"Clean",
"Publish",
"PublishLinux",
"PublishWin"
]
}
},
"Target": {
"type": "array",
"description": "List of targets to be invoked. Default is '{default_target}'",
"items": {
"type": "string",
"enum": [
"Clean",
"Publish",
"PublishLinux",
"PublishWin"
]
}
},
"Verbosity": {
"type": "string",
"description": "Logging verbosity during build execution. Default is 'Normal'",
"enum": [
"Minimal",
"Normal",
"Quiet",
"Verbose"
]
},
"Version": {
"type": "string"
}
}
}
}
}

4
.nuke/parameters.json Normal file
View File

@@ -0,0 +1,4 @@
{
"$schema": "./build.schema.json",
"Solution": "K8sFileBrowser.sln"
}

View File

@@ -2,12 +2,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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "build\_build.csproj", "{C0C29CAE-A5C1-43B8-BFF8-BFE718FE04E8}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
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
{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

View File

@@ -20,7 +20,7 @@
Accent="#677696"
RegionColor="#282c34"
ErrorText="Red"
AltHigh="#282c34"
AltHigh="#343a45"
AltMediumLow="#2c313c"
ListLow="#21252b"
ListMedium="#2c313c"

View File

@@ -0,0 +1,3 @@
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<ReactiveUI />
</Weavers>

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<!-- This file was generated by Fody. Manual changes to this file will be lost when your project is rebuilt. -->
<xs:element name="Weavers">
<xs:complexType>
<xs:all>
<xs:element name="ReactiveUI" minOccurs="0" maxOccurs="1" type="xs:anyType" />
</xs:all>
<xs:attribute name="VerifyAssembly" type="xs:boolean">
<xs:annotation>
<xs:documentation>'true' to run assembly verification (PEVerify) on the target assembly after all weavers have been executed.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="VerifyIgnoreCodes" type="xs:string">
<xs:annotation>
<xs:documentation>A comma-separated list of error codes that can be safely ignored in assembly verification.</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="GenerateXsd" type="xs:boolean">
<xs:annotation>
<xs:documentation>'false' to turn off automatic generation of the XML Schema file.</xs:documentation>
</xs:annotation>
</xs:attribute>
</xs:complexType>
</xs:element>
</xs:schema>

View File

@@ -9,7 +9,8 @@
<Configurations>Debug;Release</Configurations>
<Platforms>AnyCPU</Platforms>
<ApplicationIcon>Assets/app.ico</ApplicationIcon>
<Version>0.0.8</Version>
<Version>0.1.2</Version>
<RuntimeIdentifiers>win-x64;linux-x64</RuntimeIdentifiers>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DefineConstants>TRACE</DefineConstants>
@@ -31,6 +32,7 @@
<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" />

View File

@@ -19,6 +19,8 @@ public class FileInformation
public string Size { get; set; } = string.Empty;
public DateTimeOffset? Date { get; set; }
public string DateTimeOffsetString => Date?.ToString("yyyy-MM-dd HH:mm:ss") ?? string.Empty;
public bool IsFile => Type == FileType.File;
public bool IsDirectory => Type == FileType.Directory;
public bool IsUnknown => Type == FileType.Unknown;

View File

@@ -4,109 +4,86 @@ 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;
private ClusterContext? _selectedClusterContext;
#region Properties
public ClusterContext? SelectedClusterContext
{
get => _selectedClusterContext;
set => this.RaiseAndSetIfChanged(ref _selectedClusterContext, value);
}
[Reactive]
public string? Version { get; set; }
private IEnumerable<Namespace> _namespaces = null!;
public IEnumerable<Namespace> Namespaces
{
get => _namespaces;
set => this.RaiseAndSetIfChanged(ref _namespaces, value);
}
[Reactive]
public IEnumerable<ClusterContext> ClusterContexts { get; set; } = null!;
private Namespace? _selectedNamespace;
[Reactive]
public ClusterContext? SelectedClusterContext { get; set; }
public Namespace? SelectedNamespace
{
get => _selectedNamespace;
set => this.RaiseAndSetIfChanged(ref _selectedNamespace, value);
}
[Reactive]
public IEnumerable<Namespace> Namespaces { get; set; } = null!;
private ObservableAsPropertyHelper<IEnumerable<Pod>> _pods = null!;
public IEnumerable<Pod> Pods => _pods.Value;
[Reactive]
public Namespace? SelectedNamespace { get; set; }
private Pod? _selectedPod;
[Reactive]
public IEnumerable<Pod> Pods { get; set; } = null!;
public Pod? SelectedPod
{
get => _selectedPod;
set => this.RaiseAndSetIfChanged(ref _selectedPod, value);
}
[Reactive]
public Pod? SelectedPod { get; set; }
private IEnumerable<Container>? _containers;
public IEnumerable<Container>? Containers
{
get => _containers;
set => this.RaiseAndSetIfChanged(ref _containers, value);
}
[Reactive]
public IEnumerable<Container>? Containers { get; set; }
private Container? _selectedContainer;
[Reactive]
public Container? SelectedContainer { get; set; }
public Container? SelectedContainer
{
get => _selectedContainer;
set => this.RaiseAndSetIfChanged(ref _selectedContainer, value);
}
[Reactive]
public IEnumerable<FileInformation> FileInformation { get; set; } = null!;
private ObservableAsPropertyHelper<IEnumerable<FileInformation>> _fileInformation = null!;
public IEnumerable<FileInformation> FileInformation => _fileInformation.Value;
[Reactive]
public FileInformation? SelectedFile { get; set; }
private FileInformation? _selectedFile;
[Reactive]
public string? SelectedPath { get; set; }
public FileInformation? SelectedFile
{
get => _selectedFile;
set => this.RaiseAndSetIfChanged(ref _selectedFile, value);
}
[Reactive]
public Message Message { get; set; } = null!;
private string? _selectedPath;
private string _lastDirectory = ".";
public string? SelectedPath
{
get => _selectedPath;
set => this.RaiseAndSetIfChanged(ref _selectedPath, value);
}
#endregion Properties
private Message _message = null!;
public Message Message
{
get => _message;
set => this.RaiseAndSetIfChanged(ref _message, value);
}
#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!;
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();
ConfigureDownloadFileCommand(kubernetesService);
ConfigureRefreshCommand(kubernetesService);
ConfigureDownloadLogCommand(kubernetesService);
ConfigureParentDirectoryCommand();
ConfigureGetPodsForNamespaceCommand(kubernetesService);
@@ -122,68 +99,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);
loadContexts.Execute()
.Throttle(new TimeSpan(10))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x =>
{
ResetNamespaces();
ClusterContexts = x.OrderBy(c => c.Name);
// 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)
.Throttle(new TimeSpan(10))
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(x => SelectedContainer = x?.FirstOrDefault());
}
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);
.FirstOrDefault(c => c.Name == kubernetesService.GetCurrentContext());
});
}
private void RegisterReadNamespaces(IKubernetesService kubernetesService)
@@ -194,16 +128,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.IsNullOrEmpty())
.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()
@@ -222,7 +222,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)
@@ -236,9 +236,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();
@@ -247,7 +248,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)
@@ -262,9 +281,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();
@@ -273,7 +293,12 @@ 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 SetLastDirectory(string saveFileName)
{
_lastDirectory = saveFileName.Substring(0, saveFileName.LastIndexOf('\\'));
}
private void ConfigureOpenDirectoryCommand()
@@ -292,9 +317,13 @@ public class MainWindowViewModel : ViewModelBase
isDirectory, RxApp.MainThreadScheduler);
OpenCommand.ThrownExceptions.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(ex => ShowErrorMessage(ex.Message).ConfigureAwait(false).GetAwaiter().GetResult());
.Subscribe(ShowErrorMessage);
}
#endregion Configure Commands
#region Get Data
private static async Task<IEnumerable<Pod>> PodsAsync(Namespace? ns, IKubernetesService kubernetesService)
{
if (ns == null)
@@ -318,10 +347,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);
}
}
@@ -349,7 +376,50 @@ public class MainWindowViewModel : ViewModelBase
}).ToList();
}
#endregion Get Data
#region Reset Data
private void ResetPath()
{
FileInformation = new List<FileInformation>();
SelectedPath = null;
SelectedContainer = null;
}
private void ResetContainers()
{
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()
{
Message = new Message
{
@@ -358,26 +428,37 @@ public class MainWindowViewModel : ViewModelBase
IsError = false
};
}
}
private async Task ShowErrorMessage(string message)
private void ShowErrorMessage(string message)
{
Message = new Message
RxApp.MainThreadScheduler.Schedule(Action);
return;
async void Action()
{
IsVisible = true,
Text = message,
IsError = true
};
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>
@@ -90,7 +91,12 @@
</ComboBox>
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" Margin="10" HorizontalAlignment="Right">
<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>
@@ -100,9 +106,6 @@
<Button Command="{Binding DownloadCommand}" VerticalAlignment="Center" ToolTip.Tip="Download File">
<PathIcon Data="{StaticResource arrow_download_regular}"></PathIcon>
</Button>
<Button Command="{Binding DownloadLogCommand}" VerticalAlignment="Center" ToolTip.Tip="Download Pod Log">
<PathIcon Data="{StaticResource document_one_page_regular}"></PathIcon>
</Button>
</StackPanel>
</Grid>
<DataGrid Grid.Row="1"
@@ -121,11 +124,11 @@
<Setter Property="Padding" Value="10"></Setter>
</Style>
<Style Selector="DataGridCell">
<Setter Property="FontSize" Value="12"></Setter>
<Setter Property="FontSize" Value="14"></Setter>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTemplateColumn Header="Type">
<DataGridTemplateColumn Header="Type" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="models:FileInformation">
<Border ToolTip.Tip="{Binding Type}" Background="Transparent">
@@ -144,7 +147,7 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Name" Width="*">
<DataGridTemplateColumn Header="Name" Width="*" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="models:FileInformation">
<Border Background="Transparent">
@@ -158,11 +161,11 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Size">
<DataGridTemplateColumn Header="Size" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="models:FileInformation">
<Border Background="Transparent">
<TextBlock Text="{Binding Size}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Size}" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 10 0"/>
<Interaction.Behaviors>
<EventTriggerBehavior EventName="DoubleTapped">
<InvokeCommandAction Command="{Binding ((vm:MainWindowViewModel)DataContext).OpenCommand, RelativeSource={RelativeSource AncestorType=Window }}" />
@@ -172,14 +175,14 @@
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTemplateColumn Header="Date">
<DataGridTemplateColumn Header="Date" CanUserSort="False">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate DataType="models:FileInformation">
<Border Background="Transparent">
<TextBlock Text="{Binding Date}" VerticalAlignment="Center"/>
<TextBlock Text="{Binding DateTimeOffsetString}" VerticalAlignment="Center" Margin="10 0 8 0"/>
<Interaction.Behaviors>
<EventTriggerBehavior EventName="DoubleTapped">
<InvokeCommandAction Command="{Binding ((vm:MainWindowViewModel)DataContext).OpenCommand, RelativeSource={RelativeSource AncestorType=Window }}" />
<InvokeCommandAction Command="{Binding ((vm:MainWindowViewModel)DataContext).OpenCommand, RelativeSource={RelativeSource AncestorType=Window}}" />
</EventTriggerBehavior>
</Interaction.Behaviors>
</Border>

View File

@@ -2,3 +2,15 @@
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)

7
build.cmd Executable file
View File

@@ -0,0 +1,7 @@
:; set -eo pipefail
:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
:; ${SCRIPT_DIR}/build.sh "$@"
:; exit $?
@ECHO OFF
powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %*

74
build.ps1 Normal file
View File

@@ -0,0 +1,74 @@
[CmdletBinding()]
Param(
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$BuildArguments
)
Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)"
Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 }
$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent
###########################################################################
# CONFIGURATION
###########################################################################
$BuildProjectFile = "$PSScriptRoot\build\_build.csproj"
$TempDirectory = "$PSScriptRoot\\.nuke\temp"
$DotNetGlobalFile = "$PSScriptRoot\\global.json"
$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1"
$DotNetChannel = "STS"
$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1
$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1
$env:DOTNET_MULTILEVEL_LOOKUP = 0
###########################################################################
# EXECUTION
###########################################################################
function ExecSafe([scriptblock] $cmd) {
& $cmd
if ($LASTEXITCODE) { exit $LASTEXITCODE }
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and `
$(dotnet --version) -and $LASTEXITCODE -eq 0) {
$env:DOTNET_EXE = (Get-Command "dotnet").Path
}
else {
# Download install script
$DotNetInstallFile = "$TempDirectory\dotnet-install.ps1"
New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
(New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile)
# If global.json exists, load expected version
if (Test-Path $DotNetGlobalFile) {
$DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json)
if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) {
$DotNetVersion = $DotNetGlobal.sdk.version
}
}
# Install by channel or version
$DotNetDirectory = "$TempDirectory\dotnet-win"
if (!(Test-Path variable:DotNetVersion)) {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath }
} else {
ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath }
}
$env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe"
}
Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)"
if (Test-Path env:NUKE_ENTERPRISE_TOKEN) {
& $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null
& $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null
}
ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet }
ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }

67
build.sh Executable file
View File

@@ -0,0 +1,67 @@
#!/usr/bin/env bash
bash --version 2>&1 | head -n 1
set -eo pipefail
SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
###########################################################################
# CONFIGURATION
###########################################################################
BUILD_PROJECT_FILE="$SCRIPT_DIR/build/_build.csproj"
TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp"
DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh"
DOTNET_CHANNEL="STS"
export DOTNET_CLI_TELEMETRY_OPTOUT=1
export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
export DOTNET_MULTILEVEL_LOOKUP=0
###########################################################################
# EXECUTION
###########################################################################
function FirstJsonValue {
perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}"
}
# If dotnet CLI is installed globally and it matches requested version, use for execution
if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then
export DOTNET_EXE="$(command -v dotnet)"
else
# Download install script
DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
mkdir -p "$TEMP_DIRECTORY"
curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
chmod +x "$DOTNET_INSTALL_FILE"
# If global.json exists, load expected version
if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then
DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")")
if [[ "$DOTNET_VERSION" == "" ]]; then
unset DOTNET_VERSION
fi
fi
# Install by channel or version
DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
if [[ -z ${DOTNET_VERSION+x} ]]; then
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
else
"$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
fi
export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
fi
echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)"
if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "NUKE_ENTERPRISE_TOKEN" != "" ]]; then
"$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true
"$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true
fi
"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"

11
build/.editorconfig Normal file
View File

@@ -0,0 +1,11 @@
[*.cs]
dotnet_style_qualification_for_field = false:warning
dotnet_style_qualification_for_property = false:warning
dotnet_style_qualification_for_method = false:warning
dotnet_style_qualification_for_event = false:warning
dotnet_style_require_accessibility_modifiers = never:warning
csharp_style_expression_bodied_methods = true:silent
csharp_style_expression_bodied_properties = true:warning
csharp_style_expression_bodied_indexers = true:warning
csharp_style_expression_bodied_accessors = true:warning

96
build/Build.cs Normal file
View File

@@ -0,0 +1,96 @@
using System.IO;
using System.IO.Compression;
using Nuke.Common;
using Nuke.Common.IO;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using static Nuke.Common.Tools.DotNet.DotNetTasks;
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";
AbsolutePath SourceDirectory => RootDirectory / "K8sFileBrowser";
AbsolutePath OutputDirectory => RootDirectory / "output";
AbsolutePath WinOutputDirectory => OutputDirectory / "win";
AbsolutePath LinuxOutputDirectory => OutputDirectory / "linux";
AbsolutePath WinZip => OutputDirectory / $"K8sFileBrowser_{Version}.zip";
AbsolutePath LinuxGz => OutputDirectory / $"K8sFileBrowser_{Version}.tgz";
AbsolutePath ProjectFile => SourceDirectory / "K8sFileBrowser.csproj";
readonly string ExcludedExtensions = "pdb";
public static int Main () => Execute<Build>(x => x.Publish);
Target Clean => _ => _
.Executes(() =>
{
OutputDirectory.DeleteDirectory();
});
Target PublishWin => _ => _
.DependsOn(Clean)
.Executes(() =>
{
DotNetPublish(s => s
.SetProject(ProjectFile)
.SetConfiguration(Configuration)
.SetOutput(WinOutputDirectory)
.SetSelfContained(true)
.SetFramework("net7.0")
.SetRuntime("win-x64")
.SetPublishSingleFile(true)
.SetPublishReadyToRun(true)
.SetAuthors("Andreas Billmann")
.SetCopyright("Copyright (c) 2023")
.SetVersion(Version)
.SetProcessArgumentConfigurator(_ => _
.Add("-p:IncludeNativeLibrariesForSelfExtract=true")));
WinOutputDirectory.ZipTo(
WinZip,
filter: x => !x.HasExtension(ExcludedExtensions),
compressionLevel: CompressionLevel.SmallestSize,
fileMode: FileMode.CreateNew);
});
Target PublishLinux => _ => _
.DependsOn(Clean)
.Executes(() =>
{
DotNetPublish(s => s
.SetProject(ProjectFile)
.SetConfiguration(Configuration)
.SetOutput(LinuxOutputDirectory)
.SetSelfContained(true)
.SetFramework("net7.0")
.SetRuntime("linux-x64")
.SetPublishSingleFile(true)
.SetPublishReadyToRun(true)
.SetAuthors("Andreas Billmann")
.SetCopyright("Copyright (c) 2023")
.SetVersion(Version)
.SetProcessArgumentConfigurator(_ => _
.Add("-p:IncludeNativeLibrariesForSelfExtract=true")));
LinuxOutputDirectory.TarGZipTo(
LinuxGz,
filter: x => !x.HasExtension(ExcludedExtensions),
fileMode: FileMode.CreateNew);
});
Target Publish => _ => _
.DependsOn(PublishWin, PublishLinux)
.Executes(() =>
{
});
}

16
build/Configuration.cs Normal file
View File

@@ -0,0 +1,16 @@
using System;
using System.ComponentModel;
using System.Linq;
using Nuke.Common.Tooling;
[TypeConverter(typeof(TypeConverter<Configuration>))]
public class Configuration : Enumeration
{
public static Configuration Debug = new Configuration { Value = nameof(Debug) };
public static Configuration Release = new Configuration { Value = nameof(Release) };
public static implicit operator string(Configuration configuration)
{
return configuration.Value;
}
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.props file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.props', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<!-- This file prevents unintended imports of unrelated MSBuild files -->
<!-- Uncomment to include parent Directory.Build.targets file -->
<!--<Import Project="$([MSBuild]::GetPathOfFileAbove('Directory.Build.targets', '$(MSBuildThisFileDirectory)../'))" />-->
</Project>

17
build/_build.csproj Normal file
View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace></RootNamespace>
<NoWarn>CS0649;CS0169</NoWarn>
<NukeRootDirectory>..</NukeRootDirectory>
<NukeScriptDirectory>..</NukeScriptDirectory>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="7.0.2" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,28 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=HeapView_002EDelegateAllocation/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=VariableHidesOuterVariable/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=ClassNeverInstantiated_002EGlobal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=MemberCanBeMadeStatic_002ELocal/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/InspectionSeverities/=InterpolatedStringExpressionIsNotIFormattable/@EntryIndexedValue">DO_NOT_SHOW</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_INTERNAL_MODIFIER/@EntryValue">Implicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/DEFAULT_PRIVATE_MODIFIER/@EntryValue">Implicit</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/METHOD_OR_OPERATOR_BODY/@EntryValue">ExpressionBody</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpCodeStyle/ThisQualifier/INSTANCE_MEMBERS_QUALIFY_MEMBERS/@EntryValue">0</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/ANONYMOUS_METHOD_DECLARATION_BRACES/@EntryValue">NEXT_LINE</s:String>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/KEEP_USER_LINEBREAKS/@EntryValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_AFTER_INVOCATION_LPAR/@EntryValue">False</s:Boolean>
<s:Int64 x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/MAX_ATTRIBUTE_LENGTH_FOR_SAME_LINE/@EntryValue">120</s:Int64>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/PLACE_FIELD_ATTRIBUTE_ON_SAME_LINE_EX/@EntryValue">IF_OWNER_IS_SINGLE_LINE</s:String>
<s:String x:Key="/Default/CodeStyle/CodeFormatting/CSharpFormat/WRAP_ARGUMENTS_STYLE/@EntryValue">WRAP_IF_LONG</s:String>
<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: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>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpRenamePlacementToArrangementMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<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>

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB