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}