edc_web_components/components/
create_asset.rs1use 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 .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}