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