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