edc-web-components 0.5.6

A Rust Web Components with patternfly-yew client for EDC
Documentation
use crate::components::{ListAssetsGallery, SelectedFederatedCatalogOffer};
use crate::contexts::use_edc_connector_context;
use crate::models::{AssetItem, DatasetExtraFields};
use crate::services::get_dsp_endpoint;
use edc_connector_client::EdcConnectorApiVersion;
use edc_connector_client::types::Protocol;
use edc_connector_client::types::catalog::CatalogRequest;
use edc_connector_client::types::query::Query;
use edc_identity_hub_client::models::DidWeb;
use patternfly_yew::prelude::*;
use yew::prelude::*;
use yew::suspense::use_future_with;

#[derive(Clone, Debug, PartialEq, Properties)]
pub struct OfferPageProps {
  pub on_selected_offer: Callback<SelectedFederatedCatalogOffer>,
  pub on_new_asset: Callback<()>,
  pub on_new_policy: Callback<()>,
  pub on_new_contract: Callback<()>,
  pub participant_did: String,
  #[prop_or_default]
  pub display_search: bool,
  #[prop_or_default]
  pub search: Option<String>,
  #[prop_or_default]
  pub dcterm_types: Vec<String>,
}

#[component]
pub fn OfferPage(props: &OfferPageProps) -> Html {
  let refresh = use_state(|| 0usize);
  let offset = use_state(|| 0usize);
  let limit = use_state(|| 10usize);
  let path = web_sys::window().unwrap().location().pathname().unwrap();

  let on_offset = use_callback(
    (refresh.clone(), offset.setter()),
    |offset, (refresh, offset_setter)| {
      offset_setter.set(offset);
      refresh.set(**refresh + 1);
    },
  );

  let on_limit = use_callback(
    (refresh.clone(), limit.setter()),
    |limit, (refresh, limit_setter)| {
      limit_setter.set(limit);
      refresh.set(**refresh + 1);
    },
  );

  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())
  };

  let sub_title = if path.contains("participant") {
    String::from("Data offerings available from the selected participant: ")
      + &*props.participant_did.clone()
  } else {
    String::from(
      "Review and verify your active data offerings exactly as they are displayed to external participants searching the network.",
    )
  };

  html!(
    <Stack gutter=true>
      <StackItem>
        <Split gutter=true>
          <SplitItem fill=true>
            <Title level={Level::H3} size={Size::XXLarge}>{ "List Offers" }</Title>
            <p>{ sub_title }</p>
          </SplitItem>
        </Split>
      </StackItem>
      { search_component }
      <StackItem>
        <Suspense {fallback}>
          <OfferPageInner
            offset={*offset}
            limit={*limit}
            {on_offset}
            {on_limit}
            force_refresh={*refresh}
            on_selected_offer={props.on_selected_offer.clone()}
            on_new_asset={props.on_new_asset.clone()}
            on_new_policy={props.on_new_policy.clone()}
            on_new_contract={props.on_new_contract.clone()}
            participant_did={props.participant_did.clone()}
            {search}
            dcterm_types={props.dcterm_types.clone()}
          />
        </Suspense>
      </StackItem>
    </Stack>
  )
}

#[derive(Clone, Debug, PartialEq, Properties)]
pub struct OfferPageInnerProps {
  pub offset: usize,
  pub limit: usize,
  pub on_offset: Callback<usize>,
  pub on_limit: Callback<usize>,
  pub force_refresh: usize,
  pub on_selected_offer: Callback<SelectedFederatedCatalogOffer>,
  pub on_new_asset: Callback<()>,
  pub on_new_policy: Callback<()>,
  pub on_new_contract: Callback<()>,
  pub participant_did: String,
  #[prop_or_default]
  pub search: Option<String>,
  #[prop_or_default]
  pub dcterm_types: Vec<String>,
}

#[component]
pub fn OfferPageInner(props: &OfferPageInnerProps) -> HtmlResult {
  let edc_connector_context = use_edc_connector_context();

  let asset_items = use_future_with(
    (
      props.participant_did.clone(),
      edc_connector_context,
      props.limit,
      props.offset,
      props.search.clone(),
      props.dcterm_types.clone(),
      props.force_refresh,
    ),
    |parameters| async move {
      let (participant_did, edc_connector_context, limit, offset, search, dcterm_types, _) =
        (*parameters).clone();

      if let Some(did_web) = DidWeb::new(&participant_did)
        && let Some(dsp_endpoint) = get_dsp_endpoint(&did_web).await
        && let Some(client) = edc_connector_context.get_client()
      {
        let request = CatalogRequest::builder()
          .counter_party_address(dsp_endpoint.clone())
          .counter_party_id(&participant_did)
          .protocol(Protocol::default())
          .query_spec(
            Query::builder()
              .limit(limit as u32)
              .offset(offset as u32)
              .build(),
          )
          .build();

        if let Ok(response) = client
          .catalogue(EdcConnectorApiVersion::V4)
          .request::<DatasetExtraFields>(&request)
          .await
        {
          response
            .datasets()
            .iter()
            .map(|dataset| {
              let dataset_id = dataset.id().to_string();
              let asset_item = AssetItem::from(dataset);

              let selected_offer = SelectedFederatedCatalogOffer {
                originator: dsp_endpoint.clone(),
                provider_id: participant_did.clone(),
                dataset_id,
              };

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

  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 = { Action::new("Create my first asset", props.on_new_asset.reform(|_| ())) };

    let secondaries = vec![
      Action::new("Create my first policy", props.on_new_policy.reform(|_| ())),
      Action::new(
        "Create my first contract",
        props.on_new_contract.reform(|_| ()),
      ),
    ];

    Ok(html! {
      <EmptyState title="Empty state" icon={Icon::Cubes} {primary} {secondaries}>
        <div>
          <p>{ "There is no offer yet." }</p>
          <p>
            { "The first step consists of creating an asset, then a policy, and finally a contract definition." }
          </p>
          <p>{ "This results in an offer." }</p>
        </div>
      </EmptyState>
    })
  } else {
    Ok(html!(<ListAssetsGallery {asset_items} {onshow} />))
  }
}