Skip to content

Commit f669c09

Browse files
committed
Merge branch 'main' into dean/agent-status
2 parents b01d2e1 + 641f1bc commit f669c09

12 files changed

+475
-15
lines changed

App/App.xaml.cs

+8-6
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ namespace Coder.Desktop.App;
1212
public partial class App : Application
1313
{
1414
private readonly IServiceProvider _services;
15-
private TrayWindow? _trayWindow;
1615

1716
public App()
1817
{
1918
var services = new ServiceCollection();
2019
services.AddSingleton<ICredentialManager, CredentialManager>();
2120
services.AddSingleton<IRpcController, RpcController>();
2221

23-
// TrayWindow pages and view models
22+
// SignInWindow views and view models
23+
services.AddTransient<SignInViewModel>();
24+
services.AddTransient<SignInWindow>();
25+
26+
// TrayWindow views and view models
2427
services.AddTransient<TrayWindowDisconnectedViewModel>();
2528
services.AddTransient<TrayWindowDisconnectedPage>();
2629
services.AddTransient<TrayWindowLoginRequiredViewModel>();
@@ -42,12 +45,11 @@ public App()
4245

4346
protected override void OnLaunched(LaunchActivatedEventArgs args)
4447
{
45-
_trayWindow = _services.GetRequiredService<TrayWindow>();
46-
// Just hide the window rather than closing it.
47-
_trayWindow.Closed += (sender, args) =>
48+
var trayWindow = _services.GetRequiredService<TrayWindow>();
49+
trayWindow.Closed += (sender, args) =>
4850
{
4951
args.Handled = true;
50-
_trayWindow.AppWindow.Hide();
52+
trayWindow.AppWindow.Hide();
5153
};
5254
}
5355
}

App/DisplayScale.cs

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System;
2+
using System.Runtime.InteropServices;
3+
using Microsoft.UI.Xaml;
4+
using WinRT.Interop;
5+
6+
namespace Coder.Desktop.App;
7+
8+
/// <summary>
9+
/// A static utility class to house methods related to the visual scale of the display monitor.
10+
/// </summary>
11+
public static class DisplayScale
12+
{
13+
public static double WindowScale(Window win)
14+
{
15+
var hwnd = WindowNative.GetWindowHandle(win);
16+
var dpi = NativeApi.GetDpiForWindow(hwnd);
17+
if (dpi == 0) return 1; // assume scale of 1
18+
return dpi / 96.0; // 96 DPI == 1
19+
}
20+
21+
public class NativeApi
22+
{
23+
[DllImport("user32.dll")]
24+
public static extern int GetDpiForWindow(IntPtr hwnd);
25+
}
26+
}

App/Services/CredentialManager.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.Threading.Tasks;
77
using Coder.Desktop.App.Models;
88
using Coder.Desktop.Vpn.Utilities;
9+
using CoderSdk;
910

1011
namespace Coder.Desktop.App.Services;
1112

@@ -63,13 +64,12 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
6364
if (apiToken.Length != 33)
6465
throw new ArgumentOutOfRangeException(nameof(apiToken), "API token must be 33 characters long");
6566

66-
// TODO: this code seems to hang?
67-
/*
6867
try
6968
{
7069
var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
71-
cts.CancelAfter(TimeSpan.FromSeconds(5));
70+
cts.CancelAfter(TimeSpan.FromSeconds(15));
7271
var sdkClient = new CoderApiClient(uri);
72+
sdkClient.SetSessionToken(apiToken);
7373
// TODO: we should probably perform a version check here too,
7474
// rather than letting the service do it on Start
7575
_ = await sdkClient.GetBuildInfo(cts.Token);
@@ -79,7 +79,6 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
7979
{
8080
throw new InvalidOperationException("Could not connect to or verify Coder server", e);
8181
}
82-
*/
8382

8483
WriteCredentials(new RawCredentials
8584
{

App/ViewModels/SignInViewModel.cs

+138
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Coder.Desktop.App.Services;
5+
using Coder.Desktop.App.Views;
6+
using CommunityToolkit.Mvvm.ComponentModel;
7+
using CommunityToolkit.Mvvm.Input;
8+
using Microsoft.UI.Xaml;
9+
10+
namespace Coder.Desktop.App.ViewModels;
11+
12+
/// <summary>
13+
/// The View Model backing the sign in window and all its associated pages.
14+
/// </summary>
15+
public partial class SignInViewModel : ObservableObject
16+
{
17+
private readonly ICredentialManager _credentialManager;
18+
19+
[ObservableProperty]
20+
[NotifyPropertyChangedFor(nameof(CoderUrlError))]
21+
[NotifyPropertyChangedFor(nameof(GenTokenUrl))]
22+
public partial string CoderUrl { get; set; } = string.Empty;
23+
24+
[ObservableProperty]
25+
[NotifyPropertyChangedFor(nameof(CoderUrlError))]
26+
public partial bool CoderUrlTouched { get; set; } = false;
27+
28+
[ObservableProperty]
29+
[NotifyPropertyChangedFor(nameof(ApiTokenError))]
30+
public partial string ApiToken { get; set; } = string.Empty;
31+
32+
[ObservableProperty]
33+
[NotifyPropertyChangedFor(nameof(ApiTokenError))]
34+
public partial bool ApiTokenTouched { get; set; } = false;
35+
36+
[ObservableProperty]
37+
public partial string? SignInError { get; set; } = null;
38+
39+
[ObservableProperty]
40+
public partial bool SignInLoading { get; set; } = false;
41+
42+
public string? CoderUrlError => CoderUrlTouched ? _coderUrlError : null;
43+
44+
private string? _coderUrlError
45+
{
46+
get
47+
{
48+
if (!Uri.TryCreate(CoderUrl, UriKind.Absolute, out var uri))
49+
return "Invalid URL";
50+
if (uri.Scheme is not "http" and not "https")
51+
return "Must be a HTTP or HTTPS URL";
52+
if (uri.PathAndQuery != "/")
53+
return "Must be a root URL with no path or query";
54+
return null;
55+
}
56+
}
57+
58+
public string? ApiTokenError => ApiTokenTouched ? _apiTokenError : null;
59+
60+
private string? _apiTokenError => string.IsNullOrWhiteSpace(ApiToken) ? "Invalid token" : null;
61+
62+
public Uri GenTokenUrl
63+
{
64+
get
65+
{
66+
// In case somehow the URL is invalid, just default to coder.com.
67+
// The HyperlinkButton will crash the entire app if the URL is
68+
// invalid.
69+
try
70+
{
71+
var baseUri = new Uri(CoderUrl.Trim());
72+
var cliAuthUri = new Uri(baseUri, "/cli-auth");
73+
return cliAuthUri;
74+
}
75+
catch
76+
{
77+
return new Uri("https://coder.com");
78+
}
79+
}
80+
}
81+
82+
public SignInViewModel(ICredentialManager credentialManager)
83+
{
84+
_credentialManager = credentialManager;
85+
}
86+
87+
public void CoderUrl_FocusLost(object sender, RoutedEventArgs e)
88+
{
89+
CoderUrlTouched = true;
90+
}
91+
92+
public void ApiToken_FocusLost(object sender, RoutedEventArgs e)
93+
{
94+
ApiTokenTouched = true;
95+
}
96+
97+
[RelayCommand]
98+
public void UrlPage_Next(SignInWindow signInWindow)
99+
{
100+
CoderUrlTouched = true;
101+
if (_coderUrlError != null) return;
102+
signInWindow.NavigateToTokenPage();
103+
}
104+
105+
[RelayCommand]
106+
public void TokenPage_Back(SignInWindow signInWindow)
107+
{
108+
ApiToken = "";
109+
signInWindow.NavigateToUrlPage();
110+
}
111+
112+
[RelayCommand]
113+
public async Task TokenPage_SignIn(SignInWindow signInWindow)
114+
{
115+
CoderUrlTouched = true;
116+
ApiTokenTouched = true;
117+
if (_coderUrlError != null || _apiTokenError != null) return;
118+
119+
try
120+
{
121+
SignInLoading = true;
122+
SignInError = null;
123+
124+
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(15));
125+
await _credentialManager.SetCredentials(CoderUrl.Trim(), ApiToken.Trim(), cts.Token);
126+
127+
signInWindow.Close();
128+
}
129+
catch (Exception e)
130+
{
131+
SignInError = $"Failed to sign in: {e}";
132+
}
133+
finally
134+
{
135+
SignInLoading = false;
136+
}
137+
}
138+
}
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,34 @@
1-
using CommunityToolkit.Mvvm.ComponentModel;
1+
using System;
2+
using Coder.Desktop.App.Views;
23
using CommunityToolkit.Mvvm.Input;
4+
using Microsoft.Extensions.DependencyInjection;
35

46
namespace Coder.Desktop.App.ViewModels;
57

6-
public partial class TrayWindowLoginRequiredViewModel : ObservableObject
8+
public partial class TrayWindowLoginRequiredViewModel
79
{
10+
private readonly IServiceProvider _services;
11+
12+
private SignInWindow? _signInWindow;
13+
14+
public TrayWindowLoginRequiredViewModel(IServiceProvider services)
15+
{
16+
_services = services;
17+
}
18+
819
[RelayCommand]
920
public void Login()
1021
{
11-
// TODO: open the login window
22+
// This is safe against concurrent access since it all happens in the
23+
// UI thread.
24+
if (_signInWindow != null)
25+
{
26+
_signInWindow.Activate();
27+
return;
28+
}
29+
30+
_signInWindow = _services.GetRequiredService<SignInWindow>();
31+
_signInWindow.Closed += (_, _) => _signInWindow = null;
32+
_signInWindow.Activate();
1233
}
1334
}

App/Views/Pages/SignInTokenPage.xaml

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
3+
<Page
4+
x:Class="Coder.Desktop.App.Views.Pages.SignInTokenPage"
5+
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
6+
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
7+
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
8+
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
9+
mc:Ignorable="d"
10+
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
11+
12+
<StackPanel
13+
Orientation="Vertical"
14+
HorizontalAlignment="Stretch"
15+
VerticalAlignment="Top"
16+
Padding="20"
17+
Spacing="10">
18+
19+
<TextBlock
20+
Text="Coder Desktop"
21+
FontSize="24"
22+
VerticalAlignment="Center"
23+
HorizontalAlignment="Center" />
24+
25+
<Grid>
26+
<Grid.ColumnDefinitions>
27+
<ColumnDefinition Width="Auto" />
28+
<ColumnDefinition Width="*" />
29+
</Grid.ColumnDefinitions>
30+
<Grid.RowDefinitions>
31+
<RowDefinition Height="1*" />
32+
<RowDefinition Height="1*" />
33+
<RowDefinition Height="1*" />
34+
<RowDefinition Height="1*" />
35+
<RowDefinition Height="1*" />
36+
</Grid.RowDefinitions>
37+
38+
<TextBlock
39+
Grid.Column="0"
40+
Grid.Row="0"
41+
Text="Server URL"
42+
HorizontalAlignment="Right"
43+
Padding="10" />
44+
45+
<TextBlock
46+
Grid.Column="1"
47+
Grid.Row="0"
48+
HorizontalAlignment="Stretch"
49+
VerticalAlignment="Center"
50+
Padding="10"
51+
Text="{x:Bind ViewModel.CoderUrl, Mode=OneWay}" />
52+
53+
<TextBlock
54+
Grid.Column="0"
55+
Grid.Row="2"
56+
Text="Session Token"
57+
HorizontalAlignment="Right"
58+
Padding="10" />
59+
60+
<TextBox
61+
Grid.Column="1"
62+
Grid.Row="2"
63+
HorizontalAlignment="Stretch"
64+
PlaceholderText="Paste your token here"
65+
LostFocus="{x:Bind ViewModel.ApiToken_FocusLost, Mode=OneWay}"
66+
Text="{x:Bind ViewModel.ApiToken, Mode=TwoWay}"
67+
InputScope="Password" />
68+
69+
<TextBlock
70+
Grid.Column="1"
71+
Grid.Row="3"
72+
Text="{x:Bind ViewModel.ApiTokenError, Mode=OneWay}"
73+
Foreground="Red" />
74+
75+
<HyperlinkButton
76+
Grid.Column="1"
77+
Grid.Row="4"
78+
Content="Generate a token via the Web UI"
79+
NavigateUri="{x:Bind ViewModel.GenTokenUrl, Mode=OneWay}" />
80+
</Grid>
81+
82+
<StackPanel
83+
Orientation="Horizontal"
84+
HorizontalAlignment="Center"
85+
Spacing="10">
86+
87+
<Button
88+
Content="Back" HorizontalAlignment="Right"
89+
Command="{x:Bind ViewModel.TokenPage_BackCommand, Mode=OneWay}"
90+
CommandParameter="{x:Bind SignInWindow, Mode=OneWay}" />
91+
92+
<Button
93+
Content="Sign In"
94+
HorizontalAlignment="Left"
95+
Style="{StaticResource AccentButtonStyle}"
96+
Command="{x:Bind ViewModel.TokenPage_SignInCommand, Mode=OneWay}"
97+
CommandParameter="{x:Bind SignInWindow, Mode=OneWay}" />
98+
</StackPanel>
99+
100+
<TextBlock
101+
Text="{x:Bind ViewModel.SignInError, Mode=OneWay}"
102+
HorizontalAlignment="Center"
103+
Foreground="Red" />
104+
</StackPanel>
105+
</Page>
+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Coder.Desktop.App.ViewModels;
2+
using Microsoft.UI.Xaml.Controls;
3+
4+
namespace Coder.Desktop.App.Views.Pages;
5+
6+
/// <summary>
7+
/// A sign in page to accept the user's Coder token.
8+
/// </summary>
9+
public sealed partial class SignInTokenPage : Page
10+
{
11+
public readonly SignInViewModel ViewModel;
12+
public readonly SignInWindow SignInWindow;
13+
14+
public SignInTokenPage(SignInWindow parent, SignInViewModel viewModel)
15+
{
16+
InitializeComponent();
17+
ViewModel = viewModel;
18+
SignInWindow = parent;
19+
}
20+
}

0 commit comments

Comments
 (0)