dev_server_modal.rs

  1use channel::{ChannelStore, DevServer, RemoteProject};
  2use client::{ChannelId, DevServerId, RemoteProjectId};
  3use editor::Editor;
  4use gpui::{
  5    AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
  6    ScrollHandle, Task, View, ViewContext,
  7};
  8use rpc::proto::{self, CreateDevServerResponse, DevServerStatus};
  9use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip};
 10use util::ResultExt;
 11use workspace::ModalView;
 12
 13pub struct DevServerModal {
 14    mode: Mode,
 15    focus_handle: FocusHandle,
 16    scroll_handle: ScrollHandle,
 17    channel_store: Model<ChannelStore>,
 18    channel_id: ChannelId,
 19    remote_project_name_editor: View<Editor>,
 20    remote_project_path_editor: View<Editor>,
 21    dev_server_name_editor: View<Editor>,
 22    _subscriptions: [gpui::Subscription; 2],
 23}
 24
 25#[derive(Default)]
 26struct CreateDevServer {
 27    creating: Option<Task<()>>,
 28    dev_server: Option<CreateDevServerResponse>,
 29}
 30
 31struct CreateRemoteProject {
 32    dev_server_id: DevServerId,
 33    creating: Option<Task<()>>,
 34    remote_project: Option<proto::RemoteProject>,
 35}
 36
 37enum Mode {
 38    Default,
 39    CreateRemoteProject(CreateRemoteProject),
 40    CreateDevServer(CreateDevServer),
 41}
 42
 43impl DevServerModal {
 44    pub fn new(
 45        channel_store: Model<ChannelStore>,
 46        channel_id: ChannelId,
 47        cx: &mut ViewContext<Self>,
 48    ) -> Self {
 49        let name_editor = cx.new_view(|cx| Editor::single_line(cx));
 50        let path_editor = cx.new_view(|cx| Editor::single_line(cx));
 51        let dev_server_name_editor = cx.new_view(|cx| {
 52            let mut editor = Editor::single_line(cx);
 53            editor.set_placeholder_text("Dev server name", cx);
 54            editor
 55        });
 56
 57        let focus_handle = cx.focus_handle();
 58
 59        let subscriptions = [
 60            cx.observe(&channel_store, |_, _, cx| {
 61                cx.notify();
 62            }),
 63            cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }),
 64        ];
 65
 66        Self {
 67            mode: Mode::Default,
 68            focus_handle,
 69            scroll_handle: ScrollHandle::new(),
 70            channel_store,
 71            channel_id,
 72            remote_project_name_editor: name_editor,
 73            remote_project_path_editor: path_editor,
 74            dev_server_name_editor,
 75            _subscriptions: subscriptions,
 76        }
 77    }
 78
 79    pub fn create_remote_project(
 80        &mut self,
 81        dev_server_id: DevServerId,
 82        cx: &mut ViewContext<Self>,
 83    ) {
 84        let channel_id = self.channel_id;
 85        let name = self
 86            .remote_project_name_editor
 87            .read(cx)
 88            .text(cx)
 89            .trim()
 90            .to_string();
 91        let path = self
 92            .remote_project_path_editor
 93            .read(cx)
 94            .text(cx)
 95            .trim()
 96            .to_string();
 97
 98        if name == "" {
 99            return;
100        }
101        if path == "" {
102            return;
103        }
104
105        let create = self.channel_store.update(cx, |store, cx| {
106            store.create_remote_project(channel_id, dev_server_id, name, path, cx)
107        });
108
109        let task = cx.spawn(|this, mut cx| async move {
110            let result = create.await;
111            if let Err(e) = &result {
112                cx.prompt(
113                    gpui::PromptLevel::Critical,
114                    "Failed to create project",
115                    Some(&format!("{:?}. Please try again.", e)),
116                    &["Ok"],
117                )
118                .await
119                .log_err();
120            }
121            this.update(&mut cx, |this, _| {
122                this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
123                    dev_server_id,
124                    creating: None,
125                    remote_project: result.ok().and_then(|r| r.remote_project),
126                });
127            })
128            .log_err();
129        });
130
131        self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
132            dev_server_id,
133            creating: Some(task),
134            remote_project: None,
135        });
136    }
137
138    pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
139        let name = self
140            .dev_server_name_editor
141            .read(cx)
142            .text(cx)
143            .trim()
144            .to_string();
145
146        if name == "" {
147            return;
148        }
149
150        let dev_server = self.channel_store.update(cx, |store, cx| {
151            store.create_dev_server(self.channel_id, name.clone(), cx)
152        });
153
154        let task = cx.spawn(|this, mut cx| async move {
155            match dev_server.await {
156                Ok(dev_server) => {
157                    this.update(&mut cx, |this, _| {
158                        this.mode = Mode::CreateDevServer(CreateDevServer {
159                            creating: None,
160                            dev_server: Some(dev_server),
161                        });
162                    })
163                    .log_err();
164                }
165                Err(e) => {
166                    cx.prompt(
167                        gpui::PromptLevel::Critical,
168                        "Failed to create server",
169                        Some(&format!("{:?}. Please try again.", e)),
170                        &["Ok"],
171                    )
172                    .await
173                    .log_err();
174                    this.update(&mut cx, |this, _| {
175                        this.mode = Mode::CreateDevServer(Default::default());
176                    })
177                    .log_err();
178                }
179            }
180        });
181
182        self.mode = Mode::CreateDevServer(CreateDevServer {
183            creating: Some(task),
184            dev_server: None,
185        });
186        cx.notify()
187    }
188
189    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
190        match self.mode {
191            Mode::Default => cx.emit(DismissEvent),
192            Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
193                self.mode = Mode::Default;
194                cx.notify();
195            }
196        }
197    }
198
199    fn render_dev_server(
200        &mut self,
201        dev_server: &DevServer,
202        cx: &mut ViewContext<Self>,
203    ) -> impl IntoElement {
204        let channel_store = self.channel_store.read(cx);
205        let dev_server_id = dev_server.id;
206        let status = dev_server.status;
207
208        v_flex()
209            .w_full()
210            .child(
211                h_flex()
212                    .group("dev-server")
213                    .justify_between()
214                    .child(
215                        h_flex()
216                            .gap_2()
217                            .child(
218                                div()
219                                    .id(("status", dev_server.id.0))
220                                    .relative()
221                                    .child(Icon::new(IconName::Server).size(IconSize::Small))
222                                    .child(
223                                        div().absolute().bottom_0().left(rems_from_px(8.0)).child(
224                                            Indicator::dot().color(match status {
225                                                DevServerStatus::Online => Color::Created,
226                                                DevServerStatus::Offline => Color::Deleted,
227                                            }),
228                                        ),
229                                    )
230                                    .tooltip(move |cx| {
231                                        Tooltip::text(
232                                            match status {
233                                                DevServerStatus::Online => "Online",
234                                                DevServerStatus::Offline => "Offline",
235                                            },
236                                            cx,
237                                        )
238                                    }),
239                            )
240                            .child(dev_server.name.clone())
241                            .child(
242                                h_flex()
243                                    .visible_on_hover("dev-server")
244                                    .gap_1()
245                                    .child(
246                                        IconButton::new("edit-dev-server", IconName::Pencil)
247                                            .disabled(true) //TODO implement this on the collab side
248                                            .tooltip(|cx| {
249                                                Tooltip::text("Coming Soon - Edit dev server", cx)
250                                            }),
251                                    )
252                                    .child(
253                                        IconButton::new("remove-dev-server", IconName::Trash)
254                                            .disabled(true) //TODO implement this on the collab side
255                                            .tooltip(|cx| {
256                                                Tooltip::text("Coming Soon - Remove dev server", cx)
257                                            }),
258                                    ),
259                            ),
260                    )
261                    .child(
262                        h_flex().gap_1().child(
263                            IconButton::new("add-remote-project", IconName::Plus)
264                                .tooltip(|cx| Tooltip::text("Add a remote project", cx))
265                                .on_click(cx.listener(move |this, _, cx| {
266                                    this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
267                                        dev_server_id,
268                                        creating: None,
269                                        remote_project: None,
270                                    });
271                                    cx.notify();
272                                })),
273                        ),
274                    ),
275            )
276            .child(
277                v_flex()
278                    .w_full()
279                    .bg(cx.theme().colors().title_bar_background)
280                    .border()
281                    .border_color(cx.theme().colors().border_variant)
282                    .rounded_md()
283                    .my_1()
284                    .py_0p5()
285                    .px_3()
286                    .child(
287                        List::new().empty_message("No projects.").children(
288                            channel_store
289                                .remote_projects_for_id(dev_server.channel_id)
290                                .iter()
291                                .filter_map(|remote_project| {
292                                    if remote_project.dev_server_id == dev_server.id {
293                                        Some(self.render_remote_project(remote_project, cx))
294                                    } else {
295                                        None
296                                    }
297                                }),
298                        ),
299                    ),
300            )
301        // .child(div().ml_8().child(
302        //     Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener(
303        //         move |this, _, cx| {
304        //             this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
305        //                 dev_server_id,
306        //                 creating: None,
307        //                 remote_project: None,
308        //             });
309        //             cx.notify();
310        //         },
311        //     )),
312        // ))
313    }
314
315    fn render_remote_project(
316        &mut self,
317        project: &RemoteProject,
318        _: &mut ViewContext<Self>,
319    ) -> impl IntoElement {
320        h_flex()
321            .gap_2()
322            .child(Icon::new(IconName::FileTree))
323            .child(Label::new(project.name.clone()))
324            .child(Label::new(format!("({})", project.path.clone())).color(Color::Muted))
325    }
326
327    fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
328        let Mode::CreateDevServer(CreateDevServer {
329            creating,
330            dev_server,
331        }) = &self.mode
332        else {
333            unreachable!()
334        };
335
336        self.dev_server_name_editor.update(cx, |editor, _| {
337            editor.set_read_only(creating.is_some() || dev_server.is_some())
338        });
339        v_flex()
340            .px_1()
341            .pt_0p5()
342            .gap_px()
343            .child(
344                v_flex().py_0p5().px_1().child(
345                    h_flex()
346                        .px_1()
347                        .py_0p5()
348                        .child(
349                            IconButton::new("back", IconName::ArrowLeft)
350                                .style(ButtonStyle::Transparent)
351                                .on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| {
352                                    this.mode = Mode::Default;
353                                    cx.notify();
354                                })),
355                        )
356                        .child(Headline::new("Register dev server")),
357                ),
358            )
359            .child(
360                h_flex()
361                    .ml_5()
362                    .gap_2()
363                    .child("Name")
364                    .child(self.dev_server_name_editor.clone())
365                    .on_action(
366                        cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
367                    )
368                    .when(creating.is_none() && dev_server.is_none(), |div| {
369                        div.child(
370                            Button::new("create-dev-server", "Create").on_click(cx.listener(
371                                move |this, _, cx| {
372                                    this.create_dev_server(cx);
373                                },
374                            )),
375                        )
376                    })
377                    .when(creating.is_some() && dev_server.is_none(), |div| {
378                        div.child(Button::new("create-dev-server", "Creating...").disabled(true))
379                    }),
380            )
381            .when_some(dev_server.clone(), |div, dev_server| {
382                let channel_store = self.channel_store.read(cx);
383                let status = channel_store
384                    .find_dev_server_by_id(DevServerId(dev_server.dev_server_id))
385                    .map(|server| server.status)
386                    .unwrap_or(DevServerStatus::Offline);
387                let instructions = SharedString::from(format!(
388                    "zed --dev-server-token {}",
389                    dev_server.access_token
390                ));
391                div.child(
392                    v_flex()
393                        .ml_8()
394                        .gap_2()
395                        .child(Label::new(format!(
396                            "Please log into `{}` and run:",
397                            dev_server.name
398                        )))
399                        .child(instructions.clone())
400                        .child(
401                            IconButton::new("copy-access-token", IconName::Copy)
402                                .on_click(cx.listener(move |_, _, cx| {
403                                    cx.write_to_clipboard(ClipboardItem::new(
404                                        instructions.to_string(),
405                                    ))
406                                }))
407                                .icon_size(IconSize::Small)
408                                .tooltip(|cx| Tooltip::text("Copy access token", cx)),
409                        )
410                        .when(status == DevServerStatus::Offline, |this| {
411                            this.child(Label::new("Waiting for connection..."))
412                        })
413                        .when(status == DevServerStatus::Online, |this| {
414                            this.child(Label::new("Connection established! 🎊")).child(
415                                Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
416                                    this.mode = Mode::Default;
417                                    cx.notify();
418                                })),
419                            )
420                        }),
421                )
422            })
423    }
424
425    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
426        let channel_store = self.channel_store.read(cx);
427        let dev_servers = channel_store.dev_servers_for_id(self.channel_id);
428        // let dev_servers = Vec::new();
429
430        v_flex()
431            .id("scroll-container")
432            .h_full()
433            .overflow_y_scroll()
434            .track_scroll(&self.scroll_handle)
435            .px_1()
436            .pt_0p5()
437            .gap_px()
438            .child(
439                ModalHeader::new("Manage Remote Project")
440                    .child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
441            )
442            .child(
443                ModalContent::new().child(
444                    List::new()
445                        .empty_message("No dev servers registered.")
446                        .header(Some(
447                            ListHeader::new("Dev Servers").end_slot(
448                                Button::new("register-dev-server-button", "New Server")
449                                    .icon(IconName::Plus)
450                                    .icon_position(IconPosition::Start)
451                                    .tooltip(|cx| Tooltip::text("Register a new dev server", cx))
452                                    .on_click(cx.listener(|this, _, cx| {
453                                        this.mode = Mode::CreateDevServer(Default::default());
454                                        this.dev_server_name_editor
455                                            .read(cx)
456                                            .focus_handle(cx)
457                                            .focus(cx);
458                                        cx.notify();
459                                    })),
460                            ),
461                        ))
462                        .children(dev_servers.iter().map(|dev_server| {
463                            self.render_dev_server(dev_server, cx).into_any_element()
464                        })),
465                ),
466            )
467    }
468
469    fn render_create_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
470        let Mode::CreateRemoteProject(CreateRemoteProject {
471            dev_server_id,
472            creating,
473            remote_project,
474        }) = &self.mode
475        else {
476            unreachable!()
477        };
478        let channel_store = self.channel_store.read(cx);
479        let (dev_server_name, dev_server_status) = channel_store
480            .find_dev_server_by_id(*dev_server_id)
481            .map(|server| (server.name.clone(), server.status))
482            .unwrap_or((SharedString::from(""), DevServerStatus::Offline));
483        v_flex()
484            .px_1()
485            .pt_0p5()
486            .gap_px()
487            .child(
488                ModalHeader::new("Manage Remote Project")
489                    .child(Headline::new("Manage Remote Projects")),
490            )
491            .child(
492                h_flex()
493                    .py_0p5()
494                    .px_1()
495                    .child(div().px_1().py_0p5().child(
496                        IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener(
497                            |this, _, cx| {
498                                this.mode = Mode::Default;
499                                cx.notify()
500                            },
501                        )),
502                    ))
503                    .child("Add Project..."),
504            )
505            .child(
506                h_flex()
507                    .ml_5()
508                    .gap_2()
509                    .child(
510                        div()
511                            .id(("status", dev_server_id.0))
512                            .relative()
513                            .child(Icon::new(IconName::Server))
514                            .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
515                                Indicator::dot().color(match dev_server_status {
516                                    DevServerStatus::Online => Color::Created,
517                                    DevServerStatus::Offline => Color::Deleted,
518                                }),
519                            ))
520                            .tooltip(move |cx| {
521                                Tooltip::text(
522                                    match dev_server_status {
523                                        DevServerStatus::Online => "Online",
524                                        DevServerStatus::Offline => "Offline",
525                                    },
526                                    cx,
527                                )
528                            }),
529                    )
530                    .child(dev_server_name.clone()),
531            )
532            .child(
533                h_flex()
534                    .ml_5()
535                    .gap_2()
536                    .child("Name")
537                    .child(self.remote_project_name_editor.clone())
538                    .on_action(cx.listener(|this, _: &menu::Confirm, cx| {
539                        cx.focus_view(&this.remote_project_path_editor)
540                    })),
541            )
542            .child(
543                h_flex()
544                    .ml_5()
545                    .gap_2()
546                    .child("Path")
547                    .child(self.remote_project_path_editor.clone())
548                    .on_action(
549                        cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
550                    )
551                    .when(creating.is_none() && remote_project.is_none(), |div| {
552                        div.child(Button::new("create-remote-server", "Create").on_click({
553                            let dev_server_id = *dev_server_id;
554                            cx.listener(move |this, _, cx| {
555                                this.create_remote_project(dev_server_id, cx)
556                            })
557                        }))
558                    })
559                    .when(creating.is_some(), |div| {
560                        div.child(Button::new("create-dev-server", "Creating...").disabled(true))
561                    }),
562            )
563            .when_some(remote_project.clone(), |div, remote_project| {
564                let channel_store = self.channel_store.read(cx);
565                let status = channel_store
566                    .find_remote_project_by_id(RemoteProjectId(remote_project.id))
567                    .map(|project| {
568                        if project.project_id.is_some() {
569                            DevServerStatus::Online
570                        } else {
571                            DevServerStatus::Offline
572                        }
573                    })
574                    .unwrap_or(DevServerStatus::Offline);
575                div.child(
576                    v_flex()
577                        .ml_5()
578                        .ml_8()
579                        .gap_2()
580                        .when(status == DevServerStatus::Offline, |this| {
581                            this.child(Label::new("Waiting for project..."))
582                        })
583                        .when(status == DevServerStatus::Online, |this| {
584                            this.child(Label::new("Project online! 🎊")).child(
585                                Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
586                                    this.mode = Mode::Default;
587                                    cx.notify();
588                                })),
589                            )
590                        }),
591                )
592            })
593    }
594}
595impl ModalView for DevServerModal {}
596
597impl FocusableView for DevServerModal {
598    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
599        self.focus_handle.clone()
600    }
601}
602
603impl EventEmitter<DismissEvent> for DevServerModal {}
604
605impl Render for DevServerModal {
606    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
607        div()
608            .track_focus(&self.focus_handle)
609            .elevation_3(cx)
610            .key_context("DevServerModal")
611            .on_action(cx.listener(Self::cancel))
612            .pb_4()
613            .w(rems(34.))
614            .min_h(rems(20.))
615            .max_h(rems(40.))
616            .child(match &self.mode {
617                Mode::Default => self.render_default(cx).into_any_element(),
618                Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(),
619                Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
620            })
621    }
622}