Public Access
251 lines
15 KiB
XML
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="" 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>
|