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