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