@orsetra/shared-ui 1.3.16 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/ui/alert-dialog.tsx +4 -4
- package/components/ui/code-editor.tsx +42 -0
- package/components/ui/confirmation-dialog.tsx +1 -0
- package/components/ui/environment-picker-dialog.tsx +95 -0
- package/components/ui/index.ts +9 -0
- package/components/ui/k8s-objects-code.tsx +69 -0
- package/components/ui/kv-dynamic-input.tsx +116 -0
- package/components/ui/kv-input.tsx +1 -1
- package/components/ui/secret-context.tsx +36 -0
- package/components/ui/secret-input.tsx +69 -0
- package/components/ui/secrets-input.tsx +76 -0
- package/components/ui/select-input.tsx +44 -0
- package/components/ui/strings-input.tsx +4 -4
- package/components/ui/structs-input.tsx +331 -0
- package/components/ui/tooltip.tsx +1 -1
- package/package.json +1 -1
|
@@ -18,7 +18,7 @@ const AlertDialogOverlay = React.forwardRef<
|
|
|
18
18
|
>(({ className, ...props }, ref) => (
|
|
19
19
|
<AlertDialogPrimitive.Overlay
|
|
20
20
|
className={cn(
|
|
21
|
-
"fixed inset-0 z-50 bg-
|
|
21
|
+
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
|
22
22
|
className
|
|
23
23
|
)}
|
|
24
24
|
{...props}
|
|
@@ -36,7 +36,7 @@ const AlertDialogContent = React.forwardRef<
|
|
|
36
36
|
<AlertDialogPrimitive.Content
|
|
37
37
|
ref={ref}
|
|
38
38
|
className={cn(
|
|
39
|
-
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-ibm-gray-20 bg-white p-6 shadow-
|
|
39
|
+
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-ibm-gray-20 bg-white p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%]",
|
|
40
40
|
className
|
|
41
41
|
)}
|
|
42
42
|
{...props}
|
|
@@ -103,7 +103,7 @@ const AlertDialogAction = React.forwardRef<
|
|
|
103
103
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
|
|
104
104
|
>(({ className, ...props }, ref) => (
|
|
105
105
|
<AlertDialogPrimitive.Action asChild>
|
|
106
|
-
<Button ref={ref} className={className} {...props} />
|
|
106
|
+
<Button ref={ref} className={cn("rounded-none", className)} {...props} />
|
|
107
107
|
</AlertDialogPrimitive.Action>
|
|
108
108
|
))
|
|
109
109
|
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
|
|
@@ -113,7 +113,7 @@ const AlertDialogCancel = React.forwardRef<
|
|
|
113
113
|
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
|
|
114
114
|
>(({ className, ...props }, ref) => (
|
|
115
115
|
<AlertDialogPrimitive.Cancel asChild>
|
|
116
|
-
<Button ref={ref} variant="secondary" className={cn("mt-2 sm:mt-0", className)} {...props} />
|
|
116
|
+
<Button ref={ref} variant="secondary" className={cn("rounded-none mt-2 sm:mt-0", className)} {...props} />
|
|
117
117
|
</AlertDialogPrimitive.Cancel>
|
|
118
118
|
))
|
|
119
119
|
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React from "react"
|
|
4
|
+
|
|
5
|
+
interface CodeEditorProps {
|
|
6
|
+
id?: string
|
|
7
|
+
value?: string
|
|
8
|
+
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement>) => void
|
|
9
|
+
onBlur?: (e: React.FocusEvent<HTMLTextAreaElement>) => void
|
|
10
|
+
placeholder?: string
|
|
11
|
+
disabled?: boolean
|
|
12
|
+
required?: boolean
|
|
13
|
+
className?: string
|
|
14
|
+
rows?: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CodeEditor({
|
|
18
|
+
id,
|
|
19
|
+
value,
|
|
20
|
+
onChange,
|
|
21
|
+
onBlur,
|
|
22
|
+
placeholder,
|
|
23
|
+
disabled,
|
|
24
|
+
required,
|
|
25
|
+
className = "",
|
|
26
|
+
rows = 14,
|
|
27
|
+
}: CodeEditorProps) {
|
|
28
|
+
return (
|
|
29
|
+
<textarea
|
|
30
|
+
id={id}
|
|
31
|
+
value={value ?? ""}
|
|
32
|
+
onChange={onChange}
|
|
33
|
+
onBlur={onBlur}
|
|
34
|
+
placeholder={placeholder}
|
|
35
|
+
disabled={disabled}
|
|
36
|
+
required={required}
|
|
37
|
+
rows={rows}
|
|
38
|
+
spellCheck={false}
|
|
39
|
+
className={`w-full font-mono text-xs text-ibm-gray-80 bg-ibm-gray-10 border border-ibm-gray-30 px-3 py-2 resize-y focus:outline-none focus:ring-1 focus:ring-ibm-blue-60 placeholder:text-ibm-gray-40 disabled:opacity-50 ${className}`}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./dialog"
|
|
5
|
+
import { Loader2, Globe } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
interface Environment {
|
|
8
|
+
name: string
|
|
9
|
+
alias?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface EnvironmentPickerDialogProps {
|
|
13
|
+
open: boolean
|
|
14
|
+
onOpenChange: (open: boolean) => void
|
|
15
|
+
projectName: string
|
|
16
|
+
fetchEnvironments: (projectName: string) => Promise<{ envs: Environment[] }>
|
|
17
|
+
onSelect: (envName: string) => void
|
|
18
|
+
excludeEnvs?: string[]
|
|
19
|
+
title?: string
|
|
20
|
+
description?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function EnvironmentPickerDialog({
|
|
24
|
+
open,
|
|
25
|
+
onOpenChange,
|
|
26
|
+
projectName,
|
|
27
|
+
fetchEnvironments,
|
|
28
|
+
onSelect,
|
|
29
|
+
excludeEnvs = [],
|
|
30
|
+
title = "Select an environment",
|
|
31
|
+
description,
|
|
32
|
+
}: EnvironmentPickerDialogProps) {
|
|
33
|
+
const [environments, setEnvironments] = useState<Environment[]>([])
|
|
34
|
+
const [loading, setLoading] = useState(true)
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open) loadEnvironments()
|
|
38
|
+
}, [open])
|
|
39
|
+
|
|
40
|
+
const loadEnvironments = async () => {
|
|
41
|
+
setLoading(true)
|
|
42
|
+
try {
|
|
43
|
+
const response = await fetchEnvironments(projectName)
|
|
44
|
+
const all = response.envs || []
|
|
45
|
+
setEnvironments(excludeEnvs.length ? all.filter((e) => !excludeEnvs.includes(e.name)) : all)
|
|
46
|
+
} catch {
|
|
47
|
+
// silent
|
|
48
|
+
} finally {
|
|
49
|
+
setLoading(false)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
55
|
+
<DialogContent className="rounded-none max-w-lg">
|
|
56
|
+
<DialogHeader>
|
|
57
|
+
<DialogTitle>{title}</DialogTitle>
|
|
58
|
+
{description && <DialogDescription>{description}</DialogDescription>}
|
|
59
|
+
</DialogHeader>
|
|
60
|
+
|
|
61
|
+
<div className="mt-4">
|
|
62
|
+
{loading ? (
|
|
63
|
+
<div className="flex items-center justify-center py-8">
|
|
64
|
+
<Loader2 className="h-5 w-5 animate-spin text-ibm-blue-60" />
|
|
65
|
+
<span className="ml-2 text-sm text-ibm-gray-70">Loading environments...</span>
|
|
66
|
+
</div>
|
|
67
|
+
) : environments.length === 0 ? (
|
|
68
|
+
<div className="text-center py-8 text-sm text-ibm-gray-60">
|
|
69
|
+
No environments found
|
|
70
|
+
</div>
|
|
71
|
+
) : (
|
|
72
|
+
<div className="border border-ibm-gray-20 max-h-60 overflow-y-auto">
|
|
73
|
+
{environments.map((env, index) => (
|
|
74
|
+
<button
|
|
75
|
+
key={env.name}
|
|
76
|
+
type="button"
|
|
77
|
+
className={`w-full text-left px-4 py-3 hover:bg-ibm-gray-10 transition-colors flex items-center gap-3 ${index !== environments.length - 1 ? "border-b border-ibm-gray-20" : ""}`}
|
|
78
|
+
onClick={() => {
|
|
79
|
+
onSelect(env.name)
|
|
80
|
+
onOpenChange(false)
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
<Globe className="h-4 w-4 text-ibm-gray-70 flex-shrink-0" />
|
|
84
|
+
<span className="text-sm font-medium text-ibm-gray-100">
|
|
85
|
+
{env.alias || env.name}
|
|
86
|
+
</span>
|
|
87
|
+
</button>
|
|
88
|
+
))}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</DialogContent>
|
|
93
|
+
</Dialog>
|
|
94
|
+
)
|
|
95
|
+
}
|
package/components/ui/index.ts
CHANGED
|
@@ -65,3 +65,12 @@ export { Toggle } from './toggle'
|
|
|
65
65
|
export { StringsInput } from './strings-input'
|
|
66
66
|
export { KVInput } from './kv-input'
|
|
67
67
|
export { NumbersInput } from './numbers-input'
|
|
68
|
+
export { KVDynamicInput } from './kv-dynamic-input'
|
|
69
|
+
export { SelectInput } from './select-input'
|
|
70
|
+
export { SecretContextProvider, useSecretContext, type SecretEntry } from './secret-context'
|
|
71
|
+
export { SecretInput } from './secret-input'
|
|
72
|
+
export { SecretsInput } from './secrets-input'
|
|
73
|
+
export { StructsInput } from './structs-input'
|
|
74
|
+
export { CodeEditor } from './code-editor'
|
|
75
|
+
export { K8sObjectsCode } from './k8s-objects-code'
|
|
76
|
+
export { EnvironmentPickerDialog, type EnvironmentPickerDialogProps } from './environment-picker-dialog'
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from "react"
|
|
4
|
+
import * as yaml from "js-yaml"
|
|
5
|
+
|
|
6
|
+
interface K8sObjectsCodeProps {
|
|
7
|
+
id?: string
|
|
8
|
+
value?: any[] | string
|
|
9
|
+
onChange?: (val: any[]) => void
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
required?: boolean
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function toYamlStr(val: any[] | string | undefined): string {
|
|
16
|
+
if (!val) return ""
|
|
17
|
+
if (typeof val === "string") return val
|
|
18
|
+
try {
|
|
19
|
+
return yaml.dump(val, { lineWidth: -1, noRefs: true })
|
|
20
|
+
} catch {
|
|
21
|
+
return JSON.stringify(val, null, 2)
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function K8sObjectsCode({ id, value, onChange, disabled, required, className = "" }: K8sObjectsCodeProps) {
|
|
26
|
+
const [text, setText] = useState(() => toYamlStr(value))
|
|
27
|
+
const [parseError, setParseError] = useState<string | null>(null)
|
|
28
|
+
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
setText(toYamlStr(value))
|
|
31
|
+
}, [value])
|
|
32
|
+
|
|
33
|
+
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
34
|
+
const raw = e.target.value
|
|
35
|
+
setText(raw)
|
|
36
|
+
if (!raw.trim()) {
|
|
37
|
+
setParseError(null)
|
|
38
|
+
onChange?.([])
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
const docs = yaml.loadAll(raw) as any[]
|
|
43
|
+
const arr = docs.flat().filter(Boolean)
|
|
44
|
+
setParseError(null)
|
|
45
|
+
onChange?.(arr)
|
|
46
|
+
} catch (err: any) {
|
|
47
|
+
setParseError(err.message?.split("\n")[0] ?? "Invalid YAML")
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<div className="space-y-1">
|
|
53
|
+
<textarea
|
|
54
|
+
id={id}
|
|
55
|
+
value={text}
|
|
56
|
+
onChange={handleChange}
|
|
57
|
+
disabled={disabled}
|
|
58
|
+
required={required}
|
|
59
|
+
rows={14}
|
|
60
|
+
spellCheck={false}
|
|
61
|
+
placeholder={"- from:\n uri: timer:tick\n steps:\n - to: log:hello"}
|
|
62
|
+
className={`w-full font-mono text-xs text-ibm-gray-80 bg-ibm-gray-10 border px-3 py-2 resize-y focus:outline-none focus:ring-1 focus:ring-ibm-blue-60 disabled:opacity-50 ${parseError ? "border-red-500" : "border-ibm-gray-30"} ${className}`}
|
|
63
|
+
/>
|
|
64
|
+
{parseError && (
|
|
65
|
+
<p className="text-xs text-red-500 font-mono break-all">{parseError}</p>
|
|
66
|
+
)}
|
|
67
|
+
</div>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react"
|
|
4
|
+
import { Input } from "./input"
|
|
5
|
+
import { Plus, X } from "lucide-react"
|
|
6
|
+
|
|
7
|
+
interface KVDynamicInputProps {
|
|
8
|
+
value?: Record<string, string>
|
|
9
|
+
onChange?: (value: Record<string, string>) => void
|
|
10
|
+
disabled?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function KVDynamicInput({ value = {}, onChange, disabled = false }: KVDynamicInputProps) {
|
|
14
|
+
const [pairs, setPairs] = useState<Record<string, string>>(value)
|
|
15
|
+
const [showAdd, setShowAdd] = useState(false)
|
|
16
|
+
const [draftKey, setDraftKey] = useState("")
|
|
17
|
+
const [draftValue, setDraftValue] = useState("")
|
|
18
|
+
const keyRef = useRef<HTMLInputElement>(null)
|
|
19
|
+
const skipSync = useRef(false)
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (skipSync.current) { skipSync.current = false; return }
|
|
23
|
+
setPairs(value ?? {})
|
|
24
|
+
}, [value])
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (showAdd) keyRef.current?.focus()
|
|
28
|
+
}, [showAdd])
|
|
29
|
+
|
|
30
|
+
const emit = (next: Record<string, string>) => {
|
|
31
|
+
skipSync.current = true
|
|
32
|
+
onChange?.(next)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleAdd = () => {
|
|
36
|
+
const key = draftKey.trim()
|
|
37
|
+
if (!key) return
|
|
38
|
+
const next = { ...pairs, [key]: draftValue.trim() }
|
|
39
|
+
setPairs(next)
|
|
40
|
+
emit(next)
|
|
41
|
+
setDraftKey("")
|
|
42
|
+
setDraftValue("")
|
|
43
|
+
setShowAdd(false)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const handleRemove = (key: string) => {
|
|
47
|
+
const next = { ...pairs }
|
|
48
|
+
delete next[key]
|
|
49
|
+
setPairs(next)
|
|
50
|
+
emit(next)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
54
|
+
if (e.key === "Enter") { e.preventDefault(); handleAdd() }
|
|
55
|
+
if (e.key === "Escape") { setShowAdd(false); setDraftKey(""); setDraftValue("") }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<div className="space-y-2">
|
|
60
|
+
{/* Chips row */}
|
|
61
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
62
|
+
{Object.entries(pairs).map(([key, val]) => (
|
|
63
|
+
<span key={key} className="inline-flex items-center gap-1 px-2 py-1 bg-ibm-gray-10 border border-ibm-gray-20 text-xs font-mono">
|
|
64
|
+
<span className="text-ibm-gray-100">{key}</span>
|
|
65
|
+
{val && <><span className="text-ibm-gray-40">=</span><span className="text-ibm-blue-70">{val}</span></>}
|
|
66
|
+
{!disabled && (
|
|
67
|
+
<button type="button" onClick={() => handleRemove(key)} className="ml-0.5 text-ibm-gray-50 hover:text-red-600 transition-colors">
|
|
68
|
+
<X className="h-3 w-3" />
|
|
69
|
+
</button>
|
|
70
|
+
)}
|
|
71
|
+
</span>
|
|
72
|
+
))}
|
|
73
|
+
|
|
74
|
+
{/* Add trigger */}
|
|
75
|
+
{!showAdd && !disabled && (
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={() => setShowAdd(true)}
|
|
79
|
+
className="inline-flex items-center justify-center h-6 w-6 border border-dashed border-ibm-gray-30 text-ibm-gray-50 hover:border-ibm-blue-60 hover:text-ibm-blue-60 transition-colors"
|
|
80
|
+
>
|
|
81
|
+
<Plus className="h-3.5 w-3.5" />
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
|
|
86
|
+
{/* Inline add row */}
|
|
87
|
+
{showAdd && (
|
|
88
|
+
<div className="flex items-center gap-2">
|
|
89
|
+
<Input
|
|
90
|
+
ref={keyRef}
|
|
91
|
+
placeholder="Key"
|
|
92
|
+
value={draftKey}
|
|
93
|
+
onChange={(e) => setDraftKey(e.target.value)}
|
|
94
|
+
onKeyDown={handleKeyDown}
|
|
95
|
+
className="rounded-none flex-1 h-10"
|
|
96
|
+
/>
|
|
97
|
+
<Input
|
|
98
|
+
placeholder="Value"
|
|
99
|
+
value={draftValue}
|
|
100
|
+
onChange={(e) => setDraftValue(e.target.value)}
|
|
101
|
+
onKeyDown={handleKeyDown}
|
|
102
|
+
className="rounded-none flex-1 h-10"
|
|
103
|
+
/>
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
onClick={handleAdd}
|
|
107
|
+
disabled={!draftKey.trim()}
|
|
108
|
+
className="h-10 w-9 flex items-center justify-center border border-ibm-gray-30 bg-white text-ibm-gray-60 hover:bg-ibm-blue-10 hover:border-ibm-blue-60 hover:text-ibm-blue-60 disabled:opacity-40 disabled:pointer-events-none transition-colors flex-shrink-0"
|
|
109
|
+
>
|
|
110
|
+
<Plus className="h-4 w-4" />
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
)}
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react"
|
|
4
|
+
import type { ReactNode } from "react"
|
|
5
|
+
|
|
6
|
+
export interface SecretEntry {
|
|
7
|
+
name: string
|
|
8
|
+
description?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SecretContextValue {
|
|
12
|
+
templateId?: string
|
|
13
|
+
fetchSecrets: (templateId?: string) => Promise<SecretEntry[]>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const SecretContext = createContext<SecretContextValue | null>(null)
|
|
17
|
+
|
|
18
|
+
export function SecretContextProvider({
|
|
19
|
+
children,
|
|
20
|
+
templateId,
|
|
21
|
+
fetchSecrets,
|
|
22
|
+
}: {
|
|
23
|
+
children: ReactNode
|
|
24
|
+
templateId?: string
|
|
25
|
+
fetchSecrets: (templateId?: string) => Promise<SecretEntry[]>
|
|
26
|
+
}) {
|
|
27
|
+
return (
|
|
28
|
+
<SecretContext.Provider value={{ templateId, fetchSecrets }}>
|
|
29
|
+
{children}
|
|
30
|
+
</SecretContext.Provider>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useSecretContext(): SecretContextValue | null {
|
|
35
|
+
return useContext(SecretContext)
|
|
36
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { Loader2 } from "lucide-react"
|
|
5
|
+
import { SelectInput } from "./select-input"
|
|
6
|
+
import { useSecretContext } from "./secret-context"
|
|
7
|
+
import type { SecretEntry } from "./secret-context"
|
|
8
|
+
|
|
9
|
+
interface SecretInputProps {
|
|
10
|
+
id?: string
|
|
11
|
+
type?: string
|
|
12
|
+
value?: string
|
|
13
|
+
onChange?: (value: string) => void
|
|
14
|
+
disabled?: boolean
|
|
15
|
+
className?: string
|
|
16
|
+
placeholder?: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function SecretInput({
|
|
20
|
+
id,
|
|
21
|
+
type,
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
disabled,
|
|
25
|
+
className,
|
|
26
|
+
placeholder = "Select a secret",
|
|
27
|
+
}: SecretInputProps) {
|
|
28
|
+
const ctx = useSecretContext()
|
|
29
|
+
const [secrets, setSecrets] = useState<SecretEntry[]>([])
|
|
30
|
+
const [loading, setLoading] = useState(false)
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!ctx) return
|
|
34
|
+
setLoading(true)
|
|
35
|
+
ctx.fetchSecrets(type === "configs" ? ctx.templateId : type)
|
|
36
|
+
.then(setSecrets)
|
|
37
|
+
.catch(() => setSecrets([]))
|
|
38
|
+
.finally(() => setLoading(false))
|
|
39
|
+
}, [ctx?.templateId])
|
|
40
|
+
|
|
41
|
+
if (loading) {
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex items-center gap-2 h-10 px-3 border border-ibm-gray-30 bg-white text-sm text-ibm-gray-60">
|
|
44
|
+
<Loader2 className="h-4 w-4 animate-spin flex-shrink-0" />
|
|
45
|
+
Loading secrets…
|
|
46
|
+
</div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!ctx) {
|
|
51
|
+
return (
|
|
52
|
+
<div className="flex items-center h-10 px-3 border border-ibm-gray-30 bg-ibm-gray-10 text-sm text-ibm-gray-50">
|
|
53
|
+
No secret context provided
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<SelectInput
|
|
60
|
+
id={id}
|
|
61
|
+
value={value}
|
|
62
|
+
onChange={onChange}
|
|
63
|
+
disabled={disabled}
|
|
64
|
+
className={className}
|
|
65
|
+
placeholder={placeholder}
|
|
66
|
+
options={secrets.map((s) => ({ label: s.description || s.name, value: s.name }))}
|
|
67
|
+
/>
|
|
68
|
+
)
|
|
69
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect } from "react"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { X, Plus } from "lucide-react"
|
|
6
|
+
import { SecretInput } from "./secret-input"
|
|
7
|
+
|
|
8
|
+
interface SecretsInputProps {
|
|
9
|
+
id?: string
|
|
10
|
+
value?: string[]
|
|
11
|
+
onChange?: (value: string[]) => void
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
[key: string]: any
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function SecretsInput({ id, value = [], onChange, disabled = false, ...rest }: SecretsInputProps) {
|
|
17
|
+
const [items, setItems] = useState<string[]>(value.filter(Boolean))
|
|
18
|
+
const [draft, setDraft] = useState("")
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const filtered = value.filter(Boolean)
|
|
22
|
+
if (JSON.stringify(filtered) !== JSON.stringify(items)) {
|
|
23
|
+
setItems(filtered)
|
|
24
|
+
}
|
|
25
|
+
}, [value])
|
|
26
|
+
|
|
27
|
+
const handleAdd = () => {
|
|
28
|
+
if (!draft || items.includes(draft)) return
|
|
29
|
+
const next = [...items, draft]
|
|
30
|
+
setItems(next)
|
|
31
|
+
onChange?.(next)
|
|
32
|
+
setDraft("")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const handleRemove = (index: number) => {
|
|
36
|
+
const next = items.filter((_, i) => i !== index)
|
|
37
|
+
setItems(next)
|
|
38
|
+
onChange?.(next)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="space-y-2">
|
|
43
|
+
{items.map((item, index) => (
|
|
44
|
+
<div key={index} className="flex items-center gap-2">
|
|
45
|
+
<span className="flex-1 text-sm px-3 h-10 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate font-mono">
|
|
46
|
+
{item}
|
|
47
|
+
</span>
|
|
48
|
+
<Button
|
|
49
|
+
type="button"
|
|
50
|
+
variant="secondary"
|
|
51
|
+
onClick={() => handleRemove(index)}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
className="rounded-none h-10 w-10 p-0 shrink-0"
|
|
54
|
+
>
|
|
55
|
+
<X className="h-3.5 w-3.5" />
|
|
56
|
+
</Button>
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
|
|
60
|
+
<div className="flex gap-2">
|
|
61
|
+
<div className="flex-1">
|
|
62
|
+
<SecretInput value={draft} onChange={setDraft} disabled={disabled} type={id} />
|
|
63
|
+
</div>
|
|
64
|
+
<Button
|
|
65
|
+
type="button"
|
|
66
|
+
variant="secondary"
|
|
67
|
+
onClick={handleAdd}
|
|
68
|
+
disabled={disabled || !draft}
|
|
69
|
+
className="rounded-none h-10 w-10 p-0 shrink-0"
|
|
70
|
+
>
|
|
71
|
+
<Plus className="h-3.5 w-3.5" />
|
|
72
|
+
</Button>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
|
4
|
+
|
|
5
|
+
interface SelectOption {
|
|
6
|
+
label: string
|
|
7
|
+
value: any
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface SelectInputProps {
|
|
11
|
+
id?: string
|
|
12
|
+
value?: string
|
|
13
|
+
onChange?: (value: string) => void
|
|
14
|
+
options?: SelectOption[]
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
required?: boolean
|
|
17
|
+
className?: string
|
|
18
|
+
placeholder?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function SelectInput({
|
|
22
|
+
id,
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
options = [],
|
|
26
|
+
disabled = false,
|
|
27
|
+
className,
|
|
28
|
+
placeholder = "Select an option",
|
|
29
|
+
}: SelectInputProps) {
|
|
30
|
+
return (
|
|
31
|
+
<Select value={value || undefined} onValueChange={onChange} disabled={disabled}>
|
|
32
|
+
<SelectTrigger id={id} className={className}>
|
|
33
|
+
<SelectValue placeholder={placeholder} />
|
|
34
|
+
</SelectTrigger>
|
|
35
|
+
<SelectContent>
|
|
36
|
+
{options.map((opt) => (
|
|
37
|
+
<SelectItem key={String(opt.value)} value={String(opt.value)}>
|
|
38
|
+
{opt.label}
|
|
39
|
+
</SelectItem>
|
|
40
|
+
))}
|
|
41
|
+
</SelectContent>
|
|
42
|
+
</Select>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
@@ -54,7 +54,7 @@ export function StringsInput({
|
|
|
54
54
|
<div className="space-y-2">
|
|
55
55
|
{items.map((item, index) => (
|
|
56
56
|
<div key={index} className="flex items-center gap-2">
|
|
57
|
-
<span className="flex-1 text-sm px-3 h-
|
|
57
|
+
<span className="flex-1 text-sm px-3 h-10 flex items-center border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate">
|
|
58
58
|
{item}
|
|
59
59
|
</span>
|
|
60
60
|
<Button
|
|
@@ -62,7 +62,7 @@ export function StringsInput({
|
|
|
62
62
|
variant="secondary"
|
|
63
63
|
onClick={() => handleRemove(index)}
|
|
64
64
|
disabled={disabled}
|
|
65
|
-
className="rounded-none h-
|
|
65
|
+
className="rounded-none h-10 w-10 p-0 shrink-0"
|
|
66
66
|
>
|
|
67
67
|
<X className="h-3.5 w-3.5" />
|
|
68
68
|
</Button>
|
|
@@ -75,14 +75,14 @@ export function StringsInput({
|
|
|
75
75
|
onKeyDown={handleKeyDown}
|
|
76
76
|
disabled={disabled}
|
|
77
77
|
placeholder={placeholder}
|
|
78
|
-
className="rounded-none flex-1 h-
|
|
78
|
+
className="rounded-none flex-1 h-10 text-sm"
|
|
79
79
|
/>
|
|
80
80
|
<Button
|
|
81
81
|
type="button"
|
|
82
82
|
variant="secondary"
|
|
83
83
|
onClick={handleAdd}
|
|
84
84
|
disabled={disabled || !inputValue.trim()}
|
|
85
|
-
className="rounded-none h-
|
|
85
|
+
className="rounded-none h-10 w-10 p-0 shrink-0"
|
|
86
86
|
>
|
|
87
87
|
<Plus className="h-3.5 w-3.5" />
|
|
88
88
|
</Button>
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect } from "react"
|
|
4
|
+
import { Button } from "./button"
|
|
5
|
+
import { Label } from "./label"
|
|
6
|
+
import { Input } from "./input"
|
|
7
|
+
import { Textarea } from "./textarea"
|
|
8
|
+
import { Switch } from "./switch"
|
|
9
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./select"
|
|
10
|
+
import { StringsInput } from "./strings-input"
|
|
11
|
+
import { KVInput } from "./kv-input"
|
|
12
|
+
import { NumbersInput } from "./numbers-input"
|
|
13
|
+
import { Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react"
|
|
14
|
+
|
|
15
|
+
interface UIParamValidate {
|
|
16
|
+
required?: boolean
|
|
17
|
+
defaultValue?: any
|
|
18
|
+
options?: Array<{ label: string; value: any }>
|
|
19
|
+
immutable?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface UIParam {
|
|
23
|
+
jsonKey: string
|
|
24
|
+
label?: string
|
|
25
|
+
uiType: string
|
|
26
|
+
validate?: UIParamValidate
|
|
27
|
+
subParameters?: UIParam[]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface StructsInputProps {
|
|
31
|
+
id?: string
|
|
32
|
+
value?: any[]
|
|
33
|
+
onChange?: (value: any[]) => void
|
|
34
|
+
disabled?: boolean
|
|
35
|
+
subParameters?: UIParam[]
|
|
36
|
+
label?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface StructItemProps {
|
|
40
|
+
index: number
|
|
41
|
+
value: any
|
|
42
|
+
onChange: (value: any) => void
|
|
43
|
+
onRemove: () => void
|
|
44
|
+
subParameters: UIParam[]
|
|
45
|
+
disabled?: boolean
|
|
46
|
+
isExpanded: boolean
|
|
47
|
+
onToggleExpand: () => void
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function StructItem({ index, value, onChange, onRemove, subParameters, disabled, isExpanded, onToggleExpand }: StructItemProps) {
|
|
51
|
+
const handleFieldChange = (key: string, fieldValue: any) => {
|
|
52
|
+
onChange({ ...value, [key]: fieldValue })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const renderField = (param: UIParam) => {
|
|
56
|
+
const fieldValue = value?.[param.jsonKey] ?? param.validate?.defaultValue
|
|
57
|
+
const required = param.validate?.required || false
|
|
58
|
+
const fieldDisabled = disabled || (param.validate?.immutable) || false
|
|
59
|
+
const label = param.label || param.jsonKey
|
|
60
|
+
|
|
61
|
+
switch (param.uiType) {
|
|
62
|
+
case "Input":
|
|
63
|
+
case "ImageInput":
|
|
64
|
+
return (
|
|
65
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
66
|
+
<Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
|
|
67
|
+
{label}
|
|
68
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
69
|
+
</Label>
|
|
70
|
+
<Input
|
|
71
|
+
id={`${param.jsonKey}-${index}`}
|
|
72
|
+
value={fieldValue ?? ""}
|
|
73
|
+
onChange={(e) => handleFieldChange(param.jsonKey, e.target.value)}
|
|
74
|
+
disabled={fieldDisabled}
|
|
75
|
+
required={required}
|
|
76
|
+
className="rounded-none h-8 text-sm"
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
case "Textarea":
|
|
82
|
+
return (
|
|
83
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
84
|
+
<Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
|
|
85
|
+
{label}
|
|
86
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
87
|
+
</Label>
|
|
88
|
+
<Textarea
|
|
89
|
+
id={`${param.jsonKey}-${index}`}
|
|
90
|
+
value={fieldValue ?? ""}
|
|
91
|
+
onChange={(e) => handleFieldChange(param.jsonKey, e.target.value)}
|
|
92
|
+
disabled={fieldDisabled}
|
|
93
|
+
required={required}
|
|
94
|
+
className="rounded-none min-h-[60px] text-sm"
|
|
95
|
+
/>
|
|
96
|
+
</div>
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
case "Number":
|
|
100
|
+
return (
|
|
101
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
102
|
+
<Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
|
|
103
|
+
{label}
|
|
104
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
105
|
+
</Label>
|
|
106
|
+
<Input
|
|
107
|
+
id={`${param.jsonKey}-${index}`}
|
|
108
|
+
type="number"
|
|
109
|
+
value={fieldValue ?? ""}
|
|
110
|
+
onChange={(e) => {
|
|
111
|
+
const val = e.target.value === "" ? undefined : Number(e.target.value)
|
|
112
|
+
handleFieldChange(param.jsonKey, val)
|
|
113
|
+
}}
|
|
114
|
+
disabled={fieldDisabled}
|
|
115
|
+
required={required}
|
|
116
|
+
className="rounded-none h-8 text-sm"
|
|
117
|
+
/>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
case "Switch":
|
|
122
|
+
return (
|
|
123
|
+
<div key={param.jsonKey} className="flex items-center space-x-2 py-1">
|
|
124
|
+
<Switch
|
|
125
|
+
id={`${param.jsonKey}-${index}`}
|
|
126
|
+
checked={fieldValue || false}
|
|
127
|
+
onCheckedChange={(checked) => handleFieldChange(param.jsonKey, checked)}
|
|
128
|
+
disabled={fieldDisabled}
|
|
129
|
+
/>
|
|
130
|
+
<Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs cursor-pointer">
|
|
131
|
+
{label}
|
|
132
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
133
|
+
</Label>
|
|
134
|
+
</div>
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
case "Select":
|
|
138
|
+
return (
|
|
139
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
140
|
+
<Label htmlFor={`${param.jsonKey}-${index}`} className="text-xs">
|
|
141
|
+
{label}
|
|
142
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
143
|
+
</Label>
|
|
144
|
+
<Select
|
|
145
|
+
value={fieldValue || ""}
|
|
146
|
+
onValueChange={(val) => handleFieldChange(param.jsonKey, val)}
|
|
147
|
+
disabled={fieldDisabled}
|
|
148
|
+
>
|
|
149
|
+
<SelectTrigger className="rounded-none h-8 text-sm">
|
|
150
|
+
<SelectValue placeholder={`Select ${label}`} />
|
|
151
|
+
</SelectTrigger>
|
|
152
|
+
<SelectContent>
|
|
153
|
+
{param.validate?.options?.map((opt) => (
|
|
154
|
+
<SelectItem key={opt.value} value={String(opt.value)}>
|
|
155
|
+
{opt.label}
|
|
156
|
+
</SelectItem>
|
|
157
|
+
))}
|
|
158
|
+
</SelectContent>
|
|
159
|
+
</Select>
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
case "KV":
|
|
164
|
+
return (
|
|
165
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
166
|
+
<Label className="text-xs">
|
|
167
|
+
{label}
|
|
168
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
169
|
+
</Label>
|
|
170
|
+
<KVInput
|
|
171
|
+
value={fieldValue || {}}
|
|
172
|
+
onChange={(val) => handleFieldChange(param.jsonKey, val)}
|
|
173
|
+
disabled={fieldDisabled}
|
|
174
|
+
/>
|
|
175
|
+
</div>
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
case "Strings":
|
|
179
|
+
return (
|
|
180
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
181
|
+
<Label className="text-xs">
|
|
182
|
+
{label}
|
|
183
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
184
|
+
</Label>
|
|
185
|
+
<StringsInput
|
|
186
|
+
value={fieldValue || []}
|
|
187
|
+
onChange={(val) => handleFieldChange(param.jsonKey, val)}
|
|
188
|
+
disabled={fieldDisabled}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
case "Numbers":
|
|
194
|
+
return (
|
|
195
|
+
<div key={param.jsonKey} className="space-y-1">
|
|
196
|
+
<Label className="text-xs">
|
|
197
|
+
{label}
|
|
198
|
+
{required && <span className="text-red-600 ml-1">*</span>}
|
|
199
|
+
</Label>
|
|
200
|
+
<NumbersInput
|
|
201
|
+
value={fieldValue || []}
|
|
202
|
+
onChange={(val) => handleFieldChange(param.jsonKey, val)}
|
|
203
|
+
disabled={fieldDisabled}
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
default:
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const displayName = value?.name || value?.containerName || `Item ${index + 1}`
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<div className="border border-ibm-gray-20 bg-white">
|
|
217
|
+
<div className="flex items-center justify-between p-3 bg-ibm-gray-10 border-b border-ibm-gray-20">
|
|
218
|
+
<button
|
|
219
|
+
type="button"
|
|
220
|
+
onClick={onToggleExpand}
|
|
221
|
+
className="flex items-center gap-2 flex-1 text-left hover:text-ibm-blue-60 transition-colors"
|
|
222
|
+
>
|
|
223
|
+
{isExpanded ? (
|
|
224
|
+
<ChevronDown className="h-4 w-4 text-ibm-gray-70" />
|
|
225
|
+
) : (
|
|
226
|
+
<ChevronRight className="h-4 w-4 text-ibm-gray-70" />
|
|
227
|
+
)}
|
|
228
|
+
<span className="text-sm font-medium text-ibm-gray-100">{displayName}</span>
|
|
229
|
+
</button>
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={onRemove}
|
|
233
|
+
disabled={disabled}
|
|
234
|
+
className="p-1.5 text-ibm-gray-50 hover:text-ibm-red-60 hover:bg-ibm-red-10 transition-colors disabled:opacity-40"
|
|
235
|
+
title="Remove"
|
|
236
|
+
>
|
|
237
|
+
<Trash2 className="h-4 w-4" />
|
|
238
|
+
</button>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
{isExpanded && (
|
|
242
|
+
<div className="p-4 space-y-3">
|
|
243
|
+
{subParameters.length === 0 ? (
|
|
244
|
+
<div className="text-sm text-ibm-gray-60 italic">
|
|
245
|
+
No fields defined for this structure
|
|
246
|
+
</div>
|
|
247
|
+
) : (
|
|
248
|
+
subParameters.map((param) => renderField(param))
|
|
249
|
+
)}
|
|
250
|
+
</div>
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function StructsInput({ id, value = [], onChange, disabled = false, subParameters = [], label }: StructsInputProps) {
|
|
257
|
+
const [items, setItems] = useState<any[]>(Array.isArray(value) ? value : [])
|
|
258
|
+
const [expandedItems, setExpandedItems] = useState<Set<number>>(new Set([0]))
|
|
259
|
+
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
setItems(Array.isArray(value) ? value : [])
|
|
262
|
+
}, [value])
|
|
263
|
+
|
|
264
|
+
const handleAdd = () => {
|
|
265
|
+
const newItem: any = {}
|
|
266
|
+
subParameters.forEach((param) => {
|
|
267
|
+
if (param.validate?.defaultValue !== undefined) {
|
|
268
|
+
newItem[param.jsonKey] = param.validate.defaultValue
|
|
269
|
+
}
|
|
270
|
+
})
|
|
271
|
+
const newItems = [...items, newItem]
|
|
272
|
+
setItems(newItems)
|
|
273
|
+
onChange?.(newItems)
|
|
274
|
+
setExpandedItems(new Set([...expandedItems, items.length]))
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const handleRemove = (index: number) => {
|
|
278
|
+
const newItems = items.filter((_, i) => i !== index)
|
|
279
|
+
setItems(newItems)
|
|
280
|
+
onChange?.(newItems)
|
|
281
|
+
const newExpanded = new Set<number>()
|
|
282
|
+
expandedItems.forEach((i) => {
|
|
283
|
+
if (i < index) newExpanded.add(i)
|
|
284
|
+
else if (i > index) newExpanded.add(i - 1)
|
|
285
|
+
})
|
|
286
|
+
setExpandedItems(newExpanded)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const handleChange = (index: number, itemValue: any) => {
|
|
290
|
+
const newItems = [...items]
|
|
291
|
+
newItems[index] = itemValue
|
|
292
|
+
setItems(newItems)
|
|
293
|
+
onChange?.(newItems)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const toggleExpand = (index: number) => {
|
|
297
|
+
const newExpanded = new Set(expandedItems)
|
|
298
|
+
if (newExpanded.has(index)) newExpanded.delete(index)
|
|
299
|
+
else newExpanded.add(index)
|
|
300
|
+
setExpandedItems(newExpanded)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return (
|
|
304
|
+
<div className="space-y-3">
|
|
305
|
+
{items.map((item, index) => (
|
|
306
|
+
<StructItem
|
|
307
|
+
key={index}
|
|
308
|
+
index={index}
|
|
309
|
+
value={item}
|
|
310
|
+
onChange={(val) => handleChange(index, val)}
|
|
311
|
+
onRemove={() => handleRemove(index)}
|
|
312
|
+
subParameters={subParameters}
|
|
313
|
+
disabled={disabled}
|
|
314
|
+
isExpanded={expandedItems.has(index)}
|
|
315
|
+
onToggleExpand={() => toggleExpand(index)}
|
|
316
|
+
/>
|
|
317
|
+
))}
|
|
318
|
+
|
|
319
|
+
<Button
|
|
320
|
+
type="button"
|
|
321
|
+
variant="secondary"
|
|
322
|
+
onClick={handleAdd}
|
|
323
|
+
disabled={disabled}
|
|
324
|
+
className="w-full rounded-none"
|
|
325
|
+
leftIcon={<Plus className="h-4 w-4" />}
|
|
326
|
+
>
|
|
327
|
+
Add {label || "Item"}
|
|
328
|
+
</Button>
|
|
329
|
+
</div>
|
|
330
|
+
)
|
|
331
|
+
}
|
|
@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
|
|
19
19
|
ref={ref}
|
|
20
20
|
sideOffset={sideOffset}
|
|
21
21
|
className={cn(
|
|
22
|
-
"z-50 overflow-hidden rounded-md border bg-
|
|
22
|
+
"z-50 overflow-hidden rounded-md border border-ibm-gray-20 bg-white px-3 py-1.5 text-sm text-ibm-gray-100 shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
25
|
{...props}
|