Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit6210775

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-0581c609d668This PR also contains a small refactor for the `Agent` `primaryHost`, removing all the subsequent nil checks as we know it exists on creation.
1 parent5f067b6 commit6210775

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)asyncthrows(DaemonError){}
23+
func createSession(
24+
arg _:CreateSyncSessionRequest,
25+
promptCallback _:(
26+
@MainActor(String)->Void
27+
)?
28+
)asyncthrows(DaemonError){}
2429

2530
func deleteSessions(ids _:[String])asyncthrows(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
@Publishedvarstate:Coder_Desktop.VPNServiceState=.connected
77
@PublishedvarmenuState: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
letshouldFail:Bool
3030
letlongError="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-
varprimaryHost:String?{ hosts.first}
21+
letprimaryHost:String
2322
}
2423

2524
enumAgentStatus:Int,Equatable,Comparable{
@@ -69,6 +68,9 @@ struct VPNMenuState {
6968
invalidAgents.append(agent)
7069
return
7170
}
71+
// Remove trailing dot if present
72+
letnonEmptyHosts= 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-
varonlineAgents:[Agent]{
139-
agents.map(\.value).filter{ $0.primaryHost!=nil}
140-
}
141+
varonlineAgents:[Agent]{ agents.map(\.value)}
141142

142143
mutatingfunc 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
@StateprivatevarcreateError:DaemonError?
1616
@StateprivatevarpickingRemote:Bool=false
1717

18+
@StateprivatevarlastPromptMessage:String?
19+
1820
varbody:someView{
1921
letagents= 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){ agentin
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(nilasString?)
@@ -62,6 +64,12 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
6264
Divider()
6365
HStack{
6466
Spacer()
67+
iflet 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{awaitsubmit()}}
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
privatevaritemName:AttributedString{
6868
letname=switch item{
69-
caselet.agent(agent): agent.primaryHost??"\(item.wsName).\(state.hostnameSuffix)"
69+
caselet.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 caselet.agent(agent)= item,let copyableDNS= agent.primaryHost{
106+
if caselet.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 caselet.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+
varpromptMessages:[String]=[]
6465
tryawait 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
classMockFileSyncDaemon:FileSyncDaemon{
3232
varlogFile:URL=.init(filePath:"~/log.txt")
3333

34+
varlastPromptMessage:String?
35+
3436
varsessionState:[VPNLib.FileSyncSession]=[]
3537

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

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

5257
func pauseSessions(ids _:[String])asyncthrows(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)asyncthrows(DaemonError)
17+
func createSession(
18+
arg:CreateSyncSessionRequest,
19+
promptCallback:(@MainActor(String)->Void)?
20+
)asyncthrows(DaemonError)
1821
func deleteSessions(ids:[String])asyncthrows(DaemonError)
1922
func pauseSessions(ids:[String])asyncthrows(DaemonError)
2023
func resumeSessions(ids:[String])asyncthrows(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)asyncthrows(DaemonError){
20+
func createSession(
21+
arg:CreateSyncSessionRequest,
22+
promptCallback:(@MainActor(String)->Void)?=nil
23+
)asyncthrows(DaemonError){
2124
if case.stopped= state{
2225
dothrows(DaemonError){
2326
tryawaitstart()
@@ -26,7 +29,7 @@ public extension MutagenDaemon {
2629
throw error
2730
}
2831
}
29-
let(stream, promptID)=tryawaithost()
32+
let(stream, promptID)=tryawaithost(promptCallback: promptCallback)
3033
defer{ stream.cancel()}
3134
letreq=Synchronization_CreateRequest.with{ reqin
3235
req.prompter= promptID

‎Coder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ import GRPC
33
extensionMutagenDaemon{
44
typealiasPromptStream=GRPCAsyncBidirectionalStreamingCall<Prompting_HostRequest,Prompting_HostResponse>
55

6-
func host(allowPrompts:Bool=true)asyncthrows(DaemonError)->(PromptStream, identifier:String){
6+
func host(
7+
allowPrompts:Bool=true,
8+
promptCallback:(@MainActor(String)->Void)?=nil
9+
)asyncthrows(DaemonError)->(PromptStream, identifier:String){
710
letstream= client!.prompt.makeHostCall()
811

912
do{
@@ -39,6 +42,8 @@ extension MutagenDaemon {
3942
}
4043
// Any other messages that require a non-empty response will
4144
// cause the create op to fail, showing an error. This is ok for now.
45+
}else{
46+
Task{@MainActorinpromptCallback?(msg.message)}
4247
}
4348
tryawait stream.requestStream.send(reply)
4449
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp