Public Access
372 lines
22 KiB
XML
372 lines
22 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:import="using:DodoSSH.Client.Import"
|
|
x:Class="DodoSSH.Client.App.Views.HetznerImportScreen"
|
|
x:DataType="vm:HetznerImportViewModel">
|
|
|
|
<!--
|
|
Importing the servers in a Hetzner Cloud project.
|
|
|
|
Drawn over the Preferences page inside settings mode, exactly as ImportScreen.axaml is — SettingsNav
|
|
stays lit on Preferences, the titlebar says "Back to preferences", and neither is reachable from the nav
|
|
rail. See MainWindowViewModel.IsHetznerImportOpen.
|
|
|
|
── WHY THIS ONE IS A WIZARD AND THE ssh_config IMPORTER IS NOT ────────────────────────────────────────
|
|
That screen reads a file that is already on this machine, so it can scan the moment it opens and show a
|
|
list. This one cannot: it needs a credential that has to be made in somebody else's console first, and
|
|
then it makes a request over the internet. Those are two separate moments and the screen is honest about
|
|
it — step one asks for the token, step two shows what the project holds, step three says what was stored.
|
|
Both of the first two write nothing.
|
|
|
|
── THE TOKEN IS NOT STORED ────────────────────────────────────────────────────────────────────────────
|
|
It is used for one request and cleared, whether the read succeeded or not. Nothing here writes it to the
|
|
vault, to settings or to a log, and the next import asks again — the same argument ImportScreen's key
|
|
tick makes for not remembering a "yes". The note above the box says which permission is needed (Read),
|
|
that a token is one project rather than one account, and that dedicated servers behind Hetzner's Robot
|
|
are not covered by any of this.
|
|
|
|
── NOTHING IMPORTED HERE CONNECTS ON ITS OWN ──────────────────────────────────────────────────────────
|
|
Hetzner has never held the private half of an SSH key, so no key can come with these hosts. That is said
|
|
above the button rather than after it, on the same reasoning ImportScreen.axaml gives for its own version
|
|
of this sentence: somebody who is not told will conclude the import was broken.
|
|
-->
|
|
|
|
<UserControl.Styles>
|
|
<!-- Matching ImportScreen's table exactly: this is the same table about a different source. -->
|
|
<Style Selector="Border.importrow:pointerover">
|
|
<Setter Property="Background" Value="{StaticResource Track}" />
|
|
</Style>
|
|
<Style Selector="Border.meaningchip.new">
|
|
<Setter Property="Background" Value="{StaticResource LiveWash}" />
|
|
</Style>
|
|
<Style Selector="Border.meaningchip.new > TextBlock">
|
|
<Setter Property="Foreground" Value="{StaticResource Live}" />
|
|
</Style>
|
|
<Style Selector="Border.meaningchip.exists">
|
|
<Setter Property="Background" Value="{StaticResource Chip}" />
|
|
</Style>
|
|
<Style Selector="Border.meaningchip.exists > TextBlock">
|
|
<Setter Property="Foreground" Value="{StaticResource TextFaint}" />
|
|
</Style>
|
|
<Style Selector="Border.meaningchip.warn">
|
|
<Setter Property="Background" Value="{StaticResource WarnWash}" />
|
|
</Style>
|
|
<Style Selector="Border.meaningchip.warn > TextBlock">
|
|
<Setter Property="Foreground" Value="{StaticResource WarnText}" />
|
|
</Style>
|
|
</UserControl.Styles>
|
|
|
|
<Grid MaxWidth="1100" Margin="40" HorizontalAlignment="Stretch" RowDefinitions="Auto,Auto,*">
|
|
|
|
<!-- ============ HEADER ============ -->
|
|
<StackPanel Grid.Row="0" Spacing="10">
|
|
<TextBlock Classes="settingstitle" Text="Import from Hetzner Cloud" />
|
|
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
<TextBlock Classes="label" Text="{Binding StepLabel}" FontSize="9.5" LetterSpacing="1"
|
|
VerticalAlignment="Center" />
|
|
<TextBlock Classes="mono" Text="{Binding StepTitle}" FontSize="12"
|
|
Foreground="{StaticResource TextFaint}" VerticalAlignment="Center" />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
|
|
<TextBlock Grid.Row="1" Classes="hint" Text="{Binding Status}" FontSize="12" Margin="0,12,0,0"
|
|
TextWrapping="Wrap" />
|
|
|
|
<Panel Grid.Row="2" Margin="0,18,0,0">
|
|
|
|
<!-- ============ STEP 1 · THE TOKEN ============ -->
|
|
<Grid IsVisible="{Binding IsTokenStep}" RowDefinitions="Auto,*">
|
|
|
|
<Border Grid.Row="0" CornerRadius="12" Background="{StaticResource Raised}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="24,22">
|
|
<StackPanel Spacing="14">
|
|
<TextBlock Text="Cloud API token" FontSize="13.5" FontWeight="SemiBold"
|
|
Foreground="{StaticResource Text}" />
|
|
<TextBlock Classes="hint" FontSize="11.5" LineHeight="17.5" TextWrapping="Wrap"
|
|
Text="{Binding TokenNote}" />
|
|
<TextBox Text="{Binding Token}" PasswordChar="•" Height="42" FontSize="13"
|
|
FontFamily="{StaticResource MonoFont}"
|
|
PlaceholderText="64 characters, from Security → API tokens">
|
|
<!-- Enter reads the project, so a pasted token does not need the mouse to go anywhere. -->
|
|
<TextBox.KeyBindings>
|
|
<KeyBinding Gesture="Enter" Command="{Binding ScanCommand}" />
|
|
</TextBox.KeyBindings>
|
|
</TextBox>
|
|
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
|
<Button Classes="ghost" Height="42" Content="CANCEL" Command="{Binding CancelCommand}"
|
|
ToolTip.Tip="Back to Preferences. Nothing has been sent." />
|
|
<Button Classes="accent" Height="42" Content="READ THE PROJECT"
|
|
Command="{Binding ScanCommand}" IsEnabled="{Binding !IsBusy}"
|
|
ToolTip.Tip="Lists the project's servers. Nothing is stored, and the token is forgotten as soon as the list arrives." />
|
|
</StackPanel>
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
</Grid>
|
|
|
|
<!-- ============ STEP 2 · WHAT THE PROJECT HOLDS ============ -->
|
|
<Grid IsVisible="{Binding IsChooseStep}" RowDefinitions="Auto,Auto,*,Auto">
|
|
|
|
<!--
|
|
What every imported host gets: which address, which login user, which port. One set of answers for
|
|
the whole import rather than one per row, because an estate is reached the same way throughout —
|
|
a per-server dropdown would be forty answers to one question.
|
|
-->
|
|
<Border Grid.Row="0" CornerRadius="12" Background="{StaticResource Raised}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="20,18">
|
|
<StackPanel Spacing="14">
|
|
|
|
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto" ColumnSpacing="10">
|
|
<TextBlock Grid.Column="0" Classes="label" Text="DIAL" FontSize="9.5" LetterSpacing="1"
|
|
VerticalAlignment="Center" />
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
|
<!--
|
|
Chips rather than radio buttons: this application has no RadioButton style, and Fluent's
|
|
own beside a row of chips reads as belonging to a different program. Button.chiptoggle is
|
|
the tag picker's control, which is already "one of these is on" drawn as a chip.
|
|
-->
|
|
<Button Classes="chiptoggle" Content="PUBLIC IPv4"
|
|
Classes.worn="{Binding UsesPublicIPv4}"
|
|
Command="{Binding UseAddressCommand}"
|
|
CommandParameter="{x:Static import:HetznerAddressKind.PublicIPv4}" />
|
|
<Button Classes="chiptoggle" Content="PUBLIC IPv6"
|
|
Classes.worn="{Binding UsesPublicIPv6}"
|
|
Command="{Binding UseAddressCommand}"
|
|
CommandParameter="{x:Static import:HetznerAddressKind.PublicIPv6}" />
|
|
<Button Classes="chiptoggle" Content="PRIVATE NETWORK"
|
|
Classes.worn="{Binding UsesPrivate}"
|
|
Command="{Binding UseAddressCommand}"
|
|
CommandParameter="{x:Static import:HetznerAddressKind.Private}" />
|
|
</StackPanel>
|
|
<TextBlock Grid.Column="2" Classes="label" Text="USER" FontSize="9.5" LetterSpacing="1"
|
|
VerticalAlignment="Center" Margin="14,0,0,0" />
|
|
<TextBox Grid.Column="3" Text="{Binding Username}" Width="120" Height="34"
|
|
FontFamily="{StaticResource MonoFont}" FontSize="12.5"
|
|
ToolTip.Tip="Every Hetzner Linux image puts the project's SSH keys on root and creates no other account." />
|
|
<TextBox Grid.Column="4" Text="{Binding PortText}" Width="66" Height="34"
|
|
FontFamily="{StaticResource MonoFont}" FontSize="12.5"
|
|
ToolTip.Tip="The SSH port every imported host gets." />
|
|
</Grid>
|
|
|
|
<!--
|
|
◆ WHERE THEY LAND, AND WHAT THEY AUTHENTICATE WITH. The two answers this import used to
|
|
decide on somebody's behalf: the active vault, and nothing.
|
|
|
|
A project belongs to a team more often than to a person, so the vault is asked rather than
|
|
assumed — see HetznerImportViewModel.VaultChoices, and the host editor's own picker for why
|
|
this is its own selection rather than the keychain screen's standing preference. It is hidden
|
|
at one vault, which is a question with one answer.
|
|
|
|
The key is the half Hetzner cannot supply and the keychain usually already has: the public
|
|
half went to Hetzner when the servers were made, the private half never did. Pointing forty
|
|
hosts at it here is one answer; without it, it was forty visits to forty host editors. The
|
|
picker is hidden entirely on a keychain with no keys in it.
|
|
-->
|
|
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="18">
|
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"
|
|
IsVisible="{Binding HasVaultChoice}">
|
|
<TextBlock Classes="label" Text="VAULT" FontSize="9.5" LetterSpacing="1"
|
|
VerticalAlignment="Center" />
|
|
<ComboBox Width="230" Height="34" ItemsSource="{Binding VaultChoices}"
|
|
SelectedItem="{Binding SelectedVault}"
|
|
ToolTip.Tip="Which vault the imported hosts are written into. A shared one is readable by everybody who holds its key.">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:VaultChoiceViewModel">
|
|
<TextBlock Text="{Binding Display}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
</StackPanel>
|
|
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"
|
|
IsVisible="{Binding HasKeyChoice}">
|
|
<TextBlock Classes="label" Text="KEY" FontSize="9.5" LetterSpacing="1"
|
|
VerticalAlignment="Center" />
|
|
<ComboBox Width="260" Height="34" ItemsSource="{Binding KeyChoices}"
|
|
SelectedItem="{Binding SelectedKey}"
|
|
ToolTip.Tip="A key already in your keychain, which every imported host is pointed at. Nothing is copied — the host stores the key's id, exactly as one bound by hand does.">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:HetznerKeyChoice">
|
|
<TextBlock Text="{Binding Label}" FontSize="12" />
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
</StackPanel>
|
|
</Grid>
|
|
|
|
<TextBlock Classes="hint" FontSize="11.5" LineHeight="17" TextWrapping="Wrap"
|
|
Text="{Binding AddressNote}" />
|
|
|
|
<TextBlock Classes="hint" FontSize="11" IsVisible="{Binding !IsPortValid}"
|
|
Foreground="{StaticResource WarnText}"
|
|
Text="That is not a port. Give a number between 1 and 65535." />
|
|
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<!-- What the project could not be made to say, at project level. -->
|
|
<Border Grid.Row="1" Margin="0,14,0,0" Padding="14,10" CornerRadius="10"
|
|
Background="{StaticResource WarnWash}" BorderBrush="{StaticResource WarnSoft}"
|
|
BorderThickness="1" IsVisible="{Binding HasWarnings}">
|
|
<ItemsControl ItemsSource="{Binding Warnings}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="x:String">
|
|
<TextBlock Text="{Binding}" Foreground="{StaticResource WarnText}" FontSize="11.5"
|
|
TextWrapping="Wrap" Margin="0,2" />
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</Border>
|
|
|
|
<!-- ============ THE TABLE ============ -->
|
|
<Border Grid.Row="2" Margin="0,14,0,0" CornerRadius="12" Background="{StaticResource Pane}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" ClipToBounds="True"
|
|
IsVisible="{Binding HasRows}">
|
|
<Grid RowDefinitions="Auto,*">
|
|
|
|
<Border Grid.Row="0" Padding="24,14,24,10" BorderBrush="{StaticResource Border}"
|
|
BorderThickness="0,0,0,1">
|
|
<Grid ColumnDefinitions="24,1*,1.3*,0.6*,1.1*,1.3*">
|
|
<!-- A Button rather than a CheckBox, for the reason ImportScreen.axaml's own header gives. -->
|
|
<Button Grid.Column="0" Classes="flat" Width="18" Height="18" Padding="0"
|
|
VerticalAlignment="Center" Command="{Binding ToggleAllCommand}"
|
|
ToolTip.Tip="Tick or untick every row this address choice can reach">
|
|
<Panel Width="18" Height="18">
|
|
<Border CornerRadius="5" Background="{StaticResource Accent}"
|
|
IsVisible="{Binding AllTicked}">
|
|
<TextBlock FontFamily="{StaticResource IconFont}" Text="" FontSize="13"
|
|
Foreground="White" HorizontalAlignment="Center"
|
|
VerticalAlignment="Center" />
|
|
</Border>
|
|
<Border CornerRadius="5" BorderBrush="{StaticResource BorderMid}" BorderThickness="1.5"
|
|
IsVisible="{Binding !AllTicked}" />
|
|
</Panel>
|
|
</Button>
|
|
<TextBlock Grid.Column="1" Classes="label" Text="SERVER" FontSize="9.5" LetterSpacing="1"
|
|
Margin="14,0,0,0" />
|
|
<TextBlock Grid.Column="2" Classes="label" Text="ADDRESS" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="3" Classes="label" Text="STATE" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="4" Classes="label" Text="TYPE" FontSize="9.5" LetterSpacing="1" />
|
|
<TextBlock Grid.Column="5" Classes="label" Text="WHAT THIS MEANS" FontSize="9.5"
|
|
LetterSpacing="1" />
|
|
</Grid>
|
|
</Border>
|
|
|
|
<!-- Virtualised, for an inventory of hundreds of machines: see the transfer queue's own note. -->
|
|
<ItemsControl Grid.Row="1" ItemsSource="{Binding Rows}">
|
|
<ItemsControl.Template>
|
|
<ControlTemplate>
|
|
<ScrollViewer>
|
|
<ItemsPresenter Name="PART_ItemsPresenter" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="12,8" />
|
|
</ScrollViewer>
|
|
</ControlTemplate>
|
|
</ItemsControl.Template>
|
|
<ItemsControl.ItemsPanel>
|
|
<ItemsPanelTemplate>
|
|
<VirtualizingStackPanel />
|
|
</ItemsPanelTemplate>
|
|
</ItemsControl.ItemsPanel>
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate x:DataType="vm:HetznerRowViewModel">
|
|
<Border Classes="importrow" CornerRadius="8">
|
|
<Grid ColumnDefinitions="24,1*,1.3*,0.6*,1.1*,1.3*" Margin="12" MinHeight="20">
|
|
<!--
|
|
Disabled where the address choice cannot reach this server, which is the private
|
|
case: a server on no private network is not importable and must not be silently
|
|
given a public address instead.
|
|
-->
|
|
<CheckBox Grid.Column="0" IsChecked="{Binding IsSelected}"
|
|
IsEnabled="{Binding CanImport}" VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="1" Classes="mono" Text="{Binding Name}" FontSize="12.5"
|
|
FontWeight="Medium" Foreground="{StaticResource Text}" Margin="14,0,10,0"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="2" Classes="mono" Text="{Binding Address}" FontSize="11.5"
|
|
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="3" Classes="mono" Text="{Binding Status}" FontSize="11.5"
|
|
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
<TextBlock Grid.Column="4" Classes="mono" Text="{Binding Kind}" FontSize="11.5"
|
|
Foreground="{StaticResource TextDim}" Margin="0,0,10,0"
|
|
TextTrimming="CharacterEllipsis" VerticalAlignment="Center" />
|
|
<Border Grid.Column="5" Classes="meaningchip" CornerRadius="5" Padding="8,3"
|
|
HorizontalAlignment="Left" VerticalAlignment="Center"
|
|
Classes.new="{Binding IsMeaningNew}"
|
|
Classes.exists="{Binding IsMeaningExisting}"
|
|
Classes.warn="{Binding IsMeaningWarned}" ToolTip.Tip="{Binding Meaning}">
|
|
<TextBlock Text="{Binding Meaning}" FontSize="10" FontWeight="SemiBold"
|
|
MaxWidth="230" TextTrimming="CharacterEllipsis" />
|
|
</Border>
|
|
</Grid>
|
|
</Border>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
|
|
</Grid>
|
|
</Border>
|
|
|
|
<!-- ============ FOOTER ============ -->
|
|
<StackPanel Grid.Row="3" Margin="0,16,0,0" Spacing="14">
|
|
|
|
<!--
|
|
Said before the button. Whether a credential comes with the host is the difference between a
|
|
bookmark that connects and one that asks for a password, and nothing about a Hetzner project can
|
|
supply one.
|
|
-->
|
|
<TextBlock Classes="hint" FontSize="11.5" TextWrapping="Wrap"
|
|
Text="{Binding AuthenticationNote}" />
|
|
|
|
<Grid ColumnDefinitions="*,Auto">
|
|
<TextBlock Grid.Column="0" Classes="hint" FontSize="12" VerticalAlignment="Center"
|
|
Text="{Binding SelectionSummary}" />
|
|
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10">
|
|
<Button Classes="ghost" Height="44" Content="ANOTHER TOKEN"
|
|
Command="{Binding RestartCommand}"
|
|
ToolTip.Tip="Back to the token box, for another project. This list goes with it — the token it came from was not kept." />
|
|
<Button Classes="ghost" Height="44" Content="CANCEL" Command="{Binding CancelCommand}"
|
|
ToolTip.Tip="Back to Preferences. Nothing is stored." />
|
|
<Button Classes="accent" Height="44" Content="{Binding ImportLabel}"
|
|
Command="{Binding ImportCommand}" IsEnabled="{Binding CanImport}" />
|
|
</StackPanel>
|
|
</Grid>
|
|
|
|
</StackPanel>
|
|
|
|
</Grid>
|
|
|
|
<!-- ============ STEP 3 · WHAT WAS STORED ============ -->
|
|
<Grid IsVisible="{Binding IsDoneStep}" RowDefinitions="Auto,*,Auto">
|
|
|
|
<Border Grid.Row="0" CornerRadius="12" Background="{StaticResource Raised}"
|
|
BorderBrush="{StaticResource Border}" BorderThickness="1" Padding="24,22">
|
|
<StackPanel Spacing="12">
|
|
<TextBlock Text="What happens next" FontSize="13.5" FontWeight="SemiBold"
|
|
Foreground="{StaticResource Text}" />
|
|
<TextBlock Classes="hint" FontSize="11.5" LineHeight="17.5" TextWrapping="Wrap"
|
|
Text="{Binding AuthenticationNote}" />
|
|
<TextBlock Classes="hint" FontSize="11.5" LineHeight="17.5" TextWrapping="Wrap"
|
|
Text="The list this came from is one press away. Going back to it and importing the rest costs nothing — what was just stored is marked as already here. It is the list this scan produced, though, not a fresh one: a server made in the Hetzner console since then is not in it." />
|
|
</StackPanel>
|
|
</Border>
|
|
|
|
<StackPanel Grid.Row="2" Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right"
|
|
Margin="0,16,0,0">
|
|
<Button Classes="ghost" Height="44" Content="ANOTHER TOKEN" Command="{Binding RestartCommand}"
|
|
ToolTip.Tip="Back to the token box, for another project." />
|
|
<Button Classes="ghost" Height="44" Content="BACK TO THE LIST"
|
|
Command="{Binding ReviewCommand}"
|
|
ToolTip.Tip="The same servers, with what you just stored marked as already here." />
|
|
<Button Classes="accent" Height="44" Content="DONE" Command="{Binding CancelCommand}"
|
|
ToolTip.Tip="Back to Preferences." />
|
|
</StackPanel>
|
|
|
|
</Grid>
|
|
|
|
</Panel>
|
|
|
|
</Grid>
|
|
|
|
</UserControl>
|