Skip to main content

edc_web_components/pages/
new_contract_negotiation_page.rs

1use crate::components::{DatasetCard, PolicyCard};
2use crate::contexts::use_edc_connector_context;
3use crate::models::{AssetItem, DataspaceDataset, PolicyDefinitionItem};
4use base64::prelude::*;
5use edc_connector_client::EdcConnectorApiVersion;
6use edc_connector_client::types::Protocol;
7use edc_connector_client::types::catalog::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  #[prop_or("Negotiate a Contract".to_string())]
21  pub title: String,
22  #[prop_or(Some("Start a new negotiation on the selected data offer and proposing terms to establish a binding sharing contract.".to_string()))]
23  pub tag_line: Option<String>,
24  pub originator: String,
25  pub provider_id: String,
26  pub dataset_id: String,
27  pub on_contract_negotiation_id: Callback<String>,
28}
29
30#[component]
31pub fn NewContractNegotiationPage(props: &NewContractNegotiationPageProps) -> Html {
32  let tag_line = props
33    .tag_line
34    .as_ref()
35    .map(|tag_line| html!(<p>{ tag_line }</p>))
36    .unwrap_or_default();
37
38  html!(
39    <Stack gutter=true>
40      <StackItem>
41        <Title level={Level::H3} size={Size::XXLarge}>{ &props.title }</Title>
42        { tag_line }
43      </StackItem>
44      <StackItem>
45        <Suspense>
46          <NewContractNegotiationPageInner
47            originator={props.originator.clone()}
48            provider_id={props.provider_id.clone()}
49            dataset_id={props.dataset_id.clone()}
50            on_contract_negotiation_id={props.on_contract_negotiation_id.clone()}
51          />
52        </Suspense>
53      </StackItem>
54    </Stack>
55  )
56}
57
58#[derive(Clone, Debug, PartialEq, Properties)]
59pub struct NewContractNegotiationPageInnerProps {
60  pub originator: String,
61  pub provider_id: String,
62  pub dataset_id: String,
63  pub on_contract_negotiation_id: Callback<String>,
64}
65
66#[component]
67pub fn NewContractNegotiationPageInner(props: &NewContractNegotiationPageInnerProps) -> HtmlResult {
68  let edc_connector_context = use_edc_connector_context();
69
70  let catalog_dataset = use_future_with(
71    (
72      edc_connector_context.clone(),
73      props.originator.clone(),
74      props.provider_id.clone(),
75      props.dataset_id.clone(),
76    ),
77    |parameters| async move {
78      let (edc_connector_context, originator, provider_id, dataset_id) = (*parameters).clone();
79
80      if let Some(edc_connector_client) = edc_connector_context.get_client() {
81        let dataset_request = DatasetRequest::builder()
82          .id(dataset_id.clone())
83          .counter_party_address(originator)
84          .counter_party_id(provider_id)
85          .protocol(Protocol::default())
86          .build();
87
88        let asset = edc_connector_client
89          .catalogue(EdcConnectorApiVersion::V4)
90          .dataset(&dataset_request)
91          .await
92          .ok();
93
94        let offers = asset
95          .as_ref()
96          .map(|asset| asset.offers().to_vec())
97          .unwrap_or_default();
98
99        let dataset = asset
100          .as_ref()
101          .map(AssetItem::from)
102          .map(DataspaceDataset::from);
103
104        (dataset, offers)
105      } else {
106        (None, vec![])
107      }
108    },
109  )?;
110
111  let (catalog_dataset, policies) = (*catalog_dataset).clone();
112
113  let selected_offer = use_state_eq(|| None);
114  let signing = use_state(|| false);
115
116  let onchange = use_callback(selected_offer.clone(), |policy: Policy, selected_offer| {
117    selected_offer.set(Some(policy))
118  });
119
120  let onclick = use_callback(
121    (
122      edc_connector_context.clone(),
123      catalog_dataset.clone(),
124      selected_offer.clone(),
125      props.originator.clone(),
126      props.provider_id.clone(),
127      signing.setter(),
128      props.on_contract_negotiation_id.clone(),
129    ),
130    |_,
131     (
132      edc_connector_context,
133      catalog_dataset,
134      selected_offer,
135      originator,
136      provider_id,
137      signing_setter,
138      on_contract_negotiation_id,
139    )| {
140      let edc_connector_context = edc_connector_context.clone();
141      if let Some(policy) = (**selected_offer).clone()
142        && let Some(catalog_dataset) = catalog_dataset
143      {
144        let originator = originator.clone();
145        let provider_id = provider_id.clone();
146        let asset_id = catalog_dataset.id.to_string();
147
148        signing_setter.set(true);
149        let signing_setter = signing_setter.clone();
150        let on_contract_negotiation_id = on_contract_negotiation_id.clone();
151
152        spawn_local(async move {
153          if let Some(edc_client) = edc_connector_context.get_client() {
154            let counter_party_address = originator.clone();
155            let counter_party_id = provider_id.clone();
156
157            log::info!("counter_party_address: {}", counter_party_address);
158            let policy: Policy = policy.clone();
159
160            if let Some(id) = policy.id() {
161              let policy_builder = Policy::builder()
162                .assigner(provider_id)
163                .id(id)
164                .kind(PolicyKind::Offer)
165                .permissions(
166                  policy
167                    .permissions()
168                    .iter()
169                    .map(|permission| {
170                      Permission::builder()
171                        .action(edc_connector_client::types::policy::Action::Simple(
172                          "use".to_string(),
173                        ))
174                        .constraints(
175                          permission
176                            .constraints()
177                            .iter()
178                            .map(|constraint| match constraint {
179                              Constraint::Atomic(atomic_constraint) => {
180                                let left_operand = match &atomic_constraint.left_operand {
181                                  LeftOperand::Simple(simple) => {
182                                    LeftOperand::Simple(simple.to_string())
183                                  }
184                                  LeftOperand::Id { id } => LeftOperand::Simple(id.to_string()),
185                                };
186
187                                let operator = match &atomic_constraint.operator {
188                                  Operator::Simple(simple) => Operator::Simple(simple.to_string()),
189                                  Operator::Id { id } => Operator::Simple(
190                                    id.to_string().replace("http://www.w3.org/ns/odrl/2/", ""),
191                                  ),
192                                };
193
194                                Constraint::Atomic(AtomicConstraint {
195                                  left_operand,
196                                  operator,
197                                  right_operand: atomic_constraint.right_operand.clone(),
198                                })
199                              }
200                              Constraint::MultiplicityConstraint(multiplicity_constraint) => {
201                                Constraint::MultiplicityConstraint(multiplicity_constraint.clone())
202                              }
203                            })
204                            .collect(),
205                        )
206                        .build()
207                    })
208                    .collect(),
209                )
210                .prohibitions(policy.prohibitions().to_vec())
211                .obligations(policy.obligations().to_vec())
212                .target(Target::Simple(asset_id));
213
214              let policy = policy_builder.build();
215
216              let new_contract_request = ContractRequest::builder()
217                .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
218                .counter_party_address(&counter_party_address)
219                .counter_party_id(counter_party_id)
220                .policy(policy)
221                .build();
222
223              match edc_client
224                .contract_negotiations(EdcConnectorApiVersion::V4)
225                .initiate(&new_contract_request)
226                .await
227              {
228                Ok(contract_negotiation_id) => {
229                  log::info!(
230                    "Contract negotiation initiated: {}",
231                    contract_negotiation_id.id()
232                  );
233
234                  on_contract_negotiation_id.emit(contract_negotiation_id.id().to_string());
235                }
236                Err(error) => {
237                  log::error!("Error initiating contract negotiation: {}", error);
238                }
239              }
240            };
241          }
242
243          signing_setter.set(false);
244        })
245      }
246    },
247  );
248
249  if let Some(catalog_dataset) = catalog_dataset {
250    let disabled = *signing;
251
252    let offers = policies
253      .iter()
254      .filter_map(|policy: &Policy| {
255        if let Some(offer_id) = policy.id().and_then(|policy| policy.split(':').next()) {
256          BASE64_STANDARD
257            .decode(offer_id)
258            .ok()
259            .and_then(|offer_id| String::from_utf8(offer_id).ok())
260            .map(|offer_id| (policy, offer_id))
261        } else {
262          None
263        }
264      })
265      .map(|(policy, _offer_id): (&Policy, String)| {
266        let onchange = onchange.clone();
267        let policy_definition_item = PolicyDefinitionItem::from(policy);
268        let selected = (*selected_offer).as_ref() == Some(policy);
269
270        let policy = policy.clone();
271
272        html!(
273          <PolicyCard
274            {policy_definition_item}
275            {selected}
276            {disabled}
277            on_click={onchange.reform(move |_| policy.clone())}
278          />
279        )
280      });
281
282    let disabled = selected_offer.is_none() || *signing;
283
284    Ok(html!(
285      <>
286        <DatasetCard dataset={catalog_dataset} />
287        <DescriptionList>
288          <DescriptionGroup term="Offers">
289            <Gallery gutter=true>{ for offers }</Gallery>
290          </DescriptionGroup>
291        </DescriptionList>
292        <Split gutter=true>
293          <SplitItem fill=true />
294          <SplitItem>
295            <Button variant={ButtonVariant::Primary} icon={Icon::Check} {disabled} {onclick}>
296              { "Sign" }
297            </Button>
298          </SplitItem>
299        </Split>
300      </>
301    ))
302  } else {
303    Ok(html!({ "The offer is not available." }))
304  }
305}