Skip to main content

edc_web_components/components/
create_asset.rs

1use crate::components::{DatasetCard, MultiStateSelector, StringListEdit};
2use crate::contexts::use_edc_connector_context;
3use crate::models::{Creator, DataspaceDataset, Thumbnail};
4use edc_connector_client::EdcConnectorApiVersion;
5use edc_connector_client::types::properties::ToValue;
6use edc_connector_client::types::{asset::NewAsset, data_address::DataAddress};
7use patternfly_yew::prelude::*;
8use std::collections::HashMap;
9use yew::platform::spawn_local;
10use yew::prelude::*;
11
12#[derive(Clone, Debug, PartialEq, Properties)]
13pub struct CreateAssetProps {
14  #[prop_or_default]
15  pub company_name: Option<String>,
16  #[prop_or_default]
17  pub company_logo_url: Option<String>,
18  #[prop_or_default]
19  pub on_create: Callback<()>,
20  #[prop_or_default]
21  pub dcterm_types: Option<Vec<(String, String)>>,
22}
23
24#[component]
25pub fn CreateAsset(props: &CreateAssetProps) -> Html {
26  let edc_connector_context = use_edc_connector_context();
27
28  let name = use_state(String::new);
29  let version = use_state(String::new);
30  let description = use_state(String::new);
31  let thumbnail_url = use_state(String::new);
32  let keywords = use_state(Vec::<String>::new);
33  let landing_page = use_state(String::new);
34
35  let company_name = use_state(|| props.company_name.clone().unwrap_or_default());
36  let company_logo_url = use_state(|| props.company_logo_url.clone().unwrap_or_default());
37
38  let base_url = use_state(String::new);
39  let content_type = use_state(|| "application/json".to_string());
40  let proxy_path = use_state(|| false);
41  let proxy_query_params = use_state(|| false);
42  let proxy_method = use_state(|| false);
43  let proxy_body = use_state(|| false);
44  let client_id = use_state(String::new);
45  let client_secret_key = use_state(String::new);
46  let token_url = use_state(String::new);
47  let scopes = use_state(|| "openid".to_string());
48  let headers = use_state(HashMap::<String, String>::new);
49
50  let selected_dcterm_types = use_state(|| {
51    props
52      .dcterm_types
53      .as_ref()
54      .map(|dcterm_types| {
55        dcterm_types
56          .iter()
57          .map(|(_, label)| (label.clone(), false))
58          .collect::<Vec<(String, bool)>>()
59      })
60      .unwrap_or_default()
61  });
62
63  let onsubmit = use_callback(
64    (
65      edc_connector_context.clone(),
66      (
67        name.clone(),
68        version.clone(),
69        description.clone(),
70        thumbnail_url.clone(),
71        keywords.clone(),
72        landing_page.clone(),
73        selected_dcterm_types.clone(),
74        props.dcterm_types.clone(),
75      ),
76      base_url.clone(),
77      (company_name.clone(), company_logo_url.clone()),
78      content_type.clone(),
79      (
80        proxy_path.clone(),
81        proxy_query_params.clone(),
82        proxy_method.clone(),
83        proxy_body.clone(),
84      ),
85      headers.clone(),
86      (
87        client_id.clone(),
88        client_secret_key.clone(),
89        token_url.clone(),
90        scopes.clone(),
91      ),
92      props.on_create.clone(),
93    ),
94    |event: SubmitEvent,
95     (
96      edc_connector_context,
97      (
98        name,
99        version,
100        description,
101        thumbnail_url,
102        keywords,
103        landing_page,
104        selected_dcterm_types,
105        dcterm_types,
106      ),
107      base_url,
108      (company_name, company_logo_url),
109      content_type,
110      (proxy_path, proxy_query_params, proxy_method, proxy_body),
111      headers,
112      (client_id, client_secret_key, token_url, scopes),
113      on_create,
114    )| {
115      event.prevent_default();
116
117      let name = (**name).clone();
118      let version = (**version).clone();
119      let description = (**description).clone();
120      let thumbnail_url = (**thumbnail_url).clone();
121      let keywords = (**keywords).clone();
122      let landing_page = (**landing_page).clone();
123
124      let base_url = (**base_url).clone();
125      let company_name = (**company_name).clone();
126      let company_logo_url = (**company_logo_url).clone();
127      let content_type = (**content_type).clone();
128      let proxy_path = **proxy_path;
129      let proxy_query_params = **proxy_query_params;
130      let proxy_method = **proxy_method;
131      let proxy_body = **proxy_body;
132      let headers = (**headers).clone();
133      let client_id = (**client_id).clone();
134      let client_secret_key = (**client_secret_key).clone();
135      let token_url = (**token_url).clone();
136      let scopes = (**scopes).clone();
137
138      let dcterm_types = {
139        let selected_dcterm_types = (**selected_dcterm_types).clone();
140        let dcterm_types = (*dcterm_types).clone();
141
142        if let Some(dcterm_types) = dcterm_types {
143          selected_dcterm_types
144            .iter()
145            .zip(dcterm_types.iter())
146            .filter(|((_, selected), (_, _))| *selected)
147            .map(|((_, _), (id, _))| id.clone())
148            .collect::<Vec<String>>()
149        } else {
150          vec![]
151        }
152      };
153
154      let edc_connector_context = edc_connector_context.clone();
155      let on_create = on_create.clone();
156
157      spawn_local(async move {
158        let mut data_address_builder = DataAddress::builder()
159          .kind("HttpData")
160          .property("baseUrl", base_url)
161          .property("proxyPath", if proxy_path { "true" } else { "false" })
162          .property(
163            "proxyQueryParams",
164            if proxy_query_params { "true" } else { "false" },
165          )
166          .property("proxyMethod", if proxy_method { "true" } else { "false" })
167          .property("proxyBody", if proxy_body { "true" } else { "false" });
168
169        if !client_id.is_empty() {
170          data_address_builder = data_address_builder.property("oauth2:clientId", client_id)
171        };
172
173        if !client_secret_key.is_empty() {
174          data_address_builder =
175            data_address_builder.property("oauth2:clientSecretKey", &client_secret_key)
176        };
177
178        if !token_url.is_empty() {
179          data_address_builder = data_address_builder.property("oauth2:tokenUrl", &token_url)
180        };
181
182        if !scopes.is_empty() {
183          data_address_builder = data_address_builder.property("oauth2:scope", &scopes)
184        };
185
186        struct Creator {
187          name: String,
188          logo_url: String,
189        }
190
191        impl ToValue for Creator {
192          fn into_value(self) -> serde_json::Value {
193            serde_json::json!({
194              "@type": [
195                "http://xmlns.com/foaf/0.1/Organization",
196                "http://www.w3.org/ns/prov#Agent"
197              ],
198              "http://xmlns.com/foaf/0.1/name": self.name,
199              "http://xmlns.com/foaf/0.1/thumbnail": {
200                "rdf:resource": self.logo_url,
201              },
202            })
203          }
204        }
205
206        struct Thumbnail {
207          resource: String,
208        }
209
210        impl ToValue for Thumbnail {
211          fn into_value(self) -> serde_json::Value {
212            serde_json::json!({
213              "rdf:resource": self.resource,
214            })
215          }
216        }
217
218        for (key, value) in &headers {
219          data_address_builder = data_address_builder.property(&format!("header:{key}"), value);
220        }
221
222        let data_address = data_address_builder.build().unwrap();
223
224        let new_asset_builder = NewAsset::builder()
225          // .id(&identifier)
226          .data_address(data_address)
227          .property("name", name.clone())
228          .property("http://www.w3.org/ns/dcat#version", version)
229          .property("contenttype", content_type)
230          .property(
231            "http://purl.org/dc/terms/creator",
232            Creator {
233              name: company_name,
234              logo_url: company_logo_url,
235            },
236          );
237
238        let new_asset_builder = if !dcterm_types.is_empty() {
239          new_asset_builder.property("http://purl.org/dc/terms/type", dcterm_types)
240        } else {
241          new_asset_builder
242        };
243
244        let new_asset_builder = if !name.is_empty() {
245          new_asset_builder.property("http://purl.org/dc/terms/title", name)
246        } else {
247          new_asset_builder
248        };
249
250        let new_asset_builder = if !description.is_empty() {
251          new_asset_builder.property("http://www.w3.org/2000/01/rdf-schema#comment", description)
252        } else {
253          new_asset_builder
254        };
255
256        let new_asset_builder = if !thumbnail_url.is_empty() {
257          new_asset_builder.property(
258            "http://xmlns.com/foaf/0.1/thumbnail",
259            Thumbnail {
260              resource: thumbnail_url,
261            },
262          )
263        } else {
264          new_asset_builder
265        };
266
267        let new_asset_builder = if !keywords.is_empty() {
268          new_asset_builder.property("http://www.w3.org/ns/dcat#keyword", keywords)
269        } else {
270          new_asset_builder
271        };
272
273        let new_asset_builder = if !landing_page.is_empty() {
274          new_asset_builder.property("http://www.w3.org/ns/dcat#landingPage", landing_page)
275        } else {
276          new_asset_builder
277        };
278
279        let new_asset = new_asset_builder.build();
280
281        if let Some(client) = edc_connector_context.get_client() {
282          if let Err(error) = client
283            .assets(EdcConnectorApiVersion::V4)
284            .create(&new_asset)
285            .await
286          {
287            log::error!("Error creating asset: {}", error);
288          } else {
289            on_create.emit(());
290          }
291        }
292      })
293    },
294  );
295
296  let onchange_name = use_callback(name.setter(), |value, setter| {
297    setter.set(value);
298  });
299
300  let onchange_version = use_callback(version.setter(), |value, setter| {
301    setter.set(value);
302  });
303
304  let onchange_description = use_callback(description.setter(), |value, setter| {
305    setter.set(value);
306  });
307
308  let onchange_thumbnail_url = use_callback(thumbnail_url.setter(), |value, setter| {
309    setter.set(value);
310  });
311
312  let onchange_keywords = use_callback(keywords.setter(), |keywords, setter| {
313    setter.set(keywords);
314  });
315
316  let onchange_landing_page = use_callback(landing_page.setter(), |value, setter| {
317    setter.set(value);
318  });
319
320  let onchange_company_name = use_callback(company_name.setter(), |value, setter| {
321    setter.set(value);
322  });
323
324  let onchange_company_logo_url = use_callback(company_logo_url.setter(), |value, setter| {
325    setter.set(value);
326  });
327
328  let onchange_base_url = use_callback(base_url.setter(), |value, setter| {
329    setter.set(value);
330  });
331
332  let onchange_content_type = use_callback(content_type.setter(), |value, setter| {
333    setter.set(value);
334  });
335
336  let onchange_proxy_path = use_callback(proxy_path.setter(), |value, setter| {
337    setter.set(value);
338  });
339
340  let onchange_proxy_query_params = use_callback(proxy_query_params.setter(), |value, setter| {
341    setter.set(value);
342  });
343
344  let onchange_proxy_method = use_callback(proxy_method.setter(), |value, setter| {
345    setter.set(value);
346  });
347
348  let onchange_proxy_body = use_callback(proxy_body.setter(), |value, setter| {
349    setter.set(value);
350  });
351
352  let onchange_client_id = use_callback(client_id.setter(), |value, setter| {
353    setter.set(value);
354  });
355
356  let onchange_client_secret_key = use_callback(client_secret_key.setter(), |value, setter| {
357    setter.set(value);
358  });
359
360  let onchange_token_url = use_callback(token_url.setter(), |value, setter| {
361    setter.set(value);
362  });
363
364  let onchange_scopes = use_callback(scopes.setter(), |value, setter| {
365    setter.set(value);
366  });
367
368  let disabled = (*name).is_empty() || (*base_url).is_empty();
369
370  let dataset = DataspaceDataset {
371    id: "".to_string(),
372    title: (*name).clone(),
373    version: semver::Version::parse(&version).ok(),
374    comment: if (*description).is_empty() {
375      None
376    } else {
377      Some((*description).clone())
378    },
379    thumbnail: if (*thumbnail_url).is_empty() {
380      None
381    } else {
382      Some(Thumbnail {
383        resource: Some((*thumbnail_url).clone()),
384      })
385    },
386    creator: if (*company_logo_url).is_empty() {
387      None
388    } else {
389      Some(Creator {
390        name: None,
391        thumbnail: Some(Thumbnail {
392          resource: Some((*company_logo_url).clone()),
393        }),
394      })
395    },
396    keywords: (*keywords).clone(),
397    policies: vec![],
398    dcterm_types: vec![],
399  };
400
401  let card_style = "background-color: var(--pf-v6-c-page--BackgroundColor);";
402
403  let on_selected = use_callback(
404    selected_dcterm_types.setter(),
405    |selected_dcterm_types: Vec<(String, bool)>, selected_dcterm_types_setter| {
406      selected_dcterm_types_setter.set(selected_dcterm_types);
407    },
408  );
409
410  let offer_types = if let Some(dc_term_types) = &props.dcterm_types {
411    let selected_dcterm_types = (*selected_dcterm_types).clone();
412
413    let selectable_items = dc_term_types
414      .iter()
415      .zip(selected_dcterm_types.iter())
416      .map(|((_, label), (_, selected))| (label.clone(), *selected))
417      .collect::<Vec<(String, bool)>>();
418
419    html!(
420      <FormGroup label="Dataset Type(s)">
421        <MultiStateSelector {selectable_items} {on_selected} />
422      </FormGroup>
423    )
424  } else {
425    html!()
426  };
427
428  html!(
429    <Form {onsubmit}>
430      <Card>
431        <CardHeader>
432          <Title level={Level::H2} size={Size::XXXLarge}>{ "Provider Information" }</Title>
433        </CardHeader>
434        <CardBody>
435          <FormGroup label="Company Name">
436            <TextInput
437              required=true
438              value={(*company_name).to_string()}
439              onchange={onchange_company_name}
440              readonly={props.company_name.is_some()}
441            />
442          </FormGroup>
443          <FormGroup label="Company Logo URL">
444            <TextInput
445              required=true
446              value={(*company_logo_url).to_string()}
447              onchange={onchange_company_logo_url}
448              readonly={props.company_logo_url.is_some()}
449            />
450          </FormGroup>
451        </CardBody>
452      </Card>
453      <Card>
454        <CardHeader>
455          <CardHeaderMain>
456            <Title level={Level::H2} size={Size::XXXLarge}>{ "Dataset Description" }</Title>
457            <p>{ "This section defines the elements visible to the other participants." }</p>
458          </CardHeaderMain>
459        </CardHeader>
460        <CardBody>
461          <Flex>
462            <FlexItem
463              modifiers={[FlexModifier::Flex1.all(), FlexModifier::Align(Alignment::Start).all()]}
464            >
465              <FormGroup label="Title" required=true>
466                <TextInput required=true value={(*name).to_string()} onchange={onchange_name} />
467              </FormGroup>
468              <FormGroupValidated<TextInput>
469                label="Version"
470                required=true
471                validator={Validator::from(|ctx: ValidationContext<String>| {
472                  if ctx.initial {
473                    ValidationResult::default()
474                  } else if ctx.value.is_empty() {
475                    ValidationResult::error("Must not be empty")
476                  } else {
477                    if semver::Version::parse(&ctx.value).is_ok() {
478                      ValidationResult::ok()
479                    } else {
480                      ValidationResult::error("Not a semantic versioning format (MAJOR.MINOR.PATCH)")
481                    }
482                  }
483               })}
484              >
485                <TextInput
486                  required=true
487                  value={(*version).to_string()}
488                  onchange={onchange_version}
489                />
490              </FormGroupValidated<TextInput>>
491              <FormGroup label="Description">
492                <TextArea
493                  required=true
494                  value={(*description).to_string()}
495                  onchange={onchange_description}
496                />
497              </FormGroup>
498              <FormGroup label="Thumbnail URL">
499                <TextInput
500                  required=true
501                  value={(*thumbnail_url).to_string()}
502                  onchange={onchange_thumbnail_url}
503                />
504              </FormGroup>
505              <FormGroup label="Keywords">
506                <StringListEdit values={(*keywords).clone()} onchange={onchange_keywords} />
507              </FormGroup>
508              <FormGroup label="Landing Page">
509                <TextInput
510                  required=true
511                  value={(*landing_page).to_string()}
512                  onchange={onchange_landing_page}
513                />
514              </FormGroup>
515              { offer_types }
516            </FlexItem>
517            <FlexItem modifiers={[FlexModifier::Align(Alignment::Start).all()]}>
518              <DatasetCard {dataset} />
519            </FlexItem>
520          </Flex>
521        </CardBody>
522      </Card>
523      <Card style={card_style}>
524        <CardHeader>
525          <CardHeaderMain>
526            <Title level={Level::H2} size={Size::XXXLarge}>{ "Dataset Source" }</Title>
527            <p>
528              { "This section defines the private settings for communication and data exchange with other participants within the dataspace." }
529            </p>
530            <p>{ "None of the fields here are ever exposed externally." }</p>
531          </CardHeaderMain>
532        </CardHeader>
533        <CardBody>
534          <Split gutter=true>
535            <SplitItem fill=true>
536              <FormGroup label="Base URL" required=true>
537                <TextInput
538                  required=true
539                  value={(*base_url).to_string()}
540                  onchange={onchange_base_url}
541                  r#type={TextInputType::Url}
542                />
543              </FormGroup>
544              <FormGroup label="Content Type">
545                <TextInput value={(*content_type).to_string()} onchange={onchange_content_type} />
546              </FormGroup>
547              <FormGroup label="Proxy Path">
548                <Switch checked={*proxy_path} onchange={onchange_proxy_path} />
549              </FormGroup>
550              <FormGroup label="Proxy Query Parameters">
551                <Switch checked={*proxy_query_params} onchange={onchange_proxy_query_params} />
552              </FormGroup>
553              <FormGroup label="Proxy Method">
554                <Switch checked={*proxy_method} onchange={onchange_proxy_method} />
555              </FormGroup>
556              <FormGroup label="Proxy Body">
557                <Switch checked={*proxy_body} onchange={onchange_proxy_body} />
558              </FormGroup>
559            </SplitItem>
560            <SplitItem fill=true>
561              <FormGroup label="OpenID Client ID">
562                <TextInput value={(*client_id).clone()} onchange={onchange_client_id} />
563              </FormGroup>
564              <FormGroup label="OpenID Client Secret Key">
565                <TextInput
566                  value={(*client_secret_key).clone()}
567                  onchange={onchange_client_secret_key}
568                />
569              </FormGroup>
570              <FormGroup label="OpenID Token URL">
571                <TextInput value={(*token_url).clone()} onchange={onchange_token_url} />
572              </FormGroup>
573              <FormGroup label="OpenID Scopes">
574                <TextInput value={(*scopes).clone()} onchange={onchange_scopes} />
575              </FormGroup>
576            </SplitItem>
577          </Split>
578        </CardBody>
579      </Card>
580      <ActionGroup>
581        <Button
582          variant={ButtonVariant::Primary}
583          label="Submit"
584          r#type={ButtonType::Submit}
585          {disabled}
586        />
587        <Button variant={ButtonVariant::Secondary} label="Reset" r#type={ButtonType::Reset} />
588      </ActionGroup>
589    </Form>
590  )
591}