@@ -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 dontRestart : Bool = false
1416
1517var body : some View {
1618Group {
@@ -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) { sessionin
50+ FileSyncSessionModal < VPN , FS > ( existingSession: session)
51+ . frame ( width: 700 )
52+ } . alert ( " Error " , isPresented: Binding (
53+ get: { deleteError!= nil } ,
54+ set: { isPresentedin
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: { isPresentedin
68+ if !isPresented{
69+ if dontRestart{
70+ dontRestart= false
71+ return
72+ }
73+ loading= true
74+ Task {
75+ await fileSync. tryStart ( )
76+ loading= false
77+ }
78+ }
79+ }
80+ ) ) {
81+ Button ( " Restart " ) { }
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+ dontRestart= 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} ) {
48145Divider ( )
49146Button {
50147Task {
148+ // TODO: Support pausing & resuming multiple sessions at once
51149 loading= true
52150defer { 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) { sessionin
103- FileSyncSessionModal < VPN , FS > ( existingSession: session)
104- . frame ( width: 700 )
105- } . alert ( " Error " , isPresented: Binding (
106- get: { deleteError!= nil } ,
107- set: { isPresentedin
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