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                  (
209                    "policy:isan".to_string(),
210                    serde_json::Value::String("123456".to_string()),
211                  ),
212                  (
213                    "policy:fpd".to_string(),
214                    serde_json::Value::String("25".to_string()),
215                  ),
216                ]));
217
218              let policy = policy_builder.build();
219
220              let new_contract_request = ContractRequest::builder()
221                .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
222                .counter_party_address(&counter_party_address)
223                .counter_party_id(counter_party_id)
224                .policy(policy)
225                .build();
226
227              match edc_client
228                .contract_negotiations(EdcConnectorApiVersion::V4)
229                .initiate(&new_contract_request)
230                .await
231              {
232                Ok(contract_negotiation_id) => {
233                  log::info!(
234                    "Contract negotiation initiated: {}",
235                    contract_negotiation_id.id()
236                  );
237
238                  on_contract_negotiation_id.emit(contract_negotiation_id.id().to_string());
239                }
240                Err(error) => {
241                  log::error!("Error initiating contract negotiation: {}", error);
242                }
243              }
244            };
245          }
246
247          signing_setter.set(false);
248        })
249      }
250    },
251  );
252
253  if let Some(catalog_dataset) = catalog_dataset {
254    let provider_id = props.provider_id.clone();
255    let asset_id = catalog_dataset.0.clone();
256    let asset_name = catalog_dataset.1.clone();
257    let policies = catalog_dataset.2;
258    let disabled = *signing;
259
260    let offers = policies
261      .iter()
262      .filter_map(|policy: &Policy| {
263        if let Some(offer_id) = policy.id().and_then(|policy| policy.split(':').next()) {
264          BASE64_STANDARD
265            .decode(offer_id)
266            .ok()
267            .and_then(|offer_id| String::from_utf8(offer_id).ok())
268            .map(|offer_id| (policy, offer_id))
269        } else {
270          None
271        }
272      })
273      .map(|(policy, offer_id): (&Policy, String)| {
274        let onchange = onchange.clone();
275
276        let permissions = policy.permissions().iter().map(|permission| {
277          html! {
278            <ConstraintRenderer
279              action={permission.action().clone()}
280              constraints={permission.constraints().to_vec()}
281            />
282          }
283        });
284
285        let obligations = policy.obligations().iter().map(|obligation| {
286          html! {
287            <ConstraintRenderer
288              action={obligation.action().clone()}
289              constraints={obligation.constraints().to_vec()}
290            />
291          }
292        });
293
294        let prohibitions = policy.prohibitions().iter().map(|prohibition| {
295          html! {
296            <ConstraintRenderer
297              action={prohibition.action().clone()}
298              constraints={prohibition.constraints().to_vec()}
299            />
300          }
301        });
302
303        let policy = policy.clone();
304
305        html!(
306          <Card id="selectable-offer" selectable=true {disabled}>
307            <CardHeader
308              selectable_actions={yew::props!(CardSelectableActionsObjectProperties {
309                      action: CardSelectableActionsVariant::SingleSelect {
310                          onchange: Some(onchange.reform(move |_| policy.clone())),
311                      },
312                      base: yew::props!(CardSelectableActionsObjectBase {
313                          name: "selectable-offer"
314                      })
315                  })}
316            >
317              <CardTitle>{ offer_id.to_string() }</CardTitle>
318            </CardHeader>
319            <CardBody>
320              <DescriptionList>
321                <DescriptionGroup term="Permissions">{ for permissions }</DescriptionGroup>
322                <DescriptionGroup term="Obligations">{ for obligations }</DescriptionGroup>
323                <DescriptionGroup term="Prohibitions">{ for prohibitions }</DescriptionGroup>
324              </DescriptionList>
325            </CardBody>
326          </Card>
327        )
328      });
329
330    let disabled = selected_offer.is_none() || *signing;
331
332    Ok(html!(
333      <>
334        <DescriptionList>
335          <DescriptionGroup term="Provider ID">{ provider_id }</DescriptionGroup>
336          <DescriptionGroup term="Asset ID">{ asset_id }</DescriptionGroup>
337          <DescriptionGroup term="Asset Name">{ asset_name }</DescriptionGroup>
338          <DescriptionGroup term="Offer IDs">
339            <Gallery gutter=true>{ for offers }</Gallery>
340          </DescriptionGroup>
341        </DescriptionList>
342        <Split gutter=true>
343          <SplitItem fill=true />
344          <SplitItem>
345            <Button variant={ButtonVariant::Primary} icon={Icon::Check} {disabled} {onclick}>
346              { "Sign" }
347            </Button>
348          </SplitItem>
349        </Split>
350      </>
351    ))
352  } else {
353    Ok(html!({ "The offer is not available." }))
354  }
355}