dev_servers.rs

  1use std::time::Duration;
  2
  3use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
  4use editor::Editor;
  5use feature_flags::FeatureFlagAppExt;
  6use feature_flags::FeatureFlagViewExt;
  7use gpui::{
  8    percentage, Action, Animation, AnimationExt, AnyElement, AppContext, ClipboardItem,
  9    DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation,
 10    View, ViewContext,
 11};
 12use rpc::{
 13    proto::{CreateDevServerResponse, DevServerStatus},
 14    ErrorCode, ErrorExt,
 15};
 16use settings::Settings;
 17use theme::ThemeSettings;
 18use ui::{prelude::*, Indicator, List, ListHeader, ListItem, ModalContent, ModalHeader, Tooltip};
 19use ui_text_field::{FieldLabelLayout, TextField};
 20use util::ResultExt;
 21use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
 22
 23use crate::OpenRemote;
 24
 25pub struct DevServerProjects {
 26    mode: Mode,
 27    focus_handle: FocusHandle,
 28    scroll_handle: ScrollHandle,
 29    dev_server_store: Model<dev_server_projects::Store>,
 30    project_path_input: View<Editor>,
 31    dev_server_name_input: View<TextField>,
 32    _subscription: gpui::Subscription,
 33}
 34
 35#[derive(Default)]
 36struct CreateDevServer {
 37    creating: bool,
 38    dev_server: Option<CreateDevServerResponse>,
 39}
 40
 41#[derive(Clone)]
 42struct CreateDevServerProject {
 43    dev_server_id: DevServerId,
 44    creating: bool,
 45}
 46
 47enum Mode {
 48    Default(Option<CreateDevServerProject>),
 49    CreateDevServer(CreateDevServer),
 50}
 51
 52impl DevServerProjects {
 53    pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
 54        cx.observe_flag::<feature_flags::Remoting, _>(|enabled, workspace, _| {
 55            if enabled {
 56                Self::register_open_remote_action(workspace);
 57            }
 58        })
 59        .detach();
 60
 61        if cx.has_flag::<feature_flags::Remoting>() {
 62            Self::register_open_remote_action(workspace);
 63        }
 64    }
 65
 66    fn register_open_remote_action(workspace: &mut Workspace) {
 67        workspace.register_action(|workspace, _: &OpenRemote, cx| {
 68            workspace.toggle_modal(cx, |cx| Self::new(cx))
 69        });
 70    }
 71
 72    pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {
 73        workspace.update(cx, |workspace, cx| {
 74            workspace.toggle_modal(cx, |cx| Self::new(cx))
 75        })
 76    }
 77
 78    pub fn new(cx: &mut ViewContext<Self>) -> Self {
 79        let project_path_input = cx.new_view(|cx| {
 80            let mut editor = Editor::single_line(cx);
 81            editor.set_placeholder_text("Project path", cx);
 82            editor
 83        });
 84        let dev_server_name_input =
 85            cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked));
 86
 87        let focus_handle = cx.focus_handle();
 88        let dev_server_store = dev_server_projects::Store::global(cx);
 89
 90        let subscription = cx.observe(&dev_server_store, |_, _, cx| {
 91            cx.notify();
 92        });
 93
 94        Self {
 95            mode: Mode::Default(None),
 96            focus_handle,
 97            scroll_handle: ScrollHandle::new(),
 98            dev_server_store,
 99            project_path_input,
100            dev_server_name_input,
101            _subscription: subscription,
102        }
103    }
104
105    pub fn create_dev_server_project(
106        &mut self,
107        dev_server_id: DevServerId,
108        cx: &mut ViewContext<Self>,
109    ) {
110        let path = self.project_path_input.read(cx).text(cx).trim().to_string();
111
112        if path == "" {
113            return;
114        }
115
116        if self
117            .dev_server_store
118            .read(cx)
119            .projects_for_server(dev_server_id)
120            .iter()
121            .any(|p| p.path == path)
122        {
123            cx.spawn(|_, mut cx| async move {
124                cx.prompt(
125                    gpui::PromptLevel::Critical,
126                    "Failed to create project",
127                    Some(&format!(
128                        "Project {} already exists for this dev server.",
129                        path
130                    )),
131                    &["Ok"],
132                )
133                .await
134            })
135            .detach_and_log_err(cx);
136            return;
137        }
138
139        let create = {
140            let path = path.clone();
141            self.dev_server_store.update(cx, |store, cx| {
142                store.create_dev_server_project(dev_server_id, path, cx)
143            })
144        };
145
146        cx.spawn(|this, mut cx| async move {
147            let result = create.await;
148            this.update(&mut cx, |this, cx| {
149                if result.is_ok() {
150                    this.project_path_input.update(cx, |editor, cx| {
151                        editor.set_text("", cx);
152                    });
153                    this.mode = Mode::Default(None);
154                } else {
155                    this.mode = Mode::Default(Some(CreateDevServerProject {
156                        dev_server_id,
157                        creating: false,
158                    }));
159                }
160            })
161            .log_err();
162            result
163        })
164        .detach_and_prompt_err("Failed to create project", cx, move |e, _| {
165            match e.error_code() {
166                ErrorCode::DevServerOffline => Some(
167                    "The dev server is offline. Please log in and check it is connected."
168                        .to_string(),
169                ),
170                ErrorCode::DevServerProjectPathDoesNotExist => {
171                    Some(format!("The path `{}` does not exist on the server.", path))
172                }
173                _ => None,
174            }
175        });
176
177        self.mode = Mode::Default(Some(CreateDevServerProject {
178            dev_server_id,
179            creating: true,
180        }));
181    }
182
183    pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
184        let name = self
185            .dev_server_name_input
186            .read(cx)
187            .editor()
188            .read(cx)
189            .text(cx)
190            .trim()
191            .to_string();
192
193        if name == "" {
194            return;
195        }
196
197        let dev_server = self
198            .dev_server_store
199            .update(cx, |store, cx| store.create_dev_server(name.clone(), cx));
200
201        cx.spawn(|this, mut cx| async move {
202            let result = dev_server.await;
203
204            this.update(&mut cx, |this, cx| match &result {
205                Ok(dev_server) => {
206                    this.focus_handle.focus(cx);
207                    this.mode = Mode::CreateDevServer(CreateDevServer {
208                        creating: false,
209                        dev_server: Some(dev_server.clone()),
210                    });
211                }
212                Err(_) => {
213                    this.mode = Mode::CreateDevServer(Default::default());
214                }
215            })
216            .log_err();
217            result
218        })
219        .detach_and_prompt_err("Failed to create server", cx, |_, _| None);
220
221        self.mode = Mode::CreateDevServer(CreateDevServer {
222            creating: true,
223            dev_server: None,
224        });
225        cx.notify()
226    }
227
228    fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
229        let answer = cx.prompt(
230            gpui::PromptLevel::Destructive,
231            "Are you sure?",
232            Some("This will delete the dev server and all of its remote projects."),
233            &["Delete", "Cancel"],
234        );
235
236        cx.spawn(|this, mut cx| async move {
237            let answer = answer.await?;
238
239            if answer != 0 {
240                return Ok(());
241            }
242
243            let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
244                this.dev_server_store.update(cx, |store, _| {
245                    store
246                        .projects_for_server(id)
247                        .into_iter()
248                        .map(|project| project.id)
249                        .collect()
250                })
251            })?;
252
253            this.update(&mut cx, |this, cx| {
254                this.dev_server_store
255                    .update(cx, |store, cx| store.delete_dev_server(id, cx))
256            })?
257            .await?;
258
259            for id in project_ids {
260                WORKSPACE_DB
261                    .delete_workspace_by_dev_server_project_id(id)
262                    .await
263                    .log_err();
264            }
265            Ok(())
266        })
267        .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
268    }
269
270    fn delete_dev_server_project(
271        &mut self,
272        id: DevServerProjectId,
273        path: &str,
274        cx: &mut ViewContext<Self>,
275    ) {
276        let answer = cx.prompt(
277            gpui::PromptLevel::Destructive,
278            format!("Delete \"{}\"?", path).as_str(),
279            Some("This will delete the remote project. You can always re-add it later."),
280            &["Delete", "Cancel"],
281        );
282
283        cx.spawn(|this, mut cx| async move {
284            let answer = answer.await?;
285
286            if answer != 0 {
287                return Ok(());
288            }
289
290            this.update(&mut cx, |this, cx| {
291                this.dev_server_store
292                    .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
293            })?
294            .await?;
295
296            WORKSPACE_DB
297                .delete_workspace_by_dev_server_project_id(id)
298                .await
299                .log_err();
300
301            Ok(())
302        })
303        .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
304    }
305
306    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
307        match &self.mode {
308            Mode::Default(None) => {}
309            Mode::Default(Some(create_project)) => {
310                self.create_dev_server_project(create_project.dev_server_id, cx);
311            }
312            Mode::CreateDevServer(state) => {
313                if !state.creating && state.dev_server.is_none() {
314                    self.create_dev_server(cx);
315                }
316            }
317        }
318    }
319
320    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
321        match self.mode {
322            Mode::Default(None) => cx.emit(DismissEvent),
323            _ => {
324                self.mode = Mode::Default(None);
325                self.focus_handle(cx).focus(cx);
326                cx.notify();
327            }
328        }
329    }
330
331    fn render_dev_server(
332        &mut self,
333        dev_server: &DevServer,
334        mut create_project: Option<CreateDevServerProject>,
335        cx: &mut ViewContext<Self>,
336    ) -> impl IntoElement {
337        let dev_server_id = dev_server.id;
338        let status = dev_server.status;
339        if create_project
340            .as_ref()
341            .is_some_and(|cp| cp.dev_server_id != dev_server.id)
342        {
343            create_project = None;
344        }
345
346        v_flex()
347            .w_full()
348            .child(
349                h_flex()
350                    .group("dev-server")
351                    .justify_between()
352                    .child(
353                        h_flex()
354                            .gap_2()
355                            .child(
356                                div()
357                                    .id(("status", dev_server.id.0))
358                                    .relative()
359                                    .child(Icon::new(IconName::Server).size(IconSize::Small))
360                                    .child(
361                                        div().absolute().bottom_0().left(rems_from_px(8.0)).child(
362                                            Indicator::dot().color(match status {
363                                                DevServerStatus::Online => Color::Created,
364                                                DevServerStatus::Offline => Color::Hidden,
365                                            }),
366                                        ),
367                                    )
368                                    .tooltip(move |cx| {
369                                        Tooltip::text(
370                                            match status {
371                                                DevServerStatus::Online => "Online",
372                                                DevServerStatus::Offline => "Offline",
373                                            },
374                                            cx,
375                                        )
376                                    }),
377                            )
378                            .child(dev_server.name.clone())
379                            .child(
380                                h_flex()
381                                    .visible_on_hover("dev-server")
382                                    .gap_1()
383                                    .child(
384                                        IconButton::new("edit-dev-server", IconName::Pencil)
385                                            .disabled(true) //TODO implement this on the collab side
386                                            .tooltip(|cx| {
387                                                Tooltip::text("Coming Soon - Edit dev server", cx)
388                                            }),
389                                    )
390                                    .child({
391                                        let dev_server_id = dev_server.id;
392                                        IconButton::new("remove-dev-server", IconName::Trash)
393                                            .on_click(cx.listener(move |this, _, cx| {
394                                                this.delete_dev_server(dev_server_id, cx)
395                                            }))
396                                            .tooltip(|cx| Tooltip::text("Remove dev server", cx))
397                                    }),
398                            ),
399                    )
400                    .child(
401                        h_flex().gap_1().child(
402                            IconButton::new(
403                                ("add-remote-project", dev_server_id.0),
404                                IconName::Plus,
405                            )
406                            .tooltip(|cx| Tooltip::text("Add a remote project", cx))
407                            .on_click(cx.listener(
408                                move |this, _, cx| {
409                                    if let Mode::Default(project) = &mut this.mode {
410                                        *project = Some(CreateDevServerProject {
411                                            dev_server_id,
412                                            creating: false,
413                                        });
414                                    }
415                                    this.project_path_input.read(cx).focus_handle(cx).focus(cx);
416                                    cx.notify();
417                                },
418                            )),
419                        ),
420                    ),
421            )
422            .child(
423                v_flex()
424                    .w_full()
425                    .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
426                    .border()
427                    .border_color(cx.theme().colors().border_variant)
428                    .rounded_md()
429                    .my_1()
430                    .py_0p5()
431                    .px_3()
432                    .child(
433                        List::new()
434                            .empty_message("No projects.")
435                            .children(
436                                self.dev_server_store
437                                    .read(cx)
438                                    .projects_for_server(dev_server.id)
439                                    .iter()
440                                    .map(|p| self.render_dev_server_project(p, cx)),
441                            )
442                            .when_some(create_project, |el, create_project| {
443                                el.child(self.render_create_new_project(&create_project, cx))
444                            }),
445                    ),
446            )
447    }
448
449    fn render_create_new_project(
450        &mut self,
451        create_project: &CreateDevServerProject,
452        _: &mut ViewContext<Self>,
453    ) -> impl IntoElement {
454        ListItem::new("create-remote-project")
455            .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
456            .child(self.project_path_input.clone())
457            .child(
458                div()
459                    .w(IconSize::Medium.rems())
460                    .when(create_project.creating, |el| {
461                        el.child(
462                            Icon::new(IconName::ArrowCircle)
463                                .size(IconSize::Medium)
464                                .with_animation(
465                                    "arrow-circle",
466                                    Animation::new(Duration::from_secs(2)).repeat(),
467                                    |icon, delta| {
468                                        icon.transform(Transformation::rotate(percentage(delta)))
469                                    },
470                                ),
471                        )
472                    }),
473            )
474    }
475
476    fn render_dev_server_project(
477        &mut self,
478        project: &DevServerProject,
479        cx: &mut ViewContext<Self>,
480    ) -> impl IntoElement {
481        let dev_server_project_id = project.id;
482        let project_id = project.project_id;
483        let is_online = project_id.is_some();
484        let project_path = project.path.clone();
485
486        ListItem::new(("remote-project", dev_server_project_id.0))
487            .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
488            .child(
489                    Label::new(project.path.clone())
490            )
491            .on_click(cx.listener(move |_, _, cx| {
492                if let Some(project_id) = project_id {
493                    if let Some(app_state) = AppState::global(cx).upgrade() {
494                        workspace::join_dev_server_project(project_id, app_state, None, cx)
495                            .detach_and_prompt_err("Could not join project", cx, |_, _| None)
496                    }
497                } else {
498                    cx.spawn(|_, mut cx| async move {
499                        cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err();
500                    }).detach();
501                }
502            }))
503            .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
504                .on_click(cx.listener(move |this, _, cx| {
505                    this.delete_dev_server_project(dev_server_project_id, &project_path, cx)
506                }))
507                .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
508    }
509
510    fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
511        let Mode::CreateDevServer(CreateDevServer {
512            creating,
513            dev_server,
514        }) = &self.mode
515        else {
516            unreachable!()
517        };
518
519        self.dev_server_name_input.update(cx, |input, cx| {
520            input.set_disabled(*creating || dev_server.is_some(), cx);
521        });
522
523        v_flex()
524            .id("scroll-container")
525            .h_full()
526            .overflow_y_scroll()
527            .track_scroll(&self.scroll_handle)
528            .px_1()
529            .pt_0p5()
530            .gap_px()
531            .child(
532                ModalHeader::new("remote-projects")
533                    .show_back_button(true)
534                    .child(Headline::new("New dev server").size(HeadlineSize::Small)),
535            )
536            .child(
537                ModalContent::new().child(
538                    v_flex()
539                        .w_full()
540                        .child(
541                            h_flex()
542                                .pb_2()
543                                .items_end()
544                                .w_full()
545                                .px_2()
546                                .border_b_1()
547                                .border_color(cx.theme().colors().border)
548                                .child(
549                                    div()
550                                        .pl_2()
551                                        .max_w(rems(16.))
552                                        .child(self.dev_server_name_input.clone()),
553                                )
554                                .child(
555                                    div()
556                                        .pl_1()
557                                        .pb(px(3.))
558                                        .when(!*creating && dev_server.is_none(), |div| {
559                                            div.child(Button::new("create-dev-server", "Create").on_click(
560                                                cx.listener(move |this, _, cx| {
561                                                    this.create_dev_server(cx);
562                                                }),
563                                            ))
564                                        })
565                                        .when(*creating && dev_server.is_none(), |div| {
566                                            div.child(
567                                                Button::new("create-dev-server", "Creating...")
568                                                    .disabled(true),
569                                            )
570                                        }),
571                                )
572                        )
573                        .when(dev_server.is_none(), |div| {
574                            div.px_2().child(Label::new("Once you have created a dev server, you will be given a command to run on the server to register it.").color(Color::Muted))
575                        })
576                        .when_some(dev_server.clone(), |div, dev_server| {
577                            let status = self
578                                .dev_server_store
579                                .read(cx)
580                                .dev_server_status(DevServerId(dev_server.dev_server_id));
581
582                            let instructions = SharedString::from(format!(
583                                "zed --dev-server-token {}",
584                                dev_server.access_token
585                            ));
586                            div.child(
587                                v_flex()
588                                    .pl_2()
589                                    .pt_2()
590                                    .gap_2()
591                                    .child(
592                                        h_flex().justify_between().w_full()
593                                            .child(Label::new(format!(
594                                                    "Please log into `{}` and run:",
595                                                    dev_server.name
596                                            )))
597                                            .child(
598                                                Button::new("copy-access-token", "Copy Instructions")
599                                                    .icon(Some(IconName::Copy))
600                                                    .icon_size(IconSize::Small)
601                                                    .on_click({
602                                                        let instructions = instructions.clone();
603                                                        cx.listener(move |_, _, cx| {
604                                                        cx.write_to_clipboard(ClipboardItem::new(
605                                                            instructions.to_string(),
606                                                        ))
607                                                    })})
608                                            )
609                                    )
610                                    .child(
611                                        v_flex()
612                                        .w_full()
613                                        .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct
614                                        .border()
615                                        .border_color(cx.theme().colors().border_variant)
616                                        .rounded_md()
617                                        .my_1()
618                                        .py_0p5()
619                                        .px_3()
620                                        .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone())
621                                        .child(Label::new(instructions))
622                                    )
623                                    .when(status == DevServerStatus::Offline, |this| {
624                                        this.child(
625
626                                        h_flex()
627                                            .gap_2()
628                                            .child(
629                                                Icon::new(IconName::ArrowCircle)
630                                                    .size(IconSize::Medium)
631                                                    .with_animation(
632                                                        "arrow-circle",
633                                                        Animation::new(Duration::from_secs(2)).repeat(),
634                                                        |icon, delta| {
635                                                            icon.transform(Transformation::rotate(percentage(delta)))
636                                                        },
637                                                    ),
638                                            )
639                                            .child(
640                                                Label::new("Waiting for connection…"),
641                                            )
642                                        )
643                                    })
644                                    .when(status == DevServerStatus::Online, |this| {
645                                        this.child(Label::new("🎊 Connection established!"))
646                                            .child(
647                                                h_flex().justify_end().child(
648                                                    Button::new("done", "Done").on_click(cx.listener(
649                                                        |_, _, cx| {
650                                                            cx.dispatch_action(menu::Cancel.boxed_clone())
651                                                        },
652                                                    ))
653                                                ),
654                                            )
655                                    }),
656                            )
657                        }),
658                )
659            )
660    }
661
662    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
663        let dev_servers = self.dev_server_store.read(cx).dev_servers();
664
665        let Mode::Default(create_dev_server_project) = &self.mode else {
666            unreachable!()
667        };
668        let create_dev_server_project = create_dev_server_project.clone();
669
670        v_flex()
671            .id("scroll-container")
672            .h_full()
673            .overflow_y_scroll()
674            .track_scroll(&self.scroll_handle)
675            .px_1()
676            .pt_0p5()
677            .gap_px()
678            .child(
679                ModalHeader::new("remote-projects")
680                    .show_dismiss_button(true)
681                    .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
682            )
683            .child(
684                ModalContent::new().child(
685                    List::new()
686                        .empty_message("No dev servers registered.")
687                        .header(Some(
688                            ListHeader::new("Dev Servers").end_slot(
689                                Button::new("register-dev-server-button", "New Server")
690                                    .icon(IconName::Plus)
691                                    .icon_position(IconPosition::Start)
692                                    .tooltip(|cx| Tooltip::text("Register a new dev server", cx))
693                                    .on_click(cx.listener(|this, _, cx| {
694                                        this.mode = Mode::CreateDevServer(Default::default());
695
696                                        this.dev_server_name_input.update(cx, |input, cx| {
697                                            input.editor().update(cx, |editor, cx| {
698                                                editor.set_text("", cx);
699                                            });
700                                            input.focus_handle(cx).focus(cx)
701                                        });
702
703                                        cx.notify();
704                                    })),
705                            ),
706                        ))
707                        .children(dev_servers.iter().map(|dev_server| {
708                            self.render_dev_server(
709                                dev_server,
710                                create_dev_server_project.clone(),
711                                cx,
712                            )
713                            .into_any_element()
714                        })),
715                ),
716            )
717    }
718
719    // fn render_create_dev_server_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
720    //     let Mode::CreateDevServerProject(CreateDevServerProject {
721    //         dev_server_id,
722    //         creating,
723    //         dev_server_project,
724    //     }) = &self.mode
725    //     else {
726    //         unreachable!()
727    //     };
728
729    //     let dev_server = self
730    //         .dev_server_store
731    //         .read(cx)
732    //         .dev_server(*dev_server_id)
733    //         .cloned();
734
735    //     let (dev_server_name, dev_server_status) = dev_server
736    //         .map(|server| (server.name, server.status))
737    //         .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
738
739    //     v_flex()
740    //         .px_1()
741    //         .pt_0p5()
742    //         .gap_px()
743    //         .child(
744    //             v_flex().py_0p5().px_1().child(
745    //                 h_flex()
746    //                     .px_1()
747    //                     .py_0p5()
748    //                     .child(
749    //                         IconButton::new("back", IconName::ArrowLeft)
750    //                             .style(ButtonStyle::Transparent)
751    //                             .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| {
752    //                                 cx.dispatch_action(menu::Cancel.boxed_clone())
753    //                             })),
754    //                     )
755    //                     .child(Headline::new("Add remote project").size(HeadlineSize::Small)),
756    //             ),
757    //         )
758    //         .child(
759    //             h_flex()
760    //                 .ml_5()
761    //                 .gap_2()
762    //                 .child(
763    //                     div()
764    //                         .id(("status", dev_server_id.0))
765    //                         .relative()
766    //                         .child(Icon::new(IconName::Server))
767    //                         .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
768    //                             Indicator::dot().color(match dev_server_status {
769    //                                 DevServerStatus::Online => Color::Created,
770    //                                 DevServerStatus::Offline => Color::Hidden,
771    //                             }),
772    //                         ))
773    //                         .tooltip(move |cx| {
774    //                             Tooltip::text(
775    //                                 match dev_server_status {
776    //                                     DevServerStatus::Online => "Online",
777    //                                     DevServerStatus::Offline => "Offline",
778    //                                 },
779    //                                 cx,
780    //                             )
781    //                         }),
782    //                 )
783    //                 .child(dev_server_name.clone()),
784    //         )
785    //         .child(
786    //             h_flex()
787    //                 .ml_5()
788    //                 .gap_2()
789    //                 .child(self.project_path_input.clone())
790    //                 .when(!*creating && dev_server_project.is_none(), |div| {
791    //                     div.child(Button::new("create-remote-server", "Create").on_click({
792    //                         let dev_server_id = *dev_server_id;
793    //                         cx.listener(move |this, _, cx| {
794    //                             this.create_dev_server_project(dev_server_id, cx)
795    //                         })
796    //                     }))
797    //                 })
798    //                 .when(*creating, |div| {
799    //                     div.child(Button::new("create-dev-server", "Creating...").disabled(true))
800    //                 }),
801    //         )
802    //         .when_some(dev_server_project.clone(), |div, dev_server_project| {
803    //             let status = self
804    //                 .dev_server_store
805    //                 .read(cx)
806    //                 .dev_server_project(DevServerProjectId(dev_server_project.id))
807    //                 .map(|project| {
808    //                     if project.project_id.is_some() {
809    //                         DevServerStatus::Online
810    //                     } else {
811    //                         DevServerStatus::Offline
812    //                     }
813    //                 })
814    //                 .unwrap_or(DevServerStatus::Offline);
815    //             div.child(
816    //                 v_flex()
817    //                     .ml_5()
818    //                     .ml_8()
819    //                     .gap_2()
820    //                     .when(status == DevServerStatus::Offline, |this| {
821    //                         this.child(Label::new("Waiting for project..."))
822    //                     })
823    //                     .when(status == DevServerStatus::Online, |this| {
824    //                         this.child(Label::new("Project online! 🎊")).child(
825    //                             Button::new("done", "Done").on_click(cx.listener(|_, _, cx| {
826    //                                 cx.dispatch_action(menu::Cancel.boxed_clone())
827    //                             })),
828    //                         )
829    //                     }),
830    //             )
831    //         })
832    // }
833}
834impl ModalView for DevServerProjects {}
835
836impl FocusableView for DevServerProjects {
837    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
838        self.focus_handle.clone()
839    }
840}
841
842impl EventEmitter<DismissEvent> for DevServerProjects {}
843
844impl Render for DevServerProjects {
845    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
846        div()
847            .track_focus(&self.focus_handle)
848            .elevation_3(cx)
849            .key_context("DevServerModal")
850            .on_action(cx.listener(Self::cancel))
851            .on_action(cx.listener(Self::confirm))
852            .on_mouse_down_out(cx.listener(|this, _, cx| {
853                if matches!(this.mode, Mode::Default(None)) {
854                    cx.emit(DismissEvent)
855                }
856            }))
857            .pb_4()
858            .w(rems(34.))
859            .min_h(rems(20.))
860            .max_h(rems(40.))
861            .child(match &self.mode {
862                Mode::Default(_) => self.render_default(cx).into_any_element(),
863                Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
864            })
865    }
866}