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 keywords: (*keywords).clone(),
397 policies: vec![],
398 dcterm_types: vec![],
399 };
400
401 let card_style = "background-color: var(--pf-v6-c-page--BackgroundColor);";
402
403 let on_selected = use_callback(
404 selected_dcterm_types.setter(),
405 |selected_dcterm_types: Vec<(String, bool)>, selected_dcterm_types_setter| {
406 selected_dcterm_types_setter.set(selected_dcterm_types);
407 },
408 );
409
410 let offer_types = if let Some(dc_term_types) = &props.dcterm_types {
411 let selected_dcterm_types = (*selected_dcterm_types).clone();
412
413 let selectable_items = dc_term_types
414 .iter()
415 .zip(selected_dcterm_types.iter())
416 .map(|((_, label), (_, selected))| (label.clone(), *selected))
417 .collect::<Vec<(String, bool)>>();
418
419 html!(
420 <FormGroup label="Dataset Type(s)">
421 <MultiStateSelector {selectable_items} {on_selected} />
422 </FormGroup>
423 )
424 } else {
425 html!()
426 };
427
428 html!(
429 <Form {onsubmit}>
430 <Card>
431 <CardHeader>
432 <Title level={Level::H2} size={Size::XXXLarge}>{ "Provider Information" }</Title>
433 </CardHeader>
434 <CardBody>
435 <FormGroup label="Company Name">
436 <TextInput
437 required=true
438 value={(*company_name).to_string()}
439 onchange={onchange_company_name}
440 readonly={props.company_name.is_some()}
441 />
442 </FormGroup>
443 <FormGroup label="Company Logo URL">
444 <TextInput
445 required=true
446 value={(*company_logo_url).to_string()}
447 onchange={onchange_company_logo_url}
448 readonly={props.company_logo_url.is_some()}
449 />
450 </FormGroup>
451 </CardBody>
452 </Card>
453 <Card>
454 <CardHeader>
455 <CardHeaderMain>
456 <Title level={Level::H2} size={Size::XXXLarge}>{ "Dataset Description" }</Title>
457 <p>{ "This section defines the elements visible to the other participants." }</p>
458 </CardHeaderMain>
459 </CardHeader>
460 <CardBody>
461 <Flex>
462 <FlexItem
463 modifiers={[FlexModifier::Flex1.all(), FlexModifier::Align(Alignment::Start).all()]}
464 >
465 <FormGroup label="Title" required=true>
466 <TextInput required=true value={(*name).to_string()} onchange={onchange_name} />
467 </FormGroup>
468 <FormGroupValidated<TextInput>
469 label="Version"
470 required=true
471 validator={Validator::from(|ctx: ValidationContext<String>| {
472 if ctx.initial {
473 ValidationResult::default()
474 } else if ctx.value.is_empty() {
475 ValidationResult::error("Must not be empty")
476 } else {
477 if semver::Version::parse(&ctx.value).is_ok() {
478 ValidationResult::ok()
479 } else {
480 ValidationResult::error("Not a semantic versioning format (MAJOR.MINOR.PATCH)")
481 }
482 }
483 })}
484 >
485 <TextInput
486 required=true
487 value={(*version).to_string()}
488 onchange={onchange_version}
489 />
490 </FormGroupValidated<TextInput>>
491 <FormGroup label="Description">
492 <TextArea
493 required=true
494 value={(*description).to_string()}
495 onchange={onchange_description}
496 />
497 </FormGroup>
498 <FormGroup label="Thumbnail URL">
499 <TextInput
500 required=true
501 value={(*thumbnail_url).to_string()}
502 onchange={onchange_thumbnail_url}
503 />
504 </FormGroup>
505 <FormGroup label="Keywords">
506 <StringListEdit values={(*keywords).clone()} onchange={onchange_keywords} />
507 </FormGroup>
508 <FormGroup label="Landing Page">
509 <TextInput
510 required=true
511 value={(*landing_page).to_string()}
512 onchange={onchange_landing_page}
513 />
514 </FormGroup>
515 { offer_types }
516 </FlexItem>
517 <FlexItem modifiers={[FlexModifier::Align(Alignment::Start).all()]}>
518 <DatasetCard {dataset} />
519 </FlexItem>
520 </Flex>
521 </CardBody>
522 </Card>
523 <Card style={card_style}>
524 <CardHeader>
525 <CardHeaderMain>
526 <Title level={Level::H2} size={Size::XXXLarge}>{ "Dataset Source" }</Title>
527 <p>
528 { "This section defines the private settings for communication and data exchange with other participants within the dataspace." }
529 </p>
530 <p>{ "None of the fields here are ever exposed externally." }</p>
531 </CardHeaderMain>
532 </CardHeader>
533 <CardBody>
534 <Split gutter=true>
535 <SplitItem fill=true>
536 <FormGroup label="Base URL" required=true>
537 <TextInput
538 required=true
539 value={(*base_url).to_string()}
540 onchange={onchange_base_url}
541 r#type={TextInputType::Url}
542 />
543 </FormGroup>
544 <FormGroup label="Content Type">
545 <TextInput value={(*content_type).to_string()} onchange={onchange_content_type} />
546 </FormGroup>
547 <FormGroup label="Proxy Path">
548 <Switch checked={*proxy_path} onchange={onchange_proxy_path} />
549 </FormGroup>
550 <FormGroup label="Proxy Query Parameters">
551 <Switch checked={*proxy_query_params} onchange={onchange_proxy_query_params} />
552 </FormGroup>
553 <FormGroup label="Proxy Method">
554 <Switch checked={*proxy_method} onchange={onchange_proxy_method} />
555 </FormGroup>
556 <FormGroup label="Proxy Body">
557 <Switch checked={*proxy_body} onchange={onchange_proxy_body} />
558 </FormGroup>
559 </SplitItem>
560 <SplitItem fill=true>
561 <FormGroup label="OpenID Client ID">
562 <TextInput value={(*client_id).clone()} onchange={onchange_client_id} />
563 </FormGroup>
564 <FormGroup label="OpenID Client Secret Key">
565 <TextInput
566 value={(*client_secret_key).clone()}
567 onchange={onchange_client_secret_key}
568 />
569 </FormGroup>
570 <FormGroup label="OpenID Token URL">
571 <TextInput value={(*token_url).clone()} onchange={onchange_token_url} />
572 </FormGroup>
573 <FormGroup label="OpenID Scopes">
574 <TextInput value={(*scopes).clone()} onchange={onchange_scopes} />
575 </FormGroup>
576 </SplitItem>
577 </Split>
578 </CardBody>
579 </Card>
580 <ActionGroup>
581 <Button
582 variant={ButtonVariant::Primary}
583 label="Submit"
584 r#type={ButtonType::Submit}
585 {disabled}
586 />
587 <Button variant={ButtonVariant::Secondary} label="Reset" r#type={ButtonType::Reset} />
588 </ActionGroup>
589 </Form>
590 )
591}