edc_web_components/pages/
show_transfer_process.rs1use crate::components::{
2 AssetReference, ContractAgreementReference, Identifier, TransferProcessStatus,
3};
4use crate::contexts::use_edc_connector_context;
5use edc_connector_client::types::transfer_process::{TransferProcessKind, TransferProcessState};
6use patternfly_yew::prelude::*;
7use web_sys::wasm_bindgen::{JsCast, JsValue};
8use web_sys::{BlobPropertyBag, HtmlAnchorElement};
9use yew::platform::spawn_local;
10use yew::prelude::*;
11use yew::suspense::use_future_with;
12
13#[derive(Clone, Debug, PartialEq, Properties)]
14pub struct ShowTransferProcessPageProps {
15 #[prop_or("Transfer".to_string())]
16 pub title: String,
17 #[prop_or(None)]
18 pub tag_line: Option<String>,
19 pub transfer_process_id: String,
20}
21
22#[component]
23pub fn ShowTransferProcessPage(props: &ShowTransferProcessPageProps) -> Html {
24 let tag_line = props
25 .tag_line
26 .as_ref()
27 .map(|tag_line| html!(<p>{ tag_line }</p>))
28 .unwrap_or_default();
29
30 html!(
31 <>
32 <Title level={Level::H2} size={Size::XXXLarge}>{ &props.title }</Title>
33 { tag_line }
34 <Suspense fallback={html! {<Bullseye><Spinner /></Bullseye>}}>
35 <ShowTransferProcessPageInner transfer_process_id={props.transfer_process_id.clone()} />
36 </Suspense>
37 </>
38 )
39}
40
41#[component]
42pub fn ShowTransferProcessPageInner(props: &ShowTransferProcessPageProps) -> HtmlResult {
43 let edc_connector_client = use_edc_connector_context();
44
45 let refresh = use_state(|| 0usize);
46
47 let transfer_process = use_future_with(
48 (
49 props.transfer_process_id.clone(),
50 edc_connector_client.clone(),
51 refresh.clone(),
52 ),
53 |properties| async move {
54 let (transfer_process_id, edc_connector_client, _) = (*properties).clone();
55
56 if let Some(client) = edc_connector_client.get_client() {
57 client
58 .transfer_processes(edc_connector_client::EdcConnectorApiVersion::V4)
59 .get(&transfer_process_id)
60 .await
61 .ok()
62 } else {
63 None
64 }
65 },
66 )?;
67
68 let on_started = use_callback(refresh.setter(), |_, refresh_setter| {
69 refresh_setter.set(1);
70 });
71
72 let on_finalized = use_callback(refresh.setter(), |_, refresh_setter| {
73 refresh_setter.set(1);
74 });
75
76 let transfer_process = (*transfer_process).clone();
77
78 let do_transfer = use_callback(
79 edc_connector_client.clone(),
80 |transfer_process_id: String, edc_connector_client| {
81 let edc_connector_client = edc_connector_client.clone();
82 let transfer_process_id = transfer_process_id.clone();
83
84 spawn_local(async move {
85 if let Some(client) = edc_connector_client.get_client()
86 && let Ok(data_address) = client
87 .edrs(edc_connector_client::EdcConnectorApiVersion::V3)
88 .get_data_address(&transfer_process_id)
89 .await
90 && let Ok(Some(endpoint)) = data_address.property::<String>("endpoint")
91 && let Ok(Some(authorization)) = data_address.property::<String>("authorization")
92 {
93 let client = reqwest::Client::new();
94
95 if let Ok(response) = client
96 .get(endpoint)
97 .header("Authorization", authorization)
98 .send()
99 .await
100 {
101 let content_type = response
102 .headers()
103 .get("Content-Type")
104 .map(|header_value| {
105 header_value
106 .to_str()
107 .unwrap_or("application/octet-stream")
108 .to_string()
109 })
110 .unwrap_or("application/octet-stream".to_string());
111
112 let extension = mime2ext::mime2ext(&content_type).unwrap_or("bin");
113
114 log::info!("Response: {:?}", content_type);
115 log::info!("Response: {:?}", response.status());
116
117 if content_type.starts_with("text/html")
118 && let Some(location) = response
119 .headers()
120 .get("Location")
121 .map(|value| value.to_str().unwrap_or_default())
122 && let Some(window) = web_sys::window()
123 {
124 let _ = window.open_with_url_and_target(location, "_blank");
125 } else if let Ok(data) = response.bytes().await
126 && let Err(error) =
127 save_byte_array(&format!("data.{extension}"), &content_type, &data)
128 {
129 log::error!("Error saving byte array: {:?}", error);
130 } else {
131 log::info!("Downloaded data");
132 }
133 }
134 }
135 });
136 },
137 );
138
139 let backdropper = use_backdrop();
140 let show_enpoint_information = use_callback(
141 (edc_connector_client.clone(), backdropper),
142 |transfer_process_id: String, (edc_connector_client, backdropper)| {
143 let edc_connector_client = edc_connector_client.clone();
144 let backdropper = backdropper.clone();
145 let transfer_process_id = transfer_process_id.clone();
146
147 spawn_local(async move {
148 if let Some(client) = edc_connector_client.get_client()
149 && let Ok(data_address) = client
150 .edrs(edc_connector_client::EdcConnectorApiVersion::V3)
151 .get_data_address(&transfer_process_id)
152 .await
153 && let Ok(Some(endpoint)) = data_address.property::<String>("endpoint")
154 && let Ok(Some(authorization)) = data_address.property::<String>("authorization")
155 && let Some(backdropper) = backdropper
156 {
157 backdropper.open(Backdrop::new(html!(
158 <Bullseye>
159 <Modal title="Transfer Endpoint Information" variant={ModalVariant::Medium}>
160 <DescriptionList>
161 <DescriptionGroup term="Endpoint URL">
162 <Clipboard readonly=true value={endpoint} />
163 </DescriptionGroup>
164 <DescriptionGroup term="Authorization Header">
165 <Clipboard readonly=true value={authorization} />
166 </DescriptionGroup>
167 </DescriptionList>
168 </Modal>
169 </Bullseye>
170 )));
171 }
172 });
173 },
174 );
175
176 let suspend_transfer = use_callback(
177 edc_connector_client.clone(),
178 |transfer_process_id: String, edc_connector_client| {
179 let edc_connector_client = edc_connector_client.clone();
180 let transfer_process_id = transfer_process_id.clone();
181
182 spawn_local(async move {
183 if let Some(client) = edc_connector_client.get_client()
184 && let Err(error) = client
185 .transfer_processes(edc_connector_client::EdcConnectorApiVersion::V3)
186 .suspend(&transfer_process_id, "completed")
187 .await
188 {
189 log::error!("Error getting data address {error}");
190 }
191 });
192 },
193 );
194
195 if let Some(transfer_process) = transfer_process {
196 let transfer_process_id = transfer_process.id().to_string();
197 let endpoint_transfer_process_id = transfer_process.id().to_string();
198 let complete_transfer_process_id = transfer_process.id().to_string();
199
200 let data_access = if transfer_process.state() == &TransferProcessState::Started
201 && transfer_process.kind() == &TransferProcessKind::Consumer
202 {
203 html!(
204 <Card style="border: 1px solid var(--pf-global--active-color--100, #06c);">
205 <CardTitle>
206 <Title level={Level::H2}>{ "Data Access" }</Title>
207 </CardTitle>
208 <CardBody>
209 <Stack gutter=true>
210 <StackItem>
211 <Split gutter=true>
212 <SplitItem>
213 <yew_icons::Icon data={yew_icons::IconData::LUCIDE_CHECK} />
214 </SplitItem>
215 <SplitItem fill=true>
216 { "Download it now, or get the endpoint and token to call it from your own system." }
217 </SplitItem>
218 </Split>
219 </StackItem>
220 <StackItem>
221 <Flex>
222 <FlexItem>
223 <Button
224 variant={ButtonVariant::Primary}
225 onclick={do_transfer.reform(move |_| transfer_process_id.clone())}
226 icon={Icon::Download}
227 >
228 { "Retrieve Dataset" }
229 </Button>
230 </FlexItem>
231 <FlexItem>
232 <Button
233 variant={ButtonVariant::Primary}
234 onclick={show_enpoint_information.reform(move |_| endpoint_transfer_process_id.clone())}
235 icon={Icon::Code}
236 >
237 { "Endpoint Information" }
238 </Button>
239 </FlexItem>
240 </Flex>
241 </StackItem>
242 </Stack>
243 </CardBody>
244 </Card>
245 )
246 } else {
247 html!()
248 };
249
250 let suspend = if transfer_process.state() == &TransferProcessState::Started {
251 html!(
252 <FlexItem>
253 <Button
254 variant={ButtonVariant::Warning}
255 onclick={suspend_transfer.reform(move |_| complete_transfer_process_id.clone())}
256 icon={Icon::Pause}
257 >
258 { "Suspend Transfer" }
259 </Button>
260 </FlexItem>
261 )
262 } else {
263 html!()
264 };
265
266 let correlation_id = transfer_process
267 .correlation_id()
268 .map(|correlation_id| html!(<Identifier id={correlation_id.to_string()} />))
269 .unwrap_or(html!(
270 <HelperText>
271 <HelperTextItem variant={HelperTextItemVariant::Intermediate}>{ "None" }</HelperTextItem>
272 </HelperText>
273 ));
274
275 let kind = crate::models::TransferProcessKind::from(transfer_process.kind()).to_string();
276
277 let state_date = chrono::DateTime::from_timestamp_millis(transfer_process.state_timestamp())
278 .unwrap_or_default()
279 .to_string();
280
281 let state = Some(
282 crate::models::TransferProcessState::from(transfer_process.state()).to_string(),
283 )
284 .map(|value| {
285 let color = match value.as_str() {
286 "Started" => Color::Green,
287 "Terminated" => Color::Red,
288 _ => Color::Blue,
289 };
290 let text_help = match value.as_str() {
291 "Terminated" => "at".to_string(),
292 _ => "since".to_string(),
293 };
294
295 html!(
296 <DescriptionGroup term="State">
297 <Label label={value} {color} />
298 <HelperText>
299 <HelperTextItem variant={HelperTextItemVariant::Intermediate}>
300 { format!("{} {}", text_help, state_date) }
301 </HelperTextItem>
302 </HelperText>
303 </DescriptionGroup>
304 )
305 });
306
307 Ok(html!(
308 <Stack gutter=true>
309 <StackItem>
310 <Flex modifiers={[FlexModifier::Justify(Justify::Start)]}>
311 <FlexItem modifiers={[FlexModifier::Flex1, FlexModifier::Align(Alignment::Stretch)]}>
312 <Card full_height=true>
313 <CardTitle>
314 <Title level={Level::H2}>{ "Transfer Properties" }</Title>
315 </CardTitle>
316 <CardBody>
317 <DescriptionList mode={[DescriptionListMode::Horizontal]}>
318 <DescriptionGroup term="Id">
319 <Identifier id={transfer_process.id().to_string()} />
320 </DescriptionGroup>
321 { state }
322 <DescriptionGroup term="Kind">{ kind }</DescriptionGroup>
323 <DescriptionGroup term="Transfer Type">
324 { transfer_process.transfer_type() }
325 </DescriptionGroup>
326 <DescriptionGroup term="Correlation Transfer ID">
327 { correlation_id }
328 </DescriptionGroup>
329 </DescriptionList>
330 </CardBody>
331 <CardFooter>{ suspend }</CardFooter>
332 </Card>
333 </FlexItem>
334 <FlexItem modifiers={[FlexModifier::Flex1, FlexModifier::Align(Alignment::Stretch)]}>
335 <Stack gutter=true>
336 <StackItem>
337 <Card>
338 <CardTitle>
339 <Title level={Level::H2}>{ "Linked to" }</Title>
340 </CardTitle>
341 <CardBody>
342 <Flex modifiers={[FlexModifier::Column.lg()]}>
343 <FlexItem>
344 <Card>
345 <CardBody>
346 <Split gutter=true>
347 <SplitItem>
348 <yew_icons::Icon data={yew_icons::IconData::LUCIDE_BOX} />
349 </SplitItem>
350 <SplitItem fill=true>
351 <DescriptionGroup term="Asset">
352 <AssetReference
353 asset_id={transfer_process.asset_id().to_string()}
354 />
355 <Identifier id={transfer_process.asset_id().to_string()} />
356 </DescriptionGroup>
357 </SplitItem>
358 </Split>
359 </CardBody>
360 </Card>
361 </FlexItem>
362 <FlexItem>
363 <Card>
364 <CardBody>
365 <Split gutter=true>
366 <SplitItem>
367 <yew_icons::Icon
368 data={yew_icons::IconData::LUCIDE_HEART_HANDSHAKE}
369 />
370 </SplitItem>
371 <SplitItem fill=true>
372 <DescriptionGroup term="Contract Agreement">
373 <div class="pf-v6-u-font-weight-bold">{ "Agreement" }</div>
374 <Identifier id={transfer_process.contract_id().to_string()} />
375 </DescriptionGroup>
376 </SplitItem>
377 <SplitItem>
378 <ContractAgreementReference
379 contract_agreement_id={transfer_process.contract_id().to_string()}
380 />
381 </SplitItem>
382 </Split>
383 </CardBody>
384 </Card>
385 </FlexItem>
386 </Flex>
387 </CardBody>
388 </Card>
389 </StackItem>
390 <StackItem>{ data_access }</StackItem>
391 </Stack>
392 </FlexItem>
393 </Flex>
394 </StackItem>
395 <StackItem>
396 <Flex>
397 <FlexItem modifiers={[FlexModifier::Flex1, FlexModifier::Align(Alignment::Start)]}>
398 <Card>
399 <CardTitle>
400 <Title level={Level::H2}>{ "Transfer Process" }</Title>
401 </CardTitle>
402 <CardBody>
403 <TransferProcessStatus
404 transfer_process_id={props.transfer_process_id.clone()}
405 {on_started}
406 {on_finalized}
407 />
408 </CardBody>
409 </Card>
410 </FlexItem>
411 </Flex>
412 </StackItem>
413 </Stack>
414 ))
415 } else {
416 Ok(html!(
417 format!(
418 "Transfer Process with id {} not found.",
419 props.transfer_process_id
420 )
421 ))
422 }
423}
424
425fn save_byte_array(name: &str, mime_type: &str, data: &[u8]) -> Result<(), JsValue> {
426 use web_sys::{Blob, Url, js_sys::Uint8Array};
427
428 let props = BlobPropertyBag::new();
430 props.set_type(mime_type);
431
432 let blob = Blob::new_with_u8_array_sequence_and_options(
433 &JsValue::from(vec![Uint8Array::new_from_slice(data)]),
434 &props,
435 )?;
436
437 let document = web_sys::window()
439 .and_then(|window| window.document())
440 .ok_or(JsValue::null())?;
441
442 let link = document.create_element("a")?;
443
444 let url = Url::create_object_url_with_blob(&blob)?;
446 link.set_attribute("href", &url)?;
447 link.set_attribute("download", name)?;
448
449 link.dyn_into::<HtmlAnchorElement>()?.click();
450 Url::revoke_object_url(&url)?;
451
452 Ok(())
453}