@@ -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,138 @@ 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+ // You can't have styled text in alert messages
90+ Text ( """
91+ File sync daemon failed: \( fileSync. state. description) \n \n \( fileSync. recentLogs. joined ( separator: " \n " ) )
92+ """ )
93+ } . task {
94+ // When the Window is visible, poll for session updates every
95+ // two seconds.
96+ while !Task. isCancelled {
97+ if !fileSync. state. isFailed {
98+ await fileSync. refreshSessions ( )
99+ }
100+ try ? await Task . sleep ( for: . seconds( 2 ) )
101+ }
102+ } . onAppear {
103+ isVisible = true
104+ } . onDisappear {
105+ isVisible = false
106+ // If the failure alert is dismissed without restarting the daemon,
107+ // (by clicking cancel) this makes it clear that the daemon
108+ // is still in a failed state.
109+ } . navigationTitle ( " Coder File Sync \( fileSync. state. isFailed ? " - Failed " : " " ) " )
110+ . disabled ( loading)
111+ }
112+
113+ var tableFooter : some View {
114+ VStack ( alignment: . leading, spacing: 0 ) {
115+ Divider ( )
116+ HStack ( spacing: 0 ) {
117+ Button {
118+ addingNewSession = true
119+ } label: {
120+ Image ( systemName: " plus " )
121+ . frame ( width: 24 , height: 24 )
122+ } . disabled ( vpn. menuState. agents. isEmpty)
123+ Divider ( )
124+ Button {
125+ Task {
126+ loading = true
127+ defer { loading = false }
128+ do throws ( DaemonError) {
129+ // TODO: Support selecting & deleting multiple sessions at once
130+ try await fileSync. deleteSessions ( ids: [ selection!] )
131+ if fileSync. sessionState. isEmpty {
132+ // Last session was deleted, stop the daemon
133+ await fileSync. stop ( )
134+ }
135+ } catch {
136+ deleteError = error
137+ }
138+ selection = nil
139+ }
140+ } label: {
141+ Image ( systemName: " minus " ) . frame ( width: 24 , height: 24 )
142+ } . disabled ( selection == nil )
143+ if let selection {
144+ if let selectedSession = fileSync. sessionState. first ( where: { $0. id == selection } ) {
48145 Divider ( )
49146 Button {
50147 Task {
148+ // TODO: Support pausing & resuming multiple sessions at once
51149 loading = true
52150 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
151+ switch selectedSession. status {
152+ case . paused:
153+ try await fileSync. resumeSessions ( ids: [ selectedSession. id] )
154+ default :
155+ try await fileSync. pauseSessions ( ids: [ selectedSession. id] )
62156 }
63- selection = nil
64157 }
65158 } 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- }
159+ switch selectedSession. status {
160+ case . paused:
161+ Image ( systemName: " play " ) . frame ( width: 24 , height: 24 )
162+ default :
163+ Image ( systemName: " pause " ) . frame ( width: 24 , height: 24 )
91164 }
92165 }
93166 }
94- . buttonStyle ( . borderless)
95167 }
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 ) )
118168 }
119- } . disabled ( loading)
169+ . buttonStyle ( . borderless)
170+ }
171+ . background ( . primary. opacity ( 0.04 ) )
172+ . fixedSize ( horizontal: false , vertical: true )
120173 }
121174}
122175
0 commit comments