Skip to main content

edc_web_components/components/
create_asset.rs

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