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