* [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax
@ 2025-09-03 13:09 Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 01/10] pdm-api-types: resources: add helper methods for fields Dominik Csapak
` (10 more replies)
0 siblings, 11 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
this introduces a more complex search syntax for the resources api call
and uses that with the dashboard to show relevant resources, e.g.
when clicking on the remotes panel when there are failed remotes, it
adds a search to the box that filters for offline remotes. Same
for clicking on the running vm count, etc.
The syntax is a first draft of mine, we can still tweak and change it
as we see fit, but it's a start.
a 'normal' search term gets filtered to id/name so that does not change
as before, but you can now specify 'categories' with `category:value`
e.g. it's now possible to search for `type:remote` or `status:offline`
it also adds the possibility to mark terms as required like this:
+someterm
required terms have to exist in the resulting resource, while optional
ones are OR'd (so at least one optional match must exist)
Not implemented yet are (but can be done afterwards):
* GUI for filtering
* Include subscription status
* negation of terms (this get's complicated fast ;) )
changes from v4:
* integrated suggestions from wolfgang and shannon (see specific patches
for details)
* made SearchTerm always parse from &str, so an empty string as well
as things like ":foo", "foo:" and "+" are valid searchterms now
(even if they can't find anything at the moment)
* added more useful searches to the node panel
(added a status when e.g. nodes are offline or unknown)
changes from v3:
* use 'starts_with' for type/status filtering
* add missing doc comments
* add more tests
* refactor guest panel in dashboard to use `List`
* add icon in guest panel for discoverability
changes from v2:
* incorporated stefans feedback for FromIterator, Default, etc.
* added tests for the 'matches' function
* add required search term for the guest 'All' text
changes from v1:
* added commit to improve text width
* added commit to add clear trigger to search box
* rebased on master
Dominik Csapak (10):
pdm-api-types: resources: add helper methods for fields
lib: add pdm-search crate
server: api: resources: add more complex filter syntax
ui: add possibility to insert into search box
ui: dashboard: remotes panel: open search on click
ui: dashboard: guest panel: search for guest states when clicking on
them
ui: dashboard: search for nodes when clicking on the nodes panel
ui: search box: add clear trigger
ui: dashboard: guest panel: use `List` instead of `DataTable`
ui: dashboard: guest panel: add search icon for better discoverability
Cargo.toml | 2 +
lib/pdm-api-types/src/resource.rs | 72 ++++++++
lib/pdm-search/Cargo.toml | 9 +
lib/pdm-search/src/lib.rs | 267 ++++++++++++++++++++++++++++++
server/Cargo.toml | 1 +
server/src/api/resources.rs | 229 +++++++++++++++++++++++--
ui/Cargo.toml | 1 +
ui/src/dashboard/guest_panel.rs | 210 +++++++++++++----------
ui/src/dashboard/mod.rs | 63 +++++--
ui/src/dashboard/remote_panel.rs | 30 +++-
ui/src/lib.rs | 3 +
ui/src/main.rs | 17 +-
ui/src/pve/mod.rs | 21 ++-
ui/src/search_provider.rs | 35 ++++
ui/src/widget/search_box.rs | 40 ++++-
15 files changed, 877 insertions(+), 123 deletions(-)
create mode 100644 lib/pdm-search/Cargo.toml
create mode 100644 lib/pdm-search/src/lib.rs
create mode 100644 ui/src/search_provider.rs
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 01/10] pdm-api-types: resources: add helper methods for fields
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 02/10] lib: add pdm-search crate Dominik Csapak
` (9 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
namely 'resource_type' and 'status'. All resources have those fields
in one way or another, so adding a helper that one does not have to
use `match` on every call site makes code there shorter.
Adds a ResourceType enum as a helper, since that can come in handy when
comparing/showing/etc. the types
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v4:
* added ResourceType enum, that converts from str and to str
lib/pdm-api-types/src/resource.rs | 72 +++++++++++++++++++++++++++++++
1 file changed, 72 insertions(+)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 6227855..fd2d49b 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -1,3 +1,4 @@
+use anyhow::{bail, Error};
use serde::{Deserialize, Serialize};
use proxmox_schema::api;
@@ -62,6 +63,77 @@ impl Resource {
Resource::PbsDatastore(r) => r.name.as_str(),
}
}
+
+ pub fn resource_type(&self) -> ResourceType {
+ match self {
+ Resource::PveStorage(_) => ResourceType::PveStorage,
+ Resource::PveQemu(_) => ResourceType::PveQemu,
+ Resource::PveLxc(_) => ResourceType::PveLxc,
+ Resource::PveNode(_) | Resource::PbsNode(_) => ResourceType::Node,
+ Resource::PbsDatastore(_) => ResourceType::PbsDatastore,
+ }
+ }
+
+ pub fn status(&self) -> &str {
+ match self {
+ Resource::PveStorage(r) => r.status.as_str(),
+ Resource::PveQemu(r) => r.status.as_str(),
+ Resource::PveLxc(r) => r.status.as_str(),
+ Resource::PveNode(r) => r.status.as_str(),
+ Resource::PbsNode(r) => {
+ if r.uptime > 0 {
+ "online"
+ } else {
+ "offline"
+ }
+ }
+ Resource::PbsDatastore(_) => "online",
+ }
+ }
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum ResourceType {
+ PveStorage,
+ PveQemu,
+ PveLxc,
+ PbsDatastore,
+ Node,
+}
+
+impl ResourceType {
+ /// Returns a string representation of the type
+ pub fn as_str(&self) -> &'static str {
+ match self {
+ ResourceType::PveStorage => "storage",
+ ResourceType::PveQemu => "qemu",
+ ResourceType::PveLxc => "lxc",
+ ResourceType::PbsDatastore => "datastore",
+ ResourceType::Node => "node",
+ }
+ }
+}
+
+impl std::fmt::Display for ResourceType {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}", self.as_str())
+ }
+}
+
+impl std::str::FromStr for ResourceType {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let resource_type = match s {
+ "storage" => ResourceType::PveStorage,
+ "qemu" => ResourceType::PveQemu,
+ "lxc" => ResourceType::PveLxc,
+ "datastore" => ResourceType::PbsDatastore,
+ "node" => ResourceType::Node,
+ _ => bail!("invalid resource type"),
+ };
+ Ok(resource_type)
+ }
}
#[api(
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 02/10] lib: add pdm-search crate
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 01/10] pdm-api-types: resources: add helper methods for fields Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 03/10] server: api: resources: add more complex filter syntax Dominik Csapak
` (8 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
Introduce a new create for search & filter related code. It currently
includes basic parsing & testing of search terms. Intended to be used on
some API calls that allow for more complex filters, such as the
resources API.
Contains a `SearchTerm` and a `Search` struct. The former represents
a single term to search for, with an optional category and if it's
optional or not. The latter represents a full search with multiple
terms.
Short syntax summary:
'sometext' : normal search term
'+sometext' : required search term
'cat:text' : looks for 'text' in the category 'cat'
required terms have to exist in the resulting match, while optional
ones are OR'd (so at least one optional match must exist)
This is loosely inspired by various search syntaxes, e.g. the one from
gitlab[0] or query-dsl from elastic[1]
[1] https://docs.gitlab.com/user/search/advanced_search/
[2] https://www.elastic.co/docs/reference/query-languages/query-dsl/query-dsl-simple-query-string-query
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v4:
* derive default
* impl From<&str> instead of FromStr for SearchTerm, so it cannot fail
now
* removed category_value_required (a specific implementation lives in
the api now)
* added 'is_optional' to SearchTerm, so the matches callback can check
that too
* use Into<String> instead of ToString
* optimized the display trait for Search
Cargo.toml | 2 +
lib/pdm-search/Cargo.toml | 9 ++
lib/pdm-search/src/lib.rs | 267 ++++++++++++++++++++++++++++++++++++++
3 files changed, 278 insertions(+)
create mode 100644 lib/pdm-search/Cargo.toml
create mode 100644 lib/pdm-search/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index f07733d..ae2816a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,7 @@ members = [
"lib/pdm-api-types",
"lib/pdm-client",
"lib/pdm-config",
+ "lib/pdm-search",
"lib/pdm-ui-shared",
"cli/client",
@@ -86,6 +87,7 @@ pdm-api-types = { path = "lib/pdm-api-types" }
pdm-buildcfg = { path = "lib/pdm-buildcfg" }
pdm-config = { path = "lib/pdm-config" }
pdm-client = { version = "0.2", path = "lib/pdm-client" }
+pdm-search = { version = "0.2", path = "lib/pdm-search" }
pdm-ui-shared = { version = "0.2", path = "lib/pdm-ui-shared" }
proxmox-fido2 = { path = "cli/proxmox-fido2" }
diff --git a/lib/pdm-search/Cargo.toml b/lib/pdm-search/Cargo.toml
new file mode 100644
index 0000000..083d62c
--- /dev/null
+++ b/lib/pdm-search/Cargo.toml
@@ -0,0 +1,9 @@
+[package]
+name = "pdm-search"
+description = "Proxmox Datacenter Manager shared ui modules"
+homepage = "https://www.proxmox.com"
+
+version.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
diff --git a/lib/pdm-search/src/lib.rs b/lib/pdm-search/src/lib.rs
new file mode 100644
index 0000000..fa484cf
--- /dev/null
+++ b/lib/pdm-search/src/lib.rs
@@ -0,0 +1,267 @@
+//! Abstraction over a [`Search`] that contains multiple [`SearchTerm`]s.
+//!
+//! Provides methods to filter an item over a combination of such terms and
+//! construct them from text, and serialize them back to text.
+use std::fmt;
+
+#[derive(Default, Clone)]
+pub struct Search {
+ required_terms: Vec<SearchTerm>,
+ optional_terms: Vec<SearchTerm>,
+}
+
+impl FromIterator<SearchTerm> for Search {
+ fn from_iter<T: IntoIterator<Item = SearchTerm>>(iter: T) -> Self {
+ let (optional_terms, required_terms) = iter.into_iter().partition(|term| term.optional);
+
+ Self {
+ required_terms,
+ optional_terms,
+ }
+ }
+}
+
+impl<S: AsRef<str>> From<S> for Search {
+ fn from(value: S) -> Self {
+ value
+ .as_ref()
+ .split_whitespace()
+ .map(SearchTerm::from)
+ .collect()
+ }
+}
+
+impl Search {
+ /// Create a new empty [`Search`]
+ pub fn new() -> Self {
+ Self::with_terms(Vec::new())
+ }
+
+ /// Returns true if no [`SearchTerm`] exist
+ pub fn is_empty(&self) -> bool {
+ self.required_terms.is_empty() && self.optional_terms.is_empty()
+ }
+
+ /// Create a new [`Search`] with the given [`SearchTerm`]s
+ pub fn with_terms<I: IntoIterator<Item = SearchTerm>>(terms: I) -> Self {
+ terms.into_iter().collect()
+ }
+
+ /// Test if the given `Fn(&SearchTerm) -> bool` for all [`SearchTerm`] configured matches
+ ///
+ /// Returns true if it matches considering the constraints:
+ /// if there are no filters, returns true
+ pub fn matches<F: FnMut(&SearchTerm) -> bool>(&self, mut matches: F) -> bool {
+ if self.is_empty() {
+ return true;
+ }
+
+ if self.required_terms.iter().map(&mut matches).any(|f| !f) {
+ return false;
+ }
+
+ if !self.optional_terms.is_empty()
+ && self.optional_terms.iter().map(&mut matches).all(|f| !f)
+ {
+ return false;
+ }
+
+ true
+ }
+}
+
+impl fmt::Display for Search {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ let mut sep = "";
+ for term in self.required_terms.iter().chain(self.optional_terms.iter()) {
+ write!(f, "{sep}{term}")?;
+ sep = " ";
+ }
+
+ Ok(())
+ }
+}
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct SearchTerm {
+ optional: bool,
+ pub value: String,
+ pub category: Option<String>,
+}
+
+impl SearchTerm {
+ /// Creates a new [`SearchTerm`].
+ pub fn new<S: Into<String>>(term: S) -> Self {
+ Self {
+ value: term.into(),
+ optional: false,
+ category: None,
+ }
+ }
+
+ /// Builder style method to set the category
+ pub fn category<S: ToString>(mut self, category: Option<S>) -> Self {
+ self.category = category.map(|s| s.to_string());
+ self
+ }
+
+ /// Builder style method to mark this [`SearchTerm`] as optional
+ pub fn optional(mut self, optional: bool) -> Self {
+ self.optional = optional;
+ self
+ }
+
+ /// Returns if the search term is optional
+ pub fn is_optional(&self) -> bool {
+ self.optional
+ }
+}
+
+impl<S: AsRef<str>> From<S> for SearchTerm {
+ fn from(value: S) -> Self {
+ let term = value.as_ref();
+ let mut optional = true;
+ let term = if let Some(rest) = term.strip_prefix("+") {
+ if rest.is_empty() {
+ term
+ } else {
+ optional = false;
+ rest
+ }
+ } else {
+ term
+ };
+
+ let (term, category) = match term.split_once(':') {
+ Some((category, new_term)) if category.is_empty() || new_term.is_empty() => {
+ (term, None)
+ }
+ Some((category, new_term)) => (new_term, Some(category)),
+ None => (term, None),
+ };
+
+ SearchTerm::new(term).optional(optional).category(category)
+ }
+}
+
+impl fmt::Display for SearchTerm {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if !self.optional {
+ f.write_str("+")?;
+ }
+
+ if let Some(cat) = &self.category {
+ f.write_str(cat)?;
+ f.write_str(":")?;
+ }
+
+ f.write_str(&self.value)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::{Search, SearchTerm};
+
+ #[test]
+ fn parse_test_simple_filter() {
+ assert_eq!(
+ SearchTerm::from("foo"),
+ SearchTerm::new("foo").optional(true),
+ );
+ }
+
+ #[test]
+ fn parse_test_requires_filter() {
+ assert_eq!(SearchTerm::from("+foo"), SearchTerm::new("foo"),);
+ }
+
+ #[test]
+ fn parse_test_category_filter() {
+ assert_eq!(
+ SearchTerm::from("foo:bar"),
+ SearchTerm::new("bar").optional(true).category(Some("foo"))
+ );
+ assert_eq!(
+ SearchTerm::from("+foo:bar"),
+ SearchTerm::new("bar").category(Some("foo"))
+ );
+ }
+
+ #[test]
+ fn parse_test_special_filter() {
+ assert_eq!(
+ SearchTerm::from(":bar"),
+ SearchTerm::new(":bar").optional(true)
+ );
+ assert_eq!(SearchTerm::from("+cat:"), SearchTerm::new("cat:"));
+ assert_eq!(SearchTerm::from("+"), SearchTerm::new("+").optional(true));
+ assert_eq!(SearchTerm::from(":"), SearchTerm::new(":").optional(true));
+ }
+
+ #[test]
+ fn match_tests() {
+ let search = Search::from_iter(vec![
+ SearchTerm::new("required1").optional(false),
+ SearchTerm::new("required2").optional(false),
+ SearchTerm::new("optional1").optional(true),
+ SearchTerm::new("optional2").optional(true),
+ ]);
+
+ // each case contains results for
+ // required1, required2, optional1, optional2
+ // and if it should match or not
+ let cases = [
+ ((true, true, true, false), true),
+ ((true, true, false, true), true),
+ ((true, true, true, true), true),
+ ((true, true, false, false), false),
+ ((true, false, false, false), false),
+ ((false, false, false, false), false),
+ ((false, true, false, false), false),
+ ((false, false, true, true), false),
+ ((false, true, true, true), false),
+ ((true, false, true, true), false),
+ ((true, false, true, false), false),
+ ((false, true, true, false), false),
+ ((false, false, true, false), false),
+ ((true, false, false, true), false),
+ ((false, true, false, true), false),
+ ((false, false, false, true), false),
+ ];
+ for (input, expected) in cases {
+ assert!(
+ search.matches(|term| {
+ match term.value.as_str() {
+ "required1" => input.0,
+ "required2" => input.1,
+ "optional1" => input.2,
+ "optional2" => input.3,
+ _ => unreachable!(),
+ }
+ }) == expected
+ )
+ }
+ }
+
+ #[test]
+ fn test_display() {
+ let term = SearchTerm::new("foo");
+ assert_eq!("+foo", &term.to_string());
+
+ let term = SearchTerm::new("foo").optional(true);
+ assert_eq!("foo", &term.to_string());
+
+ let term = SearchTerm::new("foo").optional(false);
+ assert_eq!("+foo", &term.to_string());
+
+ let term = SearchTerm::new("foo").category(Some("bar"));
+ assert_eq!("+bar:foo", &term.to_string());
+
+ let term = SearchTerm::new("foo").optional(true).category(Some("bar"));
+ assert_eq!("bar:foo", &term.to_string());
+
+ let term = SearchTerm::new("foo").optional(false).category(Some("bar"));
+ assert_eq!("+bar:foo", &term.to_string());
+ }
+}
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 03/10] server: api: resources: add more complex filter syntax
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 01/10] pdm-api-types: resources: add helper methods for fields Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 02/10] lib: add pdm-search crate Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 04/10] ui: add possibility to insert into search box Dominik Csapak
` (7 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
by using the new pdm-search crate for the resources api call.
We have to do 3 filter passes:
* one fast pass for remotes if the filter are constructed in a way that
must filter to 'remote' (in this case we don't have to look at/return
the resources at all, and can skip remotes that don't match)
* a pass for the resources
* a second pass for the remotes that check if they match for
remote/non-remote mixed results
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v4:
* add a MatchCategory helper enum, so we don't have the risk
of parsing handling the same categories differently
* return an option for the resource matching method, so that we can
differentiate between cases that we can decide or not
server/Cargo.toml | 1 +
server/src/api/resources.rs | 229 ++++++++++++++++++++++++++++++++++--
2 files changed, 217 insertions(+), 13 deletions(-)
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 46bef67..9eefa0f 100644
--- a/server/Cargo.toml
+++ b/server/Cargo.toml
@@ -74,6 +74,7 @@ proxmox-acme-api = { workspace = true, features = [ "impl" ] }
pdm-api-types.workspace = true
pdm-buildcfg.workspace = true
pdm-config.workspace = true
+pdm-search.workspace = true
pve-api-types = { workspace = true, features = [ "client" ] }
pbs-api-types.workspace = true
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index a9217b0..98c4dea 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::sync::{LazyLock, RwLock};
-use anyhow::{format_err, Error};
+use anyhow::{bail, format_err, Error};
use futures::future::join_all;
use futures::FutureExt;
@@ -15,12 +15,13 @@ use pdm_api_types::subscription::{
NodeSubscriptionInfo, RemoteSubscriptionState, RemoteSubscriptions, SubscriptionLevel,
};
use pdm_api_types::{Authid, PRIV_RESOURCE_AUDIT};
+use pdm_search::{Search, SearchTerm};
use proxmox_access_control::CachedUserInfo;
use proxmox_router::{
http_bail, list_subdirs_api_method, Permission, Router, RpcEnvironment, SubdirMap,
};
use proxmox_rrd_api_types::RrdTimeframe;
-use proxmox_schema::api;
+use proxmox_schema::{api, parse_boolean};
use proxmox_sortable_macro::sortable;
use proxmox_subscription::SubscriptionStatus;
use pve_api_types::{ClusterResource, ClusterResourceType};
@@ -46,6 +47,89 @@ const SUBDIRS: SubdirMap = &sorted!([
),
]);
+enum MatchCategory {
+ Type,
+ Name,
+ Id,
+ Status,
+ Template,
+ Remote,
+}
+
+impl std::str::FromStr for MatchCategory {
+ type Err = Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let category = match s {
+ "type" => MatchCategory::Type,
+ "name" => MatchCategory::Name,
+ "id" => MatchCategory::Id,
+ "status" => MatchCategory::Status,
+ "template" => MatchCategory::Template,
+ "remote" => MatchCategory::Remote,
+ _ => bail!("invalid category"),
+ };
+ Ok(category)
+ }
+}
+
+impl MatchCategory {
+ fn matches(&self, value: &str, search_term: &str) -> bool {
+ match self {
+ MatchCategory::Type | MatchCategory::Status => value.starts_with(search_term),
+ MatchCategory::Name | MatchCategory::Id | MatchCategory::Remote => {
+ value.contains(search_term)
+ }
+ MatchCategory::Template => match (parse_boolean(value), parse_boolean(search_term)) {
+ (Ok(a), Ok(b)) => a == b,
+ _ => false,
+ },
+ }
+ }
+}
+
+// returns None if we can't decide if it matches, currently only for the `Remote` category`
+fn resource_matches_search_term(resource: &Resource, term: &SearchTerm) -> Option<bool> {
+ let matches = match term.category.as_deref().map(|c| c.parse::<MatchCategory>()) {
+ Some(Ok(category)) => match category {
+ MatchCategory::Type => category.matches(resource.resource_type().as_str(), &term.value),
+ MatchCategory::Name => category.matches(resource.name(), &term.value),
+ MatchCategory::Id => category.matches(&resource.id(), &term.value),
+ MatchCategory::Status => category.matches(resource.status(), &term.value),
+ MatchCategory::Template => match resource {
+ Resource::PveQemu(PveQemuResource { template, .. })
+ | Resource::PveLxc(PveLxcResource { template, .. }) => {
+ category.matches(&template.to_string(), &term.value)
+ }
+ _ => false,
+ },
+ MatchCategory::Remote => return None, // this has to be checked beforehand
+ },
+ Some(Err(_)) => false,
+ None => resource.name().contains(&term.value) || resource.id().contains(&term.value),
+ };
+ Some(matches)
+}
+
+fn remote_matches_search_term(remote_name: &str, online: Option<bool>, term: &SearchTerm) -> bool {
+ match term.category.as_deref().map(|c| c.parse::<MatchCategory>()) {
+ Some(Ok(category)) => match category {
+ MatchCategory::Type => category.matches("remote", &term.value),
+ MatchCategory::Name | MatchCategory::Remote | MatchCategory::Id => {
+ category.matches(remote_name, &term.value)
+ }
+ MatchCategory::Status => match online {
+ Some(true) => category.matches("online", &term.value),
+ Some(false) => category.matches("offline", &term.value),
+ None => true,
+ },
+ MatchCategory::Template => todo!(),
+ },
+ Some(Err(_)) => false,
+ None => remote_name.contains(&term.value) || "remote".starts_with(&term.value),
+ }
+}
+
#[api(
// FIXME:: see list-like API calls in resource routers, we probably want more fine-grained
// checks..
@@ -83,6 +167,41 @@ pub async fn get_resources(
get_resources_impl(max_age, search, Some(rpcenv)).await
}
+// helper to determine if the combination of search terms requires the results
+// to be remotes, so we can skip looking at resources
+fn is_remotes_only(filters: &Search) -> bool {
+ let mut is_required = false;
+ let mut optional_matches = 0;
+ let mut optional_terms = 0;
+ filters.matches(|term| {
+ if term.is_optional() {
+ optional_terms += 1;
+ }
+ match term.category.as_deref() {
+ Some("remote") => {
+ if !term.is_optional() {
+ is_required = true;
+ } else {
+ optional_matches += 1;
+ }
+ }
+ Some("type") if "remote".starts_with(&term.value) => {
+ if !term.is_optional() {
+ is_required = true;
+ } else {
+ optional_matches += 1;
+ }
+ }
+ None => {}
+ _ => {}
+ }
+ // search is short-circuited, so to iterate over all, return true on required and false on optional
+ !term.is_optional()
+ });
+
+ is_required || (optional_matches > 0 && optional_matches == optional_terms)
+}
+
// called from resource_cache where no RPCEnvironment is initialized..
pub(crate) async fn get_resources_impl(
max_age: u64,
@@ -105,6 +224,10 @@ pub(crate) async fn get_resources_impl(
let (remotes_config, _) = pdm_config::remotes::config()?;
let mut join_handles = Vec::new();
+ let filters = search.map(Search::from).unwrap_or_default();
+
+ let remotes_only = is_remotes_only(&filters);
+
for (remote_name, remote) in remotes_config {
if let Some(ref auth_id) = opt_auth_id {
let remote_privs = user_info.lookup_privs(auth_id, &["resource", &remote_name]);
@@ -112,12 +235,30 @@ pub(crate) async fn get_resources_impl(
continue;
}
}
+
+ if remotes_only
+ && !filters.matches(|term| remote_matches_search_term(&remote_name, None, term))
+ {
+ continue;
+ }
+ let filter = filters.clone();
let handle = tokio::spawn(async move {
- let (resources, error) = match get_resources_for_remote(remote, max_age).await {
+ let (mut resources, error) = match get_resources_for_remote(remote, max_age).await {
Ok(resources) => (resources, None),
Err(error) => (Vec::new(), Some(error.to_string())),
};
+ if remotes_only {
+ resources.clear();
+ } else if !filter.is_empty() {
+ resources.retain(|resource| {
+ filter.matches(|filter| {
+ // if we get can't decide if it matches, don't filter it out
+ resource_matches_search_term(resource, filter).unwrap_or(true)
+ })
+ });
+ }
+
RemoteResources {
remote: remote_name,
resources,
@@ -133,17 +274,15 @@ pub(crate) async fn get_resources_impl(
remote_resources.push(handle.await?);
}
- if let Some(search) = search {
- // FIXME implement more complex filter syntax
- remote_resources.retain_mut(|res| {
- if res.remote.contains(&search) {
- true
- } else {
- res.resources
- .retain(|res| res.id().contains(&search) || res.name().contains(&search));
- !res.resources.is_empty()
+ if !filters.is_empty() {
+ remote_resources.retain(|res| {
+ if !res.resources.is_empty() {
+ return true;
}
- });
+ filters.matches(|filter| {
+ remote_matches_search_term(&res.remote, Some(res.error.is_none()), filter)
+ })
+ })
}
Ok(remote_resources)
@@ -728,3 +867,67 @@ fn map_pbs_datastore_status(remote: &str, status: DataStoreStatusListItem) -> Re
disk: status.used.unwrap_or_default(),
})
}
+
+#[cfg(test)]
+mod tests {
+ use crate::api::resources::is_remotes_only;
+ use pdm_search::{Search, SearchTerm};
+
+ #[test]
+ fn is_remote_only() {
+ let remote_term = SearchTerm::new("foo").category(Some("remote"));
+ let remote_term_optional = remote_term.clone().optional(true);
+
+ let other_term = SearchTerm::new("foo");
+ let other_term_optional = other_term.clone().optional(true);
+
+ let type_remote_term = SearchTerm::new("remote").category(Some("type"));
+ let type_remote_term_optional = type_remote_term.clone().optional(true);
+
+ let type_other_term = SearchTerm::new("foo").category(Some("type"));
+ let type_other_term_optional = type_other_term.clone().optional(true);
+
+ let cases = vec![
+ (vec![other_term.clone()], false),
+ (vec![other_term_optional.clone()], false),
+ (vec![remote_term.clone()], true),
+ (vec![remote_term_optional.clone()], true),
+ (vec![type_other_term.clone()], false),
+ (vec![type_other_term_optional.clone()], false),
+ (
+ vec![SearchTerm::new("re").optional(true).category(Some("type"))],
+ true,
+ ),
+ (vec![type_remote_term.clone()], true),
+ (vec![type_remote_term_optional.clone()], true),
+ (
+ vec![
+ type_remote_term_optional.clone(),
+ other_term_optional.clone(),
+ ],
+ false,
+ ),
+ (
+ vec![
+ type_other_term_optional.clone(),
+ other_term_optional.clone(),
+ ],
+ false,
+ ),
+ (
+ vec![
+ type_remote_term.clone(),
+ type_other_term_optional.clone(),
+ other_term_optional.clone(),
+ ],
+ true,
+ ),
+ (vec![other_term.clone(), type_remote_term.clone()], true),
+ ];
+
+ for (count, (case, expected)) in cases.into_iter().enumerate() {
+ let search = Search::from_iter(case.into_iter());
+ assert_eq!(is_remotes_only(&search), expected, "case: {count}");
+ }
+ }
+}
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 04/10] ui: add possibility to insert into search box
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (2 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 03/10] server: api: resources: add more complex filter syntax Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 05/10] ui: dashboard: remotes panel: open search on click Dominik Csapak
` (6 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
by implementing a 'SearchProvider' context. This enables us to
insert a search term from everywhere. This can be helpful e.g. if we
want to prefill the search box with a specific pattern
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
no changes in v5
ui/Cargo.toml | 1 +
ui/src/lib.rs | 3 +++
ui/src/main.rs | 17 ++++++++++++-----
ui/src/search_provider.rs | 35 +++++++++++++++++++++++++++++++++++
ui/src/widget/search_box.rs | 26 +++++++++++++++++++++-----
5 files changed, 72 insertions(+), 10 deletions(-)
create mode 100644 ui/src/search_provider.rs
diff --git a/ui/Cargo.toml b/ui/Cargo.toml
index f87db6b..1c68909 100644
--- a/ui/Cargo.toml
+++ b/ui/Cargo.toml
@@ -43,6 +43,7 @@ pbs-api-types = "1.0.3"
pdm-api-types = { version = "0.2", path = "../lib/pdm-api-types" }
pdm-ui-shared = { version = "0.2", path = "../lib/pdm-ui-shared" }
pdm-client = { version = "0.2", path = "../lib/pdm-client" }
+pdm-search = { version = "0.2", path = "../lib/pdm-search" }
[patch.crates-io]
# proxmox-client = { path = "../../proxmox/proxmox-client" }
diff --git a/ui/src/lib.rs b/ui/src/lib.rs
index e3755ec..edb50f9 100644
--- a/ui/src/lib.rs
+++ b/ui/src/lib.rs
@@ -21,6 +21,9 @@ pub use remotes::RemoteConfigPanel;
mod top_nav_bar;
pub use top_nav_bar::TopNavBar;
+mod search_provider;
+pub use search_provider::SearchProvider;
+
mod dashboard;
pub use dashboard::Dashboard;
use yew_router::prelude::RouterScopeExt;
diff --git a/ui/src/main.rs b/ui/src/main.rs
index 6e2c9b2..be0c10c 100644
--- a/ui/src/main.rs
+++ b/ui/src/main.rs
@@ -22,7 +22,7 @@ use proxmox_yew_comp::{
//use pbs::MainMenu;
use pdm_api_types::subscription::{RemoteSubscriptionState, RemoteSubscriptions};
-use pdm_ui::{register_pve_tasks, MainMenu, RemoteList, TopNavBar};
+use pdm_ui::{register_pve_tasks, MainMenu, RemoteList, SearchProvider, TopNavBar};
type MsgRemoteList = Result<RemoteList, Error>;
@@ -46,6 +46,7 @@ struct DatacenterManagerApp {
remote_list: RemoteList,
remote_list_error: Option<String>,
remote_list_timeout: Option<Timeout>,
+ search_provider: SearchProvider,
}
async fn check_subscription() -> Msg {
@@ -166,6 +167,7 @@ impl Component for DatacenterManagerApp {
remote_list: Vec::new().into(),
remote_list_error: None,
remote_list_timeout: None,
+ search_provider: SearchProvider::new(),
};
this.on_login(ctx, false);
@@ -258,10 +260,15 @@ impl Component for DatacenterManagerApp {
.with_optional_child(subscription_alert);
let context = self.remote_list.clone();
-
- DesktopApp::new(
- html! {<ContextProvider<RemoteList> {context}>{body}</ContextProvider<RemoteList>>},
- )
+ let search_context = self.search_provider.clone();
+
+ DesktopApp::new(html! {
+ <ContextProvider<SearchProvider> context={search_context}>
+ <ContextProvider<RemoteList> {context}>
+ {body}
+ </ContextProvider<RemoteList>>
+ </ContextProvider<SearchProvider>>
+ })
.into()
}
}
diff --git a/ui/src/search_provider.rs b/ui/src/search_provider.rs
new file mode 100644
index 0000000..441cc2b
--- /dev/null
+++ b/ui/src/search_provider.rs
@@ -0,0 +1,35 @@
+use yew::Callback;
+
+use pwt::state::{SharedState, SharedStateObserver};
+
+use pdm_search::Search;
+
+#[derive(Clone, PartialEq)]
+pub struct SearchProvider {
+ state: SharedState<String>,
+}
+
+impl SearchProvider {
+ pub fn new() -> Self {
+ Self {
+ state: SharedState::new("".into()),
+ }
+ }
+
+ pub fn add_listener(
+ &self,
+ cb: impl Into<Callback<SharedState<String>>>,
+ ) -> SharedStateObserver<String> {
+ self.state.add_listener(cb)
+ }
+
+ pub fn search(&self, search_term: Search) {
+ **self.state.write() = search_term.to_string();
+ }
+}
+
+pub fn get_search_provider<T: yew::Component>(ctx: &yew::Context<T>) -> Option<SearchProvider> {
+ let (provider, _context_listener) = ctx.link().context(Callback::from(|_| {}))?;
+
+ Some(provider)
+}
diff --git a/ui/src/widget/search_box.rs b/ui/src/widget/search_box.rs
index 0aeedb7..6b2478f 100644
--- a/ui/src/widget/search_box.rs
+++ b/ui/src/widget/search_box.rs
@@ -9,13 +9,15 @@ use yew::{
};
use pwt::{
- dom::focus::FocusTracker,
- dom::IntoHtmlElement,
+ dom::{focus::FocusTracker, IntoHtmlElement},
prelude::*,
props::CssLength,
+ state::{SharedState, SharedStateObserver},
widget::{form::Field, Container},
};
+use crate::search_provider::get_search_provider;
+
use super::ResourceTree;
#[derive(Properties, PartialEq)]
@@ -35,7 +37,7 @@ impl From<SearchBox> for VNode {
}
pub enum Msg {
- ChangeTerm(String),
+ ChangeTerm(String, bool), // force value
FocusChange(bool),
ToggleFocus,
}
@@ -48,6 +50,8 @@ pub struct PdmSearchBox {
focus: bool,
global_shortcut_listener: Closure<dyn Fn(KeyboardEvent)>,
toggle_focus: bool,
+ _observer: Option<SharedStateObserver<String>>,
+ force_value: bool,
}
impl Component for PdmSearchBox {
@@ -57,6 +61,14 @@ impl Component for PdmSearchBox {
fn create(ctx: &yew::Context<Self>) -> Self {
let link = ctx.link().clone();
+ let _observer = get_search_provider(ctx).map(|search| {
+ search.add_listener(ctx.link().batch_callback(|value: SharedState<String>| {
+ vec![
+ Msg::ToggleFocus,
+ Msg::ChangeTerm(value.read().clone(), true),
+ ]
+ }))
+ });
Self {
search_field_ref: Default::default(),
search_box_ref: Default::default(),
@@ -72,13 +84,16 @@ impl Component for PdmSearchBox {
_ => {}
}
})),
+ _observer,
+ force_value: false,
}
}
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
- Msg::ChangeTerm(term) => {
+ Msg::ChangeTerm(term, force_value) => {
self.search_term = term;
+ self.force_value = force_value;
true
}
Msg::FocusChange(focus) => {
@@ -122,7 +137,8 @@ impl Component for PdmSearchBox {
Field::new()
.placeholder(tr!("Search (Ctrl+Space / Ctrl+Shift+F)"))
.node_ref(self.search_field_ref.clone())
- .on_input(ctx.link().callback(Msg::ChangeTerm)),
+ .value(self.force_value.then_some(self.search_term.clone()))
+ .on_input(ctx.link().callback(|term| Msg::ChangeTerm(term, false))),
)
.with_child(search_result)
.into()
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 05/10] ui: dashboard: remotes panel: open search on click
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (3 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 04/10] ui: add possibility to insert into search box Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 06/10] ui: dashboard: guest panel: search for guest states when clicking on them Dominik Csapak
` (5 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
using the SearchProvider, insert a search term into the search box to
find and show either all remotes (when healthy) or the failed ones.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
no changes in v5
ui/src/dashboard/remote_panel.rs | 30 +++++++++++++++++++++++++++++-
1 file changed, 29 insertions(+), 1 deletion(-)
diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs
index 7471fb6..7f5b61d 100644
--- a/ui/src/dashboard/remote_panel.rs
+++ b/ui/src/dashboard/remote_panel.rs
@@ -1,5 +1,6 @@
use std::rc::Rc;
+use pdm_search::{Search, SearchTerm};
use proxmox_yew_comp::Status;
use pwt::{
css,
@@ -14,6 +15,8 @@ use yew::{
use pdm_api_types::resource::ResourcesStatus;
+use crate::search_provider::get_search_provider;
+
#[derive(Properties, PartialEq)]
/// A panel for showing the overall remotes status
pub struct RemotePanel {
@@ -38,13 +41,20 @@ impl From<RemotePanel> for VNode {
struct PdmRemotePanel {}
impl Component for PdmRemotePanel {
- type Message = &'static str;
+ type Message = Search;
type Properties = RemotePanel;
fn create(_ctx: &yew::Context<Self>) -> Self {
Self {}
}
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ if let Some(search) = get_search_provider(ctx) {
+ search.search(msg);
+ }
+ false
+ }
+
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
let props = ctx.props();
if props.status.is_none() {
@@ -77,6 +87,13 @@ impl Component for PdmRemotePanel {
};
Column::new()
.tabindex(if failure { 0 } else { -1 })
+ .onclick(ctx.link().callback(move |_| create_search_term(failure)))
+ .onkeydown(ctx.link().batch_callback(move |event: KeyboardEvent| {
+ match event.key().as_str() {
+ "Enter" | " " => Some(create_search_term(failure)),
+ _ => None,
+ }
+ }))
.padding(4)
.class(css::FlexFit)
.class(css::AlignItems::Center)
@@ -88,3 +105,14 @@ impl Component for PdmRemotePanel {
.into()
}
}
+
+fn create_search_term(failure: bool) -> Search {
+ if failure {
+ Search::with_terms(vec![
+ SearchTerm::new("remote").category(Some("type")),
+ SearchTerm::new("offline").category(Some("status")),
+ ])
+ } else {
+ Search::with_terms(vec![SearchTerm::new("remote").category(Some("type"))])
+ }
+}
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 06/10] ui: dashboard: guest panel: search for guest states when clicking on them
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (4 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 05/10] ui: dashboard: remotes panel: open search on click Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 07/10] ui: dashboard: search for nodes when clicking on the nodes panel Dominik Csapak
` (4 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
by using the SearchProvider, we create a fitting search term for the
clicked/selected type of guest.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v4:
* implement into ResourceType from GuestType, so we use the same
type as the backend does to filter, minimizes the risk that we
diverge with that
ui/src/dashboard/guest_panel.rs | 87 +++++++++++++++++++++++++++++++--
ui/src/pve/mod.rs | 11 ++++-
2 files changed, 92 insertions(+), 6 deletions(-)
diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
index 8d130c3..678e30f 100644
--- a/ui/src/dashboard/guest_panel.rs
+++ b/ui/src/dashboard/guest_panel.rs
@@ -1,19 +1,26 @@
use std::rc::Rc;
-use pdm_api_types::resource::GuestStatusCount;
+use pdm_api_types::resource::{GuestStatusCount, ResourceType};
+use pdm_search::{Search, SearchTerm};
use proxmox_yew_comp::GuestState;
use pwt::{
prelude::*,
props::ExtractPrimaryKey,
state::Store,
widget::{
- data_table::{DataTable, DataTableColumn, DataTableHeader},
+ data_table::{
+ DataTable, DataTableColumn, DataTableHeader, DataTableKeyboardEvent,
+ DataTableMouseEvent, DataTableRowRenderArgs,
+ },
Fa,
},
};
-use yew::virtual_dom::{VComp, VNode};
+use yew::{
+ virtual_dom::{Key, VComp, VNode},
+ Properties,
+};
-use crate::pve::GuestType;
+use crate::{pve::GuestType, search_provider::get_search_provider};
use super::loading_column;
@@ -101,13 +108,20 @@ fn columns(guest_type: GuestType) -> Rc<Vec<DataTableHeader<StatusRow>>> {
pub struct PdmGuestPanel {}
impl yew::Component for PdmGuestPanel {
- type Message = String;
+ type Message = Search;
type Properties = GuestPanel;
fn create(_ctx: &yew::Context<Self>) -> Self {
Self {}
}
+ fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
+ if let Some(provider) = get_search_provider(ctx) {
+ provider.search(msg);
+ }
+ false
+ }
+
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
let props = ctx.props();
if props.status.is_none() {
@@ -137,7 +151,70 @@ impl yew::Component for PdmGuestPanel {
.striped(false)
.borderless(true)
.bordered(false)
+ .row_render_callback(|renderer: &mut DataTableRowRenderArgs<StatusRow>| {
+ renderer.class.push("pwt-pointer");
+ })
+ .on_row_keydown({
+ let store = store.clone();
+ let link = ctx.link().clone();
+ move |event: &mut DataTableKeyboardEvent| match event.key().as_str() {
+ " " | "Enter" => search_callback(&link, &store, guest_type, &event.record_key),
+ _ => {}
+ }
+ })
+ .on_row_click({
+ let store = store.clone();
+ let link = ctx.link().clone();
+ move |event: &mut DataTableMouseEvent| {
+ search_callback(&link, &store, guest_type, &event.record_key);
+ }
+ })
.show_header(false)
.into()
}
}
+
+fn search_callback(
+ link: &html::Scope<PdmGuestPanel>,
+ store: &Store<StatusRow>,
+ guest_type: GuestType,
+ key: &Key,
+) {
+ if let Some((_, record)) = store.filtered_data().find(|(_, rec)| rec.key() == *key) {
+ let (status, template) = match &*record.record() {
+ StatusRow::State(guest_state, _) => match guest_state {
+ GuestState::Running => (Some("running"), Some(false)),
+ GuestState::Paused => (Some("paused"), Some(false)),
+ GuestState::Stopped => (Some("stopped"), Some(false)),
+ GuestState::Template => (None, Some(true)),
+ GuestState::Unknown => (Some("unknown"), None),
+ },
+ StatusRow::All(_) => (None, None),
+ };
+
+ link.send_message(create_guest_search_term(guest_type, status, template));
+ }
+}
+
+fn create_guest_search_term(
+ guest_type: GuestType,
+ status: Option<&'static str>,
+ template: Option<bool>,
+) -> Search {
+ let resource_type: ResourceType = guest_type.into();
+ if status.is_none() && template.is_none() {
+ return Search::with_terms(vec![
+ SearchTerm::new(resource_type.as_str()).category(Some("type"))
+ ]);
+ }
+
+ let mut terms = vec![SearchTerm::new(resource_type.as_str()).category(Some("type"))];
+
+ if let Some(template) = template {
+ terms.push(SearchTerm::new(template.to_string()).category(Some("template")));
+ }
+ if let Some(status) = status {
+ terms.push(SearchTerm::new(status).category(Some("status")));
+ }
+ Search::with_terms(terms)
+}
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index a3cf573..dd5e04f 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -20,7 +20,7 @@ use pwt::{
widget::{Column, Panel, Row},
};
-use pdm_api_types::resource::PveResource;
+use pdm_api_types::resource::{PveResource, ResourceType};
pub mod lxc;
pub mod node;
@@ -81,6 +81,15 @@ impl Display for GuestType {
}
}
+impl From<GuestType> for ResourceType {
+ fn from(value: GuestType) -> Self {
+ match value {
+ GuestType::Qemu => ResourceType::PveQemu,
+ GuestType::Lxc => ResourceType::PveLxc,
+ }
+ }
+}
+
#[derive(PartialEq, Clone, Copy)]
pub struct GuestInfo {
pub guest_type: GuestType,
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 07/10] ui: dashboard: search for nodes when clicking on the nodes panel
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (5 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 06/10] ui: dashboard: guest panel: search for guest states when clicking on them Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 08/10] ui: search box: add clear trigger Dominik Csapak
` (3 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
similar to what we do for the remote panel
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
changes from v4:
* also include status for offline/unknown nodes in search term
ui/src/dashboard/mod.rs | 63 ++++++++++++++++++++++++++++++++++-------
1 file changed, 52 insertions(+), 11 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 2478d66..0b12d5c 100644
--- a/ui/src/dashboard/mod.rs
+++ b/ui/src/dashboard/mod.rs
@@ -26,9 +26,10 @@ use pwt::{
use pdm_api_types::resource::{GuestStatusCount, NodeStatusCount, ResourcesStatus};
use pdm_client::types::TopEntity;
+use pdm_search::{Search, SearchTerm};
use proxmox_client::ApiResponseData;
-use crate::{pve::GuestType, remotes::AddWizard, RemoteList};
+use crate::{pve::GuestType, remotes::AddWizard, search_provider::get_search_provider, RemoteList};
mod top_entities;
pub use top_entities::TopEntities;
@@ -87,6 +88,7 @@ pub enum Msg {
Reload,
UpdateConfig(DashboardConfig),
ConfigWindow(bool),
+ Search(Search),
}
pub struct PdmDashboard {
@@ -114,18 +116,37 @@ impl PdmDashboard {
.into()
}
- fn create_node_panel(&self, icon: &str, title: String, status: &NodeStatusCount) -> Panel {
+ fn create_node_panel(
+ &self,
+ ctx: &yew::Context<Self>,
+ icon: &str,
+ title: String,
+ status: &NodeStatusCount,
+ ) -> Panel {
+ let mut search_terms = vec![SearchTerm::new("node").category(Some("type"))];
let (status_icon, text): (Fa, String) = match status {
NodeStatusCount {
- online, offline, ..
- } if *offline > 0 => (
- Status::Error.into(),
- tr!("{0} of {1} nodes are offline", offline, online),
- ),
- NodeStatusCount { unknown, .. } if *unknown > 0 => (
- Status::Warning.into(),
- tr!("{0} nodes have an unknown status", unknown),
- ),
+ online,
+ offline,
+ unknown,
+ } if *offline > 0 => {
+ search_terms.push(SearchTerm::new("offline").category(Some("status")));
+ (
+ Status::Error.into(),
+ tr!(
+ "{0} of {1} nodes are offline",
+ offline,
+ online + offline + unknown,
+ ),
+ )
+ }
+ NodeStatusCount { unknown, .. } if *unknown > 0 => {
+ search_terms.push(SearchTerm::new("unknown").category(Some("status")));
+ (
+ Status::Warning.into(),
+ tr!("{0} nodes have an unknown status", unknown),
+ )
+ }
// FIXME, get more detailed status about the failed remotes (name, type, error)?
NodeStatusCount { online, .. } if self.status.failed_remotes > 0 => (
Status::Unknown.into(),
@@ -135,6 +156,7 @@ impl PdmDashboard {
(Status::Success.into(), tr!("{0} nodes online", online))
}
};
+ let search = Search::with_terms(search_terms);
Panel::new()
.flex(1.0)
.width(300)
@@ -143,6 +165,18 @@ impl PdmDashboard {
.with_child(
Column::new()
.padding(4)
+ .class("pwt-pointer")
+ .onclick(ctx.link().callback({
+ let search = search.clone();
+ move |_| Msg::Search(search.clone())
+ }))
+ .onkeydown(ctx.link().batch_callback({
+ let search = search.clone();
+ move |event: KeyboardEvent| match event.key().as_str() {
+ "Enter" | " " => Some(Msg::Search(search.clone())),
+ _ => None,
+ }
+ }))
.class(FlexFit)
.class(AlignItems::Center)
.class(JustifyContent::Center)
@@ -290,6 +324,12 @@ impl Component for PdmDashboard {
self.show_config_window = false;
true
}
+ Msg::Search(search_term) => {
+ if let Some(provider) = get_search_provider(ctx) {
+ provider.search(search_term.into());
+ }
+ false
+ }
}
}
@@ -334,6 +374,7 @@ impl Component for PdmDashboard {
)),
)
.with_child(self.create_node_panel(
+ ctx,
"building",
tr!("Virtual Environment Nodes"),
&self.status.pve_nodes,
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 08/10] ui: search box: add clear trigger
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (6 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 07/10] ui: dashboard: search for nodes when clicking on the nodes panel Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 09/10] ui: dashboard: guest panel: use `List` instead of `DataTable` Dominik Csapak
` (2 subsequent siblings)
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
that removes the search term on click. We also need to set the
flex-basis for now, as otherwise the field 'wobbles' when showing or
hiding the trigger.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
no changes in v5
ui/src/widget/search_box.rs | 14 +++++++++++++-
1 file changed, 13 insertions(+), 1 deletion(-)
diff --git a/ui/src/widget/search_box.rs b/ui/src/widget/search_box.rs
index 6b2478f..52bb156 100644
--- a/ui/src/widget/search_box.rs
+++ b/ui/src/widget/search_box.rs
@@ -13,7 +13,7 @@ use pwt::{
prelude::*,
props::CssLength,
state::{SharedState, SharedStateObserver},
- widget::{form::Field, Container},
+ widget::{form::Field, Container, Trigger},
};
use crate::search_provider::get_search_provider;
@@ -128,16 +128,28 @@ impl Component for PdmSearchBox {
.height(400)
.class("pwt-shadow2");
+ let clear_trigger_icon = if self.search_term.is_empty() {
+ ""
+ } else {
+ "fa fa-times"
+ };
+
Container::new()
.onfocusin(self.focus_tracker.get_focus_callback(true))
.onfocusout(self.focus_tracker.get_focus_callback(false))
.flex(2.0)
+ .style("flex-basis", "230px") // to avoid changing size with trigger
.min_width(230) // placeholder text
.with_child(
Field::new()
.placeholder(tr!("Search (Ctrl+Space / Ctrl+Shift+F)"))
.node_ref(self.search_field_ref.clone())
.value(self.force_value.then_some(self.search_term.clone()))
+ .with_trigger(
+ Trigger::new(clear_trigger_icon)
+ .onclick(ctx.link().callback(|_| Msg::ChangeTerm("".into(), true))),
+ true,
+ )
.on_input(ctx.link().callback(|term| Msg::ChangeTerm(term, false))),
)
.with_child(search_result)
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 09/10] ui: dashboard: guest panel: use `List` instead of `DataTable`
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (7 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 08/10] ui: search box: add clear trigger Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 10/10] ui: dashboard: guest panel: add search icon for better discoverability Dominik Csapak
2025-09-04 17:21 ` [pdm-devel] applied-series: [PATCH datacenter-manager v5 00/10] implement more complex search syntax Thomas Lamprecht
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
since this is a relatively simple list, using the datatable is a big
overhead. Converting it to a simple list, while keeping all the
functionality we had before, so we can remove quite a bit of code and
not run into issues with different padding scaling (e.g. with the
relaxed spacing)
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
no changes in v5
ui/src/dashboard/guest_panel.rs | 206 +++++++++++++-------------------
ui/src/pve/mod.rs | 10 ++
2 files changed, 93 insertions(+), 123 deletions(-)
diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
index 678e30f..af35c2c 100644
--- a/ui/src/dashboard/guest_panel.rs
+++ b/ui/src/dashboard/guest_panel.rs
@@ -4,19 +4,12 @@ use pdm_api_types::resource::{GuestStatusCount, ResourceType};
use pdm_search::{Search, SearchTerm};
use proxmox_yew_comp::GuestState;
use pwt::{
+ css::{self, TextAlign},
prelude::*,
- props::ExtractPrimaryKey,
- state::Store,
- widget::{
- data_table::{
- DataTable, DataTableColumn, DataTableHeader, DataTableKeyboardEvent,
- DataTableMouseEvent, DataTableRowRenderArgs,
- },
- Fa,
- },
+ widget::{Container, Fa, List, ListTile},
};
use yew::{
- virtual_dom::{Key, VComp, VNode},
+ virtual_dom::{VComp, VNode},
Properties,
};
@@ -49,62 +42,6 @@ pub enum StatusRow {
All(u64),
}
-impl ExtractPrimaryKey for StatusRow {
- fn extract_key(&self) -> yew::virtual_dom::Key {
- yew::virtual_dom::Key::from(match self {
- StatusRow::State(state, _) => match state {
- GuestState::Running => "running",
- GuestState::Paused => "paused",
- GuestState::Stopped => "stopped",
- GuestState::Template => "template",
- GuestState::Unknown => "unknown",
- },
- StatusRow::All(_) => "all",
- })
- }
-}
-
-fn columns(guest_type: GuestType) -> Rc<Vec<DataTableHeader<StatusRow>>> {
- Rc::new(vec![
- DataTableColumn::new("icon")
- .width("3em")
- .render(move |item: &StatusRow| {
- match item {
- StatusRow::State(state, _) => (*state).into(),
- StatusRow::All(_) => match guest_type {
- GuestType::Qemu => Fa::new("desktop"),
- GuestType::Lxc => Fa::new("cubes"),
- },
- }
- .fixed_width()
- .into()
- })
- .into(),
- DataTableColumn::new("text")
- .flex(5)
- .render(|item: &StatusRow| {
- match item {
- StatusRow::State(GuestState::Running, _) => tr!("running"),
- StatusRow::State(GuestState::Stopped, _) => tr!("stopped"),
- StatusRow::State(GuestState::Paused, _) => tr!("paused"),
- StatusRow::State(GuestState::Template, _) => tr!("Template"),
- StatusRow::State(GuestState::Unknown, _) => tr!("Unknown"),
- StatusRow::All(_) => tr!("All"),
- }
- .into()
- })
- .into(),
- DataTableColumn::new("count")
- .flex(1)
- .justify("right")
- .render(|item: &StatusRow| match item {
- StatusRow::State(_, count) => count.into(),
- StatusRow::All(count) => count.into(),
- })
- .into(),
- ])
-}
-
pub struct PdmGuestPanel {}
impl yew::Component for PdmGuestPanel {
@@ -124,76 +61,99 @@ impl yew::Component for PdmGuestPanel {
fn view(&self, ctx: &yew::Context<Self>) -> yew::Html {
let props = ctx.props();
- if props.status.is_none() {
- return loading_column().into();
- }
let guest_type = props.guest_type;
- let status = ctx.props().status.clone().unwrap();
+ let status = match &props.status {
+ Some(status) => status,
+ None => return loading_column().into(),
+ };
- let store = Store::new();
- store.set_data(vec![
+ let data = vec![
StatusRow::State(GuestState::Running, status.running),
StatusRow::State(GuestState::Stopped, status.stopped),
StatusRow::State(GuestState::Template, status.template),
StatusRow::State(GuestState::Unknown, status.unknown),
StatusRow::All(status.running + status.stopped + status.template + status.unknown),
- ]);
+ ];
+
+ let tiles: Vec<_> = data
+ .into_iter()
+ .filter_map(|row| create_list_tile(ctx.link(), guest_type, row))
+ .collect();
+
+ let list = List::new(tiles.len() as u64, move |idx: u64| {
+ tiles[idx as usize].clone()
+ })
+ .padding(4)
+ .class(css::Flex::Fill)
+ .grid_template_columns("auto auto 1fr");
- store.set_filter(|rec: &StatusRow| match rec {
- StatusRow::State(_, count) if *count > 0 => true,
- StatusRow::State(GuestState::Running | GuestState::Stopped, _) => true,
- StatusRow::All(_) => true,
- _ => false,
- });
-
- DataTable::new(columns(guest_type), store.clone())
- .padding(4)
- .striped(false)
- .borderless(true)
- .bordered(false)
- .row_render_callback(|renderer: &mut DataTableRowRenderArgs<StatusRow>| {
- renderer.class.push("pwt-pointer");
- })
- .on_row_keydown({
- let store = store.clone();
- let link = ctx.link().clone();
- move |event: &mut DataTableKeyboardEvent| match event.key().as_str() {
- " " | "Enter" => search_callback(&link, &store, guest_type, &event.record_key),
- _ => {}
- }
- })
- .on_row_click({
- let store = store.clone();
- let link = ctx.link().clone();
- move |event: &mut DataTableMouseEvent| {
- search_callback(&link, &store, guest_type, &event.record_key);
- }
- })
- .show_header(false)
- .into()
+ list.into()
}
}
-fn search_callback(
+fn create_list_tile(
link: &html::Scope<PdmGuestPanel>,
- store: &Store<StatusRow>,
guest_type: GuestType,
- key: &Key,
-) {
- if let Some((_, record)) = store.filtered_data().find(|(_, rec)| rec.key() == *key) {
- let (status, template) = match &*record.record() {
- StatusRow::State(guest_state, _) => match guest_state {
- GuestState::Running => (Some("running"), Some(false)),
- GuestState::Paused => (Some("paused"), Some(false)),
- GuestState::Stopped => (Some("stopped"), Some(false)),
- GuestState::Template => (None, Some(true)),
- GuestState::Unknown => (Some("unknown"), None),
- },
- StatusRow::All(_) => (None, None),
- };
-
- link.send_message(create_guest_search_term(guest_type, status, template));
- }
+ status_row: StatusRow,
+) -> Option<ListTile> {
+ let (icon, text, count, status, template) = match status_row {
+ StatusRow::State(guest_state, count) => match guest_state {
+ GuestState::Template | GuestState::Unknown if count == 0 => return None,
+ GuestState::Paused => return None,
+ GuestState::Running => (
+ Fa::from(guest_state),
+ tr!("running"),
+ count,
+ Some("running"),
+ Some(false),
+ ),
+ GuestState::Stopped => (
+ Fa::from(guest_state),
+ tr!("stopped"),
+ count,
+ Some("stopped"),
+ Some(false),
+ ),
+ GuestState::Template => (
+ Fa::from(guest_state),
+ tr!("Template"),
+ count,
+ None,
+ Some(true),
+ ),
+ GuestState::Unknown => (
+ Fa::from(guest_state),
+ tr!("Unknown"),
+ count,
+ Some("unknown"),
+ None,
+ ),
+ },
+ StatusRow::All(count) => (Fa::from(guest_type), tr!("All"), count, None, None),
+ };
+
+ Some(
+ ListTile::new()
+ .tabindex(0)
+ .interactive(true)
+ .with_child(icon)
+ .with_child(Container::new().padding_x(2).with_child(text))
+ .with_child(
+ Container::new()
+ .class(TextAlign::Right)
+ // FIXME: replace with `column_gap` to `List` when implemented
+ .padding_end(2)
+ .with_child(count),
+ )
+ // FIXME: repalce with on_activate for `ListTile` when implemented
+ .onclick(link.callback(move |_| create_guest_search_term(guest_type, status, template)))
+ .onkeydown(link.batch_callback(
+ move |event: KeyboardEvent| match event.key().as_str() {
+ "Enter" | " " => Some(create_guest_search_term(guest_type, status, template)),
+ _ => None,
+ },
+ )),
+ )
}
fn create_guest_search_term(
diff --git a/ui/src/pve/mod.rs b/ui/src/pve/mod.rs
index dd5e04f..496cbc6 100644
--- a/ui/src/pve/mod.rs
+++ b/ui/src/pve/mod.rs
@@ -90,6 +90,16 @@ impl From<GuestType> for ResourceType {
}
}
+impl From<GuestType> for Fa {
+ fn from(val: GuestType) -> Self {
+ let icon = match val {
+ GuestType::Qemu => "desktop",
+ GuestType::Lxc => "cubes",
+ };
+ Fa::new(icon)
+ }
+}
+
#[derive(PartialEq, Clone, Copy)]
pub struct GuestInfo {
pub guest_type: GuestType,
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v5 10/10] ui: dashboard: guest panel: add search icon for better discoverability
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (8 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 09/10] ui: dashboard: guest panel: use `List` instead of `DataTable` Dominik Csapak
@ 2025-09-03 13:09 ` Dominik Csapak
2025-09-04 17:21 ` [pdm-devel] applied-series: [PATCH datacenter-manager v5 00/10] implement more complex search syntax Thomas Lamprecht
10 siblings, 0 replies; 12+ messages in thread
From: Dominik Csapak @ 2025-09-03 13:09 UTC (permalink / raw)
To: pdm-devel
of the search function. With this icon, it should be clear that one
can click/press these items.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
no changes in v5
ui/src/dashboard/guest_panel.rs | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
index af35c2c..814ecfa 100644
--- a/ui/src/dashboard/guest_panel.rs
+++ b/ui/src/dashboard/guest_panel.rs
@@ -85,7 +85,7 @@ impl yew::Component for PdmGuestPanel {
})
.padding(4)
.class(css::Flex::Fill)
- .grid_template_columns("auto auto 1fr");
+ .grid_template_columns("auto auto 1fr auto");
list.into()
}
@@ -145,6 +145,7 @@ fn create_list_tile(
.padding_end(2)
.with_child(count),
)
+ .with_child(Fa::new("search"))
// FIXME: repalce with on_activate for `ListTile` when implemented
.onclick(link.callback(move |_| create_guest_search_term(guest_type, status, template)))
.onkeydown(link.batch_callback(
--
2.47.2
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
* [pdm-devel] applied-series: [PATCH datacenter-manager v5 00/10] implement more complex search syntax
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
` (9 preceding siblings ...)
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 10/10] ui: dashboard: guest panel: add search icon for better discoverability Dominik Csapak
@ 2025-09-04 17:21 ` Thomas Lamprecht
10 siblings, 0 replies; 12+ messages in thread
From: Thomas Lamprecht @ 2025-09-04 17:21 UTC (permalink / raw)
To: pdm-devel, Dominik Csapak
On Wed, 03 Sep 2025 15:09:16 +0200, Dominik Csapak wrote:
> this introduces a more complex search syntax for the resources api call
> and uses that with the dashboard to show relevant resources, e.g.
> when clicking on the remotes panel when there are failed remotes, it
> adds a search to the box that filters for offline remotes. Same
> for clicking on the running vm count, etc.
>
> The syntax is a first draft of mine, we can still tweak and change it
> as we see fit, but it's a start.
>
> [...]
Applied, thanks!
Some things that might still be improved:
- clear the field if one navigates away? Or at least make the text stand out
less in that case, that probably would be good to do generally when the field
is not active.
- the "click to search" is IMO a bit subtle and slightly surprising, especially
when one doesn't click on the search icon or when there isn't even such a
search icon. Don't get me wrong, I get where that idea come from and do not
find it bad per se and have no definitive improvement suggestions ready, as
it's not really a blocker or set in stone we can also wait on feedback or
some real good idea, these things often just needs a bit of time actually
using them.
[01/10] pdm-api-types: resources: add helper methods for fields
commit: ac6a0c8bc64de7e11e5aac4589e46e84f4401f09
[02/10] lib: add pdm-search crate
commit: 5bb918229abd1fd6d74c14855ca42e81455b45ae
[03/10] server: api: resources: add more complex filter syntax
commit: 23639dc1814643b2dd4159f41f1a43b970195150
[04/10] ui: add possibility to insert into search box
commit: 70faf3e26dda4d3d0e6fe2adf84929be3090029c
[05/10] ui: dashboard: remotes panel: open search on click
commit: 43d3f0a8430180dea2b831da57fe91abfba72639
[06/10] ui: dashboard: guest panel: search for guest states when clicking on them
commit: 5a4fb5d4d4a41d054275d3c3f2f621bc731b991c
[07/10] ui: dashboard: search for nodes when clicking on the nodes panel
commit: 662af17ec051ad03b7e86252ddf7cf7c6e8f450a
[08/10] ui: search box: add clear trigger
commit: 9dfa2c6480f202eeb281ee902d882f9c7d040a51
[09/10] ui: dashboard: guest panel: use `List` instead of `DataTable`
commit: 811d430805438162d5ca5b31f8f347b896cfa3cf
[10/10] ui: dashboard: guest panel: add search icon for better discoverability
commit: a1604aac7bfe5405354010b30d170ea7caeebc7f
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 12+ messages in thread
end of thread, other threads:[~2025-09-04 17:26 UTC | newest]
Thread overview: 12+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-09-03 13:09 [pdm-devel] [PATCH datacenter-manager v5 00/10] implement more complex search syntax Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 01/10] pdm-api-types: resources: add helper methods for fields Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 02/10] lib: add pdm-search crate Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 03/10] server: api: resources: add more complex filter syntax Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 04/10] ui: add possibility to insert into search box Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 05/10] ui: dashboard: remotes panel: open search on click Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 06/10] ui: dashboard: guest panel: search for guest states when clicking on them Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 07/10] ui: dashboard: search for nodes when clicking on the nodes panel Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 08/10] ui: search box: add clear trigger Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 09/10] ui: dashboard: guest panel: use `List` instead of `DataTable` Dominik Csapak
2025-09-03 13:09 ` [pdm-devel] [PATCH datacenter-manager v5 10/10] ui: dashboard: guest panel: add search icon for better discoverability Dominik Csapak
2025-09-04 17:21 ` [pdm-devel] applied-series: [PATCH datacenter-manager v5 00/10] implement more complex search syntax Thomas Lamprecht
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.