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    landing_page: if (*landing_page).is_empty() {
397      None
398    } else {
399      Some((*landing_page).clone())
400    },
401    keywords: (*keywords).clone(),
402    policies: vec![],
403    dcterm_types: vec![],
404  };
405
406  let card_style = "background-color: var(--pf-v6-c-page--BackgroundColor);";
407
408  let on_selected = use_callback(
409    selected_dcterm_types.setter(),
410    |selected_dcterm_types: Vec<(String, bool)>, selected_dcterm_types_setter| {
411      selected_dcterm_types_setter.set(selected_dcterm_types);
412    },
413  );
414
415  let offer_types = if let Some(dc_term_types) = &props.dcterm_types {
416    let selected_dcterm_types = (*selected_dcterm_types).clone();
417
418    let selectable_items = dc_term_types
419      .iter()
420      .zip(selected_dcterm_types.iter())
421      .map(|((_, label), (_, selected))| (label.clone(), *selected))
422      .collect::<Vec<(String, bool)>>();
423
424    html!(
425      <FormGroup label="Dataset Type(s)">
426        <MultiStateSelector {selectable_items} {on_selected} />
427      </FormGroup>
428    )
429  } else {
430    html!()
431  };
432
433  html!(
434    <Form {onsubmit}>
435      <Card>
436        <CardHeader>
437          <Title level={Level::H2} size={Size::XXXLarge}>{ "Provider Information" }</Title>
438        </CardHeader>
439        <CardBody>
440          <FormGroup label="Company Name">
441            <TextInput
442              required=true
443              value={(*company_name).to_string()}
444              onchange={onchange_company_name}
445              readonly={props.company_name.is_some()}
446            />
447          </FormGroup>
448          <FormGroup label="Company Logo URL">
449            <TextInput
450              required=true
451              value={(*company_logo_url).to_string()}
452              onchange={onchange_company_logo_url}
453              readonly={props.company_logo_url.is_some()}
454            />
455          </FormGroup>
456        </CardBody>
457      </Card>
458      <Card>
459        <CardHeader>
460          <CardHeaderMain>
461            <Title level={Level::H2} size={Size::XXXLarge}>{ "Dataset Description" }</Title>
462            <p>{ "This section defines the elements visible to the other participants." }</p>
463          </CardHeaderMain>
464        </CardHeader>
465        <CardBody>
466          <Flex>
467            <FlexItem
468              modifiers={[FlexModifier::Flex1.all(), FlexModifier::Align(Alignment::Start).all()]}
469            >
470              <FormGroup label="Title" required=true>
471                <TextInput required=true value={(*name).to_string()} onchange={onchange_name} />
472              </FormGroup>
473              <FormGroupValidated<TextInput>
474                label="Version"
475                required=true
476                validator={Validator::from(|ctx: ValidationContext<String>| {
477                  if ctx.initial {
478                    ValidationResult::default()
479                  } else if ctx.value.is_empty() {
480                    ValidationResult::error("Must not be empty")
481                  } else {
482                    if semver::Version::parse(&ctx.value).is_ok() {
483                      ValidationResult::ok()
484                    } else {
485                      ValidationResult::error("Not a semantic versioning format (MAJOR.MINOR.PATCH)")
486                    }
487                  }
488               })}
489              >
490                <TextInput
491                  required=true
492                  value={(*version).to_string()}
493                  onchange={onchange_version}
494                />
495              </FormGroupValidated<TextInput>>
496              <FormGroup label="Description">
497                <TextArea
498                  required=true
499                  value={(*description).to_string()}
500                  onchange={onchange_description}
501                />
502              </FormGroup>
503              <FormGroup label="Thumbnail URL">
504                <TextInput
505                  required=true
506                  value={(*thumbnail_url).to_string()}
507                  onchange={onchange_thumbnail_url}
508                />
509              </FormGroup>
510              <FormGroup label="Keywords">
511                <StringListEdit values={(*keywords).clone()} onchange={onchange_keywords} />
512              </FormGroup>
513              <FormGroup label="Landing Page">
514                <TextInput
515                  required=true
516                  value={(*landing_page).to_string()}
517                  onchange={onchange_landing_page}
518                />
519              </FormGroup>
520              { offer_types }
521            </FlexItem>
522            <FlexItem modifiers={[FlexModifier::Align(Alignment::Start).all()]}>
523              <DatasetCard {dataset} />
524            </FlexItem>
525          </Flex>
526        </CardBody>
527      </Card>
528      <Card style={card_style}>
529        <CardHeader>
530          <CardHeaderMain>
531            <Title level={Level::H2} size={Size::XXXLarge}>{ "Dataset Source" }</Title>
532            <p>
533              { "This section defines the private settings for communication and data exchange with other participants within the dataspace." }
534            </p>
535            <p>{ "None of the fields here are ever exposed externally." }</p>
536          </CardHeaderMain>
537        </CardHeader>
538        <CardBody>
539          <Split gutter=true>
540            <SplitItem fill=true>
541              <FormGroup label="Base URL" required=true>
542                <TextInput
543                  required=true
544                  value={(*base_url).to_string()}
545                  onchange={onchange_base_url}
546                  r#type={TextInputType::Url}
547                />
548              </FormGroup>
549              <FormGroup label="Content Type">
550                <TextInput value={(*content_type).to_string()} onchange={onchange_content_type} />
551              </FormGroup>
552              <FormGroup label="Proxy Path">
553                <Switch checked={*proxy_path} onchange={onchange_proxy_path} />
554              </FormGroup>
555              <FormGroup label="Proxy Query Parameters">
556                <Switch checked={*proxy_query_params} onchange={onchange_proxy_query_params} />
557              </FormGroup>
558              <FormGroup label="Proxy Method">
559                <Switch checked={*proxy_method} onchange={onchange_proxy_method} />
560              </FormGroup>
561              <FormGroup label="Proxy Body">
562                <Switch checked={*proxy_body} onchange={onchange_proxy_body} />
563              </FormGroup>
564            </SplitItem>
565            <SplitItem fill=true>
566              <FormGroup label="OpenID Client ID">
567                <TextInput value={(*client_id).clone()} onchange={onchange_client_id} />
568              </FormGroup>
569              <FormGroup label="OpenID Client Secret Key">
570                <TextInput
571                  value={(*client_secret_key).clone()}
572                  onchange={onchange_client_secret_key}
573                />
574              </FormGroup>
575              <FormGroup label="OpenID Token URL">
576                <TextInput value={(*token_url).clone()} onchange={onchange_token_url} />
577              </FormGroup>
578              <FormGroup label="OpenID Scopes">
579                <TextInput value={(*scopes).clone()} onchange={onchange_scopes} />
580              </FormGroup>
581            </SplitItem>
582          </Split>
583        </CardBody>
584      </Card>
585      <ActionGroup>
586        <Button
587          variant={ButtonVariant::Primary}
588          label="Submit"
589          r#type={ButtonType::Submit}
590          {disabled}
591        />
592        <Button variant={ButtonVariant::Secondary} label="Reset" r#type={ButtonType::Reset} />
593      </ActionGroup>
594    </Form>
595  )
596}