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 .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}