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