1use std::collections::HashMap;
7use wasm_bindgen::JsCast;
8use web_sys::HtmlInputElement;
9use yew::prelude::*;
10
11#[derive(Clone, Default, PartialEq)]
17pub struct InstanceData {
18 pub name: String,
19 pub description: String,
20 pub config: HashMap<String, String>,
21 pub is_default: bool,
22 pub capabilities: Capabilities,
23}
24
25#[derive(Clone, Default, PartialEq)]
27pub struct Capabilities {
28 pub network_access: bool,
29 pub filesystem_access: bool,
30 pub env_access: bool,
31 pub network_allowlist: Vec<String>,
32 pub filesystem_paths: Vec<String>,
33 pub env_vars: Vec<String>,
34}
35
36#[derive(Properties, PartialEq)]
41pub struct InstanceEditorProps {
42 pub skill: String,
44 #[prop_or_default]
46 pub instance: Option<InstanceData>,
47 pub on_save: Callback<InstanceData>,
49 pub on_cancel: Callback<()>,
51}
52
53#[function_component(InstanceEditor)]
55pub fn instance_editor(props: &InstanceEditorProps) -> Html {
56 let name = use_state(|| {
58 props
59 .instance
60 .as_ref()
61 .map(|i| i.name.clone())
62 .unwrap_or_default()
63 });
64 let description = use_state(|| {
65 props
66 .instance
67 .as_ref()
68 .map(|i| i.description.clone())
69 .unwrap_or_default()
70 });
71 let config = use_state(|| {
72 props
73 .instance
74 .as_ref()
75 .map(|i| i.config.clone())
76 .unwrap_or_default()
77 });
78 let is_default = use_state(|| {
79 props
80 .instance
81 .as_ref()
82 .map(|i| i.is_default)
83 .unwrap_or(false)
84 });
85 let capabilities = use_state(|| {
86 props
87 .instance
88 .as_ref()
89 .map(|i| i.capabilities.clone())
90 .unwrap_or_default()
91 });
92
93 let validation_errors = use_state(HashMap::<String, String>::new);
95 let is_testing = use_state(|| false);
96 let test_result = use_state(|| None::<Result<String, String>>);
97
98 let on_name_change = {
100 let name = name.clone();
101 let validation_errors = validation_errors.clone();
102 Callback::from(move |e: InputEvent| {
103 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
104 let value = input.value();
105 name.set(value.clone());
106
107 let mut errors = (*validation_errors).clone();
109 if value.is_empty() {
110 errors.insert("name".to_string(), "Instance name is required".to_string());
111 } else if !value.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
112 errors.insert(
113 "name".to_string(),
114 "Name can only contain letters, numbers, hyphens, and underscores".to_string(),
115 );
116 } else {
117 errors.remove("name");
118 }
119 validation_errors.set(errors);
120 })
121 };
122
123 let on_description_change = {
124 let description = description.clone();
125 Callback::from(move |e: InputEvent| {
126 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
127 description.set(input.value());
128 })
129 };
130
131 let on_config_change = {
132 let config = config.clone();
133 Callback::from(move |new_config: HashMap<String, String>| {
134 config.set(new_config);
135 })
136 };
137
138 let on_capabilities_change = {
139 let capabilities = capabilities.clone();
140 Callback::from(move |new_caps: Capabilities| {
141 capabilities.set(new_caps);
142 })
143 };
144
145 let on_default_change = {
146 let is_default = is_default.clone();
147 Callback::from(move |e: Event| {
148 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
149 is_default.set(input.checked());
150 })
151 };
152
153 let on_test = {
154 let is_testing = is_testing.clone();
155 let test_result = test_result.clone();
156 let config = config.clone();
157 Callback::from(move |_| {
158 is_testing.set(true);
159 test_result.set(None);
160
161 let config = (*config).clone();
163 let is_testing = is_testing.clone();
164 let test_result = test_result.clone();
165
166 let unresolved_count = config
168 .values()
169 .filter(|v| v.contains("${") && v.contains("}"))
170 .count();
171
172 gloo_timers::callback::Timeout::new(500, move || {
174 is_testing.set(false);
175 if unresolved_count == 0 {
176 test_result.set(Some(Ok("Configuration is valid".to_string())));
177 } else {
178 test_result.set(Some(Err(format!(
179 "Unresolved environment variables in {} config value(s)",
180 unresolved_count
181 ))));
182 }
183 })
184 .forget();
185 })
186 };
187
188 let on_save = {
189 let on_save = props.on_save.clone();
190 let name = name.clone();
191 let description = description.clone();
192 let config = config.clone();
193 let is_default = is_default.clone();
194 let capabilities = capabilities.clone();
195 let validation_errors = validation_errors.clone();
196 Callback::from(move |_| {
197 let mut errors = HashMap::new();
199 if (*name).is_empty() {
200 errors.insert("name".to_string(), "Instance name is required".to_string());
201 }
202
203 if !errors.is_empty() {
204 validation_errors.set(errors);
205 return;
206 }
207
208 let data = InstanceData {
209 name: (*name).clone(),
210 description: (*description).clone(),
211 config: (*config).clone(),
212 is_default: *is_default,
213 capabilities: (*capabilities).clone(),
214 };
215 on_save.emit(data);
216 })
217 };
218
219 let on_cancel = {
220 let on_cancel = props.on_cancel.clone();
221 Callback::from(move |_| on_cancel.emit(()))
222 };
223
224 let is_editing = props.instance.is_some();
225 let title = if is_editing {
226 "Edit Instance"
227 } else {
228 "Create Instance"
229 };
230
231 html! {
232 <div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col">
233 <div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
235 <h2 class="text-xl font-semibold text-gray-900 dark:text-white">
236 { title }
237 </h2>
238 <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
239 { format!("Configure instance for {}", props.skill) }
240 </p>
241 </div>
242
243 <div class="flex-1 overflow-y-auto p-6 space-y-6">
245 <div>
247 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
248 { "Instance Name" }
249 <span class="text-red-500 ml-1">{ "*" }</span>
250 </label>
251 <input
252 type="text"
253 value={(*name).clone()}
254 oninput={on_name_change}
255 placeholder="e.g., production, staging, dev"
256 disabled={is_editing}
257 class={classes!(
258 "w-full", "px-3", "py-2", "rounded-md", "border",
259 "bg-white", "dark:bg-gray-900",
260 "text-gray-900", "dark:text-white",
261 "focus:ring-2", "focus:ring-primary-500", "focus:border-primary-500",
262 if validation_errors.contains_key("name") {
263 "border-red-500"
264 } else {
265 "border-gray-300 dark:border-gray-600"
266 },
267 if is_editing { "bg-gray-100 dark:bg-gray-800 cursor-not-allowed" } else { "" }
268 )}
269 />
270 if let Some(error) = validation_errors.get("name") {
271 <p class="mt-1 text-sm text-red-500">{ error }</p>
272 }
273 </div>
274
275 <div>
277 <label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
278 { "Description" }
279 </label>
280 <input
281 type="text"
282 value={(*description).clone()}
283 oninput={on_description_change}
284 placeholder="Optional description for this instance"
285 class="w-full px-3 py-2 rounded-md border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
286 />
287 </div>
288
289 <div class="flex items-center gap-2">
291 <input
292 type="checkbox"
293 checked={*is_default}
294 onchange={on_default_change}
295 class="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
296 />
297 <label class="text-sm text-gray-700 dark:text-gray-300">
298 { "Set as default instance" }
299 </label>
300 </div>
301
302 <ConfigKeyValueEditor
304 pairs={(*config).clone()}
305 on_change={on_config_change}
306 />
307
308 <EnvironmentVariablePreview pairs={(*config).clone()} />
310
311 <CapabilitiesEditor
313 capabilities={(*capabilities).clone()}
314 on_change={on_capabilities_change}
315 />
316
317 if let Some(result) = &*test_result {
319 <div class={classes!(
320 "p-3", "rounded-md", "text-sm",
321 match result {
322 Ok(_) => "bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-300 border border-green-200 dark:border-green-800",
323 Err(_) => "bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-300 border border-red-200 dark:border-red-800",
324 }
325 )}>
326 { match result {
327 Ok(msg) => html! { <><span class="font-medium">{ "✓ " }</span>{ msg }</> },
328 Err(msg) => html! { <><span class="font-medium">{ "✗ " }</span>{ msg }</> },
329 }}
330 </div>
331 }
332 </div>
333
334 <div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900 flex items-center justify-between">
336 <button
337 onclick={on_cancel}
338 class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors"
339 >
340 { "Cancel" }
341 </button>
342 <div class="flex gap-2">
343 <button
344 onclick={on_test}
345 disabled={*is_testing}
346 class="px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-md transition-colors disabled:opacity-50"
347 >
348 if *is_testing {
349 { "Testing..." }
350 } else {
351 { "Test Configuration" }
352 }
353 </button>
354 <button
355 onclick={on_save}
356 class="px-4 py-2 text-sm font-medium text-white bg-primary-600 hover:bg-primary-700 rounded-md transition-colors"
357 >
358 { if is_editing { "Save Changes" } else { "Create Instance" } }
359 </button>
360 </div>
361 </div>
362 </div>
363 }
364}
365
366#[derive(Properties, PartialEq)]
371pub struct ConfigKeyValueEditorProps {
372 pub pairs: HashMap<String, String>,
373 pub on_change: Callback<HashMap<String, String>>,
374}
375
376#[function_component(ConfigKeyValueEditor)]
377pub fn config_key_value_editor(props: &ConfigKeyValueEditorProps) -> Html {
378 let pairs_vec: Vec<(String, String)> = props.pairs.clone().into_iter().collect();
380
381 let add_pair = {
382 let on_change = props.on_change.clone();
383 let pairs = props.pairs.clone();
384 Callback::from(move |_| {
385 let mut new_pairs = pairs.clone();
386 let mut key_num = 1;
388 let mut new_key = format!("KEY_{}", key_num);
389 while new_pairs.contains_key(&new_key) {
390 key_num += 1;
391 new_key = format!("KEY_{}", key_num);
392 }
393 new_pairs.insert(new_key, String::new());
394 on_change.emit(new_pairs);
395 })
396 };
397
398 html! {
399 <div class="space-y-3">
400 <div class="flex items-center justify-between">
401 <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
402 { "Configuration" }
403 </label>
404 <button
405 onclick={add_pair}
406 class="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium"
407 >
408 { "+ Add Key" }
409 </button>
410 </div>
411
412 if pairs_vec.is_empty() {
413 <div class="text-sm text-gray-500 dark:text-gray-400 italic py-4 text-center border border-dashed border-gray-300 dark:border-gray-600 rounded-md">
414 { "No configuration values. Click \"+ Add Key\" to add one." }
415 </div>
416 } else {
417 <div class="space-y-2">
418 <div class="grid grid-cols-12 gap-2 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
420 <div class="col-span-4">{ "Key" }</div>
421 <div class="col-span-7">{ "Value" }</div>
422 <div class="col-span-1"></div>
423 </div>
424
425 { for pairs_vec.iter().map(|(key, value)| {
426 let key = key.clone();
427 let value = value.clone();
428 let on_change = props.on_change.clone();
429 let pairs = props.pairs.clone();
430
431 let key_for_row = key.clone();
433 let key_for_key_change = key.clone();
434 let key_for_value_change = key.clone();
435 let key_for_delete = key.clone();
436
437 html! {
438 <ConfigKeyValueRow
439 key_name={key_for_row}
440 value={value}
441 on_key_change={Callback::from({
442 let on_change = on_change.clone();
443 let pairs = pairs.clone();
444 let old_key = key_for_key_change;
445 move |new_key: String| {
446 let mut new_pairs = pairs.clone();
447 if let Some(val) = new_pairs.remove(&old_key) {
448 new_pairs.insert(new_key, val);
449 }
450 on_change.emit(new_pairs);
451 }
452 })}
453 on_value_change={Callback::from({
454 let on_change = on_change.clone();
455 let pairs = pairs.clone();
456 let key = key_for_value_change;
457 move |new_value: String| {
458 let mut new_pairs = pairs.clone();
459 new_pairs.insert(key.clone(), new_value);
460 on_change.emit(new_pairs);
461 }
462 })}
463 on_delete={Callback::from({
464 let on_change = on_change.clone();
465 let pairs = pairs.clone();
466 let key = key_for_delete;
467 move |_| {
468 let mut new_pairs = pairs.clone();
469 new_pairs.remove(&key);
470 on_change.emit(new_pairs);
471 }
472 })}
473 />
474 }
475 }) }
476 </div>
477 }
478
479 <p class="text-xs text-gray-500 dark:text-gray-400">
480 { "Use " }
481 <code class="bg-gray-100 dark:bg-gray-700 px-1 rounded">{ "${VAR_NAME}" }</code>
482 { " to reference environment variables." }
483 </p>
484 </div>
485 }
486}
487
488#[derive(Properties, PartialEq)]
493struct ConfigKeyValueRowProps {
494 key_name: String,
495 value: String,
496 on_key_change: Callback<String>,
497 on_value_change: Callback<String>,
498 on_delete: Callback<()>,
499}
500
501#[function_component(ConfigKeyValueRow)]
502fn config_key_value_row(props: &ConfigKeyValueRowProps) -> Html {
503 let on_key_input = {
504 let on_key_change = props.on_key_change.clone();
505 Callback::from(move |e: InputEvent| {
506 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
507 on_key_change.emit(input.value());
508 })
509 };
510
511 let on_value_input = {
512 let on_value_change = props.on_value_change.clone();
513 Callback::from(move |e: InputEvent| {
514 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
515 on_value_change.emit(input.value());
516 })
517 };
518
519 let on_delete = {
520 let on_delete = props.on_delete.clone();
521 Callback::from(move |_| on_delete.emit(()))
522 };
523
524 let has_env_ref = props.value.contains("${") && props.value.contains("}");
526
527 html! {
528 <div class="grid grid-cols-12 gap-2 items-center">
529 <div class="col-span-4">
530 <input
531 type="text"
532 value={props.key_name.clone()}
533 oninput={on_key_input}
534 placeholder="KEY"
535 class="w-full px-2 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-white font-mono focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
536 />
537 </div>
538 <div class="col-span-7">
539 <div class="relative">
540 <input
541 type="text"
542 value={props.value.clone()}
543 oninput={on_value_input}
544 placeholder="value or ${ENV_VAR}"
545 class={classes!(
546 "w-full", "px-2", "py-1.5", "text-sm", "rounded", "border",
547 "bg-white", "dark:bg-gray-900", "text-gray-900", "dark:text-white",
548 "focus:ring-1", "focus:ring-primary-500", "focus:border-primary-500",
549 if has_env_ref {
550 "border-amber-400 dark:border-amber-500 pr-8"
551 } else {
552 "border-gray-300 dark:border-gray-600"
553 }
554 )}
555 />
556 if has_env_ref {
557 <span class="absolute right-2 top-1/2 -translate-y-1/2 text-amber-500" title="Contains environment variable reference">
558 { "$" }
559 </span>
560 }
561 </div>
562 </div>
563 <div class="col-span-1 flex justify-center">
564 <button
565 onclick={on_delete}
566 class="p-1 text-gray-400 hover:text-red-500 transition-colors"
567 title="Delete"
568 >
569 <svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
570 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
571 </svg>
572 </button>
573 </div>
574 </div>
575 }
576}
577
578#[derive(Properties, PartialEq)]
583pub struct EnvironmentVariablePreviewProps {
584 pub pairs: HashMap<String, String>,
585}
586
587#[function_component(EnvironmentVariablePreview)]
588pub fn environment_variable_preview(props: &EnvironmentVariablePreviewProps) -> Html {
589 let env_refs: Vec<(String, String, Option<String>)> = props
591 .pairs
592 .iter()
593 .filter_map(|(key, value)| {
594 let mut refs = Vec::new();
596 let mut remaining = value.as_str();
597 while let Some(start) = remaining.find("${") {
598 if let Some(end) = remaining[start..].find('}') {
599 let var_name = &remaining[start + 2..start + end];
600 refs.push(var_name.to_string());
601 remaining = &remaining[start + end + 1..];
602 } else {
603 break;
604 }
605 }
606 if refs.is_empty() {
607 None
608 } else {
609 Some((key.clone(), refs.join(", "), None)) }
611 })
612 .collect();
613
614 if env_refs.is_empty() {
615 return html! {};
616 }
617
618 html! {
619 <div class="border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900/20 rounded-md p-4">
620 <div class="flex items-start gap-2">
621 <svg class="w-5 h-5 text-amber-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
622 <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
623 </svg>
624 <div class="flex-1">
625 <h4 class="text-sm font-medium text-amber-800 dark:text-amber-200">
626 { "Environment Variable References" }
627 </h4>
628 <p class="text-xs text-amber-700 dark:text-amber-300 mt-1">
629 { "These will be resolved at runtime on the server." }
630 </p>
631 <div class="mt-3 space-y-1">
632 { for env_refs.iter().map(|(key, vars, _resolved)| {
633 html! {
634 <div class="flex items-center gap-2 text-sm">
635 <code class="text-amber-700 dark:text-amber-300 font-mono">{ key }</code>
636 <span class="text-amber-600 dark:text-amber-400">{ "→" }</span>
637 <code class="text-amber-800 dark:text-amber-200 font-mono">{ format!("${{{}}}", vars) }</code>
638 </div>
639 }
640 }) }
641 </div>
642 </div>
643 </div>
644 </div>
645 }
646}
647
648#[derive(Properties, PartialEq)]
653pub struct CapabilitiesEditorProps {
654 pub capabilities: Capabilities,
655 pub on_change: Callback<Capabilities>,
656}
657
658#[function_component(CapabilitiesEditor)]
659pub fn capabilities_editor(props: &CapabilitiesEditorProps) -> Html {
660 let on_network_change = {
661 let on_change = props.on_change.clone();
662 let caps = props.capabilities.clone();
663 Callback::from(move |e: Event| {
664 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
665 let mut new_caps = caps.clone();
666 new_caps.network_access = input.checked();
667 on_change.emit(new_caps);
668 })
669 };
670
671 let on_filesystem_change = {
672 let on_change = props.on_change.clone();
673 let caps = props.capabilities.clone();
674 Callback::from(move |e: Event| {
675 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
676 let mut new_caps = caps.clone();
677 new_caps.filesystem_access = input.checked();
678 on_change.emit(new_caps);
679 })
680 };
681
682 let on_env_change = {
683 let on_change = props.on_change.clone();
684 let caps = props.capabilities.clone();
685 Callback::from(move |e: Event| {
686 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
687 let mut new_caps = caps.clone();
688 new_caps.env_access = input.checked();
689 on_change.emit(new_caps);
690 })
691 };
692
693 let on_network_allowlist_change = {
694 let on_change = props.on_change.clone();
695 let caps = props.capabilities.clone();
696 Callback::from(move |e: InputEvent| {
697 let input: HtmlInputElement = e.target().unwrap().dyn_into().unwrap();
698 let mut new_caps = caps.clone();
699 new_caps.network_allowlist = input
700 .value()
701 .split(',')
702 .map(|s| s.trim().to_string())
703 .filter(|s| !s.is_empty())
704 .collect();
705 on_change.emit(new_caps);
706 })
707 };
708
709 html! {
710 <div class="space-y-4">
711 <label class="text-sm font-medium text-gray-700 dark:text-gray-300">
712 { "Capabilities" }
713 </label>
714
715 <div class="space-y-3 pl-1">
716 <div class="space-y-2">
718 <label class="flex items-center gap-2 cursor-pointer">
719 <input
720 type="checkbox"
721 checked={props.capabilities.network_access}
722 onchange={on_network_change}
723 class="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
724 />
725 <span class="text-sm text-gray-700 dark:text-gray-300">
726 { "Network Access" }
727 </span>
728 </label>
729
730 if props.capabilities.network_access {
731 <div class="ml-6">
732 <label class="block text-xs text-gray-500 dark:text-gray-400 mb-1">
733 { "Allowed hosts (comma-separated)" }
734 </label>
735 <input
736 type="text"
737 value={props.capabilities.network_allowlist.join(", ")}
738 oninput={on_network_allowlist_change}
739 placeholder="api.example.com, *.internal.net"
740 class="w-full px-2 py-1.5 text-sm rounded border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-900 text-gray-900 dark:text-white focus:ring-1 focus:ring-primary-500 focus:border-primary-500"
741 />
742 </div>
743 }
744 </div>
745
746 <label class="flex items-center gap-2 cursor-pointer">
748 <input
749 type="checkbox"
750 checked={props.capabilities.filesystem_access}
751 onchange={on_filesystem_change}
752 class="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
753 />
754 <span class="text-sm text-gray-700 dark:text-gray-300">
755 { "Filesystem Access" }
756 </span>
757 </label>
758
759 <label class="flex items-center gap-2 cursor-pointer">
761 <input
762 type="checkbox"
763 checked={props.capabilities.env_access}
764 onchange={on_env_change}
765 class="w-4 h-4 text-primary-600 bg-gray-100 border-gray-300 rounded focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"
766 />
767 <span class="text-sm text-gray-700 dark:text-gray-300">
768 { "Environment Variables Access" }
769 </span>
770 </label>
771 </div>
772
773 <p class="text-xs text-gray-500 dark:text-gray-400">
774 { "Capabilities control what resources the skill can access at runtime." }
775 </p>
776 </div>
777 }
778}
779
780#[derive(Properties, PartialEq)]
785pub struct InstanceEditorModalProps {
786 pub open: bool,
788 pub skill: String,
790 #[prop_or_default]
792 pub instance: Option<InstanceData>,
793 pub on_save: Callback<InstanceData>,
795 pub on_close: Callback<()>,
797}
798
799#[function_component(InstanceEditorModal)]
800pub fn instance_editor_modal(props: &InstanceEditorModalProps) -> Html {
801 if !props.open {
802 return html! {};
803 }
804
805 let on_backdrop_click = {
806 let on_close = props.on_close.clone();
807 Callback::from(move |_| on_close.emit(()))
808 };
809
810 let on_content_click = Callback::from(|e: MouseEvent| {
811 e.stop_propagation();
812 });
813
814 html! {
815 <div
816 class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm animate-fade-in"
817 onclick={on_backdrop_click}
818 >
819 <div onclick={on_content_click} class="animate-scale-in">
820 <InstanceEditor
821 skill={props.skill.clone()}
822 instance={props.instance.clone()}
823 on_save={props.on_save.clone()}
824 on_cancel={props.on_close.clone()}
825 />
826 </div>
827 </div>
828 }
829}