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