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