edc_web_components/pages/
offer_page.rs1use 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}