edc_web_components/pages/
new_contract_negotiation_page.rs1use 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}