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