Skip to main content

edc_web_components/pages/
new_contract_negotiation_page.rs

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