Skip to content

Commit 6210775

Browse files
feat: add progress messages when creating sync sessions (#139)
This loading might take a minute on a poor connection, and there's currently no feedback indicating what's going on, so we can display the prompt messages in the meantime. i.e. setting up a workspace with a fair bit of latency: https://github.com/user-attachments/assets/4321fbf7-8be6-4d4b-aead-0581c609d668 This PR also contains a small refactor for the `Agent` `primaryHost`, removing all the subsequent nil checks as we know it exists on creation.
1 parent 5f067b6 commit 6210775

File tree

11 files changed

+73
-33
lines changed

11 files changed

+73
-33
lines changed

Coder-Desktop/Coder-Desktop/Preview Content/PreviewFileSync.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ final class PreviewFileSync: FileSyncDaemon {
2020
state = .stopped
2121
}
2222

23-
func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
23+
func createSession(
24+
arg _: CreateSyncSessionRequest,
25+
promptCallback _: (
26+
@MainActor (String) -> Void
27+
)?
28+
) async throws(DaemonError) {}
2429

2530
func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
2631

Coder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,25 +6,25 @@ final class PreviewVPN: Coder_Desktop.VPNService {
66
@Published var state: Coder_Desktop.VPNServiceState = .connected
77
@Published var menuState: VPNMenuState = .init(agents: [
88
UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2",
9-
wsID: UUID()),
9+
wsID: UUID(), primaryHost: "asdf.coder"),
1010
UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"],
11-
wsName: "testing-a-very-long-name", wsID: UUID()),
11+
wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"),
1212
UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc",
13-
wsID: UUID()),
13+
wsID: UUID(), primaryHost: "asdf.coder"),
1414
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor",
15-
wsID: UUID()),
15+
wsID: UUID(), primaryHost: "asdf.coder"),
1616
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "example",
17-
wsID: UUID()),
17+
wsID: UUID(), primaryHost: "asdf.coder"),
1818
UUID(): Agent(id: UUID(), name: "dev", status: .error, hosts: ["asdf.coder"], wsName: "dogfood2",
19-
wsID: UUID()),
19+
wsID: UUID(), primaryHost: "asdf.coder"),
2020
UUID(): Agent(id: UUID(), name: "dev", status: .okay, hosts: ["asdf.coder"],
21-
wsName: "testing-a-very-long-name", wsID: UUID()),
21+
wsName: "testing-a-very-long-name", wsID: UUID(), primaryHost: "asdf.coder"),
2222
UUID(): Agent(id: UUID(), name: "dev", status: .warn, hosts: ["asdf.coder"], wsName: "opensrc",
23-
wsID: UUID()),
23+
wsID: UUID(), primaryHost: "asdf.coder"),
2424
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "gvisor",
25-
wsID: UUID()),
25+
wsID: UUID(), primaryHost: "asdf.coder"),
2626
UUID(): Agent(id: UUID(), name: "dev", status: .off, hosts: ["asdf.coder"], wsName: "example",
27-
wsID: UUID()),
27+
wsID: UUID(), primaryHost: "asdf.coder"),
2828
], workspaces: [:])
2929
let shouldFail: Bool
3030
let longError = "This is a long error to test the UI with long error messages"

Coder-Desktop/Coder-Desktop/VPN/MenuState.swift

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1818
return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending
1919
}
2020

21-
// Hosts arrive sorted by length, the shortest looks best in the UI.
22-
var primaryHost: String? { hosts.first }
21+
let primaryHost: String
2322
}
2423

2524
enum AgentStatus: Int, Equatable, Comparable {
@@ -69,6 +68,9 @@ struct VPNMenuState {
6968
invalidAgents.append(agent)
7069
return
7170
}
71+
// Remove trailing dot if present
72+
let nonEmptyHosts = agent.fqdn.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 }
73+
7274
// An existing agent with the same name, belonging to the same workspace
7375
// is from a previous workspace build, and should be removed.
7476
agents.filter { $0.value.name == agent.name && $0.value.wsID == wsID }
@@ -81,10 +83,11 @@ struct VPNMenuState {
8183
name: agent.name,
8284
// If last handshake was not within last five minutes, the agent is unhealthy
8385
status: agent.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .warn,
84-
// Remove trailing dot if present
85-
hosts: agent.fqdn.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 },
86+
hosts: nonEmptyHosts,
8687
wsName: workspace.name,
87-
wsID: wsID
88+
wsID: wsID,
89+
// Hosts arrive sorted by length, the shortest looks best in the UI.
90+
primaryHost: nonEmptyHosts.first!
8891
)
8992
}
9093

@@ -135,9 +138,7 @@ struct VPNMenuState {
135138
return items.sorted()
136139
}
137140

138-
var onlineAgents: [Agent] {
139-
agents.map(\.value).filter { $0.primaryHost != nil }
140-
}
141+
var onlineAgents: [Agent] { agents.map(\.value) }
141142

142143
mutating func clear() {
143144
agents.removeAll()

Coder-Desktop/Coder-Desktop/Views/FileSync/FileSyncSessionModal.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
1515
@State private var createError: DaemonError?
1616
@State private var pickingRemote: Bool = false
1717

18+
@State private var lastPromptMessage: String?
19+
1820
var body: some View {
1921
let agents = vpn.menuState.onlineAgents
2022
VStack(spacing: 0) {
@@ -40,7 +42,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
4042
Section {
4143
Picker("Workspace", selection: $remoteHostname) {
4244
ForEach(agents, id: \.id) { agent in
43-
Text(agent.primaryHost!).tag(agent.primaryHost!)
45+
Text(agent.primaryHost).tag(agent.primaryHost)
4446
}
4547
// HACK: Silence error logs for no-selection.
4648
Divider().tag(nil as String?)
@@ -62,6 +64,12 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
6264
Divider()
6365
HStack {
6466
Spacer()
67+
if let msg = lastPromptMessage {
68+
Text(msg).foregroundStyle(.secondary)
69+
}
70+
if loading {
71+
ProgressView().controlSize(.small)
72+
}
6573
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
6674
Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }}
6775
.keyboardShortcut(.defaultAction)
@@ -103,8 +111,10 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
103111
arg: .init(
104112
alpha: .init(path: localPath, protocolKind: .local),
105113
beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname))
106-
)
114+
),
115+
promptCallback: { lastPromptMessage = $0 }
107116
)
117+
lastPromptMessage = nil
108118
} catch {
109119
createError = error
110120
return

Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ struct MenuItemView: View {
6666

6767
private var itemName: AttributedString {
6868
let name = switch item {
69-
case let .agent(agent): agent.primaryHost ?? "\(item.wsName).\(state.hostnameSuffix)"
69+
case let .agent(agent): agent.primaryHost
7070
case .offlineWorkspace: "\(item.wsName).\(state.hostnameSuffix)"
7171
}
7272

@@ -103,10 +103,10 @@ struct MenuItemView: View {
103103
}
104104
Spacer()
105105
}.buttonStyle(.plain)
106-
if case let .agent(agent) = item, let copyableDNS = agent.primaryHost {
106+
if case let .agent(agent) = item {
107107
Button {
108108
NSPasteboard.general.clearContents()
109-
NSPasteboard.general.setString(copyableDNS, forType: .string)
109+
NSPasteboard.general.setString(agent.primaryHost, forType: .string)
110110
} label: {
111111
Image(systemName: "doc.on.doc")
112112
.symbolVariant(.fill)
@@ -143,7 +143,6 @@ struct MenuItemView: View {
143143
// If this menu item is an agent, and the user is logged in
144144
if case let .agent(agent) = item,
145145
let client = state.client,
146-
let host = agent.primaryHost,
147146
let baseAccessURL = state.baseAccessURL,
148147
// Like the CLI, we'll re-use the existing session token to populate the URL
149148
let sessionToken = state.sessionToken
@@ -166,7 +165,7 @@ struct MenuItemView: View {
166165
.flatMap(\.self)
167166
.first(where: { $0.id == agent.id })
168167
{
169-
apps = agentToApps(logger, wsAgent, host, baseAccessURL, sessionToken)
168+
apps = agentToApps(logger, wsAgent, agent.primaryHost, baseAccessURL, sessionToken)
170169
} else {
171170
logger.error("Could not find agent '\(agent.id)' in workspace '\(item.wsName)' resources")
172171
}

Coder-Desktop/Coder-DesktopTests/AgentsTests.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ struct AgentsTests {
2727
status: status,
2828
hosts: ["a\($0).coder"],
2929
wsName: "ws\($0)",
30-
wsID: UUID()
30+
wsID: UUID(),
31+
primaryHost: "a\($0).coder"
3132
)
3233
return (agent.id, agent)
3334
})

Coder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class FileSyncDaemonTests {
6161
#expect(statesEqual(daemon.state, .stopped))
6262
#expect(daemon.sessionState.count == 0)
6363

64+
var promptMessages: [String] = []
6465
try await daemon.createSession(
6566
arg: .init(
6667
alpha: .init(
@@ -71,9 +72,16 @@ class FileSyncDaemonTests {
7172
path: mutagenBetaDirectory.path(),
7273
protocolKind: .local
7374
)
74-
)
75+
),
76+
promptCallback: {
77+
promptMessages.append($0)
78+
}
7579
)
7680

81+
// There should be at least one prompt message
82+
// Usually "Creating session..."
83+
#expect(promptMessages.count > 0)
84+
7785
// Daemon should have started itself
7886
#expect(statesEqual(daemon.state, .running))
7987
#expect(daemon.sessionState.count == 1)

Coder-Desktop/Coder-DesktopTests/Util.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ class MockVPNService: VPNService, ObservableObject {
3131
class MockFileSyncDaemon: FileSyncDaemon {
3232
var logFile: URL = .init(filePath: "~/log.txt")
3333

34+
var lastPromptMessage: String?
35+
3436
var sessionState: [VPNLib.FileSyncSession] = []
3537

3638
func refreshSessions() async {}
@@ -47,7 +49,10 @@ class MockFileSyncDaemon: FileSyncDaemon {
4749
[]
4850
}
4951

50-
func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
52+
func createSession(
53+
arg _: CreateSyncSessionRequest,
54+
promptCallback _: (@MainActor (String) -> Void)?
55+
) async throws(DaemonError) {}
5156

5257
func pauseSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
5358

Coder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ public protocol FileSyncDaemon: ObservableObject {
1414
func tryStart() async
1515
func stop() async
1616
func refreshSessions() async
17-
func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError)
17+
func createSession(
18+
arg: CreateSyncSessionRequest,
19+
promptCallback: (@MainActor (String) -> Void)?
20+
) async throws(DaemonError)
1821
func deleteSessions(ids: [String]) async throws(DaemonError)
1922
func pauseSessions(ids: [String]) async throws(DaemonError)
2023
func resumeSessions(ids: [String]) async throws(DaemonError)

Coder-Desktop/VPNLib/FileSync/FileSyncManagement.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ public extension MutagenDaemon {
1717
sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) }
1818
}
1919

20-
func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) {
20+
func createSession(
21+
arg: CreateSyncSessionRequest,
22+
promptCallback: (@MainActor (String) -> Void)? = nil
23+
) async throws(DaemonError) {
2124
if case .stopped = state {
2225
do throws(DaemonError) {
2326
try await start()
@@ -26,7 +29,7 @@ public extension MutagenDaemon {
2629
throw error
2730
}
2831
}
29-
let (stream, promptID) = try await host()
32+
let (stream, promptID) = try await host(promptCallback: promptCallback)
3033
defer { stream.cancel() }
3134
let req = Synchronization_CreateRequest.with { req in
3235
req.prompter = promptID

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy