@checkstack/ui 0.5.3 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/package.json +5 -12
- package/src/components/AnimatedNumber.tsx +48 -0
- package/src/components/CodeEditor/CodeEditor.tsx +14 -420
- package/src/components/CodeEditor/MonacoEditor.tsx +530 -0
- package/src/components/CodeEditor/generateTypeDefinitions.ts +169 -0
- package/src/components/CodeEditor/index.ts +4 -3
- package/src/components/CodeEditor/templateUtils.test.ts +87 -0
- package/src/components/CodeEditor/templateUtils.ts +81 -0
- package/src/components/DynamicForm/FormField.tsx +13 -7
- package/src/components/DynamicForm/MultiTypeEditorField.tsx +33 -0
- package/src/components/DynamicForm/utils.ts +3 -0
- package/src/hooks/useAnimatedNumber.ts +83 -0
- package/src/index.ts +2 -0
- package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +0 -173
- package/src/components/CodeEditor/languageSupport/enterBehavior.ts +0 -35
- package/src/components/CodeEditor/languageSupport/index.ts +0 -22
- package/src/components/CodeEditor/languageSupport/json-utils.ts +0 -117
- package/src/components/CodeEditor/languageSupport/json.test.ts +0 -274
- package/src/components/CodeEditor/languageSupport/json.ts +0 -139
- package/src/components/CodeEditor/languageSupport/markdown-utils.ts +0 -65
- package/src/components/CodeEditor/languageSupport/markdown.test.ts +0 -245
- package/src/components/CodeEditor/languageSupport/markdown.ts +0 -134
- package/src/components/CodeEditor/languageSupport/types.ts +0 -48
- package/src/components/CodeEditor/languageSupport/xml-utils.ts +0 -94
- package/src/components/CodeEditor/languageSupport/xml.test.ts +0 -239
- package/src/components/CodeEditor/languageSupport/xml.ts +0 -116
- package/src/components/CodeEditor/languageSupport/yaml-utils.ts +0 -101
- package/src/components/CodeEditor/languageSupport/yaml.test.ts +0 -203
- package/src/components/CodeEditor/languageSupport/yaml.ts +0 -120
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import Editor, {
|
|
3
|
+
loader,
|
|
4
|
+
type OnMount,
|
|
5
|
+
type Monaco,
|
|
6
|
+
} from "@monaco-editor/react";
|
|
7
|
+
import type { editor, Position } from "monaco-editor";
|
|
8
|
+
import { detectOpenTemplate, detectAutoClosedBraces } from "./templateUtils";
|
|
9
|
+
|
|
10
|
+
// Configure Monaco to use self-hosted files (for air-gapped environments)
|
|
11
|
+
// The @monaco-editor/react package will load from node_modules by default
|
|
12
|
+
loader.config({
|
|
13
|
+
"vs/nls": { availableLanguages: { "*": "en" } },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export type CodeEditorLanguage =
|
|
17
|
+
| "json"
|
|
18
|
+
| "yaml"
|
|
19
|
+
| "xml"
|
|
20
|
+
| "markdown"
|
|
21
|
+
| "javascript"
|
|
22
|
+
| "typescript"
|
|
23
|
+
| "shell";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A single payload property available for templating
|
|
27
|
+
*/
|
|
28
|
+
export interface TemplateProperty {
|
|
29
|
+
/** Full path to the property, e.g., "payload.incident.title" */
|
|
30
|
+
path: string;
|
|
31
|
+
/** Type of the property, e.g., "string", "number", "boolean" */
|
|
32
|
+
type: string;
|
|
33
|
+
/** Optional description of the property */
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CodeEditorProps {
|
|
38
|
+
/** Unique identifier for the editor */
|
|
39
|
+
id?: string;
|
|
40
|
+
/** Current value of the editor */
|
|
41
|
+
value: string;
|
|
42
|
+
/** Callback when the value changes */
|
|
43
|
+
onChange: (value: string) => void;
|
|
44
|
+
/** Language for syntax highlighting */
|
|
45
|
+
language?: CodeEditorLanguage;
|
|
46
|
+
/** Minimum height of the editor */
|
|
47
|
+
minHeight?: string;
|
|
48
|
+
/** Whether the editor is read-only */
|
|
49
|
+
readOnly?: boolean;
|
|
50
|
+
/** Placeholder text when empty */
|
|
51
|
+
placeholder?: string;
|
|
52
|
+
/**
|
|
53
|
+
* TypeScript type definitions to inject for IntelliSense.
|
|
54
|
+
* Generated from JSON schemas for context-aware autocomplete.
|
|
55
|
+
*/
|
|
56
|
+
typeDefinitions?: string;
|
|
57
|
+
/**
|
|
58
|
+
* Optional template properties for autocomplete.
|
|
59
|
+
* When provided, typing "{{" triggers autocomplete with available template variables.
|
|
60
|
+
*/
|
|
61
|
+
templateProperties?: TemplateProperty[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Map language names to Monaco language IDs
|
|
65
|
+
const languageMap: Record<string, string> = {
|
|
66
|
+
json: "json",
|
|
67
|
+
yaml: "yaml",
|
|
68
|
+
xml: "xml",
|
|
69
|
+
markdown: "markdown",
|
|
70
|
+
javascript: "javascript",
|
|
71
|
+
typescript: "typescript",
|
|
72
|
+
shell: "shell",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// Track if we've registered the json-template language
|
|
76
|
+
let jsonTemplateLanguageRegistered = false;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Default type definitions for backend TypeScript/JavaScript editors.
|
|
80
|
+
* Provides console and fetch APIs without DOM types.
|
|
81
|
+
* Used when no custom typeDefinitions are provided to the editor.
|
|
82
|
+
*/
|
|
83
|
+
const DEFAULT_BACKEND_TYPE_DEFINITIONS = `
|
|
84
|
+
/** Expected return type for healthcheck scripts */
|
|
85
|
+
interface HealthCheckScriptResult {
|
|
86
|
+
/** Whether the health check passed */
|
|
87
|
+
success: boolean;
|
|
88
|
+
/** Optional status message */
|
|
89
|
+
message?: string;
|
|
90
|
+
/** Optional numeric value for metrics */
|
|
91
|
+
value?: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Console for logging */
|
|
95
|
+
declare const console: {
|
|
96
|
+
/** Log an info message */
|
|
97
|
+
log(...args: unknown[]): void;
|
|
98
|
+
/** Log a warning message */
|
|
99
|
+
warn(...args: unknown[]): void;
|
|
100
|
+
/** Log an error message */
|
|
101
|
+
error(...args: unknown[]): void;
|
|
102
|
+
/** Log an info message */
|
|
103
|
+
info(...args: unknown[]): void;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** HTTP Request configuration */
|
|
107
|
+
interface RequestInit {
|
|
108
|
+
method?: string;
|
|
109
|
+
headers?: Record<string, string> | Headers;
|
|
110
|
+
body?: string | FormData | URLSearchParams;
|
|
111
|
+
mode?: 'cors' | 'no-cors' | 'same-origin';
|
|
112
|
+
credentials?: 'omit' | 'same-origin' | 'include';
|
|
113
|
+
cache?: 'default' | 'no-store' | 'reload' | 'no-cache' | 'force-cache';
|
|
114
|
+
redirect?: 'follow' | 'error' | 'manual';
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** HTTP Response */
|
|
119
|
+
interface Response {
|
|
120
|
+
readonly ok: boolean;
|
|
121
|
+
readonly status: number;
|
|
122
|
+
readonly statusText: string;
|
|
123
|
+
readonly headers: Headers;
|
|
124
|
+
readonly url: string;
|
|
125
|
+
readonly redirected: boolean;
|
|
126
|
+
json(): Promise<unknown>;
|
|
127
|
+
text(): Promise<string>;
|
|
128
|
+
blob(): Promise<Blob>;
|
|
129
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
130
|
+
clone(): Response;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** HTTP Headers */
|
|
134
|
+
interface Headers {
|
|
135
|
+
get(name: string): string | null;
|
|
136
|
+
has(name: string): boolean;
|
|
137
|
+
set(name: string, value: string): void;
|
|
138
|
+
append(name: string, value: string): void;
|
|
139
|
+
delete(name: string): void;
|
|
140
|
+
forEach(callback: (value: string, key: string) => void): void;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Fetch API for making HTTP requests */
|
|
144
|
+
declare function fetch(input: string | URL, init?: RequestInit): Promise<Response>;
|
|
145
|
+
`;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* A code editor component with syntax highlighting, IntelliSense, and template autocomplete.
|
|
149
|
+
* Uses Monaco Editor (VS Code's editor) for full TypeScript/JavaScript language support.
|
|
150
|
+
*/
|
|
151
|
+
export const CodeEditor: React.FC<CodeEditorProps> = ({
|
|
152
|
+
id,
|
|
153
|
+
value,
|
|
154
|
+
onChange,
|
|
155
|
+
language = "json",
|
|
156
|
+
minHeight = "100px",
|
|
157
|
+
readOnly = false,
|
|
158
|
+
placeholder,
|
|
159
|
+
typeDefinitions,
|
|
160
|
+
templateProperties,
|
|
161
|
+
}) => {
|
|
162
|
+
const editorRef = React.useRef<editor.IStandaloneCodeEditor | null>(null);
|
|
163
|
+
const monacoRef = React.useRef<Monaco | null>(null);
|
|
164
|
+
const disposablesRef = React.useRef<{ dispose: () => void }[]>([]);
|
|
165
|
+
// Track when editor is mounted to re-trigger useEffects
|
|
166
|
+
const [isEditorMounted, setIsEditorMounted] = React.useState(false);
|
|
167
|
+
|
|
168
|
+
// Register json-template language BEFORE editor creates the model
|
|
169
|
+
const handleEditorWillMount = (monaco: Monaco) => {
|
|
170
|
+
if (!jsonTemplateLanguageRegistered) {
|
|
171
|
+
monaco.languages.register({ id: "json-template" });
|
|
172
|
+
// Use JSON syntax highlighting
|
|
173
|
+
monaco.languages.setLanguageConfiguration("json-template", {
|
|
174
|
+
comments: {
|
|
175
|
+
lineComment: "//",
|
|
176
|
+
blockComment: ["/*", "*/"],
|
|
177
|
+
},
|
|
178
|
+
brackets: [
|
|
179
|
+
["{", "}"],
|
|
180
|
+
["[", "]"],
|
|
181
|
+
],
|
|
182
|
+
autoClosingPairs: [
|
|
183
|
+
{ open: "{", close: "}" },
|
|
184
|
+
{ open: "[", close: "]" },
|
|
185
|
+
{ open: '"', close: '"' },
|
|
186
|
+
],
|
|
187
|
+
});
|
|
188
|
+
// Custom tokenizer that highlights {{...}} templates specially
|
|
189
|
+
monaco.languages.setMonarchTokensProvider("json-template", {
|
|
190
|
+
tokenizer: {
|
|
191
|
+
root: [
|
|
192
|
+
[/\{\{[^}]*\}\}/, "variable"], // Template syntax - highlight specially
|
|
193
|
+
[/"([^"\\]|\\.)*$/, "string.invalid"], // Unterminated string
|
|
194
|
+
[/"/, "string", "@string"],
|
|
195
|
+
[/[{}[\]]/, "delimiter.bracket"],
|
|
196
|
+
[/-?\d+\.?\d*([eE][-+]?\d+)?/, "number"],
|
|
197
|
+
[/true|false/, "keyword"],
|
|
198
|
+
[/null/, "keyword"],
|
|
199
|
+
[/[,:]/, "delimiter"],
|
|
200
|
+
[/\s+/, "white"],
|
|
201
|
+
],
|
|
202
|
+
string: [
|
|
203
|
+
[/[^\\"]+/, "string"],
|
|
204
|
+
[/\\./, "string.escape"],
|
|
205
|
+
[/"/, "string", "@pop"],
|
|
206
|
+
],
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
jsonTemplateLanguageRegistered = true;
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Handle editor mount
|
|
214
|
+
const handleEditorDidMount: OnMount = (editor, monaco) => {
|
|
215
|
+
editorRef.current = editor;
|
|
216
|
+
monacoRef.current = monaco;
|
|
217
|
+
setIsEditorMounted(true);
|
|
218
|
+
|
|
219
|
+
// Configure TypeScript/JavaScript to exclude DOM types for backend scripts
|
|
220
|
+
// This prevents Web API autocompletions (AudioContext, Canvas, etc.)
|
|
221
|
+
if (language === "typescript" || language === "javascript") {
|
|
222
|
+
const defaults =
|
|
223
|
+
language === "typescript"
|
|
224
|
+
? monaco.languages.typescript.typescriptDefaults
|
|
225
|
+
: monaco.languages.typescript.javascriptDefaults;
|
|
226
|
+
|
|
227
|
+
// Configure compiler options to exclude DOM types but keep ES libs
|
|
228
|
+
// This gives us Promise, Array, etc. but not AudioContext, Canvas, etc.
|
|
229
|
+
defaults.setCompilerOptions({
|
|
230
|
+
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
|
231
|
+
module: monaco.languages.typescript.ModuleKind.ESNext,
|
|
232
|
+
lib: ["esnext"], // Include ES libs but NOT DOM
|
|
233
|
+
allowNonTsExtensions: true,
|
|
234
|
+
noEmit: true,
|
|
235
|
+
strict: true,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
// Suppress certain diagnostics that don't apply to our script context
|
|
239
|
+
// - 1108: "A 'return' statement can only be used within a function body"
|
|
240
|
+
// Our scripts are wrapped in async function at runtime, so return is valid
|
|
241
|
+
defaults.setDiagnosticsOptions({
|
|
242
|
+
diagnosticCodesToIgnore: [1108],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// Disable fetching default lib content (prevents DOM types loading)
|
|
246
|
+
defaults.setEagerModelSync(true);
|
|
247
|
+
|
|
248
|
+
// Add custom type definitions if provided, otherwise add minimal defaults
|
|
249
|
+
const definitions = typeDefinitions ?? DEFAULT_BACKEND_TYPE_DEFINITIONS;
|
|
250
|
+
const lib = defaults.addExtraLib(definitions, "file:///context.d.ts");
|
|
251
|
+
disposablesRef.current.push(lib);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Handle validation for template syntax in JSON
|
|
255
|
+
// Since we're using json-template language (no built-in validation),
|
|
256
|
+
// we run our own validation with preprocessed content.
|
|
257
|
+
if (
|
|
258
|
+
templateProperties &&
|
|
259
|
+
templateProperties.length > 0 &&
|
|
260
|
+
language === "json"
|
|
261
|
+
) {
|
|
262
|
+
const model = editor.getModel();
|
|
263
|
+
if (model) {
|
|
264
|
+
// Custom validation function
|
|
265
|
+
const runCustomValidation = () => {
|
|
266
|
+
const content = model.getValue();
|
|
267
|
+
|
|
268
|
+
// Replace {{...}} templates with valid JSON strings of same length
|
|
269
|
+
// Using same length ensures error positions map correctly
|
|
270
|
+
const preprocessed = content.replaceAll(
|
|
271
|
+
/\{\{[^}]*\}\}/g,
|
|
272
|
+
(match) => `"${"_".repeat(Math.max(0, match.length - 2))}"`,
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// Try to parse the preprocessed JSON
|
|
276
|
+
const markers: editor.IMarkerData[] = [];
|
|
277
|
+
try {
|
|
278
|
+
JSON.parse(preprocessed);
|
|
279
|
+
} catch (error) {
|
|
280
|
+
if (error instanceof SyntaxError) {
|
|
281
|
+
// Extract position from error message (varies by browser)
|
|
282
|
+
const message = error.message;
|
|
283
|
+
// Try to find line/column info - JSON.parse errors are at position
|
|
284
|
+
const posMatch = message.match(/position (\d+)/i);
|
|
285
|
+
|
|
286
|
+
if (posMatch) {
|
|
287
|
+
const pos = Number.parseInt(posMatch[1], 10);
|
|
288
|
+
const position = model.getPositionAt(pos);
|
|
289
|
+
markers.push({
|
|
290
|
+
severity: monaco.MarkerSeverity.Error,
|
|
291
|
+
message: message,
|
|
292
|
+
startLineNumber: position.lineNumber,
|
|
293
|
+
startColumn: position.column,
|
|
294
|
+
endLineNumber: position.lineNumber,
|
|
295
|
+
endColumn: position.column + 1,
|
|
296
|
+
});
|
|
297
|
+
} else {
|
|
298
|
+
// Fallback: put error at start
|
|
299
|
+
markers.push({
|
|
300
|
+
severity: monaco.MarkerSeverity.Error,
|
|
301
|
+
message: message,
|
|
302
|
+
startLineNumber: 1,
|
|
303
|
+
startColumn: 1,
|
|
304
|
+
endLineNumber: 1,
|
|
305
|
+
endColumn: 2,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
monaco.editor.setModelMarkers(model, "json-template", markers);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Run validation on content changes
|
|
315
|
+
const contentListener = model.onDidChangeContent(() => {
|
|
316
|
+
runCustomValidation();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Run initial validation
|
|
320
|
+
runCustomValidation();
|
|
321
|
+
|
|
322
|
+
disposablesRef.current.push(contentListener, {
|
|
323
|
+
dispose: () => {
|
|
324
|
+
// Re-enable Monaco's JSON validation when unmounted
|
|
325
|
+
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
|
326
|
+
validate: true,
|
|
327
|
+
});
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
// Track the template provider separately so we can update it
|
|
335
|
+
const templateProviderRef = React.useRef<{ dispose: () => void } | undefined>(
|
|
336
|
+
undefined,
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
// Cleanup on unmount
|
|
340
|
+
React.useEffect(() => {
|
|
341
|
+
return () => {
|
|
342
|
+
for (const d of disposablesRef.current) {
|
|
343
|
+
d.dispose();
|
|
344
|
+
}
|
|
345
|
+
disposablesRef.current = [];
|
|
346
|
+
if (templateProviderRef.current) {
|
|
347
|
+
templateProviderRef.current.dispose();
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}, []);
|
|
351
|
+
|
|
352
|
+
// Update template completion provider when templateProperties changes
|
|
353
|
+
React.useEffect(() => {
|
|
354
|
+
if (!monacoRef.current) return;
|
|
355
|
+
|
|
356
|
+
if (templateProviderRef.current) {
|
|
357
|
+
templateProviderRef.current.dispose();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!templateProperties || templateProperties.length === 0) return;
|
|
361
|
+
|
|
362
|
+
const monaco = monacoRef.current;
|
|
363
|
+
|
|
364
|
+
// Compute effective language - same logic as used for Editor
|
|
365
|
+
const hasTemplates = templateProperties && templateProperties.length > 0;
|
|
366
|
+
const providerLanguage =
|
|
367
|
+
hasTemplates && language === "json"
|
|
368
|
+
? "json-template"
|
|
369
|
+
: (languageMap[language] ?? language);
|
|
370
|
+
|
|
371
|
+
const provider = monaco.languages.registerCompletionItemProvider(
|
|
372
|
+
providerLanguage,
|
|
373
|
+
{
|
|
374
|
+
triggerCharacters: ["{"],
|
|
375
|
+
provideCompletionItems: (
|
|
376
|
+
model: editor.ITextModel,
|
|
377
|
+
position: Position,
|
|
378
|
+
) => {
|
|
379
|
+
// Get full content and cursor offset for utility functions
|
|
380
|
+
const content = model.getValue();
|
|
381
|
+
const cursorOffset = model.getOffsetAt(position);
|
|
382
|
+
|
|
383
|
+
// Use tested utility to detect if we're in an open template
|
|
384
|
+
const openTemplate = detectOpenTemplate({ content, cursorOffset });
|
|
385
|
+
|
|
386
|
+
if (!openTemplate.isInTemplate) {
|
|
387
|
+
return { suggestions: [] };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const query = openTemplate.query.toLowerCase();
|
|
391
|
+
const startColumn = openTemplate.startColumn;
|
|
392
|
+
|
|
393
|
+
// Check if Monaco auto-closed with }} after cursor using tested utility
|
|
394
|
+
const autoClosedBraces = detectAutoClosedBraces({
|
|
395
|
+
content,
|
|
396
|
+
cursorOffset,
|
|
397
|
+
});
|
|
398
|
+
const endColumn = position.column + autoClosedBraces;
|
|
399
|
+
|
|
400
|
+
const suggestions = templateProperties
|
|
401
|
+
.filter(
|
|
402
|
+
(prop) => query === "" || prop.path.toLowerCase().includes(query),
|
|
403
|
+
)
|
|
404
|
+
.map((prop, index) => ({
|
|
405
|
+
label: `{{${prop.path}}}`,
|
|
406
|
+
kind: monaco.languages.CompletionItemKind.Variable,
|
|
407
|
+
detail: prop.type,
|
|
408
|
+
documentation: prop.description,
|
|
409
|
+
insertText: `{{${prop.path}}}`,
|
|
410
|
+
// Use sortText starting with space to appear before other completions
|
|
411
|
+
sortText: ` ${String(index).padStart(4, "0")}`,
|
|
412
|
+
// Use the query as filterText so it matches what user typed
|
|
413
|
+
filterText: `{{${query}${prop.path}`,
|
|
414
|
+
preselect: index === 0, // Preselect first item
|
|
415
|
+
range: {
|
|
416
|
+
startLineNumber: position.lineNumber,
|
|
417
|
+
startColumn: startColumn,
|
|
418
|
+
endLineNumber: position.lineNumber,
|
|
419
|
+
endColumn: endColumn,
|
|
420
|
+
},
|
|
421
|
+
}));
|
|
422
|
+
|
|
423
|
+
return { suggestions, incomplete: false };
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
templateProviderRef.current = provider;
|
|
429
|
+
|
|
430
|
+
return () => {
|
|
431
|
+
provider.dispose();
|
|
432
|
+
};
|
|
433
|
+
}, [templateProperties, language, isEditorMounted]);
|
|
434
|
+
|
|
435
|
+
// Update type definitions when they change
|
|
436
|
+
React.useEffect(() => {
|
|
437
|
+
if (!monacoRef.current) return;
|
|
438
|
+
if (language !== "typescript" && language !== "javascript") return;
|
|
439
|
+
|
|
440
|
+
const monaco = monacoRef.current;
|
|
441
|
+
const defaults =
|
|
442
|
+
language === "typescript"
|
|
443
|
+
? monaco.languages.typescript.typescriptDefaults
|
|
444
|
+
: monaco.languages.typescript.javascriptDefaults;
|
|
445
|
+
|
|
446
|
+
// Configure compiler options to exclude DOM types but keep ES libs
|
|
447
|
+
defaults.setCompilerOptions({
|
|
448
|
+
target: monaco.languages.typescript.ScriptTarget.ESNext,
|
|
449
|
+
module: monaco.languages.typescript.ModuleKind.ESNext,
|
|
450
|
+
lib: ["esnext"], // Include ES libs but NOT DOM
|
|
451
|
+
allowNonTsExtensions: true,
|
|
452
|
+
noEmit: true,
|
|
453
|
+
strict: true,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// Suppress certain diagnostics that don't apply to our script context
|
|
457
|
+
defaults.setDiagnosticsOptions({
|
|
458
|
+
diagnosticCodesToIgnore: [1108], // "A 'return' statement can only be used within a function body"
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// Add type definitions (custom or default)
|
|
462
|
+
const definitions = typeDefinitions ?? DEFAULT_BACKEND_TYPE_DEFINITIONS;
|
|
463
|
+
const lib = defaults.addExtraLib(definitions, "file:///context.d.ts");
|
|
464
|
+
disposablesRef.current.push(lib);
|
|
465
|
+
|
|
466
|
+
return () => {
|
|
467
|
+
lib.dispose();
|
|
468
|
+
};
|
|
469
|
+
}, [typeDefinitions, language]);
|
|
470
|
+
|
|
471
|
+
// Calculate height from minHeight
|
|
472
|
+
const heightValue = minHeight.replace("px", "");
|
|
473
|
+
const numericHeight = Number.parseInt(heightValue, 10) || 100;
|
|
474
|
+
|
|
475
|
+
// Compute effective language - use json-template when we have templates for JSON
|
|
476
|
+
const hasTemplates = templateProperties && templateProperties.length > 0;
|
|
477
|
+
const effectiveLanguage =
|
|
478
|
+
hasTemplates && language === "json"
|
|
479
|
+
? "json-template"
|
|
480
|
+
: (languageMap[language] ?? language);
|
|
481
|
+
|
|
482
|
+
return (
|
|
483
|
+
<div
|
|
484
|
+
id={id}
|
|
485
|
+
className="w-full rounded-md border border-input bg-background font-mono text-sm focus-within:ring-2 focus-within:ring-ring focus-within:border-transparent transition-all box-border overflow-visible"
|
|
486
|
+
style={{ minHeight }}
|
|
487
|
+
>
|
|
488
|
+
<Editor
|
|
489
|
+
height={`${Math.max(numericHeight, 100)}px`}
|
|
490
|
+
language={effectiveLanguage}
|
|
491
|
+
value={value}
|
|
492
|
+
onChange={(newValue) => onChange(newValue ?? "")}
|
|
493
|
+
beforeMount={handleEditorWillMount}
|
|
494
|
+
onMount={handleEditorDidMount}
|
|
495
|
+
options={{
|
|
496
|
+
readOnly,
|
|
497
|
+
minimap: { enabled: false },
|
|
498
|
+
lineNumbers: "on",
|
|
499
|
+
lineNumbersMinChars: 3,
|
|
500
|
+
folding: false,
|
|
501
|
+
wordWrap: "on",
|
|
502
|
+
scrollBeyondLastLine: false,
|
|
503
|
+
fontSize: 14,
|
|
504
|
+
fontFamily: "ui-monospace, monospace",
|
|
505
|
+
tabSize: 2,
|
|
506
|
+
automaticLayout: true,
|
|
507
|
+
scrollbar: {
|
|
508
|
+
vertical: "auto",
|
|
509
|
+
horizontal: "auto",
|
|
510
|
+
verticalScrollbarSize: 10,
|
|
511
|
+
horizontalScrollbarSize: 10,
|
|
512
|
+
},
|
|
513
|
+
overviewRulerLanes: 0,
|
|
514
|
+
hideCursorInOverviewRuler: true,
|
|
515
|
+
overviewRulerBorder: false,
|
|
516
|
+
renderLineHighlight: "none",
|
|
517
|
+
contextmenu: false,
|
|
518
|
+
// Placeholder support via aria-label
|
|
519
|
+
ariaLabel: placeholder ?? "Code editor",
|
|
520
|
+
}}
|
|
521
|
+
theme="vs-dark"
|
|
522
|
+
loading={
|
|
523
|
+
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
|
524
|
+
Loading editor...
|
|
525
|
+
</div>
|
|
526
|
+
}
|
|
527
|
+
/>
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type { JsonSchemaProperty } from "../DynamicForm/types";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Options for generating TypeScript type definitions from JSON Schema.
|
|
5
|
+
*/
|
|
6
|
+
export interface GenerateTypesOptions {
|
|
7
|
+
/** For integration scripts - the event payload schema */
|
|
8
|
+
eventPayloadSchema?: JsonSchemaProperty;
|
|
9
|
+
|
|
10
|
+
/** For healthcheck scripts - the collector config schema */
|
|
11
|
+
collectorConfigSchema?: JsonSchemaProperty;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate TypeScript type definitions from JSON Schema.
|
|
16
|
+
* These are injected into Monaco Editor for real IntelliSense.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```typescript
|
|
20
|
+
* const typeDefinitions = generateTypeDefinitions({
|
|
21
|
+
* eventPayloadSchema: eventSchema,
|
|
22
|
+
* });
|
|
23
|
+
* <CodeEditor typeDefinitions={typeDefinitions} language="typescript" />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function generateTypeDefinitions(options: GenerateTypesOptions): string {
|
|
27
|
+
const lines: string[] = [];
|
|
28
|
+
|
|
29
|
+
// Integration script context (event-based)
|
|
30
|
+
if (options.eventPayloadSchema) {
|
|
31
|
+
const payloadType = jsonSchemaToTypeScript(options.eventPayloadSchema);
|
|
32
|
+
|
|
33
|
+
lines.push(`
|
|
34
|
+
/** Expected return type for integration scripts */
|
|
35
|
+
interface IntegrationScriptResult {
|
|
36
|
+
/** Optional external ID for tracking (e.g., ticket number, message ID) */
|
|
37
|
+
id?: string;
|
|
38
|
+
/** Optional external ID alias */
|
|
39
|
+
externalId?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Event being delivered to this integration script */
|
|
43
|
+
declare const context: {
|
|
44
|
+
/** Event information */
|
|
45
|
+
readonly event: {
|
|
46
|
+
/** Fully qualified event ID (e.g., "incident.created") */
|
|
47
|
+
readonly eventId: string;
|
|
48
|
+
/** Event payload data */
|
|
49
|
+
readonly payload: ${payloadType};
|
|
50
|
+
/** ISO timestamp when the event occurred */
|
|
51
|
+
readonly timestamp: string;
|
|
52
|
+
/** Unique delivery ID for this delivery attempt */
|
|
53
|
+
readonly deliveryId: string;
|
|
54
|
+
};
|
|
55
|
+
/** Subscription that triggered this delivery */
|
|
56
|
+
readonly subscription: {
|
|
57
|
+
/** Subscription ID */
|
|
58
|
+
readonly id: string;
|
|
59
|
+
/** Subscription name */
|
|
60
|
+
readonly name: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Healthcheck script context (config-based)
|
|
67
|
+
if (options.collectorConfigSchema) {
|
|
68
|
+
const configType = jsonSchemaToTypeScript(options.collectorConfigSchema);
|
|
69
|
+
|
|
70
|
+
lines.push(`
|
|
71
|
+
/** Expected return type for healthcheck scripts */
|
|
72
|
+
interface HealthCheckScriptResult {
|
|
73
|
+
/** Whether the health check passed */
|
|
74
|
+
success: boolean;
|
|
75
|
+
/** Optional status message */
|
|
76
|
+
message?: string;
|
|
77
|
+
/** Optional numeric value for metrics */
|
|
78
|
+
value?: number;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Context available in healthcheck inline scripts */
|
|
82
|
+
declare const context: {
|
|
83
|
+
/** Collector configuration */
|
|
84
|
+
readonly config: ${configType};
|
|
85
|
+
};
|
|
86
|
+
`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Always include console and fetch
|
|
90
|
+
lines.push(`
|
|
91
|
+
/** Console for logging (logs appear in delivery events) */
|
|
92
|
+
declare const console: {
|
|
93
|
+
/** Log an info message */
|
|
94
|
+
log(...args: unknown[]): void;
|
|
95
|
+
/** Log a warning message */
|
|
96
|
+
warn(...args: unknown[]): void;
|
|
97
|
+
/** Log an error message */
|
|
98
|
+
error(...args: unknown[]): void;
|
|
99
|
+
/** Log an info message */
|
|
100
|
+
info(...args: unknown[]): void;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/** Fetch API for making HTTP requests */
|
|
104
|
+
declare function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
|
|
105
|
+
`);
|
|
106
|
+
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Convert a JSON Schema to a TypeScript type string.
|
|
112
|
+
* Handles objects, arrays, primitives, and enums.
|
|
113
|
+
*/
|
|
114
|
+
function jsonSchemaToTypeScript(
|
|
115
|
+
schema: JsonSchemaProperty,
|
|
116
|
+
indent: number = 0,
|
|
117
|
+
): string {
|
|
118
|
+
const pad = " ".repeat(indent);
|
|
119
|
+
|
|
120
|
+
// Object type with properties
|
|
121
|
+
if (schema.type === "object" && schema.properties) {
|
|
122
|
+
const props = Object.entries(schema.properties)
|
|
123
|
+
.map(([key, propSchema]) => {
|
|
124
|
+
const optional = schema.required?.includes(key) ? "" : "?";
|
|
125
|
+
const type = jsonSchemaToTypeScript(propSchema, indent + 1);
|
|
126
|
+
const description = propSchema.description
|
|
127
|
+
? `/** ${propSchema.description} */\n${pad} `
|
|
128
|
+
: "";
|
|
129
|
+
return `${description}readonly ${key}${optional}: ${type};`;
|
|
130
|
+
})
|
|
131
|
+
.map((line) =>
|
|
132
|
+
line
|
|
133
|
+
.split("\n")
|
|
134
|
+
.map((l) => `${pad} ${l}`)
|
|
135
|
+
.join("\n"),
|
|
136
|
+
)
|
|
137
|
+
.join("\n");
|
|
138
|
+
return `{\n${props}\n${pad}}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Array type
|
|
142
|
+
if (schema.type === "array" && schema.items) {
|
|
143
|
+
const itemType = jsonSchemaToTypeScript(schema.items, indent);
|
|
144
|
+
return `readonly ${itemType}[]`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Enum type
|
|
148
|
+
if (schema.enum) {
|
|
149
|
+
return schema.enum.map((v) => JSON.stringify(v)).join(" | ");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Primitive types
|
|
153
|
+
if (schema.type === "string") return "string";
|
|
154
|
+
if (schema.type === "number" || schema.type === "integer") return "number";
|
|
155
|
+
if (schema.type === "boolean") return "boolean";
|
|
156
|
+
if (schema.type === "null") return "null";
|
|
157
|
+
|
|
158
|
+
// Record/dictionary type
|
|
159
|
+
if (schema.type === "object" && schema.additionalProperties) {
|
|
160
|
+
const valueType =
|
|
161
|
+
typeof schema.additionalProperties === "object"
|
|
162
|
+
? jsonSchemaToTypeScript(schema.additionalProperties, indent)
|
|
163
|
+
: "unknown";
|
|
164
|
+
return `Record<string, ${valueType}>`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fallback
|
|
168
|
+
return "unknown";
|
|
169
|
+
}
|
|
@@ -5,6 +5,7 @@ export {
|
|
|
5
5
|
type TemplateProperty,
|
|
6
6
|
} from "./CodeEditor";
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
export {
|
|
9
|
+
generateTypeDefinitions,
|
|
10
|
+
type GenerateTypesOptions,
|
|
11
|
+
} from "./generateTypeDefinitions";
|