Skip to main content

edc_web_components/pages/
offer_page.rs

1use crate::components::{ListAssetsGallery, SelectedFederatedCatalogOffer};
2use crate::contexts::use_edc_connector_context;
3use crate::models::{AssetItem, DatasetExtraFields};
4use crate::services::DidResolver;
5use edc_connector_client::EdcConnectorApiVersion;
6use edc_connector_client::types::Protocol;
7use edc_connector_client::types::catalog::CatalogRequest;
8use edc_connector_client::types::query::Query;
9use edc_identity_hub_client::models::{DidWeb, IdentityServiceType};
10use patternfly_yew::prelude::*;
11use yew::prelude::*;
12use yew::suspense::use_future_with;
13
14#[derive(Clone, Debug, PartialEq, Properties)]
15pub struct OfferPageProps {
16  pub onselectedoffer: Callback<SelectedFederatedCatalogOffer>,
17  pub on_new_asset: Callback<()>,
18  pub on_new_policy: Callback<()>,
19  pub on_new_contract: Callback<()>,
20  pub participant_did: String,
21}
22
23#[component]
24pub fn OfferPage(props: &OfferPageProps) -> Html {
25  let refresh = use_state(|| 0usize);
26  let offset = use_state(|| 0usize);
27  let limit = use_state(|| 10usize);
28
29  let on_offset = use_callback(
30    (refresh.clone(), offset.setter()),
31    |offset, (refresh, offset_setter)| {
32      offset_setter.set(offset);
33      refresh.set(**refresh + 1);
34    },
35  );
36
37  let on_limit = use_callback(
38    (refresh.clone(), limit.setter()),
39    |limit, (refresh, limit_setter)| {
40      limit_setter.set(limit);
41      refresh.set(**refresh + 1);
42    },
43  );
44
45  let fallback = html! {
46    <Bullseye>
47      <Spinner size={SpinnerSize::Lg} />
48    </Bullseye>
49  };
50
51  html!(
52    <Stack gutter=true>
53      <StackItem>
54        <Split gutter=true>
55          <SplitItem fill=true>
56            <Title level={Level::H3} size={Size::XXLarge}>{ "List Offers" }</Title>
57          </SplitItem>
58        </Split>
59      </StackItem>
60      <StackItem>
61        <Suspense {fallback}>
62          <OfferPageInner
63            offset={*offset}
64            limit={*limit}
65            {on_offset}
66            {on_limit}
67            force_refresh={*refresh}
68            onselectedoffer={props.onselectedoffer.clone()}
69            on_new_asset={props.on_new_asset.clone()}
70            on_new_policy={props.on_new_policy.clone()}
71            on_new_contract={props.on_new_contract.clone()}
72            participant_did={props.participant_did.clone()}
73          />
74        </Suspense>
75      </StackItem>
76    </Stack>
77  )
78}
79
80#[derive(Clone, Debug, PartialEq, Properties)]
81pub struct OfferPageInnerProps {
82  pub offset: usize,
83  pub limit: usize,
84  pub on_offset: Callback<usize>,
85  pub on_limit: Callback<usize>,
86  pub force_refresh: usize,
87  pub onselectedoffer: Callback<SelectedFederatedCatalogOffer>,
88  pub on_new_asset: Callback<()>,
89  pub on_new_policy: Callback<()>,
90  pub on_new_contract: Callback<()>,
91  pub participant_did: String,
92}
93
94#[component]
95pub fn OfferPageInner(props: &OfferPageInnerProps) -> HtmlResult {
96  let edc_connector_context = use_edc_connector_context();
97
98  let asset_items = use_future_with(
99    (
100      props.participant_did.clone(),
101      edc_connector_context,
102      props.limit,
103      props.offset,
104      props.force_refresh,
105    ),
106    |parameters| async move {
107      let (participant_did, edc_connector_context, limit, offset, _) = (*parameters).clone();
108
109      if let Some(did_web) = DidWeb::new(&participant_did)
110        && let Ok(did_data) = DidResolver::new(reqwest::Client::new())
111          .resolve(did_web)
112          .await
113        && let Some(identity_service) = did_data
114          .get_identity_services(IdentityServiceType::DataService)
115          .first()
116        && let Some(dataspace_service_client) = identity_service.get_dataspace_service_client()
117        && let Some(dsp_endpoint) = dataspace_service_client.get_first_service_endpoint().await
118        && let Some(client) = edc_connector_context.get_client()
119      {
120        let request = CatalogRequest::builder()
121          .counter_party_address(dsp_endpoint.clone())
122          .counter_party_id(&participant_did)
123          .protocol(Protocol::default())
124          .query_spec(
125            Query::builder()
126              .limit(limit as u32)
127              .offset(offset as u32)
128              .build(),
129          )
130          .build();
131
132        if let Ok(response) = client
133          .catalogue(EdcConnectorApiVersion::V4)
134          .request::<DatasetExtraFields>(&request)
135          .await
136        {
137          response
138            .datasets()
139            .iter()
140            .map(|dataset| {
141              let dataset_id = dataset.id().to_string();
142              let asset_item = AssetItem::from(dataset);
143
144              let selected_offer = SelectedFederatedCatalogOffer {
145                originator: dsp_endpoint.clone(),
146                provider_id: participant_did.clone(),
147                dataset_id,
148              };
149
150              (asset_item, selected_offer)
151            })
152            .collect::<Vec<_>>()
153            .into_iter()
154            .unzip()
155        } else {
156          (Vec::new(), Vec::new())
157        }
158      } else {
159        (Vec::new(), Vec::new())
160      }
161    },
162  )?;
163
164  let (asset_items, selected_offers): (Vec<AssetItem>, Vec<SelectedFederatedCatalogOffer>) =
165    (*asset_items).clone();
166
167  let onshow = use_callback(
168    (props.onselectedoffer.clone(), selected_offers),
169    |dataset_id, (onselectedoffer, selected_offers)| {
170      if let Some(selected_offer) = selected_offers
171        .iter()
172        .find(|selected_offer| selected_offer.dataset_id == dataset_id)
173      {
174        onselectedoffer.emit(selected_offer.clone());
175      }
176    },
177  );
178
179  if asset_items.is_empty() {
180    let primary = { Action::new("Create my first asset", props.on_new_asset.reform(|_| ())) };
181
182    let secondaries = vec![
183      Action::new("Create my first policy", props.on_new_policy.reform(|_| ())),
184      Action::new(
185        "Create my first contract",
186        props.on_new_contract.reform(|_| ()),
187      ),
188    ];
189
190    Ok(html! {
191      <EmptyState title="Empty state" icon={Icon::Cubes} {primary} {secondaries}>
192        <div>
193          <p>{ "There is no offer yet." }</p>
194          <p>
195            { "The first step consists of creating an asset, then a policy, and finally a contract definition." }
196          </p>
197          <p>{ "This results in an offer." }</p>
198        </div>
199      </EmptyState>
200    })
201  } else {
202    Ok(html!(<ListAssetsGallery {asset_items} {onshow} />))
203  }
204}