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 = originator.clone();
146            let counter_party_id = provider_id.clone();
147
148            log::info!("counter_party_address: {}", counter_party_address);
149            let policy: Policy = policy.clone();
150
151            if let Some(id) = policy.id() {
152              let policy_builder = Policy::builder()
153                .assigner(provider_id)
154                .id(id)
155                .kind(PolicyKind::Offer)
156                .permissions(
157                  policy
158                    .permissions()
159                    .iter()
160                    .map(|permission| {
161                      Permission::builder()
162                        .action(edc_connector_client::types::policy::Action::Simple(
163                          "use".to_string(),
164                        ))
165                        .constraints(
166                          permission
167                            .constraints()
168                            .iter()
169                            .map(|constraint| match constraint {
170                              Constraint::Atomic(atomic_constraint) => {
171                                let left_operand = match &atomic_constraint.left_operand {
172                                  LeftOperand::Simple(simple) => {
173                                    LeftOperand::Simple(simple.to_string())
174                                  }
175                                  LeftOperand::Id { id } => LeftOperand::Simple(id.to_string()),
176                                };
177
178                                let operator = match &atomic_constraint.operator {
179                                  Operator::Simple(simple) => Operator::Simple(simple.to_string()),
180                                  Operator::Id { id } => Operator::Simple(
181                                    id.to_string().replace("http://www.w3.org/ns/odrl/2/", ""),
182                                  ),
183                                };
184
185                                Constraint::Atomic(AtomicConstraint {
186                                  left_operand,
187                                  operator,
188                                  right_operand: atomic_constraint.right_operand.clone(),
189                                })
190                              }
191                              Constraint::MultiplicityConstraint(multiplicity_constraint) => {
192                                Constraint::MultiplicityConstraint(multiplicity_constraint.clone())
193                              }
194                            })
195                            .collect(),
196                        )
197                        .build()
198                    })
199                    .collect(),
200                )
201                .prohibitions(policy.prohibitions().to_vec())
202                .obligations(policy.obligations().to_vec())
203                .target(Target::Simple(asset_id));
204
205              let policy = policy_builder.build();
206
207              let new_contract_request = ContractRequest::builder()
208                .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
209                .counter_party_address(&counter_party_address)
210                .counter_party_id(counter_party_id)
211                .policy(policy)
212                .build();
213
214              match edc_client
215                .contract_negotiations(EdcConnectorApiVersion::V4)
216                .initiate(&new_contract_request)
217                .await
218              {
219                Ok(contract_negotiation_id) => {
220                  log::info!(
221                    "Contract negotiation initiated: {}",
222                    contract_negotiation_id.id()
223                  );
224
225                  on_contract_negotiation_id.emit(contract_negotiation_id.id().to_string());
226                }
227                Err(error) => {
228                  log::error!("Error initiating contract negotiation: {}", error);
229                }
230              }
231            };
232          }
233
234          signing_setter.set(false);
235        })
236      }
237    },
238  );
239
240  if let Some(catalog_dataset) = catalog_dataset {
241    let provider_id = props.provider_id.clone();
242    let asset_id = catalog_dataset.0.clone();
243    let asset_name = catalog_dataset.1.clone();
244    let policies = catalog_dataset.2;
245    let disabled = *signing;
246
247    let offers = policies
248      .iter()
249      .filter_map(|policy: &Policy| {
250        if let Some(offer_id) = policy.id().and_then(|policy| policy.split(':').next()) {
251          BASE64_STANDARD
252            .decode(offer_id)
253            .ok()
254            .and_then(|offer_id| String::from_utf8(offer_id).ok())
255            .map(|offer_id| (policy, offer_id))
256        } else {
257          None
258        }
259      })
260      .map(|(policy, offer_id): (&Policy, String)| {
261        let onchange = onchange.clone();
262
263        let permissions = policy.permissions().iter().map(|permission| {
264          html! {
265            <ConstraintRenderer
266              action={permission.action().clone()}
267              constraints={permission.constraints().to_vec()}
268            />
269          }
270        });
271
272        let obligations = policy.obligations().iter().map(|obligation| {
273          html! {
274            <ConstraintRenderer
275              action={obligation.action().clone()}
276              constraints={obligation.constraints().to_vec()}
277            />
278          }
279        });
280
281        let prohibitions = policy.prohibitions().iter().map(|prohibition| {
282          html! {
283            <ConstraintRenderer
284              action={prohibition.action().clone()}
285              constraints={prohibition.constraints().to_vec()}
286            />
287          }
288        });
289
290        let policy = policy.clone();
291
292        html!(
293          <Card id="selectable-offer" selectable=true {disabled}>
294            <CardHeader
295              selectable_actions={yew::props!(CardSelectableActionsObjectProperties {
296                      action: CardSelectableActionsVariant::SingleSelect {
297                          onchange: Some(onchange.reform(move |_| policy.clone())),
298                      },
299                      base: yew::props!(CardSelectableActionsObjectBase {
300                          name: "selectable-offer"
301                      })
302                  })}
303            >
304              <CardTitle>{ offer_id.to_string() }</CardTitle>
305            </CardHeader>
306            <CardBody>
307              <DescriptionList>
308                <DescriptionGroup term="Permissions">{ for permissions }</DescriptionGroup>
309                <DescriptionGroup term="Obligations">{ for obligations }</DescriptionGroup>
310                <DescriptionGroup term="Prohibitions">{ for prohibitions }</DescriptionGroup>
311              </DescriptionList>
312            </CardBody>
313          </Card>
314        )
315      });
316
317    let disabled = selected_offer.is_none() || *signing;
318
319    Ok(html!(
320      <>
321        <DescriptionList>
322          <DescriptionGroup term="Provider ID">{ provider_id }</DescriptionGroup>
323          <DescriptionGroup term="Asset ID">{ asset_id }</DescriptionGroup>
324          <DescriptionGroup term="Asset Name">{ asset_name }</DescriptionGroup>
325          <DescriptionGroup term="Offer IDs">
326            <Gallery gutter=true>{ for offers }</Gallery>
327          </DescriptionGroup>
328        </DescriptionList>
329        <Split gutter=true>
330          <SplitItem fill=true />
331          <SplitItem>
332            <Button variant={ButtonVariant::Primary} icon={Icon::Check} {disabled} {onclick}>
333              { "Sign" }
334            </Button>
335          </SplitItem>
336        </Split>
337      </>
338    ))
339  } else {
340    Ok(html!({ "The offer is not available." }))
341  }
342}