@orsetra/shared-ui 1.1.8 → 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.
- package/components/extends/ArrayItemGroup/index.tsx +56 -0
- package/components/extends/CPUNumber/index.tsx +33 -0
- package/components/extends/ClassStorageSelect/index.tsx +11 -0
- package/components/extends/ClusterSelect/index.tsx +11 -0
- package/components/extends/ComponentSelect/index.tsx +112 -0
- package/components/extends/DiskNumber/index.tsx +34 -0
- package/components/extends/EnvSelect/index.tsx +11 -0
- package/components/extends/Group/index.tsx +150 -0
- package/components/extends/Ignore/index.tsx +11 -0
- package/components/extends/ImageInput/index.less +49 -0
- package/components/extends/ImageInput/index.tsx +193 -0
- package/components/extends/ImageSecretSelect/index.tsx +168 -0
- package/components/extends/KV/index.tsx +23 -0
- package/components/extends/MemoryNumber/index.tsx +34 -0
- package/components/extends/Numbers/index.tsx +23 -0
- package/components/extends/PVCSelect/index.tsx +12 -0
- package/components/extends/PolicySelect/index.tsx +53 -0
- package/components/extends/SecretKeySelect/index.tsx +95 -0
- package/components/extends/SecretSelect/index.tsx +76 -0
- package/components/extends/StepSelect/index.tsx +74 -0
- package/components/extends/Strings/index.tsx +23 -0
- package/components/extends/Switch/index.tsx +23 -0
- package/components/extends/index.ts +21 -0
- package/components/ui/combobox.tsx +137 -0
- package/components/ui/index.ts +3 -0
- package/components/ui/kv-input.tsx +117 -0
- package/components/ui/multi-select.tsx +151 -0
- package/components/ui/numbers-input.tsx +87 -0
- package/components/ui/project-selector-modal.tsx +95 -37
- package/components/ui/strings-input.tsx +83 -0
- package/index.ts +12 -0
- package/package.json +2 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { ChevronDown, ChevronUp, Trash2 } from 'lucide-react';
|
|
3
|
+
import { Card, CardContent, CardHeader } from '../../ui/card';
|
|
4
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '../../ui/collapsible';
|
|
5
|
+
import { cn } from '../../../lib/utils';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
id: string;
|
|
9
|
+
children?: React.ReactNode;
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
labelTitle: string | React.ReactElement;
|
|
12
|
+
delete: (id: string) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const ArrayItemGroup: React.FC<Props> = ({ id, children, loading, labelTitle, delete: onDelete }) => {
|
|
16
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<Card className={cn(
|
|
20
|
+
"mb-4 border border-ui-border hover:border-ibm-blue-60 transition-colors",
|
|
21
|
+
loading && "opacity-50 pointer-events-none"
|
|
22
|
+
)}>
|
|
23
|
+
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
24
|
+
<CardHeader className="p-4">
|
|
25
|
+
<div className="flex items-center justify-between">
|
|
26
|
+
<div className="flex-1">{labelTitle}</div>
|
|
27
|
+
<div className="flex items-center gap-2">
|
|
28
|
+
<CollapsibleTrigger asChild>
|
|
29
|
+
<button
|
|
30
|
+
className="p-1 hover:bg-ibm-gray-10 rounded transition-colors"
|
|
31
|
+
aria-label={isOpen ? "Collapse" : "Expand"}
|
|
32
|
+
>
|
|
33
|
+
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
34
|
+
</button>
|
|
35
|
+
</CollapsibleTrigger>
|
|
36
|
+
<button
|
|
37
|
+
onClick={() => onDelete(id)}
|
|
38
|
+
className="p-1 hover:bg-ibm-red-10 text-ibm-red-60 rounded transition-colors"
|
|
39
|
+
aria-label="Delete"
|
|
40
|
+
>
|
|
41
|
+
<Trash2 className="h-4 w-4" />
|
|
42
|
+
</button>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</CardHeader>
|
|
46
|
+
<CollapsibleContent>
|
|
47
|
+
<CardContent className="p-4 bg-white border-t border-ui-border">
|
|
48
|
+
{children}
|
|
49
|
+
</CardContent>
|
|
50
|
+
</CollapsibleContent>
|
|
51
|
+
</Collapsible>
|
|
52
|
+
</Card>
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export default ArrayItemGroup;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Input } from '../../ui/input';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
value?: any;
|
|
6
|
+
id: string;
|
|
7
|
+
onChange: (value: any) => void;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const CPUNumber: React.FC<Props> = ({ value, id, onChange, disabled }) => {
|
|
12
|
+
const initValue = value ? parseFloat(value) : undefined;
|
|
13
|
+
|
|
14
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
onChange(e.target.value);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<Input
|
|
21
|
+
id={id}
|
|
22
|
+
type="number"
|
|
23
|
+
disabled={disabled}
|
|
24
|
+
onChange={handleChange}
|
|
25
|
+
value={initValue}
|
|
26
|
+
className="flex-1"
|
|
27
|
+
/>
|
|
28
|
+
<span className="text-sm text-ibm-gray-70">Core</span>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default CPUNumber;
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Check, ChevronsUpDown, X } from 'lucide-react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
5
|
+
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem } from '@/components/ui/command';
|
|
6
|
+
import { Badge } from '@/components/ui/badge';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
value?: string[];
|
|
11
|
+
id: string;
|
|
12
|
+
onChange: (value: string[]) => void;
|
|
13
|
+
disabled: boolean;
|
|
14
|
+
options?: Array<{ label: string; value: string }>;
|
|
15
|
+
placeholder?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ComponentSelect: React.FC<Props> = ({
|
|
19
|
+
value = [],
|
|
20
|
+
id,
|
|
21
|
+
onChange,
|
|
22
|
+
disabled,
|
|
23
|
+
options = [],
|
|
24
|
+
placeholder = 'Please select the components'
|
|
25
|
+
}) => {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
|
|
28
|
+
const handleSelect = (selectedValue: string) => {
|
|
29
|
+
const newValue = value.includes(selectedValue)
|
|
30
|
+
? value.filter((v) => v !== selectedValue)
|
|
31
|
+
: [...value, selectedValue];
|
|
32
|
+
onChange(newValue);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const handleRemove = (valueToRemove: string) => {
|
|
36
|
+
onChange(value.filter((v) => v !== valueToRemove));
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const selectedLabels = options
|
|
40
|
+
.filter((opt) => value.includes(opt.value))
|
|
41
|
+
.map((opt) => opt.label);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="w-full">
|
|
45
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
46
|
+
<PopoverTrigger asChild>
|
|
47
|
+
<Button
|
|
48
|
+
id={id}
|
|
49
|
+
variant="secondary"
|
|
50
|
+
role="combobox"
|
|
51
|
+
aria-expanded={open}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
className="w-full justify-between h-auto min-h-[40px] px-3 py-2"
|
|
54
|
+
style={{ borderRadius: 0 }}
|
|
55
|
+
>
|
|
56
|
+
<div className="flex flex-wrap gap-1 flex-1">
|
|
57
|
+
{value.length === 0 ? (
|
|
58
|
+
<span className="text-ibm-gray-50">{placeholder}</span>
|
|
59
|
+
) : (
|
|
60
|
+
selectedLabels.map((label, index) => (
|
|
61
|
+
<Badge
|
|
62
|
+
key={value[index]}
|
|
63
|
+
variant="secondary"
|
|
64
|
+
className="mr-1"
|
|
65
|
+
style={{ borderRadius: 0 }}
|
|
66
|
+
>
|
|
67
|
+
{label}
|
|
68
|
+
<button
|
|
69
|
+
className="ml-1 hover:text-ibm-red-60"
|
|
70
|
+
onClick={(e) => {
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
handleRemove(value[index]);
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
<X className="h-3 w-3" />
|
|
76
|
+
</button>
|
|
77
|
+
</Badge>
|
|
78
|
+
))
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
82
|
+
</Button>
|
|
83
|
+
</PopoverTrigger>
|
|
84
|
+
<PopoverContent className="w-full p-0" style={{ borderRadius: 0 }}>
|
|
85
|
+
<Command>
|
|
86
|
+
<CommandInput placeholder="Search components..." />
|
|
87
|
+
<CommandEmpty>No component found.</CommandEmpty>
|
|
88
|
+
<CommandGroup className="max-h-64 overflow-auto">
|
|
89
|
+
{options.map((option) => (
|
|
90
|
+
<CommandItem
|
|
91
|
+
key={option.value}
|
|
92
|
+
value={option.value}
|
|
93
|
+
onSelect={() => handleSelect(option.value)}
|
|
94
|
+
>
|
|
95
|
+
<Check
|
|
96
|
+
className={cn(
|
|
97
|
+
"mr-2 h-4 w-4",
|
|
98
|
+
value.includes(option.value) ? "opacity-100" : "opacity-0"
|
|
99
|
+
)}
|
|
100
|
+
/>
|
|
101
|
+
{option.label}
|
|
102
|
+
</CommandItem>
|
|
103
|
+
))}
|
|
104
|
+
</CommandGroup>
|
|
105
|
+
</Command>
|
|
106
|
+
</PopoverContent>
|
|
107
|
+
</Popover>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
export default ComponentSelect;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Input } from '../../ui/input';
|
|
3
|
+
|
|
4
|
+
type Props = {
|
|
5
|
+
id: string;
|
|
6
|
+
onChange: (value: any) => void;
|
|
7
|
+
value?: any;
|
|
8
|
+
disabled: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DiskNumber: React.FC<Props> = ({ value, id, onChange, disabled }) => {
|
|
12
|
+
const initValue = value ? parseInt(value.replace('Gi', ''), 10) : undefined;
|
|
13
|
+
|
|
14
|
+
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
15
|
+
onChange(e.target.value + 'Gi');
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex items-center gap-2">
|
|
20
|
+
<Input
|
|
21
|
+
id={id}
|
|
22
|
+
type="number"
|
|
23
|
+
min="0"
|
|
24
|
+
disabled={disabled}
|
|
25
|
+
onChange={handleChange}
|
|
26
|
+
value={initValue}
|
|
27
|
+
className="flex-1"
|
|
28
|
+
/>
|
|
29
|
+
<span className="text-sm text-ibm-gray-70">Gi</span>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default DiskNumber;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { ChevronDown, ChevronUp } from 'lucide-react';
|
|
3
|
+
import { Card, CardContent, CardHeader } from '@/components/ui/card';
|
|
4
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible';
|
|
5
|
+
import { Switch } from '@/components/ui/switch';
|
|
6
|
+
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
type Props = {
|
|
10
|
+
title?: string | React.ReactNode;
|
|
11
|
+
description?: string | React.ReactNode;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
// if required is false, this will be effective
|
|
14
|
+
closed?: boolean;
|
|
15
|
+
// If set is true, in any case, the group is closed initially.
|
|
16
|
+
initClose?: boolean;
|
|
17
|
+
loading?: boolean;
|
|
18
|
+
hasToggleIcon?: boolean;
|
|
19
|
+
required?: boolean;
|
|
20
|
+
field?: any;
|
|
21
|
+
jsonKey?: string;
|
|
22
|
+
propertyValue?: any;
|
|
23
|
+
alwaysShow?: boolean;
|
|
24
|
+
disableAddon?: boolean;
|
|
25
|
+
onChange?: (values: any) => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const Group: React.FC<Props> = (props) => {
|
|
29
|
+
const { title, description, children, hasToggleIcon, loading, required, disableAddon = false, jsonKey = '', propertyValue = {}, alwaysShow = false, closed: initialClosed, initClose, field, onChange } = props;
|
|
30
|
+
|
|
31
|
+
const [isOpen, setIsOpen] = useState(true);
|
|
32
|
+
const [enable, setEnable] = useState(required);
|
|
33
|
+
const [checked, setChecked] = useState(false);
|
|
34
|
+
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
const findKey = propertyValue && Object.keys(propertyValue).find((item) => item === jsonKey);
|
|
38
|
+
if (findKey || alwaysShow) {
|
|
39
|
+
setEnable(true);
|
|
40
|
+
setIsOpen(!initClose);
|
|
41
|
+
setChecked(true);
|
|
42
|
+
} else if (required) {
|
|
43
|
+
setEnable(true);
|
|
44
|
+
setIsOpen(!initClose);
|
|
45
|
+
setChecked(true);
|
|
46
|
+
} else {
|
|
47
|
+
setEnable(false);
|
|
48
|
+
setIsOpen(!(initialClosed || initClose));
|
|
49
|
+
setChecked(false);
|
|
50
|
+
}
|
|
51
|
+
}, [jsonKey, propertyValue, alwaysShow, required, initialClosed, initClose]);
|
|
52
|
+
|
|
53
|
+
const removeJsonKeyValue = () => {
|
|
54
|
+
if (field && onChange) {
|
|
55
|
+
field.remove(jsonKey);
|
|
56
|
+
const values = field.getValues();
|
|
57
|
+
onChange(values);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const handleSwitchChange = (event: boolean) => {
|
|
62
|
+
if (event === true) {
|
|
63
|
+
setEnable(true);
|
|
64
|
+
setIsOpen(true);
|
|
65
|
+
setChecked(true);
|
|
66
|
+
} else {
|
|
67
|
+
setShowConfirmDialog(true);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleConfirmDisable = () => {
|
|
72
|
+
setEnable(false);
|
|
73
|
+
setIsOpen(true);
|
|
74
|
+
setChecked(false);
|
|
75
|
+
removeJsonKeyValue();
|
|
76
|
+
setShowConfirmDialog(false);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<>
|
|
81
|
+
<Card className={cn(
|
|
82
|
+
"mb-4 border border-ui-border hover:border-ibm-blue-60 transition-colors",
|
|
83
|
+
loading && "opacity-50 pointer-events-none"
|
|
84
|
+
)}>
|
|
85
|
+
{title && (
|
|
86
|
+
<CardHeader className="p-4">
|
|
87
|
+
<div className="flex items-start justify-between gap-4">
|
|
88
|
+
<div className="flex-1">
|
|
89
|
+
<div className="flex items-center gap-1">
|
|
90
|
+
{required && <span className="text-ibm-red-60">*</span>}
|
|
91
|
+
<span className="text-sm font-medium">{title}</span>
|
|
92
|
+
</div>
|
|
93
|
+
{description && (
|
|
94
|
+
<div className="text-xs text-ibm-gray-70 mt-1">{description}</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex items-center gap-2">
|
|
98
|
+
{!required && (
|
|
99
|
+
<Switch
|
|
100
|
+
checked={checked}
|
|
101
|
+
disabled={disableAddon}
|
|
102
|
+
onCheckedChange={handleSwitchChange}
|
|
103
|
+
/>
|
|
104
|
+
)}
|
|
105
|
+
{enable && hasToggleIcon && (
|
|
106
|
+
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
|
|
107
|
+
<CollapsibleTrigger asChild>
|
|
108
|
+
<button
|
|
109
|
+
className="p-1 hover:bg-ibm-gray-10 rounded transition-colors"
|
|
110
|
+
aria-label={isOpen ? "Collapse" : "Expand"}
|
|
111
|
+
>
|
|
112
|
+
{isOpen ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
|
113
|
+
</button>
|
|
114
|
+
</CollapsibleTrigger>
|
|
115
|
+
</Collapsible>
|
|
116
|
+
)}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</CardHeader>
|
|
120
|
+
)}
|
|
121
|
+
{enable && (
|
|
122
|
+
<Collapsible open={isOpen}>
|
|
123
|
+
<CollapsibleContent>
|
|
124
|
+
<CardContent className="p-4 bg-white">
|
|
125
|
+
{children}
|
|
126
|
+
</CardContent>
|
|
127
|
+
</CollapsibleContent>
|
|
128
|
+
</Collapsible>
|
|
129
|
+
)}
|
|
130
|
+
</Card>
|
|
131
|
+
|
|
132
|
+
<AlertDialog open={showConfirmDialog} onOpenChange={setShowConfirmDialog}>
|
|
133
|
+
<AlertDialogContent>
|
|
134
|
+
<AlertDialogHeader>
|
|
135
|
+
<AlertDialogTitle>Confirm Action</AlertDialogTitle>
|
|
136
|
+
<AlertDialogDescription>
|
|
137
|
+
The configuration will be reset if the switch is turned off. Are you sure want to do this?
|
|
138
|
+
</AlertDialogDescription>
|
|
139
|
+
</AlertDialogHeader>
|
|
140
|
+
<AlertDialogFooter>
|
|
141
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
142
|
+
<AlertDialogAction onClick={handleConfirmDisable}>Continue</AlertDialogAction>
|
|
143
|
+
</AlertDialogFooter>
|
|
144
|
+
</AlertDialogContent>
|
|
145
|
+
</AlertDialog>
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default Group;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
.image-input-container {
|
|
2
|
+
display: flex;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.image-info {
|
|
6
|
+
width: 100%;
|
|
7
|
+
min-height: 40px;
|
|
8
|
+
padding: 8px;
|
|
9
|
+
overflow-x: hidden;
|
|
10
|
+
background: rgba(171, 205, 239, 0.2);
|
|
11
|
+
.container-base {
|
|
12
|
+
display: flex;
|
|
13
|
+
padding: 8px;
|
|
14
|
+
.docker-base {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: column;
|
|
17
|
+
justify-content: flex-start;
|
|
18
|
+
}
|
|
19
|
+
.docker-logo {
|
|
20
|
+
width: 50px;
|
|
21
|
+
margin: 0 16px 0 0;
|
|
22
|
+
}
|
|
23
|
+
.name {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-direction: column;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
font-size: 14px;
|
|
28
|
+
.desc {
|
|
29
|
+
padding: 0 8px;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
.desc {
|
|
33
|
+
margin-top: 8px;
|
|
34
|
+
font-size: 14px;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
.image-item {
|
|
38
|
+
display: flex;
|
|
39
|
+
padding: 8px;
|
|
40
|
+
svg {
|
|
41
|
+
margin-right: 6px;
|
|
42
|
+
font-size: 24px;
|
|
43
|
+
line-height: 60px;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
.message {
|
|
47
|
+
color: var(--danger-color);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { HardDrive, Upload, Container, Loader2 } from 'lucide-react';
|
|
3
|
+
import { getImageInfo, getImageRepos, type ImageInfo } from '@/api/repository';
|
|
4
|
+
import { Input } from '@/components/ui/input';
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Label } from '@/components/ui/label';
|
|
7
|
+
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
+
import { beautifyTime, beautifyBinarySize } from '@/utils/common';
|
|
9
|
+
import ImageSecretSelect from '../ImageSecretSelect';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
value?: string;
|
|
14
|
+
id: string;
|
|
15
|
+
label: string;
|
|
16
|
+
required?: boolean;
|
|
17
|
+
onChange: (value: string) => void;
|
|
18
|
+
disabled: boolean;
|
|
19
|
+
project?: string;
|
|
20
|
+
secretValue?: string[];
|
|
21
|
+
onSecretChange: (value?: string[]) => void;
|
|
22
|
+
secretID: string;
|
|
23
|
+
getImageRepos: (params: { project: string }) => Promise<any>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const ImageInput: React.FC<Props> = ({
|
|
27
|
+
value,
|
|
28
|
+
id,
|
|
29
|
+
label,
|
|
30
|
+
required,
|
|
31
|
+
onChange,
|
|
32
|
+
disabled,
|
|
33
|
+
project,
|
|
34
|
+
secretValue,
|
|
35
|
+
onSecretChange,
|
|
36
|
+
secretID,
|
|
37
|
+
getImageRepos,
|
|
38
|
+
}) => {
|
|
39
|
+
const [imageInfo, setImageInfo] = useState<ImageInfo | undefined>(undefined);
|
|
40
|
+
const [imageName, setImageName] = useState(value || '');
|
|
41
|
+
const [loading, setLoading] = useState(false);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
if (value && value !== imageName) {
|
|
45
|
+
setImageName(value);
|
|
46
|
+
loadImageInfo(value);
|
|
47
|
+
}
|
|
48
|
+
}, [value]);
|
|
49
|
+
|
|
50
|
+
const loadImageInfo = (name?: string) => {
|
|
51
|
+
const imageToLoad = name || imageName;
|
|
52
|
+
if (project && imageToLoad) {
|
|
53
|
+
onChange(imageToLoad);
|
|
54
|
+
setLoading(true);
|
|
55
|
+
getImageInfo({ project: project, name: imageToLoad })
|
|
56
|
+
.then((res: ImageInfo) => {
|
|
57
|
+
if (res) {
|
|
58
|
+
setImageInfo(res);
|
|
59
|
+
if (res.secretNames) {
|
|
60
|
+
onSecretChange(res.secretNames);
|
|
61
|
+
} else {
|
|
62
|
+
onSecretChange(undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
.finally(() => {
|
|
67
|
+
setLoading(false);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const secrets = imageInfo?.secretNames || secretValue;
|
|
73
|
+
const secretDisabled = disabled || !!imageInfo?.secretNames;
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="space-y-4">
|
|
77
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
78
|
+
<div className="md:col-span-2">
|
|
79
|
+
<Label htmlFor={id} className={cn(required && 'after:content-["*"] after:ml-0.5 after:text-red-500')}>
|
|
80
|
+
{label}
|
|
81
|
+
</Label>
|
|
82
|
+
<Input
|
|
83
|
+
id={id}
|
|
84
|
+
value={imageName}
|
|
85
|
+
onChange={(e) => setImageName(e.target.value)}
|
|
86
|
+
disabled={disabled}
|
|
87
|
+
onBlur={() => loadImageInfo()}
|
|
88
|
+
placeholder="Enter image name"
|
|
89
|
+
className="mt-1"
|
|
90
|
+
/>
|
|
91
|
+
<p className="text-sm text-ibm-gray-70 mt-1">
|
|
92
|
+
To deploy from a private registry, you need to create a config configuration
|
|
93
|
+
</p>
|
|
94
|
+
</div>
|
|
95
|
+
<div>
|
|
96
|
+
<Label htmlFor={secretID}>Secret</Label>
|
|
97
|
+
<ImageSecretSelect
|
|
98
|
+
id={secretID}
|
|
99
|
+
disabled={secretDisabled}
|
|
100
|
+
onChange={onSecretChange}
|
|
101
|
+
value={secrets}
|
|
102
|
+
project={project}
|
|
103
|
+
getImageRepos={getImageRepos}
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
{loading && (
|
|
109
|
+
<div className="flex items-center justify-center py-8">
|
|
110
|
+
<Loader2 className="h-6 w-6 animate-spin text-ibm-blue-60" />
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
|
|
114
|
+
{!loading && imageInfo && (
|
|
115
|
+
<Card>
|
|
116
|
+
<CardContent className="p-4">
|
|
117
|
+
{imageInfo.info ? (
|
|
118
|
+
<div className="space-y-4">
|
|
119
|
+
<div className="flex items-start gap-3">
|
|
120
|
+
<Container className="h-8 w-8 text-ibm-blue-60 flex-shrink-0" />
|
|
121
|
+
<div className="flex-1">
|
|
122
|
+
<div className="font-semibold text-lg">{imageInfo.name}</div>
|
|
123
|
+
<div className="flex flex-wrap gap-3 text-sm text-ibm-gray-70 mt-1">
|
|
124
|
+
<span title={imageInfo.info.created}>
|
|
125
|
+
{beautifyTime(imageInfo.info.created)}
|
|
126
|
+
</span>
|
|
127
|
+
<span>{beautifyBinarySize(imageInfo.size || 0)}</span>
|
|
128
|
+
{imageInfo.info.architecture && (
|
|
129
|
+
<span>{imageInfo.info.architecture}</span>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-4 border-t">
|
|
136
|
+
<div className="flex items-start gap-2">
|
|
137
|
+
<HardDrive className="h-5 w-5 text-ibm-gray-70 flex-shrink-0 mt-0.5" />
|
|
138
|
+
<div className="flex-1">
|
|
139
|
+
<div className="text-sm font-medium mb-1">Volumes</div>
|
|
140
|
+
{imageInfo.info.config?.Volumes ? (
|
|
141
|
+
<div className="flex flex-wrap gap-1">
|
|
142
|
+
{Object.keys(imageInfo.info.config.Volumes).map((path, idx) => (
|
|
143
|
+
<Badge key={idx} variant="outline" className="bg-green-50 text-green-700 border-green-200">
|
|
144
|
+
{path}
|
|
145
|
+
</Badge>
|
|
146
|
+
))}
|
|
147
|
+
</div>
|
|
148
|
+
) : (
|
|
149
|
+
<span className="text-sm text-ibm-gray-70">No default Volume config</span>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
|
|
154
|
+
<div className="flex items-start gap-2">
|
|
155
|
+
<Upload className="h-5 w-5 text-ibm-gray-70 flex-shrink-0 mt-0.5" />
|
|
156
|
+
<div className="flex-1">
|
|
157
|
+
<div className="text-sm font-medium mb-1">Exposed Ports</div>
|
|
158
|
+
{imageInfo.info.config?.ExposedPorts ? (
|
|
159
|
+
<div className="flex flex-wrap gap-1">
|
|
160
|
+
{Object.keys(imageInfo.info.config.ExposedPorts).map((port, idx) => (
|
|
161
|
+
<Badge key={idx} variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
|
|
162
|
+
{port}
|
|
163
|
+
</Badge>
|
|
164
|
+
))}
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<span className="text-sm text-ibm-gray-70">No default Port config</span>
|
|
168
|
+
)}
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<div className="flex items-start gap-2">
|
|
173
|
+
<Container className="h-5 w-5 text-ibm-gray-70 flex-shrink-0 mt-0.5" />
|
|
174
|
+
<div className="flex-1">
|
|
175
|
+
<div className="text-sm font-medium mb-1">Registry</div>
|
|
176
|
+
<Badge variant="outline" title={imageInfo.registry}>
|
|
177
|
+
{imageInfo.registry}
|
|
178
|
+
</Badge>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
) : imageInfo.message ? (
|
|
184
|
+
<div className="text-sm text-ibm-red-60">{imageInfo.message}</div>
|
|
185
|
+
) : null}
|
|
186
|
+
</CardContent>
|
|
187
|
+
</Card>
|
|
188
|
+
)}
|
|
189
|
+
</div>
|
|
190
|
+
);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
export default ImageInput;
|