edc_web_components/pages/
new_contract_negotiation_page.rs1use crate::components::PolicyCard;
2use crate::contexts::use_edc_connector_context;
3use crate::models::{DatasetExtraFields, PolicyDefinitionItem};
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 catalog_dataset.extra.description.clone(),
95 catalog_dataset.extra.creator.clone(),
96 )
97 })
98 } else {
99 None
100 }
101 },
102 )?;
103
104 let catalog_dataset = (*catalog_dataset).clone();
105
106 let selected_offer = use_state_eq(|| None);
107 let signing = use_state(|| false);
108
109 let onchange = use_callback(selected_offer.clone(), |policy: Policy, selected_offer| {
110 selected_offer.set(Some(policy))
111 });
112
113 let onclick = use_callback(
114 (
115 edc_connector_context.clone(),
116 catalog_dataset.clone(),
117 selected_offer.clone(),
118 props.originator.clone(),
119 props.provider_id.clone(),
120 signing.setter(),
121 props.on_contract_negotiation_id.clone(),
122 ),
123 |_,
124 (
125 edc_connector_context,
126 catalog_dataset,
127 selected_offer,
128 originator,
129 provider_id,
130 signing_setter,
131 on_contract_negotiation_id,
132 )| {
133 let edc_connector_context = edc_connector_context.clone();
134 if let Some(policy) = (**selected_offer).clone()
135 && let Some(catalog_dataset) = catalog_dataset
136 {
137 let originator = originator.clone();
138 let provider_id = provider_id.clone();
139 let asset_id = catalog_dataset.0.to_string();
140
141 signing_setter.set(true);
142 let signing_setter = signing_setter.clone();
143 let on_contract_negotiation_id = on_contract_negotiation_id.clone();
144
145 spawn_local(async move {
146 if let Some(edc_client) = edc_connector_context.get_client() {
147 let counter_party_address = originator.clone();
148 let counter_party_id = provider_id.clone();
149
150 log::info!("counter_party_address: {}", counter_party_address);
151 let policy: Policy = policy.clone();
152
153 if let Some(id) = policy.id() {
154 let policy_builder = Policy::builder()
155 .assigner(provider_id)
156 .id(id)
157 .kind(PolicyKind::Offer)
158 .permissions(
159 policy
160 .permissions()
161 .iter()
162 .map(|permission| {
163 Permission::builder()
164 .action(edc_connector_client::types::policy::Action::Simple(
165 "use".to_string(),
166 ))
167 .constraints(
168 permission
169 .constraints()
170 .iter()
171 .map(|constraint| match constraint {
172 Constraint::Atomic(atomic_constraint) => {
173 let left_operand = match &atomic_constraint.left_operand {
174 LeftOperand::Simple(simple) => {
175 LeftOperand::Simple(simple.to_string())
176 }
177 LeftOperand::Id { id } => LeftOperand::Simple(id.to_string()),
178 };
179
180 let operator = match &atomic_constraint.operator {
181 Operator::Simple(simple) => Operator::Simple(simple.to_string()),
182 Operator::Id { id } => Operator::Simple(
183 id.to_string().replace("http://www.w3.org/ns/odrl/2/", ""),
184 ),
185 };
186
187 Constraint::Atomic(AtomicConstraint {
188 left_operand,
189 operator,
190 right_operand: atomic_constraint.right_operand.clone(),
191 })
192 }
193 Constraint::MultiplicityConstraint(multiplicity_constraint) => {
194 Constraint::MultiplicityConstraint(multiplicity_constraint.clone())
195 }
196 })
197 .collect(),
198 )
199 .build()
200 })
201 .collect(),
202 )
203 .prohibitions(policy.prohibitions().to_vec())
204 .obligations(policy.obligations().to_vec())
205 .target(Target::Simple(asset_id));
206
207 let policy = policy_builder.build();
208
209 let new_contract_request = ContractRequest::builder()
210 .protocol(Protocol::new("dataspace-protocol-http:2025-1"))
211 .counter_party_address(&counter_party_address)
212 .counter_party_id(counter_party_id)
213 .policy(policy)
214 .build();
215
216 match edc_client
217 .contract_negotiations(EdcConnectorApiVersion::V4)
218 .initiate(&new_contract_request)
219 .await
220 {
221 Ok(contract_negotiation_id) => {
222 log::info!(
223 "Contract negotiation initiated: {}",
224 contract_negotiation_id.id()
225 );
226
227 on_contract_negotiation_id.emit(contract_negotiation_id.id().to_string());
228 }
229 Err(error) => {
230 log::error!("Error initiating contract negotiation: {}", error);
231 }
232 }
233 };
234 }
235
236 signing_setter.set(false);
237 })
238 }
239 },
240 );
241
242 if let Some(catalog_dataset) = catalog_dataset {
243 let provider_id = props.provider_id.clone();
244 let asset_id = catalog_dataset.0.clone();
245 let asset_name = catalog_dataset.1.clone();
246 let policies = catalog_dataset.2;
247 let description = catalog_dataset.3;
248 let creator = catalog_dataset.4;
249 let disabled = *signing;
250
251 let offers = policies
252 .iter()
253 .filter_map(|policy: &Policy| {
254 if let Some(offer_id) = policy.id().and_then(|policy| policy.split(':').next()) {
255 BASE64_STANDARD
256 .decode(offer_id)
257 .ok()
258 .and_then(|offer_id| String::from_utf8(offer_id).ok())
259 .map(|offer_id| (policy, offer_id))
260 } else {
261 None
262 }
263 })
264 .map(|(policy, _offer_id): (&Policy, String)| {
265 let onchange = onchange.clone();
266 let policy_definition_item = PolicyDefinitionItem::from(policy);
267 let selected = (*selected_offer).as_ref() == Some(policy);
268
269 let policy = policy.clone();
270
271 html!(
272 <PolicyCard
273 {policy_definition_item}
274 {selected}
275 {disabled}
276 on_click={onchange.reform(move |_| policy.clone())}
277 />
278 )
279 });
280
281 let disabled = selected_offer.is_none() || *signing;
282
283 let details = if let Some(value) = description {
284 html!(<DescriptionGroup term="Description">{ value }</DescriptionGroup>)
285 } else {
286 html!()
287 };
288
289 let provider = if let Some(value) = creator {
290 html!(
291 <>
292 <DescriptionGroup term="Provider">{ value.name }</DescriptionGroup>
293 <DescriptionGroup term="Provider ID">{ provider_id }</DescriptionGroup>
294 </>
295 )
296 } else {
297 html!()
298 };
299
300 Ok(html!(
301 <>
302 <DescriptionList>
303 { provider }
304 <DescriptionGroup term="Asset ID">{ asset_id }</DescriptionGroup>
305 <DescriptionGroup term="Asset Name">{ asset_name }</DescriptionGroup>
306 { details }
307 <DescriptionGroup term="Offers">
308 <Gallery gutter=true>{ for offers }</Gallery>
309 </DescriptionGroup>
310 </DescriptionList>
311 <Split gutter=true>
312 <SplitItem fill=true />
313 <SplitItem>
314 <Button variant={ButtonVariant::Primary} icon={Icon::Check} {disabled} {onclick}>
315 { "Sign" }
316 </Button>
317 </SplitItem>
318 </Split>
319 </>
320 ))
321 } else {
322 Ok(html!({ "The offer is not available." }))
323 }
324}