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 {
#[prop_or("List Offers".to_string())]
pub title: String,
#[prop_or(Some("Review and verify your active data offerings exactly as they are displayed to external participants searching the network.".to_string()))]
pub tag_line: Option<String>,
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 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 tag_line = props
.tag_line
.as_ref()
.map(|tag_line| html!(<p>{ tag_line }</p>))
.unwrap_or_default();
html!(
<Stack gutter=true>
<StackItem>
<Title level={Level::H3} size={Size::XXLarge}>{ &props.title }</Title>
{ tag_line }
</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} />))
}
}