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::get_dsp_endpoint;
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;
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  let path = web_sys::window().unwrap().location().pathname().unwrap();
29
30  let on_offset = use_callback(
31    (refresh.clone(), offset.setter()),
32    |offset, (refresh, offset_setter)| {
33      offset_setter.set(offset);
34      refresh.set(**refresh + 1);
35    },
36  );
37
38  let on_limit = use_callback(
39    (refresh.clone(), limit.setter()),
40    |limit, (refresh, limit_setter)| {
41      limit_setter.set(limit);
42      refresh.set(**refresh + 1);
43    },
44  );
45
46  let fallback = html! {
47    <Bullseye>
48      <Spinner size={SpinnerSize::Lg} />
49    </Bullseye>
50  };
51
52  let sub_title = if path.contains("participant") {
53    String::from("Data offerings available from the selected participant: ")
54      + &*props.participant_did.clone()
55  } else {
56    String::from(
57      "Review and verify your active data offerings exactly as they are displayed to external participants searching the network.",
58    )
59  };
60
61  html!(
62    <Stack gutter=true>
63      <StackItem>
64        <Split gutter=true>
65          <SplitItem fill=true>
66            <Title level={Level::H3} size={Size::XXLarge}>{ "List Offers" }</Title>
67            <p>{ sub_title }</p>
68          </SplitItem>
69        </Split>
70      </StackItem>
71      <StackItem>
72        <Suspense {fallback}>
73          <OfferPageInner
74            offset={*offset}
75            limit={*limit}
76            {on_offset}
77            {on_limit}
78            force_refresh={*refresh}
79            onselectedoffer={props.onselectedoffer.clone()}
80            on_new_asset={props.on_new_asset.clone()}
81            on_new_policy={props.on_new_policy.clone()}
82            on_new_contract={props.on_new_contract.clone()}
83            participant_did={props.participant_did.clone()}
84          />
85        </Suspense>
86      </StackItem>
87    </Stack>
88  )
89}
90
91#[derive(Clone, Debug, PartialEq, Properties)]
92pub struct OfferPageInnerProps {
93  pub offset: usize,
94  pub limit: usize,
95  pub on_offset: Callback<usize>,
96  pub on_limit: Callback<usize>,
97  pub force_refresh: usize,
98  pub onselectedoffer: Callback<SelectedFederatedCatalogOffer>,
99  pub on_new_asset: Callback<()>,
100  pub on_new_policy: Callback<()>,
101  pub on_new_contract: Callback<()>,
102  pub participant_did: String,
103}
104
105#[component]
106pub fn OfferPageInner(props: &OfferPageInnerProps) -> HtmlResult {
107  let edc_connector_context = use_edc_connector_context();
108
109  let asset_items = use_future_with(
110    (
111      props.participant_did.clone(),
112      edc_connector_context,
113      props.limit,
114      props.offset,
115      props.force_refresh,
116    ),
117    |parameters| async move {
118      let (participant_did, edc_connector_context, limit, offset, _) = (*parameters).clone();
119
120      if let Some(did_web) = DidWeb::new(&participant_did)
121        && let Some(dsp_endpoint) = get_dsp_endpoint(&did_web).await
122        && let Some(client) = edc_connector_context.get_client()
123      {
124        let request = CatalogRequest::builder()
125          .counter_party_address(dsp_endpoint.clone())
126          .counter_party_id(&participant_did)
127          .protocol(Protocol::default())
128          .query_spec(
129            Query::builder()
130              .limit(limit as u32)
131              .offset(offset as u32)
132              .build(),
133          )
134          .build();
135
136        if let Ok(response) = client
137          .catalogue(EdcConnectorApiVersion::V4)
138          .request::<DatasetExtraFields>(&request)
139          .await
140        {
141          response
142            .datasets()
143            .iter()
144            .map(|dataset| {
145              let dataset_id = dataset.id().to_string();
146              let asset_item = AssetItem::from(dataset);
147
148              let selected_offer = SelectedFederatedCatalogOffer {
149                originator: dsp_endpoint.clone(),
150                provider_id: participant_did.clone(),
151                dataset_id,
152              };
153
154              (asset_item, selected_offer)
155            })
156            .collect::<Vec<_>>()
157            .into_iter()
158            .unzip()
159        } else {
160          (Vec::new(), Vec::new())
161        }
162      } else {
163        (Vec::new(), Vec::new())
164      }
165    },
166  )?;
167
168  let (asset_items, selected_offers): (Vec<AssetItem>, Vec<SelectedFederatedCatalogOffer>) =
169    (*asset_items).clone();
170
171  let onshow = use_callback(
172    (props.onselectedoffer.clone(), selected_offers),
173    |dataset_id, (onselectedoffer, selected_offers)| {
174      if let Some(selected_offer) = selected_offers
175        .iter()
176        .find(|selected_offer| selected_offer.dataset_id == dataset_id)
177      {
178        onselectedoffer.emit(selected_offer.clone());
179      }
180    },
181  );
182
183  if asset_items.is_empty() {
184    let primary = { Action::new("Create my first asset", props.on_new_asset.reform(|_| ())) };
185
186    let secondaries = vec![
187      Action::new("Create my first policy", props.on_new_policy.reform(|_| ())),
188      Action::new(
189        "Create my first contract",
190        props.on_new_contract.reform(|_| ()),
191      ),
192    ];
193
194    Ok(html! {
195      <EmptyState title="Empty state" icon={Icon::Cubes} {primary} {secondaries}>
196        <div>
197          <p>{ "There is no offer yet." }</p>
198          <p>
199            { "The first step consists of creating an asset, then a policy, and finally a contract definition." }
200          </p>
201          <p>{ "This results in an offer." }</p>
202        </div>
203      </EmptyState>
204    })
205  } else {
206    Ok(html!(<ListAssetsGallery {asset_items} {onshow} />))
207  }
208}