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