Public Access
641 lines
41 KiB
XML
641 lines
41 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"
|
|
xmlns:ssh="using:DodoSSH.Client.Ssh"
|
|
x:Class="DodoSSH.Client.App.Views.KeychainScreen"
|
|
x:DataType="vm:VaultViewModel">
|
|
|
|
<!--
|
|
The keychain: the SSH keys and the stored passwords. Things a person creates and edits.
|
|
|
|
Three columns, as the design has them — a category rail, one table, and a detail pane. The table has one
|
|
shape for every kind, which is what makes the ALL category possible and is why the row projection
|
|
exists; see VaultItemRowViewModel.
|
|
|
|
HOST KEYS was a fourth category here and is now a screen of its own; see KnownHostsScreen. It never fit:
|
|
the two categories left are things somebody made on purpose, and a pin is a decision recorded at the
|
|
moment of connecting — nobody goes looking for one in a list of credentials. It also has a workflow the
|
|
shared table could not serve, which is comparing an untruncated fingerprint against a published one.
|
|
|
|
Two of the design's five categories are still not here. IDENTITIES and CERTIFICATES have no item type
|
|
behind them, so listing them would be two headings that could never have anything under them. Recorded
|
|
in docs/design-import-gaps.md.
|
|
|
|
The SCOPES rail below the categories is the keychain list. Since M3 it genuinely has more than one entry
|
|
when somebody is in a team — but it is still not a selector, because every table on this screen already
|
|
spans every keychain this session holds a key for and each row names its own. What it carries instead is
|
|
the one keychain question with an answer: where a new item is filed.
|
|
|
|
── v5b — Keychain.dc.html ──────────────────────────────────────────────────────────────────────────────
|
|
A fidelity pass over the shape above, not a new one. What moved:
|
|
|
|
◆ THE HEADER. "Keychain" 33 bold, a count chip, and on the right a "Host keys" ghost button — the
|
|
design's own doorway to the pins screen, wired to the shell's existing ShowScreenCommand via
|
|
$parent[Window] since this screen's own DataContext is the vault rather than the shell — and one
|
|
"+ New key" accent button rather than the five GENERATE / + SSH KEY / + PASSWORD / + TAG / + BUCKET
|
|
buttons the toolbar used to spread across the table's own header. ◆ DECIDED DEVIATION: the design draws
|
|
one button because its mock has one "add" concept; this application has five, and folding five capabilities
|
|
into one visible button without losing any of them means the button opens a menu naming all five, in the
|
|
same order the old toolbar had them, rather than guessing which one the design's single button "really"
|
|
meant. GENERATE keeps its own tooltip; the strip's own essay comment about why it lost the word KEY is now
|
|
moot — the width pressure that produced it left with the buttons.
|
|
|
|
◆ THE RAIL. 200px, Sidebar-bg, KEYCHAIN tracked label, category rows restyled to Button.cat's own idiom
|
|
(unchanged binding, new width). Below the divider: SCOPES stays — this session's current vault, named,
|
|
since that fact is real and the design's mock is a single-vault sample with nothing to show it — and then
|
|
NEW ITEMS FILE TO, the design's own wording for the picker this screen already had as "NEW ITEMS GO TO".
|
|
Adopted rather than kept: nothing in this file's own essay comments ever defended "GO TO" over "FILE TO",
|
|
and the comment above the picker already says "where a new item is filed" — the design's word was this
|
|
screen's own vocabulary already.
|
|
|
|
◆ THE TABLE. A 44px sub-toolbar (the section summary, left; a 240px filter box, right — this table had no
|
|
filter box before, and the design's has one) and tracked column headers. The design's own five are
|
|
NAME/TYPE/VAULT/USED BY/MODIFIED; MODIFIED is dropped — VaultItem carries no timestamp of any kind (id,
|
|
secret, version, three sync flags — see docs/design-import-gaps.md) — and USED BY is real for a key or a
|
|
credential (which hosts authenticate with it, the same fact VaultViewModel.HostsBoundTo already computes
|
|
for the deletion warning) and empty for a tag (its own host count already covers the same ground) or a
|
|
bucket (nothing in this codebase resolves a host's authentication to an object store). Rows: a type glyph,
|
|
the mono name, the type word, and the vault chip — VaultItemRowViewModel.VaultBadge, empty except where
|
|
more than one vault is in play, the same convention every other list in this application follows.
|
|
|
|
◆ THE DETAIL PANE, 300px. PUBLIC KEY draws when the selected key has one stored. FINGERPRINT is not
|
|
drawn at all — this codebase has never computed one; see docs/design-import-gaps.md's own recorded gap,
|
|
"no algorithm field, no fingerprint, and computing either means parsing armour the type stores verbatim."
|
|
USED BY draws real host rows — the same HostsBoundTo scan, with each host's own two-state dot — only for
|
|
a key or a credential, and only while at least one host actually authenticates with the selected item; the
|
|
"in use · N hosts" chip beside the vault chip is gated the same way. The action buttons keep their
|
|
existing commands and their existing honesty: COPY PUBLIC KEY only for a key, MOVE only where
|
|
CanMoveSelectedItem says there is somewhere to move to, DELETE always. "Choose something on the left…"
|
|
stays as the empty state, restyled.
|
|
-->
|
|
|
|
<Grid RowDefinitions="Auto,*" Margin="26">
|
|
|
|
<!-- ============ THE HEADER ============ -->
|
|
<Grid Grid.Row="0" Margin="0,0,0,20" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
|
|
|
|
<TextBlock Grid.Column="0" Text="Keychain" FontSize="33" FontWeight="Bold" LetterSpacing="-0.5"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
|
|
|
<Border Grid.Column="1" Margin="12,0,0,0" MinWidth="34" Height="30" CornerRadius="9" Padding="8,0"
|
|
Background="{StaticResource Chip}" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" Text="{Binding TotalItemCount}" FontSize="12.5"
|
|
Foreground="{StaticResource TextDim}" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" />
|
|
</Border>
|
|
|
|
<!--
|
|
◆ "Host keys", to the pins screen. $parent[Window] is what reaches the shell from here: this
|
|
control's own DataContext is the vault, not MainWindowViewModel, and Window is the nearest ancestor
|
|
whose DataContext is the shell — see MainWindow.axaml, which sets this screen's DataContext to
|
|
{Binding Vault} rather than putting IsKeychainScreen and the vault on one element.
|
|
-->
|
|
<Button Grid.Column="3" Classes="ghost" Height="40" Margin="0,0,10,0"
|
|
Command="{Binding $parent[Window].((vm:MainWindowViewModel)DataContext).ShowScreenCommand}"
|
|
CommandParameter="{x:Static vm:ShellScreen.KnownHosts}"
|
|
ToolTip.Tip="Host keys you have approved, and how to withdraw one">
|
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15" Text=""
|
|
Foreground="{StaticResource TextFaint}" />
|
|
<TextBlock Text="Host keys" FontSize="13.5" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextDim}" />
|
|
</StackPanel>
|
|
</Button>
|
|
|
|
<!--
|
|
◆ "+ New key": one button standing in for the five this screen can still do. See the file-level
|
|
remark above for why a menu rather than a guess at which one the design meant.
|
|
-->
|
|
<Button Grid.Column="4" Classes="accent" Height="40" FontSize="13.5" Content="+ New key">
|
|
<Button.Flyout>
|
|
<MenuFlyout Placement="BottomEdgeAlignedRight">
|
|
<MenuItem Header="Generate a key…" Command="{Binding NewGeneratedKeyCommand}"
|
|
ToolTip.Tip="Makes a new key pair here, so the private half never becomes a file on this disk." />
|
|
<MenuItem Header="Add SSH key…" Command="{Binding NewKeyCommand}"
|
|
ToolTip.Tip="Pastes in a key you already have." />
|
|
<MenuItem Header="Add password…" Command="{Binding NewCredentialCommand}" />
|
|
<MenuItem Header="Add tag…" Command="{Binding NewTagCommand}"
|
|
ToolTip.Tip="A name to put on hosts. Usually made from a host's editor instead; this is for setting a scheme up before there is anything to put it on." />
|
|
<MenuItem Header="Add bucket…" Command="{Binding NewObjectStoreCommand}"
|
|
ToolTip.Tip="An S3-compatible bucket, to browse beside a host on the Files screen." />
|
|
</MenuFlyout>
|
|
</Button.Flyout>
|
|
</Button>
|
|
|
|
</Grid>
|
|
|
|
<!-- ============ THE BORDERED BODY ============ -->
|
|
<Border Grid.Row="1" BorderBrush="{StaticResource Border}" BorderThickness="1" CornerRadius="12"
|
|
ClipToBounds="True">
|
|
<Grid ColumnDefinitions="200,*,300">
|
|
|
|
<!-- ============ Categories and scopes ============ -->
|
|
<Border Grid.Column="0" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,1,0">
|
|
<ScrollViewer>
|
|
<StackPanel Margin="10,16">
|
|
|
|
<TextBlock Classes="label" Text="KEYCHAIN" Margin="12,0,12,10" FontSize="10" />
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.All}"
|
|
Classes.active="{Binding ShowsAll}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="ALL" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding TotalItemCount}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Keys}"
|
|
Classes.active="{Binding ShowsKeys}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="SSH KEYS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Keys.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Credentials}"
|
|
Classes.active="{Binding ShowsCredentials}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="PASSWORDS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Credentials.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<!--
|
|
Buckets. A category here rather than a screen of its own, unlike the approved host keys: a
|
|
bucket is something somebody creates, edits and keeps a secret for, which is what the other
|
|
two categories are. A pin is a decision recorded at connect time and is not.
|
|
-->
|
|
<!--
|
|
Tags. The odd category: it is the only one holding nothing secret — a tag is a name. It is
|
|
here because the reason a tag is an item at all is that renaming it should be one write
|
|
instead of twenty, and a rename needs somewhere to happen; so does deleting, or the host
|
|
editor's picker fills with names nobody uses and never empties.
|
|
-->
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Tags}"
|
|
Classes.active="{Binding ShowsTags}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="TAGS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Tags.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Button Classes="flat cat" Command="{Binding ShowSectionCommand}"
|
|
CommandParameter="{x:Static vm:VaultSection.Buckets}"
|
|
Classes.active="{Binding ShowsBuckets}">
|
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
|
<Border Grid.Column="0" Classes="rowmark catmark" />
|
|
<TextBlock Grid.Column="1" Text="BUCKETS" Margin="12,0,0,0" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding ObjectStores.Count}"
|
|
Foreground="{StaticResource TextFaint}" />
|
|
</Grid>
|
|
</Button>
|
|
|
|
<Border Height="1" Background="{StaticResource BorderSubtle}" Margin="12,12" />
|
|
|
|
<TextBlock Classes="label" Text="SCOPES" Margin="12,0,12,8" FontSize="10" />
|
|
|
|
<!--
|
|
Still not a selector. Every list on this screen now spans every vault this session holds a
|
|
key for, and each row names its own vault — so there is nothing to switch to. What the
|
|
picker below chooses is where a *new* item is filed, which is a different question and the
|
|
only one that has an answer worth asking for.
|
|
-->
|
|
<StackPanel Orientation="Horizontal" Margin="12,2" Spacing="7">
|
|
<Ellipse Width="6" Height="6" Fill="{StaticResource Accent}" VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" Text="{Binding HostsHeading}" FontSize="11"
|
|
Foreground="{StaticResource Text}" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Hidden at one vault, which is where most people stay. A control offering a single option is
|
|
a question with no answer. "NEW ITEMS FILE TO" — the design's own wording, adopted: this
|
|
screen's own comments already call the act "filing" (see above), so the design's label was
|
|
this application's own vocabulary already.
|
|
-->
|
|
<StackPanel Margin="12,14,12,0" Spacing="4" IsVisible="{Binding HasVaultChoice}">
|
|
<TextBlock Classes="label" Text="NEW ITEMS FILE TO" FontSize="10" />
|
|
<ComboBox ItemsSource="{Binding TargetVaults}"
|
|
SelectedItem="{Binding SelectedTargetVault}"
|
|
HorizontalAlignment="Stretch">
|
|
<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="An item filed into a shared vault is readable by everyone holding that vault's key. It defaults to your own and never moves on its own. A host is asked separately, in its own editor." />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Items that would not decrypt. Shown here rather than only in the status line because this is
|
|
the screen the number is about, and because a non-zero count after a rekey is the signal that
|
|
new grants are needed rather than a transient.
|
|
-->
|
|
<Border Classes="chip warn" Margin="12,14,12,0" HorizontalAlignment="Left"
|
|
IsVisible="{Binding HasUnreadableItems}">
|
|
<TextBlock Text="{Binding UnreadableSummary}" />
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
<!-- ============ The table ============ -->
|
|
<Grid Grid.Column="1" RowDefinitions="Auto,Auto,*" Background="{StaticResource Pane}">
|
|
|
|
<Border Grid.Row="0" Padding="16,0" Height="44"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
|
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
|
|
<TextBlock Grid.Column="0" Classes="mono" Text="{Binding SectionSummary}" FontSize="10.5"
|
|
Foreground="{StaticResource TextFaint}" Margin="0,0,10,0" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" />
|
|
<TextBox Grid.Column="1" x:Name="ItemFilterBox" Text="{Binding ItemFilter}" Width="240"
|
|
Height="30" CornerRadius="9" PlaceholderText="filter items"
|
|
VerticalAlignment="Center" />
|
|
</Grid>
|
|
</Border>
|
|
|
|
<!--
|
|
The design's columns are NAME / TYPE / VAULT / USED BY / MODIFIED. MODIFIED is not here — no
|
|
item this application stores carries a timestamp; see the file-level remark above and
|
|
docs/design-import-gaps.md.
|
|
-->
|
|
<Grid Grid.Row="1" ColumnDefinitions="2,2.2*,1*,1*,1.4*" Margin="0,10,16,6"
|
|
IsVisible="{Binding HasVaultItems}">
|
|
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" LetterSpacing="1"
|
|
Margin="12,0,8,0" />
|
|
<TextBlock Grid.Column="2" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="3" Classes="label" Text="VAULT" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="4" Classes="label" Text="USED BY" FontSize="9.5" LetterSpacing="1" />
|
|
</Grid>
|
|
|
|
<ListBox Grid.Row="2" x:Name="ItemList" Classes="filerows" Focusable="True"
|
|
ItemsSource="{Binding VaultItems}"
|
|
SelectedItem="{Binding SelectedVaultItem}"
|
|
Margin="8,0,8,10">
|
|
<ListBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultItemRowViewModel">
|
|
<Grid ColumnDefinitions="2,2.2*,1*,1*,1.4*" Height="40" Margin="6,0,10,0">
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" Margin="10,0,8,0"
|
|
VerticalAlignment="Center">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="15"
|
|
Text="{Binding IconGlyph}" Foreground="{StaticResource AccentText}"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" Text="{Binding Name}" FontSize="12.5" FontWeight="Medium"
|
|
Foreground="{StaticResource Text}" TextTrimming="CharacterEllipsis" />
|
|
</StackPanel>
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Type}" FontSize="10.5"
|
|
Foreground="{StaticResource TextGhost}" VerticalAlignment="Center" />
|
|
<Border Grid.Column="3" Classes="chip" HorizontalAlignment="Left"
|
|
VerticalAlignment="Center" IsVisible="{Binding HasVaultBadge}">
|
|
<TextBlock Text="{Binding VaultBadge}" FontSize="10" />
|
|
</Border>
|
|
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding UsedBySummary}" FontSize="10.5"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
|
TextTrimming="CharacterEllipsis" IsVisible="{Binding HasUsedBySummary}" />
|
|
</Grid>
|
|
</DataTemplate>
|
|
</ListBox.ItemTemplate>
|
|
</ListBox>
|
|
|
|
<!--
|
|
The empty state says which category is empty and what to do about it, rather than showing an
|
|
empty grid that reads as a list still loading.
|
|
-->
|
|
<TextBlock Grid.Row="2" Classes="hint" Text="{Binding EmptySectionMessage}" FontSize="12"
|
|
Margin="24" HorizontalAlignment="Center" VerticalAlignment="Center"
|
|
TextAlignment="Center" MaxWidth="320"
|
|
IsVisible="{Binding !HasVaultItems}" />
|
|
|
|
</Grid>
|
|
|
|
<!-- ============ The detail pane, and the editors ============ -->
|
|
<Border Grid.Column="2" Background="{StaticResource Sidebar}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1,0,0,0">
|
|
<ScrollViewer>
|
|
<StackPanel Margin="18,20" Spacing="14">
|
|
|
|
<!-- Nothing selected. -->
|
|
<TextBlock Classes="hint" FontSize="12"
|
|
Text="Choose something on the left to see what is known about it."
|
|
IsVisible="{Binding !HasSelectedVaultItem}" />
|
|
|
|
<StackPanel Spacing="14" IsVisible="{Binding HasSelectedVaultItem}">
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="12">
|
|
<Border Width="36" Height="36" CornerRadius="10" Background="{StaticResource AccentWash}"
|
|
VerticalAlignment="Center">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" FontSize="17"
|
|
Text="{Binding SelectedVaultItem.IconGlyph}"
|
|
Foreground="{StaticResource AccentText}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
|
</Border>
|
|
<StackPanel Spacing="4" VerticalAlignment="Center">
|
|
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Name}" FontSize="13.5"
|
|
FontWeight="Bold" Foreground="{StaticResource Text}" TextWrapping="Wrap" />
|
|
<TextBlock Classes="mono" Text="{Binding SelectedVaultItem.Detail}" FontSize="10.5"
|
|
Foreground="{StaticResource TextGhost}" TextWrapping="Wrap" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Border Classes="chip">
|
|
<TextBlock Text="{Binding SelectedVaultItem.Type}" />
|
|
</Border>
|
|
<!--
|
|
"in use · N hosts" — real only for a key or a credential with at least one host actually
|
|
bound to it, off the same VaultViewModel.HostsBoundTo scan the USED BY list below reads.
|
|
-->
|
|
<Border CornerRadius="5" Background="{StaticResource LiveWash}" Padding="9,0" Height="21"
|
|
VerticalAlignment="Center" IsVisible="{Binding HasSelectedItemInUseSummary}">
|
|
<TextBlock Text="{Binding SelectedItemInUseSummary}" FontSize="11" FontWeight="Medium"
|
|
Foreground="{StaticResource Live}" VerticalAlignment="Center" />
|
|
</Border>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
PUBLIC KEY, when the selected key has one stored — never the private half. FINGERPRINT is
|
|
not drawn: this codebase has never computed one, and computing it here would mean parsing
|
|
armour the type stores verbatim; see docs/design-import-gaps.md's own recorded gap.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding SelectedItemIsKey}">
|
|
<TextBlock Classes="label" Text="PUBLIC KEY" FontSize="10" />
|
|
<Border CornerRadius="10" Background="{StaticResource Pane}"
|
|
BorderBrush="{StaticResource BorderMid}" BorderThickness="1" Padding="12,10"
|
|
IsVisible="{Binding SelectedKey.Key.PublicKey, Converter={x:Static StringConverters.IsNotNullOrEmpty}, FallbackValue=False}">
|
|
<SelectableTextBlock Classes="mono" Text="{Binding SelectedKey.Key.PublicKey}"
|
|
FontSize="10.5" LineHeight="17"
|
|
Foreground="{StaticResource TextGhost}" TextWrapping="Wrap" />
|
|
</Border>
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="No public half is stored for this key."
|
|
IsVisible="{Binding !SelectedKey.Key.PublicKey, FallbackValue=False}" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
USED BY: real host rows, off the same scan the "in use" chip above reads, with each host's
|
|
own two-state dot — never a third colour, since nothing here pings anything.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding HasSelectedItemUsedByHosts}">
|
|
<TextBlock Classes="label" Text="USED BY" FontSize="10" />
|
|
<StackPanel Spacing="2">
|
|
<ItemsControl ItemsSource="{Binding SelectedItemUsedByHosts}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:UsedByHostRowViewModel">
|
|
<StackPanel Orientation="Horizontal" Spacing="9" Height="28">
|
|
<Ellipse Classes="dot" Classes.live="{Binding IsConnected}"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
What the design puts here beyond what is above — who added it, when, and a TEST CONNECT
|
|
button — has nothing behind it: items carry no author or timestamp, and nothing can
|
|
exercise a credential without a host to exercise it against.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Keychain items record no author or timestamp yet, so there is nothing more to show here." />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6" IsVisible="{Binding ShowsItemActions}">
|
|
<Button Classes="ghost" Content="EDIT" Command="{Binding EditSelectedItemCommand}" />
|
|
<!--
|
|
Only where there is somewhere to move to, unlike EDIT beside it, which is the same rule
|
|
the host's MOVE follows on the phone: a button that answers with "this is the only vault
|
|
you can write to" is a button that should not have been drawn. Keys and passwords only —
|
|
a tag and a bucket are read from the active vault alone, so "another vault" is not a
|
|
question they have.
|
|
-->
|
|
<Button Classes="ghost" Content="MOVE" Command="{Binding MoveSelectedItemCommand}"
|
|
IsVisible="{Binding CanMoveSelectedItem}"
|
|
ToolTip.Tip="Re-encrypts this under another vault's key, and re-aims every host and group that used it at where it has gone." />
|
|
<Button Classes="danger" Content="DELETE" Command="{Binding DeleteSelectedItemCommand}" />
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The public half only, and there is no button for the other one. Installing a key means
|
|
pasting this line into a host's authorized_keys; a private key on the clipboard is a
|
|
private key in every application on the machine.
|
|
-->
|
|
<Button Classes="ghost" Content="Copy public key" HorizontalAlignment="Left"
|
|
IsVisible="{Binding SelectedItemIsKey}"
|
|
Command="{Binding CopyPublicKeyCommand}"
|
|
ToolTip.Tip="Copies the authorized_keys line for this key, which is what a host needs to let it in." />
|
|
|
|
<!--
|
|
◆ MOVING THE ITEM TO ANOTHER VAULT, in the place those buttons were. See HostDrawer.axaml —
|
|
the thing a shared vault could not do until now: a key typed into a personal vault before
|
|
the team existed was stuck there, and the only way across was to paste the private half
|
|
into a second item and delete the first.
|
|
-->
|
|
<StackPanel Spacing="8" IsVisible="{Binding IsMovingItem}">
|
|
<TextBlock Classes="label" Text="MOVE TO VAULT" FontSize="10" />
|
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding MoveItemVaultChoices}"
|
|
SelectedItem="{Binding SelectedMoveItemVault}">
|
|
<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="It is re-encrypted with the other vault's key, so everybody who holds that key can read it and nobody in the vault it leaves can." />
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
IsVisible="{Binding HasMovingItemUsage}"
|
|
Text="{Binding MovingItemUsage}" />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="MOVE" Command="{Binding ConfirmMoveItemCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelMoveItemCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The question DELETE asks, in the place the buttons above were. Here rather than over the
|
|
screen, because this pane is where the item being deleted is described.
|
|
-->
|
|
<Border Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
|
BorderThickness="1" CornerRadius="10" Padding="12"
|
|
IsVisible="{Binding IsConfirmingDeletion}">
|
|
<views:ConfirmDeleteCard />
|
|
</Border>
|
|
|
|
</StackPanel>
|
|
|
|
<!--
|
|
Making a key, as opposed to pasting one in. A step of its own and a short one: an algorithm,
|
|
a comment, and a button.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsGeneratingKey}">
|
|
<TextBlock Classes="label" Text="NEW SSH KEY" FontSize="10" />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="flat choice" Content="ED25519"
|
|
Classes.active="{Binding GeneratesEd25519}"
|
|
Command="{Binding ChooseKeyAlgorithmCommand}"
|
|
CommandParameter="{x:Static ssh:SshKeyAlgorithm.Ed25519}"
|
|
ToolTip.Tip="What every current OpenSSH prefers. Small, fast, and generated instantly." />
|
|
<Button Classes="flat choice" Content="RSA 4096"
|
|
Classes.active="{Binding GeneratesRsa}"
|
|
Command="{Binding ChooseKeyAlgorithmCommand}"
|
|
CommandParameter="{x:Static ssh:SshKeyAlgorithm.Rsa4096}"
|
|
ToolTip.Tip="For servers too old to accept Ed25519. Larger, and a few seconds to generate." />
|
|
</StackPanel>
|
|
|
|
<TextBox Text="{Binding GenerateComment}" PlaceholderText="name — also the key's comment" />
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap"
|
|
Text="This is what the key is called here and what is written into it, so the line on a host says where it came from." />
|
|
|
|
<TextBlock Classes="hint" FontSize="10.5" TextWrapping="Wrap" Margin="0,4,0,0"
|
|
Text="The key file itself has no passphrase. Your keychain passphrase is what protects it, and it never reaches the server in a form it can read." />
|
|
|
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,8,0,0">
|
|
<Button Classes="accent" Content="GENERATE" Command="{Binding GenerateKeyCommand}"
|
|
IsEnabled="{Binding !IsBusy}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelGenerateKeyCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- The key editor. -->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingKey}">
|
|
<TextBlock Classes="label" Text="SSH KEY" FontSize="10" />
|
|
<TextBox Text="{Binding KeyEditorLabel}" PlaceholderText="name" />
|
|
<!--
|
|
Not a password box. The armour has to be visible to be pasted and checked — a masked
|
|
multi-line box makes "did the whole key arrive?" unanswerable — and the mistake this
|
|
actually prevents is pasting the .pub file, which SshKeySecret.TryValidate rejects by name.
|
|
-->
|
|
<TextBox Text="{Binding KeyEditorPrivateKey}"
|
|
PlaceholderText="-----BEGIN OPENSSH PRIVATE KEY-----"
|
|
AcceptsReturn="True" Height="96" TextWrapping="NoWrap"
|
|
FontFamily="{StaticResource MonoFont}" FontSize="11.5" />
|
|
<TextBox Text="{Binding KeyEditorPassphrase}"
|
|
PlaceholderText="passphrase, if the key has one" PasswordChar="•" />
|
|
<TextBox Text="{Binding KeyEditorPublicKey}" PlaceholderText="public half (optional)" />
|
|
<TextBox Text="{Binding KeyEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="The key and its passphrase are encrypted here and never reach the server in a form it can read. Storing both together is the point of a keychain: on a disk the passphrase protects the key, and in here your keychain passphrase protects both." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveKeyCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelKeyEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- The password editor. -->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingCredential}">
|
|
<TextBlock Classes="label" Text="PASSWORD" FontSize="10" />
|
|
<TextBox Text="{Binding CredentialEditorLabel}" PlaceholderText="name" />
|
|
<!--
|
|
Optional, and the reason a credential is worth being its own item rather than two more
|
|
fields on a host: one account on twenty machines is described once and rotated once. Left
|
|
blank, each host supplies its own username and only the password is shared.
|
|
-->
|
|
<TextBox Text="{Binding CredentialEditorUsername}"
|
|
PlaceholderText="username (blank: use each host's own)" />
|
|
<!--
|
|
Masked, unlike the private key box, and the difference is not inconsistency. A key's armour
|
|
has to be visible to be checked for truncation after a paste; a password is short, usually
|
|
typed, and shoulder-surfing is the likelier problem.
|
|
-->
|
|
<TextBox Text="{Binding CredentialEditorPassword}" PlaceholderText="password" PasswordChar="•" />
|
|
<TextBox Text="{Binding CredentialEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Encrypted here and never sent to the server in a form it can read. Bind a host to this in the host's own editor, under how it authenticates." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveCredentialCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelCredentialEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!--
|
|
The tag editor, and the whole of it is one box. What it does not have is the point: renaming
|
|
a tag touches no host, because every host wearing it names its id. That is the entire reason
|
|
a tag is an item rather than a string repeated inside twenty payloads.
|
|
-->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingTag}">
|
|
<TextBlock Classes="label" Text="TAG" FontSize="10" />
|
|
<TextBox Text="{Binding TagEditorLabel}" PlaceholderText="name">
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding SaveTagCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Renaming a tag changes it everywhere at once. No host is rewritten — each one names this tag rather than repeating its name." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveTagCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelTagEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<!-- The bucket editor. -->
|
|
<StackPanel Spacing="6" IsVisible="{Binding IsEditingObjectStore}">
|
|
<TextBlock Classes="label" Text="BUCKET" FontSize="10" />
|
|
<TextBox Text="{Binding BucketEditorLabel}" PlaceholderText="name" />
|
|
<TextBox Text="{Binding BucketEditorBucket}" PlaceholderText="bucket" />
|
|
<TextBox Text="{Binding BucketEditorAccessKeyId}" PlaceholderText="access key id" />
|
|
<!--
|
|
Masked, like a password and for the same reason: a secret access key is one. The access
|
|
key id beside it is an identifier and is shown, which is also why the two are separate
|
|
boxes.
|
|
-->
|
|
<TextBox Text="{Binding BucketEditorSecretAccessKey}" PlaceholderText="secret access key"
|
|
PasswordChar="•" />
|
|
<TextBox Text="{Binding BucketEditorRegion}" PlaceholderText="region (e.g. eu-west-1)" />
|
|
<!--
|
|
Blank means Amazon, and then the region resolves the host. Anything else is a full URL,
|
|
which is what makes this work against a self-hosted service.
|
|
-->
|
|
<TextBox Text="{Binding BucketEditorEndpoint}"
|
|
PlaceholderText="endpoint (blank: Amazon S3)" />
|
|
<CheckBox IsChecked="{Binding BucketEditorUsePathStyle}"
|
|
Content="Address the bucket as a path" />
|
|
<!--
|
|
Said where the decision is made. Getting this wrong produces a DNS failure whose message
|
|
mentions neither buckets nor this setting, which is the worst kind of thing to leave to a
|
|
guess.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Off for Amazon S3. On for most self-hosted services — MinIO and Ceph have no wildcard DNS, so the bucket cannot be a subdomain." />
|
|
<TextBox Text="{Binding BucketEditorNotes}" PlaceholderText="notes" AcceptsReturn="True"
|
|
Height="44" TextWrapping="Wrap" />
|
|
<TextBlock Classes="hint" FontSize="10.5"
|
|
Text="Encrypted here, keys and endpoint alike, and never sent to the server in a form it can read. Pick this bucket on the Files screen to browse it." />
|
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
|
<Button Classes="accent" Content="SAVE" Command="{Binding SaveObjectStoreCommand}" />
|
|
<Button Classes="ghost" Content="CANCEL" Command="{Binding CancelObjectStoreEditCommand}" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
</StackPanel>
|
|
</ScrollViewer>
|
|
</Border>
|
|
|
|
</Grid>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|