From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 027BB1FF179 for ; Wed, 12 Nov 2025 17:19:17 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 35D6A9733; Wed, 12 Nov 2025 17:20:04 +0100 (CET) From: Dominik Csapak To: pdm-devel@lists.proxmox.com Date: Wed, 12 Nov 2025 17:11:43 +0100 Message-ID: <20251112161900.75032-9-d.csapak@proxmox.com> X-Mailer: git-send-email 2.47.3 In-Reply-To: <20251112161900.75032-1-d.csapak@proxmox.com> References: <20251112161900.75032-1-d.csapak@proxmox.com> MIME-Version: 1.0 X-SPAM-LEVEL: Spam detection results: 0 AWL -0.171 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment POISEN_SPAM_PILL 0.1 Meta: its spam POISEN_SPAM_PILL_2 0.1 random spam to be learned in bayes POISEN_SPAM_PILL_4 0.1 random spam to be learned in bayes PROLO_LEO1 0.1 Meta Catches all Leo drug variations so far SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Subject: [pdm-devel] [PATCH datacenter-manager 08/18] ui: dashboard: prepare view for editing custom views X-BeenThere: pdm-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox Datacenter Manager development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: Proxmox Datacenter Manager development discussion Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: pdm-devel-bounces@lists.proxmox.com Sender: "pdm-devel" This adds a mechanism to edit a view, namely it adds an edit button (pencil) in the status row. When in 'edit mode' one can: * drag the panels around * delete panels * add new panels * set the 'flex' value of panels * add a new row at the end * delete a whole row There is currently no mechanism to persistently save the result, but that's only a case of wiring the 'on_update_layout' callback to e.g. a backend api call. Also the editing is only active when the view is not named 'dashboard'. The drag&drop works with desktop and touchscreens, but on touchscreens, there is no 'drag item' shown currently. The menu structure for adding new items could probably be improved, but that should not be a big issue. For handling the 'editing overlay' of the panels, there is a new 'RowElement' component that just abstracts that away to have a less code in the RowView component. Signed-off-by: Dominik Csapak --- ui/Cargo.toml | 2 +- ui/css/pdm.scss | 4 + ui/src/dashboard/view.rs | 88 +++-- ui/src/dashboard/view/row_element.rs | 130 +++++++ ui/src/dashboard/view/row_view.rs | 519 ++++++++++++++++++++++++++- 5 files changed, 697 insertions(+), 46 deletions(-) create mode 100644 ui/src/dashboard/view/row_element.rs diff --git a/ui/Cargo.toml b/ui/Cargo.toml index 780b4acf..ee95ae4a 100644 --- a/ui/Cargo.toml +++ b/ui/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" wasm-bindgen = "0.2.92" wasm-bindgen-futures = "0.4" wasm-logger = "0.2" -web-sys = { version = "0.3", features = ["Location"] } +web-sys = { version = "0.3", features = ["Location", "DataTransfer"] } yew = { version = "0.21", features = ["csr"] } yew-router = { version = "0.18" } diff --git a/ui/css/pdm.scss b/ui/css/pdm.scss index 92182a47..71cd4b05 100644 --- a/ui/css/pdm.scss +++ b/ui/css/pdm.scss @@ -120,3 +120,7 @@ background-color: var(--pwt-color-background); } } + +.dragging-item { + opacity: 0.5; +} diff --git a/ui/src/dashboard/view.rs b/ui/src/dashboard/view.rs index c5149d69..3182d4ea 100644 --- a/ui/src/dashboard/view.rs +++ b/ui/src/dashboard/view.rs @@ -39,6 +39,8 @@ use pdm_client::types::TopEntities; mod row_view; pub use row_view::RowView; +mod row_element; + #[derive(Debug, Clone, PartialEq, Copy)] pub enum EditingMessage { Start, @@ -79,6 +81,7 @@ pub enum Msg { Reload(bool), // force ConfigWindow(bool), // show UpdateConfig(RefreshConfig), + LayoutUpdate(ViewLayout), } struct ViewComp { @@ -97,6 +100,8 @@ struct ViewComp { load_finished_time: Option, show_config_window: bool, show_create_wizard: Option, + + editing_state: SharedState>, } fn render_widget( @@ -276,6 +281,8 @@ impl Component for ViewComp { loading: true, show_config_window: false, show_create_wizard: None, + + editing_state: SharedState::new(Vec::new()), } } @@ -331,6 +338,12 @@ impl Component for ViewComp { self.show_config_window = false; } + Msg::LayoutUpdate(view_layout) => { + // FIXME: update backend layout + if let Some(template) = &mut self.template.data { + template.layout = view_layout; + } + } } true } @@ -345,51 +358,65 @@ impl Component for ViewComp { } fn view(&self, ctx: &yew::Context) -> yew::Html { + let props = ctx.props(); if !self.template.has_data() { return Progress::new().into(); } let mut view = Column::new().class(css::FlexFit).with_child( Container::new() - .class("pwt-content-spacer-padding") + .padding(4) .class("pwt-content-spacer-colors") .class("pwt-default-colors") - .with_child(DashboardStatusRow::new( - self.load_finished_time, - self.refresh_config - .refresh_interval - .unwrap_or(DEFAULT_REFRESH_INTERVAL_S), - ctx.link().callback(Msg::Reload), - ctx.link().callback(|_| Msg::ConfigWindow(true)), - )), + .with_child( + DashboardStatusRow::new( + self.load_finished_time, + self.refresh_config + .refresh_interval + .unwrap_or(DEFAULT_REFRESH_INTERVAL_S), + ctx.link().callback(Msg::Reload), + ctx.link().callback(|_| Msg::ConfigWindow(true)), + ) + .editing_state( + (props.view != "dashboard").then_some(self.editing_state.clone()), + ), + ), ); + if !has_sub_panel(self.template.data.as_ref()) { view.add_child( Row::new() - .class("pwt-content-spacer") - .with_child(create_subscription_panel(self.subscriptions.clone())), + .padding_x(4) + .padding_bottom(4) + .padding_top(0) + .class("pwt-content-spacer-colors") + .with_child(create_subscription_panel(self.subscriptions.clone()).flex(1.0)), ); } match self.template.data.as_ref().map(|template| &template.layout) { Some(ViewLayout::Rows { rows }) => { - view.add_child(RowView::new(rows.clone(), { - let link = ctx.link().clone(); - let status = self.status.clone(); - let subscriptions = self.subscriptions.clone(); - let top_entities = self.top_entities.clone(); - let statistics = self.statistics.clone(); - let refresh_config = self.refresh_config.clone(); - move |widget: &RowWidget| { - render_widget( - link.clone(), - widget, - status.clone(), - subscriptions.clone(), - top_entities.clone(), - statistics.clone(), - refresh_config.clone(), - ) - } - })); + view.add_child( + RowView::new(rows.clone(), { + let link = ctx.link().clone(); + let status = self.status.clone(); + let subscriptions = self.subscriptions.clone(); + let top_entities = self.top_entities.clone(); + let statistics = self.statistics.clone(); + let refresh_config = self.refresh_config.clone(); + move |widget: &RowWidget| { + render_widget( + link.clone(), + widget, + status.clone(), + subscriptions.clone(), + top_entities.clone(), + statistics.clone(), + refresh_config.clone(), + ) + } + }) + .editing_state(self.editing_state.clone()) + .on_update_layout(ctx.link().callback(Msg::LayoutUpdate)), + ); } None => {} } @@ -490,6 +517,7 @@ async fn load_template() -> Result { \"leaderboard-type\": \"node-memory\" } ], + [], [ { \"flex\": 5.0, diff --git a/ui/src/dashboard/view/row_element.rs b/ui/src/dashboard/view/row_element.rs new file mode 100644 index 00000000..d242195c --- /dev/null +++ b/ui/src/dashboard/view/row_element.rs @@ -0,0 +1,130 @@ +use yew::html::IntoEventCallback; + +use pwt::css; +use pwt::prelude::*; +use pwt::props::RenderFn; +use pwt::widget::{ActionIcon, Card, Fa, Panel, Row}; +use pwt_macros::{builder, widget}; + +use crate::dashboard::types::RowWidget; + +#[widget(comp=RowElementComp, @element)] +#[derive(PartialEq, Properties, Clone)] +#[builder] +pub struct RowElement { + item: RowWidget, + widget_renderer: RenderFn, + + #[builder] + #[prop_or_default] + edit_mode: bool, + + #[builder] + #[prop_or_default] + is_dragging: bool, + + #[builder_cb(IntoEventCallback, into_event_callback, ())] + #[prop_or_default] + on_remove: Option>, + + #[builder_cb(IntoEventCallback, into_event_callback, u32)] + #[prop_or_default] + on_flex_change: Option>, +} + +impl RowElement { + pub fn new(item: RowWidget, widget_renderer: impl Into>) -> Self { + let widget_renderer = widget_renderer.into(); + yew::props!(Self { + item, + widget_renderer + }) + } +} + +pub enum Msg { + FlexReduce, + FlexIncrease, +} + +pub struct RowElementComp {} + +impl Component for RowElementComp { + type Message = Msg; + type Properties = RowElement; + + fn create(_ctx: &Context) -> Self { + Self {} + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + let props = ctx.props(); + let flex = props.item.flex.unwrap_or(1.0) as u32; + match msg { + Msg::FlexReduce => { + if let Some(on_flex_change) = &props.on_flex_change { + on_flex_change.emit(flex.saturating_sub(1)) + } + } + Msg::FlexIncrease => { + if let Some(on_flex_change) = &props.on_flex_change { + on_flex_change.emit(flex.saturating_add(1)) + } + } + } + + true + } + + fn view(&self, ctx: &Context) -> Html { + let props = ctx.props(); + let widget = props.widget_renderer.apply(&props.item); + + let edit_overlay = Card::new() + .padding(2) + .style("z-index", "10") + .class(css::AlignItems::Center) + .with_child(Fa::new("bars").style("cursor", "grab").padding_end(1)) + .with_child(tr!("Flex")) + .with_child( + ActionIcon::new("fa fa-minus") + .on_activate(ctx.link().callback(|_| Msg::FlexReduce)), + ) + .with_child(props.item.flex.unwrap_or(1.0) as u32) + .with_child( + ActionIcon::new("fa fa-plus") + .on_activate(ctx.link().callback(|_| Msg::FlexIncrease)), + ) + .with_child(ActionIcon::new("fa fa-times").on_activate({ + let on_remove = props.on_remove.clone(); + move |_| { + if let Some(on_remove) = &on_remove { + on_remove.emit(()); + } + } + })); + + Panel::new() + .with_std_props(&props.std_props) + .listeners(&props.listeners) + .border(true) + .class(props.is_dragging.then_some("dragging-item")) + .attribute("draggable", if props.edit_mode { "true" } else { "false" }) + .style("position", "relative") + .with_child(widget) + .with_optional_child( + props.edit_mode.then_some( + Row::new() + .gap(2) + .class(css::Display::Flex) + .class(css::AlignItems::Start) + .class(css::JustifyContent::End) + .key("overlay") + .style("position", "absolute") + .style("inset", "0") + .with_child(edit_overlay), + ), + ) + .into() + } +} diff --git a/ui/src/dashboard/view/row_view.rs b/ui/src/dashboard/view/row_view.rs index 69300327..512e63e7 100644 --- a/ui/src/dashboard/view/row_view.rs +++ b/ui/src/dashboard/view/row_view.rs @@ -1,21 +1,42 @@ use std::collections::HashMap; use std::rc::Rc; +use gloo_timers::callback::Timeout; +use wasm_bindgen::JsCast; +use web_sys::Element; +use yew::html::{IntoEventCallback, IntoPropValue}; use yew::virtual_dom::{VComp, VNode}; use pwt::css; use pwt::prelude::*; use pwt::props::RenderFn; -use pwt::widget::{Column, Container, Panel, Row}; +use pwt::state::{SharedState, SharedStateObserver}; +use pwt::widget::menu::{Menu, MenuButton, MenuItem}; +use pwt::widget::{ActionIcon, Button, Column, Container, Row, Tooltip}; use pwt_macros::builder; -use crate::dashboard::types::RowWidget; +use crate::dashboard::types::{RowWidget, ViewLayout, WidgetType}; +use crate::dashboard::view::row_element::RowElement; +use crate::dashboard::view::EditingMessage; + +use pdm_api_types::remotes::RemoteType; #[derive(Properties, PartialEq)] #[builder] pub struct RowView { rows: Vec>, widget_renderer: RenderFn, + + #[prop_or_default] + #[builder(IntoPropValue, into_prop_value)] + /// If set, enables/disables editing mode + editing_state: Option>>, + + #[prop_or_default] + #[builder_cb(IntoEventCallback, into_event_callback, ViewLayout)] + /// Will be called if there is an [`EditingController`] and the editing + /// is finished. + on_update_layout: Option>, } impl RowView { @@ -33,6 +54,33 @@ impl From for VNode { } } +pub enum OverEvent { + Pointer(PointerEvent), + Drag(DragEvent), +} + +pub enum DragMsg { + Start(Position), + End, + DragOver(OverEvent, Position), + Enter(Position), +} + +pub enum MoveDirection { + Up, + Down, +} +pub enum Msg { + DragEvent(DragMsg), + AddRow, + RemoveRow(usize), // idx + EditFlex(Position, u32), + AddWidget(Position, WidgetType), + RemoveWidget(Position), + MoveRow(usize, MoveDirection), // idx + HandleEditMessages, +} + #[derive(Clone, Copy, Debug, PartialEq)] /// Represents the position of a widget in a row view pub struct Position { @@ -42,6 +90,16 @@ pub struct Position { pub struct RowViewComp { current_layout: Vec>, + new_layout: Option>>, + dragging: Option, // index of item + dragging_target: Option, // index of item + drag_timeout: Option, + + next_row_indices: HashMap, // for saving the max index for new widgets + + node_ref: NodeRef, + edit_mode: bool, + _editing_observer: Option>>, } fn extract_row_layout(rows: &Vec>) -> Vec> { @@ -65,7 +123,7 @@ fn extract_row_layout(rows: &Vec>) -> Vec) -> Self { @@ -75,14 +133,189 @@ impl Component for RowViewComp { for (row_idx, row) in current_layout.iter().enumerate() { next_row_indices.insert(row_idx, row.len()); } - Self { current_layout } + + let _editing_observer = ctx + .props() + .editing_state + .as_ref() + .map(|state| state.add_listener(ctx.link().callback(|_| Msg::HandleEditMessages))); + + Self { + new_layout: None, + current_layout, + dragging: None, + dragging_target: None, + drag_timeout: None, + next_row_indices, + node_ref: NodeRef::default(), + edit_mode: false, + _editing_observer, + } + } + + fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { + match msg { + Msg::RemoveRow(idx) => { + self.current_layout.remove(idx); + } + Msg::AddRow => { + self.current_layout.push(Vec::new()); + } + Msg::DragEvent(drag_msg) => match drag_msg { + DragMsg::Start(coords) => { + self.dragging = Some(coords); + self.dragging_target = Some(coords); + } + DragMsg::End => { + self.dragging = None; + self.dragging_target = None; + if let Some(layout) = self.new_layout.take() { + self.current_layout = layout; + } + } + DragMsg::DragOver(event, position) => { + // check if the pointer is at a position where the item can be dropped + // without flickering, namely where it fits from it's dimensions + let (target, pointer_pos) = match event { + OverEvent::Pointer(event) => ( + event.target().and_then(|t| t.dyn_into::().ok()), + (event.client_x(), event.client_y()), + ), + OverEvent::Drag(event) => ( + event.target().and_then(|t| t.dyn_into::().ok()), + (event.client_x(), event.client_y()), + ), + }; + if let Some(el) = self.node_ref.cast::() { + if let Ok(Some(dragging_el)) = el.query_selector(".dragging-item") { + let dragging_rect = dragging_el.get_bounding_client_rect(); + + if let Some(target) = target { + let target_rect = target.get_bounding_client_rect(); + + let x = pointer_pos.0 as f64; + let x_min = target_rect.x(); + let x_max = target_rect.x() + dragging_rect.width(); + + let y = pointer_pos.1 as f64; + let y_min = target_rect.y(); + let y_max = target_rect.y() + dragging_rect.height(); + + if x >= x_min && x <= x_max && y >= y_min && y <= y_max { + ctx.link() + .send_message(Msg::DragEvent(DragMsg::Enter(position))); + } + } + } + } + } + DragMsg::Enter(coords) => { + if let Some(source_coords) = self.dragging { + let mut new_layout = self.current_layout.clone(); + let item = new_layout[source_coords.row].remove(source_coords.item); + let target_idx = new_layout[coords.row].len().min(coords.item); + new_layout[coords.row].insert(target_idx, item); + self.new_layout = Some(new_layout); + } + self.dragging_target = Some(coords); + } + }, + Msg::EditFlex(coords, flex) => { + self.current_layout[coords.row][coords.item].1.flex = Some(flex as f32); + } + Msg::AddWidget(coords, widget_type) => { + let next_idx = *self.next_row_indices.get(&coords.row).unwrap_or(&0); + self.next_row_indices + .insert(coords.row, next_idx.saturating_add(1)); + self.current_layout[coords.row].insert( + coords.item, + ( + Position { + row: coords.row, + item: next_idx, + }, + RowWidget { + flex: None, + title: None, + r#type: widget_type, + }, + ), + ); + } + Msg::RemoveWidget(coords) => { + self.current_layout[coords.row].remove(coords.item); + } + Msg::MoveRow(old, direction) => { + let mut new_layout = self.current_layout.clone(); + let row = new_layout.remove(old); + let new_idx = match direction { + MoveDirection::Up => old.saturating_sub(1), + MoveDirection::Down => old.saturating_add(1).min(new_layout.len()), + }; + new_layout.insert(new_idx, row); + self.current_layout = new_layout; + } + Msg::HandleEditMessages => { + let props = ctx.props(); + let state = match props.editing_state.clone() { + Some(state) => state, + None => return false, + }; + + if state.read().len() == 0 { + return false; + } // Note: avoid endless loop + + let list = state.write().split_off(0); + let mut editing = self.edit_mode; + let mut trigger_finish = false; + let mut cancel = false; + for msg in list { + match msg { + EditingMessage::Start => editing = true, + EditingMessage::Cancel => { + if editing { + cancel = true; + } + editing = false; + } + EditingMessage::Finish => { + if editing { + trigger_finish = true; + } + editing = false; + } + } + } + if let (true, Some(on_update_layout)) = (trigger_finish, &props.on_update_layout) { + let rows = self + .current_layout + .iter() + .map(|row| row.iter().map(|(_, item)| item.clone()).collect()) + .collect(); + on_update_layout.emit(ViewLayout::Rows { rows }); + } + if cancel { + self.current_layout = extract_row_layout(&props.rows); + } + self.edit_mode = editing; + if !self.edit_mode { + self.dragging = None; + self.dragging_target = None; + self.drag_timeout = None; + } + } + } + true } fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { let props = ctx.props(); - if props.rows != old_props.rows { - self.current_layout = extract_row_layout(&props.rows); + let new_layout = extract_row_layout(&props.rows); + if new_layout != self.current_layout { + self.current_layout = new_layout; + } } true @@ -90,8 +323,11 @@ impl Component for RowViewComp { fn view(&self, ctx: &Context) -> Html { let props = ctx.props(); - let mut view = Column::new(); - let layout = &self.current_layout; + let mut view = Column::new().onpointerup( + (self.dragging.is_some() && self.edit_mode) + .then_some(ctx.link().callback(|_| Msg::DragEvent(DragMsg::End))), + ); + let layout = self.new_layout.as_ref().unwrap_or(&self.current_layout); let mut row = Row::new() .padding_x(2) .class("pwt-content-spacer-colors") @@ -104,7 +340,7 @@ impl Component for RowViewComp { .sum(); let gaps_ratio = 1.0; //items.len().saturating_sub(1) as f32 / items.len() as f32; - for (_item_idx, (coords, item)) in items.iter().enumerate() { + for (item_idx, (coords, item)) in items.iter().enumerate() { let flex = item.flex.unwrap_or(1.0); let flex_ratio = 95.0 * (flex.max(1.0)) / flex_sum; // we have to subtract the gaps too @@ -112,27 +348,280 @@ impl Component for RowViewComp { "{} {} calc({}% - calc({} * var(--pwt-spacer-4)))", flex, flex, flex_ratio, gaps_ratio ); + let current_coords = Position { + row: row_idx, + item: item_idx, + }; - let widget = props.widget_renderer.apply(&item); - let row_element = Panel::new() - .border(true) + let row_element = RowElement::new(item.clone(), props.widget_renderer.clone()) .margin_x(2) .margin_bottom(4) + .edit_mode(self.edit_mode) + .is_dragging(self.dragging_target == Some(current_coords)) .key(format!("item-{}-{}", coords.row, coords.item)) .style("flex", flex_style) - .with_child(widget); + .style("touch-action", self.edit_mode.then_some("none")) + .on_remove( + ctx.link() + .callback(move |_| Msg::RemoveWidget(current_coords)), + ) + .on_flex_change( + ctx.link() + .callback(move |flex| Msg::EditFlex(current_coords, flex)), + ) + .ondragstart(ctx.link().callback(move |event: DragEvent| { + let data = event.data_transfer().unwrap(); + let _ = data.clear_data(); + let _ = data.set_data("", ""); + Msg::DragEvent(DragMsg::Start(current_coords)) + })) + .onpointerdown(self.edit_mode.then_some(ctx.link().callback( + move |event: PointerEvent| { + // we need to release the pointer capture to trigger pointer events + // on other elements + if let Some(target) = event + .target() + .and_then(|target| target.dyn_into::().ok()) + { + let _ = target.release_pointer_capture(event.pointer_id()); + } + Msg::DragEvent(DragMsg::Start(current_coords)) + }, + ))) + .ondragend(ctx.link().callback(|_| Msg::DragEvent(DragMsg::End))) + .onpointermove((self.dragging.is_some() && self.edit_mode).then_some( + ctx.link().callback(move |event: PointerEvent| { + Msg::DragEvent(DragMsg::DragOver( + OverEvent::Pointer(event), + current_coords, + )) + }), + )) + .ondragover((self.dragging.is_some() && self.edit_mode).then_some( + ctx.link().callback(move |event: DragEvent| { + Msg::DragEvent(DragMsg::DragOver( + OverEvent::Drag(event), + current_coords, + )) + }), + )) + .ondragover(|event: DragEvent| event.prevent_default()) + .ondrop(ctx.link().callback(|event: DragEvent| { + event.prevent_default(); + Msg::DragEvent(DragMsg::End) + })); row.add_child(row_element); } + if self.edit_mode { + let drop_coords = Position { + row: row_idx, + item: items.len().saturating_sub(1), + }; + let is_first_row = row_idx == 0; + let is_last_row = row_idx == (layout.len().saturating_sub(1)); + row.add_child( + Container::new() + .key(format!("row-add-{}", row_idx)) + .style("flex", "1 1 100%") + .margin_x(2) + .margin_bottom(4) + .padding_bottom(4) + .border_bottom(true) + .ondragenter( + ctx.link() + .callback(move |_| Msg::DragEvent(DragMsg::Enter(drop_coords))), + ) + .onpointerenter( + (self.dragging.is_some() && self.edit_mode) + .then_some(ctx.link().callback(move |_| { + Msg::DragEvent(DragMsg::Enter(drop_coords)) + })), + ) + // necessary for drop event to trigger + .ondragover(|event: DragEvent| event.prevent_default()) + .ondrop(ctx.link().callback(|event: DragEvent| { + event.prevent_default(); + Msg::DragEvent(DragMsg::End) + })) + .with_child( + Row::new() + .gap(2) + .with_child( + MenuButton::new(tr!("Add Widget")) + .class(css::ColorScheme::Primary) + .show_arrow(true) + .icon_class("fa fa-plus-circle") + .menu(create_menu( + ctx, + Position { + row: row_idx, + item: items.len(), + }, + )), + ) + .with_child( + Button::new(tr!("Remove Row")) + .icon_class("fa fa-times") + .class(css::ColorScheme::Error) + .on_activate( + ctx.link().callback(move |_| Msg::RemoveRow(row_idx)), + ), + ) + .with_flex_spacer() + .with_child( + Tooltip::new( + ActionIcon::new("fa fa-arrow-down") + .on_activate(ctx.link().callback(move |_| { + Msg::MoveRow(row_idx, MoveDirection::Down) + })) + .disabled(is_last_row), + ) + .tip(tr!("Move Row down")), + ) + .with_child( + Tooltip::new( + ActionIcon::new("fa fa-arrow-up") + .on_activate(ctx.link().callback(move |_| { + Msg::MoveRow(row_idx, MoveDirection::Up) + })) + .disabled(is_first_row), + ) + .tip(tr!("Move Row up")), + ), + ), + ); + } row.add_child( Container::new() .key(format!("spacer-{row_idx}")) .style("flex", "1 1 100%"), ); } - + if self.edit_mode { + row.add_child( + Container::new() + .key("add-row") + .padding_x(2) + .style("flex", "1 1 100%") + .with_child( + Button::new(tr!("Add Row")) + .class(css::ColorScheme::Secondary) + .icon_class("fa fa-plus-circle") + .on_activate(ctx.link().callback(|_| Msg::AddRow)), + ), + ); + } view.add_child(row); - view.into() + view.into_html_with_ref(self.node_ref.clone()) } } + +fn create_menu(ctx: &yew::Context, new_coords: Position) -> Menu { + let create_callback = |widget: WidgetType| { + ctx.link() + .callback(move |_| Msg::AddWidget(new_coords, widget.clone())) + }; + Menu::new() + .with_item( + MenuItem::new(tr!("Remote Panel")) + .on_select(create_callback(WidgetType::Remotes { show_wizard: true })), + ) + .with_item( + MenuItem::new(tr!("Node Panels")).menu( + Menu::new() + .with_item( + MenuItem::new(tr!("All Nodes")) + .on_select(create_callback(WidgetType::Nodes { remote_type: None })), + ) + .with_item(MenuItem::new(tr!("PBS Nodes")).on_select(create_callback( + WidgetType::Nodes { + remote_type: Some(RemoteType::Pbs), + }, + ))) + .with_item(MenuItem::new(tr!("PVE Nodes")).on_select(create_callback( + WidgetType::Nodes { + remote_type: Some(RemoteType::Pve), + }, + ))), + ), + ) + .with_item( + MenuItem::new(tr!("Guest Panels")).menu( + Menu::new() + .with_item( + MenuItem::new(tr!("All Guests")) + .on_select(create_callback(WidgetType::Guests { guest_type: None })), + ) + .with_item( + MenuItem::new(tr!("Virtual Machines")).on_select(create_callback( + WidgetType::Guests { + guest_type: Some(crate::pve::GuestType::Qemu), + }, + )), + ) + .with_item( + MenuItem::new(tr!("Linux Container")).on_select(create_callback( + WidgetType::Guests { + guest_type: Some(crate::pve::GuestType::Lxc), + }, + )), + ), + ), + ) + .with_item( + MenuItem::new(tr!("Subscription Panel")) + .on_select(create_callback(WidgetType::Subscription)), + ) + .with_item( + MenuItem::new(tr!("PBS Datastores")) + .on_select(create_callback(WidgetType::PbsDatastores)), + ) + .with_item( + MenuItem::new(tr!("Leaderboards")).menu( + Menu::new() + .with_item( + MenuItem::new(tr!("Guests with Highest CPU Usage")).on_select( + create_callback(WidgetType::Leaderboard { + leaderboard_type: + crate::dashboard::types::LeaderboardType::GuestCpu, + }), + ), + ) + .with_item( + MenuItem::new(tr!("Nodes With the Hightest CPU Usagge)")).on_select( + create_callback(WidgetType::Leaderboard { + leaderboard_type: crate::dashboard::types::LeaderboardType::NodeCpu, + }), + ), + ) + .with_item( + MenuItem::new(tr!("Nodes With the Highest Memory Usage")).on_select( + create_callback(WidgetType::Leaderboard { + leaderboard_type: + crate::dashboard::types::LeaderboardType::NodeMemory, + }), + ), + ), + ), + ) + .with_item( + MenuItem::new(tr!("Task Summaries")).menu( + Menu::new() + .with_item(MenuItem::new(tr!("Task Summary by Category")).on_select( + create_callback(WidgetType::TaskSummary { + grouping: crate::dashboard::types::TaskSummaryGrouping::Category, + }), + )) + .with_item( + MenuItem::new(tr!("Task Summary Sorted by Failed Tasks")).on_select( + create_callback(WidgetType::TaskSummary { + grouping: crate::dashboard::types::TaskSummaryGrouping::Remote, + }), + ), + ), + ), + ) + .with_item(MenuItem::new(tr!("SDN Panel")).on_select(create_callback(WidgetType::Sdn))) +} -- 2.47.3 _______________________________________________ pdm-devel mailing list pdm-devel@lists.proxmox.com https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel