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