Skip to content

Commit e25df16

Browse files
committed
feat: add stubbed file sync UI
1 parent 173554d commit e25df16

18 files changed

+323
-16
lines changed

Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 8 additions & 1 deletion
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

@@ -56,9 +62,10 @@ class AppDelegate: NSObject, NSApplicationDelegate {
5662
await self.state.handleTokenExpiry()
5763
}
5864
}, content: {
59-
VPNMenu<CoderVPNService>().frame(width: 256)
65+
VPNMenu<CoderVPNService, MutagenDaemon>().frame(width: 256)
6066
.environmentObject(self.vpn)
6167
.environmentObject(self.state)
68+
.environmentObject(self.fileSyncDaemon)
6269
}
6370
))
6471
// Subscribe to system VPN updates
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import VPNLib
2+
3+
@MainActor
4+
final class PreviewFileSync: FileSyncDaemon {
5+
var state: DaemonState = .running
6+
7+
init() {}
8+
9+
func start() async throws(DaemonError) {
10+
state = .running
11+
}
12+
13+
func stop() async {
14+
state = .stopped
15+
}
16+
17+
func listSessions() async throws -> [FileSyncSession] {
18+
[]
19+
}
20+
21+
func createSession(with _: FileSyncSession) async throws {}
22+
}

Coder-Desktop/Coder-Desktop/State.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,14 @@ class AppState: ObservableObject {
6363
}
6464
}
6565

66+
// Temporary feature flag
67+
@Published var showFileSyncUI: Bool = UserDefaults.standard.bool(forKey: Keys.showFileSyncUI) {
68+
didSet {
69+
guard persistent else { return }
70+
UserDefaults.standard.set(showFileSyncUI, forKey: Keys.showFileSyncUI)
71+
}
72+
}
73+
6674
func tunnelProviderProtocol() -> NETunnelProviderProtocol? {
6775
if !hasSession { return nil }
6876
let proto = NETunnelProviderProtocol()
@@ -164,6 +172,8 @@ class AppState: ObservableObject {
164172
static let literalHeaders = "LiteralHeaders"
165173
static let stopVPNOnQuit = "StopVPNOnQuit"
166174
static let startVPNOnLaunch = "StartVPNOnLaunch"
175+
176+
static let showFileSyncUI = "showFileSyncUI"
167177
}
168178
}
169179

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
struct FileSyncRow: Identifiable {
5+
var id = UUID()
6+
var localPath: URL
7+
var workspace: String
8+
// This is a string as to be host-OS agnostic
9+
var remotePath: String
10+
var status: FileSyncStatus
11+
var size: String
12+
}
13+
14+
enum FileSyncStatus {
15+
case unknown
16+
case error(String)
17+
case okay
18+
case paused
19+
case needsAttention(String)
20+
case working(String)
21+
22+
var color: Color {
23+
switch self {
24+
case .okay:
25+
.white
26+
case .paused:
27+
.secondary
28+
case .unknown:
29+
.red
30+
case .error:
31+
.red
32+
case .needsAttention:
33+
.orange
34+
case .working:
35+
.white
36+
}
37+
}
38+
39+
var description: String {
40+
switch self {
41+
case .unknown:
42+
"Unknown"
43+
case let .error(msg):
44+
msg
45+
case .okay:
46+
"OK"
47+
case .paused:
48+
"Paused"
49+
case let .needsAttention(msg):
50+
msg
51+
case let .working(msg):
52+
msg
53+
}
54+
}
55+
56+
var body: some View {
57+
Text(description).foregroundColor(color)
58+
}
59+
}
60+
61+
struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
62+
@EnvironmentObject var vpn: VPN
63+
64+
@State private var selection: FileSyncRow.ID?
65+
@State private var addingNewSession: Bool = false
66+
@State private var items: [FileSyncRow] = []
67+
68+
var body: some View {
69+
Group {
70+
Table(items, selection: $selection) {
71+
TableColumn("Local Path") { row in
72+
Text(row.localPath.path())
73+
}.width(min: 200, ideal: 240)
74+
TableColumn("Workspace", value: \.workspace)
75+
.width(min: 100, ideal: 120)
76+
TableColumn("Remote Path", value: \.remotePath)
77+
.width(min: 100, ideal: 120)
78+
TableColumn("Status") { $0.status.body }
79+
.width(min: 80, ideal: 100)
80+
TableColumn("Size") { item in
81+
Text(item.size)
82+
}
83+
.width(min: 60, ideal: 80)
84+
}
85+
.frame(minWidth: 400, minHeight: 200)
86+
.padding(.bottom, 25)
87+
.overlay(alignment: .bottom) {
88+
VStack(alignment: .leading, spacing: 0) {
89+
Divider()
90+
HStack(spacing: 0) {
91+
Button {
92+
addingNewSession = true
93+
} label: {
94+
Image(systemName: "plus")
95+
.frame(width: 24, height: 24)
96+
}.disabled(vpn.menuState.agents.isEmpty)
97+
Divider()
98+
Button {
99+
// TODO: Remove from list
100+
} label: {
101+
Image(systemName: "minus").frame(width: 24, height: 24)
102+
}.disabled(selection == nil)
103+
if let selection {
104+
if let selectedSession = items.first(where: { $0.id == selection }) {
105+
Divider()
106+
Button {
107+
// TODO: Pause & Unpause
108+
} label: {
109+
switch selectedSession.status {
110+
case .paused:
111+
Image(systemName: "play").frame(width: 24, height: 24)
112+
default:
113+
Image(systemName: "pause").frame(width: 24, height: 24)
114+
}
115+
}
116+
}
117+
}
118+
}
119+
.buttonStyle(.borderless)
120+
}
121+
.background(.primary.opacity(0.04))
122+
.fixedSize(horizontal: false, vertical: true)
123+
}
124+
}.sheet(isPresented: $addingNewSession) {
125+
FileSyncSessionModal<VPN, FS>()
126+
.frame(width: 550)
127+
}.onTapGesture {
128+
selection = nil
129+
}
130+
}
131+
}
132+
133+
#if DEBUG
134+
#Preview {
135+
FileSyncConfig<PreviewVPN, PreviewFileSync>()
136+
.environmentObject(AppState(persistent: false))
137+
.environmentObject(PreviewVPN())
138+
.environmentObject(PreviewFileSync())
139+
}
140+
#endif
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import SwiftUI
2+
import VPNLib
3+
4+
struct FileSyncSessionModal<VPN: VPNService, FS: FileSyncDaemon>: View {
5+
var existingSession: FileSyncRow?
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: UUID?
12+
@State private var remotePath: String = ""
13+
14+
var body: some View {
15+
let agents = vpn.menuState.onlineAgents
16+
VStack(spacing: 0) {
17+
Form {
18+
Section {
19+
HStack {
20+
Text("Local Path")
21+
Text(localPath)
22+
Spacer()
23+
Button {
24+
let panel = NSOpenPanel()
25+
panel.allowsMultipleSelection = false
26+
panel.canChooseDirectories = true
27+
panel.canChooseFiles = false
28+
if panel.runModal() == .OK {
29+
localPath = panel.url?.path(percentEncoded: false) ?? "<none>"
30+
}
31+
} label: {
32+
Image(systemName: "folder")
33+
}
34+
}
35+
}
36+
Section {
37+
Picker("Workspace", selection: $workspace) {
38+
ForEach(agents) { agent in
39+
Text(agent.primaryHost!).tag(agent.id)
40+
}
41+
}
42+
}
43+
Section {
44+
TextField("Remote Path", text: $remotePath)
45+
}
46+
}.formStyle(.grouped).scrollDisabled(true).padding(.horizontal)
47+
Divider()
48+
HStack {
49+
Spacer()
50+
Button("Cancel", action: { dismiss() }).keyboardShortcut(.cancelAction)
51+
Button(existingSession == nil ? "Add" : "Save", action: submit)
52+
.keyboardShortcut(.defaultAction)
53+
}.padding(20)
54+
}.onAppear {
55+
if existingSession != nil {
56+
// TODO: Populate form
57+
} else {
58+
workspace = agents.first?.id
59+
}
60+
}
61+
}
62+
63+
func submit() {
64+
defer {
65+
// TODO: Instruct window to refresh state via gRPC
66+
dismiss()
67+
}
68+
if existingSession != nil {
69+
// TODO: Delete existing
70+
}
71+
// TODO: Insert
72+
}
73+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ struct GeneralTab: View {
1818
Text("Start Coder Connect on launch")
1919
}
2020
}
21+
Section {
22+
Toggle(isOn: $state.showFileSyncUI) {
23+
Text("Show experimental File Sync UI")
24+
}
25+
}
2126
}.formStyle(.grouped)
2227
}
2328
}

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