Skip to main content

edc_web_components/pages/
show_transfer_process.rs

1use 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  // Build file data & metadata
429  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  // Add the link element
438  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  // Set link attributes
445  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}