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 base64::prelude::*;
4use edc_connector_client::EdcConnectorApiVersion;
5use edc_connector_client::types::Protocol;
6use edc_connector_client::types::contract_negotiation::ContractRequest;
7use edc_connector_client::types::policy::{
8  AtomicConstraint, Constraint, LeftOperand, Operator, Permission, Policy, PolicyKind, Target,
9};
10use edc_federated_catalog_client::{FederatedCatalogClient, FederatedCatalogClientVersion};
11use patternfly_yew::prelude::*;
12use std::fmt::Debug;
13use yew::platform::spawn_local;
14use yew::prelude::*;
15use yew::suspense::use_future_with;
16use yew_oauth2::hook::use_latest_access_token;
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  let latest_access_token_context = use_latest_access_token().unwrap();
63
64  let federated_catalog_dataset = use_future_with(
65    (latest_access_token_context, props.dataset_id.clone()),
66    |parameters| async move {
67      let (latest_access_token_context, dataset_id) = (*parameters).clone();
68
69      let server_url = web_sys::window().unwrap().location().origin().unwrap();
70      let federated_catalog_client = FederatedCatalogClient::new(
71        reqwest::Client::new(),
72        format!("{server_url}/federated-catalog"),
73        latest_access_token_context.access_token(),
74        FederatedCatalogClientVersion::V4,
75      );
76
77      federated_catalog_client
78        .get_offer_by_dataset_id(dataset_id.clone())
79        .await
80        .unwrap_or_default()
81        .and_then(|federated_catalog_offer| {
82          federated_catalog_offer
83            .dataset
84            .into_iter()
85            .find(|dataset| dataset.id == dataset_id)
86        })
87    },
88  )?;
89
90  let federated_catalog_dataset = (*federated_catalog_dataset).clone();
91
92  let selected_offer = use_state_eq(|| None);
93  let signing = use_state(|| false);
94
95  let onchange = use_callback(selected_offer.clone(), |policy: Policy, selected_offer| {
96    selected_offer.set(Some(policy))
97  });
98
99  let onclick = use_callback(
100    (
101      edc_connector_context.clone(),
102      federated_catalog_dataset.clone(),
103      selected_offer.clone(),
104      props.originator.clone(),
105      props.provider_id.clone(),
106      signing.setter(),
107      props.on_contract_negotiation_id.clone(),
108    ),
109    |_,
110     (
111      edc_connector_context,
112      federated_catalog_dataset,
113      selected_offer,
114      originator,
115      provider_id,
116      signing_setter,
117      on_contract_negotiation_id,
118    )| {
119      let edc_connector_context = edc_connector_context.clone();
120      if let Some(policy) = (**selected_offer).clone()
121        && let Some(federated_catalog_dataset) = federated_catalog_dataset
122      {
123        let originator = originator.clone();
124        let provider_id = provider_id.clone();
125        let asset_id = federated_catalog_dataset.id.clone();
126
127        signing_setter.set(true);
128        let signing_setter = signing_setter.clone();
129        let on_contract_negotiation_id = on_contract_negotiation_id.clone();
130
131        spawn_local(async move {
132          if let Some(edc_client) = edc_connector_context.get_client() {
133            //let counter_party_address = federated_catalog_offer.service.endpoint_url.clone();
134            let counter_party_address = originator.clone();
135            let counter_party_id = provider_id.clone();
136
137            log::warn!("counter_party_address: {}", counter_party_address);
138            let policy: Policy = policy.clone();
139
140            if let Some(id) = policy.id() {
141              let policy_builder = Policy::builder()
142                .assigner(provider_id)
143                .id(id)
144                .kind(PolicyKind::Offer)
145                .permissions(
146                  policy
147                    .permissions()
148                    .iter()
149                    .map(|permission| {
150                      Permission::builder()
151                        .action(edc_connector_client::types::policy::Action::Simple(
152                          "use".to_string(),
153                        ))
154                        .constraints(
155                          permission
156                            .constraints()
157                            .iter()
158                            .map(|constraint| match constraint {
159                              Constraint::Atomic(atomic_constraint) => {
160                                let left_operand = match &atomic_constraint.left_operand {
161                                  LeftOperand::Simple(simple) => {
162                                    LeftOperand::Simple(simple.to_string())
163                                  }
164                                  LeftOperand::Id { id } => LeftOperand::Simple(id.to_string()),
165                                };
166
167                                let operator = match &atomic_constraint.operator {
168                                  Operator::Simple(simple) => Operator::Simple(simple.to_string()),
169                                  Operator::Id { id } => Operator::Simple(
170                                    id.to_string().replace("http://www.w3.org/ns/odrl/2/", ""),
171                                  ),
172                                };
173
174                                Constraint::Atomic(AtomicConstraint {
175                                  left_operand,
176                                  operator,
177                                  right_operand: atomic_constraint.right_operand.clone(),
178                                })
179                              }
180                              Constraint::MultiplicityConstraint(multiplicity_constraint) => {
181                                Constraint::MultiplicityConstraint(multiplicity_constraint.clone())
182                              }
183                            })
184                            .collect(),
185                        )
186                        .build()
187                    })
188                    .collect(),
189                )
190                .prohibitions(policy.prohibitions().to_vec())
191                .obligations(policy.obligations().to_vec())
192                .target(Target::Simple(asset_id));
193
194              let policy = policy_builder.build();
195
196              let new_contract_request = ContractRequest::builder()
197                .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
198                .counter_party_address(&counter_party_address)
199                .counter_party_id(counter_party_id)
200                .policy(policy)
201                .build();
202
203              match edc_client
204                .contract_negotiations(EdcConnectorApiVersion::V4)
205                .initiate(&new_contract_request)
206                .await
207              {
208                Ok(contract_negotiation_id) => {
209                  log::info!(
210                    "Contract negotiation initiated: {}",
211                    contract_negotiation_id.id()
212                  );
213
214                  on_contract_negotiation_id.emit(contract_negotiation_id.id().to_string());
215                }
216                Err(error) => {
217                  log::error!("Error initiating contract negotiation: {}", error);
218                }
219              }
220            };
221          }
222
223          signing_setter.set(false);
224        })
225      }
226    },
227  );
228
229  if let Some(federated_catalog_dataset) = federated_catalog_dataset {
230    let provider_id = props.provider_id.clone();
231    let asset_id = federated_catalog_dataset.id.clone();
232    let asset_name = federated_catalog_dataset.name.clone();
233    let policies = federated_catalog_dataset.has_policy;
234    let disabled = *signing;
235
236    let offers = policies
237      .iter()
238      .filter_map(|policy: &Policy| {
239        if let Some(offer_id) = policy.id().and_then(|policy| policy.split(':').next()) {
240          BASE64_STANDARD
241            .decode(offer_id)
242            .ok()
243            .and_then(|offer_id| String::from_utf8(offer_id).ok())
244            .map(|offer_id| (policy, offer_id))
245        } else {
246          None
247        }
248      })
249      .map(|(policy, offer_id): (&Policy, String)| {
250        let onchange = onchange.clone();
251
252        let permissions = policy.permissions().iter().map(|permission| {
253          html! {
254            <ConstraintRenderer
255              action={permission.action().clone()}
256              constraints={permission.constraints().to_vec()}
257            />
258          }
259        });
260
261        let obligations = policy.obligations().iter().map(|obligation| {
262          html! {
263            <ConstraintRenderer
264              action={obligation.action().clone()}
265              constraints={obligation.constraints().to_vec()}
266            />
267          }
268        });
269
270        let prohibitions = policy.prohibitions().iter().map(|prohibition| {
271          html! {
272            <ConstraintRenderer
273              action={prohibition.action().clone()}
274              constraints={prohibition.constraints().to_vec()}
275            />
276          }
277        });
278
279        let policy = policy.clone();
280
281        html!(
282          <Card id="selectable-offer" selectable=true {disabled}>
283            <CardHeader
284              selectable_actions={yew::props!(CardSelectableActionsObjectProperties {
285                      action: CardSelectableActionsVariant::SingleSelect {
286                          onchange: Some(onchange.reform(move |_| policy.clone())),
287                      },
288                      base: yew::props!(CardSelectableActionsObjectBase {
289                          name: "selectable-offer"
290                      })
291                  })}
292            >
293              <CardTitle>{ offer_id.to_string() }</CardTitle>
294            </CardHeader>
295            <CardBody>
296              <DescriptionList>
297                <DescriptionGroup term="Permissions">{ for permissions }</DescriptionGroup>
298                <DescriptionGroup term="Obligations">{ for obligations }</DescriptionGroup>
299                <DescriptionGroup term="Prohibitions">{ for prohibitions }</DescriptionGroup>
300              </DescriptionList>
301            </CardBody>
302          </Card>
303        )
304      });
305
306    let disabled = selected_offer.is_none() || *signing;
307
308    Ok(html!(
309      <>
310        <DescriptionList>
311          <DescriptionGroup term="Provider ID">{ provider_id }</DescriptionGroup>
312          <DescriptionGroup term="Asset ID">{ asset_id }</DescriptionGroup>
313          <DescriptionGroup term="Asset Name">{ asset_name }</DescriptionGroup>
314          <DescriptionGroup term="Offer IDs">
315            <Gallery>{ for offers }</Gallery>
316          </DescriptionGroup>
317        </DescriptionList>
318        <Split gutter=true>
319          <SplitItem fill=true />
320          <SplitItem>
321            <Button variant={ButtonVariant::Primary} icon={Icon::Check} {disabled} {onclick}>
322              { "Sign" }
323            </Button>
324          </SplitItem>
325        </Split>
326      </>
327    ))
328  } else {
329    Ok(html!({ "The offer is not available." }))
330  }
331}