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