Skip to content

Commit ae1e3b0

Browse files
committed
feat: add stubbed file sync UI
1 parent 4fb7970 commit ae1e3b0

19 files changed

+402
-46
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ struct DesktopApp: App {
2323
.environmentObject(appDelegate.state)
2424
}
2525
.windowResizability(.contentSize)
26+
Window("File Sync", id: Windows.fileSync.rawValue) {
27+
FileSyncConfig<CoderVPNService, MutagenDaemon>()
28+
.environmentObject(appDelegate.state)
29+
.environmentObject(appDelegate.fileSyncDaemon)
30+
.environmentObject(appDelegate.vpn)
31+
}
2632
}
2733
}
2834

@@ -41,9 +47,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
4147
}
4248
vpn.installSystemExtension()
4349
#if arch(arm64)
44-
let mutagenBinary = "mutagen-darwin-arm64"
50+
let mutagenBinary = "mutagen-darwin-arm64"
4551
#elseif arch(x86_64)
46-
let mutagenBinary = "mutagen-darwin-amd64"
52+
let mutagenBinary = "mutagen-darwin-amd64"
4753
#endif
4854
fileSyncDaemon = MutagenDaemon(
4955
mutagenPath: Bundle.main.url(forResource: mutagenBinary, withExtension: nil)
@@ -61,9 +67,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
6167
await self.state.handleTokenExpiry()
6268
}
6369
}, content: {
64-
VPNMenu<CoderVPNService>().frame(width: 256)
70+
VPNMenu<CoderVPNService, MutagenDaemon>().frame(width: 256)
6571
.environmentObject(self.vpn)
6672
.environmentObject(self.state)
73+
.environmentObject(self.fileSyncDaemon)
6774
}
6875
))
6976
// Subscribe to system VPN updates
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import VPNLib
2+
3+
@MainActor
4+
final class PreviewFileSync: FileSyncDaemon {
5+
var sessionState: [VPNLib.FileSyncSession] = []
6+
7+
var state: DaemonState = .running
8+
9+
init() {}
10+
11+
func refreshSessions() async {}
12+
13+
func start() async throws(DaemonError) {
14+
state = .running
15+
}
16+
17+
func stop() async {
18+
state = .stopped
19+
}
20+
21+
func createSession(localPath _: String, agentHost _: String, remotePath _: String) async throws(DaemonError) {}
22+
23+
func deleteSessions(ids _: [String]) async throws(VPNLib.DaemonError) {}
24+
}

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22
import SwiftUI
33
import VPNLib
44

5-
struct Agent: Identifiable, Equatable, Comparable {
5+
struct Agent: Identifiable, Equatable, Comparable, Hashable {
66
let id: UUID
77
let name: String
88
let status: AgentStatus
@@ -135,6 +135,10 @@ struct VPNMenuState {
135135
return items.sorted()
136136
}
137137

138+
var onlineAgents: [Agent] {
139+
agents.map(\.value).filter { $0.primaryHost != nil }
140+
}
141+
138142
mutating func clear() {
139143
agents.removeAll()
140144
workspaces.removeAll()
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
5+
@EnvironmentObject var vpn: VPN
6+
@EnvironmentObject var fileSync: FS
7+
8+
@State private var selection: FileSyncSession.ID?
9+
@State private var addingNewSession: Bool = false
10+
@State private var editingSession: FileSyncSession?
11+
12+
@State private var loading: Bool = false
13+
@State private var deleteError: DaemonError?
14+
15+
var body: some View {
16+
Group {
17+
Table(fileSync.sessionState, selection: $selection) {
18+
TableColumn("Local Path") {
19+
Text($0.alphaPath).help($0.alphaPath)
20+
}.width(min: 200, ideal: 240)
21+
TableColumn("Workspace", value: \.agentHost)
22+
.width(min: 100, ideal: 120)
23+
TableColumn("Remote Path", value: \.betaPath)
24+
.width(min: 100, ideal: 120)
25+
TableColumn("Status") { $0.status.body }
26+
.width(min: 80, ideal: 100)
27+
TableColumn("Size") { item in
28+
Text(item.size)
29+
}
30+
.width(min: 60, ideal: 80)
31+
}
32+
.frame(minWidth: 400, minHeight: 200)
33+
.padding(.bottom, 25)
34+
.overlay(alignment: .bottom) {
35+
VStack(alignment: .leading, spacing: 0) {
36+
Divider()
37+
HStack(spacing: 0) {
38+
Button {
39+
addingNewSession = true
40+
} label: {
41+
Image(systemName: "plus")
42+
.frame(width: 24, height: 24)
43+
}.disabled(vpn.menuState.agents.isEmpty)
44+
Divider()
45+
Button {
46+
Task {
47+
loading = true
48+
defer { loading = false }
49+
do throws(DaemonError) {
50+
try await fileSync.deleteSessions(ids: [selection!])
51+
} catch {
52+
deleteError = error
53+
}
54+
await fileSync.refreshSessions()
55+
selection = nil
56+
}
57+
} label: {
58+
Image(systemName: "minus").frame(width: 24, height: 24)
59+
}.disabled(selection == nil)
60+
if let selection {
61+
if let selectedSession = fileSync.sessionState.first(where: { $0.id == selection }) {
62+
Divider()
63+
Button {
64+
// TODO: Pause & Unpause
65+
} label: {
66+
switch selectedSession.status {
67+
case .paused:
68+
Image(systemName: "play").frame(width: 24, height: 24)
69+
default:
70+
Image(systemName: "pause").frame(width: 24, height: 24)
71+
}
72+
}
73+
}
74+
}
75+
}
76+
.buttonStyle(.borderless)
77+
}
78+
.background(.primary.opacity(0.04))
79+
.fixedSize(horizontal: false, vertical: true)
80+
}
81+
}.sheet(isPresented: $addingNewSession) {
82+
FileSyncSessionModal<VPN, FS>()
83+
.frame(width: 700)
84+
}.sheet(item: $editingSession) { session in
85+
FileSyncSessionModal<VPN, FS>(existingSession: session)
86+
.frame(width: 700)
87+
}.alert("Error", isPresented: Binding(
88+
get: { deleteError != nil },
89+
set: { isPresented in
90+
if !isPresented {
91+
deleteError = nil
92+
}
93+
}
94+
)) {} message: {
95+
Text(deleteError?.description ?? "An unknown error occurred. This should never happen.")
96+
}.task {
97+
while !Task.isCancelled {
98+
await fileSync.refreshSessions()
99+
try? await Task.sleep(for: .seconds(2))
100+
}
101+
}.disabled(loading)
102+
}
103+
}
104+
105+
#if DEBUG
106+
#Preview {
107+
FileSyncConfig<PreviewVPN, PreviewFileSync>()
108+
.environmentObject(AppState(persistent: false))
109+
.environmentObject(PreviewVPN())
110+
.environmentObject(PreviewFileSync())
111+
}
112+
#endif
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
5+
var existingSession: FileSyncSession?
6+
@Environment(\.dismiss) private var dismiss
7+
@EnvironmentObject private var vpn: VPN
8+
@EnvironmentObject private var fileSync: FS
9+
10+
@State private var localPath: String = ""
11+
@State private var workspace: Agent?
12+
@State private var remotePath: String = ""
13+
14+
@State private var loading: Bool = false
15+
@State private var createError: DaemonError?
16+
17+
var body: some View {
18+
let agents = vpn.menuState.onlineAgents
19+
VStack(spacing: 0) {
20+
Form {
21+
Section {
22+
HStack(spacing: 5) {
23+
TextField("Local Path", text: $localPath)
24+
Spacer()
25+
Button {
26+
let panel = NSOpenPanel()
27+
panel.directoryURL = FileManager.default.homeDirectoryForCurrentUser
28+
panel.allowsMultipleSelection = false
29+
panel.canChooseDirectories = true
30+
panel.canChooseFiles = false
31+
if panel.runModal() == .OK {
32+
localPath = panel.url?.path(percentEncoded: false) ?? "<none>"
33+
}
34+
} label: {
35+
Image(systemName: "folder")
36+
}
37+
}
38+
}
39+
Section {
40+
Picker("Workspace", selection: $workspace) {
41+
ForEach(agents, id: \.id) { agent in
42+
Text(agent.primaryHost!).tag(agent)
43+
}
44+
// HACK: Silence error logs for no-selection.
45+
Divider().tag(nil as Agent?)
46+
}
47+
}
48+
Section {
49+
TextField("Remote Path", text: $remotePath)
50+
}
51+
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
52+
Divider()
53+
HStack {
54+
Spacer()
55+
if loading {
56+
ProgressView()
57+
}
58+
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
59+
Button(existingSession == nil ? "Add" : "Save") { Task { await submit() }}
60+
.keyboardShortcut(.defaultAction)
61+
}.padding(20)
62+
}.onAppear {
63+
if let existingSession {
64+
localPath = existingSession.alphaPath
65+
workspace = agents.first { $0.primaryHost == existingSession.agentHost }
66+
remotePath = existingSession.betaPath
67+
} else {
68+
// Set the picker to the first agent by default
69+
workspace = agents.first
70+
}
71+
}.disabled(loading)
72+
.alert("Error", isPresented: Binding(
73+
get: { createError != nil },
74+
set: { if $0 { createError = nil } }
75+
)) {} message: {
76+
Text(createError?.description ?? "An unknown error occurred. This should never happen.")
77+
}
78+
}
79+
80+
func submit() async {
81+
createError = nil
82+
guard let workspace else {
83+
return
84+
}
85+
loading = true
86+
defer { loading = false }
87+
do throws(DaemonError) {
88+
if let existingSession {
89+
// TODO: Support selecting & deleting multiple sessions at once
90+
try await fileSync.deleteSessions(ids: [existingSession.id])
91+
}
92+
try await fileSync.createSession(
93+
localPath: localPath,
94+
agentHost: workspace.primaryHost!,
95+
remotePath: remotePath
96+
)
97+
} catch {
98+
createError = error
99+
return
100+
}
101+
dismiss()
102+
}
103+
}

Coder-Desktop/Coder-Desktop/Views/LoginForm.swift

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,8 @@ struct LoginForm: View {
4848
loginError = nil
4949
}
5050
}
51-
)) {
52-
Button("OK", role: .cancel) {}.keyboardShortcut(.defaultAction)
53-
} message: {
54-
Text(loginError?.description ?? "")
51+
)) {} message: {
52+
Text(loginError?.description ?? "An unknown error occurred. This should never happen.")
5553
}.disabled(loading)
5654
.frame(width: 550)
5755
.fixedSize()

Coder-Desktop/Coder-Desktop/Views/Settings/LiteralHeadersSection.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
1515
Toggle(isOn: $state.useLiteralHeaders) {
1616
Text("HTTP Headers")
1717
Text("When enabled, these headers will be included on all outgoing HTTP requests.")
18-
if vpn.state != .disabled { Text("Cannot be modified while Coder Connect is enabled.") }
18+
if !vpn.state.canBeStarted { Text("Cannot be modified while Coder Connect is enabled.") }
1919
}
2020
.controlSize(.large)
2121

@@ -65,7 +65,7 @@ struct LiteralHeadersSection<VPN: VPNService>: View {
6565
LiteralHeaderModal(existingHeader: header)
6666
}.onTapGesture {
6767
selectedHeader = nil
68-
}.disabled(vpn.state != .disabled)
68+
}.disabled(!vpn.state.canBeStarted)
6969
.onReceive(inspection.notice) { inspection.visit(self, $0) } // ViewInspector
7070
}
7171
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import SwiftUI
2+
3+
struct StatusDot: View {
4+
let color: Color
5+
6+
var body: some View {
7+
ZStack {
8+
Circle()
9+
.fill(color.opacity(0.4))
10+
.frame(width: 12, height: 12)
11+
Circle()
12+
.fill(color.opacity(1.0))
13+
.frame(width: 7, height: 7)
14+
}
15+
}
16+
}

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