@@ -11,6 +11,8 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
11
11
12
12
@State private var loading : Bool = false
13
13
@State private var deleteError : DaemonError ?
14
+ @State private var isVisible : Bool = false
15
+ @State private var dontRetry : Bool = false
14
16
15
17
var body : some View {
16
18
Group {
@@ -36,87 +38,138 @@ struct FileSyncConfig<VPN: VPNService, FS: FileSyncDaemon>: View {
36
38
. frame ( minWidth: 400 , minHeight: 200 )
37
39
. padding ( . bottom, 25 )
38
40
. 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 } ) {
48
145
Divider ( )
49
146
Button {
50
147
Task {
148
+ // TODO: Support pausing & resuming multiple sessions at once
51
149
loading = true
52
150
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] )
62
156
}
63
- selection = nil
64
157
}
65
158
} 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 )
91
164
}
92
165
}
93
166
}
94
- . buttonStyle ( . borderless)
95
167
}
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 ) )
118
168
}
119
- } . disabled ( loading)
169
+ . buttonStyle ( . borderless)
170
+ }
171
+ . background ( . primary. opacity ( 0.04 ) )
172
+ . fixedSize ( horizontal: false , vertical: true )
120
173
}
121
174
}
122
175
0 commit comments