@motiadev/workbench 0.4.3-beta.96 → 0.4.4-beta.97

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.
@@ -16,5 +16,5 @@ export const EndpointBodyPanel = ({ endpoint, onChange, onValidate }) => {
16
16
  if (!shouldHaveBody) {
17
17
  return null;
18
18
  }
19
- return (_jsx(Panel, { title: "Body", size: "sm", contentClassName: "p-0", "data-testid": "endpoint-body-panel", children: onChange ? (_jsx(JsonEditor, { value: body, schema: endpoint.bodySchema, onChange: handleBodyChange, onValidate: onValidate })) : (_jsx(ReactJson, { src: convertJsonSchemaToJson(endpoint.bodySchema ?? {}), theme: "default", enableClipboard: false })) }));
19
+ return (_jsx(Panel, { title: "Body", size: "sm", contentClassName: "p-0", "data-testid": "endpoint-body-panel", children: onChange ? (_jsx(JsonEditor, { value: body, schema: endpoint.bodySchema, onChange: handleBodyChange, onValidate: onValidate })) : (_jsx(ReactJson, { src: convertJsonSchemaToJson(endpoint.bodySchema), theme: "default", enableClipboard: false, style: { backgroundColor: 'transparent' } })) }));
20
20
  };
@@ -1,5 +1,5 @@
1
- import { FC } from 'react';
2
1
  import { ApiEndpoint } from '@/types/endpoint';
2
+ import { FC } from 'react';
3
3
  type Props = {
4
4
  endpoint: ApiEndpoint;
5
5
  onChange?: (pathParamsValues: Record<string, string>) => void;
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Input, Panel } from '@motiadev/ui';
3
- import { Fragment, useMemo, useState } from 'react';
3
+ import { Fragment, useState } from 'react';
4
4
  import { usePathParams } from './hooks/use-path-params';
5
5
  export const EndpointPathParamsPanel = ({ endpoint, onChange }) => {
6
6
  const pathParams = usePathParams(endpoint.path);
@@ -10,19 +10,8 @@ export const EndpointPathParamsPanel = ({ endpoint, onChange }) => {
10
10
  setPathParamsValues(newPathParamsValues);
11
11
  onChange?.(newPathParamsValues);
12
12
  };
13
- const subtitle = useMemo(() => {
14
- if (onChange) {
15
- return Object.entries(pathParamsValues).reduce((acc, [param, value]) => {
16
- if (!value) {
17
- return acc;
18
- }
19
- return acc.replace(`:${param}`, value);
20
- }, endpoint.path);
21
- }
22
- return endpoint.path;
23
- }, [pathParamsValues, endpoint.path, onChange]);
24
13
  if (!pathParams.length) {
25
14
  return null;
26
15
  }
27
- return (_jsx(Panel, { title: "Path params", subtitle: subtitle, size: "sm", variant: "outlined", children: _jsx("div", { className: "grid gap-3", style: { gridTemplateColumns: '1fr 2fr' }, children: pathParams.map((param) => (_jsxs(Fragment, { children: [_jsx("div", { className: "font-bold leading-[36px] flex text-xs", children: param }), _jsx("div", { className: "flex items-center text-xs", children: onChange ? (_jsx(Input, { className: "w-full text-xs", placeholder: `:${param}`, value: pathParamsValues[param], onChange: (e) => onPathParamChange(param, e.target.value) })) : (_jsx("span", { className: "text-xs", children: `:${param}` })) })] }, param))) }) }));
16
+ return (_jsx(Panel, { title: "Path params", size: "sm", variant: "default", children: _jsx("div", { className: "grid gap-3", style: { gridTemplateColumns: '1fr 2fr' }, children: pathParams.map((param) => (_jsxs(Fragment, { children: [_jsx("div", { className: "font-bold leading-[36px] flex text-xs", children: param }), _jsx("div", { className: "flex items-center text-xs", children: onChange && (_jsx(Input, { className: "w-full text-xs", placeholder: param, value: pathParamsValues[param], onChange: (e) => onPathParamChange(param, e.target.value) })) })] }, param))) }) }));
28
17
  };
@@ -1 +1 @@
1
- export declare const convertJsonSchemaToJson: (schema: Record<string, any>) => any;
1
+ export declare const convertJsonSchemaToJson: (schema?: Record<string, any>) => any;
@@ -1,7 +1,7 @@
1
1
  import { FC } from 'react';
2
2
  type JsonEditorProps = {
3
3
  value: string;
4
- schema: Record<string, unknown> | undefined;
4
+ schema?: Record<string, unknown>;
5
5
  onChange: (value: string) => void;
6
6
  onValidate?: (isValid: boolean) => void;
7
7
  };
@@ -7,17 +7,19 @@ export const JsonEditor = ({ value, schema, onChange, onValidate }) => {
7
7
  const theme = useThemeStore((state) => state.theme);
8
8
  const editorTheme = useMemo(() => (theme === 'dark' ? 'vs-dark' : 'light'), [theme]);
9
9
  useEffect(() => {
10
- if (!monaco || !schema)
10
+ if (!monaco)
11
11
  return;
12
12
  monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
13
13
  validate: true,
14
- schemas: [
15
- {
16
- uri: window.location.href,
17
- fileMatch: ['*'],
18
- schema,
19
- },
20
- ],
14
+ schemas: schema
15
+ ? [
16
+ {
17
+ uri: window.location.href,
18
+ fileMatch: ['*'],
19
+ schema,
20
+ },
21
+ ]
22
+ : [],
21
23
  });
22
24
  }, [monaco, schema]);
23
25
  return (_jsx(Editor, { "data-testid": "json-editor", height: "200px", language: "json", value: value, theme: editorTheme, onChange: (value) => {
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { StateItem } from './hooks/states-hooks';
3
+ type Props = {
4
+ state: StateItem;
5
+ };
6
+ export declare const StateDetails: React.FC<Props>;
7
+ export {};
@@ -0,0 +1,34 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Badge } from '@/components/ui/badge';
3
+ import { Panel } from '@motiadev/ui';
4
+ import { Copy, Database, Hash } from 'lucide-react';
5
+ import { useMemo } from 'react';
6
+ import JsonView from 'react18-json-view';
7
+ export const StateDetails = ({ state }) => {
8
+ const stateMetadata = useMemo(() => {
9
+ const valueSize = JSON.stringify(state.value).length;
10
+ const sizeLabel = valueSize < 1024 ? `${valueSize} bytes` : `${(valueSize / 1024).toFixed(1)} KB`;
11
+ return {
12
+ size: sizeLabel,
13
+ isComplex: typeof state.value === 'object' && state.value !== null,
14
+ hasNesting: typeof state.value === 'object' &&
15
+ state.value !== null &&
16
+ Object.values(state.value).some((v) => typeof v === 'object'),
17
+ };
18
+ }, [state.value]);
19
+ const copyToClipboard = async () => {
20
+ try {
21
+ await navigator.clipboard.writeText(JSON.stringify(state.value, null, 2));
22
+ }
23
+ catch (err) {
24
+ console.error('Failed to copy to clipboard:', err);
25
+ }
26
+ };
27
+ return (_jsxs(Panel, { title: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Database, { className: "w-4 h-4" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "State Overview" })] }), actions: [
28
+ {
29
+ icon: _jsx(Copy, {}),
30
+ onClick: copyToClipboard,
31
+ label: 'Copy',
32
+ },
33
+ ], children: [_jsxs("div", { className: "space-y-3", children: [_jsxs("div", { className: "grid grid-cols-2 gap-2 text-xs", children: [_jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "text-muted-foreground font-medium", children: "Type" }), _jsx("div", { children: _jsx(Badge, { variant: "secondary", className: "text-xs", children: state.type }) })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "text-muted-foreground font-medium", children: "Size" }), _jsx("div", { className: "text-foreground font-mono", children: stateMetadata.size })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "text-muted-foreground font-medium", children: "Group ID" }), _jsx("div", { className: "text-foreground font-mono text-xs bg-background px-2 py-1 rounded border", children: state.groupId })] }), _jsxs("div", { className: "space-y-1", children: [_jsx("span", { className: "text-muted-foreground font-medium", children: "Key" }), _jsxs("div", { className: "text-foreground font-mono text-xs bg-background px-2 py-1 rounded border flex items-center gap-1", children: [_jsx(Hash, { className: "w-3 h-3" }), state.key] })] })] }), stateMetadata.isComplex && (_jsx("div", { className: "pt-2 border-t border-border/50", children: _jsxs("div", { className: "flex gap-2", children: [_jsx(Badge, { variant: "outline", className: "text-xs", children: stateMetadata.hasNesting ? 'Nested Object' : 'Simple Object' }), Array.isArray(state.value) && (_jsxs(Badge, { variant: "outline", className: "text-xs", children: [state.value.length, " items"] }))] }) }))] }), _jsxs("div", { className: "space-y-2", children: [_jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Value Structure" }), _jsx("div", { className: "bg-background border border-border rounded-lg p-3 overflow-auto max-h-[400px]", children: _jsx(JsonView, { src: state.value, theme: "default", enableClipboard: false, collapsed: 2 }) })] })] }));
34
+ };
@@ -0,0 +1,7 @@
1
+ import React from 'react';
2
+ import { StateItem } from './hooks/states-hooks';
3
+ type Props = {
4
+ state: StateItem;
5
+ };
6
+ export declare const StateEditor: React.FC<Props>;
7
+ export {};
@@ -0,0 +1,62 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Button, Panel } from '@motiadev/ui';
3
+ import { AlertCircle, Check, Database, Loader2, Save } from 'lucide-react';
4
+ import { useState, useCallback, useEffect, useRef } from 'react';
5
+ import { JsonEditor } from '../endpoints/json-editor';
6
+ export const StateEditor = ({ state }) => {
7
+ const [isRequestLoading, setIsRequestLoading] = useState(false);
8
+ const [isValid, setIsValid] = useState(true);
9
+ const [jsonValue, setJsonValue] = useState(JSON.stringify(state.value, null, 2));
10
+ const [hasChanges, setHasChanges] = useState(false);
11
+ const [saveStatus, setSaveStatus] = useState('idle');
12
+ const lastSavedValue = useRef(JSON.stringify(state.value, null, 2));
13
+ useEffect(() => {
14
+ setJsonValue(JSON.stringify(state.value, null, 2));
15
+ }, [state.value]);
16
+ const handleJsonChange = useCallback((value) => {
17
+ setHasChanges(value !== lastSavedValue.current);
18
+ setJsonValue(value);
19
+ setSaveStatus('idle');
20
+ }, []);
21
+ const handleSave = async () => {
22
+ if (!isValid || !hasChanges)
23
+ return;
24
+ try {
25
+ setIsRequestLoading(true);
26
+ setSaveStatus('idle');
27
+ const response = await fetch('/motia/state', {
28
+ method: 'POST',
29
+ headers: {
30
+ 'Content-Type': 'application/json',
31
+ },
32
+ body: JSON.stringify({
33
+ key: state.key,
34
+ groupId: state.groupId,
35
+ value: JSON.parse(jsonValue),
36
+ }),
37
+ });
38
+ if (!response.ok) {
39
+ throw new Error(`HTTP error! status: ${response.status}`);
40
+ }
41
+ lastSavedValue.current = jsonValue;
42
+ setSaveStatus('success');
43
+ setHasChanges(false);
44
+ setTimeout(() => {
45
+ setSaveStatus('idle');
46
+ }, 3000);
47
+ }
48
+ catch (error) {
49
+ console.error('Failed to save state:', error);
50
+ setSaveStatus('error');
51
+ }
52
+ finally {
53
+ setIsRequestLoading(false);
54
+ }
55
+ };
56
+ const resetChanges = useCallback(() => {
57
+ setJsonValue(JSON.stringify(state.value, null, 2));
58
+ setHasChanges(false);
59
+ setSaveStatus('idle');
60
+ }, [state.value]);
61
+ return (_jsxs(Panel, { title: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Database, { className: "w-4 h-4" }), _jsx("h3", { className: "text-sm font-semibold text-foreground", children: "Edit State Value" })] }), children: [_jsx("p", { className: "text-xs text-muted-foreground mb-3", children: "Modify the state value using the JSON editor below. Changes will be saved to the system when you click Save." }), _jsxs("div", { className: "text-xs text-muted-foreground space-y-1", children: [_jsxs("div", { children: [_jsx("strong", { children: "Group:" }), " ", _jsx("code", { className: "bg-background px-1 py-0.5 rounded", children: state.groupId })] }), _jsxs("div", { children: [_jsx("strong", { children: "Key:" }), " ", _jsx("code", { className: "bg-background px-1 py-0.5 rounded", children: state.key })] })] }), _jsxs("div", { className: "space-y-3 pt-2", children: [_jsxs("div", { className: "relative", children: [_jsx(JsonEditor, { value: jsonValue, onChange: handleJsonChange, onValidate: setIsValid }), !isValid && (_jsxs("div", { className: "absolute top-2 right-2 bg-destructive/90 text-destructive-foreground px-2 py-1 rounded text-xs flex items-center gap-1", children: [_jsx(AlertCircle, { className: "w-3 h-3" }), "Invalid JSON"] }))] }), saveStatus === 'success' && (_jsx("div", { className: "bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg p-3", children: _jsxs("div", { className: "flex items-center gap-2 text-green-700 dark:text-green-400 text-sm", children: [_jsx(Check, { className: "w-4 h-4" }), "State saved successfully!"] }) })), saveStatus === 'error' && (_jsx("div", { className: "bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-3", children: _jsxs("div", { className: "flex items-center gap-2 text-red-700 dark:text-red-400 text-sm", children: [_jsx(AlertCircle, { className: "w-4 h-4" }), "Failed to save state. Please try again."] }) }))] }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [_jsx("div", { className: "text-xs text-muted-foreground", children: hasChanges ? (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-2 h-2 bg-orange-500 rounded-full" }), "Unsaved changes"] })) : (_jsxs("span", { className: "flex items-center gap-1", children: [_jsx("div", { className: "w-2 h-2 bg-green-500 rounded-full" }), "Up to date"] })) }), _jsxs("div", { className: "flex items-center gap-2", children: [hasChanges && (_jsx(Button, { variant: "secondary", onClick: resetChanges, disabled: isRequestLoading, children: "Reset" })), _jsx(Button, { onClick: handleSave, variant: "accent", disabled: isRequestLoading || !isValid || !hasChanges, "data-testid": "state-save-button", children: isRequestLoading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "w-3 h-3 animate-spin mr-1" }), "Saving..."] })) : (_jsxs(_Fragment, { children: [_jsx(Save, { className: "w-3 h-3 mr-1" }), "Save Changes"] })) })] })] })] }));
62
+ };
@@ -4,5 +4,5 @@ type Props = {
4
4
  state: StateItem;
5
5
  onClose: () => void;
6
6
  };
7
- export declare const StateDetail: React.FC<Props>;
7
+ export declare const StateSidebar: React.FC<Props>;
8
8
  export {};
@@ -0,0 +1,17 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Sidebar } from '@/components/sidebar/sidebar';
3
+ import { X } from 'lucide-react';
4
+ import { StateDetails } from './state-details';
5
+ import { StateEditor } from './state-editor';
6
+ export const StateSidebar = ({ state, onClose }) => {
7
+ return (_jsx(Sidebar, { onClose: onClose, title: "State Details", initialWidth: 500, tabs: [
8
+ {
9
+ label: 'Overview',
10
+ content: _jsx(StateDetails, { state: state }),
11
+ },
12
+ {
13
+ label: 'Editor',
14
+ content: _jsx(StateEditor, { state: state }),
15
+ },
16
+ ], actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }] }));
17
+ };
@@ -4,7 +4,7 @@ import { cn } from '@motiadev/ui';
4
4
  import { useMemo } from 'react';
5
5
  import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
6
6
  import { useGetStateItems } from './hooks/states-hooks';
7
- import { StateDetail } from './state-detail';
7
+ import { StateSidebar } from './state-sidebar';
8
8
  export const StatesPage = () => {
9
9
  const selectedStateId = useGlobalStore((state) => state.selectedStateId);
10
10
  const selectStateId = useGlobalStore((state) => state.selectStateId);
@@ -12,7 +12,7 @@ export const StatesPage = () => {
12
12
  const selectedItem = useMemo(() => (selectedStateId ? items.find((item) => `${item.groupId}:${item.key}` === selectedStateId) : null), [items, selectedStateId]);
13
13
  const handleRowClick = (item) => selectStateId(`${item.groupId}:${item.key}`);
14
14
  const onClose = () => selectStateId(undefined);
15
- return (_jsxs("div", { className: "flex flex-row gap-4 h-full", children: [selectedItem && _jsx(StateDetail, { state: selectedItem, onClose: onClose }), _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "rounded-0", children: "Group ID" }), _jsx(TableHead, { children: "Key" }), _jsx(TableHead, { children: "Type" })] }) }), _jsx(TableBody, { children: items.map((item) => (_jsxs(TableRow, { "data-testid": `item-${item}`, onClick: () => handleRowClick(item), className: cn('font-mono font-semibold cursor-pointer border-0', selectedItem === item
15
+ return (_jsxs("div", { className: "flex flex-row gap-4 h-full", children: [selectedItem && _jsx(StateSidebar, { state: selectedItem, onClose: onClose }), _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 bg-background", children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "rounded-0", children: "Group ID" }), _jsx(TableHead, { children: "Key" }), _jsx(TableHead, { children: "Type" })] }) }), _jsx(TableBody, { children: items.map((item) => (_jsxs(TableRow, { "data-testid": `item-${item}`, onClick: () => handleRowClick(item), className: cn('font-mono font-semibold cursor-pointer border-0', selectedItem === item
16
16
  ? 'bg-muted-foreground/10 hover:bg-muted-foreground/20'
17
17
  : 'hover:bg-muted-foreground/10'), children: [_jsx(TableCell, { className: "hover:bg-transparent", children: item.groupId }), _jsx(TableCell, { className: "hover:bg-transparent", children: item.key }), _jsx(TableCell, { className: "hover:bg-transparent", children: item.type })] }, `${item.groupId}:${item.key}`))) })] })] }));
18
18
  };