disconnected_overlay.rs

  1use std::path::PathBuf;
  2
  3use dev_server_projects::DevServer;
  4use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
  5use remote::SshConnectionOptions;
  6use settings::Settings;
  7use ui::{
  8    div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
  9    Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
 10    ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, ViewContext,
 11};
 12use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
 13
 14use crate::{
 15    open_dev_server_project, open_ssh_project, remote_servers::reconnect_to_dev_server_project,
 16    RemoteServerProjects, SshSettings,
 17};
 18
 19enum Host {
 20    RemoteProject,
 21    DevServerProject(DevServer),
 22    SshRemoteProject(SshConnectionOptions),
 23}
 24
 25pub struct DisconnectedOverlay {
 26    workspace: WeakView<Workspace>,
 27    host: Host,
 28    focus_handle: FocusHandle,
 29}
 30
 31impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
 32impl FocusableView for DisconnectedOverlay {
 33    fn focus_handle(&self, _cx: &gpui::AppContext) -> gpui::FocusHandle {
 34        self.focus_handle.clone()
 35    }
 36}
 37impl ModalView for DisconnectedOverlay {
 38    fn fade_out_background(&self) -> bool {
 39        true
 40    }
 41}
 42
 43impl DisconnectedOverlay {
 44    pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 45        cx.subscribe(workspace.project(), |workspace, project, event, cx| {
 46            if !matches!(
 47                event,
 48                project::Event::DisconnectedFromHost | project::Event::DisconnectedFromSshRemote
 49            ) {
 50                return;
 51            }
 52            let handle = cx.view().downgrade();
 53            let dev_server = project
 54                .read(cx)
 55                .dev_server_project_id()
 56                .and_then(|id| {
 57                    dev_server_projects::Store::global(cx)
 58                        .read(cx)
 59                        .dev_server_for_project(id)
 60                })
 61                .cloned();
 62
 63            let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
 64            let host = if let Some(dev_server) = dev_server {
 65                Host::DevServerProject(dev_server)
 66            } else if let Some(ssh_connection_options) = ssh_connection_options {
 67                Host::SshRemoteProject(ssh_connection_options)
 68            } else {
 69                Host::RemoteProject
 70            };
 71
 72            workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
 73                workspace: handle,
 74                host,
 75                focus_handle: cx.focus_handle(),
 76            });
 77        })
 78        .detach();
 79    }
 80
 81    fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
 82        cx.emit(DismissEvent);
 83
 84        match &self.host {
 85            Host::DevServerProject(dev_server) => {
 86                self.reconnect_to_dev_server(dev_server.clone(), cx);
 87            }
 88            Host::SshRemoteProject(ssh_connection_options) => {
 89                self.reconnect_to_ssh_remote(ssh_connection_options.clone(), cx);
 90            }
 91            _ => {}
 92        }
 93    }
 94
 95    fn reconnect_to_dev_server(&self, dev_server: DevServer, cx: &mut ViewContext<Self>) {
 96        let Some(workspace) = self.workspace.upgrade() else {
 97            return;
 98        };
 99        let Some(dev_server_project_id) = workspace
100            .read(cx)
101            .project()
102            .read(cx)
103            .dev_server_project_id()
104        else {
105            return;
106        };
107
108        if let Some(project_id) = dev_server_projects::Store::global(cx)
109            .read(cx)
110            .dev_server_project(dev_server_project_id)
111            .and_then(|project| project.project_id)
112        {
113            return workspace.update(cx, move |_, cx| {
114                open_dev_server_project(true, dev_server_project_id, project_id, cx)
115                    .detach_and_prompt_err("Failed to reconnect", cx, |_, _| None)
116            });
117        }
118
119        if dev_server.ssh_connection_string.is_some() {
120            let task = workspace.update(cx, |_, cx| {
121                reconnect_to_dev_server_project(
122                    cx.view().clone(),
123                    dev_server,
124                    dev_server_project_id,
125                    true,
126                    cx,
127                )
128            });
129
130            task.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
131        } else {
132            return workspace.update(cx, |workspace, cx| {
133                let handle = cx.view().downgrade();
134                workspace.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, handle))
135            });
136        }
137    }
138
139    fn reconnect_to_ssh_remote(
140        &self,
141        connection_options: SshConnectionOptions,
142        cx: &mut ViewContext<Self>,
143    ) {
144        let Some(workspace) = self.workspace.upgrade() else {
145            return;
146        };
147
148        let Some(ssh_project) = workspace.read(cx).serialized_ssh_project() else {
149            return;
150        };
151
152        let Some(window) = cx.window_handle().downcast::<Workspace>() else {
153            return;
154        };
155
156        let app_state = workspace.read(cx).app_state().clone();
157
158        let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
159
160        cx.spawn(move |_, mut cx| async move {
161            let nickname = cx
162                .update(|cx| {
163                    SshSettings::get_global(cx).nickname_for(
164                        &connection_options.host,
165                        connection_options.port,
166                        &connection_options.username,
167                    )
168                })
169                .ok()
170                .flatten();
171            open_ssh_project(
172                connection_options,
173                paths,
174                app_state,
175                OpenOptions {
176                    replace_window: Some(window),
177                    ..Default::default()
178                },
179                nickname,
180                &mut cx,
181            )
182            .await?;
183            Ok(())
184        })
185        .detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
186    }
187
188    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
189        cx.emit(DismissEvent)
190    }
191}
192
193impl Render for DisconnectedOverlay {
194    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
195        let can_reconnect = matches!(
196            self.host,
197            Host::DevServerProject(_) | Host::SshRemoteProject(_)
198        );
199
200        let message = match &self.host {
201            Host::RemoteProject | Host::DevServerProject(_) => {
202                "Your connection to the remote project has been lost.".to_string()
203            }
204            Host::SshRemoteProject(options) => {
205                format!(
206                    "Your connection to {} has been lost",
207                    options.connection_string()
208                )
209            }
210        };
211
212        div()
213            .track_focus(&self.focus_handle)
214            .elevation_3(cx)
215            .on_action(cx.listener(Self::cancel))
216            .occlude()
217            .w(rems(24.))
218            .max_h(rems(40.))
219            .child(
220                Modal::new("disconnected", None)
221                    .header(
222                        ModalHeader::new()
223                            .show_dismiss_button(true)
224                            .child(Headline::new("Disconnected").size(HeadlineSize::Small)),
225                    )
226                    .section(Section::new().child(Label::new(message)))
227                    .footer(
228                        ModalFooter::new().end_slot(
229                            h_flex()
230                                .gap_2()
231                                .child(
232                                    Button::new("close-window", "Close Window")
233                                        .style(ButtonStyle::Filled)
234                                        .layer(ElevationIndex::ModalSurface)
235                                        .on_click(cx.listener(move |_, _, cx| {
236                                            cx.remove_window();
237                                        })),
238                                )
239                                .when(can_reconnect, |el| {
240                                    el.child(
241                                        Button::new("reconnect", "Reconnect")
242                                            .style(ButtonStyle::Filled)
243                                            .layer(ElevationIndex::ModalSurface)
244                                            .icon(IconName::ArrowCircle)
245                                            .icon_position(IconPosition::Start)
246                                            .on_click(cx.listener(Self::handle_reconnect)),
247                                    )
248                                }),
249                        ),
250                    ),
251            )
252    }
253}