@orsetra/shared-ui 1.1.37 → 1.1.39

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.
@@ -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
+ // CamelRouteCode removed - causes SSR issues with Monaco Editor
3
+ // Use local implementation in project-manager instead
2
4
  export { default as CPUNumber } from './CPUNumber'
3
5
  export { default as ClassStorageSelect } from './ClassStorageSelect'
4
6
  export { default as ClusterSelect } from './ClusterSelect'
@@ -18,66 +18,77 @@ export function StringsInput({
18
18
  disabled = false,
19
19
  placeholder = "Enter value",
20
20
  }: StringsInputProps) {
21
- const [items, setItems] = useState<string[]>(value.length > 0 ? value : [""])
21
+ const [items, setItems] = useState<string[]>(value.filter(v => v !== ""))
22
+ const [inputValue, setInputValue] = useState("")
22
23
 
23
24
  useEffect(() => {
24
- if (value && value.length > 0 && JSON.stringify(value) !== JSON.stringify(items)) {
25
- setItems(value)
25
+ const filtered = value.filter(v => v !== "")
26
+ if (JSON.stringify(filtered) !== JSON.stringify(items)) {
27
+ setItems(filtered)
26
28
  }
27
29
  }, [value])
28
30
 
29
- const handleChange = (index: number, newValue: string) => {
30
- const newItems = [...items]
31
- newItems[index] = newValue
31
+ const handleRemove = (index: number) => {
32
+ const newItems = items.filter((_, i) => i !== index)
32
33
  setItems(newItems)
33
- onChange?.(newItems.filter(item => item !== ""))
34
+ onChange?.(newItems)
34
35
  }
35
36
 
36
37
  const handleAdd = () => {
37
- const newItems = [...items, ""]
38
+ const trimmed = inputValue.trim()
39
+ if (!trimmed) return
40
+ const newItems = [...items, trimmed]
38
41
  setItems(newItems)
42
+ onChange?.(newItems)
43
+ setInputValue("")
39
44
  }
40
45
 
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 !== ""))
46
+ const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
47
+ if (e.key === "Enter") {
48
+ e.preventDefault()
49
+ handleAdd()
50
+ }
45
51
  }
46
52
 
47
53
  return (
48
54
  <div className="space-y-2">
49
55
  {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)}
56
+ <div key={index} className="flex items-center gap-2">
57
+ <span className="flex-1 text-sm px-3 py-2 border border-ibm-gray-20 bg-ibm-gray-10 text-ibm-gray-100 truncate">
58
+ {item}
59
+ </span>
60
+ <Button
61
+ type="button"
62
+ variant="secondary"
63
+ onClick={() => handleRemove(index)}
54
64
  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
- )}
65
+ className="rounded-none h-9 w-9 p-0 shrink-0"
66
+ >
67
+ <X className="h-4 w-4" />
68
+ </Button>
69
69
  </div>
70
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>
71
+ <div className="flex gap-2">
72
+ <Input
73
+ value={inputValue}
74
+ onChange={(e) => setInputValue(e.target.value)}
75
+ onKeyDown={handleKeyDown}
76
+ disabled={disabled}
77
+ placeholder={placeholder}
78
+ className="rounded-none flex-1"
79
+ />
80
+ <Button
81
+ type="button"
82
+ variant="secondary"
83
+ leftIcon={<Plus className="h-4 w-4 mr-1" />}
84
+ onClick={handleAdd}
85
+ disabled={disabled || !inputValue.trim()}
86
+ className="rounded-none shrink-0"
87
+ >
88
+
89
+ Add
90
+ </Button>
91
+ </div>
81
92
  </div>
82
93
  )
83
94
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orsetra/shared-ui",
3
- "version": "1.1.37",
3
+ "version": "1.1.39",
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
+ }