diff --git a/crates/collab/src/db/queries/dev_servers.rs b/crates/collab/src/db/queries/dev_servers.rs index 90baaee476..38e3c0ab99 100644 --- a/crates/collab/src/db/queries/dev_servers.rs +++ b/crates/collab/src/db/queries/dev_servers.rs @@ -77,10 +77,14 @@ impl Database { user_id: UserId, ) -> crate::Result<(dev_server::Model, proto::DevServerProjectsUpdate)> { self.transaction(|tx| async move { + if name.trim().is_empty() { + return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?; + } + let dev_server = dev_server::Entity::insert(dev_server::ActiveModel { id: ActiveValue::NotSet, hashed_token: ActiveValue::Set(hashed_access_token.to_string()), - name: ActiveValue::Set(name.to_string()), + name: ActiveValue::Set(name.trim().to_string()), user_id: ActiveValue::Set(user_id), }) .exec_with_returning(&*tx) @@ -95,6 +99,66 @@ impl Database { .await } + pub async fn update_dev_server_token( + &self, + id: DevServerId, + hashed_token: &str, + user_id: UserId, + ) -> crate::Result { + self.transaction(|tx| async move { + let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else { + return Err(anyhow::anyhow!("no dev server with id {}", id))?; + }; + if dev_server.user_id != user_id { + return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?; + } + + dev_server::Entity::update(dev_server::ActiveModel { + hashed_token: ActiveValue::Set(hashed_token.to_string()), + ..dev_server.clone().into_active_model() + }) + .exec(&*tx) + .await?; + + let dev_server_projects = self + .dev_server_projects_update_internal(user_id, &tx) + .await?; + + Ok(dev_server_projects) + }) + .await + } + + pub async fn rename_dev_server( + &self, + id: DevServerId, + name: &str, + user_id: UserId, + ) -> crate::Result { + self.transaction(|tx| async move { + let Some(dev_server) = dev_server::Entity::find_by_id(id).one(&*tx).await? else { + return Err(anyhow::anyhow!("no dev server with id {}", id))?; + }; + if dev_server.user_id != user_id || name.trim().is_empty() { + return Err(anyhow::anyhow!(proto::ErrorCode::Forbidden))?; + } + + dev_server::Entity::update(dev_server::ActiveModel { + name: ActiveValue::Set(name.trim().to_string()), + ..dev_server.clone().into_active_model() + }) + .exec(&*tx) + .await?; + + let dev_server_projects = self + .dev_server_projects_update_internal(user_id, &tx) + .await?; + + Ok(dev_server_projects) + }) + .await + } + pub async fn delete_dev_server( &self, id: DevServerId, diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 59f811f0b5..bad78b845a 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -433,6 +433,8 @@ impl Server { .add_request_handler(user_handler(create_dev_server_project)) .add_request_handler(user_handler(delete_dev_server_project)) .add_request_handler(user_handler(create_dev_server)) + .add_request_handler(user_handler(regenerate_dev_server_token)) + .add_request_handler(user_handler(rename_dev_server)) .add_request_handler(user_handler(delete_dev_server)) .add_request_handler(dev_server_handler(share_dev_server_project)) .add_request_handler(dev_server_handler(shutdown_dev_server)) @@ -2343,6 +2345,12 @@ async fn create_dev_server( let access_token = auth::random_token(); let hashed_access_token = auth::hash_access_token(&access_token); + if request.name.is_empty() { + return Err(proto::ErrorCode::Forbidden + .message("Dev server name cannot be empty".to_string()) + .anyhow())?; + } + let (dev_server, status) = session .db() .await @@ -2359,6 +2367,71 @@ async fn create_dev_server( Ok(()) } +async fn regenerate_dev_server_token( + request: proto::RegenerateDevServerToken, + response: Response, + session: UserSession, +) -> Result<()> { + let dev_server_id = DevServerId(request.dev_server_id as i32); + let access_token = auth::random_token(); + let hashed_access_token = auth::hash_access_token(&access_token); + + let connection_id = session + .connection_pool() + .await + .dev_server_connection_id(dev_server_id); + if let Some(connection_id) = connection_id { + shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?; + session + .peer + .send(connection_id, proto::ShutdownDevServer {})?; + let _ = remove_dev_server_connection(dev_server_id, &session).await; + } + + let status = session + .db() + .await + .update_dev_server_token(dev_server_id, &hashed_access_token, session.user_id()) + .await?; + + send_dev_server_projects_update(session.user_id(), status, &session).await; + + response.send(proto::RegenerateDevServerTokenResponse { + dev_server_id: dev_server_id.to_proto(), + access_token: auth::generate_dev_server_token(dev_server_id.0 as usize, access_token), + })?; + Ok(()) +} + +async fn rename_dev_server( + request: proto::RenameDevServer, + response: Response, + session: UserSession, +) -> Result<()> { + if request.name.trim().is_empty() { + return Err(proto::ErrorCode::Forbidden + .message("Dev server name cannot be empty".to_string()) + .anyhow())?; + } + + let dev_server_id = DevServerId(request.dev_server_id as i32); + let dev_server = session.db().await.get_dev_server(dev_server_id).await?; + if dev_server.user_id != session.user_id() { + return Err(anyhow!(ErrorCode::Forbidden))?; + } + + let status = session + .db() + .await + .rename_dev_server(dev_server_id, &request.name, session.user_id()) + .await?; + + send_dev_server_projects_update(session.user_id(), status, &session).await; + + response.send(proto::Ack {})?; + Ok(()) +} + async fn delete_dev_server( request: proto::DeleteDevServer, response: Response, @@ -2379,6 +2452,7 @@ async fn delete_dev_server( session .peer .send(connection_id, proto::ShutdownDevServer {})?; + let _ = remove_dev_server_connection(dev_server_id, &session).await; } let status = session @@ -2551,7 +2625,8 @@ async fn shutdown_dev_server( session: DevServerSession, ) -> Result<()> { response.send(proto::Ack {})?; - shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await + shutdown_dev_server_internal(session.dev_server_id(), session.connection_id, &session).await?; + remove_dev_server_connection(session.dev_server_id(), &session).await } async fn shutdown_dev_server_internal( @@ -2591,6 +2666,21 @@ async fn shutdown_dev_server_internal( Ok(()) } +async fn remove_dev_server_connection(dev_server_id: DevServerId, session: &Session) -> Result<()> { + let dev_server_connection = session + .connection_pool() + .await + .dev_server_connection_id(dev_server_id); + + if let Some(dev_server_connection) = dev_server_connection { + session + .connection_pool() + .await + .remove_connection(dev_server_connection)?; + } + Ok(()) +} + /// Updates other participants with changes to the project async fn update_project( request: proto::UpdateProject, diff --git a/crates/collab/src/tests/dev_server_tests.rs b/crates/collab/src/tests/dev_server_tests.rs index 95becadada..d208e31363 100644 --- a/crates/collab/src/tests/dev_server_tests.rs +++ b/crates/collab/src/tests/dev_server_tests.rs @@ -315,6 +315,139 @@ async fn test_dev_server_delete( }) } +#[gpui::test] +async fn test_dev_server_rename( + cx1: &mut gpui::TestAppContext, + cx2: &mut gpui::TestAppContext, + cx3: &mut gpui::TestAppContext, +) { + let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await; + + let (_dev_server, remote_workspace) = + create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await; + + cx1.update(|cx| { + workspace::join_channel( + channel_id, + client1.app_state.clone(), + Some(remote_workspace), + cx, + ) + }) + .await + .unwrap(); + cx1.executor().run_until_parked(); + + remote_workspace + .update(cx1, |ws, cx| { + assert!(ws.project().read(cx).is_shared()); + }) + .unwrap(); + + join_channel(channel_id, &client2, cx2).await.unwrap(); + cx2.executor().run_until_parked(); + + cx1.update(|cx| { + dev_server_projects::Store::global(cx).update(cx, |store, cx| { + store.rename_dev_server( + store.dev_servers().first().unwrap().id, + "name-edited".to_string(), + cx, + ) + }) + }) + .await + .unwrap(); + + cx1.executor().run_until_parked(); + + cx1.update(|cx| { + dev_server_projects::Store::global(cx).update(cx, |store, _| { + assert_eq!(store.dev_servers().first().unwrap().name, "name-edited"); + }) + }) +} + +#[gpui::test] +async fn test_dev_server_refresh_access_token( + cx1: &mut gpui::TestAppContext, + cx2: &mut gpui::TestAppContext, + cx3: &mut gpui::TestAppContext, + cx4: &mut gpui::TestAppContext, +) { + let (server, client1, client2, channel_id) = TestServer::start2(cx1, cx2).await; + + let (_dev_server, remote_workspace) = + create_dev_server_project(&server, client1.app_state.clone(), cx1, cx3).await; + + cx1.update(|cx| { + workspace::join_channel( + channel_id, + client1.app_state.clone(), + Some(remote_workspace), + cx, + ) + }) + .await + .unwrap(); + cx1.executor().run_until_parked(); + + remote_workspace + .update(cx1, |ws, cx| { + assert!(ws.project().read(cx).is_shared()); + }) + .unwrap(); + + join_channel(channel_id, &client2, cx2).await.unwrap(); + cx2.executor().run_until_parked(); + + // Regenerate the access token + let new_token_response = cx1 + .update(|cx| { + dev_server_projects::Store::global(cx).update(cx, |store, cx| { + store.regenerate_dev_server_token(store.dev_servers().first().unwrap().id, cx) + }) + }) + .await + .unwrap(); + + cx1.executor().run_until_parked(); + + // Assert that the other client was disconnected + let (workspace, cx2) = client2.active_workspace(cx2); + cx2.update(|cx| assert!(workspace.read(cx).project().read(cx).is_disconnected())); + + // Assert that the owner of the dev server does not see the dev server as online anymore + let (workspace, cx1) = client1.active_workspace(cx1); + cx1.update(|cx| { + assert!(workspace.read(cx).project().read(cx).is_disconnected()); + dev_server_projects::Store::global(cx).update(cx, |store, _| { + assert_eq!( + store.dev_servers().first().unwrap().status, + DevServerStatus::Offline + ); + }) + }); + + // Reconnect the dev server with the new token + let _dev_server = server + .create_dev_server(new_token_response.access_token, cx4) + .await; + + cx1.executor().run_until_parked(); + + // Assert that the dev server is online again + cx1.update(|cx| { + dev_server_projects::Store::global(cx).update(cx, |store, _| { + assert_eq!(store.dev_servers().len(), 1); + assert_eq!( + store.dev_servers().first().unwrap().status, + DevServerStatus::Online + ); + }) + }); +} + #[gpui::test] async fn test_dev_server_reconnect( cx1: &mut gpui::TestAppContext, diff --git a/crates/dev_server_projects/src/dev_server_projects.rs b/crates/dev_server_projects/src/dev_server_projects.rs index aa27f3c8ca..ce3a8e6c05 100644 --- a/crates/dev_server_projects/src/dev_server_projects.rs +++ b/crates/dev_server_projects/src/dev_server_projects.rs @@ -173,6 +173,39 @@ impl Store { }) } + pub fn rename_dev_server( + &mut self, + dev_server_id: DevServerId, + name: String, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::RenameDevServer { + dev_server_id: dev_server_id.0, + name, + }) + .await?; + Ok(()) + }) + } + + pub fn regenerate_dev_server_token( + &mut self, + dev_server_id: DevServerId, + cx: &mut ModelContext, + ) -> Task> { + let client = self.client.clone(); + cx.background_executor().spawn(async move { + client + .request(proto::RegenerateDevServerToken { + dev_server_id: dev_server_id.0, + }) + .await + }) + } + pub fn delete_dev_server( &mut self, id: DevServerId, diff --git a/crates/recent_projects/src/dev_servers.rs b/crates/recent_projects/src/dev_servers.rs index 6e72eda141..f432235a5b 100644 --- a/crates/recent_projects/src/dev_servers.rs +++ b/crates/recent_projects/src/dev_servers.rs @@ -10,7 +10,7 @@ use gpui::{ View, ViewContext, }; use rpc::{ - proto::{CreateDevServerResponse, DevServerStatus}, + proto::{CreateDevServerResponse, DevServerStatus, RegenerateDevServerTokenResponse}, ErrorCode, ErrorExt, }; use settings::Settings; @@ -29,15 +29,30 @@ pub struct DevServerProjects { dev_server_store: Model, project_path_input: View, dev_server_name_input: View, + rename_dev_server_input: View, _subscription: gpui::Subscription, } -#[derive(Default)] +#[derive(Default, Clone)] struct CreateDevServer { creating: bool, dev_server: Option, } +#[derive(Clone)] +struct EditDevServer { + dev_server_id: DevServerId, + state: EditDevServerState, +} + +#[derive(Clone, PartialEq)] +enum EditDevServerState { + Default, + RenamingDevServer, + RegeneratingToken, + RegeneratedToken(RegenerateDevServerTokenResponse), +} + #[derive(Clone)] struct CreateDevServerProject { dev_server_id: DevServerId, @@ -47,6 +62,7 @@ struct CreateDevServerProject { enum Mode { Default(Option), CreateDevServer(CreateDevServer), + EditDevServer(EditDevServer), } impl DevServerProjects { @@ -83,6 +99,8 @@ impl DevServerProjects { }); let dev_server_name_input = cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked)); + let rename_dev_server_input = + cx.new_view(|cx| TextField::new(cx, "Name", "").with_label(FieldLabelLayout::Stacked)); let focus_handle = cx.focus_handle(); let dev_server_store = dev_server_projects::Store::global(cx); @@ -98,6 +116,7 @@ impl DevServerProjects { dev_server_store, project_path_input, dev_server_name_input, + rename_dev_server_input, _subscription: subscription, } } @@ -225,6 +244,88 @@ impl DevServerProjects { cx.notify() } + fn rename_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext) { + let name = self + .rename_dev_server_input + .read(cx) + .editor() + .read(cx) + .text(cx) + .trim() + .to_string(); + + let Some(dev_server) = self.dev_server_store.read(cx).dev_server(id) else { + return; + }; + + if name.is_empty() || dev_server.name == name { + return; + } + + let request = self + .dev_server_store + .update(cx, |store, cx| store.rename_dev_server(id, name, cx)); + + self.mode = Mode::EditDevServer(EditDevServer { + dev_server_id: id, + state: EditDevServerState::RenamingDevServer, + }); + + cx.spawn(|this, mut cx| async move { + request.await?; + this.update(&mut cx, move |this, cx| { + this.mode = Mode::EditDevServer(EditDevServer { + dev_server_id: id, + state: EditDevServerState::Default, + }); + cx.notify(); + }) + }) + .detach_and_prompt_err("Failed to rename dev server", cx, |_, _| None); + } + + fn refresh_dev_server_token(&mut self, id: DevServerId, cx: &mut ViewContext) { + let answer = cx.prompt( + gpui::PromptLevel::Warning, + "Are you sure?", + Some("This will invalidate the existing dev server token."), + &["Generate", "Cancel"], + ); + cx.spawn(|this, mut cx| async move { + let answer = answer.await?; + + if answer != 0 { + return Ok(()); + } + + let response = this + .update(&mut cx, move |this, cx| { + let request = this + .dev_server_store + .update(cx, |store, cx| store.regenerate_dev_server_token(id, cx)); + this.mode = Mode::EditDevServer(EditDevServer { + dev_server_id: id, + state: EditDevServerState::RegeneratingToken, + }); + cx.notify(); + request + })? + .await?; + + this.update(&mut cx, move |this, cx| { + this.mode = Mode::EditDevServer(EditDevServer { + dev_server_id: id, + state: EditDevServerState::RegeneratedToken(response), + }); + cx.notify(); + }) + .log_err(); + + Ok(()) + }) + .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None); + } + fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext) { let answer = cx.prompt( gpui::PromptLevel::Destructive, @@ -314,6 +415,17 @@ impl DevServerProjects { self.create_dev_server(cx); } } + Mode::EditDevServer(edit_dev_server) => { + if self + .rename_dev_server_input + .read(cx) + .editor() + .read(cx) + .is_focused(cx) + { + self.rename_dev_server(edit_dev_server.dev_server_id, cx); + } + } } } @@ -336,6 +448,7 @@ impl DevServerProjects { ) -> impl IntoElement { let dev_server_id = dev_server.id; let status = dev_server.status; + let dev_server_name = dev_server.name.clone(); if create_project .as_ref() .is_some_and(|cp| cp.dev_server_id != dev_server.id) @@ -375,17 +488,32 @@ impl DevServerProjects { ) }), ) - .child(dev_server.name.clone()) + .child(dev_server_name.clone()) .child( h_flex() .visible_on_hover("dev-server") .gap_1() .child( IconButton::new("edit-dev-server", IconName::Pencil) - .disabled(true) //TODO implement this on the collab side - .tooltip(|cx| { - Tooltip::text("Coming Soon - Edit dev server", cx) - }), + .on_click(cx.listener(move |this, _, cx| { + this.mode = Mode::EditDevServer(EditDevServer { + dev_server_id, + state: EditDevServerState::Default, + }); + let dev_server_name = dev_server_name.clone(); + this.rename_dev_server_input.update( + cx, + move |input, cx| { + input.editor().update( + cx, + move |editor, cx| { + editor.set_text(dev_server_name, cx) + }, + ) + }, + ) + })) + .tooltip(|cx| Tooltip::text("Edit dev server", cx)), ) .child({ let dev_server_id = dev_server.id; @@ -507,17 +635,18 @@ impl DevServerProjects { .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element())) } - fn render_create_dev_server(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let Mode::CreateDevServer(CreateDevServer { + fn render_create_dev_server( + &mut self, + state: CreateDevServer, + cx: &mut ViewContext, + ) -> impl IntoElement { + let CreateDevServer { creating, dev_server, - }) = &self.mode - else { - unreachable!() - }; + } = state; self.dev_server_name_input.update(cx, |input, cx| { - input.set_disabled(*creating || dev_server.is_some(), cx); + input.set_disabled(creating || dev_server.is_some(), cx); }); v_flex() @@ -529,7 +658,7 @@ impl DevServerProjects { .pt_0p5() .gap_px() .child( - ModalHeader::new("remote-projects") + ModalHeader::new("create-dev-server") .show_back_button(true) .child(Headline::new("New dev server").size(HeadlineSize::Small)), ) @@ -555,14 +684,14 @@ impl DevServerProjects { div() .pl_1() .pb(px(3.)) - .when(!*creating && dev_server.is_none(), |div| { + .when(!creating && dev_server.is_none(), |div| { div.child(Button::new("create-dev-server", "Create").on_click( cx.listener(move |this, _, cx| { this.create_dev_server(cx); }), )) }) - .when(*creating && dev_server.is_none(), |div| { + .when(creating && dev_server.is_none(), |div| { div.child( Button::new("create-dev-server", "Creating...") .disabled(true), @@ -579,86 +708,212 @@ impl DevServerProjects { .read(cx) .dev_server_status(DevServerId(dev_server.dev_server_id)); - let instructions = SharedString::from(format!( - "zed --dev-server-token {}", - dev_server.access_token - )); div.child( - v_flex() - .pl_2() - .pt_2() - .gap_2() - .child( - h_flex().justify_between().w_full() - .child(Label::new(format!( - "Please log into `{}` and run:", - dev_server.name - ))) - .child( - Button::new("copy-access-token", "Copy Instructions") - .icon(Some(IconName::Copy)) - .icon_size(IconSize::Small) - .on_click({ - let instructions = instructions.clone(); - cx.listener(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new( - instructions.to_string(), - )) - })}) - ) - ) - .child( - v_flex() - .w_full() - .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct - .border() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .my_1() - .py_0p5() - .px_3() - .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone()) - .child(Label::new(instructions)) - ) - .when(status == DevServerStatus::Offline, |this| { - this.child( - - h_flex() - .gap_2() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Medium) - .with_animation( - "arrow-circle", - Animation::new(Duration::from_secs(2)).repeat(), - |icon, delta| { - icon.transform(Transformation::rotate(percentage(delta))) - }, - ), - ) - .child( - Label::new("Waiting for connection…"), - ) - ) - }) - .when(status == DevServerStatus::Online, |this| { - this.child(Label::new("🎊 Connection established!")) - .child( - h_flex().justify_end().child( - Button::new("done", "Done").on_click(cx.listener( - |_, _, cx| { - cx.dispatch_action(menu::Cancel.boxed_clone()) - }, - )) - ), - ) - }), + Self::render_dev_server_token_instructions(&dev_server.access_token, &dev_server.name, status, cx) ) }), ) ) } + fn render_dev_server_token_instructions( + access_token: &str, + dev_server_name: &str, + status: DevServerStatus, + cx: &mut ViewContext, + ) -> Div { + let instructions = SharedString::from(format!("zed --dev-server-token {}", access_token)); + + v_flex() + .pl_2() + .pt_2() + .gap_2() + .child( + h_flex() + .justify_between() + .w_full() + .child(Label::new(format!( + "Please log into `{}` and run:", + dev_server_name + ))) + .child( + Button::new("copy-access-token", "Copy Instructions") + .icon(Some(IconName::Copy)) + .icon_size(IconSize::Small) + .on_click({ + let instructions = instructions.clone(); + cx.listener(move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new( + instructions.to_string(), + )) + }) + }), + ), + ) + .child( + v_flex() + .w_full() + .bg(cx.theme().colors().title_bar_background) // todo: this should be distinct + .border() + .border_color(cx.theme().colors().border_variant) + .rounded_md() + .my_1() + .py_0p5() + .px_3() + .font_family(ThemeSettings::get_global(cx).buffer_font.family.clone()) + .child(Label::new(instructions)), + ) + .when(status == DevServerStatus::Offline, |this| { + this.child(Self::render_loading_spinner("Waiting for connection…")) + }) + .when(status == DevServerStatus::Online, |this| { + this.child(Label::new("🎊 Connection established!")).child( + h_flex() + .justify_end() + .child(Button::new("done", "Done").on_click( + cx.listener(|_, _, cx| cx.dispatch_action(menu::Cancel.boxed_clone())), + )), + ) + }) + } + + fn render_loading_spinner(label: impl Into) -> Div { + h_flex() + .gap_2() + .child( + Icon::new(IconName::ArrowCircle) + .size(IconSize::Medium) + .with_animation( + "arrow-circle", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ), + ) + .child(Label::new(label)) + } + + fn render_edit_dev_server( + &mut self, + edit_dev_server: EditDevServer, + cx: &mut ViewContext, + ) -> impl IntoElement { + let dev_server_id = edit_dev_server.dev_server_id; + let dev_server = self + .dev_server_store + .read(cx) + .dev_server(dev_server_id) + .cloned(); + + let dev_server_name = dev_server + .as_ref() + .map(|dev_server| dev_server.name.clone()) + .unwrap_or_default(); + + let dev_server_status = dev_server + .map(|dev_server| dev_server.status) + .unwrap_or(DevServerStatus::Offline); + + let disabled = matches!( + edit_dev_server.state, + EditDevServerState::RenamingDevServer | EditDevServerState::RegeneratingToken + ); + self.rename_dev_server_input.update(cx, |input, cx| { + input.set_disabled(disabled, cx); + }); + + let rename_dev_server_input_text = self + .rename_dev_server_input + .read(cx) + .editor() + .read(cx) + .text(cx); + + let content = v_flex().w_full().gap_2().child( + h_flex() + .pb_2() + .border_b_1() + .border_color(cx.theme().colors().border) + .items_end() + .w_full() + .px_2() + .child( + div() + .pl_2() + .max_w(rems(16.)) + .child(self.rename_dev_server_input.clone()), + ) + .child( + div() + .pl_1() + .pb(px(3.)) + .when( + edit_dev_server.state != EditDevServerState::RenamingDevServer, + |div| { + div.child( + Button::new("rename-dev-server", "Rename") + .disabled( + rename_dev_server_input_text.trim().is_empty() + || rename_dev_server_input_text == dev_server_name, + ) + .on_click(cx.listener(move |this, _, cx| { + this.rename_dev_server(dev_server_id, cx); + cx.notify(); + })), + ) + }, + ) + .when( + edit_dev_server.state == EditDevServerState::RenamingDevServer, + |div| { + div.child( + Button::new("rename-dev-server", "Renaming...").disabled(true), + ) + }, + ), + ), + ); + + let content = content.child(match edit_dev_server.state { + EditDevServerState::RegeneratingToken => { + Self::render_loading_spinner("Generating token...") + } + EditDevServerState::RegeneratedToken(response) => { + Self::render_dev_server_token_instructions( + &response.access_token, + &dev_server_name, + dev_server_status, + cx, + ) + } + _ => h_flex().items_end().w_full().child( + Button::new("regenerate-dev-server-token", "Generate new access token") + .icon(IconName::Update) + .on_click(cx.listener(move |this, _, cx| { + this.refresh_dev_server_token(dev_server_id, cx); + cx.notify(); + })), + ), + }); + + v_flex() + .id("scroll-container") + .h_full() + .overflow_y_scroll() + .track_scroll(&self.scroll_handle) + .px_1() + .pt_0p5() + .gap_px() + .child( + ModalHeader::new("edit-dev-server") + .show_back_button(true) + .child( + Headline::new(format!("Edit {}", &dev_server_name)) + .size(HeadlineSize::Small), + ), + ) + .child(ModalContent::new().child(v_flex().w_full().child(content))) + } + fn render_default(&mut self, cx: &mut ViewContext) -> impl IntoElement { let dev_servers = self.dev_server_store.read(cx).dev_servers(); @@ -715,121 +970,6 @@ impl DevServerProjects { ), ) } - - // fn render_create_dev_server_project(&self, cx: &mut ViewContext) -> impl IntoElement { - // let Mode::CreateDevServerProject(CreateDevServerProject { - // dev_server_id, - // creating, - // dev_server_project, - // }) = &self.mode - // else { - // unreachable!() - // }; - - // let dev_server = self - // .dev_server_store - // .read(cx) - // .dev_server(*dev_server_id) - // .cloned(); - - // let (dev_server_name, dev_server_status) = dev_server - // .map(|server| (server.name, server.status)) - // .unwrap_or((SharedString::from(""), DevServerStatus::Offline)); - - // v_flex() - // .px_1() - // .pt_0p5() - // .gap_px() - // .child( - // v_flex().py_0p5().px_1().child( - // h_flex() - // .px_1() - // .py_0p5() - // .child( - // IconButton::new("back", IconName::ArrowLeft) - // .style(ButtonStyle::Transparent) - // .on_click(cx.listener(|_, _: &gpui::ClickEvent, cx| { - // cx.dispatch_action(menu::Cancel.boxed_clone()) - // })), - // ) - // .child(Headline::new("Add remote project").size(HeadlineSize::Small)), - // ), - // ) - // .child( - // h_flex() - // .ml_5() - // .gap_2() - // .child( - // div() - // .id(("status", dev_server_id.0)) - // .relative() - // .child(Icon::new(IconName::Server)) - // .child(div().absolute().bottom_0().left(rems_from_px(12.0)).child( - // Indicator::dot().color(match dev_server_status { - // DevServerStatus::Online => Color::Created, - // DevServerStatus::Offline => Color::Hidden, - // }), - // )) - // .tooltip(move |cx| { - // Tooltip::text( - // match dev_server_status { - // DevServerStatus::Online => "Online", - // DevServerStatus::Offline => "Offline", - // }, - // cx, - // ) - // }), - // ) - // .child(dev_server_name.clone()), - // ) - // .child( - // h_flex() - // .ml_5() - // .gap_2() - // .child(self.project_path_input.clone()) - // .when(!*creating && dev_server_project.is_none(), |div| { - // div.child(Button::new("create-remote-server", "Create").on_click({ - // let dev_server_id = *dev_server_id; - // cx.listener(move |this, _, cx| { - // this.create_dev_server_project(dev_server_id, cx) - // }) - // })) - // }) - // .when(*creating, |div| { - // div.child(Button::new("create-dev-server", "Creating...").disabled(true)) - // }), - // ) - // .when_some(dev_server_project.clone(), |div, dev_server_project| { - // let status = self - // .dev_server_store - // .read(cx) - // .dev_server_project(DevServerProjectId(dev_server_project.id)) - // .map(|project| { - // if project.project_id.is_some() { - // DevServerStatus::Online - // } else { - // DevServerStatus::Offline - // } - // }) - // .unwrap_or(DevServerStatus::Offline); - // div.child( - // v_flex() - // .ml_5() - // .ml_8() - // .gap_2() - // .when(status == DevServerStatus::Offline, |this| { - // this.child(Label::new("Waiting for project...")) - // }) - // .when(status == DevServerStatus::Online, |this| { - // this.child(Label::new("Project online! 🎊")).child( - // Button::new("done", "Done").on_click(cx.listener(|_, _, cx| { - // cx.dispatch_action(menu::Cancel.boxed_clone()) - // })), - // ) - // }), - // ) - // }) - // } } impl ModalView for DevServerProjects {} @@ -860,7 +1000,12 @@ impl Render for DevServerProjects { .max_h(rems(40.)) .child(match &self.mode { Mode::Default(_) => self.render_default(cx).into_any_element(), - Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(), + Mode::CreateDevServer(state) => self + .render_create_dev_server(state.clone(), cx) + .into_any_element(), + Mode::EditDevServer(state) => self + .render_edit_dev_server(state.clone(), cx) + .into_any_element(), }) } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 5f8af8e1f0..4a483d2f67 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -241,7 +241,11 @@ message Envelope { DeleteDevServerProject delete_dev_server_project = 197; GetSupermavenApiKey get_supermaven_api_key = 198; - GetSupermavenApiKeyResponse get_supermaven_api_key_response = 199; // current max + GetSupermavenApiKeyResponse get_supermaven_api_key_response = 199; + + RegenerateDevServerToken regenerate_dev_server_token = 200; + RegenerateDevServerTokenResponse regenerate_dev_server_token_response = 201; + RenameDevServer rename_dev_server = 202; // Current max } reserved 158 to 161; @@ -488,6 +492,15 @@ message CreateDevServer { string name = 2; } +message RegenerateDevServerToken { + uint64 dev_server_id = 1; +} + +message RegenerateDevServerTokenResponse { + uint64 dev_server_id = 1; + string access_token = 2; +} + message CreateDevServerResponse { uint64 dev_server_id = 1; reserved 2; @@ -498,6 +511,11 @@ message CreateDevServerResponse { message ShutdownDevServer { } +message RenameDevServer { + uint64 dev_server_id = 1; + string name = 2; +} + message DeleteDevServer { uint64 dev_server_id = 1; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d011f1d1d2..d588b93da8 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -323,6 +323,9 @@ messages!( (ValidateDevServerProjectRequest, Background), (DeleteDevServer, Foreground), (DeleteDevServerProject, Foreground), + (RegenerateDevServerToken, Foreground), + (RegenerateDevServerTokenResponse, Foreground), + (RenameDevServer, Foreground), (OpenNewBuffer, Foreground) ); @@ -430,6 +433,8 @@ request_messages!( (MultiLspQuery, MultiLspQueryResponse), (DeleteDevServer, Ack), (DeleteDevServerProject, Ack), + (RegenerateDevServerToken, RegenerateDevServerTokenResponse), + (RenameDevServer, Ack) ); entity_messages!(