Public Access
Settings → Security gains a SPELUNK section: an API address and a key, saved as a new keychain item, a Spelunk key (SyncEntityType 14, AadResourceType 17, server table spelunk_key via the AddSpelunkKeyItem migration). It is ciphertext only on the server, lives in the personal vault, syncs like every secret, and holds the key together with the one address it may be sent to. The key box empties once saved; the list shows name, address and how many hosts use each key, never the key. Saving a key for an address that already has one replaces it, and deleting goes through the shared confirmation card with the hosts that would stop connecting. A host's route names a saved key where it named a credential, in the same apiKeyCredentialId field, so no host schema changes and an older build reads such a host as one whose key credential has gone. At connect a saved key is sent only to its own address; a host naming it with another Spelunk address is refused in words. Keys kept as a credential's password still work. The editor offers saved keys first and picks the one saved for an address as it is typed. A link from a Spelunk with a saved key now connects straight away, including the first one: saving the key is the user's say-so that the first-link editor used to ask for. The address and key merge as one pair, so two concurrent edits can never pair one device's key with another's address, and the key never reaches the conflict log.
1122 lines
71 KiB
XML
1122 lines
71 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:dom="using:DodoSSH.Client.Domain"
|
|
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.
|
|
|
|
── A HEADER, A BODY THAT SCROLLS, AND A FOOTER. ─────────────────────────────────────────────────────
|
|
Three rows rather than one column, which is the shape the v4 design draws and the one change here that
|
|
is structural rather than cosmetic. The header names what the drawer is about and carries the two
|
|
controls that are true of every panel — the overflow menu and the arrow that puts the drawer away. The
|
|
footer carries the one thing each panel is for: CONNECT, or SAVE, or the question about deleting. Only
|
|
the middle scrolls, so the button somebody came here to press is never below the fold — which it was,
|
|
on a host with fifteen tags.
|
|
|
|
── 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 the pane has been asked
|
|
for", 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
|
|
320-pixel column beside a grid is 320 pixels the grid could have had.
|
|
|
|
◆ AND IT IS ASKED FOR NOW, rather than arriving with a selection. Touching a card used to open this,
|
|
which charged the width of the pane for the act of choosing; the pencil on a card is the asking. See
|
|
VaultViewModel.IsHostPaneOpen and the item template in HostsScreen.axaml.
|
|
|
|
── THE TWO PANELS ARE THE SAME CARDS ────────────────────────────────────────────────────────────────
|
|
Address, General, Connection, and — since v5 — Quick access: first as rows stating what the host is,
|
|
then as boxes for changing it. QUICK ACCESS is the one card the detail pane draws conditionally rather
|
|
than always: a host with no pinned paths has nothing to state, where an empty Address or General would
|
|
be a fact about the host rather than an absent one. The detail pane's rows are buttons that open the
|
|
editor, so a box that looks editable turns out to be, one step along; see Button.fieldrow in App.axaml
|
|
for why they are not inputs that save as you type.
|
|
|
|
The editor's four cards carry v5's own section labels — ADDRESS, GENERAL, CONNECTION, QUICK ACCESS — the
|
|
mock's tracked-out capitals rather than TextBlock.sectionhead's sentence case; see TextBlock.sectionlabel
|
|
in App.axaml and the remark on the editor's StackPanel below for why the detail pane did not move too.
|
|
|
|
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.
|
|
|
|
── WHAT THE DESIGN DRAWS HERE AND THIS PANE HAS NOT GOT ─────────────────────────────────────────────
|
|
Share this host, Add Telnet, "SSH ID, Certificate, FIDO2", and the backspace-key mapping row. Four
|
|
controls with nothing behind them: sharing is per vault and not per item, every session here is an SSH
|
|
channel, there are no identity or certificate item types, and nothing carries a terminal setting to the
|
|
renderer. They are listed in docs/design-import-gaps.md with what ships instead, and none of them is
|
|
drawn disabled.
|
|
|
|
The design's fifth missing control was the vault picker's chevron, and both halves of what it stood for
|
|
now exist without it: a host being *created* is asked which vault it goes into, in the editor below, and
|
|
an existing one is moved from the pane's ⋯ menu. The chevron itself stays undrawn, because a chevron on
|
|
a subtitle implies an edit and this is not one — the two vaults are encrypted under different keys, so a
|
|
move is a re-seal into one and a tombstone in the other, and the host takes a new id.
|
|
-->
|
|
|
|
<Border Width="320" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
|
|
|
|
<Grid RowDefinitions="Auto,*,Auto">
|
|
|
|
<!-- ============ THE HEADER ============ -->
|
|
<!--
|
|
One row for all three panels, which is why what it says is on the view model rather than repeated
|
|
three times here. See VaultViewModel.DrawerTitle.
|
|
|
|
The subtitle is the vault this host is filed in, and the design's chevron beside it is still not
|
|
drawn although a host can now be moved. The two are encrypted under different keys, so a move is a
|
|
re-seal into one vault and a tombstone in the other — it leaves the host's group and tags behind and
|
|
gives it a new id, none of which a chevron on a subtitle would lead anybody to expect. It is in the
|
|
menu instead, next to the two other things that happen to a whole host. Choosing the vault at the
|
|
moment a host is created is a different question, and it is in the editor beside the name.
|
|
-->
|
|
<Border Grid.Row="0" Padding="22,16" Background="{StaticResource Panel}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
|
<Grid ColumnDefinitions="*,Auto,Auto">
|
|
|
|
<!--
|
|
Title 20 bold and subtitle 12.5, the mock's own header sizes. What the subtitle says is
|
|
unchanged — see DrawerSubtitle's own remarks below the header for why it is the vault's name
|
|
rather than the mock's "Saving to X vault" sentence with a picker's chevron inside it.
|
|
-->
|
|
<StackPanel Grid.Column="0" VerticalAlignment="Center" Spacing="2">
|
|
<TextBlock Text="{Binding DrawerTitle}" FontSize="20" FontWeight="Bold"
|
|
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
|
<TextBlock Text="{Binding DrawerSubtitle}" FontSize="12.5"
|
|
Foreground="{StaticResource TextFaint}" TextTrimming="CharacterEllipsis"
|
|
IsVisible="{Binding DrawerSubtitle,
|
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The host's own actions, behind a menu rather than as a row of buttons under the pane. They are
|
|
what EDIT and DELETE were; a pane whose footer is CONNECT has one action worth a button, and the
|
|
rest are things you go looking for. It hides with the deletion question and with the move panel
|
|
for the reason the buttons did — see VaultViewModel.ShowsHostPaneActions.
|
|
|
|
Moving is here rather than in the editor, and the separator says which side of the line it is
|
|
on: it is not a field of the host. The two vaults are encrypted under different keys, so it is a
|
|
re-seal into one and a tombstone in the other — nothing a SAVE could do — and a picker inside
|
|
the form would let somebody correcting a port move a machine by leaving it where they found it.
|
|
-->
|
|
<Button Grid.Column="1" Classes="flat paneicon" Content="⋯"
|
|
IsVisible="{Binding ShowsHostPaneActions}"
|
|
ToolTip.Tip="More things to do with this host">
|
|
<Button.Flyout>
|
|
<MenuFlyout>
|
|
<MenuItem Header="Edit…" Command="{Binding EditSelectedHostCommand}" />
|
|
<MenuItem Header="Move to another vault…" Command="{Binding MoveHostCommand}" />
|
|
<Separator />
|
|
<MenuItem Header="Delete…" Command="{Binding DeleteHostCommand}" />
|
|
</MenuFlyout>
|
|
</Button.Flyout>
|
|
</Button>
|
|
|
|
<Button Grid.Column="2" Classes="flat paneicon" Content="⇥"
|
|
Command="{Binding CloseDrawerCommand}"
|
|
ToolTip.Tip="Closes this pane and gives the grid the width back. The host stays selected." />
|
|
|
|
</Grid>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ THE BODY SCROLLS AND THE ROWS ABOVE AND BELOW IT DO NOT.
|
|
|
|
The layout harness skips anything with a ScrollViewer in its ancestry — see
|
|
LayoutHarness.IsScrollable — so from here on it certifies that the header, the footer and the
|
|
column itself fit, rather than that every field inside the cards does. That is the true claim about
|
|
a pane that scrolls, and it is a better one than before: what the harness now measures includes the
|
|
button each panel exists to offer, which is exactly the control that used to be able to fall off the
|
|
bottom. The tag picker is why the middle has to scroll at all — its height is a chip per tag in the
|
|
keychain, wrapped, so no fixed height holds it for somebody with fifteen.
|
|
-->
|
|
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">
|
|
<Panel>
|
|
|
|
<!-- ============ WHAT THIS HOST IS ============ -->
|
|
<StackPanel Margin="16,14" Spacing="10" IsVisible="{Binding IsShowingHostDetail}">
|
|
|
|
<!--
|
|
============ ADDRESS ============
|
|
The hostname alone, beside the same mark the host's card in the grid carries. The account and
|
|
the port are in the connection card below rather than crammed into one user@host:port string:
|
|
this is the pane with room to separate them, and the two are edited in different boxes.
|
|
-->
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="8">
|
|
<TextBlock Classes="sectionhead" Text="Address" />
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<Border Grid.Column="0" Classes="tileicon" Background="{StaticResource Chip}">
|
|
<TextBlock Classes="mono" Text=">_" FontSize="11" FontWeight="Bold"
|
|
Foreground="{StaticResource AccentText}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
<!--
|
|
Classes="address" is the v5 restyle: 42 tall and bordered in accent rather than the
|
|
ordinary hairline, matched by TextBox.address in App.axaml, which the editor's own
|
|
hostname box below wears too — the two are the same field, one step apart.
|
|
-->
|
|
<Button Grid.Column="1" Classes="fieldrow address" Margin="10,0,0,0"
|
|
Command="{Binding EditSelectedHostCommand}"
|
|
ToolTip.Tip="Opens this host's editor.">
|
|
<TextBlock Classes="mono" Text="{Binding SelectedHost.Host.Hostname}" FontSize="12"
|
|
TextTrimming="CharacterEllipsis" />
|
|
</Button>
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
============ GENERAL ============
|
|
The name, where it is filed, what it wears, and whatever was written about it. Notes are the
|
|
one row that is not always there: an empty box labelled nothing is a row that says a host has
|
|
no notes, which is not a fact anybody came here for.
|
|
-->
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionhead" Text="General" Margin="0,0,0,2" />
|
|
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
|
<TextBlock Text="{Binding SelectedHost.Label}" TextTrimming="CharacterEllipsis" />
|
|
</Button>
|
|
|
|
<!--
|
|
The group, with the same mark its card carries in the grid. A host in none says so rather
|
|
than showing an empty box, for the reason the notes row is absent: blank and "none" look
|
|
identical and only one of them is an answer.
|
|
-->
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="▤" />
|
|
<TextBlock Grid.Column="1" Text="{Binding SelectedHost.GroupLabel}"
|
|
TextTrimming="CharacterEllipsis"
|
|
IsVisible="{Binding SelectedHost.HasGroup}" />
|
|
<TextBlock Grid.Column="1" Text="No group"
|
|
Foreground="{StaticResource TextFaint}"
|
|
IsVisible="{Binding !SelectedHost.HasGroup}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<!--
|
|
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 and a shared template would have to
|
|
be told which.
|
|
-->
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="#" VerticalAlignment="Top"
|
|
Margin="0,3,0,0" />
|
|
<ItemsControl Grid.Column="1" 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,2">
|
|
<TextBlock Text="{Binding}" FontSize="9.5" />
|
|
</Border>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
<TextBlock Grid.Column="1" Text="No tags" Foreground="{StaticResource TextFaint}"
|
|
IsVisible="{Binding !SelectedHost.HasTags}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}"
|
|
IsVisible="{Binding SelectedHost.Host.Notes,
|
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="✎" VerticalAlignment="Top"
|
|
Margin="0,2,0,0" />
|
|
<TextBlock Grid.Column="1" Text="{Binding SelectedHost.Host.Notes}" FontSize="12"
|
|
Foreground="{StaticResource TextDim}" TextWrapping="Wrap" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
============ CONNECTION ============
|
|
What this host dials and what it authenticates with — the two things CONNECT in the footer is
|
|
about, which is why they share a card with the box that some hosts need filled in.
|
|
|
|
Every value here is the *resolved* one, so a host that states nothing of its own shows what it
|
|
would actually use rather than a blank. Where that came from a group the row says "inherited"
|
|
beside it: the number is the same either way and the edit is not, because clearing a group's
|
|
default moves every host that never overrode it.
|
|
-->
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="6">
|
|
|
|
<Grid ColumnDefinitions="Auto,72,*">
|
|
<TextBlock Grid.Column="0" Classes="sectionhead" Text="SSH on"
|
|
VerticalAlignment="Center" />
|
|
<Button Grid.Column="1" Classes="fieldrow" Margin="8,0"
|
|
Command="{Binding EditSelectedHostCommand}">
|
|
<TextBlock Classes="mono" Text="{Binding SelectedHostPortLabel}" FontSize="12" />
|
|
</Button>
|
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6"
|
|
VerticalAlignment="Center">
|
|
<TextBlock Text="port" Foreground="{StaticResource TextDim}" />
|
|
<TextBlock Text="inherited" FontSize="11" Foreground="{StaticResource TextFaint}"
|
|
IsVisible="{Binding SelectedHostPortIsInherited}"
|
|
ToolTip.Tip="This host states no port of its own and takes its group's." />
|
|
</StackPanel>
|
|
</Grid>
|
|
|
|
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="0,4" />
|
|
|
|
<TextBlock Classes="sectionhead" Text="Credentials" Margin="0,0,0,2" />
|
|
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<Button Grid.Column="0" Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="@" />
|
|
<TextBlock Grid.Column="1" Text="{Binding SelectedHostUsernameLabel}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
</Grid>
|
|
</Button>
|
|
<TextBlock Grid.Column="1" Text="inherited" FontSize="11" Margin="6,0,0,0"
|
|
VerticalAlignment="Center" Foreground="{StaticResource TextFaint}"
|
|
IsVisible="{Binding SelectedHostUsernameIsInherited}"
|
|
ToolTip.Tip="This host states no account of its own and takes its group's." />
|
|
</Grid>
|
|
|
|
<!--
|
|
The box a host that wants a typed password needs, and it is a real one — the only input in
|
|
this pane, because what it holds is not part of the host and is never saved with it.
|
|
|
|
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. A column also has room to put REMEMBER under the box it
|
|
qualifies, which is where it reads as a property of the password rather than as a fourth
|
|
control in a row.
|
|
|
|
REMEMBER travels with the box and hides with it. It is the two-step chore the box's tooltip
|
|
used to describe — add a password under Keychain, then bind the host to it — done from the
|
|
one place that already has the password, and it takes effect only once the remote has
|
|
accepted it.
|
|
-->
|
|
<TextBox Text="{Binding ConnectPassword}" PlaceholderText="password"
|
|
PasswordChar="•" HorizontalAlignment="Stretch"
|
|
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
|
ToolTip.Tip="Typed each time unless REMEMBER is ticked, in which case it is saved to your keychain and bound to this host once the connection succeeds." />
|
|
<CheckBox IsChecked="{Binding RemembersConnectPassword}"
|
|
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
|
ToolTip.Tip="Saves this password to your keychain, bound to this host, so it is not asked for again. It syncs to your other machines, and only happens if the connection works.">
|
|
<TextBlock Text="Remember this password" Classes="hint" FontSize="12" />
|
|
</CheckBox>
|
|
|
|
<!--
|
|
What it authenticates with instead, named. The row is the item's own label and the sentence
|
|
under it is which of the three ways this is and where the binding came from — the note
|
|
rather than the one word the card shows, because 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.
|
|
-->
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}"
|
|
IsVisible="{Binding !SelectedHostAsksForAPassword}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<!--
|
|
◆ rather than the ⚿ the nav rail marks the keychain with, and that is a rendering
|
|
decision rather than a semantic one: this application substitutes the design's fonts
|
|
for Inter and the system monospace stack — see MonoFont in Palette.axaml — and U+26BF
|
|
is outside what either supplies, so it lands on whatever the platform's fallback has.
|
|
Every other glyph in this pane is from Geometric Shapes, which both faces carry.
|
|
-->
|
|
<TextBlock Grid.Column="0" Classes="fieldglyph" Text="◆" />
|
|
<TextBlock Grid.Column="1" Text="{Binding SelectedHostBindingLabel}"
|
|
TextTrimming="CharacterEllipsis" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<TextBlock Classes="hint" FontSize="11" Text="{Binding SelectedHostAuthenticationNote}"
|
|
IsVisible="{Binding SelectedHostAuthenticationNote,
|
|
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
============ QUICK ACCESS (read-only) ============
|
|
The folders pinned on this host, in the pane's own fieldrow idiom rather than the editor's rows
|
|
with a close box on each one: one Button opens the editor on a press, same as every other row
|
|
here, and what is inside it is a list rather than a single fact — the same shape the tags row
|
|
above takes for the same reason. Hidden entirely rather than shown empty, for the reason Notes
|
|
is: a card titled QUICK ACCESS with nothing under it would answer a question nobody asked here.
|
|
-->
|
|
<Border Classes="section" IsVisible="{Binding SelectedHost.HasPins}">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionhead" Text="Quick access" Margin="0,0,0,2" />
|
|
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="13"
|
|
Foreground="{StaticResource TextFaint}" Width="16" VerticalAlignment="Top"
|
|
Margin="0,3,0,0" Text="" />
|
|
<ItemsControl Grid.Column="1" ItemsSource="{Binding SelectedHost.Host.PinnedPaths}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="x:String">
|
|
<TextBlock Classes="mono" Text="{Binding}" FontSize="12"
|
|
TextTrimming="CharacterEllipsis" Margin="0,0,0,2" />
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</Grid>
|
|
</Button>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
============ FORWARDS (read-only) ============
|
|
QUICK ACCESS's shape, and hidden when empty for the same reason: a card with nothing under it
|
|
answers a question nobody asked here.
|
|
-->
|
|
<Border Classes="section" IsVisible="{Binding SelectedHost.HasForwards}">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionhead" Text="Port forwards" Margin="0,0,0,2" />
|
|
|
|
<Button Classes="fieldrow" Command="{Binding EditSelectedHostCommand}">
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="13"
|
|
Foreground="{StaticResource TextFaint}" Width="16" VerticalAlignment="Top"
|
|
Margin="0,3,0,0" Text="" />
|
|
<ItemsControl Grid.Column="1" ItemsSource="{Binding SelectedHost.Host.PortForwards}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="dom:PortForwardRule">
|
|
<TextBlock Classes="mono" Text="{Binding Notation}" FontSize="12"
|
|
TextTrimming="CharacterEllipsis" Margin="0,0,0,2" />
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</Grid>
|
|
</Button>
|
|
</StackPanel>
|
|
</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. Same four cards as the pane
|
|
above, with the boxes real — which is what makes pressing a row up there lead somewhere that
|
|
looks like where it was pressed. QUICK ACCESS is the one card that always draws here even when
|
|
the pane above hid it for having nothing to show: an empty editor section is where a first pin
|
|
gets added, so it cannot be conditional on already having one.
|
|
-->
|
|
<StackPanel Margin="16,14" Spacing="10" IsVisible="{Binding IsEditing}">
|
|
|
|
<!--
|
|
v5's own section labels — ADDRESS, GENERAL, CONNECTION, QUICK ACCESS — replace sentence-case
|
|
headings on this one panel, the editor, because it is the panel the mock actually draws; the
|
|
detail pane above keeps TextBlock.sectionhead, its own established idiom, since the mock has no
|
|
read-only view to restyle it against. See TextBlock.sectionlabel in App.axaml.
|
|
-->
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="8">
|
|
<TextBlock Classes="sectionlabel" Text="ADDRESS" />
|
|
<Grid ColumnDefinitions="Auto,*">
|
|
<Border Grid.Column="0" Classes="tileicon" Background="{StaticResource Chip}">
|
|
<TextBlock Classes="mono" Text=">_" FontSize="11" FontWeight="Bold"
|
|
Foreground="{StaticResource AccentText}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
<!--
|
|
No drawn caret: this TextBox has a real one, and the mock's blinking bar is what a real
|
|
caret looks like in a screenshot that cannot show motion. Classes="address" is the 42-tall,
|
|
accent-bordered restyle shared with the detail pane's own Hostname row above.
|
|
-->
|
|
<TextBox Grid.Column="1" Classes="address" Margin="10,0,0,0" Text="{Binding EditorHostname}"
|
|
PlaceholderText="hostname or address" />
|
|
</Grid>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionlabel" Text="GENERAL" Margin="0,0,0,2" />
|
|
|
|
<TextBox Text="{Binding EditorLabel}" PlaceholderText="name" Height="40" />
|
|
|
|
<!--
|
|
◆ WHICH VAULT THIS HOST WILL LIVE IN, asked here because it decides who can read it and
|
|
because it is the one thing on this form that no later SAVE can change: the vaults are
|
|
encrypted under different keys, so changing it is a re-seal into one and a tombstone in
|
|
the other. That is offered — "Move to another vault…" in the pane's own menu — and it is
|
|
deliberately not this control, because a picker inside the form would move a machine as a
|
|
side effect of correcting a port.
|
|
|
|
It is a field of the host rather than the keychain screen's standing "new items go to"
|
|
preference, and it is a separate selection from it — moving this one does not move that
|
|
one, and a click over there cannot move a host half-typed here.
|
|
|
|
Shown only while adding, and only where there is more than one vault that can be written
|
|
to. An existing host's row is not drawn at all rather than drawn disabled; the drawer's
|
|
header already says where the host is filed. See VaultViewModel.ShowsEditorVaultChoice.
|
|
|
|
The group picker below follows it: a group is an item in one vault, so choosing a vault
|
|
refills that list with that vault's groups and clears what was chosen from another's.
|
|
-->
|
|
<StackPanel Spacing="4" IsVisible="{Binding ShowsEditorVaultChoice}">
|
|
<ComboBox ItemsSource="{Binding EditorVaultChoices}"
|
|
SelectedItem="{Binding EditorSelectedVault}"
|
|
HorizontalAlignment="Stretch">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
|
Text="A host in a shared vault is readable by everybody holding that vault's key. It can be moved out later, from this pane's own menu — what it cannot do is become unreadable to somebody who has already synced it." />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
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" Height="40">
|
|
<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,2,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>
|
|
|
|
<!--
|
|
Add button is 40x40 and a glyph rather than the word ADD, matched to the field beside it —
|
|
the v5 mock draws every add affordance in this drawer as a square icon button; QUICK ACCESS's
|
|
own add row below is the same idiom at 36x36, one size down for its shorter field.
|
|
-->
|
|
<Grid ColumnDefinitions="*,6,Auto">
|
|
<TextBox Grid.Column="0" Text="{Binding EditorNewTag}" PlaceholderText="new tag"
|
|
Height="40">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding AddEditorTagCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<Button Grid.Column="2" Classes="ghost" Content="+" Width="40" Height="40"
|
|
FontSize="16" HorizontalContentAlignment="Center"
|
|
Command="{Binding AddEditorTagCommand}" ToolTip.Tip="Adds this tag" />
|
|
</Grid>
|
|
|
|
<TextBox Text="{Binding EditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="58" TextWrapping="Wrap" />
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionlabel" Text="CONNECTION" Margin="0,0,0,2" />
|
|
|
|
<!--
|
|
Username first and port 78 wide second, the mock's own order — reversed from the field's
|
|
earlier "port, username" layout, which put the number that is usually left blank ahead of
|
|
the one that usually is not. Both boxes are still allowed to be empty, and empty still 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 above moves.
|
|
-->
|
|
<Grid ColumnDefinitions="*,8,78">
|
|
<TextBox Grid.Column="0" Text="{Binding EditorUsername}"
|
|
PlaceholderText="{Binding EditorUsernamePlaceholder}" Height="40"
|
|
IsEnabled="{Binding EditorAsksForLogin}" />
|
|
<NumericUpDown Grid.Column="2" FontFamily="{StaticResource MonoFont}"
|
|
Value="{Binding EditorPort}" Minimum="1" Maximum="65535" FormatString="0"
|
|
ShowButtonSpinner="False" PlaceholderText="{Binding EditorPortPlaceholder}"
|
|
Height="40" />
|
|
</Grid>
|
|
|
|
<!--
|
|
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}"
|
|
IsEnabled="{Binding EditorAsksForLogin}"
|
|
HorizontalAlignment="Stretch" Height="40">
|
|
<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>
|
|
|
|
<!--
|
|
Why the two controls above are greyed out for a host Spelunk signs in to: disabled rather
|
|
than hidden, so a login somebody chose is still there to see and comes back if the box in
|
|
the Spelunk card is unticked before saving. See VaultViewModel.EditorAsksForLogin.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
|
IsVisible="{Binding !EditorAsksForLogin}"
|
|
Text="Spelunk signs you in to this host, so it needs no username, SSH key or password." />
|
|
|
|
<!--
|
|
Making a credential without leaving the host. The moment one is wanted is this one: somebody
|
|
is deciding how a host authenticates and finds the password is not in the keychain yet, and
|
|
sending them to the other screen to add it would lose the half-typed host they are standing
|
|
in. Same argument as the new-tag box further down, same immediate write, same honest
|
|
consequence — the credential stays if this editor is cancelled, because a host can only name
|
|
an id that exists.
|
|
|
|
A button beside the picker rather than an entry inside it. Every row of that list is a
|
|
binding the host can have; "make a new one" is an action, and as an entry it would sit in the
|
|
box afterwards describing a state no host can be in.
|
|
-->
|
|
<Button Classes="ghost" Content="+ NEW CREDENTIAL" HorizontalAlignment="Left"
|
|
FontSize="10.5" Height="28" Padding="10,0"
|
|
IsVisible="{Binding !IsAddingEditorCredential}"
|
|
Command="{Binding BeginEditorCredentialCommand}"
|
|
ToolTip.Tip="Adds a credential to the keychain and binds this host to it" />
|
|
|
|
<Border CornerRadius="12" Background="{StaticResource Field}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="12"
|
|
IsVisible="{Binding IsAddingEditorCredential}">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="label" Text="NEW CREDENTIAL" FontSize="10" />
|
|
<TextBox Text="{Binding EditorNewCredentialLabel}" PlaceholderText="name" Height="36" />
|
|
<!--
|
|
Optional, and what makes a credential worth being its own item: one account on twenty
|
|
machines is rotated in one place. Left blank, this host's own username is used.
|
|
-->
|
|
<TextBox Text="{Binding EditorNewCredentialUsername}" Height="36"
|
|
PlaceholderText="username (blank: use this host's own)" />
|
|
<!-- Masked, on the reasoning the keychain's own password box carries. -->
|
|
<TextBox Text="{Binding EditorNewCredentialPassword}" PlaceholderText="password"
|
|
PasswordChar="•" Height="36">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding AddEditorCredentialCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<TextBox Text="{Binding EditorNewCredentialNotes}" PlaceholderText="notes"
|
|
AcceptsReturn="True" Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="Added to the keychain as soon as you press ADD, so it stays even if you cancel this host. Renaming and deleting are on the keychain screen." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="ADD"
|
|
Command="{Binding AddEditorCredentialCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL"
|
|
Command="{Binding CancelEditorCredentialCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ THE RELAY CARD, restyled to the mock's nested-card shape — radius 12, a checkbox with the
|
|
title beside it rather than under it. The "(not wired up yet)" this title carried while the
|
|
client had no path to the relay is gone, because it has one: a relayed host is connected
|
|
through a ticket, a WebSocket and a loopback bridge. See docs/reaching-a-host-you-cannot-dial.md.
|
|
-->
|
|
<Border CornerRadius="12" Background="{StaticResource Field}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="12">
|
|
<StackPanel Spacing="6">
|
|
<CheckBox IsChecked="{Binding EditorRelayEnabled}" VerticalContentAlignment="Center">
|
|
<TextBlock Text="Connect through the server relay" FontSize="13"
|
|
FontWeight="SemiBold" Foreground="{StaticResource Text}"
|
|
TextWrapping="Wrap" />
|
|
</CheckBox>
|
|
<!--
|
|
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.
|
|
|
|
It also says the two things a user would otherwise find out by failing: the server,
|
|
not this machine, has to be able to reach the address — the relay is no help against a
|
|
deployment outside the target's network — and a relayed host does not fall back to
|
|
dialling directly, so it connects only while the server is reachable.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="11.5"
|
|
Text="The server dials this host on your behalf, for a machine this computer cannot reach — so the server has to be able to reach it. That is why the address and port are stored on the server in plain text; everything else about the host stays encrypted. A relayed host connects only while you are online." />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
◆ THE SPELUNK CARD, the relay card's twin and its alternative: ticking one unticks the other,
|
|
because a host has one route. A host ticked here is reached through the Spelunk agent already
|
|
on the server, so it needs no open SSH port at all. The fields appear only when the box is
|
|
ticked, and the key is a keychain credential — picked, never typed here — so it is encrypted
|
|
and synced like every other secret. See VaultViewModel.Spelunk.cs.
|
|
-->
|
|
<Border CornerRadius="12" Background="{StaticResource Field}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="12">
|
|
<StackPanel Spacing="6">
|
|
<CheckBox IsChecked="{Binding EditorThroughSpelunk}" VerticalContentAlignment="Center">
|
|
<TextBlock Text="Connect through Spelunk" FontSize="13"
|
|
FontWeight="SemiBold" Foreground="{StaticResource Text}"
|
|
TextWrapping="Wrap" />
|
|
</CheckBox>
|
|
<StackPanel Spacing="6" IsVisible="{Binding EditorThroughSpelunk}">
|
|
<TextBox Text="{Binding EditorSpelunkApi}" Height="36"
|
|
PlaceholderText="Spelunk API address (https://api.…)" />
|
|
<TextBox Text="{Binding EditorSpelunkServerId}" Height="36"
|
|
PlaceholderText="Spelunk server id" />
|
|
<!--
|
|
Saved Spelunk keys first (Settings → Security), each qualified by the one address it
|
|
goes to, then credentials whose password is a key. A deleted one keeps a placeholder, as
|
|
the authentication picker's does, so an unrelated edit cannot drop it. Typing an address
|
|
a key is saved for picks that key when nothing is picked yet.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="10.5" Text="Spelunk API key" />
|
|
<ComboBox ItemsSource="{Binding EditorSpelunkKeyChoices}"
|
|
SelectedItem="{Binding EditorSelectedSpelunkKey}"
|
|
HorizontalAlignment="Stretch" Height="40">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:AuthenticationChoice">
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<TextBlock Text="{Binding Label}" />
|
|
<TextBlock Text="{Binding Qualifier}" Classes="hint" FontSize="11"
|
|
VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<!--
|
|
Spelunk signs the user in: a key pair made in memory for each connection, its public
|
|
half sent with the ticket, and the agent's one-connection SSH server accepting only
|
|
that. Ticking it greys out the login above. See SpelunkRoute.SignInThroughSpelunk.
|
|
-->
|
|
<CheckBox IsChecked="{Binding EditorSpelunkSignIn}" VerticalContentAlignment="Center">
|
|
<TextBlock Text="Sign in through Spelunk (no SSH key needed)" FontSize="12.5"
|
|
Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
|
</CheckBox>
|
|
</StackPanel>
|
|
<!--
|
|
What the route asks for and what it costs, said where the decision is made: an Admin-role
|
|
Spelunk API key, sent to the address above and nowhere else, and a host that is reached
|
|
only while Spelunk and its agent are.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="11.5"
|
|
Text="Spelunk's agent on the server connects to its SSH server for you, so the server needs no open SSH port. Save your Spelunk API key (Admin role) under Settings → Security and choose it here, or keep it as a credential's password; it is sent only to the address above. Signing in through Spelunk logs you in as the account Spelunk names, with a key made for each connection and never kept. The hostname and port still name this machine's host key. Connects only while Spelunk and its agent are reachable." />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
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"
|
|
Margin="0,4,0,0"
|
|
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>
|
|
|
|
<!--
|
|
============ QUICK ACCESS ============
|
|
The folders pinned on this host. Staged on VaultViewModel.EditorPinnedPaths the way the tag
|
|
picker stages editorTagIds — see that field's own remarks — populated when the editor opens,
|
|
read back by BuildHost on Save, and left alone by CANCEL for the same reason every other field
|
|
here is: the whole editor is abandoned together.
|
|
-->
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="8">
|
|
<TextBlock Classes="sectionlabel" Text="QUICK ACCESS" />
|
|
|
|
<ItemsControl ItemsSource="{Binding EditorPinnedPaths}">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><StackPanel Spacing="6" /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="x:String">
|
|
<Border Height="33" CornerRadius="9" Background="{StaticResource Field}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="8,0">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="14"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
|
Text="" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding}" FontSize="12.5"
|
|
Margin="8,0" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<!--
|
|
$parent[ItemsControl] rather than #Board: this ItemsControl's own DataContext is
|
|
already the VaultViewModel — HostDrawer's root x:DataType is that type directly —
|
|
so one level up is enough, the same single-hop case ToggleEditorTagCommand above
|
|
already uses.
|
|
-->
|
|
<Button Grid.Column="2" Classes="flat paneicon" Content="✕" Width="22" Height="22"
|
|
FontSize="11"
|
|
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).RemoveEditorPinCommand}"
|
|
CommandParameter="{Binding}" ToolTip.Tip="Unpins this path" />
|
|
</Grid>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<Grid ColumnDefinitions="*,6,Auto">
|
|
<TextBox Grid.Column="0" FontFamily="{StaticResource MonoFont}"
|
|
Text="{Binding EditorNewPin}"
|
|
PlaceholderText="/path/to/folder" Height="36">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding AddEditorPinCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<Button Grid.Column="2" Classes="ghost" Content="+" Width="36" Height="36" FontSize="15"
|
|
HorizontalContentAlignment="Center"
|
|
Command="{Binding AddEditorPinCommand}" ToolTip.Tip="Pins this path" />
|
|
</Grid>
|
|
|
|
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
|
Text="Pinned folders appear above the terminal for this host." />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!--
|
|
============ PORT FORWARDS ============
|
|
Staged on VaultViewModel.EditorPortForwards the way QUICK ACCESS stages its pins, and saved
|
|
with the host. Each row is drawn in OpenSSH's own notation, because that is what anybody who has
|
|
used one reads fastest; the add row spells out which end the listening port is on instead,
|
|
because -L and -R are the two flags people get backwards.
|
|
|
|
No bind-address field, and that is not an omission: every listener binds loopback. A forward is
|
|
a field every member of a shared keychain can fill in, and one bound to 0.0.0.0 would hand the
|
|
remote's network to anybody who can reach this machine. See SshPortForwardSpec.
|
|
-->
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="8">
|
|
<TextBlock Classes="sectionlabel" Text="PORT FORWARDS" />
|
|
|
|
<ItemsControl ItemsSource="{Binding EditorPortForwards}">
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate><StackPanel Spacing="6" /></ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="dom:PortForwardRule">
|
|
<Border Height="33" CornerRadius="9" Background="{StaticResource Field}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="8,0">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<TextBlock Grid.Column="0" FontFamily="{StaticResource IconFont}" FontSize="14"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
|
Text="" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Notation}" FontSize="12.5"
|
|
Margin="8,0" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" ToolTip.Tip="{Binding Notation}" />
|
|
<Button Grid.Column="2" Classes="flat paneicon" Content="✕" Width="22" Height="22"
|
|
FontSize="11"
|
|
Command="{Binding $parent[ItemsControl].((vm:VaultViewModel)DataContext).RemoveEditorForwardCommand}"
|
|
CommandParameter="{Binding}" ToolTip.Tip="Removes this forward" />
|
|
</Grid>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
<ComboBox SelectedIndex="{Binding EditorNewForwardKind}" HorizontalAlignment="Stretch"
|
|
Height="36">
|
|
<ComboBoxItem Content="Local -L" />
|
|
<ComboBoxItem Content="Remote -R" />
|
|
<ComboBoxItem Content="Dynamic -D (SOCKS)" />
|
|
</ComboBox>
|
|
|
|
<!-- Which end each port is on, for the kind chosen: the thing -L and -R get backwards. -->
|
|
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
|
Text="{Binding EditorNewForwardKindHint}" />
|
|
|
|
<Grid ColumnDefinitions="104,6,*,6,Auto">
|
|
<TextBox Grid.Column="0" FontFamily="{StaticResource MonoFont}"
|
|
Text="{Binding EditorNewForwardBindPort}"
|
|
PlaceholderText="{Binding EditorNewForwardBindHint}" Height="36"
|
|
ToolTip.Tip="{Binding EditorNewForwardBindHint}">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding AddEditorForwardCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<TextBox Grid.Column="2" FontFamily="{StaticResource MonoFont}"
|
|
Text="{Binding EditorNewForwardTarget}"
|
|
IsVisible="{Binding EditorNewForwardHasTarget}"
|
|
PlaceholderText="host:port" Height="36">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding AddEditorForwardCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<Button Grid.Column="4" Classes="ghost" Content="+" Width="36" Height="36" FontSize="15"
|
|
HorizontalContentAlignment="Center"
|
|
Command="{Binding AddEditorForwardCommand}" ToolTip.Tip="Adds this forward" />
|
|
</Grid>
|
|
|
|
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
|
Text="Forwards start with every terminal opened to this host and stop when it closes. Everything listens on localhost only. A change here applies from the next connection." />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</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; the header says which of the two is about to happen, and
|
|
so does the footer's button. 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="12" Spacing="10" IsVisible="{Binding IsEditingGroup}">
|
|
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionhead" Text="Group" Margin="0,0,0,2" />
|
|
|
|
<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" />
|
|
|
|
<!--
|
|
◆ WHICH VAULT THIS GROUP WILL LIVE IN, on the same terms as the host editor's picker
|
|
above: asked while adding, hidden where there is only one vault to write to, and never
|
|
offered for an existing group. Not because the group is stuck — the card's own menu takes
|
|
it to another vault, with everything on the shelf — but because moving it is a
|
|
re-seal of every item involved into new ids, which is nothing a SAVE on this form could
|
|
do, and a picker here would do it as a side effect of correcting a default port.
|
|
|
|
A group in a shared vault is what gives a team an arrangement rather than a heap: the
|
|
people holding that vault's key see the folder, and the hosts inside it inherit its port,
|
|
its username and its key.
|
|
|
|
The parent picker below follows it, for the reason the host's group picker follows the
|
|
host's vault — a parent in another vault would be a level half the readers cannot resolve.
|
|
See VaultViewModel.ShowsGroupEditorVaultChoice.
|
|
-->
|
|
<StackPanel Spacing="4" IsVisible="{Binding ShowsGroupEditorVaultChoice}">
|
|
<ComboBox ItemsSource="{Binding GroupEditorVaultChoices}"
|
|
SelectedItem="{Binding GroupEditorSelectedVault}"
|
|
HorizontalAlignment="Stretch">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="11" TextWrapping="Wrap"
|
|
Text="A group in a shared vault is visible to everybody holding that vault's key, and only hosts in the same vault can be filed under it." />
|
|
</StackPanel>
|
|
|
|
<ComboBox ItemsSource="{Binding GroupEditorParentChoices}"
|
|
SelectedItem="{Binding GroupEditorSelectedParent}"
|
|
HorizontalAlignment="Stretch">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:GroupChoice">
|
|
<TextBlock Text="{Binding Label}" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<Border Classes="section">
|
|
<StackPanel Spacing="6">
|
|
<TextBlock Classes="sectionhead" Text="What its hosts inherit" Margin="0,0,0,2" />
|
|
|
|
<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>
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
|
|
</Panel>
|
|
</ScrollViewer>
|
|
|
|
<!-- ============ THE FOOTER ============ -->
|
|
<!--
|
|
One row, and exactly one of its five contents is showing — the same by-construction exclusivity the
|
|
panels above have, from the same flags. It is what each panel is for: connecting, saving a host,
|
|
saving a group, answering the question about deleting one, or choosing where to move one.
|
|
|
|
◆ THE QUESTION TAKES CONNECT'S PLACE rather than stacking under it, as it always did with the row of
|
|
buttons this footer replaced, so that DELETE cannot be pressed again while its own question is on
|
|
screen. See VaultViewModel.ShowsHostPaneActions. The move panel takes it for the same reason.
|
|
-->
|
|
<Border Grid.Row="2" Padding="12" Background="{StaticResource Panel}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,1,0,0">
|
|
<Panel>
|
|
|
|
<Button Classes="accent" Content="Connect" HorizontalAlignment="Stretch"
|
|
HorizontalContentAlignment="Center" Padding="10,7" FontSize="12.5"
|
|
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}"
|
|
IsVisible="{Binding ShowsHostPaneActions}" />
|
|
|
|
<Border Padding="10" Background="{StaticResource DangerWash}" CornerRadius="6"
|
|
IsVisible="{Binding IsConfirmingHostDeletion}">
|
|
<views:ConfirmDeleteCard />
|
|
</Border>
|
|
|
|
<!--
|
|
◆ MOVING THE HOST TO ANOTHER VAULT. A picker and two buttons, not a question with a yes: what
|
|
is being asked is which vault, and a move is undone by moving it back rather than by being
|
|
careful — so this is not drawn in the danger colours the deletion question uses.
|
|
|
|
The sentence under it is the part worth keeping. A group and a tag are items of the vault the
|
|
host is leaving, so neither can come; saying so here rather than only in the status line
|
|
afterwards is the difference between a warning and a surprise. What it deliberately does not
|
|
promise is anything about the key or password the host authenticates with — those resolve
|
|
across vaults, they are kept, and the status line names one that is left outside.
|
|
-->
|
|
<StackPanel Spacing="8" IsVisible="{Binding IsMovingHost}">
|
|
<TextBlock Classes="label" Text="MOVE TO VAULT" />
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveVaultChoices}"
|
|
SelectedItem="{Binding SelectedMoveVault}">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="The host is re-encrypted with the other vault's key, so everybody who holds that key can read it and nobody else can. Its group and tags stay behind — both belong to the vault it is leaving." />
|
|
|
|
<!--
|
|
◆ THE KEY, WHICH IS THE HALF THE SENTENCE ABOVE CANNOT PROMISE. A binding resolves across
|
|
vaults, so the host goes on working here either way — but the colleagues it has just joined
|
|
hold one vault's key, and a host whose key stayed behind is one they cannot connect with.
|
|
|
|
Unticked, and it has to be: moving a key into a team's vault hands it to everybody who holds
|
|
that key. The note under it is the count, because a key twenty machines authenticate with is a
|
|
different decision from one nothing else uses, and neither is visible from here otherwise.
|
|
-->
|
|
<CheckBox IsChecked="{Binding BringsTheBindingAlong}"
|
|
IsVisible="{Binding HasABindingToBring}"
|
|
ToolTip.Tip="Moves the key or password itself into the same vault, and re-aims every host and group that used it at where it has gone.">
|
|
<TextBlock Text="{Binding BindingToBringQuestion}" Classes="hint" FontSize="12"
|
|
TextWrapping="Wrap" />
|
|
</CheckBox>
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
IsVisible="{Binding HasABindingToBring}"
|
|
Text="{Binding BindingToBringNote}" />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="MOVE" Command="{Binding ConfirmMoveHostCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelMoveHostCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Save host flex-grows and Cancel is a fixed 92, the mock's own footer proportions — a Grid rather
|
|
than the StackPanel every other footer row uses, because a StackPanel measures each child at its
|
|
own size and has nothing that means "the rest of the row".
|
|
-->
|
|
<Grid ColumnDefinitions="*,8,92" IsVisible="{Binding IsEditing}">
|
|
<Button Grid.Column="0" Classes="accent" Content="Save host" Height="44"
|
|
HorizontalContentAlignment="Center" Command="{Binding SaveHostCommand}" />
|
|
<Button Grid.Column="2" Classes="ghost" Content="Cancel" Height="44"
|
|
HorizontalContentAlignment="Center" Command="{Binding CancelEditCommand}" />
|
|
</Grid>
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding IsEditingGroup}">
|
|
<Button Classes="accent" Content="{Binding GroupSaveLabel}"
|
|
Command="{Binding SaveGroupCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelGroupEditCommand}" />
|
|
</StackPanel>
|
|
|
|
</Panel>
|
|
</Border>
|
|
|
|
</Grid>
|
|
</Border>
|
|
|
|
</UserControl>
|