Public Access
Merge branch 'main' into the Android head
Main grew the screens the host-management plan called for — hosts, pins, snippets, logs, import, teams — plus the ObjectStore and Import projects behind two of them, and moved WindowsDeviceKeyStore into the desktop head's Platform folder. Five of those view models landed in a directory this branch had already moved, so they join the rest in DodoSSH.Client.Shell: git spotted the rename and put them there, and the namespaces followed. Shell picks up ObjectStore and Import as a result, which the Android head then gets transitively and will use neither of at first — scoped storage means there is no ~/.ssh/config to import, and file transfer is out of its first scope. Desktop suites green at 155 and 64.
This commit is contained in:
@@ -0,0 +1,252 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.Shell.ViewModels"
|
||||
xmlns:views="using:DodoSSH.Client.App.Views"
|
||||
x:Class="DodoSSH.Client.App.Views.HostsScreen"
|
||||
x:DataType="vm:MainWindowViewModel">
|
||||
|
||||
<!--
|
||||
The hosts screen: the list of machines, and what this application has to say about the one that is
|
||||
selected.
|
||||
|
||||
It used to be the list beside a terminal, and the terminal is no longer here. The tab strip is above
|
||||
every screen now, so a terminal is a surface the whole window switches to rather than a column on this
|
||||
one — see MainWindowViewModel.ShellSurface. What that leaves this screen is the thing its name always
|
||||
promised: an overview.
|
||||
|
||||
In its own file, rather than left in MainWindow.axaml, because nothing inside that window can be laid
|
||||
out by a test — WebView2's adapter refuses the headless session's thread — so markup that stays there
|
||||
is markup nobody can measure. The four blocks in the right column are exactly the ones that most needed
|
||||
measuring: two host key prompts and a conflict log, all three of which appear only in states a person
|
||||
has to reproduce by hand.
|
||||
|
||||
Its data context is the shell, not the vault, so that the sidebar can be handed the vault and everything
|
||||
else can bind Vault.* — the same split MainWindow.axaml had. See MainWindow.axaml's own note on why the
|
||||
two cannot be put on one element.
|
||||
-->
|
||||
|
||||
<Grid ColumnDefinitions="268,*">
|
||||
|
||||
<views:HostSidebar Grid.Column="0" x:Name="Sidebar" DataContext="{Binding Vault}" />
|
||||
|
||||
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*,Auto">
|
||||
|
||||
<!--
|
||||
Connecting. A password box only for a host that asks to be — a host bound to a stored credential or
|
||||
a key wants nothing typed here — and a sentence in its place when it does not, because "nothing
|
||||
needs typing" and "something needs typing and the box has not appeared yet" look identical and only
|
||||
one of them is fine.
|
||||
-->
|
||||
<Border Grid.Row="0" Padding="12,8" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBox Text="{Binding Vault.ConnectPassword}" PlaceholderText="password (not stored)"
|
||||
PasswordChar="•" Width="200" VerticalAlignment="Center"
|
||||
IsVisible="{Binding Vault.SelectedHostAsksForAPassword}"
|
||||
ToolTip.Tip="Typed each time and never stored. To stop typing it, add a password under Keychain and bind this host to it in the host's own editor." />
|
||||
<TextBlock Text="{Binding Vault.SelectedHostAuthenticationNote}" Classes="hint"
|
||||
FontSize="11" VerticalAlignment="Center"
|
||||
IsVisible="{Binding !Vault.SelectedHostAsksForAPassword}" />
|
||||
<Button Classes="accent" Content="CONNECT" Command="{Binding Vault.ConnectCommand}"
|
||||
IsEnabled="{Binding !Vault.IsBusy}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Row="1">
|
||||
|
||||
<!--
|
||||
Host key prompts. Unknown and changed look deliberately different: one is a decision, the other is
|
||||
a refusal. Presenting a changed key with a "continue" button is how users are taught to click
|
||||
through the one warning that matters.
|
||||
-->
|
||||
<Border Padding="12,10" Background="{StaticResource WarnWash}"
|
||||
BorderBrush="{StaticResource WarnSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Vault.HasPendingHostKey}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="This host has not been seen before. Check the fingerprint against what the server's operator published."
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Vault.PendingHostKey.Fingerprint}"
|
||||
Foreground="{StaticResource Warn}" TextWrapping="Wrap" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Button Classes="accent" Content="TRUST AND CONNECT"
|
||||
Command="{Binding Vault.TrustHostKeyCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL"
|
||||
Command="{Binding Vault.RejectHostKeyCommand}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Padding="12,10" Background="{StaticResource DangerWash}"
|
||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Vault.HasHostKeyMismatch}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="The host key changed and the connection was refused."
|
||||
Foreground="{StaticResource Danger}" FontWeight="SemiBold" />
|
||||
<SelectableTextBlock Text="{Binding Vault.HostKeyMismatch}"
|
||||
Foreground="{StaticResource Danger}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="If the server was legitimately rebuilt, edit the host and choose "Forget host key" first. There is deliberately no way to continue from here."
|
||||
Foreground="{StaticResource WarnText}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
The conflict log. The merge is only allowed to pick a winner because the value it overrode is kept
|
||||
and shown; without this panel it would be last-writer-wins with a longer explanation.
|
||||
|
||||
Bounded and scrollable, which it was not while it lived in the window. It sits on an Auto row above
|
||||
a star row, and an ItemsControl with no ceiling grows without limit — so a pass that merged twenty
|
||||
items pushed everything below it off the bottom of a screen nobody could scroll. It went unnoticed
|
||||
for as long as it did because no test could lay this markup out; that is the other half of why this
|
||||
file exists.
|
||||
-->
|
||||
<Border Padding="12,10" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1"
|
||||
IsVisible="{Binding Vault.HasConflicts}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Text="Some changes could not be merged automatically."
|
||||
Foreground="{StaticResource Info}" FontWeight="SemiBold" />
|
||||
<ScrollViewer MaxHeight="180" HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Vault.Conflicts}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ConflictRowViewModel">
|
||||
<Border Margin="0,4" Padding="8" Background="{StaticResource Raised}"
|
||||
CornerRadius="4">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="{Binding Summary}" Foreground="{StaticResource Text}"
|
||||
TextWrapping="Wrap" />
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Detail}" FontSize="11"
|
||||
Foreground="{StaticResource TextDim}"
|
||||
IsVisible="{Binding HasDetail}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
<Button Classes="ghost" Content="DISMISS ALL" HorizontalAlignment="Left"
|
||||
Command="{Binding Vault.AcknowledgeAllConflictsCommand}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
The overview proper: what is known about the host the list has selected.
|
||||
|
||||
Every fact here is one the sidebar already computes, and that is deliberate. This column was a
|
||||
terminal until this screen stopped hosting one, and filling it with something that needed new state
|
||||
would be inventing a feature to fill a rectangle. What it is for is the question the screen now has
|
||||
to answer — "which machine is this, and how will it let me in" — before the answer scrolls past in a
|
||||
list of forty.
|
||||
-->
|
||||
<ScrollViewer Grid.Row="2" HorizontalScrollBarVisibility="Disabled">
|
||||
<Panel Margin="24">
|
||||
|
||||
<StackPanel Spacing="10" HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
IsVisible="{Binding Vault.SelectedHost, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="heading" Text="{Binding Vault.SelectedHost.Label}"
|
||||
VerticalAlignment="Center" />
|
||||
<Border Classes="chip" VerticalAlignment="Center"
|
||||
IsVisible="{Binding Vault.SelectedHost.IsConnected}">
|
||||
<TextBlock Text="CONNECTED" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding Vault.SelectedHost.Address}"
|
||||
Foreground="{StaticResource TextDim}" />
|
||||
|
||||
<TextBlock Classes="hint" Text="{Binding Vault.SelectedHost.Authentication}" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" MaxWidth="440" TextWrapping="Wrap"
|
||||
Text="Press CONNECT, or double-click the host in the list. The terminal opens in the strip above and stays there while you look at anything else." />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="hint" HorizontalAlignment="Left" VerticalAlignment="Top"
|
||||
MaxWidth="440" TextWrapping="Wrap"
|
||||
Text="Choose a host on the left to see what it is and how it authenticates. Ctrl+K searches them by name."
|
||||
IsVisible="{Binding Vault.SelectedHost, Converter={x:Static ObjectConverters.IsNull}}" />
|
||||
|
||||
</Panel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!--
|
||||
Groups: making them, renaming them, and taking them away.
|
||||
|
||||
Here rather than on the Keychain screen, because a group is not a secret — it is how this screen's
|
||||
list is arranged, and the arranging belongs beside the thing arranged. Filing a host into one is done
|
||||
in the host's own editor, on the left, for the same reason its key and its password are.
|
||||
|
||||
One text box for both adding and renaming. A group has exactly one field, so a separate rename form
|
||||
would be this box with a different heading; GroupSaveLabel is what says which of the two is about to
|
||||
happen.
|
||||
-->
|
||||
<Border Grid.Row="3" Padding="12,10" Background="{StaticResource Panel}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0">
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Classes="label" Text="GROUPS" Foreground="{StaticResource TextDim}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Classes="hint" FontSize="10.5" VerticalAlignment="Center" TextWrapping="Wrap"
|
||||
Text="Headings for the list on the left. Which group a host is in is part of the host, and stays encrypted." />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Horizontal, because a group is a name and a count: a vertical list of one-line rows would take a
|
||||
third of this column to say what a row of chips says in one line.
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Disabled"
|
||||
IsVisible="{Binding Vault.HasGroups}">
|
||||
<ListBox ItemsSource="{Binding Vault.Groups}" SelectedItem="{Binding Vault.SelectedGroup}"
|
||||
Background="Transparent" MaxHeight="72">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<StackPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HostGroupRowViewModel">
|
||||
<StackPanel Margin="2,4" Spacing="1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" Foreground="{StaticResource Text}"
|
||||
FontSize="11.5" />
|
||||
<Border Classes="chip warn" Padding="4,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</ScrollViewer>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding Vault.ShowsGroupActions}">
|
||||
<TextBox Text="{Binding Vault.GroupEditorLabel}" PlaceholderText="group name" Width="180"
|
||||
FontSize="11" MinHeight="26" Padding="8,3" />
|
||||
<Button Classes="ghost" Content="{Binding Vault.GroupSaveLabel}"
|
||||
Command="{Binding Vault.SaveGroupCommand}" />
|
||||
<Button Classes="ghost" Content="RENAME SELECTED" Command="{Binding Vault.EditGroupCommand}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding Vault.DeleteGroupCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Swapped for the buttons rather than stacked under them, as the sidebar's own question is, so
|
||||
DELETE cannot be pressed again while its answer is on screen. It asks its own question only: the
|
||||
two panels share one pending deletion, and the sidebar checks the same way.
|
||||
-->
|
||||
<Border Padding="8" Background="{StaticResource DangerWash}" CornerRadius="4"
|
||||
IsVisible="{Binding Vault.IsConfirmingGroupDeletion}">
|
||||
<views:ConfirmDeleteCard DataContext="{Binding Vault}" />
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
Reference in New Issue
Block a user