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 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}