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