Skip to main content

edc_web_components/pages/
new_contract_negotiation_page.rs

1use crate::contexts::use_edc_connector_context;
2use base64::prelude::*;
3use edc_connector_client::EdcConnectorApiVersion;
4use edc_connector_client::types::catalog::CatalogRequest;
5use edc_connector_client::types::contract_negotiation::ContractRequest;
6use edc_connector_client::types::policy::{
7  AtomicConstraint, Constraint, LeftOperand, Operator, Permission, Policy, PolicyKind, Target,
8};
9use edc_connector_client::types::query::Query;
10use edc_connector_client::types::{ExtraTokenFields, Protocol};
11use edc_federated_catalog_client::{FederatedCatalogClient, FederatedCatalogClientVersion};
12use patternfly_yew::prelude::*;
13use serde::{Deserialize, Serialize};
14use std::fmt::Debug;
15use yew::platform::spawn_local;
16use yew::prelude::*;
17use yew::suspense::use_future_with;
18use yew_oauth2::hook::use_latest_access_token;
19
20#[derive(Clone, Debug, PartialEq, Properties)]
21pub struct NewContractNegotiationPageProps {
22  pub originator: String,
23  pub provider_id: String,
24  pub dataset_id: String,
25}
26
27#[component]
28pub fn NewContractNegotiationPage(props: &NewContractNegotiationPageProps) -> Html {
29  html!(
30    <Stack gutter=true>
31      <StackItem>
32        <Split gutter=true>
33          <SplitItem fill=true>
34            <Title level={Level::H3} size={Size::XXLarge}>{ "New Contract Negotiation" }</Title>
35          </SplitItem>
36        </Split>
37      </StackItem>
38      <StackItem>
39        <Card>
40          <CardBody>
41            <Suspense>
42              <NewContractNegotiationPageInner
43                originator={props.originator.clone()}
44                provider_id={props.provider_id.clone()}
45                dataset_id={props.dataset_id.clone()}
46              />
47            </Suspense>
48          </CardBody>
49        </Card>
50      </StackItem>
51    </Stack>
52  )
53}
54
55#[derive(Clone, Debug, PartialEq, Properties)]
56pub struct NewContractNegotiationPageInnerProps {
57  pub originator: String,
58  pub provider_id: String,
59  pub dataset_id: String,
60}
61
62#[component]
63pub fn NewContractNegotiationPageInner(props: &NewContractNegotiationPageInnerProps) -> HtmlResult {
64  let edc_connector_context = use_edc_connector_context();
65  let latest_access_token_context = use_latest_access_token().unwrap();
66
67  let federated_catalog_dataset = use_future_with(
68    (
69      latest_access_token_context,
70      props.originator.clone(),
71      props.provider_id.clone(),
72      props.dataset_id.clone(),
73      edc_connector_context.clone(),
74    ),
75    |parameters| async move {
76      let (latest_access_token_context, originator, provider_id, dataset_id, edc_connector_context) =
77        (*parameters).clone();
78
79      if let Some(edc_client) = edc_connector_context.get_client() {
80        // let originator = originator.clone();
81        // let provider_id = provider_id.clone();
82
83        {
84          let originator =
85            "https://controlplane.participant-li1.demo.luminvent.com/dsp/2025-1".to_string();
86          let provider_id =
87            "did:web:participant-li1.demo.luminvent.com:participant-li1".to_string();
88
89          let edc_client = edc_client.clone();
90          spawn_local(async move {
91            let query = Query::builder()
92              // .limit(*limit as u32)
93              // .offset(*offset as u32)
94              .build();
95
96            let catalogue_request = CatalogRequest::builder()
97              .counter_party_address(originator.clone())
98              .counter_party_id(provider_id.clone())
99              .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
100              .query_spec(query)
101              .build();
102
103            #[derive(Debug, Serialize, Deserialize)]
104            struct ExtraDatasetFields {}
105            impl ExtraTokenFields for ExtraDatasetFields {}
106
107            if let Ok(catalog) = edc_client
108              .catalogue(EdcConnectorApiVersion::V4)
109              .request::<ExtraDatasetFields>(&catalogue_request)
110              .await
111            {
112              log::warn!("Catalog: {:?}", catalog);
113            }
114          });
115        }
116
117        let originator =
118          "https://controlplane.participant-li2.demo.luminvent.com/dsp/2025-1".to_string();
119        let provider_id = "did:web:participant-li2.demo.luminvent.com:participant-li2".to_string();
120
121        spawn_local(async move {
122          let query = Query::builder()
123            // .limit(*limit as u32)
124            // .offset(*offset as u32)
125            .build();
126
127          let catalogue_request = CatalogRequest::builder()
128            .counter_party_address(originator.clone())
129            .counter_party_id(provider_id.clone())
130            .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
131            .query_spec(query)
132            .build();
133
134          #[derive(Debug, Serialize, Deserialize)]
135          struct ExtraDatasetFields {}
136          impl ExtraTokenFields for ExtraDatasetFields {}
137
138          if let Ok(catalog) = edc_client
139            .catalogue(EdcConnectorApiVersion::V4)
140            .request::<ExtraDatasetFields>(&catalogue_request)
141            .await
142          {
143            log::warn!("Catalog: {:?}", catalog);
144          }
145        })
146      }
147
148      let server_url = web_sys::window().unwrap().location().origin().unwrap();
149      let federated_catalog_client = FederatedCatalogClient::new(
150        reqwest::Client::new(),
151        format!("{server_url}/federated-catalog"),
152        latest_access_token_context.access_token(),
153        FederatedCatalogClientVersion::V4,
154      );
155
156      let federated_catalog_offers = federated_catalog_client
157        .list_offers()
158        .await
159        .unwrap_or_default();
160
161      federated_catalog_offers
162        .into_iter()
163        .find_map(|federated_catalog_offer| {
164          if federated_catalog_offer.originator != originator
165            && federated_catalog_offer.participant_id.id != provider_id
166          {
167            None
168          } else {
169            federated_catalog_offer
170              .dataset
171              .into_iter()
172              .find(|dataset| dataset.id == dataset_id)
173          }
174        })
175    },
176  )?;
177
178  let federated_catalog_dataset = (*federated_catalog_dataset).clone();
179
180  let selected_offer = use_state_eq(|| None);
181
182  let onchange = use_callback(selected_offer.clone(), |policy: Policy, selected_offer| {
183    selected_offer.set(Some(policy))
184  });
185
186  let onclick = use_callback(
187    (
188      edc_connector_context.clone(),
189      federated_catalog_dataset.clone(),
190      selected_offer.clone(),
191      props.originator.clone(),
192      props.provider_id.clone(),
193    ),
194    |_,
195     (
196      edc_connector_context,
197      federated_catalog_dataset,
198      selected_offer,
199      originator,
200      provider_id,
201    )| {
202      let edc_connector_context = edc_connector_context.clone();
203      if let Some(policy) = (**selected_offer).clone()
204        && let Some(federated_catalog_dataset) = federated_catalog_dataset
205      {
206        let originator = originator.clone();
207        let provider_id = provider_id.clone();
208        let asset_id = federated_catalog_dataset.id.clone();
209
210        spawn_local(async move {
211          if let Some(edc_client) = edc_connector_context.get_client() {
212            //let counter_party_address = federated_catalog_offer.service.endpoint_url.clone();
213            let counter_party_address = originator.clone();
214            let counter_party_id = provider_id.clone();
215
216            log::warn!("counter_party_address: {}", counter_party_address);
217            let policy: Policy = policy.clone();
218
219            if let Some(id) = policy.id() {
220              let policy_builder = Policy::builder()
221                .assigner(provider_id)
222                .id(id)
223                .kind(PolicyKind::Offer)
224                .permissions(
225                  policy
226                    .permissions()
227                    .iter()
228                    .map(|permission| {
229                      Permission::builder()
230                        .action(edc_connector_client::types::policy::Action::Simple(
231                          "use".to_string(),
232                        ))
233                        .constraints(
234                          permission
235                            .constraints()
236                            .iter()
237                            .map(|constraint| match constraint {
238                              Constraint::Atomic(atomic_constraint) => {
239                                let left_operand = match &atomic_constraint.left_operand {
240                                  LeftOperand::Simple(simple) => {
241                                    LeftOperand::Simple(simple.to_string())
242                                  }
243                                  LeftOperand::Id { id } => LeftOperand::Simple(id.to_string()),
244                                };
245
246                                let operator = match &atomic_constraint.operator {
247                                  Operator::Simple(simple) => Operator::Simple(simple.to_string()),
248                                  Operator::Id { id } => Operator::Simple(
249                                    id.to_string().replace("http://www.w3.org/ns/odrl/2/", ""),
250                                  ),
251                                };
252
253                                Constraint::Atomic(AtomicConstraint {
254                                  left_operand,
255                                  operator,
256                                  right_operand: atomic_constraint.right_operand.clone(),
257                                })
258                              }
259                              Constraint::MultiplicityConstraint(multiplicity_constraint) => {
260                                Constraint::MultiplicityConstraint(multiplicity_constraint.clone())
261                              }
262                            })
263                            .collect(),
264                        )
265                        .build()
266                    })
267                    .collect(),
268                )
269                .prohibitions(policy.prohibitions().to_vec())
270                .obligations(policy.obligations().to_vec())
271                .target(Target::Simple(asset_id));
272
273              let policy = policy_builder.build();
274
275              let new_contract_request = ContractRequest::builder()
276                .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
277                .counter_party_address(&counter_party_address)
278                .counter_party_id(counter_party_id)
279                .policy(policy)
280                .build();
281
282              if let Ok(contract_negotiation_id) = edc_client
283                .contract_negotiations(EdcConnectorApiVersion::V4)
284                .initiate(&new_contract_request)
285                .await
286              {
287                log::warn!(
288                  "Contract negotiation initiated: {}",
289                  contract_negotiation_id.id()
290                );
291              }
292            };
293          }
294        })
295      }
296    },
297  );
298
299  if let Some(federated_catalog_dataset) = federated_catalog_dataset {
300    let provider_id = props.provider_id.clone();
301    let asset_id = federated_catalog_dataset.id.clone();
302    let asset_name = federated_catalog_dataset.name.clone();
303    let policies = federated_catalog_dataset.has_policy;
304    let selected_offer = selected_offer.clone();
305
306    let offers = policies
307      .iter()
308      .filter_map(|policy| {
309        if let Some(offer_id) = policy.id().and_then(|policy| policy.split(':').next()) {
310          BASE64_STANDARD
311            .decode(offer_id)
312            .ok()
313            .and_then(|offer_id| String::from_utf8(offer_id).ok())
314            .map(|offer_id| (policy, offer_id))
315        } else {
316          None
317        }
318      })
319      .map(|(policy, offer_id)| {
320        let onchange = onchange.clone();
321        let policy = policy.clone();
322
323        html!(
324          <Radio
325            name="offer-id"
326            checked={(*selected_offer).as_ref() == Some(&policy)}
327            onchange={onchange.reform(move |_| policy.clone())}
328          >
329            <span>{ offer_id.to_string() }</span>
330          </Radio>
331        )
332      });
333
334    let disabled = selected_offer.is_none();
335
336    Ok(html!(
337      <>
338        <DescriptionList>
339          <DescriptionGroup term="Provider ID">{ provider_id }</DescriptionGroup>
340          <DescriptionGroup term="Asset ID">{ asset_id }</DescriptionGroup>
341          <DescriptionGroup term="Asset Name">{ asset_name }</DescriptionGroup>
342          <DescriptionGroup term="Offer IDs">{ for offers }</DescriptionGroup>
343        </DescriptionList>
344        <Split gutter=true>
345          <SplitItem fill=true />
346          <SplitItem>
347            <Button variant={ButtonVariant::Primary} icon={Icon::Check} {disabled} {onclick}>
348              { "Sign" }
349            </Button>
350          </SplitItem>
351        </Split>
352      </>
353    ))
354  } else {
355    Ok(html!({ "The offer is not available." }))
356  }
357}