Public Access
Move SFTP and S3 into the tab strip, and the host list into a card grid
Four asks in one pass over the desktop head, and two of them are furniture moving rather than anything new. THE STRIP IS THE WINDOW'S NOW, not the terminal's. Vaults, SFTP and S3 sit at its head and cannot be closed, and open terminals follow them. SFTP and S3 left the nav rail to get there, which is the one semantic change: they are the two destinations you stay in while something runs, and a rail entry is for somewhere you go and come back from. So the rail belongs to the Vaults tab and collapses with it, which also means SFTP, S3 and a terminal each get the full 1016 rather than the 826 a page gets. The tab is expressed as "a page, and not one of the two the strip took" rather than as a fourth ShellSurface. Both are still ShellScreen members and have to be — that is what they are on the phone, where they are two rows in a hub — so a surface each would have been a second way to say a thing Screen already says. IsTransfersShowing and IsBucketsShowing light the other two tabs unchanged. What is new is one field: the page Vaults comes back to, because it is the one tab with sub-navigation and therefore the one that can return to the wrong place. That is not the hidden field ShellSurface argues against — that one would be a second copy of "which page"; this is a tab remembering its own. THE HOSTS SCREEN IS A GRID, and the 268-pixel sidebar is gone. That column was choosing among forty machines and editing one of them at two-thirds width, and it was narrow so the editor beneath it could be a column at all. Cards took the first job at full width; a 304-pixel drawer took the second and collapses when nothing is selected. Pressing a group card narrows the grid; SHOW ALL is the way back. The group editor moved into the drawer as well, which finally makes IsEditingGroup mean the same thing on both heads — it was the phone's alone, because the desktop's editor was a bar that was always on screen. AreHostsExpanded and ToggleHosts went with the control that used them. They folded the whole list away under one heading, an affordance that existed because the column was narrow. Folding a single group is a different thing and is still here. THE TYPE SCALE IS A POINT LARGER and the text ramp is white. The base size was never stated anywhere — a bare TextBlock took TextElement's default of 12 — so raising the scale meant naming it, on Window and on UserControl. The second selector is not redundancy: the layout harness hosts a UserControl in a window it builds itself, and without it the suite would measure every screen a point smaller than it ships, silently. A selector on TextBlock would have been the obvious way and is wrong, because a style setter beats an inherited value and would collapse every deliberate step back to one number. #E3E7F4 is a blue-tinted white on blue-black surfaces, which costs contrast twice — once for being darker than white and once for sharing a hue with what it is drawn on. Pure white is 18.3:1 against the canvas where that was 15.5:1. Every step below moved with the top, so the intervals the design chose are kept and TextDim clears 9:1 against 6.4:1. The palette is shared, so the phone has both changes too. TWO DEFECTS THE HARNESS STRUCTURALLY CANNOT SEE, found by rendering the screen rather than by measuring it, and both now covered. The tile was 232 and was first written as 248, from arithmetic that left out the scrolling stack's own margins. Every layout test passed — the harness asks whether a control is inside the window, never how many fit on a line — so the grid quietly became one column wide at exactly the minimum this application guarantees, which is the shape cards exist to avoid. TheHostsGridKeepsTwoColumnsAtTheMinimumWithTheDrawerOpen counts columns instead, and fails at 248. And a card's text ran past its own border, because a horizontal StackPanel measures children with infinite width: a TextBlock inside one never learns it is short of room, so TextTrimming never fires. Both card rows are grids with a star column that gives way and an Auto column that does not — a hostname with its tail cut is still the machine you were looking for, where a badge or the word naming an auth method is not. The keychain header changed shape for the same class of reason. It was Auto,Auto,*,Auto with the buttons last, so the slack column was the only thing absorbing a change of width and five buttons fell off the right edge the moment the type grew. That is how GENERATE lost the word KEY once already. The summary sits in the star column and trims now, so the buttons always get their width. HostSidebarTests became HostGridTests and moved to the grid with the gestures it drives. docs/design-import-gaps.md gains a v3 section naming the five toolbar controls in the design with nothing behind them — a view-mode switch, a tag filter, a calendar, a share control and Serial — and manual-checks.md and the README follow the controls that moved.
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
<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.HostDrawer"
|
||||
x:DataType="vm:VaultViewModel">
|
||||
|
||||
<!--
|
||||
Everything about one thing: what the selected host is, the editor for it, or the editor for a group.
|
||||
|
||||
── THIS WAS HostSidebar, AND THE HOST LIST IS NOT IN IT ANY MORE. ───────────────────────────────────
|
||||
It used to be a 268-pixel column on the left holding a filter box, the list of every host, and the
|
||||
editor underneath. That column was doing two jobs at two-thirds size — choosing among forty machines,
|
||||
and editing one of them — and it did the first one badly enough that the second was the only reason to
|
||||
keep it narrow. The list is now a grid of cards filling the screen, so this control kept the half that
|
||||
is about one host and moved to the right, which is where the thing you selected belongs.
|
||||
|
||||
Renamed with the job rather than kept as HostSidebar, unlike NavRail, which kept its name when it only
|
||||
changed width. This one changed what it holds, which side it is on, and whether it is there at all.
|
||||
|
||||
── EXACTLY ONE OF THE THREE PANELS IS SHOWING ───────────────────────────────────────────────────────
|
||||
Detail, host editor, group editor. They are exclusive by construction rather than by three flags that
|
||||
could disagree: IsShowingHostDetail is defined as "neither editor is open and something is selected",
|
||||
so no two of these can be true at once. The whole control collapses when none of them is — see
|
||||
IsDrawerOpen, which the hosts screen binds — rather than standing there empty, because an empty
|
||||
300-pixel column beside a grid is 300 pixels the grid could have had.
|
||||
|
||||
Its data context is the VaultViewModel, so every binding here is a property of the vault. The hosts
|
||||
screen hands it over.
|
||||
|
||||
Nothing in here may be laid over the terminal's rectangle: it is a column of the hosts screen, and the
|
||||
hosts screen is a sibling of the WebView. See MainWindow.axaml's occlusion rule.
|
||||
-->
|
||||
|
||||
<Border Width="304" Background="{StaticResource Sidebar}"
|
||||
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
|
||||
|
||||
<!--
|
||||
◆ IT SCROLLS AS A WHOLE, and the host editor no longer carries a MaxHeight of its own.
|
||||
|
||||
The old column gave the editor 300 pixels and let the list above have the rest, so the editor had to
|
||||
be bounded separately. Here the drawer is the only thing in its column, so one ScrollViewer over all
|
||||
three panels is both simpler and more honest: whichever panel is up gets the whole height, and the
|
||||
one that overflows is the one that scrolls.
|
||||
|
||||
The cost is the one the old note recorded and it has not changed: the layout harness skips anything
|
||||
with a ScrollViewer in its ancestry — see LayoutHarness.IsScrollable — so from here on it certifies
|
||||
that this pane fits the column rather than that every field inside it does. That is the true claim
|
||||
about a pane that scrolls, and the tag picker is why it has to scroll: its height is a chip per tag
|
||||
in the keychain, wrapped, so no fixed height holds it for somebody with fifteen.
|
||||
-->
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled">
|
||||
<Panel>
|
||||
|
||||
<!-- ============ WHAT THIS HOST IS ============ -->
|
||||
<StackPanel Margin="16" Spacing="12" IsVisible="{Binding IsShowingHostDetail}">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Ellipse Classes="dot" Classes.live="{Binding SelectedHost.IsConnected}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock Text="{Binding SelectedHost.Label}" FontSize="16" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
|
||||
<SelectableTextBlock Classes="mono" Text="{Binding SelectedHost.Address}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}" TextWrapping="Wrap" />
|
||||
|
||||
<!--
|
||||
Which of the three ways this host authenticates, and where it came from. The note rather than
|
||||
the one-word Authentication the card shows: a host that inherits its group's key is the case
|
||||
where the word alone is misleading, and there is room for the sentence here.
|
||||
-->
|
||||
<TextBlock Classes="hint" FontSize="12" Text="{Binding SelectedHostAuthenticationNote}" />
|
||||
|
||||
<!--
|
||||
The tags it wears, as the same chips the card draws. Repeated rather than shared with the card's
|
||||
template because the two are different shapes — the card wraps them under a two-line summary and
|
||||
this is a column 304 wide — and a shared template would have to be told which.
|
||||
-->
|
||||
<ItemsControl ItemsSource="{Binding SelectedHost.TagLabels}"
|
||||
IsVisible="{Binding SelectedHost.HasTags}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="x:String">
|
||||
<Border Classes="chip" Padding="6,1" Margin="0,0,4,4">
|
||||
<TextBlock Text="{Binding}" FontSize="9.5" />
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<SelectableTextBlock Text="{Binding SelectedHost.Host.Notes}" FontSize="12"
|
||||
Foreground="{StaticResource TextDim}" TextWrapping="Wrap"
|
||||
IsVisible="{Binding SelectedHost.Host.Notes,
|
||||
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
|
||||
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,2" />
|
||||
|
||||
<!--
|
||||
Connecting, and the box a host that wants a typed password needs. A sentence in the box's place
|
||||
when it does not, because "nothing needs typing" and "something needs typing and the box has not
|
||||
appeared" look identical and only one of them is fine.
|
||||
|
||||
It is here rather than in a bar across the top of the screen, which is where it used to be: the
|
||||
password belongs to the host, and a box at the top of a grid of forty machines is one whose
|
||||
subject you have to work out.
|
||||
-->
|
||||
<TextBox Text="{Binding ConnectPassword}" PlaceholderText="password (not stored)"
|
||||
PasswordChar="•" HorizontalAlignment="Stretch"
|
||||
IsVisible="{Binding 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." />
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding ShowsHostActions}">
|
||||
<Button Classes="accent" Content="CONNECT" Command="{Binding ConnectCommand}"
|
||||
IsEnabled="{Binding !IsBusy}" />
|
||||
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedHostCommand}" />
|
||||
<Button Classes="ghost" Content="DELETE" Command="{Binding DeleteHostCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
<!--
|
||||
Swapped for the buttons rather than stacked under them, as it always was, so DELETE cannot be
|
||||
pressed again while its own question is on screen. See VaultViewModel.ShowsHostActions.
|
||||
-->
|
||||
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="6"
|
||||
IsVisible="{Binding IsConfirmingHostDeletion}">
|
||||
<views:ConfirmDeleteCard />
|
||||
</Border>
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="Double-clicking the card does the same as CONNECT. The terminal opens as a tab in the strip above and stays there while you look at anything else." />
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<!-- ============ THE HOST EDITOR ============ -->
|
||||
<!--
|
||||
The editor doubles as the "add" form; there is no separate dialog.
|
||||
-->
|
||||
<StackPanel Margin="16" Spacing="6" IsVisible="{Binding IsEditing}">
|
||||
|
||||
<TextBlock Classes="label" Text="HOST" Foreground="{StaticResource TextDim}" Margin="0,0,0,4" />
|
||||
|
||||
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" />
|
||||
<TextBox Text="{Binding EditorHostname}" PlaceholderText="hostname or address" />
|
||||
<!--
|
||||
Both boxes are allowed to be empty, and empty means "take the group's" rather than "unset". The
|
||||
watermark is what the host will actually use if it is left that way, which is why it is bound
|
||||
rather than literal: it changes when the group picker below moves.
|
||||
-->
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding EditorPort}" Minimum="1" Maximum="65535"
|
||||
FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="{Binding EditorPortPlaceholder}" />
|
||||
<TextBox Grid.Column="2" Text="{Binding EditorUsername}"
|
||||
PlaceholderText="{Binding EditorUsernamePlaceholder}" />
|
||||
</Grid>
|
||||
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
||||
Height="56" 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="11"
|
||||
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>
|
||||
<!--
|
||||
The tags this host wears. Chips that toggle rather than a multi-select list, because a chip is
|
||||
what a tag looks like on the card in the grid — a list of names to tick would make the user
|
||||
match an entry to a chip they can already see.
|
||||
|
||||
The box under them creates one and puts it on straight away. That is where a tag is usually
|
||||
wanted: while tagging a host and finding it does not exist yet. Unlike every other field here it
|
||||
writes to the keychain immediately, because a host can only name a tag that has an id — so
|
||||
cancelling this editor leaves the tag behind, which is honest rather than hidden. Renaming and
|
||||
deleting are on the keychain screen, where every other item kind is managed.
|
||||
-->
|
||||
<ItemsControl ItemsSource="{Binding EditorTagChoices}" IsVisible="{Binding HasTagChoices}"
|
||||
Margin="0,4,0,0">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate><WrapPanel /></ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TagChoice">
|
||||
<!--
|
||||
Worn is filled, unworn is outlined. One control per tag with two states rather than a
|
||||
checkbox beside a label: the state and the name occupy the same object, so a row of them
|
||||
reads as the host's tags rather than as a form about them.
|
||||
-->
|
||||
<Button Classes="chiptoggle" Classes.worn="{Binding IsWorn}" Margin="0,0,4,4"
|
||||
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).ToggleEditorTagCommand}"
|
||||
CommandParameter="{Binding}">
|
||||
<TextBlock Text="{Binding Label}" FontSize="10.5" />
|
||||
</Button>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<Grid ColumnDefinitions="*,6,Auto">
|
||||
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding AddEditorTagCommand}" />
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="2" Classes="ghost" Content="ADD" Command="{Binding AddEditorTagCommand}" />
|
||||
</Grid>
|
||||
|
||||
<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="11"
|
||||
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>
|
||||
|
||||
<!-- ============ THE GROUP EDITOR ============ -->
|
||||
<!--
|
||||
Here rather than on the Keychain screen, because a group is not a secret — it is how this screen's
|
||||
grid is arranged, and the arranging belongs beside the thing arranged. Filing a host into one is
|
||||
done in the host's own editor above, for the same reason its key and its password are.
|
||||
|
||||
One form for both adding and renaming; GroupSaveLabel is what says which of the two is about to
|
||||
happen. The four fields under the name are what the hosts inside inherit when they say nothing
|
||||
themselves; every one of them may be left empty, and empty means "lend nothing" rather than
|
||||
"unset". The parent picker leaves out this group and everything beneath it, so a cycle cannot be
|
||||
made here — which is a courtesy rather than the guarantee, because one assembled offline on two
|
||||
machines was never offered this list. See HostInheritance.
|
||||
-->
|
||||
<StackPanel Margin="16" Spacing="6" IsVisible="{Binding IsEditingGroup}">
|
||||
|
||||
<TextBlock Classes="label" Text="GROUP" Foreground="{StaticResource TextDim}" Margin="0,0,0,4" />
|
||||
|
||||
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
||||
Text="A heading for the grid, and the defaults every host under it inherits. Which group a host is in is part of the host, and stays encrypted." />
|
||||
|
||||
<TextBox Text="{Binding GroupEditorLabel}" PlaceholderText="group name" />
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorParentChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedParent}"
|
||||
HorizontalAlignment="Stretch">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:GroupChoice">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<Grid ColumnDefinitions="*,8,*">
|
||||
<NumericUpDown Grid.Column="0" Value="{Binding GroupEditorDefaultPort}" Minimum="1"
|
||||
Maximum="65535" FormatString="0" ShowButtonSpinner="False"
|
||||
PlaceholderText="default port" />
|
||||
<TextBox Grid.Column="2" Text="{Binding GroupEditorDefaultUsername}"
|
||||
PlaceholderText="default username" />
|
||||
</Grid>
|
||||
|
||||
<ComboBox ItemsSource="{Binding GroupEditorAuthenticationChoices}"
|
||||
SelectedItem="{Binding GroupEditorSelectedAuthentication}"
|
||||
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="11"
|
||||
VerticalAlignment="Center"
|
||||
IsVisible="{Binding Qualifier, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
|
||||
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||
<Button Classes="accent" Content="{Binding GroupSaveLabel}"
|
||||
Command="{Binding SaveGroupCommand}" />
|
||||
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelGroupEditCommand}" />
|
||||
</StackPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
</Panel>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
|
||||
</UserControl>
|
||||
Reference in New Issue
Block a user