mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-26 20:22:30 +00:00
ssh: Overhaul remoting UI (#18727)
Release Notes: - N/A --------- Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
This commit is contained in:
parent
9c5bec5efb
commit
5aa165c530
8 changed files with 361 additions and 427 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -9002,7 +9002,6 @@ dependencies = [
|
|||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"markdown",
|
||||
"menu",
|
||||
"ordered-float 2.10.1",
|
||||
"picker",
|
||||
|
|
1
assets/icons/trash_alt.svg
Normal file
1
assets/icons/trash_alt.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
|
After Width: | Height: | Size: 330 B |
|
@ -22,7 +22,6 @@ futures.workspace = true
|
|||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
ordered-float.workspace = true
|
||||
picker.workspace = true
|
||||
|
|
|
@ -8,17 +8,18 @@ use anyhow::Result;
|
|||
use client::Client;
|
||||
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
|
||||
use editor::Editor;
|
||||
use gpui::pulsating_between;
|
||||
use gpui::AsyncWindowContext;
|
||||
use gpui::ClipboardItem;
|
||||
use gpui::PathPromptOptions;
|
||||
use gpui::Subscription;
|
||||
use gpui::Task;
|
||||
use gpui::WeakView;
|
||||
use gpui::{
|
||||
percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,
|
||||
FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext,
|
||||
percentage, Action, Animation, AnimationExt, AnyElement, AppContext, DismissEvent,
|
||||
EventEmitter, FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View,
|
||||
ViewContext,
|
||||
};
|
||||
use markdown::Markdown;
|
||||
use markdown::MarkdownStyle;
|
||||
use project::terminals::wrap_for_ssh;
|
||||
use project::terminals::SshCommand;
|
||||
use rpc::proto::RegenerateDevServerTokenResponse;
|
||||
|
@ -35,8 +36,8 @@ use terminal_view::terminal_panel::TerminalPanel;
|
|||
use ui::ElevationIndex;
|
||||
use ui::Section;
|
||||
use ui::{
|
||||
prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader,
|
||||
RadioWithLabel, Tooltip,
|
||||
prelude::*, IconButtonShape, Indicator, List, ListItem, Modal, ModalFooter, ModalHeader,
|
||||
Tooltip,
|
||||
};
|
||||
use ui_input::{FieldLabelLayout, TextField};
|
||||
use util::ResultExt;
|
||||
|
@ -62,7 +63,6 @@ pub struct DevServerProjects {
|
|||
workspace: WeakView<Workspace>,
|
||||
project_path_input: View<Editor>,
|
||||
dev_server_name_input: View<TextField>,
|
||||
markdown: View<Markdown>,
|
||||
_dev_server_subscription: Subscription,
|
||||
}
|
||||
|
||||
|
@ -132,26 +132,6 @@ impl DevServerProjects {
|
|||
..Default::default()
|
||||
});
|
||||
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: base_style,
|
||||
code_block: gpui::StyleRefinement {
|
||||
text: Some(gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Plex Mono".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
..Default::default()
|
||||
};
|
||||
let markdown =
|
||||
cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
|
||||
|
||||
Self {
|
||||
mode: Mode::Default(None),
|
||||
focus_handle,
|
||||
|
@ -159,7 +139,6 @@ impl DevServerProjects {
|
|||
dev_server_store,
|
||||
project_path_input,
|
||||
dev_server_name_input,
|
||||
markdown,
|
||||
workspace,
|
||||
_dev_server_subscription: subscription,
|
||||
}
|
||||
|
@ -845,7 +824,7 @@ impl DevServerProjects {
|
|||
})
|
||||
.child({
|
||||
let dev_server_id = dev_server.id;
|
||||
IconButton::new("remove-dev-server", IconName::Trash)
|
||||
IconButton::new("remove-dev-server", IconName::TrashAlt)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.delete_dev_server(dev_server_id, cx)
|
||||
}))
|
||||
|
@ -913,40 +892,73 @@ impl DevServerProjects {
|
|||
) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px(Spacing::Small.rems(cx) + Spacing::Small.rems(cx))
|
||||
.child(
|
||||
h_flex().group("ssh-server").justify_between().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
div()
|
||||
.id(("status", ix))
|
||||
.relative()
|
||||
.child(Icon::new(IconName::Server).size(IconSize::Small)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.max_w(rems(26.))
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.child(Label::new(ssh_connection.host.clone())),
|
||||
)
|
||||
.child(h_flex().visible_on_hover("ssh-server").gap_1().child({
|
||||
IconButton::new("remove-dev-server", IconName::Trash)
|
||||
.on_click(
|
||||
cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Remove Dev Server", cx))
|
||||
})),
|
||||
),
|
||||
h_flex()
|
||||
.w_full()
|
||||
.group("ssh-server")
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.w_full()
|
||||
.child(
|
||||
div()
|
||||
.id(("status", ix))
|
||||
.relative()
|
||||
.child(Icon::new(IconName::Server).size(IconSize::Small)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.max_w(rems(26.))
|
||||
.overflow_hidden()
|
||||
.whitespace_nowrap()
|
||||
.child(Label::new(ssh_connection.host.clone())),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.visible_on_hover("ssh-server")
|
||||
.gap_1()
|
||||
.child({
|
||||
IconButton::new("copy-dev-server-address", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.update_settings_file(cx, move |servers, cx| {
|
||||
if let Some(content) = servers
|
||||
.ssh_connections
|
||||
.as_ref()
|
||||
.and_then(|connections| {
|
||||
connections
|
||||
.get(ix)
|
||||
.map(|connection| connection.host.clone())
|
||||
})
|
||||
{
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||
content,
|
||||
));
|
||||
}
|
||||
});
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Copy Server Address", cx))
|
||||
})
|
||||
.child({
|
||||
IconButton::new("remove-dev-server", IconName::TrashAlt)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.delete_ssh_server(ix, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Remove Dev Server", cx))
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(cx.theme().colors().background)
|
||||
.border_1()
|
||||
.border_l_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.my_1()
|
||||
.mx_1p5()
|
||||
.py_0p5()
|
||||
.px_3()
|
||||
.child(
|
||||
|
@ -956,12 +968,17 @@ impl DevServerProjects {
|
|||
self.render_ssh_project(ix, &ssh_connection, pix, p, cx)
|
||||
}))
|
||||
.child(
|
||||
ListItem::new("new-remote_project")
|
||||
.start_slot(Icon::new(IconName::Plus))
|
||||
.child(Label::new("Open folder…"))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.create_ssh_project(ix, ssh_connection.clone(), cx);
|
||||
})),
|
||||
h_flex().child(
|
||||
Button::new("new-remote_project", "Open Folder…")
|
||||
.icon(IconName::Plus)
|
||||
.size(ButtonSize::Default)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.create_ssh_project(ix, ssh_connection.clone(), cx);
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -978,7 +995,8 @@ impl DevServerProjects {
|
|||
let project = project.clone();
|
||||
let server = server.clone();
|
||||
ListItem::new(("remote-project", ix))
|
||||
.start_slot(Icon::new(IconName::FileTree))
|
||||
.spacing(ui::ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Folder).color(Color::Muted))
|
||||
.child(Label::new(project.paths.join(", ")))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
let Some(app_state) = this
|
||||
|
@ -1014,7 +1032,7 @@ impl DevServerProjects {
|
|||
.detach();
|
||||
}))
|
||||
.end_hover_slot::<AnyElement>(Some(
|
||||
IconButton::new("remove-remote-project", IconName::Trash)
|
||||
IconButton::new("remove-remote-project", IconName::TrashAlt)
|
||||
.on_click(
|
||||
cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)),
|
||||
)
|
||||
|
@ -1026,7 +1044,7 @@ impl DevServerProjects {
|
|||
fn update_settings_file(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
f: impl FnOnce(&mut RemoteSettingsContent) + Send + Sync + 'static,
|
||||
f: impl FnOnce(&mut RemoteSettingsContent, &AppContext) + Send + Sync + 'static,
|
||||
) {
|
||||
let Some(fs) = self
|
||||
.workspace
|
||||
|
@ -1035,11 +1053,11 @@ impl DevServerProjects {
|
|||
else {
|
||||
return;
|
||||
};
|
||||
update_settings_file::<SshSettings>(fs, cx, move |setting, _| f(setting));
|
||||
update_settings_file::<SshSettings>(fs, cx, move |setting, cx| f(setting, cx));
|
||||
}
|
||||
|
||||
fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext<Self>) {
|
||||
self.update_settings_file(cx, move |setting| {
|
||||
self.update_settings_file(cx, move |setting, _| {
|
||||
if let Some(connections) = setting.ssh_connections.as_mut() {
|
||||
connections.remove(server);
|
||||
}
|
||||
|
@ -1047,7 +1065,7 @@ impl DevServerProjects {
|
|||
}
|
||||
|
||||
fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
|
||||
self.update_settings_file(cx, move |setting| {
|
||||
self.update_settings_file(cx, move |setting, _| {
|
||||
if let Some(server) = setting
|
||||
.ssh_connections
|
||||
.as_mut()
|
||||
|
@ -1063,7 +1081,7 @@ impl DevServerProjects {
|
|||
connection_options: remote::SshConnectionOptions,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.update_settings_file(cx, move |setting| {
|
||||
self.update_settings_file(cx, move |setting, _| {
|
||||
setting
|
||||
.ssh_connections
|
||||
.get_or_insert(Default::default())
|
||||
|
@ -1124,7 +1142,7 @@ impl DevServerProjects {
|
|||
}).detach();
|
||||
}
|
||||
}))
|
||||
.end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
|
||||
.end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::TrashAlt)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.delete_dev_server_project(dev_server_project_id, cx)
|
||||
}))
|
||||
|
@ -1148,250 +1166,109 @@ impl DevServerProjects {
|
|||
kind = NewServerKind::DirectSSH;
|
||||
}
|
||||
|
||||
let status = dev_server_id
|
||||
.map(|id| self.dev_server_store.read(cx).dev_server_status(id))
|
||||
.unwrap_or_default();
|
||||
|
||||
let name = self.dev_server_name_input.update(cx, |input, cx| {
|
||||
self.dev_server_name_input.update(cx, |input, cx| {
|
||||
input.editor().update(cx, |editor, cx| {
|
||||
if editor.text(cx).is_empty() {
|
||||
match kind {
|
||||
NewServerKind::DirectSSH => editor.set_placeholder_text("ssh host", cx),
|
||||
NewServerKind::LegacySSH => editor.set_placeholder_text("ssh host", cx),
|
||||
NewServerKind::Manual => editor.set_placeholder_text("example-host", cx),
|
||||
}
|
||||
editor.set_placeholder_text("ssh me@my.server / ssh@secret-box:2222", cx);
|
||||
}
|
||||
editor.text(cx)
|
||||
})
|
||||
});
|
||||
|
||||
const MANUAL_SETUP_MESSAGE: &str =
|
||||
"Generate a token for this server and follow the steps to set Zed up on that machine.";
|
||||
const SSH_SETUP_MESSAGE: &str =
|
||||
"Enter the command you use to SSH into this server.\nFor example: `ssh me@my.server` or `ssh me@secret-box:2222`.";
|
||||
|
||||
Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
|
||||
.header(
|
||||
ModalHeader::new()
|
||||
.headline("Create Dev Server")
|
||||
.show_back_button(true),
|
||||
)
|
||||
.section(
|
||||
Section::new()
|
||||
.header(if kind == NewServerKind::Manual {
|
||||
"Server Name".into()
|
||||
} else {
|
||||
"SSH arguments".into()
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.max_w(rems(16.))
|
||||
.child(self.dev_server_name_input.clone()),
|
||||
),
|
||||
)
|
||||
.section(
|
||||
Section::new_contained()
|
||||
.header("Connection Method".into())
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.px_2()
|
||||
.gap_y(Spacing::Large.rems(cx))
|
||||
.when(ssh_prompt.is_none(), |el| {
|
||||
el.child(
|
||||
v_flex()
|
||||
.when(use_direct_ssh, |el| {
|
||||
el.child(RadioWithLabel::new(
|
||||
"use-server-name-in-ssh",
|
||||
Label::new("Connect via SSH (default)"),
|
||||
NewServerKind::DirectSSH == kind,
|
||||
cx.listener({
|
||||
move |this, _, cx| {
|
||||
if let Mode::CreateDevServer(
|
||||
CreateDevServer { kind, .. },
|
||||
) = &mut this.mode
|
||||
{
|
||||
*kind = NewServerKind::DirectSSH;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
}),
|
||||
))
|
||||
})
|
||||
.when(!use_direct_ssh, |el| {
|
||||
el.child(RadioWithLabel::new(
|
||||
"use-server-name-in-ssh",
|
||||
Label::new("Configure over SSH (default)"),
|
||||
kind == NewServerKind::LegacySSH,
|
||||
cx.listener({
|
||||
move |this, _, cx| {
|
||||
if let Mode::CreateDevServer(
|
||||
CreateDevServer { kind, .. },
|
||||
) = &mut this.mode
|
||||
{
|
||||
*kind = NewServerKind::LegacySSH;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
}),
|
||||
))
|
||||
})
|
||||
.child(RadioWithLabel::new(
|
||||
"use-server-name-in-ssh",
|
||||
Label::new("Configure manually"),
|
||||
kind == NewServerKind::Manual,
|
||||
cx.listener({
|
||||
move |this, _, cx| {
|
||||
if let Mode::CreateDevServer(
|
||||
CreateDevServer { kind, .. },
|
||||
) = &mut this.mode
|
||||
{
|
||||
*kind = NewServerKind::Manual;
|
||||
}
|
||||
cx.notify()
|
||||
}
|
||||
}),
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when(dev_server_id.is_none() && ssh_prompt.is_none(), |el| {
|
||||
el.child(
|
||||
if kind == NewServerKind::Manual {
|
||||
Label::new(MANUAL_SETUP_MESSAGE)
|
||||
} else {
|
||||
Label::new(SSH_SETUP_MESSAGE)
|
||||
}
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when_some(ssh_prompt, |el, ssh_prompt| el.child(ssh_prompt))
|
||||
.when(dev_server_id.is_some() && access_token.is_none(), |el| {
|
||||
el.child(
|
||||
if kind == NewServerKind::Manual {
|
||||
Label::new(
|
||||
"Note: updating the dev server generate a new token",
|
||||
)
|
||||
} else {
|
||||
Label::new(SSH_SETUP_MESSAGE)
|
||||
}
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
})
|
||||
.when_some(access_token.clone(), {
|
||||
|el, access_token| {
|
||||
el.child(self.render_dev_server_token_creating(
|
||||
access_token,
|
||||
name,
|
||||
kind,
|
||||
status,
|
||||
creating,
|
||||
cx,
|
||||
))
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new().end_slot(if status == DevServerStatus::Online {
|
||||
Button::new("create-dev-server", "Done")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
cx.focus(&this.focus_handle);
|
||||
this.mode = Mode::Default(None);
|
||||
cx.notify();
|
||||
}))
|
||||
} else {
|
||||
Button::new(
|
||||
"create-dev-server",
|
||||
if kind == NewServerKind::Manual {
|
||||
if dev_server_id.is_some() {
|
||||
"Update"
|
||||
} else {
|
||||
"Create"
|
||||
}
|
||||
} else if dev_server_id.is_some() {
|
||||
"Reconnect"
|
||||
} else {
|
||||
"Connect"
|
||||
},
|
||||
)
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.disabled(creating && dev_server_id.is_none())
|
||||
.on_click(cx.listener({
|
||||
let access_token = access_token.clone();
|
||||
move |this, _, cx| {
|
||||
if kind == NewServerKind::DirectSSH {
|
||||
this.create_ssh_server(cx);
|
||||
return;
|
||||
}
|
||||
this.create_or_update_dev_server(
|
||||
kind,
|
||||
dev_server_id,
|
||||
access_token.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_dev_server_token_creating(
|
||||
&self,
|
||||
access_token: String,
|
||||
dev_server_name: String,
|
||||
kind: NewServerKind,
|
||||
status: DevServerStatus,
|
||||
creating: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Div {
|
||||
self.markdown.update(cx, |markdown, cx| {
|
||||
if kind == NewServerKind::Manual {
|
||||
markdown.reset(format!("Please log into '{}'. If you don't yet have Zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen, to start Zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx);
|
||||
} else {
|
||||
markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using the manual setup.".to_string(), cx);
|
||||
}
|
||||
});
|
||||
|
||||
let theme = cx.theme();
|
||||
v_flex()
|
||||
.pl_2()
|
||||
.pt_2()
|
||||
.gap_2()
|
||||
.child(v_flex().w_full().text_sm().child(self.markdown.clone()))
|
||||
.map(|el| {
|
||||
if status == DevServerStatus::Offline && kind != NewServerKind::Manual && !creating
|
||||
{
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Disconnected).size(IconSize::Medium))
|
||||
.child(Label::new("Not connected")),
|
||||
)
|
||||
} else if status == DevServerStatus::Offline {
|
||||
el.child(Self::render_loading_spinner("Waiting for connection…"))
|
||||
} else {
|
||||
el.child(Label::new("🎊 Connection established!"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_loading_spinner(label: impl Into<SharedString>) -> Div {
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.id("create-dev-server")
|
||||
.overflow_hidden()
|
||||
.size_full()
|
||||
.flex_1()
|
||||
.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))),
|
||||
h_flex()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.items_center()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.child(
|
||||
IconButton::new("cancel-dev-server-creation", IconName::ArrowLeft)
|
||||
.shape(IconButtonShape::Square)
|
||||
.on_click(|_, cx| {
|
||||
cx.dispatch_action(menu::Cancel.boxed_clone());
|
||||
}),
|
||||
)
|
||||
.child(Label::new("Connect New Dev Server")),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.p_3()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.child(Label::new("SSH Arguments"))
|
||||
.child(
|
||||
Label::new("Enter the command you use to SSH into this server.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_2()
|
||||
.w_full()
|
||||
.gap_2()
|
||||
.child(self.dev_server_name_input.clone())
|
||||
.child(
|
||||
Button::new("create-dev-server", "Connect Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.disabled(creating && dev_server_id.is_none())
|
||||
.on_click(cx.listener({
|
||||
let access_token = access_token.clone();
|
||||
move |this, _, cx| {
|
||||
if kind == NewServerKind::DirectSSH {
|
||||
this.create_ssh_server(cx);
|
||||
return;
|
||||
}
|
||||
this.create_or_update_dev_server(
|
||||
kind,
|
||||
dev_server_id,
|
||||
access_token.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(Label::new(label))
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(theme.colors().editor_background)
|
||||
.w_full()
|
||||
.map(|this| {
|
||||
if let Some(ssh_prompt) = ssh_prompt {
|
||||
this.child(h_flex().w_full().child(ssh_prompt))
|
||||
} else {
|
||||
let color = Color::Muted.color(cx);
|
||||
this.child(
|
||||
h_flex()
|
||||
.p_2()
|
||||
.w_full()
|
||||
.content_center()
|
||||
.gap_2()
|
||||
.child(h_flex().w_full())
|
||||
.child(
|
||||
div().p_1().rounded_lg().bg(color).with_animation(
|
||||
"pulse-ssh-waiting-for-connection",
|
||||
Animation::new(Duration::from_secs(2))
|
||||
.repeat()
|
||||
.with_easing(pulsating_between(0.2, 0.5)),
|
||||
move |this, progress| this.bg(color.opacity(progress)),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Waiting for connection…")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
.child(h_flex().w_full()),
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
|
@ -1416,64 +1293,73 @@ impl DevServerProjects {
|
|||
creating_dev_server = Some(*dev_server_id);
|
||||
};
|
||||
|
||||
let footer = format!("Connections: {}", ssh_connections.len() + dev_servers.len());
|
||||
Modal::new("remote-projects", Some(self.scroll_handle.clone()))
|
||||
.header(
|
||||
ModalHeader::new()
|
||||
.show_dismiss_button(true)
|
||||
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::Small)),
|
||||
ModalHeader::new().child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::XSmall))
|
||||
.child(
|
||||
Button::new("register-dev-server-button", "Connect New Server")
|
||||
.style(ButtonStyle::Filled)
|
||||
.layer(ElevationIndex::ModalSurface)
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||
kind: if SshSettings::get_global(cx).use_direct_ssh() {
|
||||
NewServerKind::DirectSSH
|
||||
} else {
|
||||
NewServerKind::LegacySSH
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
this.dev_server_name_input.update(cx, |text_field, cx| {
|
||||
text_field.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("", cx);
|
||||
});
|
||||
});
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
),
|
||||
)
|
||||
.section(
|
||||
Section::new().child(
|
||||
div().child(
|
||||
List::new()
|
||||
.empty_message("No dev servers registered yet.")
|
||||
.header(Some(
|
||||
ListHeader::new("Connections").end_slot(
|
||||
Button::new("register-dev-server-button", "Connect New Server")
|
||||
.icon(IconName::Plus)
|
||||
.icon_position(IconPosition::Start)
|
||||
.icon_color(Color::Muted)
|
||||
.on_click(cx.listener(|this, _, cx| {
|
||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||
kind: if SshSettings::get_global(cx)
|
||||
.use_direct_ssh()
|
||||
{
|
||||
NewServerKind::DirectSSH
|
||||
} else {
|
||||
NewServerKind::LegacySSH
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
this.dev_server_name_input.update(
|
||||
cx,
|
||||
|text_field, cx| {
|
||||
text_field.editor().update(cx, |editor, cx| {
|
||||
editor.set_text("", cx);
|
||||
});
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
})),
|
||||
),
|
||||
))
|
||||
.children(ssh_connections.iter().cloned().enumerate().map(
|
||||
|(ix, connection)| {
|
||||
self.render_ssh_connection(ix, connection, cx)
|
||||
.into_any_element()
|
||||
},
|
||||
))
|
||||
.children(dev_servers.iter().map(|dev_server| {
|
||||
let creating = if creating_dev_server == Some(dev_server.id) {
|
||||
is_creating
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.render_dev_server(dev_server, creating, cx)
|
||||
.into_any_element()
|
||||
})),
|
||||
),
|
||||
Section::new().padded(false).child(
|
||||
div()
|
||||
.border_y_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.w_full()
|
||||
.child(
|
||||
div().p_2().child(
|
||||
List::new()
|
||||
.empty_message("No dev servers registered yet.")
|
||||
.children(ssh_connections.iter().cloned().enumerate().map(
|
||||
|(ix, connection)| {
|
||||
self.render_ssh_connection(ix, connection, cx)
|
||||
.into_any_element()
|
||||
},
|
||||
))
|
||||
.children(dev_servers.iter().map(|dev_server| {
|
||||
let creating = if creating_dev_server == Some(dev_server.id)
|
||||
{
|
||||
is_creating
|
||||
} else {
|
||||
None
|
||||
};
|
||||
self.render_dev_server(dev_server, creating, cx)
|
||||
.into_any_element()
|
||||
})),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.footer(
|
||||
ModalFooter::new()
|
||||
.start_slot(div().child(Label::new(footer).size(LabelSize::Small))),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1501,7 +1387,6 @@ impl Render for DevServerProjects {
|
|||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.track_focus(&self.focus_handle)
|
||||
.p_2()
|
||||
.elevation_3(cx)
|
||||
.key_context("DevServerModal")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
|
|
|
@ -5,9 +5,9 @@ use auto_update::AutoUpdater;
|
|||
use editor::Editor;
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
||||
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
|
||||
Transformation, View,
|
||||
percentage, px, Action, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext,
|
||||
DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion,
|
||||
SharedString, Task, Transformation, View,
|
||||
};
|
||||
use gpui::{AppContext, Model};
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
|
@ -16,9 +16,9 @@ use schemars::JsonSchema;
|
|||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use ui::{
|
||||
h_flex, v_flex, Color, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement,
|
||||
IntoElement, Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext,
|
||||
WindowContext,
|
||||
div, h_flex, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, FluentBuilder as _, Icon,
|
||||
IconButton, IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled,
|
||||
StyledExt as _, Tooltip, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use workspace::{AppState, ModalView, Workspace};
|
||||
|
||||
|
@ -140,47 +140,57 @@ impl SshPrompt {
|
|||
}
|
||||
|
||||
impl Render for SshPrompt {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.key_context("PasswordPrompt")
|
||||
.p_4()
|
||||
.size_full()
|
||||
.justify_start()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(if self.error_message.is_some() {
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Medium)
|
||||
.color(Color::Error)
|
||||
.into_any_element()
|
||||
} else {
|
||||
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)))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
v_flex()
|
||||
.p_4()
|
||||
.size_full()
|
||||
.child(
|
||||
Label::new(format!("ssh {}…", self.connection_string))
|
||||
.size(ui::LabelSize::Large),
|
||||
),
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(h_flex().w_full())
|
||||
.child(if self.error_message.is_some() {
|
||||
Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Medium)
|
||||
.color(Color::Error)
|
||||
.into_any_element()
|
||||
} else {
|
||||
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,
|
||||
)))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
.child(Label::new(format!(
|
||||
"Connecting to {}…",
|
||||
self.connection_string
|
||||
)))
|
||||
.child(h_flex().w_full()),
|
||||
)
|
||||
.when_some(self.error_message.as_ref(), |el, error| {
|
||||
el.child(Label::new(error.clone()))
|
||||
})
|
||||
.when(
|
||||
self.error_message.is_none() && self.status_message.is_some(),
|
||||
|el| el.child(Label::new(self.status_message.clone().unwrap())),
|
||||
)
|
||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
el.child(Label::new(prompt.0.clone()))
|
||||
.child(self.editor.clone())
|
||||
}),
|
||||
)
|
||||
.when_some(self.error_message.as_ref(), |el, error| {
|
||||
el.child(Label::new(error.clone()))
|
||||
})
|
||||
.when(
|
||||
self.error_message.is_none() && self.status_message.is_some(),
|
||||
|el| el.child(Label::new(self.status_message.clone().unwrap())),
|
||||
)
|
||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
el.child(Label::new(prompt.0.clone()))
|
||||
.child(self.editor.clone())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,14 +212,41 @@ impl SshConnectionModal {
|
|||
|
||||
impl Render for SshConnectionModal {
|
||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||
let connection_string = self.prompt.read(cx).connection_string.clone();
|
||||
let theme = cx.theme();
|
||||
let header_color = theme.colors().element_background;
|
||||
let body_color = theme.colors().background;
|
||||
v_flex()
|
||||
.elevation_3(cx)
|
||||
.p_4()
|
||||
.gap_2()
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.w(px(400.))
|
||||
.child(self.prompt.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.p_1()
|
||||
.border_b_1()
|
||||
.border_color(theme.colors().border)
|
||||
.bg(header_color)
|
||||
.justify_between()
|
||||
.child(
|
||||
IconButton::new("ssh-connection-cancel", IconName::ArrowLeft)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.on_click(|_, cx| cx.dispatch_action(menu::Cancel.boxed_clone()))
|
||||
.tooltip(|cx| Tooltip::for_action("Back", &menu::Cancel, cx)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(Icon::new(IconName::Server).size(IconSize::XSmall))
|
||||
.child(
|
||||
Label::new(connection_string)
|
||||
.size(ui::LabelSize::Small)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
.child(div()),
|
||||
)
|
||||
.child(h_flex().bg(body_color).w_full().child(self.prompt.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -275,6 +275,7 @@ pub enum IconName {
|
|||
Tab,
|
||||
Terminal,
|
||||
Trash,
|
||||
TrashAlt,
|
||||
TriangleRight,
|
||||
Undo,
|
||||
Unpin,
|
||||
|
|
|
@ -262,6 +262,7 @@ impl RenderOnce for ModalFooter {
|
|||
#[derive(IntoElement)]
|
||||
pub struct Section {
|
||||
contained: bool,
|
||||
padded: bool,
|
||||
header: Option<SectionHeader>,
|
||||
meta: Option<SharedString>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
|
@ -277,6 +278,7 @@ impl Section {
|
|||
pub fn new() -> Self {
|
||||
Self {
|
||||
contained: false,
|
||||
padded: true,
|
||||
header: None,
|
||||
meta: None,
|
||||
children: SmallVec::new(),
|
||||
|
@ -286,6 +288,7 @@ impl Section {
|
|||
pub fn new_contained() -> Self {
|
||||
Self {
|
||||
contained: true,
|
||||
padded: true,
|
||||
header: None,
|
||||
meta: None,
|
||||
children: SmallVec::new(),
|
||||
|
@ -306,6 +309,10 @@ impl Section {
|
|||
self.meta = Some(meta.into());
|
||||
self
|
||||
}
|
||||
pub fn padded(mut self, padded: bool) -> Self {
|
||||
self.padded = padded;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for Section {
|
||||
|
@ -320,22 +327,27 @@ impl RenderOnce for Section {
|
|||
section_bg.fade_out(0.96);
|
||||
|
||||
let children = if self.contained {
|
||||
v_flex().flex_1().px(Spacing::XLarge.rems(cx)).child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(section_bg)
|
||||
.py(Spacing::Medium.rems(cx))
|
||||
.gap_y(Spacing::Small.rems(cx))
|
||||
.child(div().flex().flex_1().size_full().children(self.children)),
|
||||
)
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.when(self.padded, |this| this.px(Spacing::XLarge.rems(cx)))
|
||||
.child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.rounded_md()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(section_bg)
|
||||
.py(Spacing::Medium.rems(cx))
|
||||
.gap_y(Spacing::Small.rems(cx))
|
||||
.child(div().flex().flex_1().size_full().children(self.children)),
|
||||
)
|
||||
} else {
|
||||
v_flex()
|
||||
.w_full()
|
||||
.gap_y(Spacing::Small.rems(cx))
|
||||
.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))
|
||||
.when(self.padded, |this| {
|
||||
this.px(Spacing::Medium.rems(cx) + Spacing::Medium.rems(cx))
|
||||
})
|
||||
.children(self.children)
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use gpui::{hsla, Styled, WindowContext};
|
|||
use crate::prelude::*;
|
||||
use crate::ElevationIndex;
|
||||
|
||||
fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
|
||||
fn elevated<E: Styled>(this: E, cx: &WindowContext, index: ElevationIndex) -> E {
|
||||
this.bg(cx.theme().colors().elevated_surface_background)
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
|
@ -11,7 +11,7 @@ fn elevated<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -
|
|||
.shadow(index.shadow())
|
||||
}
|
||||
|
||||
fn elevated_borderless<E: Styled>(this: E, cx: &mut WindowContext, index: ElevationIndex) -> E {
|
||||
fn elevated_borderless<E: Styled>(this: E, cx: &WindowContext, index: ElevationIndex) -> E {
|
||||
this.bg(cx.theme().colors().elevated_surface_background)
|
||||
.rounded_lg()
|
||||
.shadow(index.shadow())
|
||||
|
@ -38,14 +38,14 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||
///
|
||||
/// Example Elements: Title Bar, Panel, Tab Bar, Editor
|
||||
fn elevation_1(self, cx: &mut WindowContext) -> Self {
|
||||
fn elevation_1(self, cx: &WindowContext) -> Self {
|
||||
elevated(self, cx, ElevationIndex::Surface)
|
||||
}
|
||||
|
||||
/// See [`elevation_1`].
|
||||
///
|
||||
/// Renders a borderless version [`elevation_1`].
|
||||
fn elevation_1_borderless(self, cx: &mut WindowContext) -> Self {
|
||||
fn elevation_1_borderless(self, cx: &WindowContext) -> Self {
|
||||
elevated_borderless(self, cx, ElevationIndex::Surface)
|
||||
}
|
||||
|
||||
|
@ -54,14 +54,14 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||
///
|
||||
/// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels
|
||||
fn elevation_2(self, cx: &mut WindowContext) -> Self {
|
||||
fn elevation_2(self, cx: &WindowContext) -> Self {
|
||||
elevated(self, cx, ElevationIndex::ElevatedSurface)
|
||||
}
|
||||
|
||||
/// See [`elevation_2`].
|
||||
///
|
||||
/// Renders a borderless version [`elevation_2`].
|
||||
fn elevation_2_borderless(self, cx: &mut WindowContext) -> Self {
|
||||
fn elevation_2_borderless(self, cx: &WindowContext) -> Self {
|
||||
elevated_borderless(self, cx, ElevationIndex::ElevatedSurface)
|
||||
}
|
||||
|
||||
|
@ -74,24 +74,24 @@ pub trait StyledExt: Styled + Sized {
|
|||
/// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()`
|
||||
///
|
||||
/// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs
|
||||
fn elevation_3(self, cx: &mut WindowContext) -> Self {
|
||||
fn elevation_3(self, cx: &WindowContext) -> Self {
|
||||
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||
}
|
||||
|
||||
/// See [`elevation_3`].
|
||||
///
|
||||
/// Renders a borderless version [`elevation_3`].
|
||||
fn elevation_3_borderless(self, cx: &mut WindowContext) -> Self {
|
||||
fn elevation_3_borderless(self, cx: &WindowContext) -> Self {
|
||||
elevated_borderless(self, cx, ElevationIndex::ModalSurface)
|
||||
}
|
||||
|
||||
/// The theme's primary border color.
|
||||
fn border_primary(self, cx: &mut WindowContext) -> Self {
|
||||
fn border_primary(self, cx: &WindowContext) -> Self {
|
||||
self.border_color(cx.theme().colors().border)
|
||||
}
|
||||
|
||||
/// The theme's secondary or muted border color.
|
||||
fn border_muted(self, cx: &mut WindowContext) -> Self {
|
||||
fn border_muted(self, cx: &WindowContext) -> Self {
|
||||
self.border_color(cx.theme().colors().border_variant)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue