Public Access
Merge branch 'claude/sftp-s3-connection-ui-b0730f'
This commit is contained in:
@@ -193,9 +193,10 @@ launch, because no device key is registered.
|
|||||||
|
|
||||||
### Moving files
|
### Moving files
|
||||||
|
|
||||||
**FILES** in the nav rail is a two-pane browser: this machine on the left, the host on the right, and a
|
**SFTP** and **S3** in the tab strip are a two-pane browser: this machine on the left, the remote on the
|
||||||
queue underneath. Choose a host, press **CONNECT**, then select a file in either pane and press the arrow
|
right, and a queue underneath. The right-hand pane opens on an invitation rather than a listing — press
|
||||||
pointing the way you want it to go.
|
**SELECT HOST**, choose one, press **CONNECT** — and once something is open, select a file in either pane
|
||||||
|
and press the arrow pointing the way you want it to go.
|
||||||
|
|
||||||
Two things about it are worth expecting rather than discovering.
|
Two things about it are worth expecting rather than discovering.
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,21 @@
|
|||||||
panes are symmetrical apart from one column: PERMS is remote-only, because a POSIX mode is not a fact
|
panes are symmetrical apart from one column: PERMS is remote-only, because a POSIX mode is not a fact
|
||||||
about a file on the machine this client is developed on.
|
about a file on the machine this client is developed on.
|
||||||
|
|
||||||
|
── v3 ────────────────────────────────────────────────────────────────────────────────────────────────
|
||||||
|
◆ THE CONNECT BAR IS GONE, and everything it held is inside the right-hand pane now.
|
||||||
|
|
||||||
|
It was a 44-pixel strip across the top of the screen holding a heading, a picker, a password box and a
|
||||||
|
button — chrome spanning both panes to configure one of them, and drawn at full width whether or not
|
||||||
|
anything was ever going to be connected. Underneath it sat a pane that was empty for exactly the same
|
||||||
|
reason, saying so in a sentence nobody had to be told twice.
|
||||||
|
|
||||||
|
So the pane says it instead. Disconnected, the right-hand half is an invitation where the listing would
|
||||||
|
be: what this screen is for, what pressing the button will cost, and the button. Connected, the
|
||||||
|
invitation is replaced by the listing and the connection states itself in a strip above it, beside the
|
||||||
|
control that closes it — which is the arrangement the two facts were always about. The heading the bar
|
||||||
|
carried is not reprinted anywhere: the tab in the strip says SFTP or S3, and it says it whether or not
|
||||||
|
this screen is showing.
|
||||||
|
|
||||||
What the design has and this does not: `sftp over bastion-eu`, which needs jump hosts the connection
|
What the design has and this does not: `sftp over bastion-eu`, which needs jump hosts the connection
|
||||||
layer has not got. See docs/design-import-gaps.md.
|
layer has not got. See docs/design-import-gaps.md.
|
||||||
-->
|
-->
|
||||||
@@ -71,105 +86,10 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
|
|
||||||
<Grid RowDefinitions="44,*,Auto">
|
<Grid RowDefinitions="*,Auto">
|
||||||
|
|
||||||
<!-- ============ The host, and the connection ============ -->
|
|
||||||
<Border Grid.Row="0" Padding="14,0" BorderBrush="{StaticResource Border}" BorderThickness="0,0,0,1">
|
|
||||||
<Grid ColumnDefinitions="Auto,Auto,180,Auto,Auto,Auto,*" VerticalAlignment="Center">
|
|
||||||
|
|
||||||
<!--
|
|
||||||
The screen names whichever remote it is offering, because since v2 it is reached as two
|
|
||||||
destinations rather than one: SFTP and S3 are separate tabs in the strip — they were rail entries
|
|
||||||
until v3 — and this control draws both. A single "FILES" heading over a bucket picker would name
|
|
||||||
neither of them.
|
|
||||||
-->
|
|
||||||
<TextBlock Grid.Column="0" Classes="mono" Text="SFTP" FontSize="12" FontWeight="SemiBold"
|
|
||||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
|
||||||
Margin="0,0,12,0" IsVisible="{Binding ShowsHostPicker}" />
|
|
||||||
<TextBlock Grid.Column="0" Classes="mono" Text="S3" FontSize="12" FontWeight="SemiBold"
|
|
||||||
LetterSpacing="1" Foreground="{StaticResource Text}" VerticalAlignment="Center"
|
|
||||||
Margin="0,0,12,0" IsVisible="{Binding ShowsBucketPicker}" />
|
|
||||||
|
|
||||||
<!--
|
|
||||||
The HOST / BUCKET pair that used to sit here is gone: which sort of remote this screen offers is
|
|
||||||
now the tab you chose, and a toggle that silently moved you to the other one would leave the lit
|
|
||||||
tab naming a screen you are no longer on. What sets it is ShowFiles on the shell, which is what
|
|
||||||
the tab calls.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<ComboBox Grid.Column="2" ItemsSource="{Binding Hosts}"
|
|
||||||
SelectedItem="{Binding SelectedHost}"
|
|
||||||
IsEnabled="{Binding !IsConnected}"
|
|
||||||
IsVisible="{Binding ShowsHostPicker}"
|
|
||||||
PlaceholderText="choose a host">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:HostRowViewModel">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
|
||||||
Foreground="{StaticResource Text}" />
|
|
||||||
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10"
|
|
||||||
Foreground="{StaticResource TextFaint}" />
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<ComboBox Grid.Column="2" ItemsSource="{Binding Buckets}"
|
|
||||||
SelectedItem="{Binding SelectedBucket}"
|
|
||||||
IsEnabled="{Binding !IsConnected}"
|
|
||||||
IsVisible="{Binding ShowsBucketPicker}"
|
|
||||||
PlaceholderText="choose a bucket">
|
|
||||||
<ComboBox.ItemTemplate>
|
|
||||||
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
|
||||||
<StackPanel>
|
|
||||||
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
|
||||||
Foreground="{StaticResource Text}" />
|
|
||||||
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="10"
|
|
||||||
Foreground="{StaticResource TextFaint}" />
|
|
||||||
</StackPanel>
|
|
||||||
</DataTemplate>
|
|
||||||
</ComboBox.ItemTemplate>
|
|
||||||
</ComboBox>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Only for a host bound to nothing, exactly as the hosts screen's box is — and it is a different box
|
|
||||||
holding a different value. This connection authenticates separately, so a password typed to open a
|
|
||||||
terminal was never offered here.
|
|
||||||
-->
|
|
||||||
<TextBox Grid.Column="3" Width="150" Margin="6,0,0,0" PasswordChar="•"
|
|
||||||
Text="{Binding TypedPassword}" PlaceholderText="password"
|
|
||||||
IsVisible="{Binding SelectedHostAsksForAPassword}"
|
|
||||||
IsEnabled="{Binding !IsConnected}" />
|
|
||||||
|
|
||||||
<Button Grid.Column="4" Classes="accent" Content="{Binding ConnectLabel}" Margin="6,0,0,0"
|
|
||||||
Command="{Binding ConnectCommand}"
|
|
||||||
IsVisible="{Binding !IsConnected}"
|
|
||||||
IsEnabled="{Binding !IsBusy}" />
|
|
||||||
|
|
||||||
<Button Grid.Column="4" Classes="ghost" Content="DISCONNECT" Margin="6,0,0,0"
|
|
||||||
Command="{Binding DisconnectCommand}"
|
|
||||||
IsVisible="{Binding IsConnected}" />
|
|
||||||
|
|
||||||
<Border Grid.Column="5" Classes="chip accent" Margin="8,0,0,0"
|
|
||||||
IsVisible="{Binding IsConnected}">
|
|
||||||
<TextBlock Text="{Binding ConnectedTo}" />
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Column 6, not 5, which is where it used to be — sharing a cell with the connected chip, so the two
|
|
||||||
drew over each other for as long as anything was open. That was survivable while the status was
|
|
||||||
mostly read before connecting; it is not now, because refusing to switch between SFTP and S3 while
|
|
||||||
a session is live reports itself here, which is precisely when the chip is on screen.
|
|
||||||
-->
|
|
||||||
<TextBlock Grid.Column="6" Classes="hint" Text="{Binding Status}" FontSize="11.5"
|
|
||||||
Margin="12,0,0,0" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"
|
|
||||||
TextWrapping="NoWrap" />
|
|
||||||
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- ============ The two panes ============ -->
|
<!-- ============ The two panes ============ -->
|
||||||
<Grid Grid.Row="1" ColumnDefinitions="*,64,*">
|
<Grid Grid.Row="0" ColumnDefinitions="*,64,*">
|
||||||
|
|
||||||
<!-- ==== This machine ==== -->
|
<!-- ==== This machine ==== -->
|
||||||
<!--
|
<!--
|
||||||
@@ -292,13 +212,26 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- ==== The host ==== -->
|
<!-- ==== The host ==== -->
|
||||||
<Grid Grid.Column="2" x:Name="RemotePane" RowDefinitions="Auto,Auto,Auto,Auto,*"
|
<!--
|
||||||
|
Six rows rather than five: the connection's own strip is the new one, and it is inside this pane
|
||||||
|
rather than above both because it is about this pane and nothing else.
|
||||||
|
-->
|
||||||
|
<Grid Grid.Column="2" x:Name="RemotePane" RowDefinitions="Auto,Auto,Auto,Auto,Auto,*"
|
||||||
DragDrop.AllowDrop="True">
|
DragDrop.AllowDrop="True">
|
||||||
|
|
||||||
<Border Grid.Row="0" Padding="12,7" BorderBrush="{StaticResource BorderSubtle}"
|
<Border Grid.Row="0" Padding="12,7" BorderBrush="{StaticResource BorderSubtle}"
|
||||||
BorderThickness="0,0,0,1">
|
BorderThickness="0,0,0,1">
|
||||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
<TextBlock Grid.Column="0" Classes="label" Text="HOST" VerticalAlignment="Center" />
|
<!--
|
||||||
|
Which kind of remote this pane is for, in the place the local pane names itself. It is the
|
||||||
|
only thing left saying so on the screen itself — the bar that used to print SFTP or S3 across
|
||||||
|
the top is gone — and the pair is worth keeping apart, because HOST and BUCKET is the
|
||||||
|
difference between a directory tree and a flat namespace with inferred folders in it.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="0" Classes="label" Text="HOST" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding ShowsHostPicker}" />
|
||||||
|
<TextBlock Grid.Column="0" Classes="label" Text="BUCKET" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding ShowsBucketPicker}" />
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="6">
|
||||||
<Button Classes="ghost" Content="UP" Command="{Binding RemoteUpCommand}"
|
<Button Classes="ghost" Content="UP" Command="{Binding RemoteUpCommand}"
|
||||||
IsEnabled="{Binding IsConnected}" />
|
IsEnabled="{Binding IsConnected}" />
|
||||||
@@ -310,6 +243,32 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ WHAT IS OPEN, AND WHAT CLOSES IT. Only while something is.
|
||||||
|
|
||||||
|
A row of its own rather than three more cells in the header above, and the reason is arithmetic
|
||||||
|
rather than taste: this pane is 381 pixels wide at the window's minimum, UP, REFRESH and DELETE
|
||||||
|
take most of that, and an account-at-host chip beside a DISCONNECT would have pushed one of them
|
||||||
|
off the edge. The layout suite would have caught it — which is the point of stating the number
|
||||||
|
here, so the next thing added to either row is measured against it rather than tried.
|
||||||
|
|
||||||
|
Two things and a gap, and the gap is the point: the status line was tried here and does not fit.
|
||||||
|
What is left after a 170-pixel address and a DISCONNECT is about eighty pixels, which turns every
|
||||||
|
sentence into its first word and an ellipsis. It is at the foot of the screen instead — see the
|
||||||
|
queue's own strip, which has the width for one.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="1" Padding="12,6" Background="{StaticResource Raised}"
|
||||||
|
BorderBrush="{StaticResource BorderSubtle}" BorderThickness="0,0,0,1"
|
||||||
|
IsVisible="{Binding IsConnected}">
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||||
|
<Border Grid.Column="0" Classes="chip accent" MaxWidth="170">
|
||||||
|
<TextBlock Text="{Binding ConnectedTo}" TextTrimming="CharacterEllipsis" />
|
||||||
|
</Border>
|
||||||
|
<Button Grid.Column="2" Classes="ghost" Content="DISCONNECT"
|
||||||
|
Command="{Binding DisconnectCommand}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
The question DELETE asks. Under the button rather than over the pane, so the row it is about is
|
The question DELETE asks. Under the button rather than over the pane, so the row it is about is
|
||||||
still on screen and still selected while it is being answered — and it names the full path rather
|
still on screen and still selected while it is being answered — and it names the full path rather
|
||||||
@@ -319,7 +278,7 @@
|
|||||||
application deletes is a tombstone against a copy the server still has, and a file on somebody's
|
application deletes is a tombstone against a copy the server still has, and a file on somebody's
|
||||||
host is bytes with nothing behind them.
|
host is bytes with nothing behind them.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="1" Padding="12,10" Background="{StaticResource DangerWash}"
|
<Border Grid.Row="2" Padding="12,10" Background="{StaticResource DangerWash}"
|
||||||
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
BorderBrush="{StaticResource DangerSoft}" BorderThickness="0,0,0,1"
|
||||||
IsVisible="{Binding IsConfirmingRemoteDeletion}">
|
IsVisible="{Binding IsConfirmingRemoteDeletion}">
|
||||||
<StackPanel Spacing="7">
|
<StackPanel Spacing="7">
|
||||||
@@ -338,7 +297,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto" Margin="12,6,12,4">
|
<Grid Grid.Row="3" ColumnDefinitions="*,Auto" Margin="12,6,12,4">
|
||||||
<ItemsControl Grid.Column="0" ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
<ItemsControl Grid.Column="0" ItemsSource="{Binding RemoteTrail}" VerticalAlignment="Center">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
<ItemsPanelTemplate>
|
<ItemsPanelTemplate>
|
||||||
@@ -373,14 +332,14 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Row="3" ColumnDefinitions="2,*,84,110,92" Margin="0,2,12,4">
|
<Grid Grid.Row="4" ColumnDefinitions="2,*,84,110,92" Margin="0,2,12,4">
|
||||||
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="12,0,8,0" />
|
<TextBlock Grid.Column="1" Classes="label" Text="NAME" FontSize="9.5" Margin="12,0,8,0" />
|
||||||
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
|
<TextBlock Grid.Column="2" Classes="label" Text="SIZE" FontSize="9.5" />
|
||||||
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
|
<TextBlock Grid.Column="3" Classes="label" Text="MODIFIED" FontSize="9.5" />
|
||||||
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" />
|
<TextBlock Grid.Column="4" Classes="label" Text="PERMS" FontSize="9.5" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ListBox Grid.Row="4" x:Name="RemoteList" ItemsSource="{Binding RemoteEntries}"
|
<ListBox Grid.Row="5" x:Name="RemoteList" ItemsSource="{Binding RemoteEntries}"
|
||||||
SelectedItem="{Binding SelectedRemoteEntry}">
|
SelectedItem="{Binding SelectedRemoteEntry}">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
<DataTemplate x:DataType="vm:RemoteEntryRowViewModel">
|
||||||
@@ -401,28 +360,155 @@
|
|||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
</ListBox>
|
</ListBox>
|
||||||
|
|
||||||
<StackPanel Grid.Row="4" Spacing="10" Margin="24" MaxWidth="300"
|
<!--
|
||||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
A directory with nothing in it. It no longer has to say anything about not being connected: that
|
||||||
IsVisible="{Binding !HasRemoteEntries}">
|
state is the invitation below, which covers this pane whole.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Row="5" Classes="hint" FontSize="12" Margin="24" MaxWidth="300"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" TextAlignment="Center"
|
||||||
|
Text="Nothing in this directory." IsVisible="{Binding !HasRemoteEntries}" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
============ THE INVITATION, which is what this screen opens on ============
|
||||||
|
|
||||||
|
Everything the connect bar used to hold, in the half of the screen it was always about. It covers
|
||||||
|
the pane from below the header to the bottom, opaquely: the breadcrumb, the column headings and
|
||||||
|
the empty listing are all still laid out underneath, and all three are furniture for a listing
|
||||||
|
that does not exist yet.
|
||||||
|
|
||||||
|
Two steps, not one. The first is an invitation with a button — a name for what this screen does, a
|
||||||
|
sentence about what it costs, and one thing to press — and the picker is the second. A combo box
|
||||||
|
sitting open in the middle of an empty pane would be a form with no question above it; the reason
|
||||||
|
the pane is empty is the question, and that is what the first step says.
|
||||||
|
|
||||||
|
◆ The two steps also keep this inside its budget, which is the pane's height less whatever the
|
||||||
|
queue has taken — 268 pixels with three transfers on it. The sentence and the button go away when
|
||||||
|
the picker arrives, so the tall shape is the form rather than form-plus-prose, and neither shape
|
||||||
|
reaches the floor. Anything added here has to hold that; the layout suite measures both.
|
||||||
|
-->
|
||||||
|
<Border Grid.Row="1" Grid.RowSpan="5" Background="{StaticResource Canvas}"
|
||||||
|
IsVisible="{Binding !IsConnected}">
|
||||||
|
<StackPanel Spacing="10" Margin="20" MaxWidth="320"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The mark the hosts screen puts on a group, at the size an empty state can carry one. This
|
||||||
|
application ships no icon set — see the note on colour at the top of this file — so a glyph in
|
||||||
|
a rounded square is what an icon is here, and ▤ is already the one that means "a place things
|
||||||
|
are kept".
|
||||||
|
-->
|
||||||
|
<Border Width="44" Height="44" CornerRadius="12" HorizontalAlignment="Center"
|
||||||
|
Background="{StaticResource Raised}" BorderBrush="{StaticResource Border}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<TextBlock Text="▤" FontSize="18" Foreground="{StaticResource TextDim}"
|
||||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||||||
|
Text="Connect to a host" IsVisible="{Binding ShowsHostPicker}" />
|
||||||
|
<TextBlock Classes="heading" FontSize="16" TextAlignment="Center"
|
||||||
|
Text="Open a bucket" IsVisible="{Binding ShowsBucketPicker}" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The sentence, and the button under it. Both go away once the picker is up — by then they have
|
||||||
|
been read or they have not — which is why they are one collapsing group rather than two
|
||||||
|
elements each answering the same question.
|
||||||
|
|
||||||
|
The sentence is the one the empty pane used to carry, and it is kept rather than dropped
|
||||||
|
because it is the one thing about this screen that surprises people: browsing files is a
|
||||||
|
second login, and it shows up in the host's auth log as one. The bucket's counterpart says
|
||||||
|
the opposite thing for the opposite reason — there is no session to open at all, so nothing
|
||||||
|
is dialled until a listing is asked for.
|
||||||
|
-->
|
||||||
|
<StackPanel Spacing="10" IsVisible="{Binding !IsChoosingRemote}">
|
||||||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||||||
Text="Connect to a host to browse its files. This opens its own SFTP connection, so the host records a second login — it is not the same channel as a terminal."
|
IsVisible="{Binding ShowsHostPicker}"
|
||||||
IsVisible="{Binding !IsConnected}" />
|
Text="Browsing a host's files opens its own SFTP connection, so the host records a second login — it is not the channel a terminal uses." />
|
||||||
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
<TextBlock Classes="hint" FontSize="12" TextAlignment="Center"
|
||||||
Text="Nothing in this directory."
|
IsVisible="{Binding ShowsBucketPicker}"
|
||||||
IsVisible="{Binding IsConnected}" />
|
Text="A bucket has nothing to connect to: the keys and the endpoint are tested by the first listing, not by opening a session." />
|
||||||
|
<Button Classes="accent" HorizontalAlignment="Center" Content="SELECT HOST"
|
||||||
|
IsVisible="{Binding ShowsHostPicker}"
|
||||||
|
Command="{Binding BeginChoosingRemoteCommand}" />
|
||||||
|
<Button Classes="accent" HorizontalAlignment="Center" Content="SELECT BUCKET"
|
||||||
|
IsVisible="{Binding ShowsBucketPicker}"
|
||||||
|
Command="{Binding BeginChoosingRemoteCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ==== The picker, once it has been asked for ==== -->
|
||||||
|
<StackPanel Spacing="8" IsVisible="{Binding IsChoosingRemote}">
|
||||||
|
|
||||||
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Hosts}"
|
||||||
|
SelectedItem="{Binding SelectedHost}"
|
||||||
|
IsVisible="{Binding ShowsHostPicker}"
|
||||||
|
PlaceholderText="choose a host">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:HostRowViewModel">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
||||||
|
Foreground="{StaticResource Text}" />
|
||||||
|
<TextBlock Classes="mono" Text="{Binding Address}" FontSize="10"
|
||||||
|
Foreground="{StaticResource TextFaint}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<ComboBox HorizontalAlignment="Stretch" ItemsSource="{Binding Buckets}"
|
||||||
|
SelectedItem="{Binding SelectedBucket}"
|
||||||
|
IsVisible="{Binding ShowsBucketPicker}"
|
||||||
|
PlaceholderText="choose a bucket">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:ObjectStoreRowViewModel">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Classes="mono" Text="{Binding Label}" FontSize="12"
|
||||||
|
Foreground="{StaticResource Text}" />
|
||||||
|
<TextBlock Classes="mono" Text="{Binding Description}" FontSize="10"
|
||||||
|
Foreground="{StaticResource TextFaint}" />
|
||||||
|
</StackPanel>
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Only for a host bound to nothing, exactly as the hosts screen's box is — and it is a
|
||||||
|
different box holding a different value. This connection authenticates separately, so a
|
||||||
|
password typed to open a terminal was never offered here.
|
||||||
|
-->
|
||||||
|
<TextBox PasswordChar="•" Text="{Binding TypedPassword}" PlaceholderText="password"
|
||||||
|
IsVisible="{Binding SelectedHostAsksForAPassword}" />
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center">
|
||||||
|
<Button Classes="accent" Content="{Binding ConnectLabel}"
|
||||||
|
Command="{Binding ConnectCommand}" IsEnabled="{Binding !IsBusy}" />
|
||||||
|
<Button Classes="ghost" Content="CANCEL"
|
||||||
|
Command="{Binding CancelChoosingRemoteCommand}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The status line, in the state where the connected strip above is not on screen. Wrapped rather
|
||||||
|
than trimmed, unlike its counterpart up there: what lands here is a refusal with a reason in
|
||||||
|
it — a server's own sentence about why a connection did not open — and an ellipsis through the
|
||||||
|
middle of that is a message that has been shown without being said.
|
||||||
|
-->
|
||||||
|
<TextBlock Classes="hint" FontSize="11.5" TextAlignment="Center" Text="{Binding Status}" />
|
||||||
|
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Two highlights rather than one, because refusing is worth showing. Something dragged over a
|
Two highlights rather than one, because refusing is worth showing. Something dragged over a
|
||||||
disconnected pane has to say so under the pointer — a pane that lights up nowhere reads as a
|
disconnected pane has to say so under the pointer — a pane that lights up nowhere reads as a
|
||||||
window that has stopped answering, and the answer arriving after the drop is the answer arriving
|
window that has stopped answering, and the answer arriving after the drop is the answer arriving
|
||||||
too late. See the local pane for why neither may hit-test.
|
too late. See the local pane for why neither may hit-test.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
|
<Border Grid.Row="0" Grid.RowSpan="6" IsHitTestVisible="False"
|
||||||
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
Background="{StaticResource AccentWash}" BorderBrush="{StaticResource Accent}"
|
||||||
BorderThickness="2" IsVisible="{Binding IsRemoteDropTarget}" />
|
BorderThickness="2" IsVisible="{Binding IsRemoteDropTarget}" />
|
||||||
|
|
||||||
<Border Grid.Row="0" Grid.RowSpan="5" IsHitTestVisible="False"
|
<Border Grid.Row="0" Grid.RowSpan="6" IsHitTestVisible="False"
|
||||||
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
Background="{StaticResource DangerWash}" BorderBrush="{StaticResource DangerSoft}"
|
||||||
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
|
BorderThickness="2" IsVisible="{Binding IsRemoteDropRefused}">
|
||||||
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="12"
|
<TextBlock Classes="hint" Text="Connect to a host first." FontSize="12"
|
||||||
@@ -435,7 +521,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- ============ The queue ============ -->
|
<!-- ============ The queue ============ -->
|
||||||
<Border Grid.Row="2" Background="{StaticResource Sidebar}" BorderBrush="{StaticResource Border}"
|
<Border Grid.Row="1" Background="{StaticResource Sidebar}" BorderBrush="{StaticResource Border}"
|
||||||
BorderThickness="0,1,0,0" MaxHeight="196">
|
BorderThickness="0,1,0,0" MaxHeight="196">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
|
||||||
@@ -445,6 +531,22 @@
|
|||||||
<TextBlock Grid.Column="1" Classes="mono" FontSize="10.5" Margin="10,0,0,0"
|
<TextBlock Grid.Column="1" Classes="mono" FontSize="10.5" Margin="10,0,0,0"
|
||||||
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center"
|
||||||
Text="one at a time · nothing lands at its final name until it is complete" />
|
Text="one at a time · nothing lands at its final name until it is complete" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
◆ THE STATUS LINE, in the state where the remote pane has no room for one.
|
||||||
|
|
||||||
|
It is here rather than beside DISCONNECT because this row spans the window and that one spans
|
||||||
|
half of it: what the screen has to say about a session is a sentence, and a sentence needs the
|
||||||
|
width. Only while something is open — the other half of the time it is inside the invitation
|
||||||
|
in the remote pane, next to the button that provoked it, which is where a refusal has to be.
|
||||||
|
|
||||||
|
Right-aligned in a free column, so it reads as this row's other end rather than as a third
|
||||||
|
clause of the sentence to its left.
|
||||||
|
-->
|
||||||
|
<TextBlock Grid.Column="2" Classes="hint" FontSize="11.5" Margin="16,0,0,0"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"
|
||||||
|
Text="{Binding Status}" IsVisible="{Binding IsConnected}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
@@ -511,7 +613,7 @@
|
|||||||
they arrive here on their own because this is a separate connection — a host trusted for a shell is
|
they arrive here on their own because this is a separate connection — a host trusted for a shell is
|
||||||
trusted for this too, but a host nobody has connected to at all is met here first.
|
trusted for this too, but a host nobody has connected to at all is met here first.
|
||||||
-->
|
-->
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3" Background="{StaticResource Canvas}"
|
<Border Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource Canvas}"
|
||||||
IsVisible="{Binding HasPendingHostKey}">
|
IsVisible="{Binding HasPendingHostKey}">
|
||||||
<Border Classes="card">
|
<Border Classes="card">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
@@ -528,7 +630,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border Grid.Row="0" Grid.RowSpan="3" Background="{StaticResource Canvas}"
|
<Border Grid.Row="0" Grid.RowSpan="2" Background="{StaticResource Canvas}"
|
||||||
IsVisible="{Binding HasHostKeyMismatch}">
|
IsVisible="{Binding HasHostKeyMismatch}">
|
||||||
<Border Classes="card" BorderBrush="{StaticResource DangerSoft}">
|
<Border Classes="card" BorderBrush="{StaticResource DangerSoft}">
|
||||||
<StackPanel Spacing="12">
|
<StackPanel Spacing="12">
|
||||||
|
|||||||
@@ -357,6 +357,30 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
/// <summary>Whether the picker is showing buckets.</summary>
|
/// <summary>Whether the picker is showing buckets.</summary>
|
||||||
internal bool ShowsBucketPicker => Remote is RemoteKind.Bucket;
|
internal bool ShowsBucketPicker => Remote is RemoteKind.Bucket;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the picker is open, as opposed to the invitation that offers it.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The desktop's connect bar is gone — a strip of controls across the top of a screen that is not
|
||||||
|
/// connected to anything, asking a question the empty right-hand pane was already asking silently. What
|
||||||
|
/// replaced it is the pane itself: an invitation where the listing would be, and this flag is the step
|
||||||
|
/// between "connect to a host" and the picker that does it.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// Two steps rather than a picker sitting open, because the pane is the whole answer to "why is this
|
||||||
|
/// half of the screen empty" and a combo box does not say that. The phone does not use this: its screen
|
||||||
|
/// is one pane at a time, so the picker <em>is</em> what it shows before a connection exists.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// It is cleared by everything that changes what the picker would be picking — connecting, disconnecting,
|
||||||
|
/// switching between SFTP and S3, and losing the vault — so an open form is never left over a pane that
|
||||||
|
/// has moved on. See the property-changed hooks at the foot of this file.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool isChoosingRemote;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// What the button that opens the remote says.
|
/// What the button that opens the remote says.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -375,8 +399,15 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string typedPassword = string.Empty;
|
private string typedPassword = string.Empty;
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// It opens saying what the state is rather than what to do about it, and that is a v3 change the
|
||||||
|
/// desktop forced. "Choose a host and connect to browse its files" was this line for two versions, and
|
||||||
|
/// it was the only thing on the screen saying so — the connect bar it sat in had a picker and a button
|
||||||
|
/// and no prose at all. The invitation that replaced the bar says it in a heading, a sentence and a
|
||||||
|
/// button, so a status line repeating it underneath was the same instruction three times.
|
||||||
|
/// </remarks>
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string status = "Choose a host and connect to browse its files.";
|
private string status = "Nothing is open yet.";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool isBusy;
|
private bool isBusy;
|
||||||
@@ -563,6 +594,10 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
SelectedHost = null;
|
SelectedHost = null;
|
||||||
SelectedBucket = null;
|
SelectedBucket = null;
|
||||||
TypedPassword = string.Empty;
|
TypedPassword = string.Empty;
|
||||||
|
|
||||||
|
// The picker with it. Its two lists have just been emptied, so leaving it open would show a form
|
||||||
|
// offering a choice between nothing.
|
||||||
|
IsChoosingRemote = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShowRemoteCommand was here, and it went with the toggle that invoked it. Which kind of remote this
|
// ShowRemoteCommand was here, and it went with the toggle that invoked it. Which kind of remote this
|
||||||
@@ -570,6 +605,25 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
// MainWindowViewModel.ShowFiles, which sets Remote directly because it has a refusal to make first.
|
// MainWindowViewModel.ShowFiles, which sets Remote directly because it has a refusal to make first.
|
||||||
// Keeping the command would have left one nothing could invoke.
|
// Keeping the command would have left one nothing could invoke.
|
||||||
|
|
||||||
|
/// <summary>Opens the picker, from the invitation in the empty remote pane.</summary>
|
||||||
|
[RelayCommand]
|
||||||
|
private void BeginChoosingRemote() => IsChoosingRemote = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Puts the picker away without connecting.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// The typed password goes with it. It is a secret nobody asked to keep, and leaving it in the box would
|
||||||
|
/// mean the next person to open the picker — for a different host, possibly — starts with somebody
|
||||||
|
/// else's password already typed in.
|
||||||
|
/// </remarks>
|
||||||
|
[RelayCommand]
|
||||||
|
private void CancelChoosingRemote()
|
||||||
|
{
|
||||||
|
IsChoosingRemote = false;
|
||||||
|
TypedPassword = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Opens the chosen remote, whichever kind it is.</summary>
|
/// <summary>Opens the chosen remote, whichever kind it is.</summary>
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ConnectAsync(CancellationToken cancellationToken) =>
|
private Task ConnectAsync(CancellationToken cancellationToken) =>
|
||||||
@@ -1390,6 +1444,11 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
OnPropertyChanged(nameof(ShowsBucketPicker));
|
OnPropertyChanged(nameof(ShowsBucketPicker));
|
||||||
OnPropertyChanged(nameof(ConnectLabel));
|
OnPropertyChanged(nameof(ConnectLabel));
|
||||||
OnPropertyChanged(nameof(SelectedHostAsksForAPassword));
|
OnPropertyChanged(nameof(SelectedHostAsksForAPassword));
|
||||||
|
|
||||||
|
// Arriving at the other destination shows its invitation rather than a picker somebody left open on
|
||||||
|
// this one — and the picker it would have left open is the wrong one, since the two kinds have
|
||||||
|
// different lists behind them.
|
||||||
|
IsChoosingRemote = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnIsConnectedChanged(bool value)
|
partial void OnIsConnectedChanged(bool value)
|
||||||
@@ -1397,6 +1456,11 @@ internal sealed partial class TransfersViewModel : ObservableObject, IAsyncDispo
|
|||||||
OnPropertyChanged(nameof(CanDownload));
|
OnPropertyChanged(nameof(CanDownload));
|
||||||
OnPropertyChanged(nameof(CanUpload));
|
OnPropertyChanged(nameof(CanUpload));
|
||||||
OnPropertyChanged(nameof(CanDeleteRemote));
|
OnPropertyChanged(nameof(CanDeleteRemote));
|
||||||
|
|
||||||
|
// Both ways, and the disconnecting half is the one worth stating: closing a session puts the pane
|
||||||
|
// back to its invitation rather than to the form, so what the pane shows after a disconnect is the
|
||||||
|
// same thing it showed before anything was ever connected.
|
||||||
|
IsChoosingRemote = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
|
|||||||
@@ -926,6 +926,83 @@ public sealed class ScreenLayoutTests : IAsyncLifetime
|
|||||||
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty());
|
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// ◆ The tallest the invitation gets, and the shape the connect bar's removal has to survive. Everything
|
||||||
|
/// that used to be a strip across the top of the screen is now stacked in the right-hand pane — a
|
||||||
|
/// picker, a password box and two buttons, under a heading — and that pane is only as tall as whatever
|
||||||
|
/// the queue leaves it.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// So the queue is filled first, which is what takes it to its 196-pixel maximum and leaves the panes
|
||||||
|
/// their least. Without those rows this measures the invitation with nearly twice the room it is
|
||||||
|
/// guaranteed, which is the version of this test that would pass whatever was added to the panel.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// No host is selected on purpose: a picker with nothing chosen shows the password box, because a host
|
||||||
|
/// that names neither a key nor a credential is one that will ask for a password. That is the shape with
|
||||||
|
/// the extra row in it.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheTransfersScreenFitsWithTheHostPickerOpenOverAFullQueue()
|
||||||
|
{
|
||||||
|
Enqueue(TransferDirection.Download, "artefact.tar.gz", 402_653_184, 149_000_000,
|
||||||
|
TransferState.Running, bytesPerSecond: 6_500_000);
|
||||||
|
|
||||||
|
Enqueue(TransferDirection.Upload, "site-backup-2026-07-30.sql.gz", 8_100_000_000, 3_200_000_000,
|
||||||
|
TransferState.Cancelled);
|
||||||
|
|
||||||
|
Enqueue(TransferDirection.Upload, "deploy.sh", 4_096, 0, TransferState.Failed);
|
||||||
|
|
||||||
|
transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||||
|
|
||||||
|
transfers.IsChoosingRemote.ShouldBeTrue();
|
||||||
|
transfers.SelectedHostAsksForAPassword
|
||||||
|
.ShouldBeTrue("the password box is what makes this the tall shape");
|
||||||
|
|
||||||
|
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with the picker open"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// The bucket half of the picker, which differs by one row: no password box, because an object store
|
||||||
|
/// carries its keys in the vault and has nothing to type. Measured because the two are separate markup
|
||||||
|
/// rather than one picker with its items swapped — see the note on <c>RemoteKind</c>.
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheTransfersScreenFitsWithTheBucketPickerOpen()
|
||||||
|
{
|
||||||
|
transfers.Remote = RemoteKind.Bucket;
|
||||||
|
transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||||
|
|
||||||
|
transfers.ShowsBucketPicker.ShouldBeTrue();
|
||||||
|
transfers.SelectedHostAsksForAPassword.ShouldBeFalse("a bucket never asks for one");
|
||||||
|
|
||||||
|
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with the bucket picker open"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The state the screen is in once something is open, and the reason it is worth a test of its own is
|
||||||
|
/// the strip the connect bar left behind: a chip naming the account and the endpoint, the status line,
|
||||||
|
/// and DISCONNECT — inside a pane that is under 400 pixels wide at the window's minimum, above a row
|
||||||
|
/// that already carries UP, REFRESH and DELETE.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The address is a long one deliberately. It is the part of that row with no fixed width, and a chip
|
||||||
|
/// that grew to fit whatever it was given is how the button beside it goes off the edge.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheTransfersScreenFitsWithASessionOpen()
|
||||||
|
{
|
||||||
|
transfers.IsConnected = true;
|
||||||
|
transfers.ConnectedTo = "deployment-service@releases.eu-west.internal.example:2222";
|
||||||
|
transfers.Status = "Connected to releases-eu.";
|
||||||
|
|
||||||
|
await MeasureTransfersAsync(faults => faults.ShouldBeEmpty("with a session open"));
|
||||||
|
}
|
||||||
|
|
||||||
/// <remarks>
|
/// <remarks>
|
||||||
/// The trust card covers the whole screen, and it is the one thing here a user cannot get past without
|
/// The trust card covers the whole screen, and it is the one thing here a user cannot get past without
|
||||||
/// pressing something — so a button of its own that fell outside the window would leave the screen
|
/// pressing something — so a button of its own that fell outside the window would leave the screen
|
||||||
|
|||||||
@@ -5314,6 +5314,67 @@ public sealed class ShellFlowTests : IAsyncLifetime
|
|||||||
shell.Transfers.RemoteEntries.Select(entry => entry.Name).ShouldBe(["notes.txt"]);
|
shell.Transfers.RemoteEntries.Select(entry => entry.Name).ShouldBe(["notes.txt"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <remarks>
|
||||||
|
/// <para>
|
||||||
|
/// The picker that replaced the desktop's connect bar, and the four things that put it away again. It
|
||||||
|
/// is a flag rather than a screen, so what is worth asserting is that it is never left open over a pane
|
||||||
|
/// it no longer belongs to: connecting closes it, disconnecting closes it, moving between SFTP and S3
|
||||||
|
/// closes it, and losing the vault closes it.
|
||||||
|
/// </para>
|
||||||
|
/// <para>
|
||||||
|
/// The last two are the ones that would rot quietly. A picker surviving a hop to the other tab offers
|
||||||
|
/// hosts on a screen showing buckets, and one surviving a lock offers a list that has just been emptied
|
||||||
|
/// because its rows carried decrypted secrets.
|
||||||
|
/// </para>
|
||||||
|
/// </remarks>
|
||||||
|
[Fact]
|
||||||
|
public async Task TheFilePickerIsPutAwayByEverythingThatChangesWhatItWouldBePicking()
|
||||||
|
{
|
||||||
|
var vault = await ReadyToConnectAsync();
|
||||||
|
|
||||||
|
shell.Transfers.Attach(vault, knownHosts);
|
||||||
|
shell.Transfers.IsChoosingRemote.ShouldBeFalse("the pane opens on its invitation");
|
||||||
|
|
||||||
|
// Connecting.
|
||||||
|
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||||
|
shell.Transfers.SelectedHost = shell.Transfers.Hosts[0];
|
||||||
|
|
||||||
|
await shell.Transfers.ConnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
shell.Transfers.IsConnected.ShouldBeTrue(shell.Transfers.Status);
|
||||||
|
shell.Transfers.IsChoosingRemote.ShouldBeFalse("what is open is the listing now");
|
||||||
|
|
||||||
|
// Disconnecting, which puts the pane back to the invitation rather than to the form.
|
||||||
|
await shell.Transfers.DisconnectCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
shell.Transfers.IsConnected.ShouldBeFalse(shell.Transfers.Status);
|
||||||
|
shell.Transfers.IsChoosingRemote.ShouldBeFalse();
|
||||||
|
|
||||||
|
// Moving to the other destination, which has a different list behind it.
|
||||||
|
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||||
|
shell.ShowFilesCommand.Execute(RemoteKind.Bucket);
|
||||||
|
|
||||||
|
shell.Transfers.ShowsBucketPicker.ShouldBeTrue();
|
||||||
|
shell.Transfers.IsChoosingRemote.ShouldBeFalse("that picker was offering hosts");
|
||||||
|
|
||||||
|
// Cancelling, which also takes the typed password with it.
|
||||||
|
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||||
|
shell.Transfers.TypedPassword = "hunter2";
|
||||||
|
|
||||||
|
shell.Transfers.CancelChoosingRemoteCommand.Execute(null);
|
||||||
|
|
||||||
|
shell.Transfers.IsChoosingRemote.ShouldBeFalse();
|
||||||
|
shell.Transfers.TypedPassword.ShouldBeEmpty("a secret nobody asked to keep");
|
||||||
|
|
||||||
|
// And losing the vault, which empties both lists.
|
||||||
|
shell.Transfers.BeginChoosingRemoteCommand.Execute(null);
|
||||||
|
|
||||||
|
await shell.LockCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
shell.Transfers.Hosts.ShouldBeEmpty();
|
||||||
|
shell.Transfers.IsChoosingRemote.ShouldBeFalse("a form offering a choice between nothing");
|
||||||
|
}
|
||||||
|
|
||||||
private async Task UnlockedAsync()
|
private async Task UnlockedAsync()
|
||||||
{
|
{
|
||||||
await EnrolledAndConfirmedAsync();
|
await EnrolledAndConfirmedAsync();
|
||||||
|
|||||||
Reference in New Issue
Block a user