Skip to main content

edc_web_components/components/
create_asset.rs

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