Public Access
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.
255 lines
15 KiB
XML
255 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"
|
|
xmlns:views="using:DodoSSH.Client.App.Views"
|
|
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, which names the vault while there is one and says ALL VAULTS once a team's is readable
|
|
too — a heading that went on naming the personal vault over a list containing a team's hosts would be
|
|
a quiet lie, so the rows carry the vault name instead. 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 SidebarRows}"
|
|
SelectedItem="{Binding SelectedSidebarRow}">
|
|
|
|
<!--
|
|
Two kinds of row in one list, chosen by type. It has to be one ListBox: it owns the selection and it
|
|
is where keyboard focus lands when the terminal gives it back, neither of which survives a list per
|
|
group. A vault with no groups produces no heading rows at all, so this is the list it always was.
|
|
|
|
The heading is a row rather than a container, which means the control will happily select it. That is
|
|
turned back into the previous host selection in the view model — see SelectedSidebarRow — because
|
|
CONNECT, EDIT and DELETE all act on a host and a highlighted heading is not one.
|
|
-->
|
|
<ListBox.DataTemplates>
|
|
|
|
<DataTemplate DataType="vm:SidebarGroupHeader">
|
|
<Button Classes="flat grouphead" Command="{Binding $parent[ListBox].((vm:VaultViewModel)DataContext).ToggleGroupCommand}"
|
|
CommandParameter="{Binding}"
|
|
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<TextBlock Grid.Column="0" Text="{Binding Chevron}" Foreground="{StaticResource TextFaint}"
|
|
FontSize="8" VerticalAlignment="Center" Margin="0,0,6,0" />
|
|
<TextBlock Grid.Column="1" Classes="label" Text="{Binding Label}"
|
|
Foreground="{StaticResource TextDim}" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Count}" FontSize="10"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
|
</Grid>
|
|
</Button>
|
|
</DataTemplate>
|
|
|
|
<DataTemplate 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}" />
|
|
<!--
|
|
Which vault this host is in, and only when there is more than one to be in. It decides
|
|
who else can see the host and where an edit goes back to, so on a list that spans
|
|
several vaults it is not decoration.
|
|
-->
|
|
<TextBlock Classes="mono" Text="{Binding VaultBadge}" FontSize="9.5"
|
|
Foreground="{StaticResource TextFaint}"
|
|
IsVisible="{Binding HasVaultBadge}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Grid>
|
|
</DataTemplate>
|
|
|
|
</ListBox.DataTemplates>
|
|
</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>
|
|
<!--
|
|
Which group this host is filed under. Inside the encrypted payload like everything else here, so
|
|
the server learns nothing about how the estate is organised — and a group the vault no longer has
|
|
keeps a placeholder entry, so that editing the port cannot quietly unfile the host.
|
|
-->
|
|
<ComboBox ItemsSource="{Binding EditorGroupChoices}"
|
|
SelectedItem="{Binding EditorSelectedGroup}"
|
|
HorizontalAlignment="Stretch">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:GroupChoice">
|
|
<TextBlock Text="{Binding Label}" />
|
|
</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 ShowsHostActions}">
|
|
<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>
|
|
|
|
<!--
|
|
The question DELETE asks, in the place the buttons were rather than under them. This strip is at the
|
|
bottom edge of a column whose middle is a list that has already taken every spare pixel, so a second
|
|
block below the first would push its own buttons off the window — the same reasoning that swaps the
|
|
unlock card for the sign-out card rather than stacking them. Swapping also means DELETE cannot be
|
|
pressed again while its own question is up; see VaultViewModel.ShowsHostActions.
|
|
-->
|
|
<Border Grid.Row="4" Padding="10,8" Background="{StaticResource DangerWash}"
|
|
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,1,0,0"
|
|
IsVisible="{Binding IsConfirmingHostDeletion}">
|
|
<views:ConfirmDeleteCard />
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|