first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -1,14 +1,12 @@
|
||||
use galaxyui::elements::{ChildView, Container, Dismiss, Empty};
|
||||
use galaxyui::ui_components::components::UiComponent;
|
||||
use galaxyui::{
|
||||
elements::{ChildView, Container, Dismiss, Empty},
|
||||
ui_components::components::UiComponent,
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
appearance::Appearance,
|
||||
ui_components::dialog::{dialog_styles, Dialog},
|
||||
view_components::action_button::{ActionButton, DangerPrimaryTheme, NakedTheme},
|
||||
};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::ui_components::dialog::{dialog_styles, Dialog};
|
||||
use crate::view_components::action_button::{ActionButton, DangerPrimaryTheme, NakedTheme};
|
||||
|
||||
const DIALOG_WIDTH: f32 = 450.;
|
||||
pub enum DestructiveMCPConfirmationDialogEvent {
|
||||
|
||||
@@ -1,64 +1,63 @@
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use std::sync::Arc;
|
||||
use std::{collections::HashMap, path::Path};
|
||||
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use diesel::SqliteConnection;
|
||||
use galaxy_core::{
|
||||
send_telemetry_from_ctx,
|
||||
ui::{appearance::Appearance, theme::color::internal_colors},
|
||||
};
|
||||
use galaxy_editor::{
|
||||
content::buffer::InitialBufferState, render::element::VerticalExpansionBehavior,
|
||||
#[cfg(feature = "local_fs")]
|
||||
use parking_lot::Mutex;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use settings::Setting as _;
|
||||
use uuid::Uuid;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_editor::content::buffer::InitialBufferState;
|
||||
use galaxy_editor::render::element::VerticalExpansionBehavior;
|
||||
use galaxyui::elements::{
|
||||
Border, ChildAnchor, ChildView, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Radius, Shrinkable, Stack, Text,
|
||||
};
|
||||
use galaxyui::platform::Cursor;
|
||||
use galaxyui::ui_components::components::UiComponent;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ChildAnchor, ChildView, Container, CornerRadius, CrossAxisAlignment, Flex,
|
||||
MainAxisAlignment, MainAxisSize, MouseStateHandle, OffsetPositioning, ParentAnchor,
|
||||
ParentElement, ParentOffsetBounds, Radius, Shrinkable, Stack, Text,
|
||||
},
|
||||
platform::Cursor,
|
||||
ui_components::components::UiComponent,
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
use parking_lot::Mutex;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::{
|
||||
ai::{
|
||||
blocklist::secret_redaction::find_secrets_in_text,
|
||||
mcp::{
|
||||
parsing::{prettify_json, resolve_json, ParsedTemplatableMCPServerResult},
|
||||
templatable::CloudTemplatableMCPServer,
|
||||
MCPServer, TemplatableMCPServer, TemplatableMCPServerInstallation,
|
||||
TemplatableMCPServerManager, TransportType,
|
||||
},
|
||||
},
|
||||
banner::{Banner, BannerTextContent},
|
||||
cloud_object::{CloudObject, Space},
|
||||
code::editor::view::{CodeEditorRenderOptions, CodeEditorView},
|
||||
persistence::ModelEvent,
|
||||
server::{
|
||||
cloud_objects::update_manager::InitiatedBy,
|
||||
telemetry::{MCPTemplateCreationSource, TelemetryEvent},
|
||||
},
|
||||
settings_view::mcp_servers::{
|
||||
destructive_mcp_confirmation_dialog::{
|
||||
DestructiveMCPConfirmationDialog, DestructiveMCPConfirmationDialogEvent,
|
||||
DestructiveMCPConfirmationDialogVariant,
|
||||
},
|
||||
style, ServerCardItemId,
|
||||
},
|
||||
ui_components::{buttons::icon_button, icons::Icon},
|
||||
view_components::{
|
||||
action_button::{ActionButton, DangerNakedTheme, DangerSecondaryTheme, PrimaryTheme},
|
||||
DismissibleToast,
|
||||
},
|
||||
workspace::ToastStack,
|
||||
GlobalResourceHandlesProvider,
|
||||
use crate::ai::blocklist::secret_redaction::find_secrets_in_text;
|
||||
use crate::ai::mcp::parsing::{prettify_json, resolve_json, ParsedTemplatableMCPServerResult};
|
||||
use crate::ai::mcp::templatable::CloudTemplatableMCPServer;
|
||||
use crate::ai::mcp::{
|
||||
MCPServer, TemplatableMCPServer, TemplatableMCPServerInstallation, TemplatableMCPServerManager,
|
||||
TransportType,
|
||||
};
|
||||
use crate::banner::{Banner, BannerTextContent};
|
||||
use crate::cloud_object::{CloudObject, Space};
|
||||
use crate::code::editor::view::{CodeEditorRenderOptions, CodeEditorView};
|
||||
use crate::persistence::ModelEvent;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::persistence::{database_file_path_for_scope, establish_ro_connection, PersistenceScope};
|
||||
use crate::server::cloud_objects::update_manager::InitiatedBy;
|
||||
use crate::server::telemetry::{MCPTemplateCreationSource, TelemetryEvent};
|
||||
use crate::settings_view::mcp_servers::destructive_mcp_confirmation_dialog::{
|
||||
DestructiveMCPConfirmationDialog, DestructiveMCPConfirmationDialogEvent,
|
||||
DestructiveMCPConfirmationDialogVariant,
|
||||
};
|
||||
use crate::settings_view::mcp_servers::{style, ServerCardItemId};
|
||||
use crate::terminal::safe_mode_settings::SafeModeSettings;
|
||||
use crate::ui_components::buttons::icon_button;
|
||||
use crate::ui_components::icons::Icon;
|
||||
use crate::view_components::action_button::{
|
||||
ActionButton, DangerNakedTheme, DangerSecondaryTheme, PrimaryTheme,
|
||||
};
|
||||
use crate::view_components::DismissibleToast;
|
||||
use crate::workspace::ToastStack;
|
||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||
use crate::GlobalResourceHandlesProvider;
|
||||
|
||||
const DEFAULT_JSON_TEXT: &str = r#"{
|
||||
"": {
|
||||
@@ -124,7 +123,7 @@ pub struct MCPServersEditPageView {
|
||||
log_out_icon_button_mouse_handle: MouseStateHandle,
|
||||
editing_disabled_banner: ViewHandle<Banner<()>>,
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[cfg(feature = "local_fs")]
|
||||
#[allow(dead_code)]
|
||||
database_connection: Option<Arc<Mutex<SqliteConnection>>>,
|
||||
}
|
||||
@@ -175,7 +174,7 @@ impl MCPServersEditPageView {
|
||||
true,
|
||||
),
|
||||
);
|
||||
editor.set_language_with_path(Path::new("mcp.json"), ctx);
|
||||
editor.set_language_with_local_path(Path::new("/mcp.json"), ctx);
|
||||
editor
|
||||
});
|
||||
|
||||
@@ -192,15 +191,14 @@ impl MCPServersEditPageView {
|
||||
.with_icon(Icon::Warning)
|
||||
});
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
let database_connection =
|
||||
crate::persistence::database_file_path()
|
||||
.to_str()
|
||||
.and_then(|db_url| {
|
||||
crate::persistence::establish_ro_connection(db_url)
|
||||
.ok()
|
||||
.map(|conn| Arc::new(Mutex::new(conn)))
|
||||
});
|
||||
#[cfg(feature = "local_fs")]
|
||||
let database_connection = database_file_path_for_scope(&PersistenceScope::App)
|
||||
.to_str()
|
||||
.and_then(|db_url| {
|
||||
establish_ro_connection(db_url)
|
||||
.ok()
|
||||
.map(|conn| Arc::new(Mutex::new(conn)))
|
||||
});
|
||||
|
||||
Self {
|
||||
server_card_item_id: None,
|
||||
@@ -215,7 +213,7 @@ impl MCPServersEditPageView {
|
||||
log_out_icon_button_mouse_handle: Default::default(),
|
||||
editing_disabled_banner,
|
||||
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
#[cfg(feature = "local_fs")]
|
||||
database_connection,
|
||||
}
|
||||
}
|
||||
@@ -531,10 +529,13 @@ impl MCPServersEditPageView {
|
||||
ctx: &mut ViewContext<Self>,
|
||||
templatable_mcp_server: &TemplatableMCPServer,
|
||||
) -> Result<(), String> {
|
||||
let safe_mode_enabled = *SafeModeSettings::as_ref(ctx).safe_mode_enabled.value();
|
||||
let enterprise_enforced =
|
||||
UserWorkspaces::as_ref(ctx).is_enterprise_secret_redaction_enabled();
|
||||
let contains_secrets =
|
||||
!find_secrets_in_text(&templatable_mcp_server.template.json).is_empty();
|
||||
|
||||
if contains_secrets {
|
||||
if should_block_save_for_secrets(safe_mode_enabled, enterprise_enforced, contains_secrets) {
|
||||
let window_id = ctx.window_id();
|
||||
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
toast_stack.add_ephemeral_toast(
|
||||
@@ -903,6 +904,19 @@ impl TypedActionView for MCPServersEditPageView {
|
||||
return;
|
||||
}
|
||||
|
||||
if parsed_servers
|
||||
.iter()
|
||||
.try_for_each(|parsed_server| {
|
||||
self.detect_secrets_in_templatable_mcp_server(
|
||||
ctx,
|
||||
&parsed_server.templatable_mcp_server,
|
||||
)
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
for parsed_server in parsed_servers {
|
||||
TemplatableMCPServerManager::handle(ctx).update(
|
||||
ctx,
|
||||
@@ -949,3 +963,22 @@ impl TypedActionView for MCPServersEditPageView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide whether to block saving an MCP server config because secret
|
||||
/// redaction is in force AND the parsed config contains secret-shaped strings.
|
||||
///
|
||||
/// We block only when redaction is actually active — either the user-level
|
||||
/// Settings > Privacy > Secret redaction toggle is on, or the user's workspace
|
||||
/// has enterprise enforcement enabled. With both off, the user has explicitly
|
||||
/// opted to embed secrets in the config and we save it as written (#8761).
|
||||
fn should_block_save_for_secrets(
|
||||
safe_mode_enabled: bool,
|
||||
enterprise_enforced: bool,
|
||||
contains_secrets: bool,
|
||||
) -> bool {
|
||||
(safe_mode_enabled || enterprise_enforced) && contains_secrets
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "edit_page_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
use super::should_block_save_for_secrets;
|
||||
|
||||
/// #8761: with redaction disabled and no enterprise enforcement, saving a
|
||||
/// config that contains secrets must NOT be blocked.
|
||||
#[test]
|
||||
fn does_not_block_when_redaction_off_even_if_secrets_present() {
|
||||
assert!(!should_block_save_for_secrets(false, false, true));
|
||||
}
|
||||
|
||||
/// User-level toggle on AND secrets present → block. This is the case the
|
||||
/// original check was written to catch; the redaction-aware predicate
|
||||
/// must preserve it.
|
||||
#[test]
|
||||
fn blocks_when_user_redaction_on_and_secrets_present() {
|
||||
assert!(should_block_save_for_secrets(true, false, true));
|
||||
}
|
||||
|
||||
/// Enterprise enforcement alone is enough to gate the save, even if the
|
||||
/// user toggled their personal redaction off — orgs that mandate redaction
|
||||
/// must not be bypassed at the MCP-config layer.
|
||||
#[test]
|
||||
fn blocks_when_enterprise_enforced_and_secrets_present() {
|
||||
assert!(should_block_save_for_secrets(false, true, true));
|
||||
}
|
||||
|
||||
/// Configs without any detected secrets are never blocked, regardless of
|
||||
/// the redaction-toggle state. The check is purely a guard against
|
||||
/// accidentally persisting secrets — it has nothing to add when none exist.
|
||||
#[test]
|
||||
fn does_not_block_when_no_secrets_regardless_of_toggle() {
|
||||
for safe_mode in [false, true] {
|
||||
for enterprise in [false, true] {
|
||||
assert!(
|
||||
!should_block_save_for_secrets(safe_mode, enterprise, false),
|
||||
"expected no block when contains_secrets=false \
|
||||
(safe_mode={safe_mode}, enterprise={enterprise})",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Both toggles on AND secrets present → block. Defensive: equivalent to
|
||||
/// either one being on, but exhaustively pinned for the full 2x2x2 sweep
|
||||
/// of (safe_mode, enterprise, contains_secrets).
|
||||
#[test]
|
||||
fn blocks_when_both_redactions_on_and_secrets_present() {
|
||||
assert!(should_block_save_for_secrets(true, true, true));
|
||||
}
|
||||
@@ -1,42 +1,37 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use markdown_parser::parse_markdown;
|
||||
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use warpui::elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty,
|
||||
Flex, FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment,
|
||||
MouseStateHandle, Padding, ParentElement, Radius, Shrinkable, Text,
|
||||
};
|
||||
use warpui::fonts::{Properties, Weight};
|
||||
use warpui::keymap::Keystroke;
|
||||
use warpui::platform::Cursor;
|
||||
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
|
||||
use warpui::{
|
||||
AppContext, Element, Entity, FocusContext, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
ViewHandle,
|
||||
};
|
||||
|
||||
use crate::ai::mcp::templatable_installation::{VariableType, VariableValue};
|
||||
use crate::ai::mcp::{TemplatableMCPServer, TemplatableMCPServerManager, TemplateVariable};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::editor::Event as EditorEvent;
|
||||
use crate::editor::{EditorView, SingleLineEditorOptions};
|
||||
use crate::editor::{EditorView, Event as EditorEvent, SingleLineEditorOptions};
|
||||
use crate::settings_view::mcp_servers::style::{
|
||||
INSTALLATION_MODAL_BUTTON_GAP, INSTALLATION_MODAL_BUTTON_PADDING,
|
||||
INSTALLATION_MODAL_INPUT_VERTICAL_SPACING, INSTALLATION_MODAL_LABEL_VERTICAL_SPACING,
|
||||
INSTALLATION_MODAL_PADDING, INSTALLATION_MODAL_TITLE_VERTICAL_SPACING,
|
||||
};
|
||||
use crate::ui_components::avatar::{Avatar, AvatarContent};
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::view_components::action_button::{
|
||||
ActionButton, KeystrokeSource, NakedTheme, PrimaryTheme,
|
||||
};
|
||||
use crate::view_components::dropdown::{Dropdown, DropdownItem};
|
||||
use galaxyui::elements::Shrinkable;
|
||||
use galaxyui::fonts::{Properties, Weight};
|
||||
use galaxyui::ui_components::button::ButtonVariant;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CrossAxisAlignment, Empty, Flex,
|
||||
FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment, MouseStateHandle,
|
||||
ParentElement, Text,
|
||||
},
|
||||
platform::Cursor,
|
||||
AppContext, Element, Entity, FocusContext, TypedActionView, View, ViewHandle,
|
||||
};
|
||||
use galaxyui::{SingletonEntity, ViewContext};
|
||||
use markdown_parser::parse_markdown;
|
||||
|
||||
use crate::ai::mcp::{TemplatableMCPServer, TemplatableMCPServerManager, TemplateVariable};
|
||||
|
||||
use crate::ui_components::{
|
||||
avatar::{Avatar, AvatarContent},
|
||||
blended_colors,
|
||||
};
|
||||
use galaxyui::elements::{CornerRadius, Padding, Radius};
|
||||
|
||||
use galaxy_core::ui::{
|
||||
color::coloru_with_opacity, external_product_icon::ExternalProductIcon, icons::Icon,
|
||||
};
|
||||
|
||||
pub enum InstallationModalBodyEvent {
|
||||
Cancel,
|
||||
@@ -71,26 +66,35 @@ pub struct InstallationModalBody {
|
||||
templatable_mcp_server: Option<TemplatableMCPServer>,
|
||||
instructions_in_markdown: Option<String>,
|
||||
variable_inputs: HashMap<String, VariableInput>,
|
||||
cancel_mouse_state: MouseStateHandle,
|
||||
install_mouse_state: MouseStateHandle,
|
||||
cancel_button: ViewHandle<ActionButton>,
|
||||
install_button: ViewHandle<ActionButton>,
|
||||
close_button_mouse_state: MouseStateHandle,
|
||||
is_shared: bool,
|
||||
}
|
||||
|
||||
impl Default for InstallationModalBody {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl InstallationModalBody {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let cancel_button = ctx.add_typed_action_view(|_ctx| {
|
||||
ActionButton::new("Cancel", NakedTheme).on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(InstallationModalBodyAction::Cancel);
|
||||
})
|
||||
});
|
||||
|
||||
let enter_keystroke = Keystroke::parse("enter").expect("valid keystroke");
|
||||
let install_button = ctx.add_typed_action_view(|ctx| {
|
||||
ActionButton::new("Install", PrimaryTheme)
|
||||
.with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(InstallationModalBodyAction::Install);
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
templatable_mcp_server: None,
|
||||
instructions_in_markdown: None,
|
||||
variable_inputs: HashMap::new(),
|
||||
cancel_mouse_state: Default::default(),
|
||||
install_mouse_state: Default::default(),
|
||||
cancel_button,
|
||||
install_button,
|
||||
close_button_mouse_state: Default::default(),
|
||||
is_shared: false,
|
||||
}
|
||||
@@ -440,89 +444,21 @@ impl InstallationModalBody {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_action_buttons(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let cancel_button = appearance
|
||||
.ui_builder()
|
||||
.button(ButtonVariant::Text, self.cancel_mouse_state.clone())
|
||||
.with_text_label("Cancel".into())
|
||||
.with_style(UiComponentStyles {
|
||||
font_weight: Some(Weight::Bold),
|
||||
font_color: Some(appearance.theme().active_ui_text_color().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.with_hovered_styles(UiComponentStyles {
|
||||
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(InstallationModalBodyAction::Cancel))
|
||||
.finish();
|
||||
|
||||
let corner_down_left_icon = Container::new(
|
||||
ConstrainedBox::new(
|
||||
Icon::CornerDownLeft
|
||||
.to_galaxyui_icon(appearance.theme().active_ui_text_color())
|
||||
.finish(),
|
||||
)
|
||||
.with_width(appearance.monospace_font_size())
|
||||
.with_height(appearance.monospace_font_size())
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_padding(2.)
|
||||
.with_border(Border::all(1.).with_border_fill(coloru_with_opacity(
|
||||
appearance.theme().active_ui_text_color().into(),
|
||||
60,
|
||||
)))
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||
.finish();
|
||||
|
||||
let install_button_label = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"Install",
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_size(),
|
||||
)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.with_style(Properties::default().weight(Weight::Bold))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(corner_down_left_icon)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let install_button = appearance
|
||||
.ui_builder()
|
||||
.button(ButtonVariant::Accent, self.install_mouse_state.clone())
|
||||
.with_custom_label(install_button_label)
|
||||
.with_style(UiComponentStyles {
|
||||
padding: Some(Coords::uniform(5.).left(10.).right(10.)),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(InstallationModalBodyAction::Install))
|
||||
.finish();
|
||||
|
||||
fn render_action_buttons(&self) -> Box<dyn Element> {
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
Container::new(cancel_button)
|
||||
Container::new(ChildView::new(&self.cancel_button).finish())
|
||||
.with_margin_right(INSTALLATION_MODAL_BUTTON_GAP)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(Container::new(install_button).finish())
|
||||
.with_child(Container::new(ChildView::new(&self.install_button).finish()).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_buttons_row(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let source_indicator = Self::render_source_indicator(self.is_shared, appearance);
|
||||
let action_buttons = self.render_action_buttons(appearance);
|
||||
let action_buttons = self.render_action_buttons();
|
||||
|
||||
let spacer = Shrinkable::new(1., Container::new(Empty::new().finish()).finish()).finish();
|
||||
|
||||
|
||||
@@ -1,18 +1,32 @@
|
||||
use crate::ai::mcp::templatable::GalleryData;
|
||||
use crate::ai::mcp::MCPServerUpdate;
|
||||
use crate::modal::Modal;
|
||||
use crate::modal::ModalEvent;
|
||||
use crate::modal::ModalViewState;
|
||||
use crate::server::telemetry::{MCPTemplateInstallationSource, TelemetryEvent};
|
||||
use crate::settings::{AISettings, AISettingsChangedEvent};
|
||||
use crate::settings_view::mcp_servers_page::InstallOrigin;
|
||||
use crate::settings_view::settings_page::{
|
||||
build_toggle_element, render_body_item_label, LocalOnlyIconState, ToggleState,
|
||||
};
|
||||
use crate::util::truncation::truncate_from_end;
|
||||
use crate::view_components::DismissibleToast;
|
||||
use crate::ToastStack;
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use settings::ToggleableSetting as _;
|
||||
use strum::IntoEnumIterator;
|
||||
use uuid::Uuid;
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::appearance::AppearanceEvent;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::Icon;
|
||||
use warpui::elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, MainAxisAlignment,
|
||||
MainAxisSize, ParentElement, Radius, Text,
|
||||
};
|
||||
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use warpui::ui_components::switch::SwitchStateHandle;
|
||||
use warpui::{
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::ai::mcp::gallery::MCPGalleryManagerEvent;
|
||||
use crate::ai::mcp::templatable::{GalleryData, TemplatableMCPServer};
|
||||
use crate::ai::mcp::templatable_manager::{
|
||||
TemplatableMCPServerManager, TemplatableMCPServerManagerEvent,
|
||||
};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::ai::mcp::{
|
||||
// Import events for file-based manager and watcher conditionally
|
||||
@@ -21,59 +35,39 @@ use crate::ai::mcp::{
|
||||
FileMCPWatcher,
|
||||
FileMCPWatcherEvent,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
ai::mcp::{
|
||||
gallery::MCPGalleryManagerEvent,
|
||||
logs,
|
||||
templatable::TemplatableMCPServer,
|
||||
templatable_manager::{TemplatableMCPServerManager, TemplatableMCPServerManagerEvent},
|
||||
FileBasedMCPManager, MCPGalleryManager, MCPProvider, TemplatableMCPServerInstallation,
|
||||
},
|
||||
appearance::Appearance,
|
||||
cloud_object::{
|
||||
model::persistence::{CloudModel, CloudModelEvent},
|
||||
GenericStringObjectFormat, JsonObjectType,
|
||||
},
|
||||
drive::CloudObjectTypeAndId,
|
||||
editor::{EditorView, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions},
|
||||
pane_group::Direction,
|
||||
search_bar::SearchBar,
|
||||
settings_view::mcp_servers::{
|
||||
server_card::{
|
||||
ServerCardEvent, ServerCardOptions, ServerCardStatus, ServerCardView, TitleChip,
|
||||
},
|
||||
style,
|
||||
update_modal::{UpdateModalBody, UpdateModalBodyEvent},
|
||||
ServerCardItemId,
|
||||
},
|
||||
ui_components::blended_colors,
|
||||
view_components::action_button::{ActionButton, NakedTheme},
|
||||
workflows::local_workflows::tail_command_for_shell,
|
||||
workspace::Workspace,
|
||||
workspaces::user_workspaces::UserWorkspaces,
|
||||
use crate::ai::mcp::{
|
||||
logs, FileBasedMCPManager, MCPGalleryManager, MCPProvider, MCPServerUpdate,
|
||||
TemplatableMCPServerInstallation,
|
||||
};
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::send_telemetry_from_ctx;
|
||||
use galaxy_core::ui::{appearance::AppearanceEvent, theme::color::internal_colors, Icon};
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment,
|
||||
Expanded, Fill, Flex, FormattedTextElement, HighlightedHyperlink, MainAxisAlignment,
|
||||
MainAxisSize, ParentElement, Radius, Text,
|
||||
},
|
||||
ui_components::{
|
||||
components::{Coords, UiComponent, UiComponentStyles},
|
||||
switch::SwitchStateHandle,
|
||||
},
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
use crate::appearance::Appearance;
|
||||
use crate::cloud_object::model::persistence::{CloudModel, CloudModelEvent};
|
||||
use crate::cloud_object::{GenericStringObjectFormat, JsonObjectType};
|
||||
use crate::drive::CloudObjectTypeAndId;
|
||||
use crate::editor::{
|
||||
EditorView, PropagateAndNoOpNavigationKeys, SingleLineEditorOptions, TextOptions,
|
||||
};
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use settings::ToggleableSetting as _;
|
||||
use std::cmp::Ordering;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use strum::IntoEnumIterator;
|
||||
use uuid::Uuid;
|
||||
use crate::modal::{Modal, ModalEvent, ModalViewState};
|
||||
use crate::pane_group::Direction;
|
||||
use crate::search_bar::SearchBar;
|
||||
use crate::server::telemetry::{MCPTemplateInstallationSource, TelemetryEvent};
|
||||
use crate::settings::{AISettings, AISettingsChangedEvent};
|
||||
use crate::settings_view::mcp_servers::server_card::{
|
||||
ServerCardEvent, ServerCardOptions, ServerCardStatus, ServerCardView, TitleChip,
|
||||
};
|
||||
use crate::settings_view::mcp_servers::update_modal::{UpdateModalBody, UpdateModalBodyEvent};
|
||||
use crate::settings_view::mcp_servers::{style, ServerCardItemId};
|
||||
use crate::settings_view::mcp_servers_page::InstallOrigin;
|
||||
use crate::settings_view::settings_page::{
|
||||
build_toggle_element, render_body_item_label, LocalOnlyIconState, ToggleState,
|
||||
};
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::util::truncation::truncate_from_end;
|
||||
use crate::view_components::action_button::{ActionButton, NakedTheme};
|
||||
use crate::view_components::DismissibleToast;
|
||||
use crate::workflows::local_workflows::tail_command_for_shell;
|
||||
use crate::workspace::Workspace;
|
||||
use crate::workspaces::user_workspaces::UserWorkspaces;
|
||||
use crate::ToastStack;
|
||||
|
||||
const DESCRIPTION_TEXT: &str = "Add MCP servers to extend the Warp Agent's capabilities. MCP servers expose data sources or tools to agents through a standardized interface, essentially acting like plugins. Add a custom server, or use the presets to get started with popular servers. You can also find team servers that have been shared with you here. ";
|
||||
|
||||
@@ -187,7 +181,7 @@ impl MCPServersListPageView {
|
||||
}
|
||||
});
|
||||
|
||||
let update_modal_body = ctx.add_typed_action_view(|_ctx| UpdateModalBody::new());
|
||||
let update_modal_body = ctx.add_typed_action_view(UpdateModalBody::new);
|
||||
ctx.subscribe_to_view(&update_modal_body, |me, _, event, ctx| {
|
||||
me.handle_update_modal_body_event(event, ctx);
|
||||
});
|
||||
@@ -778,7 +772,7 @@ impl MCPServersListPageView {
|
||||
} else {
|
||||
self.update_modal_state.view.update(ctx, |modal, ctx| {
|
||||
modal.body().update(ctx, |body, ctx| {
|
||||
body.set_installation(installation_uuid, server_name, available_updates);
|
||||
body.set_installation(installation_uuid, server_name, available_updates, ctx);
|
||||
ctx.notify();
|
||||
});
|
||||
});
|
||||
@@ -997,8 +991,8 @@ impl MCPServersListPageView {
|
||||
match event {
|
||||
UpdateModalBodyEvent::Cancel => {
|
||||
self.update_modal_state.view.update(ctx, |modal, ctx| {
|
||||
modal.body().update(ctx, |body, _ctx| {
|
||||
body.clear();
|
||||
modal.body().update(ctx, |body, ctx| {
|
||||
body.clear(ctx);
|
||||
});
|
||||
});
|
||||
self.update_modal_state.close();
|
||||
@@ -1015,8 +1009,8 @@ impl MCPServersListPageView {
|
||||
};
|
||||
self.process_server_update(*installation_uuid, update.clone(), ctx);
|
||||
self.update_modal_state.view.update(ctx, |modal, ctx| {
|
||||
modal.body().update(ctx, |body, _ctx| {
|
||||
body.clear();
|
||||
modal.body().update(ctx, |body, ctx| {
|
||||
body.clear(ctx);
|
||||
});
|
||||
});
|
||||
self.update_modal_state.close();
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
fmt::{Display, Formatter, Result},
|
||||
};
|
||||
use std::cmp::Ordering;
|
||||
use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
|
||||
@@ -1,45 +1,33 @@
|
||||
use galaxy_core::{
|
||||
features::FeatureFlag,
|
||||
ui::{
|
||||
external_product_icon::ExternalProductIcon,
|
||||
icons::{Icon, ICON_DIMENSIONS},
|
||||
theme::{color::internal_colors, AnsiColorIdentifier},
|
||||
},
|
||||
};
|
||||
use galaxyui::{
|
||||
accessibility::ActionAccessibilityContent,
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Expanded, Fill, Flex,
|
||||
FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseState, MouseStateHandle, Padding, ParentElement, Radius, Text, Wrap,
|
||||
},
|
||||
fonts::Weight,
|
||||
platform::Cursor,
|
||||
ui_components::{
|
||||
button::ButtonVariant,
|
||||
chip::Chip,
|
||||
components::{Coords, UiComponent, UiComponentStyles},
|
||||
switch::SwitchStateHandle,
|
||||
},
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext,
|
||||
};
|
||||
use markdown_parser::{FormattedText, FormattedTextFragment, FormattedTextLine};
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
use crate::{
|
||||
ai::mcp::{
|
||||
templatable::CloudTemplatableMCPServer, MCPServerState, TemplatableMCPServerManager,
|
||||
},
|
||||
appearance::Appearance,
|
||||
cloud_object::CloudObject,
|
||||
settings_view::mcp_servers::{style, ServerCardItemId},
|
||||
ui_components::{
|
||||
avatar::{Avatar, AvatarContent, StatusElementTypes},
|
||||
blended_colors,
|
||||
buttons::icon_button,
|
||||
red_notification_dot::RedNotificationDot,
|
||||
},
|
||||
use galaxy_core::features::FeatureFlag;
|
||||
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
|
||||
use galaxy_core::ui::icons::{Icon, ICON_DIMENSIONS};
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxy_core::ui::theme::AnsiColorIdentifier;
|
||||
use galaxyui::accessibility::ActionAccessibilityContent;
|
||||
use galaxyui::elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Expanded, Fill, Flex,
|
||||
FormattedTextElement, HighlightedHyperlink, Hoverable, MainAxisAlignment, MainAxisSize,
|
||||
MouseState, MouseStateHandle, Padding, ParentElement, Radius, Text, Wrap,
|
||||
};
|
||||
use galaxyui::fonts::Weight;
|
||||
use galaxyui::platform::Cursor;
|
||||
use galaxyui::ui_components::button::ButtonVariant;
|
||||
use galaxyui::ui_components::chip::Chip;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use galaxyui::ui_components::switch::SwitchStateHandle;
|
||||
use galaxyui::{AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext};
|
||||
|
||||
use crate::ai::mcp::templatable::CloudTemplatableMCPServer;
|
||||
use crate::ai::mcp::{MCPServerState, TemplatableMCPServerManager};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::cloud_object::{CloudObject, CloudObjectUuidLookup as _};
|
||||
use crate::settings_view::mcp_servers::{style, ServerCardItemId};
|
||||
use crate::ui_components::avatar::{Avatar, AvatarContent, StatusElementTypes};
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::ui_components::buttons::icon_button;
|
||||
use crate::ui_components::red_notification_dot::RedNotificationDot;
|
||||
|
||||
/// A chip displayed inline with the server card title, optionally with a leading icon.
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
use galaxy_core::ui::appearance::Appearance;
|
||||
use galaxyui::{
|
||||
fonts::Weight,
|
||||
ui_components::components::{Coords, UiComponentStyles},
|
||||
};
|
||||
use galaxyui::fonts::Weight;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponentStyles};
|
||||
|
||||
pub const ICON_MARGIN: f32 = 8.;
|
||||
pub const HEADER_FONT_SIZE: f32 = 18.;
|
||||
|
||||
@@ -1,3 +1,21 @@
|
||||
use chrono::{Local, TimeZone};
|
||||
use uuid::Uuid;
|
||||
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use warpui::elements::{
|
||||
Align, Border, ChildView, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Empty,
|
||||
Flex, Hoverable, MainAxisAlignment, MouseStateHandle, Padding, ParentElement, Radius,
|
||||
Shrinkable, Text,
|
||||
};
|
||||
use warpui::fonts::{Properties, Weight};
|
||||
use warpui::keymap::Keystroke;
|
||||
use warpui::platform::Cursor;
|
||||
use warpui::ui_components::components::{UiComponent, UiComponentStyles};
|
||||
use warpui::{
|
||||
AppContext, Element, Entity, SingletonEntity, TypedActionView, View, ViewContext, ViewHandle,
|
||||
};
|
||||
|
||||
use crate::ai::mcp::{Author, MCPServerUpdate};
|
||||
use crate::appearance::Appearance;
|
||||
use crate::settings_view::mcp_servers::style::{
|
||||
@@ -6,25 +24,9 @@ use crate::settings_view::mcp_servers::style::{
|
||||
use crate::ui_components::avatar::{Avatar, AvatarContent};
|
||||
use crate::ui_components::blended_colors;
|
||||
use crate::util::time_format::format_approx_duration_from_now;
|
||||
use chrono::{Local, TimeZone};
|
||||
use galaxy_core::ui::color::coloru_with_opacity;
|
||||
use galaxy_core::ui::external_product_icon::ExternalProductIcon;
|
||||
use galaxy_core::ui::icons::Icon;
|
||||
use galaxy_core::ui::theme::color::internal_colors;
|
||||
use galaxyui::elements::{Align, Empty, Padding, Shrinkable};
|
||||
use galaxyui::fonts::{Properties, Weight};
|
||||
use galaxyui::ui_components::button::ButtonVariant;
|
||||
use galaxyui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use galaxyui::SingletonEntity;
|
||||
use galaxyui::{
|
||||
elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, Flex, Hoverable,
|
||||
MainAxisAlignment, MouseStateHandle, ParentElement, Radius, Text,
|
||||
},
|
||||
platform::Cursor,
|
||||
AppContext, Element, Entity, TypedActionView, View, ViewContext,
|
||||
use crate::view_components::action_button::{
|
||||
ActionButton, KeystrokeSource, NakedTheme, PrimaryTheme,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
pub enum UpdateModalBodyEvent {
|
||||
Cancel,
|
||||
@@ -41,21 +43,47 @@ pub enum UpdateModalBodyAction {
|
||||
SelectOption(usize),
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct UpdateModalBody {
|
||||
installation_uuid: Option<Uuid>,
|
||||
server_name: Option<String>,
|
||||
update_options: Vec<MCPServerUpdate>,
|
||||
selected_updates: Vec<bool>,
|
||||
cancel_mouse_state: MouseStateHandle,
|
||||
update_mouse_state: MouseStateHandle,
|
||||
cancel_button: ViewHandle<ActionButton>,
|
||||
update_button: ViewHandle<ActionButton>,
|
||||
close_button_mouse_state: MouseStateHandle,
|
||||
option_mouse_states: Vec<MouseStateHandle>,
|
||||
}
|
||||
|
||||
impl UpdateModalBody {
|
||||
pub fn new() -> Self {
|
||||
Default::default()
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let cancel_button = ctx.add_typed_action_view(|_ctx| {
|
||||
ActionButton::new("Cancel", NakedTheme).on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(UpdateModalBodyAction::Cancel);
|
||||
})
|
||||
});
|
||||
|
||||
let enter_keystroke = Keystroke::parse("enter").expect("valid keystroke");
|
||||
let update_button = ctx.add_typed_action_view(|ctx| {
|
||||
let mut button = ActionButton::new("Update", PrimaryTheme)
|
||||
.with_keybinding(KeystrokeSource::Fixed(enter_keystroke), ctx)
|
||||
.on_click(|ctx| {
|
||||
ctx.dispatch_typed_action(UpdateModalBodyAction::Update);
|
||||
});
|
||||
// Initial state has no rows selected, so the button starts disabled.
|
||||
button.set_disabled(true, ctx);
|
||||
button
|
||||
});
|
||||
|
||||
Self {
|
||||
installation_uuid: None,
|
||||
server_name: None,
|
||||
update_options: vec![],
|
||||
selected_updates: vec![],
|
||||
cancel_button,
|
||||
update_button,
|
||||
close_button_mouse_state: Default::default(),
|
||||
option_mouse_states: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_installation(
|
||||
@@ -63,6 +91,7 @@ impl UpdateModalBody {
|
||||
installation_uuid: Uuid,
|
||||
server_name: String,
|
||||
update_options: Vec<MCPServerUpdate>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.installation_uuid = Some(installation_uuid);
|
||||
self.server_name = Some(server_name);
|
||||
@@ -71,14 +100,25 @@ impl UpdateModalBody {
|
||||
self.option_mouse_states = (0..self.update_options.len())
|
||||
.map(|_| MouseStateHandle::default())
|
||||
.collect();
|
||||
// No rows are selected yet, so the Update button must start disabled.
|
||||
self.refresh_update_button_disabled(ctx);
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
pub fn clear(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.installation_uuid = None;
|
||||
self.server_name = None;
|
||||
self.update_options = vec![];
|
||||
self.selected_updates = vec![];
|
||||
self.option_mouse_states = vec![];
|
||||
self.refresh_update_button_disabled(ctx);
|
||||
}
|
||||
|
||||
/// Sync the Update button's disabled state with the current selection.
|
||||
fn refresh_update_button_disabled(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let has_selection = self.selected_updates.iter().any(|&x| x);
|
||||
self.update_button.update(ctx, |button, ctx| {
|
||||
button.set_disabled(!has_selection, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_title(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
@@ -298,97 +338,20 @@ impl UpdateModalBody {
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_action_buttons(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let cancel_button = appearance
|
||||
.ui_builder()
|
||||
.button(ButtonVariant::Text, self.cancel_mouse_state.clone())
|
||||
.with_text_label("Cancel".into())
|
||||
.with_style(UiComponentStyles {
|
||||
font_weight: Some(Weight::Bold),
|
||||
font_color: Some(appearance.theme().active_ui_text_color().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.with_hovered_styles(UiComponentStyles {
|
||||
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
|
||||
..Default::default()
|
||||
})
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(UpdateModalBodyAction::Cancel))
|
||||
.finish();
|
||||
|
||||
let corner_down_left_icon = Container::new(
|
||||
ConstrainedBox::new(
|
||||
Icon::CornerDownLeft
|
||||
.to_galaxyui_icon(appearance.theme().active_ui_text_color())
|
||||
.finish(),
|
||||
)
|
||||
.with_width(appearance.monospace_font_size())
|
||||
.with_height(appearance.monospace_font_size())
|
||||
.finish(),
|
||||
)
|
||||
.with_uniform_padding(2.)
|
||||
.with_border(Border::all(1.).with_border_fill(coloru_with_opacity(
|
||||
appearance.theme().active_ui_text_color().into(),
|
||||
60,
|
||||
)))
|
||||
.with_corner_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||
.finish();
|
||||
|
||||
let update_button_label = Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
Text::new_inline(
|
||||
"Update",
|
||||
appearance.ui_font_family(),
|
||||
appearance.ui_font_size(),
|
||||
)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.with_style(Properties::default().weight(Weight::Bold))
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(corner_down_left_icon)
|
||||
.with_margin_left(8.)
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let mut update_button_builder = appearance
|
||||
.ui_builder()
|
||||
.button(ButtonVariant::Accent, self.update_mouse_state.clone())
|
||||
.with_custom_label(update_button_label)
|
||||
.with_style(UiComponentStyles {
|
||||
padding: Some(Coords::uniform(5.).left(10.).right(10.)),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
// Disable the update button if no updates are selected
|
||||
let has_selection = self.selected_updates.iter().any(|&x| x);
|
||||
|
||||
if !has_selection {
|
||||
update_button_builder = update_button_builder.disabled();
|
||||
}
|
||||
|
||||
let update_button = update_button_builder
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(|ctx, _, _| ctx.dispatch_typed_action(UpdateModalBodyAction::Update))
|
||||
.finish();
|
||||
|
||||
fn render_action_buttons(&self) -> Box<dyn Element> {
|
||||
Flex::row()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Center)
|
||||
.with_child(
|
||||
Container::new(cancel_button)
|
||||
Container::new(ChildView::new(&self.cancel_button).finish())
|
||||
.with_margin_right(INSTALLATION_MODAL_BUTTON_GAP)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(Container::new(update_button).finish())
|
||||
.with_child(Container::new(ChildView::new(&self.update_button).finish()).finish())
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn render_buttons_row(&self, appearance: &Appearance) -> Box<dyn Element> {
|
||||
let action_buttons = self.render_action_buttons(appearance);
|
||||
let action_buttons = self.render_action_buttons();
|
||||
|
||||
let spacer = Shrinkable::new(1., Container::new(Empty::new().finish()).finish()).finish();
|
||||
|
||||
@@ -476,9 +439,11 @@ impl TypedActionView for UpdateModalBody {
|
||||
}
|
||||
}
|
||||
UpdateModalBodyAction::SelectOption(index) => {
|
||||
// Toggle the selection at the given index
|
||||
// Toggle the selection at the given index, then sync the
|
||||
// Update button's disabled state with the new selection.
|
||||
if let Some(selected) = self.selected_updates.get_mut(*index) {
|
||||
*selected = !*selected;
|
||||
self.refresh_update_button_disabled(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user