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

feat: add progress messages when creating sync sessions#139

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
ethanndickson merged 5 commits intomainfromethan/sync-progress
May 1, 2025
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -20,7 +20,12 @@ final class PreviewFileSync: FileSyncDaemon {
state = .stopped
}

func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
func createSession(
arg _: CreateSyncSessionRequest,
promptCallback _: (
@MainActor (String) -> Void
)?
) async throws(DaemonError) {}

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

Expand Down
20 changes: 10 additions & 10 deletionsCoder-Desktop/Coder-Desktop/Preview Content/PreviewVPN.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,25 +6,25 @@ final class PreviewVPN: Coder_Desktop.VPNService {
@Publishedvarstate:Coder_Desktop.VPNServiceState=.connected
@PublishedvarmenuState:VPNMenuState=.init(agents:[
UUID():Agent(id:UUID(), name:"dev", status:.error, hosts:["asdf.coder"], wsName:"dogfood2",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.okay, hosts:["asdf.coder"],
wsName:"testing-a-very-long-name", wsID:UUID()),
wsName:"testing-a-very-long-name", wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.warn, hosts:["asdf.coder"], wsName:"opensrc",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"gvisor",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"example",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.error, hosts:["asdf.coder"], wsName:"dogfood2",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.okay, hosts:["asdf.coder"],
wsName:"testing-a-very-long-name", wsID:UUID()),
wsName:"testing-a-very-long-name", wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.warn, hosts:["asdf.coder"], wsName:"opensrc",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"gvisor",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"example",
wsID:UUID()),
wsID:UUID(), primaryHost:"asdf.coder"),
], workspaces:[:])
letshouldFail:Bool
letlongError="This is a long error to test the UI with long error messages"
Expand Down
17 changes: 9 additions & 8 deletionsCoder-Desktop/Coder-Desktop/VPN/MenuState.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -18,8 +18,7 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending
}

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

enum AgentStatus: Int, Equatable, Comparable {
Expand DownExpand Up@@ -69,6 +68,9 @@ struct VPNMenuState {
invalidAgents.append(agent)
return
}
// Remove trailing dot if present
let nonEmptyHosts = agent.fqdn.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 }
Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

The array being non-empty is checked above.


// An existing agent with the same name, belonging to the same workspace
// is from a previous workspace build, and should be removed.
agents.filter { $0.value.name == agent.name && $0.value.wsID == wsID }
Expand All@@ -81,10 +83,11 @@ struct VPNMenuState {
name: agent.name,
// If last handshake was not within last five minutes, the agent is unhealthy
status: agent.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .warn,
// Remove trailing dot if present
hosts: agent.fqdn.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 },
hosts: nonEmptyHosts,
wsName: workspace.name,
wsID: wsID
wsID: wsID,
// Hosts arrive sorted by length, the shortest looks best in the UI.
primaryHost: nonEmptyHosts.first!
)
}

Expand DownExpand Up@@ -135,9 +138,7 @@ struct VPNMenuState {
return items.sorted()
}

var onlineAgents: [Agent] {
agents.map(\.value).filter { $0.primaryHost != nil }
}
var onlineAgents: [Agent] { agents.map(\.value) }

mutating func clear() {
agents.removeAll()
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -15,6 +15,8 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
@State private var createError: DaemonError?
@State private var pickingRemote: Bool = false

@State private var lastPromptMessage: String?

var body: some View {
let agents = vpn.menuState.onlineAgents
VStack(spacing: 0) {
Expand All@@ -40,7 +42,7 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
Section {
Picker("Workspace", selection: $remoteHostname) {
ForEach(agents, id: \.id) { agent in
Text(agent.primaryHost!).tag(agent.primaryHost!)
Text(agent.primaryHost).tag(agent.primaryHost)
}
// HACK: Silence error logs for no-selection.
Divider().tag(nil as String?)
Expand All@@ -62,6 +64,12 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
Divider()
HStack {
Spacer()
if let msg = lastPromptMessage {
Text(msg).foregroundStyle(.secondary)
}
if loading {
ProgressView().controlSize(.small)
}
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }}
.keyboardShortcut(.defaultAction)
Expand DownExpand Up@@ -103,8 +111,10 @@ struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
arg: .init(
alpha: .init(path: localPath, protocolKind: .local),
beta: .init(path: remotePath, protocolKind: .ssh(host: remoteHostname))
)
),
promptCallback: { lastPromptMessage = $0 }
)
lastPromptMessage = nil
} catch {
createError = error
return
Expand Down
9 changes: 4 additions & 5 deletionsCoder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,7 +66,7 @@ struct MenuItemView: View {

privatevaritemName:AttributedString{
letname=switch item{
caselet.agent(agent): agent.primaryHost??"\(item.wsName).\(state.hostnameSuffix)"
caselet.agent(agent): agent.primaryHost
case.offlineWorkspace:"\(item.wsName).\(state.hostnameSuffix)"
}

Expand DownExpand Up@@ -103,10 +103,10 @@ struct MenuItemView: View {
}
Spacer()
}.buttonStyle(.plain)
if caselet.agent(agent)= item,let copyableDNS= agent.primaryHost{
if caselet.agent(agent)= item{
Button{
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(copyableDNS, forType:.string)
NSPasteboard.general.setString(agent.primaryHost, forType:.string)
} label:{
Image(systemName:"doc.on.doc")
.symbolVariant(.fill)
Expand DownExpand Up@@ -143,7 +143,6 @@ struct MenuItemView: View {
// If this menu item is an agent, and the user is logged in
if caselet.agent(agent)= item,
let client= state.client,
let host= agent.primaryHost,
let baseAccessURL= state.baseAccessURL,
// Like the CLI, we'll re-use the existing session token to populate the URL
let sessionToken= state.sessionToken
Expand All@@ -166,7 +165,7 @@ struct MenuItemView: View {
.flatMap(\.self)
.first(where:{ $0.id== agent.id})
{
apps=agentToApps(logger, wsAgent,host, baseAccessURL, sessionToken)
apps=agentToApps(logger, wsAgent,agent.primaryHost, baseAccessURL, sessionToken)
}else{
logger.error("Could not find agent '\(agent.id)' in workspace '\(item.wsName)' resources")
}
Expand Down
3 changes: 2 additions & 1 deletionCoder-Desktop/Coder-DesktopTests/AgentsTests.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -27,7 +27,8 @@ struct AgentsTests {
status: status,
hosts: ["a\($0).coder"],
wsName: "ws\($0)",
wsID: UUID()
wsID: UUID(),
primaryHost: "a\($0).coder"
)
return (agent.id, agent)
})
Expand Down
10 changes: 9 additions & 1 deletionCoder-Desktop/Coder-DesktopTests/FileSyncDaemonTests.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -61,6 +61,7 @@ class FileSyncDaemonTests {
#expect(statesEqual(daemon.state, .stopped))
#expect(daemon.sessionState.count == 0)

var promptMessages: [String] = []
try await daemon.createSession(
arg: .init(
alpha: .init(
Expand All@@ -71,9 +72,16 @@ class FileSyncDaemonTests {
path: mutagenBetaDirectory.path(),
protocolKind: .local
)
)
),
promptCallback: {
promptMessages.append($0)
}
)

// There should be at least one prompt message
// Usually "Creating session..."
#expect(promptMessages.count > 0)

// Daemon should have started itself
#expect(statesEqual(daemon.state, .running))
#expect(daemon.sessionState.count == 1)
Expand Down
7 changes: 6 additions & 1 deletionCoder-Desktop/Coder-DesktopTests/Util.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,6 +31,8 @@ class MockVPNService: VPNService, ObservableObject {
class MockFileSyncDaemon: FileSyncDaemon {
var logFile: URL = .init(filePath: "~/log.txt")

var lastPromptMessage: String?

var sessionState: [VPNLib.FileSyncSession] = []

func refreshSessions() async {}
Expand All@@ -47,7 +49,10 @@ class MockFileSyncDaemon: FileSyncDaemon {
[]
}

func createSession(arg _: CreateSyncSessionRequest) async throws(DaemonError) {}
func createSession(
arg _: CreateSyncSessionRequest,
promptCallback _: (@MainActor (String) -> Void)?
) async throws(DaemonError) {}

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

Expand Down
5 changes: 4 additions & 1 deletionCoder-Desktop/VPNLib/FileSync/FileSyncDaemon.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -14,7 +14,10 @@ public protocol FileSyncDaemon: ObservableObject {
func tryStart() async
func stop() async
func refreshSessions() async
func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError)
func createSession(
arg: CreateSyncSessionRequest,
promptCallback: (@MainActor (String) -> Void)?
) async throws(DaemonError)
func deleteSessions(ids: [String]) async throws(DaemonError)
func pauseSessions(ids: [String]) async throws(DaemonError)
func resumeSessions(ids: [String]) async throws(DaemonError)
Expand Down
7 changes: 5 additions & 2 deletionsCoder-Desktop/VPNLib/FileSync/FileSyncManagement.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,7 +17,10 @@ public extension MutagenDaemon {
sessionState = sessions.sessionStates.map { FileSyncSession(state: $0) }
}

func createSession(arg: CreateSyncSessionRequest) async throws(DaemonError) {
func createSession(
arg: CreateSyncSessionRequest,
promptCallback: (@MainActor (String) -> Void)? = nil
) async throws(DaemonError) {
if case .stopped = state {
do throws(DaemonError) {
try await start()
Expand All@@ -26,7 +29,7 @@ public extension MutagenDaemon {
throw error
}
}
let (stream, promptID) = try await host()
let (stream, promptID) = try await host(promptCallback: promptCallback)
defer { stream.cancel() }
let req = Synchronization_CreateRequest.with { req in
req.prompter = promptID
Expand Down
7 changes: 6 additions & 1 deletionCoder-Desktop/VPNLib/FileSync/FileSyncPrompting.swift
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -3,7 +3,10 @@ import GRPC
extension MutagenDaemon {
typealias PromptStream = GRPCAsyncBidirectionalStreamingCall<Prompting_HostRequest, Prompting_HostResponse>

func host(allowPrompts: Bool = true) async throws(DaemonError) -> (PromptStream, identifier: String) {
func host(
allowPrompts: Bool = true,
promptCallback: (@MainActor (String) -> Void)? = nil
) async throws(DaemonError) -> (PromptStream, identifier: String) {
let stream = client!.prompt.makeHostCall()

do {
Expand DownExpand Up@@ -39,6 +42,8 @@ extension MutagenDaemon {
}
// Any other messages that require a non-empty response will
// cause the create op to fail, showing an error. This is ok for now.
} else {
Task { @MainActor in promptCallback?(msg.message) }
}
try await stream.requestStream.send(reply)
}
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp