@orsetra/shared-ui 1.1.36 → 1.1.38
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/DefinitionCode/theme.ts +271 -0
- package/components/K8sObjectsCode/index.tsx +156 -0
- package/components/K8sObjectsCode/objects.tsx +27 -0
- package/components/extends/CamelRouteCode/index.tsx +593 -0
- package/components/extends/index.ts +2 -0
- package/components/ui/project-selector-modal.tsx +4 -3
- package/package.json +5 -2
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
base: 'vs-dark',
|
|
3
|
+
inherit: true,
|
|
4
|
+
rules: [
|
|
5
|
+
{
|
|
6
|
+
background: '1B191A',
|
|
7
|
+
token: '',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
foreground: '555555',
|
|
11
|
+
token: 'comment',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
foreground: '555555',
|
|
15
|
+
token: 'comment.block',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
foreground: 'ad9361',
|
|
19
|
+
token: 'string',
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
foreground: 'cccccc',
|
|
23
|
+
token: 'constant.numeric',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
foreground: 'a1a1ff',
|
|
27
|
+
token: 'keyword',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
foreground: '2f006e',
|
|
31
|
+
token: 'meta.preprocessor',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
fontStyle: 'bold',
|
|
35
|
+
token: 'keyword.control.import',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
foreground: 'a1a1ff',
|
|
39
|
+
token: 'support.function',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
foreground: '0000ff',
|
|
43
|
+
token: 'declaration.function function-result',
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
fontStyle: 'bold',
|
|
47
|
+
token: 'declaration.function function-name',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
fontStyle: 'bold',
|
|
51
|
+
token: 'declaration.function argument-name',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
foreground: '0000ff',
|
|
55
|
+
token: 'declaration.function function-arg-type',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
fontStyle: 'italic',
|
|
59
|
+
token: 'declaration.function function-argument',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
fontStyle: 'underline',
|
|
63
|
+
token: 'declaration.class class-name',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
fontStyle: 'italic underline',
|
|
67
|
+
token: 'declaration.class class-inheritance',
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
foreground: 'fff9f9',
|
|
71
|
+
background: 'ff0000',
|
|
72
|
+
fontStyle: 'bold',
|
|
73
|
+
token: 'invalid',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
background: 'ffd0d0',
|
|
77
|
+
token: 'invalid.deprecated.trailing-whitespace',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
fontStyle: 'italic',
|
|
81
|
+
token: 'declaration.section section-name',
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
foreground: 'c10006',
|
|
85
|
+
token: 'string.interpolation',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
foreground: '666666',
|
|
89
|
+
token: 'string.regexp',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
foreground: 'c1c144',
|
|
93
|
+
token: 'variable',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
foreground: '6782d3',
|
|
97
|
+
token: 'constant',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
foreground: 'afa472',
|
|
101
|
+
token: 'constant.character',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
foreground: 'de8e30',
|
|
105
|
+
fontStyle: 'bold',
|
|
106
|
+
token: 'constant.language',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
fontStyle: 'underline',
|
|
110
|
+
token: 'embedded',
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
foreground: '858ef4',
|
|
114
|
+
token: 'keyword.markup.element-name',
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
foreground: '9b456f',
|
|
118
|
+
token: 'keyword.markup.attribute-name',
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
foreground: '9b456f',
|
|
122
|
+
token: 'meta.attribute-with-value',
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
foreground: 'c82255',
|
|
126
|
+
fontStyle: 'bold',
|
|
127
|
+
token: 'keyword.exception',
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
foreground: '47b8d6',
|
|
131
|
+
token: 'keyword.operator',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
foreground: '6969fa',
|
|
135
|
+
fontStyle: 'bold',
|
|
136
|
+
token: 'keyword.control',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
foreground: '68685b',
|
|
140
|
+
token: 'meta.tag.preprocessor.xml',
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
foreground: '888888',
|
|
144
|
+
token: 'meta.tag.sgml.doctype',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
fontStyle: 'italic',
|
|
148
|
+
token: 'string.quoted.docinfo.doctype.DTD',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
foreground: '909090',
|
|
152
|
+
token: 'comment.other.server-side-include.xhtml',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
foreground: '909090',
|
|
156
|
+
token: 'comment.other.server-side-include.html',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
foreground: '858ef4',
|
|
160
|
+
token: 'text.html declaration.tag',
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
foreground: '858ef4',
|
|
164
|
+
token: 'text.html meta.tag',
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
foreground: '858ef4',
|
|
168
|
+
token: 'text.html entity.name.tag.xhtml',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
foreground: '9b456f',
|
|
172
|
+
token: 'keyword.markup.attribute-name',
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
foreground: '777777',
|
|
176
|
+
token: 'keyword.other.phpdoc.php',
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
foreground: 'c82255',
|
|
180
|
+
token: 'keyword.other.include.php',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
foreground: 'de8e20',
|
|
184
|
+
fontStyle: 'bold',
|
|
185
|
+
token: 'support.constant.core.php',
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
foreground: 'de8e10',
|
|
189
|
+
fontStyle: 'bold',
|
|
190
|
+
token: 'support.constant.std.php',
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
foreground: 'b72e1d',
|
|
194
|
+
token: 'variable.other.global.php',
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
foreground: '00ff00',
|
|
198
|
+
token: 'variable.other.global.safer.php',
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
foreground: 'bfa36d',
|
|
202
|
+
token: 'string.quoted.single.php',
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
foreground: '6969fa',
|
|
206
|
+
token: 'keyword.storage.php',
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
foreground: 'ad9361',
|
|
210
|
+
token: 'string.quoted.double.php',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
foreground: 'ec9e00',
|
|
214
|
+
token: 'entity.other.attribute-name.id.css',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
foreground: 'b8cd06',
|
|
218
|
+
fontStyle: 'bold',
|
|
219
|
+
token: 'entity.name.tag.css',
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
foreground: 'edca06',
|
|
223
|
+
token: 'entity.other.attribute-name.class.css',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
foreground: '2e759c',
|
|
227
|
+
token: 'entity.other.attribute-name.pseudo-class.css',
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
foreground: 'ffffff',
|
|
231
|
+
background: 'ff0000',
|
|
232
|
+
token: 'invalid.bad-comma.css',
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
foreground: '9b2e4d',
|
|
236
|
+
token: 'support.constant.property-value.css',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
foreground: 'e1c96b',
|
|
240
|
+
token: 'support.type.property-name.css',
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
foreground: '666633',
|
|
244
|
+
token: 'constant.other.rgb-value.css',
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
foreground: '666633',
|
|
248
|
+
token: 'support.constant.font-name.css',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
foreground: '7171f3',
|
|
252
|
+
token: 'support.constant.tm-language-def',
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
foreground: '7171f3',
|
|
256
|
+
token: 'support.constant.name.tm-language-def',
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
foreground: '6969fa',
|
|
260
|
+
token: 'keyword.other.unit.css',
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
colors: {
|
|
264
|
+
'editor.foreground': '#DADADA',
|
|
265
|
+
'editor.background': '#1B191A',
|
|
266
|
+
'editor.selectionBackground': '#73597E80',
|
|
267
|
+
'editor.lineHighlightBackground': '#353030',
|
|
268
|
+
'editorCursor.foreground': '#FFFFFF',
|
|
269
|
+
'editorWhitespace.foreground': '#4F4D4D',
|
|
270
|
+
},
|
|
271
|
+
};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { Upload, Button, Message, Field } from '@alifd/next';
|
|
2
|
+
import * as yaml from 'js-yaml';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { AiOutlineCloudUpload } from 'react-icons/ai';
|
|
5
|
+
import { v4 as uuid } from 'uuid';
|
|
6
|
+
|
|
7
|
+
import DefinitionCode from '../../components/DefinitionCode';
|
|
8
|
+
import { If } from '../../components/If';
|
|
9
|
+
import { Translation } from '../../components/Translation';
|
|
10
|
+
|
|
11
|
+
import type { KubernetesObject } from './objects';
|
|
12
|
+
|
|
13
|
+
type Props = {
|
|
14
|
+
value?: any;
|
|
15
|
+
id: string;
|
|
16
|
+
onChange: (value: any) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type State = {
|
|
20
|
+
message: string;
|
|
21
|
+
containerId: string;
|
|
22
|
+
showButton: boolean;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class K8sObjectsCode extends React.Component<Props, State> {
|
|
26
|
+
form: Field;
|
|
27
|
+
constructor(props: Props) {
|
|
28
|
+
super(props);
|
|
29
|
+
this.state = {
|
|
30
|
+
message: '',
|
|
31
|
+
containerId: uuid(),
|
|
32
|
+
showButton: false,
|
|
33
|
+
};
|
|
34
|
+
this.form = new Field(this, {
|
|
35
|
+
onChange: () => {
|
|
36
|
+
const values: { code: string } = this.form.getValues();
|
|
37
|
+
this.onChange(values.code);
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
componentDidMount = () => {
|
|
43
|
+
const { value } = this.props;
|
|
44
|
+
this.setValues(value);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
setValues = (value: KubernetesObject[]) => {
|
|
48
|
+
if (value) {
|
|
49
|
+
try {
|
|
50
|
+
let code = '---\n';
|
|
51
|
+
if (value instanceof Array) {
|
|
52
|
+
value.map((res) => {
|
|
53
|
+
if (res) {
|
|
54
|
+
code = code + yaml.dump(res) + '---\n';
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
} else {
|
|
58
|
+
code = yaml.dump(value) + '---\n';
|
|
59
|
+
}
|
|
60
|
+
this.form.setValues({ code: code });
|
|
61
|
+
} catch {}
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
onChange = (v: string) => {
|
|
66
|
+
const { onChange, value } = this.props;
|
|
67
|
+
if (onChange) {
|
|
68
|
+
try {
|
|
69
|
+
let object: any = yaml.load(v);
|
|
70
|
+
if (!(object instanceof Array)) {
|
|
71
|
+
object = [object];
|
|
72
|
+
}
|
|
73
|
+
object = object.filter((ob: any) => ob != null);
|
|
74
|
+
if (yaml.dump(value) != v) {
|
|
75
|
+
onChange(object);
|
|
76
|
+
}
|
|
77
|
+
this.setState({ message: '' });
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
if ((error.message = 'expected a single document in the stream, but found more')) {
|
|
80
|
+
try {
|
|
81
|
+
let objects = yaml.loadAll(v);
|
|
82
|
+
if (yaml.dump(value) != v) {
|
|
83
|
+
objects = objects.filter((ob: any) => ob != null);
|
|
84
|
+
onChange(objects);
|
|
85
|
+
}
|
|
86
|
+
this.setState({
|
|
87
|
+
message: '',
|
|
88
|
+
});
|
|
89
|
+
} catch (err: any) {
|
|
90
|
+
this.setState({ message: err.message });
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
this.setState({ message: error.message });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
customRequest = (option: any) => {
|
|
100
|
+
const reader = new FileReader();
|
|
101
|
+
const fileselect = option.file;
|
|
102
|
+
reader.readAsText(fileselect);
|
|
103
|
+
reader.onload = () => {
|
|
104
|
+
this.form.setValue('code', reader.result?.toString() || '');
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
file: File,
|
|
108
|
+
abort() {},
|
|
109
|
+
};
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
onConvert2WebService = () => {};
|
|
113
|
+
|
|
114
|
+
render() {
|
|
115
|
+
const { id } = this.props;
|
|
116
|
+
const { init } = this.form;
|
|
117
|
+
const { message, containerId, showButton } = this.state;
|
|
118
|
+
return (
|
|
119
|
+
<div id={id}>
|
|
120
|
+
<If condition={message}>
|
|
121
|
+
<span style={{ color: 'red' }}>{message}</span>
|
|
122
|
+
</If>
|
|
123
|
+
|
|
124
|
+
<Message type="notice" style={{ marginTop: '16px' }}>
|
|
125
|
+
<Translation>
|
|
126
|
+
The input data will be automatically formatted. Ensure that the input data is a valid k8s resource YAML.
|
|
127
|
+
</Translation>
|
|
128
|
+
</Message>
|
|
129
|
+
|
|
130
|
+
<Upload request={this.customRequest}>
|
|
131
|
+
<Button text type="normal" className="padding-left-0">
|
|
132
|
+
<AiOutlineCloudUpload />
|
|
133
|
+
<Translation>Upload Yaml File</Translation>
|
|
134
|
+
</Button>
|
|
135
|
+
</Upload>
|
|
136
|
+
|
|
137
|
+
<div id={containerId} className="guide-code">
|
|
138
|
+
<DefinitionCode containerId={containerId} language={'yaml'} readOnly={false} {...init('code')} />
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<If condition={showButton}>
|
|
142
|
+
<div style={{ marginTop: '16px' }}>
|
|
143
|
+
<span style={{ fontSize: '14px', color: '#000', marginRight: '16px' }}>
|
|
144
|
+
<Translation>Convert the kubernetes resource component to the webservice component?</Translation>
|
|
145
|
+
</span>
|
|
146
|
+
<Button type="secondary" onClick={this.onConvert2WebService}>
|
|
147
|
+
Yes
|
|
148
|
+
</Button>
|
|
149
|
+
</div>
|
|
150
|
+
</If>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export default K8sObjectsCode;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ResourceObject } from '@velaux/data';
|
|
2
|
+
|
|
3
|
+
export interface KubernetesObject extends ResourceObject {
|
|
4
|
+
apiVersion: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
spec?: Record<string, any>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function isDeployment(object: KubernetesObject): boolean {
|
|
10
|
+
return object.kind === 'Deployment';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isShowConvertButton(objects: KubernetesObject[]): boolean {
|
|
14
|
+
return objects.filter((ob) => isDeployment(ob)).length > 0;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildWebServiceBaseDeployment(object: KubernetesObject): Record<string, any> {
|
|
18
|
+
return {
|
|
19
|
+
image: object.spec?.containers[0].image,
|
|
20
|
+
ports: [],
|
|
21
|
+
readinessProbe: {},
|
|
22
|
+
livenessProbe: {},
|
|
23
|
+
env: [],
|
|
24
|
+
memory: '',
|
|
25
|
+
cpu: '',
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as monaco from 'monaco-editor';
|
|
4
|
+
import * as yaml from 'js-yaml';
|
|
5
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
AlertCircle,
|
|
8
|
+
CheckCircle2,
|
|
9
|
+
FileCode2,
|
|
10
|
+
FolderGit2,
|
|
11
|
+
Loader2,
|
|
12
|
+
Lock,
|
|
13
|
+
Upload,
|
|
14
|
+
WrapText,
|
|
15
|
+
} from 'lucide-react';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
Select,
|
|
19
|
+
SelectContent,
|
|
20
|
+
SelectItem,
|
|
21
|
+
SelectTrigger,
|
|
22
|
+
SelectValue,
|
|
23
|
+
} from '../../ui/select';
|
|
24
|
+
import { cn } from '../../../lib/utils';
|
|
25
|
+
|
|
26
|
+
// ─── Public types ────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
export type ProjectItem = {
|
|
29
|
+
id: string | number;
|
|
30
|
+
owner: string;
|
|
31
|
+
name: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type ContentItem = {
|
|
35
|
+
_links?: { git?: string; html?: string; self?: string };
|
|
36
|
+
content?: string;
|
|
37
|
+
download_url?: string;
|
|
38
|
+
encoding?: string;
|
|
39
|
+
git_url?: string;
|
|
40
|
+
html_url?: string;
|
|
41
|
+
last_commit_sha?: string;
|
|
42
|
+
name: string;
|
|
43
|
+
path: string;
|
|
44
|
+
sha?: string;
|
|
45
|
+
size?: number;
|
|
46
|
+
submodule_git_url?: string;
|
|
47
|
+
target?: string;
|
|
48
|
+
type: string;
|
|
49
|
+
url?: string;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ─── Props ───────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
type Props = {
|
|
55
|
+
/** Field identifier */
|
|
56
|
+
id: string;
|
|
57
|
+
/** Current YAML string value */
|
|
58
|
+
value?: string;
|
|
59
|
+
/** Called whenever the YAML content changes (only in editable state) */
|
|
60
|
+
onChange?: (value: string) => void;
|
|
61
|
+
/** Disable the whole widget */
|
|
62
|
+
disabled?: boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Pre-selected project.
|
|
65
|
+
* When provided the project selector is hidden; the file list is loaded immediately.
|
|
66
|
+
*/
|
|
67
|
+
project?: ProjectItem | null;
|
|
68
|
+
/** Returns the list of available projects (unused when `project` is provided) */
|
|
69
|
+
listProjects?: () => Promise<ProjectItem[]>;
|
|
70
|
+
/** Returns the file tree for a given project */
|
|
71
|
+
onProjectSelect?: (owner: string, repo: string) => Promise<ContentItem[]>;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
type Tab = 'repo' | 'editor';
|
|
75
|
+
|
|
76
|
+
// ─── Component ───────────────────────────────────────────────────────────────
|
|
77
|
+
|
|
78
|
+
const CamelRouteCode: React.FC<Props> = ({
|
|
79
|
+
id,
|
|
80
|
+
value = '',
|
|
81
|
+
onChange,
|
|
82
|
+
disabled = false,
|
|
83
|
+
project,
|
|
84
|
+
listProjects,
|
|
85
|
+
onProjectSelect,
|
|
86
|
+
}) => {
|
|
87
|
+
const [activeTab, setActiveTab] = useState<Tab>('editor');
|
|
88
|
+
|
|
89
|
+
// Editor content – shared between both tabs
|
|
90
|
+
const [editorContent, setEditorContent] = useState(value);
|
|
91
|
+
const [yamlError, setYamlError] = useState('');
|
|
92
|
+
|
|
93
|
+
// Repo-source state
|
|
94
|
+
const [projects, setProjects] = useState<ProjectItem[]>([]);
|
|
95
|
+
const [selectedProject, setSelectedProject] = useState<ProjectItem | null>(null);
|
|
96
|
+
const [files, setFiles] = useState<ContentItem[]>([]);
|
|
97
|
+
const [selectedFile, setSelectedFile] = useState<ContentItem | null>(null);
|
|
98
|
+
const [loadingProjects, setLoadingProjects] = useState(false);
|
|
99
|
+
const [loadingFiles, setLoadingFiles] = useState(false);
|
|
100
|
+
const [repoError, setRepoError] = useState('');
|
|
101
|
+
|
|
102
|
+
// Refs
|
|
103
|
+
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
104
|
+
const editorContainerRef = useRef<HTMLDivElement>(null);
|
|
105
|
+
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor | null>(null);
|
|
106
|
+
|
|
107
|
+
// The editor is read-only once a file is loaded from the repo
|
|
108
|
+
const isReadOnly = disabled || selectedFile !== null;
|
|
109
|
+
|
|
110
|
+
// Mutable refs so stable callbacks always see the latest values
|
|
111
|
+
const isReadOnlyRef = useRef(isReadOnly);
|
|
112
|
+
const onChangeRef = useRef(onChange);
|
|
113
|
+
isReadOnlyRef.current = isReadOnly;
|
|
114
|
+
onChangeRef.current = onChange;
|
|
115
|
+
|
|
116
|
+
// ── Mount Monaco once ──────────────────────────────────────────────────────
|
|
117
|
+
useEffect(() => {
|
|
118
|
+
const container = editorContainerRef.current;
|
|
119
|
+
if (!container) return;
|
|
120
|
+
|
|
121
|
+
const modelUri = monaco.Uri.parse(`camel-route:${id}/flows.yaml`);
|
|
122
|
+
let model = monaco.editor.getModel(modelUri);
|
|
123
|
+
if (!model) {
|
|
124
|
+
model = monaco.editor.createModel(editorContent, 'yaml', modelUri);
|
|
125
|
+
} else {
|
|
126
|
+
model.setValue(editorContent);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const editor = monaco.editor.create(container, {
|
|
130
|
+
model,
|
|
131
|
+
language: 'yaml',
|
|
132
|
+
readOnly: isReadOnlyRef.current,
|
|
133
|
+
theme: 'vs',
|
|
134
|
+
minimap: { enabled: false },
|
|
135
|
+
automaticLayout: true,
|
|
136
|
+
scrollBeyondLastLine: false,
|
|
137
|
+
fontSize: 12,
|
|
138
|
+
lineNumbers: 'on',
|
|
139
|
+
wordWrap: 'on',
|
|
140
|
+
padding: { top: 8, bottom: 8 },
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
editorRef.current = editor;
|
|
144
|
+
|
|
145
|
+
editor.onDidChangeModelContent(() => {
|
|
146
|
+
if (isReadOnlyRef.current) return;
|
|
147
|
+
const val = editor.getValue();
|
|
148
|
+
// Validate YAML on every change
|
|
149
|
+
try {
|
|
150
|
+
yaml.load(val);
|
|
151
|
+
setYamlError('');
|
|
152
|
+
} catch (e: any) {
|
|
153
|
+
setYamlError(e.message);
|
|
154
|
+
}
|
|
155
|
+
setEditorContent(val);
|
|
156
|
+
onChangeRef.current?.(val);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return () => {
|
|
160
|
+
editor.dispose();
|
|
161
|
+
monaco.editor.getModel(modelUri)?.dispose();
|
|
162
|
+
};
|
|
163
|
+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
|
|
164
|
+
|
|
165
|
+
// ── Sync readOnly option dynamically ──────────────────────────────────────
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
editorRef.current?.updateOptions({ readOnly: isReadOnly });
|
|
168
|
+
}, [isReadOnly]);
|
|
169
|
+
|
|
170
|
+
// ── Push external `value` prop into the editor ────────────────────────────
|
|
171
|
+
useEffect(() => {
|
|
172
|
+
const editor = editorRef.current;
|
|
173
|
+
if (!editor) return;
|
|
174
|
+
if (editor.getValue() !== value) {
|
|
175
|
+
setEditorContent(value);
|
|
176
|
+
editor.setValue(value);
|
|
177
|
+
}
|
|
178
|
+
}, [value]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
179
|
+
|
|
180
|
+
// ── Re-layout editor when switching back to the editor tab ────────────────
|
|
181
|
+
useEffect(() => {
|
|
182
|
+
if (activeTab === 'editor') {
|
|
183
|
+
requestAnimationFrame(() => editorRef.current?.layout());
|
|
184
|
+
}
|
|
185
|
+
}, [activeTab]);
|
|
186
|
+
|
|
187
|
+
// ── Load projects when repo tab is first opened (no project prop) ─────────
|
|
188
|
+
useEffect(() => {
|
|
189
|
+
if (activeTab !== 'repo' || project != null || !listProjects || projects.length > 0) return;
|
|
190
|
+
setLoadingProjects(true);
|
|
191
|
+
setRepoError('');
|
|
192
|
+
listProjects()
|
|
193
|
+
.then(setProjects)
|
|
194
|
+
.catch((e: Error) => setRepoError(e.message))
|
|
195
|
+
.finally(() => setLoadingProjects(false));
|
|
196
|
+
}, [activeTab, project, listProjects]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
197
|
+
|
|
198
|
+
// ── Load file list when project prop is provided ──────────────────────────
|
|
199
|
+
useEffect(() => {
|
|
200
|
+
if (project == null || !onProjectSelect) return;
|
|
201
|
+
setLoadingFiles(true);
|
|
202
|
+
setRepoError('');
|
|
203
|
+
onProjectSelect(project.owner, project.name)
|
|
204
|
+
.then((c) => setFiles(filterYaml(c)))
|
|
205
|
+
.catch((e: Error) => setRepoError(e.message))
|
|
206
|
+
.finally(() => setLoadingFiles(false));
|
|
207
|
+
}, [project]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
208
|
+
|
|
209
|
+
// ─── Handlers ─────────────────────────────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
const handleProjectChange = async (projectId: string) => {
|
|
212
|
+
const proj = projects.find((p) => String(p.id) === projectId) ?? null;
|
|
213
|
+
setSelectedProject(proj);
|
|
214
|
+
setFiles([]);
|
|
215
|
+
setSelectedFile(null);
|
|
216
|
+
setRepoError('');
|
|
217
|
+
if (!proj || !onProjectSelect) return;
|
|
218
|
+
setLoadingFiles(true);
|
|
219
|
+
try {
|
|
220
|
+
const contents = await onProjectSelect(proj.owner, proj.name);
|
|
221
|
+
setFiles(filterYaml(contents));
|
|
222
|
+
} catch (e: any) {
|
|
223
|
+
setRepoError(e.message);
|
|
224
|
+
} finally {
|
|
225
|
+
setLoadingFiles(false);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const handleFileChange = async (filePath: string) => {
|
|
230
|
+
const file = files.find((f) => f.path === filePath);
|
|
231
|
+
if (!file) return;
|
|
232
|
+
setRepoError('');
|
|
233
|
+
try {
|
|
234
|
+
const content = await resolveContent(file);
|
|
235
|
+
// Validate before loading
|
|
236
|
+
try { yaml.load(content); setYamlError(''); } catch (e: any) { setYamlError(e.message); }
|
|
237
|
+
setSelectedFile(file);
|
|
238
|
+
setEditorContent(content);
|
|
239
|
+
editorRef.current?.setValue(content);
|
|
240
|
+
onChange?.(content);
|
|
241
|
+
} catch (e: any) {
|
|
242
|
+
setRepoError(e.message);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
247
|
+
const file = e.target.files?.[0];
|
|
248
|
+
if (!file) return;
|
|
249
|
+
const reader = new FileReader();
|
|
250
|
+
reader.onload = () => {
|
|
251
|
+
const text = reader.result?.toString() ?? '';
|
|
252
|
+
try { yaml.load(text); setYamlError(''); } catch (err: any) { setYamlError(err.message); }
|
|
253
|
+
setEditorContent(text);
|
|
254
|
+
editorRef.current?.setValue(text);
|
|
255
|
+
onChange?.(text);
|
|
256
|
+
};
|
|
257
|
+
reader.readAsText(file);
|
|
258
|
+
e.target.value = '';
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const handleFormat = useCallback(() => {
|
|
262
|
+
const editor = editorRef.current;
|
|
263
|
+
if (!editor || isReadOnlyRef.current) return;
|
|
264
|
+
try {
|
|
265
|
+
const parsed = yaml.load(editor.getValue());
|
|
266
|
+
const formatted = yaml.dump(parsed, { indent: 2, lineWidth: -1, noRefs: true });
|
|
267
|
+
editor.setValue(formatted);
|
|
268
|
+
} catch {
|
|
269
|
+
// invalid yaml – cannot format
|
|
270
|
+
}
|
|
271
|
+
}, []);
|
|
272
|
+
|
|
273
|
+
// ─── Derived ──────────────────────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
const hasProjectProp = project != null;
|
|
276
|
+
const activeProject = hasProjectProp ? project : selectedProject;
|
|
277
|
+
const fileSelectDisabled = disabled || !activeProject || loadingFiles;
|
|
278
|
+
|
|
279
|
+
// ─── Render ───────────────────────────────────────────────────────────────
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<div className="flex flex-col border border-ibm-gray-30 bg-white overflow-hidden" style={{ height: 420 }}>
|
|
283
|
+
|
|
284
|
+
{/* ══ Content area (both panels always rendered; inactive is hidden) ══ */}
|
|
285
|
+
<div className="flex-1 min-h-0 relative">
|
|
286
|
+
|
|
287
|
+
{/* ── Tab 1 : Source repo ────────────────────────────────────────── */}
|
|
288
|
+
<div
|
|
289
|
+
className={cn(
|
|
290
|
+
'absolute inset-0 flex flex-col p-3 gap-3 bg-white',
|
|
291
|
+
activeTab !== 'repo' && 'invisible pointer-events-none',
|
|
292
|
+
)}
|
|
293
|
+
>
|
|
294
|
+
{/* Selectors row */}
|
|
295
|
+
<div className="flex items-center gap-2">
|
|
296
|
+
{/* Project selector or badge */}
|
|
297
|
+
{!hasProjectProp ? (
|
|
298
|
+
<Select onValueChange={handleProjectChange} disabled={disabled || loadingProjects}>
|
|
299
|
+
<SelectTrigger className="h-8 text-xs w-[200px]">
|
|
300
|
+
<SelectValue placeholder={loadingProjects ? 'Loading…' : 'Project'} />
|
|
301
|
+
</SelectTrigger>
|
|
302
|
+
<SelectContent>
|
|
303
|
+
{projects.map((p) => (
|
|
304
|
+
<SelectItem key={String(p.id)} value={String(p.id)}>
|
|
305
|
+
{p.owner}/{p.name}
|
|
306
|
+
</SelectItem>
|
|
307
|
+
))}
|
|
308
|
+
</SelectContent>
|
|
309
|
+
</Select>
|
|
310
|
+
) : (
|
|
311
|
+
<div className="flex items-center gap-1.5 text-xs text-ibm-gray-60 border border-ibm-gray-30 bg-ibm-gray-10 px-2 h-8 shrink-0">
|
|
312
|
+
<FolderGit2 className="w-3.5 h-3.5 text-ibm-gray-50 shrink-0" />
|
|
313
|
+
<span className="truncate max-w-[180px]">{project!.owner}/{project!.name}</span>
|
|
314
|
+
</div>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{/* File selector */}
|
|
318
|
+
<Select onValueChange={handleFileChange} disabled={fileSelectDisabled}>
|
|
319
|
+
<SelectTrigger
|
|
320
|
+
className={cn(
|
|
321
|
+
'h-8 text-xs flex-1 max-w-[280px]',
|
|
322
|
+
fileSelectDisabled && 'opacity-40',
|
|
323
|
+
)}
|
|
324
|
+
>
|
|
325
|
+
<SelectValue placeholder=".yaml file" />
|
|
326
|
+
</SelectTrigger>
|
|
327
|
+
<SelectContent>
|
|
328
|
+
{files.map((f) => (
|
|
329
|
+
<SelectItem key={f.path} value={f.path}>{f.name}</SelectItem>
|
|
330
|
+
))}
|
|
331
|
+
</SelectContent>
|
|
332
|
+
</Select>
|
|
333
|
+
|
|
334
|
+
{(loadingProjects || loadingFiles) && (
|
|
335
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin text-ibm-gray-50 shrink-0" />
|
|
336
|
+
)}
|
|
337
|
+
</div>
|
|
338
|
+
|
|
339
|
+
{/* Repo error */}
|
|
340
|
+
{repoError && (
|
|
341
|
+
<div className="flex items-center gap-1.5 text-xs text-ibm-red-40 bg-ibm-red-90 border border-ibm-red-70 px-3 py-2">
|
|
342
|
+
<AlertCircle className="w-3.5 h-3.5 shrink-0" />
|
|
343
|
+
<span className="truncate">{repoError}</span>
|
|
344
|
+
</div>
|
|
345
|
+
)}
|
|
346
|
+
|
|
347
|
+
{/* Info panel */}
|
|
348
|
+
<div className="flex-1 border border-ibm-gray-20 bg-ibm-gray-10 p-3 flex flex-col gap-3 text-xs overflow-auto">
|
|
349
|
+
{!activeProject && !selectedFile ? (
|
|
350
|
+
<p className="text-ibm-gray-40 italic">
|
|
351
|
+
Select a project then a file to load the source.
|
|
352
|
+
</p>
|
|
353
|
+
) : (
|
|
354
|
+
<>
|
|
355
|
+
{activeProject && (
|
|
356
|
+
<InfoSection label="Project">
|
|
357
|
+
<InfoRow icon={<FolderGit2 className="w-3 h-3" />}>
|
|
358
|
+
<span className="font-medium text-ibm-gray-100">
|
|
359
|
+
{activeProject.owner}/{activeProject.name}
|
|
360
|
+
</span>
|
|
361
|
+
</InfoRow>
|
|
362
|
+
<InfoRow label="ID">{String(activeProject.id)}</InfoRow>
|
|
363
|
+
</InfoSection>
|
|
364
|
+
)}
|
|
365
|
+
|
|
366
|
+
{selectedFile && (
|
|
367
|
+
<InfoSection label="Loaded file">
|
|
368
|
+
<InfoRow label="Name">
|
|
369
|
+
<span className="font-medium text-ibm-gray-100">{selectedFile.name}</span>
|
|
370
|
+
</InfoRow>
|
|
371
|
+
<InfoRow label="Path">{selectedFile.path}</InfoRow>
|
|
372
|
+
{selectedFile.size != null && (
|
|
373
|
+
<InfoRow label="Size">{formatBytes(selectedFile.size)}</InfoRow>
|
|
374
|
+
)}
|
|
375
|
+
{selectedFile.sha && (
|
|
376
|
+
<InfoRow label="SHA">{selectedFile.sha.slice(0, 7)}</InfoRow>
|
|
377
|
+
)}
|
|
378
|
+
{selectedFile.last_commit_sha && (
|
|
379
|
+
<InfoRow label="Last commit">
|
|
380
|
+
{selectedFile.last_commit_sha.slice(0, 7)}
|
|
381
|
+
</InfoRow>
|
|
382
|
+
)}
|
|
383
|
+
<InfoRow icon={<Lock className="w-3 h-3 text-ibm-gray-50" />}>
|
|
384
|
+
<span className="text-ibm-gray-50">Editor is read-only</span>
|
|
385
|
+
</InfoRow>
|
|
386
|
+
</InfoSection>
|
|
387
|
+
)}
|
|
388
|
+
|
|
389
|
+
{activeProject && !selectedFile && files.length > 0 && (
|
|
390
|
+
<p className="text-ibm-gray-40 italic">
|
|
391
|
+
{files.length} file{files.length > 1 ? 's' : ''} available — select one.
|
|
392
|
+
</p>
|
|
393
|
+
)}
|
|
394
|
+
</>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
</div>
|
|
398
|
+
|
|
399
|
+
{/* ── Tab 2 : Éditeur ────────────────────────────────────────────── */}
|
|
400
|
+
<div
|
|
401
|
+
className={cn(
|
|
402
|
+
'absolute inset-0 flex flex-col',
|
|
403
|
+
activeTab !== 'editor' && 'invisible pointer-events-none',
|
|
404
|
+
)}
|
|
405
|
+
>
|
|
406
|
+
{/* Editor toolbar */}
|
|
407
|
+
<div className="flex items-center gap-3 px-3 border-b border-ibm-gray-20 bg-ibm-gray-10 h-9 shrink-0">
|
|
408
|
+
{/* File upload – only meaningful when editor is writable */}
|
|
409
|
+
{!isReadOnly && (
|
|
410
|
+
<>
|
|
411
|
+
<input
|
|
412
|
+
ref={fileInputRef}
|
|
413
|
+
type="file"
|
|
414
|
+
accept=".yaml,.yml"
|
|
415
|
+
className="hidden"
|
|
416
|
+
onChange={handleFileUpload}
|
|
417
|
+
tabIndex={-1}
|
|
418
|
+
/>
|
|
419
|
+
<button
|
|
420
|
+
type="button"
|
|
421
|
+
onClick={() => fileInputRef.current?.click()}
|
|
422
|
+
disabled={disabled}
|
|
423
|
+
className="flex items-center gap-1.5 text-xs text-ibm-blue-60 hover:text-ibm-blue-70 disabled:opacity-40 disabled:pointer-events-none transition-colors"
|
|
424
|
+
>
|
|
425
|
+
<Upload className="w-3.5 h-3.5" />
|
|
426
|
+
Load .yaml
|
|
427
|
+
</button>
|
|
428
|
+
|
|
429
|
+
<span className="text-ibm-gray-60 select-none">|</span>
|
|
430
|
+
|
|
431
|
+
<button
|
|
432
|
+
type="button"
|
|
433
|
+
onClick={handleFormat}
|
|
434
|
+
disabled={disabled || !!yamlError}
|
|
435
|
+
title="Format YAML"
|
|
436
|
+
className="flex items-center gap-1.5 text-xs text-ibm-gray-60 hover:text-ibm-gray-80 disabled:opacity-30 disabled:pointer-events-none transition-colors"
|
|
437
|
+
>
|
|
438
|
+
<WrapText className="w-3.5 h-3.5" />
|
|
439
|
+
Format
|
|
440
|
+
</button>
|
|
441
|
+
</>
|
|
442
|
+
)}
|
|
443
|
+
|
|
444
|
+
{/* Spacer */}
|
|
445
|
+
<div className="flex-1" />
|
|
446
|
+
|
|
447
|
+
{/* YAML status indicator */}
|
|
448
|
+
{editorContent && (
|
|
449
|
+
yamlError ? (
|
|
450
|
+
<span className="flex items-center gap-1 text-[10px] text-ibm-red-40">
|
|
451
|
+
<AlertCircle className="w-3 h-3" />
|
|
452
|
+
Invalid YAML
|
|
453
|
+
</span>
|
|
454
|
+
) : (
|
|
455
|
+
<span className="flex items-center gap-1 text-[10px] text-ibm-green-40">
|
|
456
|
+
<CheckCircle2 className="w-3 h-3" />
|
|
457
|
+
Valid YAML
|
|
458
|
+
</span>
|
|
459
|
+
)
|
|
460
|
+
)}
|
|
461
|
+
|
|
462
|
+
{/* Read-only badge */}
|
|
463
|
+
{isReadOnly && !disabled && (
|
|
464
|
+
<span className="flex items-center gap-1 text-[10px] text-ibm-gray-50 ml-2">
|
|
465
|
+
<Lock className="w-3 h-3" />
|
|
466
|
+
read-only
|
|
467
|
+
</span>
|
|
468
|
+
)}
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
{/* YAML error detail */}
|
|
472
|
+
{yamlError && (
|
|
473
|
+
<div className="flex items-start gap-1.5 px-3 py-1.5 text-[11px] text-ibm-red-60 bg-ibm-red-10 border-b border-ibm-red-20 shrink-0 leading-relaxed">
|
|
474
|
+
<AlertCircle className="w-3 h-3 mt-0.5 shrink-0" />
|
|
475
|
+
<span>{yamlError}</span>
|
|
476
|
+
</div>
|
|
477
|
+
)}
|
|
478
|
+
|
|
479
|
+
{/* Monaco container – always in DOM, fills remaining space */}
|
|
480
|
+
<div
|
|
481
|
+
ref={editorContainerRef}
|
|
482
|
+
className="flex-1 min-h-0"
|
|
483
|
+
/>
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
|
|
487
|
+
{/* ══ Tabs – bottom ══ */}
|
|
488
|
+
<div className="flex shrink-0 border-t border-ibm-gray-20">
|
|
489
|
+
<TabButton
|
|
490
|
+
active={activeTab === 'repo'}
|
|
491
|
+
icon={<FolderGit2 className="w-3.5 h-3.5" />}
|
|
492
|
+
label="Source repo"
|
|
493
|
+
badge={selectedFile ? <Lock className="w-3 h-3 text-ibm-gray-50" /> : undefined}
|
|
494
|
+
onClick={() => setActiveTab('repo')}
|
|
495
|
+
disabled={disabled}
|
|
496
|
+
/>
|
|
497
|
+
<TabButton
|
|
498
|
+
active={activeTab === 'editor'}
|
|
499
|
+
icon={<FileCode2 className="w-3.5 h-3.5" />}
|
|
500
|
+
label="Editor"
|
|
501
|
+
onClick={() => setActiveTab('editor')}
|
|
502
|
+
disabled={disabled}
|
|
503
|
+
borderLeft
|
|
504
|
+
/>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
);
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// ─── Internal sub-components ─────────────────────────────────────────────────
|
|
511
|
+
|
|
512
|
+
type TabButtonProps = {
|
|
513
|
+
active: boolean;
|
|
514
|
+
icon: React.ReactNode;
|
|
515
|
+
label: string;
|
|
516
|
+
badge?: React.ReactNode;
|
|
517
|
+
onClick: () => void;
|
|
518
|
+
disabled?: boolean;
|
|
519
|
+
borderLeft?: boolean;
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
const TabButton: React.FC<TabButtonProps> = ({
|
|
523
|
+
active, icon, label, badge, onClick, disabled, borderLeft,
|
|
524
|
+
}) => (
|
|
525
|
+
<button
|
|
526
|
+
type="button"
|
|
527
|
+
onClick={onClick}
|
|
528
|
+
disabled={disabled}
|
|
529
|
+
className={cn(
|
|
530
|
+
'flex items-center gap-1.5 px-4 py-2 text-xs font-medium transition-colors',
|
|
531
|
+
'disabled:pointer-events-none disabled:opacity-40',
|
|
532
|
+
borderLeft && 'border-l border-ibm-gray-20',
|
|
533
|
+
active
|
|
534
|
+
? 'bg-white text-ibm-blue-60 border-t-2 border-t-ibm-blue-60 -mt-px'
|
|
535
|
+
: 'bg-ibm-gray-10 text-ibm-gray-60 hover:bg-ibm-gray-20 hover:text-ibm-gray-80',
|
|
536
|
+
)}
|
|
537
|
+
>
|
|
538
|
+
{icon}
|
|
539
|
+
{label}
|
|
540
|
+
{badge && <span className="ml-1">{badge}</span>}
|
|
541
|
+
</button>
|
|
542
|
+
);
|
|
543
|
+
|
|
544
|
+
type InfoSectionProps = { label: string; children: React.ReactNode };
|
|
545
|
+
|
|
546
|
+
const InfoSection: React.FC<InfoSectionProps> = ({ label, children }) => (
|
|
547
|
+
<div>
|
|
548
|
+
<p className="text-[10px] uppercase tracking-widest text-ibm-gray-40 mb-1.5">{label}</p>
|
|
549
|
+
<div className="flex flex-col gap-1 pl-1">{children}</div>
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
type InfoRowProps = {
|
|
554
|
+
label?: string;
|
|
555
|
+
icon?: React.ReactNode;
|
|
556
|
+
children: React.ReactNode;
|
|
557
|
+
};
|
|
558
|
+
|
|
559
|
+
const InfoRow: React.FC<InfoRowProps> = ({ label, icon, children }) => (
|
|
560
|
+
<div className="flex items-center gap-2 text-ibm-gray-70">
|
|
561
|
+
{icon}
|
|
562
|
+
{label && <span className="text-ibm-gray-40 w-24 shrink-0">{label}</span>}
|
|
563
|
+
<span className="truncate">{children}</span>
|
|
564
|
+
</div>
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
function filterYaml(items: ContentItem[]): ContentItem[] {
|
|
570
|
+
return items.filter(
|
|
571
|
+
(f) => f.type === 'file' && (f.name.endsWith('.yaml') || f.name.endsWith('.yml')),
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
async function resolveContent(file: ContentItem): Promise<string> {
|
|
576
|
+
if (file.content && file.encoding === 'base64') {
|
|
577
|
+
return atob(file.content.replace(/\s/g, ''));
|
|
578
|
+
}
|
|
579
|
+
if (file.download_url) {
|
|
580
|
+
const res = await fetch(file.download_url);
|
|
581
|
+
if (!res.ok) throw new Error(`Download error: ${res.statusText}`);
|
|
582
|
+
return res.text();
|
|
583
|
+
}
|
|
584
|
+
throw new Error('Unable to retrieve file content.');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function formatBytes(bytes: number): string {
|
|
588
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
589
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
590
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
export default CamelRouteCode;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
// Extended UI Components for Dynamic Forms
|
|
2
|
+
export { default as CamelRouteCode } from './CamelRouteCode'
|
|
3
|
+
export type { ProjectItem, ContentItem } from './CamelRouteCode'
|
|
2
4
|
export { default as CPUNumber } from './CPUNumber'
|
|
3
5
|
export { default as ClassStorageSelect } from './ClassStorageSelect'
|
|
4
6
|
export { default as ClusterSelect } from './ClusterSelect'
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { Loader2, FolderKanban } from "lucide-react"
|
|
22
22
|
|
|
23
23
|
export interface Project {
|
|
24
|
+
id: string,
|
|
24
25
|
name: string
|
|
25
26
|
alias?: string
|
|
26
27
|
description?: string
|
|
@@ -79,7 +80,7 @@ export function ProjectSelectorModal({
|
|
|
79
80
|
|
|
80
81
|
// If no project selected yet and we have projects, select the first one
|
|
81
82
|
if (!selectedProject && projectsList.length > 0) {
|
|
82
|
-
setSelectedProject(projectsList[0].
|
|
83
|
+
setSelectedProject(projectsList[0].id)
|
|
83
84
|
}
|
|
84
85
|
} catch (err) {
|
|
85
86
|
console.error("Failed to load projects:", err)
|
|
@@ -205,9 +206,9 @@ export function ProjectSelectorModal({
|
|
|
205
206
|
</SelectTrigger>
|
|
206
207
|
<SelectContent className="rounded-none">
|
|
207
208
|
{projects.map((project) => (
|
|
208
|
-
<SelectItem key={project.
|
|
209
|
+
<SelectItem key={project.id} value={project.id} className="rounded-none">
|
|
209
210
|
<div className="flex flex-col">
|
|
210
|
-
<span className="font-medium">{project.alias || project.
|
|
211
|
+
<span className="font-medium">{project.alias || project.id}</span>
|
|
211
212
|
{project.description && (
|
|
212
213
|
<span className="text-xs text-ibm-gray-60">{project.description}</span>
|
|
213
214
|
)}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orsetra/shared-ui",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.38",
|
|
4
4
|
"description": "Shared UI components for Orsetra platform",
|
|
5
5
|
"main": "./index.ts",
|
|
6
6
|
"types": "./index.ts",
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
"next-themes": "^0.4.4",
|
|
89
89
|
"react-avatar": "^5.0.3",
|
|
90
90
|
"js-cookie": "^3.0.5",
|
|
91
|
+
"js-yaml": "^4.1.1",
|
|
92
|
+
"monaco-editor": "^0.55.1",
|
|
91
93
|
"@types/js-cookie": "^3.0.6",
|
|
92
94
|
"react-day-picker": "8.10.1",
|
|
93
95
|
"react-easy-crop": "^5.0.8",
|
|
@@ -101,9 +103,10 @@
|
|
|
101
103
|
"zod": "^3.24.1"
|
|
102
104
|
},
|
|
103
105
|
"devDependencies": {
|
|
106
|
+
"@types/js-yaml": "^4.0.9",
|
|
104
107
|
"@types/lodash": "^4.17.24",
|
|
105
108
|
"@types/react": "^19",
|
|
106
109
|
"next": "^16.0.7",
|
|
107
110
|
"typescript": "^5"
|
|
108
111
|
}
|
|
109
|
-
}
|
|
112
|
+
}
|