@orsetra/shared-ui 1.1.9 → 1.1.10

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.
Files changed (31) hide show
  1. package/components/extends/ArrayItemGroup/index.tsx +56 -0
  2. package/components/extends/CPUNumber/index.tsx +33 -0
  3. package/components/extends/ClassStorageSelect/index.tsx +11 -0
  4. package/components/extends/ClusterSelect/index.tsx +11 -0
  5. package/components/extends/ComponentSelect/index.tsx +112 -0
  6. package/components/extends/DiskNumber/index.tsx +34 -0
  7. package/components/extends/EnvSelect/index.tsx +11 -0
  8. package/components/extends/Group/index.tsx +150 -0
  9. package/components/extends/Ignore/index.tsx +11 -0
  10. package/components/extends/ImageInput/index.less +49 -0
  11. package/components/extends/ImageInput/index.tsx +193 -0
  12. package/components/extends/ImageSecretSelect/index.tsx +168 -0
  13. package/components/extends/KV/index.tsx +23 -0
  14. package/components/extends/MemoryNumber/index.tsx +34 -0
  15. package/components/extends/Numbers/index.tsx +23 -0
  16. package/components/extends/PVCSelect/index.tsx +12 -0
  17. package/components/extends/PolicySelect/index.tsx +53 -0
  18. package/components/extends/SecretKeySelect/index.tsx +95 -0
  19. package/components/extends/SecretSelect/index.tsx +76 -0
  20. package/components/extends/StepSelect/index.tsx +74 -0
  21. package/components/extends/Strings/index.tsx +23 -0
  22. package/components/extends/Switch/index.tsx +23 -0
  23. package/components/extends/index.ts +21 -0
  24. package/components/ui/combobox.tsx +137 -0
  25. package/components/ui/index.ts +3 -0
  26. package/components/ui/kv-input.tsx +117 -0
  27. package/components/ui/multi-select.tsx +151 -0
  28. package/components/ui/numbers-input.tsx +87 -0
  29. package/components/ui/strings-input.tsx +83 -0
  30. package/index.ts +12 -0
  31. package/package.json +2 -2
@@ -0,0 +1,137 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, ChevronDown } from "lucide-react"
5
+ import { cn } from "../../lib/utils"
6
+ import { Button } from "./button"
7
+ import {
8
+ Command,
9
+ CommandEmpty,
10
+ CommandGroup,
11
+ CommandInput,
12
+ CommandItem,
13
+ } from "./command"
14
+ import {
15
+ Popover,
16
+ PopoverContent,
17
+ PopoverTrigger,
18
+ } from "./popover"
19
+
20
+ export interface ComboboxOption {
21
+ label: string
22
+ value: string
23
+ }
24
+
25
+ interface ComboboxProps {
26
+ placeholder?: string
27
+ value?: string
28
+ defaultValue?: string
29
+ onChange?: (value: string) => void
30
+ dataSource?: ComboboxOption[]
31
+ disabled?: boolean
32
+ id?: string
33
+ locale?: any
34
+ enableInput?: boolean
35
+ }
36
+
37
+ export function Combobox({
38
+ placeholder = "Select...",
39
+ value = "",
40
+ defaultValue = "",
41
+ onChange,
42
+ dataSource = [],
43
+ disabled = false,
44
+ id,
45
+ enableInput = false,
46
+ }: ComboboxProps) {
47
+ const [open, setOpen] = React.useState(false)
48
+ const [selected, setSelected] = React.useState<string>(value || defaultValue)
49
+ const [inputValue, setInputValue] = React.useState<string>(value || defaultValue)
50
+
51
+ React.useEffect(() => {
52
+ if (value !== undefined) {
53
+ setSelected(value)
54
+ setInputValue(value)
55
+ }
56
+ }, [value])
57
+
58
+ const handleSelect = (currentValue: string) => {
59
+ const newValue = currentValue === selected ? "" : currentValue
60
+ setSelected(newValue)
61
+ setInputValue(newValue)
62
+ onChange?.(newValue)
63
+ setOpen(false)
64
+ }
65
+
66
+ const handleInputChange = (newValue: string) => {
67
+ setInputValue(newValue)
68
+ if (enableInput) {
69
+ setSelected(newValue)
70
+ onChange?.(newValue)
71
+ }
72
+ }
73
+
74
+ const getLabel = (value: string) => {
75
+ return dataSource.find((option) => option.value === value)?.label || value
76
+ }
77
+
78
+ const displayValue = selected ? getLabel(selected) : placeholder
79
+
80
+ return (
81
+ <Popover open={open} onOpenChange={setOpen}>
82
+ <PopoverTrigger asChild>
83
+ <Button
84
+ id={id}
85
+ variant="secondary"
86
+ role="combobox"
87
+ aria-expanded={open}
88
+ disabled={disabled}
89
+ className={cn(
90
+ "w-full justify-between h-10",
91
+ disabled && "opacity-50 cursor-not-allowed",
92
+ !selected && "text-gray-500"
93
+ )}
94
+ style={{ borderRadius: 0 }}
95
+ >
96
+ <span className="truncate">{displayValue}</span>
97
+ <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
98
+ </Button>
99
+ </PopoverTrigger>
100
+ <PopoverContent className="w-full p-0" align="start" style={{ borderRadius: 0 }}>
101
+ <Command>
102
+ <CommandInput
103
+ placeholder={placeholder}
104
+ value={inputValue}
105
+ onValueChange={handleInputChange}
106
+ />
107
+ <CommandEmpty>
108
+ {enableInput && inputValue ? (
109
+ <div className="px-2 py-1.5 text-sm">
110
+ Press Enter to use "{inputValue}"
111
+ </div>
112
+ ) : (
113
+ "No results found."
114
+ )}
115
+ </CommandEmpty>
116
+ <CommandGroup className="max-h-64 overflow-auto">
117
+ {dataSource.map((option) => (
118
+ <CommandItem
119
+ key={option.value}
120
+ value={option.value}
121
+ onSelect={() => handleSelect(option.value)}
122
+ >
123
+ <Check
124
+ className={cn(
125
+ "mr-2 h-4 w-4",
126
+ selected === option.value ? "opacity-100" : "opacity-0"
127
+ )}
128
+ />
129
+ {option.label}
130
+ </CommandItem>
131
+ ))}
132
+ </CommandGroup>
133
+ </Command>
134
+ </PopoverContent>
135
+ </Popover>
136
+ )
137
+ }
@@ -61,3 +61,6 @@ export { Textarea } from './textarea'
61
61
  export { Toast, ToastAction, ToastClose, ToastDescription, ToastProvider, ToastTitle, ToastViewport } from './toast'
62
62
  export { ToggleGroup, ToggleGroupItem } from './toggle-group'
63
63
  export { Toggle } from './toggle'
64
+ export { StringsInput } from './strings-input'
65
+ export { KVInput } from './kv-input'
66
+ export { NumbersInput } from './numbers-input'
@@ -0,0 +1,117 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import { Input } from "./input"
5
+ import { Button } from "./button"
6
+ import { X, Plus } from "lucide-react"
7
+
8
+ interface KVPair {
9
+ key: string
10
+ value: string
11
+ }
12
+
13
+ interface KVInputProps {
14
+ value?: Record<string, string>
15
+ onChange?: (value: Record<string, string>) => void
16
+ disabled?: boolean
17
+ keyPlaceholder?: string
18
+ valuePlaceholder?: string
19
+ }
20
+
21
+ export function KVInput({
22
+ value = {},
23
+ onChange,
24
+ disabled = false,
25
+ keyPlaceholder = "Key",
26
+ valuePlaceholder = "Value",
27
+ }: KVInputProps) {
28
+ const [pairs, setPairs] = useState<KVPair[]>(() => {
29
+ const entries = Object.entries(value)
30
+ return entries.length > 0 ? entries.map(([key, value]) => ({ key, value })) : [{ key: "", value: "" }]
31
+ })
32
+
33
+ useEffect(() => {
34
+ if (value && Object.keys(value).length > 0) {
35
+ const newPairs = Object.entries(value).map(([key, val]) => ({ key, value: val }))
36
+ if (JSON.stringify(newPairs) !== JSON.stringify(pairs)) {
37
+ setPairs(newPairs)
38
+ }
39
+ }
40
+ }, [value])
41
+
42
+ const handleChange = (index: number, field: "key" | "value", newValue: string) => {
43
+ const newPairs = [...pairs]
44
+ newPairs[index][field] = newValue
45
+ setPairs(newPairs)
46
+
47
+ const result: Record<string, string> = {}
48
+ newPairs.forEach(pair => {
49
+ if (pair.key) {
50
+ result[pair.key] = pair.value
51
+ }
52
+ })
53
+ onChange?.(result)
54
+ }
55
+
56
+ const handleAdd = () => {
57
+ setPairs([...pairs, { key: "", value: "" }])
58
+ }
59
+
60
+ const handleRemove = (index: number) => {
61
+ const newPairs = pairs.filter((_, i) => i !== index)
62
+ const finalPairs = newPairs.length > 0 ? newPairs : [{ key: "", value: "" }]
63
+ setPairs(finalPairs)
64
+
65
+ const result: Record<string, string> = {}
66
+ finalPairs.forEach(pair => {
67
+ if (pair.key) {
68
+ result[pair.key] = pair.value
69
+ }
70
+ })
71
+ onChange?.(result)
72
+ }
73
+
74
+ return (
75
+ <div className="space-y-2">
76
+ {pairs.map((pair, index) => (
77
+ <div key={index} className="flex gap-2">
78
+ <Input
79
+ value={pair.key}
80
+ onChange={(e) => handleChange(index, "key", e.target.value)}
81
+ disabled={disabled}
82
+ placeholder={keyPlaceholder}
83
+ className="rounded-none flex-1"
84
+ />
85
+ <Input
86
+ value={pair.value}
87
+ onChange={(e) => handleChange(index, "value", e.target.value)}
88
+ disabled={disabled}
89
+ placeholder={valuePlaceholder}
90
+ className="rounded-none flex-1"
91
+ />
92
+ {pairs.length > 1 && (
93
+ <Button
94
+ type="button"
95
+ variant="secondary"
96
+ onClick={() => handleRemove(index)}
97
+ disabled={disabled}
98
+ className="rounded-none h-9 w-9 p-0"
99
+ >
100
+ <X className="h-4 w-4" />
101
+ </Button>
102
+ )}
103
+ </div>
104
+ ))}
105
+ <Button
106
+ type="button"
107
+ variant="secondary"
108
+ onClick={handleAdd}
109
+ disabled={disabled}
110
+ className="rounded-none"
111
+ >
112
+ <Plus className="h-4 w-4 mr-2" />
113
+ Add Item
114
+ </Button>
115
+ </div>
116
+ )
117
+ }
@@ -0,0 +1,151 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Check, X, ChevronDown } from "lucide-react"
5
+ import { cn } from "../../lib/utils"
6
+ import { Badge } from "./badge"
7
+ import { Button } from "./button"
8
+ import {
9
+ Command,
10
+ CommandEmpty,
11
+ CommandGroup,
12
+ CommandInput,
13
+ CommandItem,
14
+ } from "./command"
15
+ import {
16
+ Popover,
17
+ PopoverContent,
18
+ PopoverTrigger,
19
+ } from "./popover"
20
+
21
+ export interface MultiSelectOption {
22
+ label: string
23
+ value: string
24
+ }
25
+
26
+ interface MultiSelectProps {
27
+ placeholder?: string
28
+ value?: string[]
29
+ defaultValue?: string[]
30
+ onChange?: (value: string[]) => void
31
+ dataSource?: MultiSelectOption[]
32
+ disabled?: boolean
33
+ id?: string
34
+ locale?: any
35
+ mode?: string
36
+ }
37
+
38
+ export function MultiSelect({
39
+ placeholder = "Select items...",
40
+ value = [],
41
+ defaultValue = [],
42
+ onChange,
43
+ dataSource = [],
44
+ disabled = false,
45
+ id,
46
+ }: MultiSelectProps) {
47
+ const [open, setOpen] = React.useState(false)
48
+ const [selected, setSelected] = React.useState<string[]>(value || defaultValue)
49
+
50
+ React.useEffect(() => {
51
+ if (value !== undefined) {
52
+ setSelected(value)
53
+ }
54
+ }, [value])
55
+
56
+ const handleSelect = (currentValue: string) => {
57
+ const newSelected = selected.includes(currentValue)
58
+ ? selected.filter((item) => item !== currentValue)
59
+ : [...selected, currentValue]
60
+
61
+ setSelected(newSelected)
62
+ onChange?.(newSelected)
63
+ }
64
+
65
+ const handleRemove = (valueToRemove: string, e: React.MouseEvent) => {
66
+ e.stopPropagation()
67
+ const newSelected = selected.filter((item) => item !== valueToRemove)
68
+ setSelected(newSelected)
69
+ onChange?.(newSelected)
70
+ }
71
+
72
+ const getLabel = (value: string) => {
73
+ return dataSource.find((option) => option.value === value)?.label || value
74
+ }
75
+
76
+ return (
77
+ <Popover open={open} onOpenChange={setOpen}>
78
+ <PopoverTrigger asChild>
79
+ <Button
80
+ id={id}
81
+ variant="secondary"
82
+ role="combobox"
83
+ aria-expanded={open}
84
+ disabled={disabled}
85
+ className={cn(
86
+ "w-full justify-between h-auto min-h-10",
87
+ disabled && "opacity-50 cursor-not-allowed"
88
+ )}
89
+ style={{ borderRadius: 0 }}
90
+ >
91
+ <div className="flex flex-wrap gap-1 flex-1">
92
+ {selected.length > 0 ? (
93
+ selected.map((item) => (
94
+ <Badge
95
+ key={item}
96
+ variant="secondary"
97
+ className="mr-1"
98
+ >
99
+ {getLabel(item)}
100
+ {!disabled && (
101
+ <button
102
+ className="ml-1 ring-offset-background rounded-full outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
103
+ onKeyDown={(e) => {
104
+ if (e.key === "Enter") {
105
+ handleRemove(item, e as any)
106
+ }
107
+ }}
108
+ onMouseDown={(e) => {
109
+ e.preventDefault()
110
+ e.stopPropagation()
111
+ }}
112
+ onClick={(e) => handleRemove(item, e)}
113
+ >
114
+ <X className="h-3 w-3 text-muted-foreground hover:text-foreground" />
115
+ </button>
116
+ )}
117
+ </Badge>
118
+ ))
119
+ ) : (
120
+ <span className="text-gray-500">{placeholder}</span>
121
+ )}
122
+ </div>
123
+ <ChevronDown className="h-4 w-4 shrink-0 opacity-50" />
124
+ </Button>
125
+ </PopoverTrigger>
126
+ <PopoverContent className="w-full p-0" align="start" style={{ borderRadius: 0 }}>
127
+ <Command>
128
+ <CommandInput placeholder="Search..." />
129
+ <CommandEmpty>No results found.</CommandEmpty>
130
+ <CommandGroup className="max-h-64 overflow-auto">
131
+ {dataSource.map((option) => (
132
+ <CommandItem
133
+ key={option.value}
134
+ value={option.value}
135
+ onSelect={() => handleSelect(option.value)}
136
+ >
137
+ <Check
138
+ className={cn(
139
+ "mr-2 h-4 w-4",
140
+ selected.includes(option.value) ? "opacity-100" : "opacity-0"
141
+ )}
142
+ />
143
+ {option.label}
144
+ </CommandItem>
145
+ ))}
146
+ </CommandGroup>
147
+ </Command>
148
+ </PopoverContent>
149
+ </Popover>
150
+ )
151
+ }
@@ -0,0 +1,87 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import { Input } from "./input"
5
+ import { Button } from "./button"
6
+ import { X, Plus } from "lucide-react"
7
+
8
+ interface NumbersInputProps {
9
+ value?: number[]
10
+ onChange?: (value: number[]) => void
11
+ disabled?: boolean
12
+ placeholder?: string
13
+ }
14
+
15
+ export function NumbersInput({
16
+ value = [],
17
+ onChange,
18
+ disabled = false,
19
+ placeholder = "Enter number",
20
+ }: NumbersInputProps) {
21
+ const [items, setItems] = useState<number[]>(value.length > 0 ? value : [0])
22
+
23
+ useEffect(() => {
24
+ if (value && value.length > 0 && JSON.stringify(value) !== JSON.stringify(items)) {
25
+ setItems(value)
26
+ }
27
+ }, [value])
28
+
29
+ const handleChange = (index: number, newValue: string) => {
30
+ const numValue = newValue === "" ? 0 : Number(newValue)
31
+ const newItems = [...items]
32
+ newItems[index] = numValue
33
+ setItems(newItems)
34
+ onChange?.(newItems)
35
+ }
36
+
37
+ const handleAdd = () => {
38
+ const newItems = [...items, 0]
39
+ setItems(newItems)
40
+ onChange?.(newItems)
41
+ }
42
+
43
+ const handleRemove = (index: number) => {
44
+ const newItems = items.filter((_, i) => i !== index)
45
+ const finalItems = newItems.length > 0 ? newItems : [0]
46
+ setItems(finalItems)
47
+ onChange?.(finalItems)
48
+ }
49
+
50
+ return (
51
+ <div className="space-y-2">
52
+ {items.map((item, index) => (
53
+ <div key={index} className="flex gap-2">
54
+ <Input
55
+ type="number"
56
+ value={item}
57
+ onChange={(e) => handleChange(index, e.target.value)}
58
+ disabled={disabled}
59
+ placeholder={placeholder}
60
+ className="rounded-none flex-1"
61
+ />
62
+ {items.length > 1 && (
63
+ <Button
64
+ type="button"
65
+ variant="secondary"
66
+ onClick={() => handleRemove(index)}
67
+ disabled={disabled}
68
+ className="rounded-none h-9 w-9 p-0"
69
+ >
70
+ <X className="h-4 w-4" />
71
+ </Button>
72
+ )}
73
+ </div>
74
+ ))}
75
+ <Button
76
+ type="button"
77
+ variant="secondary"
78
+ onClick={handleAdd}
79
+ disabled={disabled}
80
+ className="rounded-none"
81
+ >
82
+ <Plus className="h-4 w-4 mr-2" />
83
+ Add Item
84
+ </Button>
85
+ </div>
86
+ )
87
+ }
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import React, { useState, useEffect } from "react"
4
+ import { Input } from "./input"
5
+ import { Button } from "./button"
6
+ import { X, Plus } from "lucide-react"
7
+
8
+ interface StringsInputProps {
9
+ value?: string[]
10
+ onChange?: (value: string[]) => void
11
+ disabled?: boolean
12
+ placeholder?: string
13
+ }
14
+
15
+ export function StringsInput({
16
+ value = [],
17
+ onChange,
18
+ disabled = false,
19
+ placeholder = "Enter value",
20
+ }: StringsInputProps) {
21
+ const [items, setItems] = useState<string[]>(value.length > 0 ? value : [""])
22
+
23
+ useEffect(() => {
24
+ if (value && value.length > 0 && JSON.stringify(value) !== JSON.stringify(items)) {
25
+ setItems(value)
26
+ }
27
+ }, [value])
28
+
29
+ const handleChange = (index: number, newValue: string) => {
30
+ const newItems = [...items]
31
+ newItems[index] = newValue
32
+ setItems(newItems)
33
+ onChange?.(newItems.filter(item => item !== ""))
34
+ }
35
+
36
+ const handleAdd = () => {
37
+ const newItems = [...items, ""]
38
+ setItems(newItems)
39
+ }
40
+
41
+ const handleRemove = (index: number) => {
42
+ const newItems = items.filter((_, i) => i !== index)
43
+ setItems(newItems.length > 0 ? newItems : [""])
44
+ onChange?.(newItems.filter(item => item !== ""))
45
+ }
46
+
47
+ return (
48
+ <div className="space-y-2">
49
+ {items.map((item, index) => (
50
+ <div key={index} className="flex gap-2">
51
+ <Input
52
+ value={item}
53
+ onChange={(e) => handleChange(index, e.target.value)}
54
+ disabled={disabled}
55
+ placeholder={placeholder}
56
+ className="rounded-none flex-1"
57
+ />
58
+ {items.length > 1 && (
59
+ <Button
60
+ type="button"
61
+ variant="secondary"
62
+ onClick={() => handleRemove(index)}
63
+ disabled={disabled}
64
+ className="rounded-none h-9 w-9 p-0"
65
+ >
66
+ <X className="h-4 w-4" />
67
+ </Button>
68
+ )}
69
+ </div>
70
+ ))}
71
+ <Button
72
+ type="button"
73
+ variant="secondary"
74
+ onClick={handleAdd}
75
+ disabled={disabled}
76
+ className="rounded-none"
77
+ >
78
+ <Plus className="h-4 w-4 mr-2" />
79
+ Add Item
80
+ </Button>
81
+ </div>
82
+ )
83
+ }
package/index.ts CHANGED
@@ -8,8 +8,20 @@ export { default as HttpClient, useHttpClient, ApiError } from './lib/http-clien
8
8
  // UI Components
9
9
  export * from './components/ui'
10
10
 
11
+ // Extended Components for Dynamic Forms
12
+ export * from './components/extends'
13
+
11
14
  // Layout Components
12
15
  export * from './components/layout'
13
16
 
14
17
  // Hooks
15
18
  export * from './hooks'
19
+
20
+ // Context
21
+ export * from './context'
22
+
23
+ // Internationalization
24
+ export { default as i18n } from './i18n'
25
+
26
+ // Utilities (additional exports)
27
+ export { locale } from './utils/locale'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Shared UI components for Orsetra platform",
5
5
  "main": "./index.ts",
6
6
  "types": "./index.ts",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@hookform/resolvers": "^3.9.1",
44
- "@orsetra/shared-types": "^1.0.2",
44
+ "@orsetra/shared-types": "^1.0.4",
45
45
  "@radix-ui/react-accordion": "1.2.2",
46
46
  "@radix-ui/react-alert-dialog": "1.1.4",
47
47
  "@radix-ui/react-aspect-ratio": "1.1.1",