@@ -11,6 +11,8 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
1111
1212 @State private var loading : Bool = false
1313 @State private var deleteError : DaemonError ?
14+ @State private var isVisible : Bool = false
15+ @State private var dontRetry : Bool = false
1416
1517 var body : some View {
1618 Group {
@@ -36,87 +38,140 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
3638 . frame ( minWidth: 400 , minHeight: 200 )
3739 . padding ( . bottom, 25 )
3840 . overlay ( alignment: . bottom) {
39- VStack ( alignment: . leading, spacing: 0 ) {
40- Divider ( )
41- HStack ( spacing: 0 ) {
42- Button {
43- addingNewSession = true
44- } label: {
45- Image ( systemName: " plus " )
46- . frame ( width: 24 , height: 24 )
47- } . disabled ( vpn. menuState. agents. isEmpty)
41+ tableFooter
42+ }
43+ // Only the table & footer should be disabled if the daemon has crashed
44+ // otherwise the alert buttons will be disabled too
45+ } . disabled ( fileSync. state. isFailed)
46+ . sheet ( isPresented: $addingNewSession) {
47+ FileSyncSessionModal < VPN , FS > ( )
48+ . frame ( width: 700 )
49+ } . sheet ( item: $editingSession) { session in
50+ FileSyncSessionModal < VPN , FS > ( existingSession: session)
51+ . frame ( width: 700 )
52+ } . alert ( " Error " , isPresented: Binding (
53+ get: { deleteError != nil } ,
54+ set: { isPresented in
55+ if !isPresented {
56+ deleteError = nil
57+ }
58+ }
59+ ) ) { } message: {
60+ Text ( deleteError? . description ?? " An unknown error occurred. " )
61+ } . alert ( " Error " , isPresented: Binding (
62+ // We only show the alert if the file config window is open
63+ // Users will see the alert symbol on the menu bar to prompt them to
64+ // open it. The requirement on `!loading` prevents the alert from
65+ // re-opening immediately.
66+ get: { !loading && isVisible && fileSync. state. isFailed } ,
67+ set: { isPresented in
68+ if !isPresented {
69+ if dontRetry {
70+ dontRetry = false
71+ return
72+ }
73+ loading = true
74+ Task {
75+ await fileSync. tryStart ( )
76+ loading = false
77+ }
78+ }
79+ }
80+ ) ) {
81+ Button ( " Retry " ) { }
82+ // This gives the user an out if the daemon is crashing on launch,
83+ // they can cancel the alert, and it will reappear if they re-open the
84+ // file sync window.
85+ Button ( " Cancel " , role: . cancel) {
86+ dontRetry = true
87+ }
88+ } message: {
89+ Text ( """
90+ File sync daemon failed. The daemon log file at \n \( fileSync. logFile. path) \n has been opened.
91+ """ ) . onAppear {
92+ // Open the log file in the default editor
93+ NSWorkspace . shared. open ( fileSync. logFile)
94+ }
95+ } . task {
96+ // When the Window is visible, poll for session updates every
97+ // two seconds.
98+ while !Task. isCancelled {
99+ if !fileSync. state. isFailed {
100+ await fileSync. refreshSessions ( )
101+ }
102+ try ? await Task . sleep ( for: . seconds( 2 ) )
103+ }
104+ } . onAppear {
105+ isVisible = true
106+ } . onDisappear {
107+ isVisible = false
108+ // If the failure alert is dismissed without restarting the daemon,
109+ // (by clicking cancel) this makes it clear that the daemon
110+ // is still in a failed state.
111+ } . navigationTitle ( " Coder File Sync \( fileSync. state. isFailed ? " - Failed " : " " ) " )
112+ . disabled ( loading)
113+ }
114+
115+ var tableFooter : some View {
116+ VStack ( alignment: . leading, spacing: 0 ) {
117+ Divider ( )
118+ HStack ( spacing: 0 ) {
119+ Button {
120+ addingNewSession = true
121+ } label: {
122+ Image ( systemName: " plus " )
123+ . frame ( width: 24 , height: 24 )
124+ } . disabled ( vpn. menuState. agents. isEmpty)
125+ Divider ( )
126+ Button {
127+ Task {
128+ loading = true
129+ defer { loading = false }
130+ do throws ( DaemonError) {
131+ // TODO: Support selecting & deleting multiple sessions at once
132+ try await fileSync. deleteSessions ( ids: [ selection!] )
133+ if fileSync. sessionState. isEmpty {
134+ // Last session was deleted, stop the daemon
135+ await fileSync. stop ( )
136+ }
137+ } catch {
138+ deleteError = error
139+ }
140+ selection = nil
141+ }
142+ } label: {
143+ Image ( systemName: " minus " ) . frame ( width: 24 , height: 24 )
144+ } . disabled ( selection == nil )
145+ if let selection {
146+ if let selectedSession = fileSync. sessionState. first ( where: { $0. id == selection } ) {
48147 Divider ( )
49148 Button {
50149 Task {
150+ // TODO: Support pausing & resuming multiple sessions at once
51151 loading = true
52152 defer { loading = false }
53- do throws ( DaemonError) {
54- // TODO: Support selecting & deleting multiple sessions at once
55- try await fileSync. deleteSessions ( ids: [ selection!] )
56- if fileSync. sessionState. isEmpty {
57- // Last session was deleted, stop the daemon
58- await fileSync. stop ( )
59- }
60- } catch {
61- deleteError = error
153+ switch selectedSession. status {
154+ case . paused:
155+ try await fileSync. resumeSessions ( ids: [ selectedSession. id] )
156+ default :
157+ try await fileSync. pauseSessions ( ids: [ selectedSession. id] )
62158 }
63- selection = nil
64159 }
65160 } label: {
66- Image ( systemName: " minus " ) . frame ( width: 24 , height: 24 )
67- } . disabled ( selection == nil )
68- if let selection {
69- if let selectedSession = fileSync. sessionState. first ( where: { $0. id == selection } ) {
70- Divider ( )
71- Button {
72- Task {
73- // TODO: Support pausing & resuming multiple sessions at once
74- loading = true
75- defer { loading = false }
76- switch selectedSession. status {
77- case . paused:
78- try await fileSync. resumeSessions ( ids: [ selectedSession. id] )
79- default :
80- try await fileSync. pauseSessions ( ids: [ selectedSession. id] )
81- }
82- }
83- } label: {
84- switch selectedSession. status {
85- case . paused:
86- Image ( systemName: " play " ) . frame ( width: 24 , height: 24 )
87- default :
88- Image ( systemName: " pause " ) . frame ( width: 24 , height: 24 )
89- }
90- }
161+ switch selectedSession. status {
162+ case . paused:
163+ Image ( systemName: " play " ) . frame ( width: 24 , height: 24 )
164+ default :
165+ Image ( systemName: " pause " ) . frame ( width: 24 , height: 24 )
91166 }
92167 }
93168 }
94- . buttonStyle ( . borderless)
95169 }
96- . background ( . primary. opacity ( 0.04 ) )
97- . fixedSize ( horizontal: false , vertical: true )
98- }
99- } . sheet ( isPresented: $addingNewSession) {
100- FileSyncSessionModal < VPN , FS > ( )
101- . frame ( width: 700 )
102- } . sheet ( item: $editingSession) { session in
103- FileSyncSessionModal < VPN , FS > ( existingSession: session)
104- . frame ( width: 700 )
105- } . alert ( " Error " , isPresented: Binding (
106- get: { deleteError != nil } ,
107- set: { isPresented in
108- if !isPresented {
109- deleteError = nil
110- }
111- }
112- ) ) { } message: {
113- Text ( deleteError? . description ?? " An unknown error occurred. " )
114- } . task {
115- while !Task. isCancelled {
116- await fileSync. refreshSessions ( )
117- try ? await Task . sleep ( for: . seconds( 2 ) )
118170 }
119- } . disabled ( loading)
171+ . buttonStyle ( . borderless)
172+ }
173+ . background ( . primary. opacity ( 0.04 ) )
174+ . fixedSize ( horizontal: false , vertical: true )
120175 }
121176}
122177
0 commit comments