edc-web-components 0.5.15

A Rust Web Components with patternfly-yew client for EDC
Documentation
use crate::components::{ListAssetsGallery, SelectedFederatedCatalogOffer};
use crate::models::AssetItem;
use edc_federated_catalog_client::{FederatedCatalogClient, FederatedCatalogClientVersion};
use patternfly_yew::prelude::*;
use yew::prelude::*;
use yew::suspense::use_future_with;
use yew_oauth2::hook::use_latest_access_token;

#[derive(Clone, Debug, PartialEq, Properties)]
pub struct FederatedCatalogPageProps {
  #[prop_or("Catalog".to_string())]
  pub title: String,
  #[prop_or("Explore and access available data offers from your list of trusted and followed participants.".to_string())]
  pub description: String,
  pub on_selected_offer: Callback<SelectedFederatedCatalogOffer>,
  #[prop_or_default]
  pub on_manage_catalog: Option<Callback<()>>,
  #[prop_or("/federated-catalog".to_string())]
  pub federated_catalog_endpoint: String,
  #[prop_or_default]
  pub display_search: bool,
  #[prop_or_default]
  pub search: Option<String>,
  #[prop_or_default]
  pub dcterm_types: Vec<String>,
  #[prop_or("No such offer".to_string())]
  pub empty_title: String,
  #[prop_or("You may not have registered participants.".to_string())]
  pub empty_description: String,
}

#[component]
pub fn FederatedCatalogPage(props: &FederatedCatalogPageProps) -> Html {
  let refresh = use_state(|| 0usize);
  let fallback = html! {
    <Bullseye>
      <Spinner size={SpinnerSize::Lg} />
    </Bullseye>
  };

  let search = use_state(String::new);

  let onchange = use_callback(search.setter(), move |value, search_setter| {
    search_setter.set(value);
  });

  let onclear = use_callback(search.setter(), move |_, search_setter| {
    search_setter.set(String::new());
  });

  let (search_component, search) = if props.display_search {
    let inner = html!(
      <StackItem>
        <SearchInput placeholder="Search" value={(*search).clone()} {onchange} {onclear} />
      </StackItem>
    );

    let search = if (*search).is_empty() {
      None
    } else {
      Some((*search).clone())
    };

    (inner, search)
  } else {
    (html!(), props.search.clone())
  };

  html!(
    <Stack gutter=true>
      <StackItem>
        <Split gutter=true>
          <SplitItem fill=true>
            <Title level={Level::H3} size={Size::XXLarge}>{ &props.title }</Title>
            <p>{ &props.description }</p>
          </SplitItem>
        </Split>
      </StackItem>
      { search_component }
      <StackItem>
        <Suspense {fallback}>
          <FederatedCatalogPageInner
            force_refresh={*refresh}
            on_selected_offer={props.on_selected_offer.clone()}
            on_manage_catalog={props.on_manage_catalog.clone()}
            federated_catalog_endpoint={props.federated_catalog_endpoint.clone()}
            {search}
            dcterm_types={props.dcterm_types.clone()}
            empty_title={props.empty_title.clone()}
            empty_description={props.empty_description.clone()}
          />
        </Suspense>
      </StackItem>
    </Stack>
  )
}

#[derive(Clone, Debug, PartialEq, Properties)]
pub struct FederatedCatalogPageInnerProps {
  pub force_refresh: usize,
  pub on_selected_offer: Callback<SelectedFederatedCatalogOffer>,
  #[prop_or_default]
  pub on_manage_catalog: Option<Callback<()>>,
  #[prop_or("/federated-catalog".to_string())]
  pub federated_catalog_endpoint: String,
  #[prop_or_default]
  pub search: Option<String>,
  #[prop_or_default]
  pub dcterm_types: Vec<String>,
  #[prop_or("No such offer".to_string())]
  pub empty_title: String,
  #[prop_or("You may not have registered participants.".to_string())]
  pub empty_description: String,
}

#[component]
pub fn FederatedCatalogPageInner(props: &FederatedCatalogPageInnerProps) -> HtmlResult {
  let latest_access_token_context = use_latest_access_token().unwrap();

  let asset_items = use_future_with(
    (
      props.federated_catalog_endpoint.clone(),
      props.search.clone(),
      props.dcterm_types.clone(),
      latest_access_token_context.clone(),
      props.force_refresh,
    ),
    |parameters| async move {
      let (federated_catalog_endpoint, search, dcterm_types, latest_access_token_context, _) =
        (*parameters).clone();

      let server_url = web_sys::window().unwrap().location().origin().unwrap();
      let federated_catalog_client = FederatedCatalogClient::new(
        reqwest::Client::new(),
        format!("{server_url}{}", federated_catalog_endpoint),
        latest_access_token_context.access_token(),
        FederatedCatalogClientVersion::V4,
      );

      federated_catalog_client
        .list_offers()
        .await
        .unwrap_or_default()
        .iter()
        .flat_map(|federated_catalog_offer| {
          federated_catalog_offer
            .dataset
            .clone()
            .into_iter()
            .map(|dataset| {
              let dataset_id = dataset.id.clone();
              let asset_item = AssetItem::from(dataset);

              let selected_offer = SelectedFederatedCatalogOffer {
                originator: federated_catalog_offer.originator.clone(),
                provider_id: federated_catalog_offer.participant_id.id.clone(),
                dataset_id,
              };

              (asset_item, selected_offer)
            })
            .filter(|(asset_item, _)| asset_item.is_filtered(&search, &dcterm_types))
            .collect::<Vec<_>>()
        })
        .unzip()
    },
  )?;

  let (asset_items, selected_offers): (Vec<AssetItem>, Vec<SelectedFederatedCatalogOffer>) =
    (*asset_items).clone();

  let onshow = use_callback(
    (props.on_selected_offer.clone(), selected_offers),
    |dataset_id, (on_selected_offer, selected_offers)| {
      if let Some(selected_offer) = selected_offers
        .iter()
        .find(|selected_offer| selected_offer.dataset_id == dataset_id)
      {
        on_selected_offer.emit(selected_offer.clone());
      }
    },
  );

  if asset_items.is_empty() {
    let primary = props.on_manage_catalog.as_ref().map(|on_manage_catalog| {
      Action::new(
        "Manage my catalog subscriptions",
        on_manage_catalog.reform(|_| ()),
      )
    });

    Ok(html! {
      <EmptyState title={props.empty_title.to_string()} {primary}>
        <div>
          <p>{ &props.empty_description }</p>
        </div>
      </EmptyState>
    })
  } else {
    Ok(html!(<ListAssetsGallery {asset_items} {onshow} />))
  }
}