* [pdm-devel] [PATCH datacenter-manager v3 1/9] pdm-api-types: resources: add helper methods for fields
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 2/9] lib: add pdm-search crate Dominik Csapak
` (9 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
lib/pdm-api-types/src/resource.rs | 27 +++++++++++++++++++++++++++
1 file changed, 27 insertions(+)
diff --git a/lib/pdm-api-types/src/resource.rs b/lib/pdm-api-types/src/resource.rs
index 6227855..0dfadd5 100644
--- a/lib/pdm-api-types/src/resource.rs
+++ b/lib/pdm-api-types/src/resource.rs
@@ -62,6 +62,33 @@ impl Resource {
Resource::PbsDatastore(r) => r.name.as_str(),
}
}
+
+ pub fn resource_type(&self) -> &str {
+ match self {
+ Resource::PveStorage(_) => "storage",
+ Resource::PveQemu(_) => "qemu",
+ Resource::PveLxc(_) => "lxc",
+ Resource::PveNode(_) | Resource::PbsNode(_) => "node",
+ Resource::PbsDatastore(_) => "datastore",
+ }
+ }
+
+ 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",
+ }
+ }
}
#[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] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 2/9] lib: add pdm-search crate
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 1/9] pdm-api-types: resources: add helper methods for fields Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-27 9:12 ` Lukas Wagner
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax Dominik Csapak
` (8 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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>
---
Cargo.toml | 2 +
lib/pdm-search/Cargo.toml | 12 ++
lib/pdm-search/src/lib.rs | 282 ++++++++++++++++++++++++++++++++++++++
3 files changed, 296 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 08b9373..236f00b 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..5f51e75
--- /dev/null
+++ b/lib/pdm-search/Cargo.toml
@@ -0,0 +1,12 @@
+[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
+
+[dependencies]
+anyhow.workspace = true
diff --git a/lib/pdm-search/src/lib.rs b/lib/pdm-search/src/lib.rs
new file mode 100644
index 0000000..85dddda
--- /dev/null
+++ b/lib/pdm-search/src/lib.rs
@@ -0,0 +1,282 @@
+//! 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::str::FromStr;
+
+use anyhow::bail;
+
+#[derive(Clone)]
+pub struct Search {
+ required_terms: Vec<SearchTerm>,
+ optional_terms: Vec<SearchTerm>,
+}
+
+impl Default for Search {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+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()
+ .filter_map(|term| term.parse().ok())
+ .collect()
+ }
+}
+
+impl Search {
+ pub fn new() -> Self {
+ Self::with_terms(Vec::new())
+ }
+
+ pub fn is_empty(&self) -> bool {
+ self.required_terms.is_empty() && self.optional_terms.is_empty()
+ }
+
+ pub fn with_terms(terms: Vec<SearchTerm>) -> 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: Fn(&SearchTerm) -> bool>(&self, matches: F) -> bool {
+ if self.is_empty() {
+ return true;
+ }
+
+ if self.required_terms.iter().map(&matches).any(|f| !f) {
+ return false;
+ }
+
+ if !self.optional_terms.is_empty() && self.optional_terms.iter().map(&matches).all(|f| !f) {
+ return false;
+ }
+
+ true
+ }
+
+ /// Returns true if the combination of [`SearchTerm`]s require that this category value must be
+ /// true. Useful to find out if some condition is required (e.g. type == 'remote')
+ pub fn category_value_required(&self, category: &str, value: &str) -> bool {
+ for term in &self.required_terms {
+ if term.category.as_deref() == Some(category) && value.contains(&term.value) {
+ return true;
+ }
+ }
+
+ let mut optional_count = 0;
+
+ for term in &self.optional_terms {
+ if term.category.as_deref() == Some(category) && term.value == value {
+ optional_count += 1;
+ }
+ }
+
+ self.required_terms.is_empty()
+ && self.optional_terms.len() == optional_count
+ && optional_count > 0
+ }
+}
+
+impl std::fmt::Display for Search {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ for (count, term) in self.required_terms.iter().enumerate() {
+ if count != 0 {
+ write!(f, " ")?;
+ }
+
+ write!(f, "{term}")?;
+ }
+
+ if !self.required_terms.is_empty() && !self.optional_terms.is_empty() {
+ write!(f, " ")?;
+ }
+
+ for (count, term) in self.optional_terms.iter().enumerate() {
+ if count != 0 {
+ write!(f, " ")?;
+ }
+
+ write!(f, "{term}")?;
+ }
+
+ 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: ToString>(term: S) -> Self {
+ Self {
+ value: term.to_string(),
+ optional: false,
+ category: None,
+ }
+ }
+
+ pub fn category<S: ToString>(mut self, category: Option<S>) -> Self {
+ self.category = category.map(|s| s.to_string());
+ self
+ }
+
+ pub fn optional(mut self, optional: bool) -> Self {
+ self.optional = optional;
+ self
+ }
+}
+
+impl FromStr for SearchTerm {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let mut optional = true;
+ let mut term: String = s.into();
+ if term.starts_with("+") {
+ optional = false;
+ term.remove(0);
+ }
+
+ let (term, category) = if let Some(idx) = term.find(":") {
+ let mut real_term = term.split_off(idx);
+ real_term.remove(0); // remove ':'
+ (real_term, Some(term))
+ } else {
+ (term, None)
+ };
+
+ if term.is_empty() {
+ bail!("term cannot be empty");
+ }
+
+ if category == Some("".into()) {
+ bail!("category cannot be empty");
+ }
+
+ Ok(SearchTerm::new(term).optional(optional).category(category))
+ }
+}
+
+impl std::fmt::Display for SearchTerm {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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!(
+ "foo".parse::<SearchTerm>().unwrap(),
+ SearchTerm::new("foo").optional(true),
+ );
+ }
+
+ #[test]
+ fn parse_test_requires_filter() {
+ assert_eq!(
+ "+foo".parse::<SearchTerm>().unwrap(),
+ SearchTerm::new("foo"),
+ );
+ }
+
+ #[test]
+ fn parse_test_category_filter() {
+ assert_eq!(
+ "foo:bar".parse::<SearchTerm>().unwrap(),
+ SearchTerm::new("bar").optional(true).category(Some("foo"))
+ );
+ assert_eq!(
+ "+foo:bar".parse::<SearchTerm>().unwrap(),
+ SearchTerm::new("bar").category(Some("foo"))
+ );
+ }
+
+ #[test]
+ fn parse_test_invalid_filter() {
+ assert!(":bar".parse::<SearchTerm>().is_err());
+ assert!("+cat:".parse::<SearchTerm>().is_err());
+ assert!("+".parse::<SearchTerm>().is_err());
+ assert!(":".parse::<SearchTerm>().is_err());
+ }
+
+ #[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 case in cases {
+ assert!(
+ search.matches(|term| {
+ match term.value.as_str() {
+ "required1" => case.0,
+ "required2" => case.1,
+ "optional1" => case.2,
+ "optional2" => case.3,
+ _ => unreachable!(),
+ }
+ }) == case.4
+ )
+ }
+ }
+}
--
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] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 2/9] lib: add pdm-search crate
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 2/9] lib: add pdm-search crate Dominik Csapak
@ 2025-08-27 9:12 ` Lukas Wagner
0 siblings, 0 replies; 18+ messages in thread
From: Lukas Wagner @ 2025-08-27 9:12 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
Looks good to me, two small suggestions inline.
Some of the `pub` fn's and struct members don't have doc comments, would
be nice if you could add them as well (can also be in a followup if you
prefer).
On Tue Aug 26, 2025 at 2:31 PM CEST, Dominik Csapak wrote:
> 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>
> ---
> Cargo.toml | 2 +
> lib/pdm-search/Cargo.toml | 12 ++
> lib/pdm-search/src/lib.rs | 282 ++++++++++++++++++++++++++++++++++++++
> 3 files changed, 296 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 08b9373..236f00b 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..5f51e75
> --- /dev/null
> +++ b/lib/pdm-search/Cargo.toml
> @@ -0,0 +1,12 @@
> +[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
> +
> +[dependencies]
> +anyhow.workspace = true
> diff --git a/lib/pdm-search/src/lib.rs b/lib/pdm-search/src/lib.rs
> new file mode 100644
> index 0000000..85dddda
> --- /dev/null
> +++ b/lib/pdm-search/src/lib.rs
> @@ -0,0 +1,282 @@
> +//! 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::str::FromStr;
> +
> +use anyhow::bail;
> +
> +#[derive(Clone)]
> +pub struct Search {
> + required_terms: Vec<SearchTerm>,
> + optional_terms: Vec<SearchTerm>,
> +}
> +
> +impl Default for Search {
> + fn default() -> Self {
> + Self::new()
> + }
> +}
> +
> +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()
> + .filter_map(|term| term.parse().ok())
> + .collect()
> + }
> +}
> +
> +impl Search {
> + pub fn new() -> Self {
> + Self::with_terms(Vec::new())
> + }
> +
> + pub fn is_empty(&self) -> bool {
> + self.required_terms.is_empty() && self.optional_terms.is_empty()
> + }
> +
> + pub fn with_terms(terms: Vec<SearchTerm>) -> 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: Fn(&SearchTerm) -> bool>(&self, matches: F) -> bool {
> + if self.is_empty() {
> + return true;
> + }
> +
> + if self.required_terms.iter().map(&matches).any(|f| !f) {
> + return false;
> + }
> +
> + if !self.optional_terms.is_empty() && self.optional_terms.iter().map(&matches).all(|f| !f) {
> + return false;
> + }
> +
> + true
> + }
> +
> + /// Returns true if the combination of [`SearchTerm`]s require that this category value must be
> + /// true. Useful to find out if some condition is required (e.g. type == 'remote')
> + pub fn category_value_required(&self, category: &str, value: &str) -> bool {
> + for term in &self.required_terms {
> + if term.category.as_deref() == Some(category) && value.contains(&term.value) {
> + return true;
> + }
> + }
> +
> + let mut optional_count = 0;
> +
> + for term in &self.optional_terms {
> + if term.category.as_deref() == Some(category) && term.value == value {
> + optional_count += 1;
> + }
> + }
> +
> + self.required_terms.is_empty()
> + && self.optional_terms.len() == optional_count
> + && optional_count > 0
> + }
> +}
> +
> +impl std::fmt::Display for Search {
> + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
> + for (count, term) in self.required_terms.iter().enumerate() {
> + if count != 0 {
> + write!(f, " ")?;
> + }
> +
> + write!(f, "{term}")?;
> + }
> +
> + if !self.required_terms.is_empty() && !self.optional_terms.is_empty() {
> + write!(f, " ")?;
> + }
> +
> + for (count, term) in self.optional_terms.iter().enumerate() {
> + if count != 0 {
> + write!(f, " ")?;
> + }
> +
> + write!(f, "{term}")?;
> + }
> +
> + Ok(())
> + }
> +}
Would be cool to also add two quick tests for category_value_required
and the Display trait implementation; I always find tests valuable as
additional documentation about how something is supposed to work.
> +
> +#[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: ToString>(term: S) -> Self {
> + Self {
> + value: term.to_string(),
> + optional: false,
> + category: None,
> + }
> + }
> +
> + pub fn category<S: ToString>(mut self, category: Option<S>) -> Self {
> + self.category = category.map(|s| s.to_string());
> + self
> + }
> +
> + pub fn optional(mut self, optional: bool) -> Self {
> + self.optional = optional;
> + self
> + }
> +}
> +
> +impl FromStr for SearchTerm {
> + type Err = anyhow::Error;
> +
> + fn from_str(s: &str) -> Result<Self, Self::Err> {
> + let mut optional = true;
> + let mut term: String = s.into();
> + if term.starts_with("+") {
> + optional = false;
> + term.remove(0);
> + }
> +
> + let (term, category) = if let Some(idx) = term.find(":") {
> + let mut real_term = term.split_off(idx);
> + real_term.remove(0); // remove ':'
> + (real_term, Some(term))
> + } else {
> + (term, None)
> + };
> +
> + if term.is_empty() {
> + bail!("term cannot be empty");
> + }
> +
> + if category == Some("".into()) {
> + bail!("category cannot be empty");
> + }
> +
> + Ok(SearchTerm::new(term).optional(optional).category(category))
> + }
> +}
> +
> +impl std::fmt::Display for SearchTerm {
> + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::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!(
> + "foo".parse::<SearchTerm>().unwrap(),
> + SearchTerm::new("foo").optional(true),
> + );
> + }
> +
> + #[test]
> + fn parse_test_requires_filter() {
> + assert_eq!(
> + "+foo".parse::<SearchTerm>().unwrap(),
> + SearchTerm::new("foo"),
> + );
> + }
> +
> + #[test]
> + fn parse_test_category_filter() {
> + assert_eq!(
> + "foo:bar".parse::<SearchTerm>().unwrap(),
> + SearchTerm::new("bar").optional(true).category(Some("foo"))
> + );
> + assert_eq!(
> + "+foo:bar".parse::<SearchTerm>().unwrap(),
> + SearchTerm::new("bar").category(Some("foo"))
> + );
> + }
> +
> + #[test]
> + fn parse_test_invalid_filter() {
> + assert!(":bar".parse::<SearchTerm>().is_err());
> + assert!("+cat:".parse::<SearchTerm>().is_err());
> + assert!("+".parse::<SearchTerm>().is_err());
> + assert!(":".parse::<SearchTerm>().is_err());
> + }
> +
> + #[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),
> + ];
Might be a bit more readable if you split the inputs and outputs as a
2-tuple, e.g.:
let cases = [
((true, true, true, false), true)),
...
]
> + for case in cases {
and then do a `for (input, expected_output)` here.
> + assert!(
> + search.matches(|term| {
> + match term.value.as_str() {
> + "required1" => case.0,
> + "required2" => case.1,
> + "optional1" => case.2,
> + "optional2" => case.3,
> + _ => unreachable!(),
> + }
> + }) == case.4
> + )
> + }
> + }
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 1/9] pdm-api-types: resources: add helper methods for fields Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 2/9] lib: add pdm-search crate Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-27 9:15 ` Lukas Wagner
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 4/9] ui: add possibility to insert into search box Dominik Csapak
` (7 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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>
---
server/Cargo.toml | 1 +
server/src/api/resources.rs | 80 +++++++++++++++++++++++++++++++------
2 files changed, 69 insertions(+), 12 deletions(-)
diff --git a/server/Cargo.toml b/server/Cargo.toml
index 24a2e40..ada7c80 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 6a8c8ef..f17fb64 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -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};
@@ -45,6 +46,44 @@ const SUBDIRS: SubdirMap = &sorted!([
),
]);
+fn resource_matches_search_term(resource: &Resource, term: &SearchTerm) -> bool {
+ match &term.category {
+ Some(category) => match category.as_str() {
+ "type" => resource.resource_type().contains(&term.value),
+ "name" => resource.name().contains(&term.value),
+ "id" => resource.id().contains(&term.value),
+ "status" => resource.status().contains(&term.value),
+ "template" => match resource {
+ Resource::PveQemu(PveQemuResource { template, .. }, ..)
+ | Resource::PveLxc(PveLxcResource { template, .. }) => {
+ match parse_boolean(&term.value) {
+ Ok(boolean) => boolean == *template,
+ Err(_) => false,
+ }
+ }
+ _ => false,
+ },
+ "remote" => true, // this has to be checked beforehand
+ _ => false,
+ },
+ None => resource.name().contains(&term.value) || resource.id().contains(&term.value),
+ }
+}
+
+fn remote_matches_search_term(remote_name: &str, online: Option<bool>, term: &SearchTerm) -> bool {
+ match term.category.as_deref() {
+ Some("remote" | "name" | "id") => remote_name.contains(&term.value),
+ Some("type") => "remote".contains(&term.value),
+ Some("status") => match online {
+ None => true,
+ Some(true) => "online".contains(&term.value),
+ Some(false) => "offline".contains(&term.value),
+ },
+ None => remote_name.contains(&term.value) || "remote".contains(&term.value),
+ Some(_) => false,
+ }
+}
+
#[api(
// FIXME:: see list-like API calls in resource routers, we probably want more fine-grained
// checks..
@@ -104,6 +143,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 = filters.category_value_required("type", "remote");
+
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]);
@@ -111,12 +154,27 @@ 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| resource_matches_search_term(resource, filter))
+ });
+ }
+
RemoteResources {
remote: remote_name,
resources,
@@ -132,17 +190,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)
--
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] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax Dominik Csapak
@ 2025-08-27 9:15 ` Lukas Wagner
2025-08-27 9:33 ` Stefan Hanreich
0 siblings, 1 reply; 18+ messages in thread
From: Lukas Wagner @ 2025-08-27 9:15 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
On Tue Aug 26, 2025 at 2:31 PM CEST, Dominik Csapak wrote:
> 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>
> ---
> server/Cargo.toml | 1 +
> server/src/api/resources.rs | 80 +++++++++++++++++++++++++++++++------
> 2 files changed, 69 insertions(+), 12 deletions(-)
>
> diff --git a/server/Cargo.toml b/server/Cargo.toml
> index 24a2e40..ada7c80 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 6a8c8ef..f17fb64 100644
> --- a/server/src/api/resources.rs
> +++ b/server/src/api/resources.rs
> @@ -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};
> @@ -45,6 +46,44 @@ const SUBDIRS: SubdirMap = &sorted!([
> ),
> ]);
>
> +fn resource_matches_search_term(resource: &Resource, term: &SearchTerm) -> bool {
> + match &term.category {
> + Some(category) => match category.as_str() {
> + "type" => resource.resource_type().contains(&term.value),
> + "name" => resource.name().contains(&term.value),
> + "id" => resource.id().contains(&term.value),
> + "status" => resource.status().contains(&term.value),
> + "template" => match resource {
> + Resource::PveQemu(PveQemuResource { template, .. }, ..)
> + | Resource::PveLxc(PveLxcResource { template, .. }) => {
> + match parse_boolean(&term.value) {
> + Ok(boolean) => boolean == *template,
> + Err(_) => false,
> + }
> + }
> + _ => false,
> + },
> + "remote" => true, // this has to be checked beforehand
> + _ => false,
> + },
> + None => resource.name().contains(&term.value) || resource.id().contains(&term.value),
> + }
> +}
> +
> +fn remote_matches_search_term(remote_name: &str, online: Option<bool>, term: &SearchTerm) -> bool {
> + match term.category.as_deref() {
> + Some("remote" | "name" | "id") => remote_name.contains(&term.value),
> + Some("type") => "remote".contains(&term.value),
This is IMO a bit odd, since this would allow you to write something
like
type:e
and still get type:remote matches, since e is contained in 'remote'.
I think for some of these I would only allow a full match, 'type' and
'status' comes to mind at a quick glance (for status online, on,
offline, off) could be allowed values)
> + Some("status") => match online {
> + None => true,
> + Some(true) => "online".contains(&term.value),
> + Some(false) => "offline".contains(&term.value),
> + },
> + None => remote_name.contains(&term.value) || "remote".contains(&term.value),
> + Some(_) => false,
> + }
> +}
> +
> #[api(
> // FIXME:: see list-like API calls in resource routers, we probably want more fine-grained
> // checks..
> @@ -104,6 +143,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 = filters.category_value_required("type", "remote");
> +
> 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]);
> @@ -111,12 +154,27 @@ 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| resource_matches_search_term(resource, filter))
> + });
> + }
> +
> RemoteResources {
> remote: remote_name,
> resources,
> @@ -132,17 +190,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)
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax
2025-08-27 9:15 ` Lukas Wagner
@ 2025-08-27 9:33 ` Stefan Hanreich
2025-08-27 20:15 ` Thomas Lamprecht
0 siblings, 1 reply; 18+ messages in thread
From: Stefan Hanreich @ 2025-08-27 9:33 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Lukas Wagner,
Dominik Csapak
On 8/27/25 11:16 AM, Lukas Wagner wrote:
[snip]
>> +fn remote_matches_search_term(remote_name: &str, online: Option<bool>, term: &SearchTerm) -> bool {
>> + match term.category.as_deref() {
>> + Some("remote" | "name" | "id") => remote_name.contains(&term.value),
>> + Some("type") => "remote".contains(&term.value),
>
> This is IMO a bit odd, since this would allow you to write something
> like
>
> type:e
>
> and still get type:remote matches, since e is contained in 'remote'.
>
> I think for some of these I would only allow a full match, 'type' and
> 'status' comes to mind at a quick glance (for status online, on,
> offline, off) could be allowed values)
>
maybe a prefix match is a good compromise? that way one can start typing
'type:remo' and get the results early without having to type out the
full status / type /...
[snip]
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax
2025-08-27 9:33 ` Stefan Hanreich
@ 2025-08-27 20:15 ` Thomas Lamprecht
0 siblings, 0 replies; 18+ messages in thread
From: Thomas Lamprecht @ 2025-08-27 20:15 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion,
Stefan Hanreich, Lukas Wagner, Dominik Csapak
On 27/08/2025 11:33, Stefan Hanreich wrote:
> On 8/27/25 11:16 AM, Lukas Wagner wrote:
>
> [snip]
>
>>> +fn remote_matches_search_term(remote_name: &str, online: Option<bool>, term: &SearchTerm) -> bool {
>>> + match term.category.as_deref() {
>>> + Some("remote" | "name" | "id") => remote_name.contains(&term.value),
>>> + Some("type") => "remote".contains(&term.value),
>>
>> This is IMO a bit odd, since this would allow you to write something
>> like
>>
>> type:e
>>
>> and still get type:remote matches, since e is contained in 'remote'.
>>
>> I think for some of these I would only allow a full match, 'type' and
>> 'status' comes to mind at a quick glance (for status online, on,
>> offline, off) could be allowed values)
>>
>
> maybe a prefix match is a good compromise? that way one can start typing
> 'type:remo' and get the results early without having to type out the
> full status / type /...
I think that would be a good compromise for now, that should be relatively
easy to grasp and still give some benefits.
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 4/9] ui: add possibility to insert into search box
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (2 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 3/9] server: api: resources: add more complex filter syntax Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 5/9] ui: dashboard: remotes panel: open search on click Dominik Csapak
` (6 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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>
---
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 ef66020..4c48502 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] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 5/9] ui: dashboard: remotes panel: open search on click
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (3 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 4/9] ui: add possibility to insert into search box Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-27 9:37 ` Lukas Wagner
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 6/9] ui: dashboard: guest panel: search for guest states when clicking on them Dominik Csapak
` (5 subsequent siblings)
10 siblings, 1 reply; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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>
---
ui/src/dashboard/remote_panel.rs | 32 +++++++++++++++++++++++++++++++-
1 file changed, 31 insertions(+), 1 deletion(-)
diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs
index 7471fb6..849b4d3 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,16 @@ 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")
+ .optional(true)
+ .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] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 5/9] ui: dashboard: remotes panel: open search on click
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 5/9] ui: dashboard: remotes panel: open search on click Dominik Csapak
@ 2025-08-27 9:37 ` Lukas Wagner
2025-08-28 8:54 ` Dominik Csapak
0 siblings, 1 reply; 18+ messages in thread
From: Lukas Wagner @ 2025-08-27 9:37 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
Cc: pdm-devel
I think this is a nice feature, but I think the discoverability is not
too great here; it seems like a feature that users could easily miss if
they don't read about it in the documentation or stumble across it by
accident. I think the main issue is that the 'card' does not look like
an interactive UI element at all; unless you hover it with the cursor
you get no hint that it is indeed clickable.
Maybe an actual button with some text or an icon (although I'm not too
sure what the text should say or the icon should look like) would be
better? What do you think?
On Tue Aug 26, 2025 at 2:31 PM CEST, Dominik Csapak wrote:
> 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>
> ---
> ui/src/dashboard/remote_panel.rs | 32 +++++++++++++++++++++++++++++++-
> 1 file changed, 31 insertions(+), 1 deletion(-)
>
> diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs
> index 7471fb6..849b4d3 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,16 @@ 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")
> + .optional(true)
> + .category(Some("type"))])
> + }
> +}
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 5/9] ui: dashboard: remotes panel: open search on click
2025-08-27 9:37 ` Lukas Wagner
@ 2025-08-28 8:54 ` Dominik Csapak
0 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-28 8:54 UTC (permalink / raw)
To: Lukas Wagner, Proxmox Datacenter Manager development discussion; +Cc: pdm-devel
On 8/27/25 11:37 AM, Lukas Wagner wrote:
> I think this is a nice feature, but I think the discoverability is not
> too great here; it seems like a feature that users could easily miss if
> they don't read about it in the documentation or stumble across it by
> accident. I think the main issue is that the 'card' does not look like
> an interactive UI element at all; unless you hover it with the cursor
> you get no hint that it is indeed clickable.
>
> Maybe an actual button with some text or an icon (although I'm not too
> sure what the text should say or the icon should look like) would be
> better? What do you think?
i get what you mean, and yeah maybe an icon to click on would be ok
(a search icon for example)
though i guess that won't work on all panels (e.g. in the remote panel
i can't think of a good place right now)
what i want to do later is to include a settings panel for the search
bar, similar to what other tools (e.g. gmail do) that lets you configure
the search parameters that get filled in the text box
this should improve discoverability very much. i didn't do it yet
simply because of time constraints
>
> On Tue Aug 26, 2025 at 2:31 PM CEST, Dominik Csapak wrote:
>> 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>
>> ---
>> ui/src/dashboard/remote_panel.rs | 32 +++++++++++++++++++++++++++++++-
>> 1 file changed, 31 insertions(+), 1 deletion(-)
>>
>> diff --git a/ui/src/dashboard/remote_panel.rs b/ui/src/dashboard/remote_panel.rs
>> index 7471fb6..849b4d3 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,16 @@ 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")
>> + .optional(true)
>> + .category(Some("type"))])
>> + }
>> +}
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 6/9] ui: dashboard: guest panel: search for guest states when clicking on them
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (4 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 5/9] ui: dashboard: remotes panel: open search on click Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 7/9] ui: dashboard: search for nodes when clicking on the nodes panel Dominik Csapak
` (4 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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>
---
ui/src/dashboard/guest_panel.rs | 82 +++++++++++++++++++++++++++++++--
1 file changed, 78 insertions(+), 4 deletions(-)
diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
index 8d130c3..1e6a9c7 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_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,67 @@ 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 {
+ if status.is_none() && template.is_none() {
+ return Search::with_terms(vec![SearchTerm::new(guest_type).category(Some("type"))]);
+ }
+
+ let mut terms = vec![SearchTerm::new(guest_type).category(Some("type"))];
+
+ if let Some(template) = template {
+ terms.push(SearchTerm::new(template).category(Some("template")));
+ }
+ if let Some(status) = status {
+ terms.push(SearchTerm::new(status).category(Some("status")));
+ }
+ Search::with_terms(terms)
+}
--
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] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 7/9] ui: dashboard: search for nodes when clicking on the nodes panel
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (5 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 6/9] ui: dashboard: guest panel: search for guest states when clicking on them Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 8/9] ui: search box: add clear trigger Dominik Csapak
` (3 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 UTC (permalink / raw)
To: pdm-devel
similar to what we do for the remote panel
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/mod.rs | 46 +++++++++++++++++++++++++++++++++++++----
1 file changed, 42 insertions(+), 4 deletions(-)
diff --git a/ui/src/dashboard/mod.rs b/ui/src/dashboard/mod.rs
index 84efb1b..1ac683b 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,13 +116,25 @@ 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 (status_icon, text): (Fa, String) = match status {
NodeStatusCount {
- online, offline, ..
+ online,
+ offline,
+ unknown,
} if *offline > 0 => (
Status::Error.into(),
- tr!("{0} of {1} nodes are offline", offline, online),
+ tr!(
+ "{0} of {1} nodes are offline",
+ offline,
+ online + offline + unknown,
+ ),
),
NodeStatusCount { unknown, .. } if *unknown > 0 => (
Status::Warning.into(),
@@ -143,6 +157,23 @@ impl PdmDashboard {
.with_child(
Column::new()
.padding(4)
+ .class("pwt-pointer")
+ .onclick(ctx.link().callback(move |_| {
+ Msg::Search(Search::with_terms(vec![
+ SearchTerm::new("node").category(Some("type"))
+ ]))
+ }))
+ .onkeydown(ctx.link().batch_callback(move |event: KeyboardEvent| {
+ match event.key().as_str() {
+ "Enter" | " " => {
+ Some(Msg::Search(Search::with_terms(vec![SearchTerm::new(
+ "node",
+ )
+ .category(Some("type"))])))
+ }
+ _ => None,
+ }
+ }))
.class(FlexFit)
.class(AlignItems::Center)
.class(JustifyContent::Center)
@@ -290,6 +321,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 +371,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] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 8/9] ui: search box: add clear trigger
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (6 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 7/9] ui: dashboard: search for nodes when clicking on the nodes panel Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 9/9] ui: dashboard: guest panel: improve column widths Dominik Csapak
` (2 subsequent siblings)
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 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>
---
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] 18+ messages in thread
* [pdm-devel] [PATCH datacenter-manager v3 9/9] ui: dashboard: guest panel: improve column widths
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (7 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 8/9] ui: search box: add clear trigger Dominik Csapak
@ 2025-08-26 12:31 ` Dominik Csapak
2025-08-26 14:22 ` [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Stefan Hanreich
2025-08-28 13:21 ` [pdm-devel] superseded: " Dominik Csapak
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-26 12:31 UTC (permalink / raw)
To: pdm-devel
Giving the text much more space than the numbers is a bit counter
productive here, since the text are fixed size, but the values can get
much wider if they're large enough. With both having the same space
(flex 1), it still is perfectly visible with the minimum width of the
component.
Signed-off-by: Dominik Csapak <d.csapak@proxmox.com>
---
ui/src/dashboard/guest_panel.rs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/ui/src/dashboard/guest_panel.rs b/ui/src/dashboard/guest_panel.rs
index 1e6a9c7..c92e2cf 100644
--- a/ui/src/dashboard/guest_panel.rs
+++ b/ui/src/dashboard/guest_panel.rs
@@ -81,7 +81,7 @@ fn columns(guest_type: GuestType) -> Rc<Vec<DataTableHeader<StatusRow>>> {
})
.into(),
DataTableColumn::new("text")
- .flex(5)
+ .flex(1)
.render(|item: &StatusRow| {
match item {
StatusRow::State(GuestState::Running, _) => tr!("running"),
--
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] 18+ messages in thread
* Re: [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (8 preceding siblings ...)
2025-08-26 12:31 ` [pdm-devel] [PATCH datacenter-manager v3 9/9] ui: dashboard: guest panel: improve column widths Dominik Csapak
@ 2025-08-26 14:22 ` Stefan Hanreich
2025-08-28 13:21 ` [pdm-devel] superseded: " Dominik Csapak
10 siblings, 0 replies; 18+ messages in thread
From: Stefan Hanreich @ 2025-08-26 14:22 UTC (permalink / raw)
To: Proxmox Datacenter Manager development discussion, Dominik Csapak
Changes to the code LGTM!
Did another quick test of this patch series on my machine by clicking on
all the elements on the dashboard and adding some custom search terms /
conditions:
Only small thing I noticed is that when clicking on Remotes, the
singular search term still isn't required. Could easily be done in a
follow-up imo.
Consider this:
Reviewed-by: Stefan Hanreich <s.hanreich@proxmox.com>
Tested-by: Stefan Hanreich <s.hanreich@proxmox.com>
On 8/26/25 2:34 PM, 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.
>
> 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 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 (9):
> 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: improve column widths
>
> Cargo.toml | 2 +
> lib/pdm-api-types/src/resource.rs | 27 +++
> lib/pdm-search/Cargo.toml | 12 ++
> lib/pdm-search/src/lib.rs | 282 ++++++++++++++++++++++++++++++
> server/Cargo.toml | 1 +
> server/src/api/resources.rs | 80 +++++++--
> ui/Cargo.toml | 1 +
> ui/src/dashboard/guest_panel.rs | 84 ++++++++-
> ui/src/dashboard/mod.rs | 46 ++++-
> ui/src/dashboard/remote_panel.rs | 32 +++-
> ui/src/lib.rs | 3 +
> ui/src/main.rs | 17 +-
> ui/src/search_provider.rs | 35 ++++
> ui/src/widget/search_box.rs | 40 ++++-
> 14 files changed, 629 insertions(+), 33 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
>
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread
* [pdm-devel] superseded: [PATCH datacenter-manager v3 0/9] implement more complex search syntax
2025-08-26 12:31 [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Dominik Csapak
` (9 preceding siblings ...)
2025-08-26 14:22 ` [pdm-devel] [PATCH datacenter-manager v3 0/9] implement more complex search syntax Stefan Hanreich
@ 2025-08-28 13:21 ` Dominik Csapak
10 siblings, 0 replies; 18+ messages in thread
From: Dominik Csapak @ 2025-08-28 13:21 UTC (permalink / raw)
To: pdm-devel
superseded by v4:
https://lore.proxmox.com/pdm-devel/20250828131832.4058422-1-d.csapak@proxmox.com/
_______________________________________________
pdm-devel mailing list
pdm-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pdm-devel
^ permalink raw reply [flat|nested] 18+ messages in thread