@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.
Files changed (30) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/package.json +5 -12
  3. package/src/components/AnimatedNumber.tsx +48 -0
  4. package/src/components/CodeEditor/CodeEditor.tsx +14 -420
  5. package/src/components/CodeEditor/MonacoEditor.tsx +530 -0
  6. package/src/components/CodeEditor/generateTypeDefinitions.ts +169 -0
  7. package/src/components/CodeEditor/index.ts +4 -3
  8. package/src/components/CodeEditor/templateUtils.test.ts +87 -0
  9. package/src/components/CodeEditor/templateUtils.ts +81 -0
  10. package/src/components/DynamicForm/FormField.tsx +13 -7
  11. package/src/components/DynamicForm/MultiTypeEditorField.tsx +33 -0
  12. package/src/components/DynamicForm/utils.ts +3 -0
  13. package/src/hooks/useAnimatedNumber.ts +83 -0
  14. package/src/index.ts +2 -0
  15. package/src/components/CodeEditor/languageSupport/enterBehavior.test.ts +0 -173
  16. package/src/components/CodeEditor/languageSupport/enterBehavior.ts +0 -35
  17. package/src/components/CodeEditor/languageSupport/index.ts +0 -22
  18. package/src/components/CodeEditor/languageSupport/json-utils.ts +0 -117
  19. package/src/components/CodeEditor/languageSupport/json.test.ts +0 -274
  20. package/src/components/CodeEditor/languageSupport/json.ts +0 -139
  21. package/src/components/CodeEditor/languageSupport/markdown-utils.ts +0 -65
  22. package/src/components/CodeEditor/languageSupport/markdown.test.ts +0 -245
  23. package/src/components/CodeEditor/languageSupport/markdown.ts +0 -134
  24. package/src/components/CodeEditor/languageSupport/types.ts +0 -48
  25. package/src/components/CodeEditor/languageSupport/xml-utils.ts +0 -94
  26. package/src/components/CodeEditor/languageSupport/xml.test.ts +0 -239
  27. package/src/components/CodeEditor/languageSupport/xml.ts +0 -116
  28. package/src/components/CodeEditor/languageSupport/yaml-utils.ts +0 -101
  29. package/src/components/CodeEditor/languageSupport/yaml.test.ts +0 -203
  30. package/src/components/CodeEditor/languageSupport/yaml.ts +0 -120
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
1
1
  # @checkstack/ui
2
2
 
3
+ ## 1.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c842373: ## Animated Numbers & Availability Stats Live Updates
8
+
9
+ ### Features
10
+
11
+ - **AnimatedNumber component** (`@checkstack/ui`): New reusable component that displays numbers with a smooth "rolling" animation when values change. Uses `requestAnimationFrame` with eased interpolation for a polished effect.
12
+ - **useAnimatedNumber hook** (`@checkstack/ui`): Underlying hook for the animation logic, can be used directly for custom implementations.
13
+ - **Live availability updates**: Availability stats (31-day and 365-day) now automatically refresh when new health check runs are received via signals.
14
+
15
+ ### Usage
16
+
17
+ ```tsx
18
+ import { AnimatedNumber } from "@checkstack/ui";
19
+
20
+ <AnimatedNumber
21
+ value={99.95}
22
+ suffix="%"
23
+ decimals={2}
24
+ duration={500}
25
+ className="text-2xl font-bold text-green-500"
26
+ />;
27
+ ```
28
+
29
+ ## 1.0.0
30
+
31
+ ### Major Changes
32
+
33
+ - f676e11: Add script execution support and migrate CodeEditor to Monaco
34
+
35
+ **Integration providers** (`@checkstack/integration-script-backend`):
36
+
37
+ - **Script** - Execute TypeScript/JavaScript with context object
38
+ - **Bash** - Execute shell scripts with environment variables ($EVENT*ID, $PAYLOAD*\*)
39
+
40
+ **Health check collectors** (`@checkstack/healthcheck-script-backend`):
41
+
42
+ - **InlineScriptCollector** - Run TypeScript directly for health checks
43
+ - **ExecuteCollector** - Bash syntax highlighting for command field
44
+
45
+ **CodeEditor migration to Monaco** (`@checkstack/ui`):
46
+
47
+ - Replaced CodeMirror with Monaco Editor (VS Code's editor)
48
+ - Full TypeScript/JavaScript IntelliSense with custom type definitions
49
+ - Added `generateTypeDefinitions()` for JSON Schema → TypeScript conversion
50
+ - Removed all CodeMirror dependencies
51
+
52
+ **Type updates** (`@checkstack/common`):
53
+
54
+ - Added `javascript`, `typescript`, and `bash` to `EditorType` union
55
+
56
+ ### Patch Changes
57
+
58
+ - Updated dependencies [f676e11]
59
+ - @checkstack/common@0.6.2
60
+ - @checkstack/frontend-api@0.3.5
61
+
3
62
  ## 0.5.3
4
63
 
5
64
  ### Patch Changes
package/package.json CHANGED
@@ -1,31 +1,24 @@
1
1
  {
2
2
  "name": "@checkstack/ui",
3
- "version": "0.5.3",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
7
- "@checkstack/common": "0.6.1",
8
- "@checkstack/frontend-api": "0.3.4",
9
- "@codemirror/autocomplete": "^6.20.0",
10
- "@codemirror/lang-json": "^6.0.2",
11
- "@codemirror/lang-markdown": "^6.5.0",
12
- "@codemirror/lang-xml": "^6.1.0",
13
- "@codemirror/lang-yaml": "^6.1.2",
14
- "@codemirror/language": "^6.12.1",
15
- "@codemirror/state": "^6.5.4",
16
- "@codemirror/view": "^6.39.11",
7
+ "@checkstack/common": "0.6.2",
8
+ "@checkstack/frontend-api": "0.3.5",
9
+ "@monaco-editor/react": "^4.7.0",
17
10
  "@radix-ui/react-accordion": "^1.2.12",
18
11
  "@radix-ui/react-dialog": "^1.1.15",
19
12
  "@radix-ui/react-popover": "^1.1.15",
20
13
  "@radix-ui/react-select": "^2.2.6",
21
14
  "@radix-ui/react-slot": "^1.2.4",
22
- "@uiw/react-codemirror": "^4.25.4",
23
15
  "ajv": "^8.17.1",
24
16
  "ajv-formats": "^3.0.1",
25
17
  "class-variance-authority": "^0.7.1",
26
18
  "clsx": "^2.1.0",
27
19
  "date-fns": "^4.1.0",
28
20
  "lucide-react": "0.562.0",
21
+ "monaco-editor": "^0.55.1",
29
22
  "react": "^18.2.0",
30
23
  "react-day-picker": "^9.13.0",
31
24
  "react-markdown": "^10.1.0",
@@ -0,0 +1,48 @@
1
+ import { useAnimatedNumber } from "../hooks/useAnimatedNumber";
2
+
3
+ interface AnimatedNumberProps {
4
+ /** The number value to display (undefined for N/A) */
5
+ value: number | undefined;
6
+ /** Animation duration in milliseconds (default: 500ms) */
7
+ duration?: number;
8
+ /** Number of decimal places (default: 2) */
9
+ decimals?: number;
10
+ /** Suffix to append after the number (e.g., "%", "ms") */
11
+ suffix?: string;
12
+ /** CSS classes for the number span */
13
+ className?: string;
14
+ /** CSS classes for the suffix span */
15
+ suffixClassName?: string;
16
+ }
17
+
18
+ /**
19
+ * Component that displays an animated number with smooth rolling effect.
20
+ * Numbers smoothly interpolate from their previous value to the new value.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * <AnimatedNumber
25
+ * value={99.95}
26
+ * suffix="%"
27
+ * className="text-2xl font-bold text-green-500"
28
+ * />
29
+ * ```
30
+ */
31
+ export function AnimatedNumber({
32
+ value,
33
+ duration = 500,
34
+ decimals = 2,
35
+ suffix,
36
+ className = "",
37
+ suffixClassName = "",
38
+ }: AnimatedNumberProps) {
39
+ const displayValue = useAnimatedNumber(value, duration, decimals);
40
+ const isNA = value === undefined;
41
+
42
+ return (
43
+ <span className={`tabular-nums ${className}`}>
44
+ {displayValue}
45
+ {!isNA && suffix && <span className={suffixClassName}>{suffix}</span>}
46
+ </span>
47
+ );
48
+ }
@@ -1,420 +1,14 @@
1
- import React from "react";
2
- import CodeMirror from "@uiw/react-codemirror";
3
- import {
4
- EditorView,
5
- ViewPlugin,
6
- Decoration,
7
- keymap,
8
- type ViewUpdate,
9
- type DecorationSet,
10
- } from "@codemirror/view";
11
- import { RangeSetBuilder, Prec } from "@codemirror/state";
12
- import {
13
- autocompletion,
14
- completionStatus,
15
- type CompletionContext,
16
- type CompletionResult,
17
- } from "@codemirror/autocomplete";
18
- import { indentUnit, getIndentUnit, indentString } from "@codemirror/language";
19
- import {
20
- jsonLanguageSupport,
21
- yamlLanguageSupport,
22
- xmlLanguageSupport,
23
- markdownLanguageSupport,
24
- isBetweenBrackets,
25
- type LanguageSupport,
26
- } from "./languageSupport";
27
-
28
- export type CodeEditorLanguage = "json" | "yaml" | "xml" | "markdown";
29
-
30
- /**
31
- * A single payload property available for templating
32
- */
33
- export interface TemplateProperty {
34
- /** Full path to the property, e.g., "payload.incident.title" */
35
- path: string;
36
- /** Type of the property, e.g., "string", "number", "boolean" */
37
- type: string;
38
- /** Optional description of the property */
39
- description?: string;
40
- }
41
-
42
- export interface CodeEditorProps {
43
- /** Unique identifier for the editor */
44
- id?: string;
45
- /** Current value of the editor */
46
- value: string;
47
- /** Callback when the value changes */
48
- onChange: (value: string) => void;
49
- /** Language for syntax highlighting */
50
- language?: CodeEditorLanguage;
51
- /** Minimum height of the editor */
52
- minHeight?: string;
53
- /** Whether the editor is read-only */
54
- readOnly?: boolean;
55
- /** Placeholder text when empty */
56
- placeholder?: 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
- // Language support registry - add new languages here
65
- const languageRegistry: Record<CodeEditorLanguage, LanguageSupport> = {
66
- json: jsonLanguageSupport,
67
- yaml: yamlLanguageSupport,
68
- xml: xmlLanguageSupport,
69
- markdown: markdownLanguageSupport,
70
- };
71
-
72
- /**
73
- * Get display type with color info for autocomplete
74
- */
75
- function getTypeInfo(type: string): string {
76
- return type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
77
- }
78
-
79
- /**
80
- * Create a ViewPlugin for template-aware syntax highlighting.
81
- * Uses the language's buildDecorations function to generate decorations.
82
- */
83
- function createTemplateHighlighter(languageSupport: LanguageSupport) {
84
- return ViewPlugin.fromClass(
85
- class {
86
- decorations: DecorationSet;
87
-
88
- constructor(view: EditorView) {
89
- this.decorations = this.buildDecorations(view);
90
- }
91
-
92
- buildDecorations(view: EditorView): DecorationSet {
93
- const builder = new RangeSetBuilder<Decoration>();
94
- const doc = view.state.doc.toString();
95
- const ranges = languageSupport.buildDecorations(doc);
96
-
97
- for (const range of ranges) {
98
- builder.add(range.from, range.to, range.decoration);
99
- }
100
-
101
- return builder.finish();
102
- }
103
-
104
- update(update: ViewUpdate) {
105
- if (update.docChanged || update.viewportChanged) {
106
- this.decorations = this.buildDecorations(update.view);
107
- }
108
- }
109
- },
110
- {
111
- decorations: (v) => v.decorations,
112
- },
113
- );
114
- }
115
-
116
- /**
117
- * Create a CodeMirror autocomplete extension for template properties.
118
- * Triggers when user types "{{" and offers completions from templateProperties.
119
- */
120
- function createTemplateAutocomplete(
121
- templateProperties: TemplateProperty[],
122
- languageSupport: LanguageSupport,
123
- ): ReturnType<typeof autocompletion> {
124
- return autocompletion({
125
- override: [
126
- (context: CompletionContext): CompletionResult | null => {
127
- // Look for {{ pattern before cursor
128
- const textBefore = context.state.sliceDoc(0, context.pos);
129
- const recentText = context.state.sliceDoc(
130
- Math.max(0, context.pos - 50),
131
- context.pos,
132
- );
133
-
134
- // Find the last {{ that doesn't have a matching }}
135
- const lastOpenBrace = recentText.lastIndexOf("{{");
136
- const lastCloseBrace = recentText.lastIndexOf("}}");
137
-
138
- if (lastOpenBrace === -1 || lastOpenBrace < lastCloseBrace) {
139
- // eslint-disable-next-line unicorn/no-null -- CodeMirror API requires null
140
- return null;
141
- }
142
-
143
- // Validate position based on text BEFORE the {{ started
144
- // We exclude the {{ from validation because it confuses most parsers
145
- const positionOfTemplateStart =
146
- context.pos - recentText.length + lastOpenBrace;
147
- const textBeforeTemplate = textBefore.slice(0, positionOfTemplateStart);
148
- if (!languageSupport.isValidTemplatePosition(textBeforeTemplate)) {
149
- // eslint-disable-next-line unicorn/no-null -- CodeMirror API requires null
150
- return null;
151
- }
152
-
153
- // Calculate the position in the document where {{ starts
154
- const startOffset = context.pos - recentText.length + lastOpenBrace;
155
- const query = recentText.slice(lastOpenBrace + 2).toLowerCase();
156
-
157
- // Check for auto-closed braces after cursor position
158
- // When user types {{ with bracket auto-close, it becomes {{}}
159
- // We need to consume any trailing }} when completing
160
- const textAfter = context.state.sliceDoc(context.pos, context.pos + 4);
161
- let endOffset = context.pos;
162
- if (textAfter.startsWith("}}")) {
163
- endOffset += 2;
164
- } else if (textAfter.startsWith("}")) {
165
- // Just one } from first auto-close
166
- endOffset += 1;
167
- }
168
-
169
- // Filter properties based on query
170
- const filtered = templateProperties.filter((prop) =>
171
- prop.path.toLowerCase().includes(query),
172
- );
173
-
174
- if (filtered.length === 0 && query.length > 0) {
175
- // eslint-disable-next-line unicorn/no-null -- CodeMirror API requires null
176
- return null;
177
- }
178
-
179
- return {
180
- from: startOffset,
181
- to: endOffset,
182
- options: filtered.map((prop) => ({
183
- label: `{{${prop.path}}}`,
184
- displayLabel: prop.path,
185
- type: "variable",
186
- detail: getTypeInfo(prop.type),
187
- info: prop.description,
188
- boost: prop.path.toLowerCase().startsWith(query) ? 1 : 0,
189
- })),
190
- validFor: /^\{\{[\w.]*$/,
191
- };
192
- },
193
- ],
194
- activateOnTyping: true,
195
- icons: false,
196
- });
197
- }
198
-
199
- /**
200
- * A code editor component with syntax highlighting and optional template autocomplete.
201
- * Wraps @uiw/react-codemirror for consistent styling and API across the platform.
202
- */
203
- export const CodeEditor: React.FC<CodeEditorProps> = ({
204
- id,
205
- value,
206
- onChange,
207
- language = "json",
208
- minHeight = "100px",
209
- readOnly = false,
210
- placeholder,
211
- templateProperties,
212
- }) => {
213
- const extensions = React.useMemo(() => {
214
- const languageSupport = languageRegistry[language];
215
- const hasTemplates = templateProperties && templateProperties.length > 0;
216
-
217
- const exts = [
218
- EditorView.lineWrapping,
219
- EditorView.theme({
220
- "&": {
221
- fontSize: "14px",
222
- fontFamily: "ui-monospace, monospace",
223
- backgroundColor: "transparent",
224
- },
225
- ".cm-scroller": {
226
- backgroundColor: "transparent",
227
- overflow: "auto",
228
- },
229
- // minHeight must be on .cm-content and .cm-gutter, not the wrapper (per CM docs)
230
- ".cm-content, .cm-gutter": {
231
- minHeight: minHeight,
232
- },
233
- ".cm-content": {
234
- padding: "10px",
235
- },
236
- // Cursor/caret styling
237
- ".cm-cursor, .cm-dropCursor": {
238
- borderLeftColor: "hsl(var(--foreground))",
239
- borderLeftWidth: "2px",
240
- },
241
- ".cm-line": {
242
- color: "hsl(var(--foreground))",
243
- },
244
- ".cm-gutters": {
245
- backgroundColor: "transparent",
246
- color: "hsl(var(--muted-foreground))",
247
- border: "none",
248
- },
249
- "&.cm-focused": {
250
- outline: "none",
251
- },
252
- // JSON syntax highlighting for dark/light mode
253
- ".cm-string": {
254
- color: "hsl(142.1, 76.2%, 36.3%)",
255
- },
256
- ".cm-number": {
257
- color: "hsl(217.2, 91.2%, 59.8%)",
258
- },
259
- ".cm-propertyName": {
260
- color: "hsl(280, 65%, 60%)",
261
- },
262
- ".cm-keyword": {
263
- color: "hsl(280, 65%, 60%)",
264
- },
265
- ".cm-punctuation": {
266
- color: "hsl(var(--muted-foreground))",
267
- },
268
- // Placeholder styling
269
- ".cm-placeholder": {
270
- color: "hsl(var(--muted-foreground))",
271
- },
272
- // JSON syntax highlighting via decorations (overrides Lezer parser)
273
- ".cm-json-property": {
274
- color: "hsl(280, 65%, 60%)",
275
- },
276
- ".cm-json-string": {
277
- color: "hsl(142.1, 76.2%, 36.3%)",
278
- },
279
- ".cm-json-number": {
280
- color: "hsl(217.2, 91.2%, 59.8%)",
281
- },
282
- ".cm-json-keyword": {
283
- color: "hsl(280, 65%, 60%)",
284
- },
285
- // Template expression highlighting
286
- ".cm-template-expression": {
287
- color: "hsl(190, 70%, 50%)",
288
- fontWeight: "500",
289
- },
290
- // Style for autocomplete popup
291
- ".cm-tooltip.cm-tooltip-autocomplete": {
292
- backgroundColor: "hsl(var(--popover))",
293
- border: "1px solid hsl(var(--border))",
294
- borderRadius: "0.375rem",
295
- boxShadow:
296
- "0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)",
297
- },
298
- ".cm-tooltip-autocomplete ul": {
299
- fontFamily: "ui-monospace, monospace",
300
- fontSize: "0.75rem",
301
- },
302
- ".cm-tooltip-autocomplete ul li": {
303
- padding: "0.25rem 0.5rem",
304
- },
305
- ".cm-tooltip-autocomplete ul li[aria-selected]": {
306
- backgroundColor: "hsl(var(--accent))",
307
- color: "hsl(var(--accent-foreground))",
308
- },
309
- ".cm-completionLabel": {
310
- fontFamily: "ui-monospace, monospace",
311
- },
312
- ".cm-completionDetail": {
313
- marginLeft: "0.5rem",
314
- fontStyle: "normal",
315
- color: "hsl(var(--muted-foreground))",
316
- },
317
- }),
318
- ];
319
-
320
- // Always add language extension for features (indentation, bracket matching, etc.)
321
- if (languageSupport) {
322
- exts.push(languageSupport.extension);
323
-
324
- // Create custom Enter key handler that applies our indentation
325
- // This is needed because the language parsers may get confused by templates
326
- // or may not provide the indentation we want
327
- const customEnterKeymap = keymap.of([
328
- {
329
- key: "Enter",
330
- run: (view) => {
331
- const state = view.state;
332
-
333
- // If autocomplete is active, let it handle Enter for selection
334
- if (completionStatus(state) === "active") {
335
- return false;
336
- }
337
-
338
- const pos = state.selection.main.head;
339
- const textBefore = state.sliceDoc(0, pos);
340
- const textAfter = state.sliceDoc(pos);
341
- const unit = getIndentUnit(state);
342
- const indent = languageSupport.calculateIndentation(
343
- textBefore + "\n",
344
- unit,
345
- );
346
- const indentStr = indentString(state, indent);
347
-
348
- // Check if we're between matching brackets/tags
349
- // This handles cases like: {|}, [|], <tag>|</tag>
350
- if (isBetweenBrackets(textBefore, textAfter)) {
351
- // Split: add newline with indent, then newline with previous indent for closing
352
- const prevIndent = Math.max(0, indent - unit);
353
- const prevIndentStr = indentString(state, prevIndent);
354
-
355
- view.dispatch({
356
- changes: {
357
- from: pos,
358
- to: pos,
359
- insert: "\n" + indentStr + "\n" + prevIndentStr,
360
- },
361
- selection: { anchor: pos + 1 + indentStr.length },
362
- scrollIntoView: true,
363
- userEvent: "input",
364
- });
365
- } else {
366
- // Normal: insert newline with calculated indentation
367
- view.dispatch({
368
- changes: { from: pos, to: pos, insert: "\n" + indentStr },
369
- selection: { anchor: pos + 1 + indentStr.length },
370
- scrollIntoView: true,
371
- userEvent: "input",
372
- });
373
- }
374
- return true;
375
- },
376
- },
377
- ]);
378
-
379
- // Always add indentation support and custom highlighter for consistent behavior
380
- // Prec.highest ensures our colors take precedence over language parser output
381
- exts.push(
382
- indentUnit.of(" "), // Configure 2-space indentation
383
- Prec.highest(customEnterKeymap), // Override default Enter behavior
384
- Prec.highest(createTemplateHighlighter(languageSupport)), // Consistent syntax colors
385
- );
386
-
387
- // Add template autocomplete if properties provided
388
- if (hasTemplates) {
389
- exts.push(
390
- createTemplateAutocomplete(templateProperties, languageSupport),
391
- );
392
- }
393
- }
394
-
395
- return exts;
396
- }, [language, templateProperties, minHeight]);
397
-
398
- return (
399
- <div
400
- id={id}
401
- 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"
402
- >
403
- <CodeMirror
404
- value={value}
405
- onChange={onChange}
406
- extensions={extensions}
407
- editable={!readOnly}
408
- placeholder={placeholder}
409
- basicSetup={{
410
- lineNumbers: true,
411
- foldGutter: false,
412
- highlightActiveLine: false,
413
- highlightSelectionMatches: false,
414
- autocompletion: false, // We use our own
415
- }}
416
- theme="none"
417
- />
418
- </div>
419
- );
420
- };
1
+ // Monaco-based CodeEditor
2
+ // Re-export all from MonacoEditor as the new CodeEditor implementation
3
+
4
+ export {
5
+ CodeEditor,
6
+ type CodeEditorProps,
7
+ type CodeEditorLanguage,
8
+ type TemplateProperty,
9
+ } from "./MonacoEditor";
10
+
11
+ export {
12
+ generateTypeDefinitions,
13
+ type GenerateTypesOptions,
14
+ } from "./generateTypeDefinitions";