Public Access
Give hosts and terminals their own screen, and the rest of the vault another
Rebuilds the client's shell from an imported design: a titlebar and nav rail it draws itself, real multi-session tabs over the one WebView, a Ctrl+K host search, and a vault screen that merges keys, passwords and pinned host keys into one table. Hosts left the vault column for their own screen beside the terminal, which is what the design asks for and turned out to be the better split anyway. Two screens the design shows have nothing behind them yet — file transfer and teams — and say so plainly rather than rendering invented data; every other gap between the design and this build is recorded in docs/design-import-gaps.md.
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:DodoSSH.Client.App.ViewModels"
|
||||
x:Class="DodoSSH.Client.App.Views.HostSidebar"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
The host list, and the editor for whichever host is open.
|
||||
|
||||
This was the top half of VaultColumn. The design gives hosts their own column beside the terminal and
|
||||
puts everything else in the vault screen, which is the split this control and VaultScreen are — and it
|
||||
is a better split than the one it replaces: the hosts list is the thing you look at while you work, and
|
||||
the keys and passwords behind it are the thing you go and manage.
|
||||
|
||||
Its data context is the VaultViewModel, so every binding here is a property of the vault. The shell
|
||||
hands it over; see MainWindow.
|
||||
|
||||
The editor stays in this column rather than moving into the terminal's half of the window, and that is
|
||||
an occlusion constraint rather than a preference: the terminal's WebView is a native child window that
|
||||
composites above anything Avalonia draws in the same rectangle, so a form laid over there would render
|
||||
underneath it with its buttons unclickable. It is also why this control is measurable at all — no part
|
||||
of it is the WebView, so the layout harness can lay it out headlessly.
|
||||
-->
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto,Auto" Background="{StaticResource Sidebar}">
|
||||
|
||||
<!--
|
||||
The filter. It narrows this list and nothing else — the connect path, the selection and the pinned
|
||||
host key list all read the unfiltered collection — so a filter left in the box can hide a host but
|
||||
can never break one.
|
||||
-->
|
||||
<Border Grid.Row="0" Padding="10,8" BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1">
|
||||
<TextBox x:Name="HostFilter" Text="{Binding HostFilter}" PlaceholderText="filter hosts" FontSize="11"
|
||||
FontFamily="{StaticResource MonoFont}" MinHeight="26" Padding="8,3" />
|
||||
</Border>
|
||||
|
||||
<!--
|
||||
One heading, for one vault. The chevron folds the list away; the count is the collection's own, so it
|
||||
follows the filter without a second number to keep in step.
|
||||
-->
|
||||
<Button Grid.Row="1" Classes="flat grouphead" Command="{Binding ToggleHostsCommand}"
|
||||
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto">
|
||||
<TextBlock Grid.Column="0" Text="▾" Foreground="{StaticResource TextFaint}" FontSize="8"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"
|
||||
IsVisible="{Binding AreHostsExpanded}" />
|
||||
<TextBlock Grid.Column="0" Text="▸" Foreground="{StaticResource TextFaint}" FontSize="8"
|
||||
VerticalAlignment="Center" Margin="0,0,6,0"
|
||||
IsVisible="{Binding !AreHostsExpanded}" />
|
||||
<TextBlock Grid.Column="1" Classes="label" Text="{Binding HostsHeading}"
|
||||
Foreground="{StaticResource TextDim}" VerticalAlignment="Center" />
|
||||
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding VisibleHosts.Count}" FontSize="10"
|
||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<!--
|
||||
Named because it is where keyboard focus lands when the user leaves the terminal.
|
||||
|
||||
Focusable, which a ListBox is not by default — Avalonia leaves focus to the items and an empty list has
|
||||
none. Without it the release-the-keyboard path is a measured no-op: it takes Win32 focus off the
|
||||
terminal's child window and then calls Focus() on something that refuses it, leaving the window with
|
||||
nothing focused and the keystrokes going nowhere.
|
||||
-->
|
||||
<ListBox Grid.Row="2" x:Name="HostList" Focusable="True"
|
||||
IsVisible="{Binding AreHostsExpanded}"
|
||||
ItemsSource="{Binding VisibleHosts}"
|
||||
SelectedItem="{Binding SelectedHost}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0,5,10,5">
|
||||
|
||||
<!-- The accent strip a selected row carries; see the style in App.axaml. -->
|
||||
<Border Grid.Column="0" Classes="rowmark" />
|
||||
|
||||
<!--
|
||||
Connected, and nothing more. Green means a terminal is open on this host right now; grey means
|
||||
there is not one. It is deliberately not reachability — nothing here pings anything, and a dot
|
||||
that meant "up" would be a claim this application never checks.
|
||||
-->
|
||||
<Ellipse Grid.Column="1" Classes="dot" Classes.live="{Binding IsConnected}"
|
||||
Margin="8,5,8,0" VerticalAlignment="Top" />
|
||||
|
||||
<StackPanel Grid.Column="2" Spacing="1">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Label}" Foreground="{StaticResource Text}"
|
||||
FontSize="11.5" FontWeight="Medium"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<Border Classes="chip warn" Padding="4,0"
|
||||
IsVisible="{Binding Badge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
||||
<TextBlock Text="{Binding Badge}" FontSize="8.5" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis" />
|
||||
<!--
|
||||
Which of the three ways this host authenticates. In the list because only one of them wants
|
||||
the password box filled in, and an empty box on a key-authenticated host is otherwise
|
||||
indistinguishable from one somebody forgot to fill in.
|
||||
-->
|
||||
<TextBlock Classes="mono" Text="{Binding Authentication}" FontSize="9.5"
|
||||
Foreground="{StaticResource TextFaint}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- The editor doubles as the "add" form; there is no separate dialog. -->
|
||||
<Border Grid.Row="3" Padding="10" Background="{StaticResource Chrome}"
|
||||
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding IsEditing}">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||||
<TextBox Text="{Binding EditorHostname}" PlaceholderText="hostname or address" />
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
|
||||
FormatString="0" ShowButtonSpinner="False" />
|
||||
<TextBox Grid.Column="2" Text="{Binding EditorUsername}" PlaceholderText="username" />
|
||||
</Grid>
|
||||
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="48" TextWrapping="Wrap" />
|
||||
<!--
|
||||
How this host authenticates: a typed password, one of the vault's keys, or one of its credentials.
|
||||
Part of the host rather than of the connection, so it follows the host to every machine; a host
|
||||
bound to something since deleted keeps a placeholder entry here, so that editing the port cannot
|
||||
quietly turn it back into a typed-password host.
|
||||
|
||||
One control for all three, which is what makes "a key or a credential, never both" impossible to
|
||||
express rather than merely invalid. The qualifier beside each label is not decoration: a key called
|
||||
"deploy" and the deploy account's password are the ordinary case, and bare labels would offer two
|
||||
identical-looking entries that authenticate completely differently.
|
||||
-->
|
||||
<ComboBox ItemsSource="{Binding EditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding EditorSelectedAuthentication}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="10"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
<CheckBox IsChecked="{Binding EditorRelayEnabled}"
|
||||
Content="Connect through the server relay" />
|
||||
<!--
|
||||
Stated at the moment the decision is made, which is the only place it means anything. With
|
||||
relay off the server stores no address at all; with it on the server must be able to resolve
|
||||
the target, or it becomes an authenticated open proxy into the operator's network.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="10"
|
||||
Text="The relay stores this host's address on the server in plain text. Everything else stays encrypted." />
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="SAVE" Command="{Binding SaveHostCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelEditCommand}" />
|
||||
</StackPanel>
|
||||
<!--
|
||||
Withdrawing host key trust lives here, in the host's own settings, because a changed host key
|
||||
is refused outright with no way to continue past it — so a legitimately rebuilt server needs
|
||||
somewhere deliberate to be re-approved from, and that somewhere must not be the warning
|
||||
itself. It takes effect when clicked rather than on Save, and the status line says so; it is
|
||||
not a field of the host.
|
||||
-->
|
||||
<Button Classes="danger" Content="FORGET HOST KEY" HorizontalAlignment="Left"
|
||||
Command="{Binding ForgetHostKeyCommand}"
|
||||
IsVisible="{Binding CanForgetHostKey}"
|
||||
ToolTip.Tip="Removes the pinned key for this host's address, so the next connection asks you to check its fingerprint again." />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="4" Padding="10,8" BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0"
|
||||
IsVisible="{Binding !IsEditing}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="ghost" Content="+ NEW HOST" Command="{Binding NewHostCommand}" />
|
||||
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteHostCommand}" />
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
Reference in New Issue
Block a user