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