Files
DodoSSH/src/DodoSSH.Client.App/Views/ImportScreen.axaml
T

251 lines
15 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
x:Class="DodoSSH.Client.App.Views.ImportScreen"
x:DataType="vm:ImportViewModel">
<!--
Importing ~/.ssh/config.
A preview and then a button, rather than one action, and that is the whole design. This reads a file
the application did not write, out of the user's home directory, and a real ssh_config often holds
forty entries for machines that stopped existing years ago. So scanning writes nothing and the list
says what each entry means; importing is a separate press on a set somebody has looked at.
v5c-3: restyled into Import.dc.html's own table over SettingsView's content column — SettingsNav stays
lit on Preferences while this is up, and the titlebar says "Back to preferences"; see
MainWindowViewModel.IsImportOpen. No longer reachable from the nav rail, exactly as before: it is a task
done once or once a year, reached from the Preferences page's own "OPEN IMPORTER" row.
── ◆ THE ONE TICK THAT READS PRIVATE KEYS ─────────────────────────────────────────────────────────────
Below the list, off, and drawn only where the scan actually found an IdentityFile. It is the only control
in this application that opens private key material out of a directory the user did not point at file by
file, so three things about how it is drawn are load-bearing rather than tidy: it is a default nobody
arrives at by accident, the sentence beside it names the directory rather than saying "keys", and nothing
is read until IMPORT is pressed — so ticking it, reading the sentence and unticking it costs nothing.
What comes back afterwards is the report under the list: one line per key file, saying which were stored,
which are protected by a passphrase this cannot know, and which were not there at all. That is reported
rather than previewed for the same reason — previewing would mean reading them.
── ◆ WHAT THIS MEANS ──────────────────────────────────────────────────────────────────────────────────
One chip per row rather than the old separate AUTHENTICATION/STATE columns, mapped off the two facts a
row actually carries: ImportRowViewModel.AlreadyPresent and HasWarnings. A skipped Host pattern (a
wildcard block) never becomes a row at all — see SshConfigImport.SkippedPatterns — so there is no third,
"skipped" state to draw here; a warned row is the amber case instead, and it wins over "already here"
because the warning is the more actionable of the two facts. See ImportRowViewModel.Meaning.
-->
<UserControl.Styles>
<!-- The row's own hover fill, matching the design's per-row style-hover — Track, the same as every other table this application draws. -->
<Style Selector="Border.importrow:pointerover">
<Setter Property="Background" Value="{StaticResource Track}" />
</Style>
<Style Selector="Border.meaningchip.new">
<Setter Property="Background" Value="{StaticResource LiveWash}" />
</Style>
<Style Selector="Border.meaningchip.new > TextBlock">
<Setter Property="Foreground" Value="{StaticResource Live}" />
</Style>
<Style Selector="Border.meaningchip.exists">
<Setter Property="Background" Value="{StaticResource Chip}" />
</Style>
<Style Selector="Border.meaningchip.exists > TextBlock">
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
</Style>
<Style Selector="Border.meaningchip.warn">
<Setter Property="Background" Value="{StaticResource WarnWash}" />
</Style>
<Style Selector="Border.meaningchip.warn > TextBlock">
<Setter Property="Foreground" Value="{StaticResource WarnText}" />
</Style>
</UserControl.Styles>
<Grid MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch" RowDefinitions="Auto,Auto,Auto,*,Auto">
<!-- ============ HEADER ============ -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" Spacing="10">
<TextBlock Classes="settingstitle" Text="Import SSH config" />
<TextBlock Classes="mono" Text="{Binding HeaderStatus}" FontSize="12"
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
</StackPanel>
<Button Grid.Column="1" Classes="ghost" Height="40" VerticalAlignment="Top" Content="SCAN AGAIN"
Command="{Binding ScanCommand}" IsEnabled="{Binding !IsBusy}"
ToolTip.Tip="Reads the file again and shows what it found. Nothing is stored." />
</Grid>
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding Status}" FontSize="12" Margin="0,10,0,0"
TextWrapping="Wrap" />
<!--
What could not be honoured, at document level. Every one of these is a way the import is quieter than
the file — an ignored Match block, a dropped ProxyCommand — and a person comparing the two needs to be
told before they conclude the parser lost something.
-->
<Border Grid.Row="2" Margin="0,14,0,0" Padding="14,10" CornerRadius="10"
Background="{StaticResource WarnWash}" BorderBrush="{StaticResource WarnSoft}"
BorderThickness="1" IsVisible="{Binding HasWarnings}">
<ItemsControl ItemsSource="{Binding Warnings}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{Binding}" Foreground="{StaticResource WarnText}" FontSize="11.5"
TextWrapping="Wrap" Margin="0,2" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
<!-- ============ THE TABLE ============ -->
<Border Grid.Row="3" Margin="0,18,0,0" CornerRadius="12" Background="{StaticResource Pane}"
BorderBrush="{StaticResource Border}" BorderThickness="1" ClipToBounds="True"
IsVisible="{Binding HasRows}">
<Grid RowDefinitions="Auto,*">
<Border Grid.Row="0" Padding="24,14,24,10" BorderBrush="{StaticResource Border}"
BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="24,1*,1.4*,0.7*,0.5*,1.3*">
<!--
The header tick-all box. A plain Button rather than a CheckBox — ImportRowViewModel's own
ToggleAllCommand is "tick everything, or untick everything" in one press, which is a command
rather than a two-way bound bool, and a CheckBox has no Command of its own to hang that on.
-->
<Button Grid.Column="0" Classes="flat" Width="18" Height="18" Padding="0"
VerticalAlignment="Center" Command="{Binding ToggleAllCommand}"
ToolTip.Tip="Tick or untick every row">
<Panel Width="18" Height="18">
<Border CornerRadius="5" Background="{StaticResource Accent}" IsVisible="{Binding AllTicked}">
<TextBlock FontFamily="{StaticResource IconFont}" Text="&#xE5CA;" FontSize="13"
Foreground="White" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border CornerRadius="5" BorderBrush="{StaticResource BorderMid}" BorderThickness="1.5"
IsVisible="{Binding !AllTicked}" />
</Panel>
</Button>
<TextBlock Grid.Column="1" Classes="label" Text="ALIAS" FontSize="9.5" LetterSpacing="1"
Margin="14,0,0,0" />
<TextBlock Grid.Column="2" Classes="label" Text="HOSTNAME" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="3" Classes="label" Text="USER" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="4" Classes="label" Text="PORT" FontSize="9.5" LetterSpacing="1" />
<TextBlock Grid.Column="5" Classes="label" Text="WHAT THIS MEANS" FontSize="9.5" LetterSpacing="1" />
</Grid>
</Border>
<!-- Virtualised, for an inventory of hundreds of machines: see the transfer queue's own note. -->
<ItemsControl Grid.Row="1" ItemsSource="{Binding Rows}">
<ItemsControl.Template>
<ControlTemplate>
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="12,8" />
</ScrollViewer>
</ControlTemplate>
</ItemsControl.Template>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ImportRowViewModel">
<Border Classes="importrow" CornerRadius="8">
<Grid ColumnDefinitions="24,1*,1.4*,0.7*,0.5*,1.3*" Margin="12" MinHeight="20">
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}" VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Alias}" FontSize="12.5"
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="14,0,10,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
ToolTip.Tip="{Binding Authentication}" />
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Hostname}" FontSize="11.5"
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding User}" FontSize="11.5"
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Port}" FontSize="11.5"
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
<Border Grid.Column="5" Classes="meaningchip" CornerRadius="5" Padding="8,3"
HorizontalAlignment="Left" VerticalAlignment="Center"
Classes.new="{Binding IsMeaningNew}" Classes.exists="{Binding IsMeaningExisting}"
Classes.warn="{Binding IsMeaningWarned}" ToolTip.Tip="{Binding Meaning}">
<TextBlock Text="{Binding Meaning}" FontSize="10" FontWeight="SemiBold"
MaxWidth="230" TextTrimming="CharacterEllipsis" />
</Border>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</Border>
<!-- ============ FOOTER ============ -->
<StackPanel Grid.Row="4" Margin="0,16,0,0" Spacing="16" IsVisible="{Binding HasRows}">
<!--
Said before the button, not after. Whether the key material comes with the host is the difference
between a bookmark that connects and one that asks for a password, and somebody who is not told
will conclude the import was broken.
-->
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap" IsVisible="{Binding !ImportsKeys}"
Text="Key files are not read. Where ssh_config names an IdentityFile the path is recorded as a note, and the host asks for a password until you bind it to a key in your keychain." />
<!--
◆ THE OPT-IN CARD. Hidden entirely where the scan found no IdentityFile anywhere — an offer to read
~/.ssh on a screen where it would read nothing is a control that teaches people to ignore it.
-->
<Border CornerRadius="12" Background="{StaticResource Raised}" BorderBrush="{StaticResource WarnSoft}"
BorderThickness="1" Padding="18,16" IsVisible="{Binding HasKeyFiles}">
<StackPanel Orientation="Horizontal" Spacing="14">
<CheckBox VerticalAlignment="Top" Margin="0,3,0,0" IsChecked="{Binding ImportsKeys}" />
<StackPanel Spacing="6">
<TextBlock Text="Also store the private keys these entries point at" FontSize="13.5"
FontWeight="SemiBold" Foreground="{StaticResource Text}" />
<TextBlock Classes="hint" FontSize="11.5" LineHeight="17.5" TextWrapping="Wrap"
Text="{Binding KeyMaterialIntro}" />
<!--
The warning sentence appears only when the tick is on, and it is the one place this application
says out loud that it is about to open private keys.
-->
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap" IsVisible="{Binding ImportsKeys}"
Foreground="{StaticResource WarnText}"
Text="Pressing IMPORT will read each host's first IdentityFile out of ~/.ssh, store it in this vault encrypted, and bind the host to it. One key is stored per file however many hosts name it, and a file already in your keychain is bound to rather than stored twice. A key protected by a passphrase comes in without one — nothing on disk says what it is — and the report below will name it." />
</StackPanel>
</StackPanel>
</Border>
<!--
◆ What became of each key file, after the fact. Capped with a scroll viewer because a config with
thirty keyed hosts would otherwise push IMPORT off the window — the one control this screen must
never lose.
-->
<Border IsVisible="{Binding HasKeyReport}" Padding="12,10" CornerRadius="10"
Background="{StaticResource Panel}" BorderBrush="{StaticResource Border}" BorderThickness="1">
<ScrollViewer MaxHeight="120">
<ItemsControl ItemsSource="{Binding KeyReport}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="x:String">
<TextBlock Text="{Binding}" Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,2" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Border>
<!-- "N of M entries selected · saving to {vault}", Cancel, Import N hosts. -->
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Classes="hint" FontSize="12" VerticalAlignment="Center"
Text="{Binding SelectionSummary}" />
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
<Button Classes="ghost" Height="44" Content="CANCEL" Command="{Binding CancelCommand}"
ToolTip.Tip="Back to Preferences. Nothing is stored." />
<Button Classes="accent" Height="44" Content="{Binding ImportLabel}" Command="{Binding ImportCommand}"
IsEnabled="{Binding !IsBusy}" />
</StackPanel>
</Grid>
</StackPanel>
</Grid>
</UserControl>