Initial commit with Advoware proxy

This commit is contained in:
root
2025-10-19 14:57:07 +00:00
commit 273aa8b549
45771 changed files with 5534555 additions and 0 deletions

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Motia
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,105 @@
# @motiadev/workbench
A web-based interface for building, visualizing, and managing Motia workflows.
## Overview
`@motiadev/workbench` provides a powerful visual interface for Motia workflows, offering:
- Flow visualization with interactive diagrams
- Real-time log monitoring
- State inspection and management
- API testing capabilities
## Installation
```bash
npm install @motiadev/workbench
# or
yarn add @motiadev/workbench
# or
pnpm add @motiadev/workbench
```
## Usage
The Workbench is automatically integrated when you run a Motia project in development mode:
```bash
npx motia dev
```
This starts the development server and makes the Workbench available at `http://localhost:3000` by default.
## Features
### Flow Visualization
Visualize your workflows as interactive diagrams, showing the connections between steps and the flow of events through your application.
### Log Monitoring
Monitor logs in real-time with filtering capabilities, log level indicators, and detailed log inspection.
### State Management
Inspect and manage application state, with support for viewing complex nested objects and state changes over time.
### API Testing
Test API endpoints directly from the Workbench interface, with support for different HTTP methods and request bodies.
## Components
The package exports several components that can be used to customize the visualization of your workflows:
```typescript
import {
EventNode,
ApiNode,
NoopNode,
BaseNode,
BaseHandle
} from '@motiadev/workbench'
```
### Node Components
- `EventNode`: Visualizes event-based steps
- `ApiNode`: Visualizes API endpoint steps
- `NoopNode`: A placeholder node with no specific functionality
- `BaseNode`: Base component for creating custom node types
- `BaseHandle`: Connection point component for nodes
## Customization
You can customize the appearance and behavior of the Workbench by creating custom node components:
```typescript
import { BaseNode, Position } from '@motiadev/workbench'
export const CustomNode = ({ data, ...props }) => {
return (
<BaseNode
{...props}
title="Custom Node"
color="#8B5CF6"
>
<div className="p-4">
{data.customContent}
</div>
</BaseNode>
)
}
```
## Technical Details
- Built with React and TypeScript
- Uses [XY Flow](https://xyflow.com/) for flow visualization
- Styled with Tailwind CSS and shadcn/ui components
- Supports real-time updates via WebSockets
## License
This package is part of the Motia framework and is licensed under the same terms.

View File

@@ -0,0 +1,105 @@
# @motiadev/workbench
A web-based interface for building, visualizing, and managing Motia workflows.
## Overview
`@motiadev/workbench` provides a powerful visual interface for Motia workflows, offering:
- Flow visualization with interactive diagrams
- Real-time log monitoring
- State inspection and management
- API testing capabilities
## Installation
```bash
npm install @motiadev/workbench
# or
yarn add @motiadev/workbench
# or
pnpm add @motiadev/workbench
```
## Usage
The Workbench is automatically integrated when you run a Motia project in development mode:
```bash
npx motia dev
```
This starts the development server and makes the Workbench available at `http://localhost:3000` by default.
## Features
### Flow Visualization
Visualize your workflows as interactive diagrams, showing the connections between steps and the flow of events through your application.
### Log Monitoring
Monitor logs in real-time with filtering capabilities, log level indicators, and detailed log inspection.
### State Management
Inspect and manage application state, with support for viewing complex nested objects and state changes over time.
### API Testing
Test API endpoints directly from the Workbench interface, with support for different HTTP methods and request bodies.
## Components
The package exports several components that can be used to customize the visualization of your workflows:
```typescript
import {
EventNode,
ApiNode,
NoopNode,
BaseNode,
BaseHandle
} from '@motiadev/workbench'
```
### Node Components
- `EventNode`: Visualizes event-based steps
- `ApiNode`: Visualizes API endpoint steps
- `NoopNode`: A placeholder node with no specific functionality
- `BaseNode`: Base component for creating custom node types
- `BaseHandle`: Connection point component for nodes
## Customization
You can customize the appearance and behavior of the Workbench by creating custom node components:
```typescript
import { BaseNode, Position } from '@motiadev/workbench'
export const CustomNode = ({ data, ...props }) => {
return (
<BaseNode
{...props}
title="Custom Node"
color="#8B5CF6"
>
<div className="p-4">
{data.customContent}
</div>
</BaseNode>
)
}
```
## Technical Details
- Built with React and TypeScript
- Uses [XY Flow](https://xyflow.com/) for flow visualization
- Styled with Tailwind CSS and shadcn/ui components
- Supports real-time updates via WebSockets
## License
This package is part of the Motia framework and is licensed under the same terms.

View File

@@ -0,0 +1,21 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "gray",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View File

@@ -0,0 +1,10 @@
export { EventNode } from './src/publicComponents/event-node';
export { ApiNode } from './src/publicComponents/api-node';
export { NoopNode } from './src/publicComponents/noop-node';
export { BaseNode } from './src/publicComponents/base-node/base-node';
export { BaseHandle } from './src/publicComponents/base-node/base-handle';
export { Position } from '@xyflow/react';
export type { EventNodeData, ApiNodeData } from './src/types/flow';
export type { ApiNodeProps, BaseNodeProps, CronNodeProps, EventNodeProps, NoopNodeProps, } from './src/publicComponents/node-props';
export type { TutorialStep } from './src/components/tutorial/engine/tutorial-types';
export { workbenchXPath } from './src/components/tutorial/engine/workbench-xpath';

View File

@@ -0,0 +1,69 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/icon.png" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=IBM+Plex+Mono:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;1,100;1,200;1,300;1,400;1,500;1,600;1,700&display=swap"
rel="stylesheet"
/>
<link
href="https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap"
rel="stylesheet"
/>
<script src="https://cdn.amplitude.com/libs/analytics-browser-2.11.1-min.js.gz"></script>
<script>
window.amplitude.init('ab2408031a38aa5cb85587a27ecfc69c', {
autocapture: {
fileDownloads: false,
formInteractions: false,
},
fetchRemoteConfig: true,
recording: false,
optOut: true,
})
</script>
<script>
const importFile = async (path) => {
// Normalize the path for cross-platform compatibility
const normalizedPath = path.replace(/\\/g, '/')
const fullPath = `${processCwd}/${normalizedPath}`.replace(/\\/g, '/')
// Handle Windows drive letters properly
let importPath = fullPath
if (navigator.platform.includes('Win')) {
// On Windows, ensure proper drive letter handling
if (fullPath.match(/^[A-Za-z]:/)) {
importPath = `/@fs/${fullPath}`
} else {
importPath = `/@fs/${fullPath}`
}
} else {
importPath = `/@fs/${fullPath}`
}
try {
return await import(/* @vite-ignore */ importPath)
} catch (error) {
console.error(`Failed to import ${path}:`, error)
// Return empty module if tutorial.tsx doesn't exist
if (path === 'tutorial.tsx') {
return { steps: [] }
}
throw error
}
}
</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Motia Workbench</title>
</head>
<body class="dark">
<div id="root"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,7 @@
export { EventNode } from './src/publicComponents/event-node';
export { ApiNode } from './src/publicComponents/api-node';
export { NoopNode } from './src/publicComponents/noop-node';
export { BaseNode } from './src/publicComponents/base-node/base-node';
export { BaseHandle } from './src/publicComponents/base-node/base-handle';
export { Position } from '@xyflow/react';
export { workbenchXPath } from './src/components/tutorial/engine/workbench-xpath';

View File

@@ -0,0 +1,2 @@
import type { Express } from 'express';
export declare const applyMiddleware: (app: Express, port: number, workbenchBase?: string) => Promise<void>;

View File

@@ -0,0 +1,79 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.applyMiddleware = void 0;
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
const vite_1 = require("vite");
const plugin_react_1 = __importDefault(require("@vitejs/plugin-react"));
const processCwdPlugin = () => {
return {
name: 'html-transform',
transformIndexHtml: (html) => {
// Normalize path for cross-platform compatibility
const cwd = process.cwd().replace(/\\/g, '/');
return html.replace('</head>', `<script>const processCwd = "${cwd}";</script></head>`);
},
};
};
const reoPlugin = () => {
return {
name: 'html-transform',
transformIndexHtml(html) {
const isAnalyticsEnabled = process.env.MOTIA_ANALYTICS_DISABLED !== 'true';
if (!isAnalyticsEnabled) {
return html;
}
// inject before </head>
return html.replace('</head>', `
<script type="text/javascript">
!function(){var e,t,n;e="d8f0ce9cae8ae64",t=function(){Reo.init({clientID:"d8f0ce9cae8ae64", source: "internal"})},(n=document.createElement("script")).src="https://static.reo.dev/"+e+"/reo.js",n.defer=!0,n.onload=t,document.head.appendChild(n)}();
</script>
</head>`);
},
};
};
const applyMiddleware = async (app, port, workbenchBase = '') => {
const vite = await (0, vite_1.createServer)({
appType: 'spa',
root: __dirname,
base: workbenchBase,
server: {
middlewareMode: true,
allowedHosts: true,
host: true,
hmr: { port: 21678 + port },
fs: {
allow: [
__dirname, // workbench root
path_1.default.join(process.cwd(), './steps'), // steps directory
path_1.default.join(process.cwd(), './tutorial.tsx'), // tutorial file
path_1.default.join(process.cwd(), './node_modules'), // node_modules directory
],
},
},
resolve: {
alias: {
'@': path_1.default.resolve(__dirname, './src'),
'@/assets': path_1.default.resolve(__dirname, './src/assets'),
},
},
plugins: [(0, plugin_react_1.default)(), processCwdPlugin(), reoPlugin()],
assetsInclude: ['**/*.png', '**/*.jpg', '**/*.jpeg', '**/*.gif', '**/*.svg', '**/*.ico', '**/*.webp', '**/*.avif'],
});
app.use(workbenchBase, vite.middlewares);
app.use(`${workbenchBase}/*`, async (req, res, next) => {
const url = req.originalUrl;
try {
const index = fs_1.default.readFileSync(path_1.default.resolve(__dirname, 'index.html'), 'utf-8');
const html = await vite.transformIndexHtml(url, index);
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
}
catch (e) {
next(e);
}
});
};
exports.applyMiddleware = applyMiddleware;

View File

@@ -0,0 +1,13 @@
import path from 'path'
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
const __dirname = dirname(fileURLToPath(import.meta.url))
export default {
plugins: {
'@tailwindcss/postcss': {
base: path.join(__dirname, './src'),
},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@@ -0,0 +1,2 @@
import { FC } from 'react';
export declare const App: FC;

View File

@@ -0,0 +1,56 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { CollapsiblePanel, CollapsiblePanelGroup, Panel, TabsContent, TabsList, TabsTrigger } from '@motiadev/ui';
import { analytics } from '@/lib/analytics';
import { ReactFlowProvider } from '@xyflow/react';
import { File, GanttChart, Link2, LogsIcon } from 'lucide-react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { FlowPage } from './components/flow/flow-page';
import { FlowTabMenuItem } from './components/flow/flow-tab-menu-item';
import { Header } from './components/header/header';
import { LogsPage } from './components/logs/logs-page';
import { TracesPage } from './components/observability/traces-page';
import { APP_SIDEBAR_CONTAINER_ID } from '@motiadev/ui';
import { StatesPage } from './components/states/states-page';
import { useTabsStore } from './stores/use-tabs-store';
import { EndpointsPage } from '@motiadev/plugin-endpoint';
var TabLocation;
(function (TabLocation) {
TabLocation["TOP"] = "top";
TabLocation["BOTTOM"] = "bottom";
})(TabLocation || (TabLocation = {}));
export const App = () => {
const tab = useTabsStore((state) => state.tab);
const setTopTab = useTabsStore((state) => state.setTopTab);
const setBottomTab = useTabsStore((state) => state.setBottomTab);
const [viewMode, setViewMode] = useState('system');
const tabChangeCallbacks = useMemo(() => ({
[TabLocation.TOP]: setTopTab,
[TabLocation.BOTTOM]: setBottomTab,
}), [setTopTab, setBottomTab]);
const onTabChange = useCallback((location) => (newTab) => {
analytics.track(`${location} tab changed`, { [`new.${location}`]: newTab, tab });
tabChangeCallbacks[location](newTab);
}, [tabChangeCallbacks, tab]);
useEffect(() => {
const url = new URL(window.location.href);
const viewMode = url.searchParams.get('view-mode');
if (viewMode) {
setViewMode(viewMode);
}
}, [setViewMode]);
if (viewMode === 'project') {
return (_jsxs("div", { className: "grid grid-rows-1 grid-cols-[1fr_auto] bg-background text-foreground h-screen", children: [_jsx("main", { className: "m-2 overflow-hidden", role: "main", children: _jsx(Panel, { tabs: [
{
label: 'Flow',
labelComponent: _jsx(FlowTabMenuItem, {}),
content: (_jsx(ReactFlowProvider, { children: _jsx("div", { className: "h-[calc(100vh-100px)] w-full", children: _jsx(FlowPage, {}) }) })),
},
{
label: 'Endpoint',
labelComponent: (_jsxs(_Fragment, { children: [_jsx(Link2, {}), "Endpoint"] })),
content: _jsx(EndpointsPage, {}),
},
] }) }), _jsx("div", { id: APP_SIDEBAR_CONTAINER_ID })] }));
}
return (_jsxs("div", { className: "grid grid-rows-[auto_1fr] grid-cols-[1fr_auto] bg-background text-foreground h-screen", children: [_jsx("div", { className: "col-span-2", children: _jsx(Header, {}) }), _jsx("main", { className: "m-2 overflow-hidden", role: "main", children: _jsxs(CollapsiblePanelGroup, { autoSaveId: "app-panel", direction: "vertical", className: "gap-1 h-full", "aria-label": "Workbench panels", children: [_jsxs(CollapsiblePanel, { id: "top-panel", variant: 'tabs', defaultTab: tab.top, onTabChange: onTabChange(TabLocation.TOP), header: _jsxs(TabsList, { children: [_jsx(TabsTrigger, { value: "flow", "data-testid": "flows-link", children: _jsx(FlowTabMenuItem, {}) }), _jsxs(TabsTrigger, { value: "endpoint", "data-testid": "endpoints-link", className: "cursor-pointer", children: [_jsx(Link2, {}), "Endpoint"] })] }), children: [_jsx(TabsContent, { value: "flow", className: "h-full", asChild: true, children: _jsx(ReactFlowProvider, { children: _jsx(FlowPage, {}) }) }), _jsx(TabsContent, { value: "endpoint", asChild: true, children: _jsx(EndpointsPage, {}) })] }), _jsxs(CollapsiblePanel, { id: "bottom-panel", variant: 'tabs', defaultTab: tab.bottom, onTabChange: onTabChange(TabLocation.BOTTOM), header: _jsxs(TabsList, { children: [_jsxs(TabsTrigger, { value: "tracing", "data-testid": "traces-link", className: "cursor-pointer", children: [_jsx(GanttChart, {}), " Tracing"] }), _jsxs(TabsTrigger, { value: "logs", "data-testid": "logs-link", className: "cursor-pointer", children: [_jsx(LogsIcon, {}), "Logs"] }), _jsxs(TabsTrigger, { value: "states", "data-testid": "states-link", className: "cursor-pointer", children: [_jsx(File, {}), "States"] })] }), children: [_jsx(TabsContent, { value: "tracing", className: "max-h-fit", asChild: true, children: _jsx(TracesPage, {}) }), _jsx(TabsContent, { value: "logs", asChild: true, children: _jsx(LogsPage, {}) }), _jsx(TabsContent, { value: "states", asChild: true, children: _jsx(StatesPage, {}) })] })] }) }), _jsx("div", { id: APP_SIDEBAR_CONTAINER_ID })] }));
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@@ -0,0 +1,3 @@
import { EdgeProps } from '@xyflow/react';
import React from 'react';
export declare const BaseEdge: React.FC<EdgeProps>;

View File

@@ -0,0 +1,39 @@
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
import { BaseEdge as BaseReactFlowEdge, EdgeLabelRenderer, getSmoothStepPath } from '@xyflow/react';
import { cva } from 'class-variance-authority';
import { cn, useThemeStore } from '@motiadev/ui';
const labelVariants = cva('absolute pointer-events-all text-cs border p-1 px-2', {
variants: {
color: {
default: 'border-[#b3b3b3] bg-[#060014] text-gray-100 font-semibold border-solid rounded-full',
conditional: 'bg-amber-300 border-amber-950 text-amber-950 border-solid font-semibold italic rounded-lg',
},
},
defaultVariants: {
color: 'default',
},
});
export const BaseEdge = (props) => {
const theme = useThemeStore((state) => state.theme);
const { sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, data } = props;
const label = data?.label;
const labelVariant = data?.labelVariant;
const virtualColor = theme === 'dark' ? 'rgb(225, 225, 225)' : 'rgb(85, 85, 85)';
const [edgePath, labelX, labelY] = getSmoothStepPath({
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
borderRadius: 20,
offset: 10,
});
return (_jsxs(_Fragment, { children: [_jsx(BaseReactFlowEdge, { path: edgePath, style: {
stroke: data?.variant === 'virtual' ? virtualColor : '#0094FF',
strokeWidth: 2,
shapeRendering: 'geometricPrecision',
fill: 'none',
mixBlendMode: 'screen',
}, className: "edge-animated" }), label && (_jsx(EdgeLabelRenderer, { children: _jsx("div", { className: cn(labelVariants({ color: labelVariant })), style: { transform: `translateX(-50%) translateY(-50%) translate(${labelX}px, ${labelY}px)` }, children: _jsx("div", { className: "text-xs font-mono", children: label }) }) }))] }));
};

View File

@@ -0,0 +1 @@
export declare const FlowLoader: () => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,4 @@
import { jsx as _jsx } from "react/jsx-runtime";
export const FlowLoader = () => {
return _jsx("div", { className: "absolute z-10 inset-0 w-full h-full bg-background" });
};

View File

@@ -0,0 +1 @@
export declare const FlowPage: () => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,20 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { useFlowStore } from '@/stores/use-flow-store';
import { useStreamItem } from '@motiadev/stream-client-react';
import { FlowView } from './flow-view';
export const FlowPage = () => {
const selectedFlowId = useFlowStore((state) => state.selectedFlowId);
const { data: flow } = useStreamItem({
streamName: '__motia.flows',
groupId: 'default',
id: selectedFlowId ?? '',
});
const { data: flowConfig } = useStreamItem({
streamName: '__motia.flowsConfig',
groupId: 'default',
id: selectedFlowId ?? '',
});
if (!flow || flow.error)
return (_jsx("div", { className: "w-full h-full bg-background flex flex-col items-center justify-center", children: _jsx("p", { children: flow?.error }) }));
return _jsx(FlowView, { flow: flow, flowConfig: flowConfig });
};

View File

@@ -0,0 +1 @@
export declare const FlowTabMenuItem: () => import("react/jsx-runtime").JSX.Element | null;

View File

@@ -0,0 +1,21 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@motiadev/ui';
import { ChevronsUpDown, Workflow } from 'lucide-react';
import { useFlowStore } from '@/stores/use-flow-store';
import { useFetchFlows } from '@/hooks/use-fetch-flows';
import { useShallow } from 'zustand/react/shallow';
import { analytics } from '@/lib/analytics';
export const FlowTabMenuItem = () => {
useFetchFlows();
const selectFlowId = useFlowStore((state) => state.selectFlowId);
const flows = useFlowStore(useShallow((state) => Object.values(state.flows)));
const selectedFlowId = useFlowStore((state) => state.selectedFlowId);
if (flows.length === 0) {
return null;
}
const handleFlowSelect = (flowId) => {
selectFlowId(flowId);
analytics.track('flow_selected', { flow: flowId });
};
return (_jsxs("div", { className: "flex flex-row justify-center items-center gap-2 cursor-pointer", children: [_jsx(Workflow, {}), selectedFlowId ?? 'No flow selected', _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx("div", { "data-testid": "flows-dropdown-trigger", className: "flex flex-row justify-center items-center gap-2 cursor-pointer", children: _jsx(ChevronsUpDown, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { className: "bg-background text-foreground flows-dropdown", children: flows.map((item) => (_jsx(DropdownMenuItem, { "data-testid": `dropdown-${item}`, className: "cursor-pointer gap-2 flow-link", onClick: () => handleFlowSelect(item), children: item }, `dropdown-${item}`))) })] })] }));
};

View File

@@ -0,0 +1,12 @@
import { EdgeData, FlowConfigResponse, FlowResponse, NodeData } from '@/types/flow';
import { Edge as ReactFlowEdge, Node as ReactFlowNode } from '@xyflow/react';
import React from 'react';
import '@xyflow/react/dist/style.css';
export type FlowNode = ReactFlowNode<NodeData>;
export type FlowEdge = ReactFlowEdge<EdgeData>;
type Props = {
flow: FlowResponse;
flowConfig: FlowConfigResponse;
};
export declare const FlowView: React.FC<Props>;
export {};

View File

@@ -0,0 +1,22 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { Background, BackgroundVariant, ReactFlow, } from '@xyflow/react';
import { useCallback, useState } from 'react';
import { BaseEdge } from './base-edge';
import { FlowLoader } from './flow-loader';
import { useGetFlowState } from './hooks/use-get-flow-state';
import { NodeOrganizer } from './node-organizer';
import '@xyflow/react/dist/style.css';
import { BackgroundEffect } from '@motiadev/ui';
const edgeTypes = {
base: BaseEdge,
};
export const FlowView = ({ flow, flowConfig }) => {
const { nodes, edges, onNodesChange, onEdgesChange, nodeTypes } = useGetFlowState(flow, flowConfig);
const [initialized, setInitialized] = useState(false);
const onInitialized = useCallback(() => setInitialized(true), []);
const onNodesChangeHandler = useCallback((changes) => onNodesChange(changes), [onNodesChange]);
if (!nodeTypes) {
return null;
}
return (_jsxs("div", { className: "w-full h-full relative", children: [!initialized && _jsx(FlowLoader, {}), _jsxs(ReactFlow, { minZoom: 0.1, nodes: nodes, edges: edges, nodeTypes: nodeTypes, edgeTypes: edgeTypes, onNodesChange: onNodesChangeHandler, onEdgesChange: onEdgesChange, className: "isolate", children: [_jsx(BackgroundEffect, {}), _jsx(Background, { variant: BackgroundVariant.Dots, gap: 20, size: 1 }), _jsx(NodeOrganizer, { onInitialized: onInitialized, nodes: nodes, edges: edges })] })] }));
};

View File

@@ -0,0 +1,10 @@
import type { EdgeData, FlowConfigResponse, FlowResponse, NodeData } from '@/types/flow';
import { Edge, Node } from '@xyflow/react';
import React from 'react';
export declare const useGetFlowState: (flow: FlowResponse, flowConfig: FlowConfigResponse) => {
nodes: Node<NodeData>[];
edges: Edge<EdgeData>[];
onNodesChange: import("@xyflow/react").OnNodesChange<Node<NodeData>>;
onEdgesChange: import("@xyflow/react").OnEdgesChange<Edge<EdgeData>>;
nodeTypes: Record<string, React.ComponentType<any>>;
};

View File

@@ -0,0 +1,137 @@
import { useEdgesState, useNodesState } from '@xyflow/react';
import isEqual from 'fast-deep-equal';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ApiFlowNode } from '../nodes/api-flow-node';
import { CronFlowNode } from '../nodes/cron-flow-node';
import { EventFlowNode } from '../nodes/event-flow-node';
import { NoopFlowNode } from '../nodes/noop-flow-node';
import { useSaveWorkflowConfig } from './use-save-workflow-config';
const DEFAULT_CONFIG = { x: 0, y: 0 };
const getNodePosition = (flowConfig, stepName) => {
return flowConfig?.config[stepName] || DEFAULT_CONFIG;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeComponentCache = new Map();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const BASE_NODE_TYPES = {
event: EventFlowNode,
api: ApiFlowNode,
noop: NoopFlowNode,
cron: CronFlowNode,
};
async function importFlow(flow, flowConfig) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeTypes = { ...BASE_NODE_TYPES };
const customNodePromises = flow.steps
.filter((step) => step.nodeComponentPath)
.map(async (step) => {
const path = step.nodeComponentPath;
// Check cache first
if (nodeComponentCache.has(path)) {
nodeTypes[path] = nodeComponentCache.get(path);
return;
}
try {
const module = await import(/* @vite-ignore */ `/@fs/${path}`);
const component = module.Node ?? module.default;
nodeComponentCache.set(path, component);
nodeTypes[path] = component;
}
catch (error) {
console.error(`Failed to load custom node component: ${path}`, error);
}
});
await Promise.all(customNodePromises);
const nodes = flow.steps.map((step) => ({
id: step.id,
type: step.nodeComponentPath || step.type,
filePath: step.filePath,
position: step.filePath ? getNodePosition(flowConfig, step.filePath) : DEFAULT_CONFIG,
data: { ...step, nodeConfig: step.filePath ? getNodePosition(flowConfig, step.filePath) : DEFAULT_CONFIG },
language: step.language,
}));
const edges = flow.edges.map((edge) => ({
...edge,
type: 'base',
}));
return { nodes, edges, nodeTypes };
}
export const useGetFlowState = (flow, flowConfig) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const [nodeTypes, setNodeTypes] = useState(BASE_NODE_TYPES);
const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const saveConfig = useSaveWorkflowConfig();
const flowIdRef = useRef('');
const saveTimeoutRef = useRef(null);
const lastSavedConfigRef = useRef(null);
const lastSavedFlowRef = useRef(null);
// eslint-disable-next-line react-hooks/exhaustive-deps
const memoizedFlowConfig = useMemo(() => flowConfig, [flowConfig?.id, flowConfig?.config]);
useEffect(() => {
if (!flow || flow.error)
return;
const hasSameConfig = isEqual(lastSavedConfigRef.current, memoizedFlowConfig?.config);
const hasSameFlow = isEqual(lastSavedFlowRef.current, flow);
if (hasSameConfig && hasSameFlow)
return;
lastSavedConfigRef.current = memoizedFlowConfig?.config;
flowIdRef.current = flow.id;
lastSavedFlowRef.current = flow;
const importFlowAsync = async () => {
try {
const { nodes, edges, nodeTypes } = await importFlow(flow, flowConfig);
setNodes(nodes);
setEdges(edges);
setNodeTypes(nodeTypes);
}
catch (error) {
console.error('Failed to import flow:', error);
}
};
importFlowAsync();
}, [flow, memoizedFlowConfig, setNodes, setEdges, flowConfig]);
const saveFlowConfig = useCallback((nodesToSave) => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
saveTimeoutRef.current = setTimeout(async () => {
const steps = nodesToSave.reduce((acc, node) => {
if (node.data.filePath) {
acc[node.data.filePath] = {
x: Math.round(node.position.x),
y: Math.round(node.position.y),
};
if (node.data.nodeConfig?.sourceHandlePosition) {
acc[node.data.filePath].sourceHandlePosition = node.data.nodeConfig.sourceHandlePosition;
}
if (node.data.nodeConfig?.targetHandlePosition) {
acc[node.data.filePath].targetHandlePosition = node.data.nodeConfig.targetHandlePosition;
}
}
return acc;
}, {});
if (!isEqual(steps, lastSavedConfigRef.current)) {
lastSavedConfigRef.current = steps;
const newConfig = { id: flowIdRef.current, config: steps };
try {
await saveConfig(newConfig);
}
catch (error) {
console.error('Failed to save flow config:', error);
}
}
}, 300);
}, [saveConfig]);
useEffect(() => {
if (nodes.length > 0) {
saveFlowConfig(nodes);
}
return () => {
if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current);
}
};
}, [nodes, saveFlowConfig]);
return useMemo(() => ({ nodes, edges, onNodesChange, onEdgesChange, nodeTypes }), [nodes, edges, onNodesChange, onEdgesChange, nodeTypes]);
};

View File

@@ -0,0 +1,2 @@
import { FlowConfigResponse } from '@/types/flow';
export declare const useSaveWorkflowConfig: () => (body: FlowConfigResponse) => Promise<any>;

View File

@@ -0,0 +1,22 @@
import { useCallback } from 'react';
export const useSaveWorkflowConfig = () => {
return useCallback(async (body) => {
try {
const response = await fetch(`/__motia/flows/${body.id}/config`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
if (!response.ok) {
throw new Error(`Failed to save config: ${response.statusText}`);
}
return await response.json();
}
catch (error) {
console.error('Error saving workflow config:', error);
throw error;
}
}, []);
};

View File

@@ -0,0 +1,10 @@
import { EdgeData, NodeData } from '@/types/flow';
import { Edge, Node } from '@xyflow/react';
import React from 'react';
type Props = {
onInitialized: () => void;
nodes: Node<NodeData>[];
edges: Edge<EdgeData>[];
};
export declare const NodeOrganizer: React.FC<Props>;
export {};

View File

@@ -0,0 +1,82 @@
import { useNodesInitialized, useReactFlow } from '@xyflow/react';
import dagre from 'dagre';
import isEqual from 'fast-deep-equal';
import { useEffect, useRef } from 'react';
const organizeNodes = (nodes, edges) => {
const dagreGraph = new dagre.graphlib.Graph({ directed: true, compound: false, multigraph: false });
dagreGraph.setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ rankdir: 'LR', ranksep: 0, nodesep: 20, edgesep: 0 });
nodes.forEach((node) => {
if (node.position.x !== 0 || node.position.y !== 0) {
dagreGraph.setNode(node.id, {
width: node.measured?.width,
height: node.measured?.height,
x: node.position.x,
y: node.position.y,
});
}
else {
dagreGraph.setNode(node.id, {
width: node.measured?.width,
height: node.measured?.height,
});
}
});
edges.forEach((edge) => {
if (typeof edge.label === 'string') {
dagreGraph.setEdge(edge.source, edge.target, {
label: edge.label ?? '',
width: edge.label.length * 40, // Add width for the label
height: 30, // Add height for the label
labelpos: 'c', // Position label in center
});
}
else {
dagreGraph.setEdge(edge.source, edge.target);
}
});
dagre.layout(dagreGraph);
return nodes.map((node) => {
if (node.position.x !== 0 || node.position.y !== 0) {
return node;
}
const { x, y } = dagreGraph.node(node.id);
const position = {
x: x - (node.measured?.width ?? 0) / 2,
y: y - (node.measured?.height ?? 0) / 2,
};
return { ...node, position };
});
};
export const NodeOrganizer = ({ onInitialized, nodes, edges }) => {
const { setNodes, getNodes, getEdges, fitView } = useReactFlow();
const nodesInitialized = useNodesInitialized();
const initialized = useRef(false);
const lastNodesRef = useRef([]);
const lastEdgesRef = useRef([]);
useEffect(() => {
if (nodesInitialized) {
if (isEqual(lastNodesRef.current, nodes) && isEqual(lastEdgesRef.current, edges)) {
return;
}
lastNodesRef.current = nodes;
lastEdgesRef.current = edges;
try {
const nodesToOrganize = nodes.some((node) => node.position.x === 0 && node.position.y === 0);
if (nodesToOrganize) {
const organizedNodes = organizeNodes(nodes, edges);
setNodes(organizedNodes);
}
if (!initialized.current) {
initialized.current = true;
onInitialized();
setTimeout(() => fitView(), 1);
}
}
catch (error) {
console.error('Error organizing nodes:', error);
}
}
}, [nodesInitialized, onInitialized, setNodes, getNodes, getEdges, fitView, nodes, edges]);
return null;
};

View File

@@ -0,0 +1,2 @@
import { ApiNodeProps } from '@/publicComponents/node-props';
export declare const ApiFlowNode: ({ data }: ApiNodeProps) => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { ApiNode } from '@/publicComponents/api-node';
export const ApiFlowNode = ({ data }) => {
return _jsx(ApiNode, { data: data });
};

View File

@@ -0,0 +1,2 @@
import { CronNodeProps } from '@/publicComponents/node-props';
export declare const CronFlowNode: ({ data }: CronNodeProps) => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { CronNode } from '@/publicComponents/cron-node';
export const CronFlowNode = ({ data }) => {
return _jsx(CronNode, { data: data });
};

View File

@@ -0,0 +1,2 @@
import { EventNodeProps } from '@/publicComponents/node-props';
export declare const EventFlowNode: ({ data }: EventNodeProps) => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { EventNode } from '@/publicComponents/event-node';
export const EventFlowNode = ({ data }) => {
return _jsx(EventNode, { data: data });
};

View File

@@ -0,0 +1,2 @@
import { NoopNodeProps } from '@/publicComponents/node-props';
export declare const NoopFlowNode: ({ data }: NoopNodeProps) => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { NoopNode } from '@/publicComponents/noop-node';
export const NoopFlowNode = ({ data }) => {
return _jsx(NoopNode, { data: data });
};

View File

@@ -0,0 +1 @@
export declare const DeployButton: () => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,28 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { analytics } from '@/lib/analytics';
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@motiadev/ui';
import { Rocket } from 'lucide-react';
import { useState } from 'react';
export const DeployButton = () => {
const [isOpen, setIsOpen] = useState(false);
const onDeployButtonClick = () => {
analytics.track('deploy_button_clicked');
};
const onDeployClick = () => {
setIsOpen(false);
analytics.track('deploy_button_deploy_clicked');
};
const onClose = () => {
setIsOpen(false);
analytics.track('deploy_button_closed');
};
const onMotiaCloudClick = () => {
setIsOpen(true);
analytics.track('deploy_button_motia_cloud_clicked');
};
const onSelfHostedClick = () => {
analytics.track('deploy_button_self_hosted_clicked');
window.open('https://www.motia.dev/docs/concepts/deployment/self-hosted', '_blank');
};
return (_jsxs(_Fragment, { children: [isOpen && (_jsxs("div", { children: [_jsx("div", { className: "fixed inset-0 z-[9999] bg-black/20 backdrop-blur-sm", onClick: () => setIsOpen(false) }), _jsxs("div", { className: "driver-popover w-[600px]! fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 z-[10000] animate-in fade-in-0 zoom-in-95", children: [_jsx("img", { src: "https://oxhhfuuoqzsaqthfairn.supabase.co/storage/v1/object/public/public-images/preview.png", alt: "Motia Cloud", className: "driver-popover-image object-cover", style: { height: 393, width: '100%' } }), _jsx("div", { className: "driver-popover-title", children: _jsx("h2", { className: "popover-title", children: "Motia Cloud is Live!" }) }), _jsx("div", { className: "driver-popover-description", children: "Deploy to production in minutes, not hours. One click gets your Motia project live with enterprise-grade reliability. Seamlessly scale, rollback instantly, and monitor everything in real-time. Your code deserves infrastructure that just works." }), _jsx("a", { href: "https://www.motia.dev/docs/concepts/deployment/motia-cloud/features", target: "_blank", className: "text-foreground text-xs font-semibold px-4 hover:underline", children: "Learn more about Motia Cloud" }), _jsx("div", { className: "driver-popover-footer flex items-center justify-end", children: _jsxs("div", { className: "driver-popover-navigation-btns flex gap-6", children: [_jsx("button", { className: "tutorial-opt-out-button text-sm! font-semibold! text-muted-foreground!", onClick: onClose, children: "Close" }), _jsx("a", { href: "https://motia.cloud?utm_source=workbench&utm_medium=referral", target: "_blank", onClick: onDeployClick, children: _jsx("button", { className: "driver-popover-next-btn", children: "Deploy!" }) })] }) })] })] })), _jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { variant: "ghost", className: "font-semibold text-sm dark:bg-white dark:text-black dark:hover:bg-white/90 bg-black/90 hover:bg-black/80 text-white", onClick: onDeployButtonClick, children: [_jsx(Rocket, {}), "Deploy"] }) }), _jsxs(DropdownMenuContent, { className: "bg-background text-foreground w-56", children: [_jsx(DropdownMenuItem, { className: "cursor-pointer h-10 font-semibold", onClick: onMotiaCloudClick, children: "Motia Cloud" }), _jsx(DropdownMenuItem, { className: "cursor-pointer h-10 font-semibold", onClick: onSelfHostedClick, children: "Self-Hosted (Docker)" })] })] })] }));
};

View File

@@ -0,0 +1,2 @@
import React from 'react';
export declare const Header: React.FC;

View File

@@ -0,0 +1,25 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import motiaLogoDark from '@/assets/motia-dark.png';
import motiaLogoLight from '@/assets/motia-light.png';
import { useThemeStore } from '@motiadev/ui';
import { useEffect, useState } from 'react';
import { Tutorial } from '../tutorial/tutorial';
import { TutorialButton } from '../tutorial/tutorial-button';
import { ThemeToggle } from '../ui/theme-toggle';
import { DeployButton } from './deploy-button';
export const Header = () => {
const [isDevMode, setIsDevMode] = useState(false);
const [isTutorialDisabled, setIsTutorialDisabled] = useState(true);
const theme = useThemeStore((state) => state.theme);
const logo = theme === 'light' ? motiaLogoLight : motiaLogoDark;
useEffect(() => {
fetch('/__motia')
.then((res) => res.json())
.then((data) => {
setIsDevMode(data.isDev);
setIsTutorialDisabled(data.isTutorialDisabled);
})
.catch((err) => console.error(err));
}, []);
return (_jsxs("header", { className: "min-h-16 px-4 gap-4 flex items-center bg-default text-default-foreground border-b", children: [_jsx("img", { src: logo, className: "h-5", id: "logo-icon", "data-testid": "logo-icon" }), _jsx("div", { className: "flex-1" }), _jsx(ThemeToggle, {}), isDevMode && !isTutorialDisabled && _jsx(TutorialButton, {}), isDevMode && _jsx(DeployButton, {}), !isTutorialDisabled && _jsx(Tutorial, {})] }));
};

View File

@@ -0,0 +1,10 @@
import React from 'react';
import 'react18-json-view/src/dark.css';
import 'react18-json-view/src/style.css';
import { Log } from '@/stores/use-logs-store';
type Props = {
log?: Log;
onClose: () => void;
};
export declare const LogDetail: React.FC<Props>;
export {};

View File

@@ -0,0 +1,37 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { formatTimestamp } from '@/lib/utils';
import { useMemo, useState } from 'react';
import ReactJson from 'react18-json-view';
import 'react18-json-view/src/dark.css';
import 'react18-json-view/src/style.css';
import { LogLevelDot } from './log-level-dot';
import { Sidebar } from '@motiadev/ui';
import { X } from 'lucide-react';
const defaultProps = ['id', 'msg', 'time', 'level', 'step', 'flows', 'traceId'];
export const LogDetail = ({ log, onClose }) => {
const [hasOtherProps, setHasOtherProps] = useState(false);
const otherPropsObject = useMemo(() => {
if (!log) {
return null;
}
const otherProps = Object.keys(log ?? {}).filter((key) => !defaultProps.includes(key));
setHasOtherProps(otherProps.length > 0);
return otherProps.reduce((acc, key) => {
acc[key] = log[key];
return acc;
}, {});
}, [log]);
if (!log) {
return null;
}
return (_jsx(Sidebar, { onClose: onClose, title: "Logs Details", subtitle: "Details including custom properties", actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }], details: [
{
label: 'Level',
value: (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LogLevelDot, { level: log.level }), _jsx("div", { className: "capitalize", children: log.level })] })),
},
{ label: 'Time', value: formatTimestamp(log.time) },
{ label: 'Step', value: log.step },
{ label: 'Flows', value: log.flows.join(', ') },
{ label: 'Trace ID', value: log.traceId },
], children: hasOtherProps && _jsx(ReactJson, { src: otherPropsObject, theme: "default", enableClipboard: false }) }));
};

View File

@@ -0,0 +1,5 @@
import React from 'react';
export declare const LogLevelBadge: React.FC<{
level: string;
className?: string;
}>;

View File

@@ -0,0 +1,11 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { Badge } from '@motiadev/ui';
const map = {
info: 'info',
error: 'error',
warn: 'warning',
debug: 'info',
};
export const LogLevelBadge = (props) => {
return (_jsx(Badge, { variant: map[props.level], className: props.className, children: props.level }));
};

View File

@@ -0,0 +1,4 @@
import * as React from 'react';
export declare const LogLevelDot: React.FC<{
level: string;
}>;

View File

@@ -0,0 +1,17 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { cva } from 'class-variance-authority';
const badgeVariants = cva('text-xs font-medium tracking-wide rounded-full h-[6px] w-[6px] m-[4px] outline-[2px]', {
variants: {
variant: {
info: 'bg-[#2862FE] outline-[#2862FE]/20',
trace: 'bg-[#2862FE] outline-[#2862FE]/20',
debug: 'bg-[#2862FE] outline-[#2862FE]/20',
error: 'bg-[#E22A6D] outline-[#E22A6D]/20',
fatal: 'bg-[#E22A6D] outline-[#E22A6D]/20',
warn: 'bg-[#F59F0B] outline-[#F59F0B]/20',
},
},
});
export const LogLevelDot = ({ level }) => {
return _jsx("div", { className: badgeVariants({ variant: level }) });
};

View File

@@ -0,0 +1 @@
export declare const LogsPage: () => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,29 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Table, TableBody, TableCell, TableRow } from '@/components/ui/table';
import { formatTimestamp } from '@/lib/utils';
import { useGlobalStore } from '@/stores/use-global-store';
import { useLogsStore } from '@/stores/use-logs-store';
import { Button, cn, Input } from '@motiadev/ui';
import { Search, Trash, X } from 'lucide-react';
import { useMemo, useState } from 'react';
import { LogDetail } from './log-detail';
import { LogLevelDot } from './log-level-dot';
export const LogsPage = () => {
const logs = useLogsStore((state) => state.logs);
const resetLogs = useLogsStore((state) => state.resetLogs);
const selectedLogId = useGlobalStore((state) => state.selectedLogId);
const selectLogId = useGlobalStore((state) => state.selectLogId);
const selectedLog = useMemo(() => (selectedLogId ? logs.find((log) => log.id === selectedLogId) : undefined), [logs, selectedLogId]);
const [search, setSearch] = useState('');
const filteredLogs = useMemo(() => {
return logs.filter((log) => {
return (log.msg.toLowerCase().includes(search.toLowerCase()) ||
log.traceId.toLowerCase().includes(search.toLowerCase()) ||
log.step.toLowerCase().includes(search.toLowerCase()));
});
}, [logs, search]);
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "grid grid-rows-[auto_1fr] h-full", "data-testid": "logs-container", children: [_jsxs("div", { className: "flex p-2 border-b gap-2", "data-testid": "logs-search-container", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(Input, { variant: "shade", value: search, onChange: (e) => setSearch(e.target.value), className: "px-9 font-medium", placeholder: "Search by Trace ID or Message" }), _jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50" }), _jsx(X, { className: "cursor-pointer absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50 hover:text-muted-foreground", onClick: () => setSearch('') })] }), _jsxs(Button, { variant: "default", onClick: resetLogs, className: "h-[34px]", children: [_jsx(Trash, {}), " Clear"] })] }), _jsx(Table, { children: _jsx(TableBody, { className: "font-mono font-medium", children: filteredLogs.map((log, index) => (_jsxs(TableRow, { "data-testid": "log-row", className: cn('font-mono font-semibold cursor-pointer border-0', {
'bg-muted-foreground/10 hover:bg-muted-foreground/20': selectedLogId === log.id,
'hover:bg-muted-foreground/10': selectedLogId !== log.id,
}), onClick: () => selectLogId(log.id), children: [_jsxs(TableCell, { "data-testid": `time-${index}`, className: "whitespace-nowrap flex items-center gap-2 text-muted-foreground", children: [_jsx(LogLevelDot, { level: log.level }), formatTimestamp(log.time)] }), _jsx(TableCell, { "data-testid": `trace-${log.traceId}`, className: "whitespace-nowrap cursor-pointer hover:text-primary text-muted-foreground", onClick: () => setSearch(log.traceId), children: log.traceId }), _jsx(TableCell, { "data-testid": `step-${index}`, "aria-label": log.step, className: "whitespace-nowrap", children: log.step }), _jsx(TableCell, { "data-testid": `msg-${index}`, "aria-label": log.msg, className: "whitespace-nowrap max-w-[500px] truncate w-full", children: log.msg })] }, index))) }) })] }), _jsx(LogDetail, { log: selectedLog, onClose: () => selectLogId(undefined) })] }));
};

View File

@@ -0,0 +1,13 @@
import React from 'react';
type Props = {
topLevelClassName?: string;
objectName?: string;
functionName: string;
args: Array<string | object | false | undefined>;
callsQuantity?: number;
};
export declare const Argument: React.FC<{
arg: string | object | false;
}>;
export declare const FunctionCall: React.FC<Props>;
export {};

View File

@@ -0,0 +1,16 @@
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
export const Argument = ({ arg }) => {
if (typeof arg === 'string') {
return _jsxs("span", { className: "font-mono text-blue-500", children: ["'", arg, "'"] });
}
else if (arg === false) {
return _jsx("span", { className: "font-mono text-blue-100 font-bold bg-blue-500/50 px-2 rounded-md", children: "value" });
}
const entries = Object.entries(arg);
return (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-mono text-green-500", children: '{ ' }), entries.map(([key, value], index) => (_jsxs("span", { children: [_jsx("span", { className: "font-mono text-green-500", children: key }), _jsx("span", { className: "font-mono text-muted-foreground", children: ":" }), " ", _jsx(Argument, { arg: value }), index < entries.length - 1 && _jsx(_Fragment, { children: ", " })] }, key))), _jsx("span", { className: "font-mono text-green-500", children: ' }' })] }));
};
export const FunctionCall = ({ topLevelClassName, objectName, functionName, args, callsQuantity }) => {
const hasCalls = callsQuantity && callsQuantity > 1;
const filteredArgs = args.filter((arg) => arg !== undefined);
return (_jsxs("div", { children: [topLevelClassName && (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-mono text-pink-500", children: topLevelClassName }), "."] })), objectName && (_jsxs(_Fragment, { children: [_jsx("span", { className: "font-mono text-pink-500", children: objectName }), "."] })), _jsx("span", { className: "font-mono text-pink-500", children: functionName }), _jsx("span", { className: "font-mono text-emerald-500", children: "(" }), filteredArgs.map((arg, index) => (_jsxs("span", { children: [_jsx(Argument, { arg: arg }), index < filteredArgs.length - 1 && _jsx(_Fragment, { children: ", " })] }, index))), _jsx("span", { className: "font-mono text-emerald-500", children: ")" }), hasCalls && _jsxs("span", { className: "font-mono text-muted-foreground", children: [" x", callsQuantity] })] }));
};

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { TraceEvent as TraceEventType } from '@/types/observability';
type Props = {
event: TraceEventType;
};
export declare const EventIcon: React.FC<Props>;
export {};

View File

@@ -0,0 +1,16 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { MessageCircle, Package, Radio, ScrollText } from 'lucide-react';
export const EventIcon = ({ event }) => {
if (event.type === 'log') {
return _jsx(ScrollText, { className: "w-4 h-4 text-muted-foreground" });
}
else if (event.type === 'emit') {
return _jsx(MessageCircle, { className: "w-4 h-4 text-muted-foreground" });
}
else if (event.type === 'state') {
return _jsx(Package, { className: "w-4 h-4 text-muted-foreground" });
}
else if (event.type === 'stream') {
return _jsx(Radio, { className: "w-4 h-4 text-muted-foreground" });
}
};

View File

@@ -0,0 +1,5 @@
import React from 'react';
import { EmitEvent } from '@/types/observability';
export declare const TraceEmitEvent: React.FC<{
event: EmitEvent;
}>;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { FunctionCall } from './code/function-call';
export const TraceEmitEvent = ({ event }) => {
return _jsx(FunctionCall, { functionName: "emit", args: [{ topic: event.topic, data: false }] });
};

View File

@@ -0,0 +1,5 @@
import { TraceEvent as TraceEventType } from '@/types/observability';
import React from 'react';
export declare const TraceEvent: React.FC<{
event: TraceEventType;
}>;

View File

@@ -0,0 +1,20 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { memo } from 'react';
import { TraceEmitEvent } from './trace-emit-event';
import { TraceLogEvent } from './trace-log-event';
import { TraceStateEvent } from './trace-state-event';
import { TraceStreamEvent } from './trace-stream-event';
export const TraceEvent = memo(({ event }) => {
if (event.type === 'log') {
return _jsx(TraceLogEvent, { event: event });
}
else if (event.type === 'emit') {
return _jsx(TraceEmitEvent, { event: event });
}
else if (event.type === 'state') {
return _jsx(TraceStateEvent, { event: event });
}
else if (event.type === 'stream') {
return _jsx(TraceStreamEvent, { event: event });
}
});

View File

@@ -0,0 +1,5 @@
import { LogEntry } from '@/types/observability';
import React from 'react';
export declare const TraceLogEvent: React.FC<{
event: LogEntry;
}>;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { LogLevelDot } from '@/components/logs/log-level-dot';
export const TraceLogEvent = ({ event }) => {
return (_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(LogLevelDot, { level: event.level }), " ", event.message] }));
};

View File

@@ -0,0 +1,5 @@
import React from 'react';
import { StateEvent } from '@/types/observability';
export declare const TraceStateEvent: React.FC<{
event: StateEvent;
}>;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { FunctionCall } from './code/function-call';
export const TraceStateEvent = ({ event }) => {
return (_jsx(FunctionCall, { objectName: "state", functionName: event.operation, args: [event.data.traceId, event.data.key, event.data.value ? false : undefined] }));
};

View File

@@ -0,0 +1,5 @@
import React from 'react';
import { StreamEvent } from '@/types/observability';
export declare const TraceStreamEvent: React.FC<{
event: StreamEvent;
}>;

View File

@@ -0,0 +1,5 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { FunctionCall } from './code/function-call';
export const TraceStreamEvent = ({ event }) => {
return (_jsx(FunctionCall, { topLevelClassName: "streams", objectName: event.streamName, functionName: event.operation, args: [event.data.groupId, event.data.id, event.data.data ? false : undefined], callsQuantity: event.calls }));
};

View File

@@ -0,0 +1,2 @@
import { TraceGroup } from '@/types/observability';
export declare const useGetEndTime: (group: TraceGroup | undefined | null) => number;

View File

@@ -0,0 +1,15 @@
import { useEffect, useState } from 'react';
export const useGetEndTime = (group) => {
const groupEndTime = group?.endTime;
const [endTime, setEndTime] = useState(groupEndTime || Date.now());
useEffect(() => {
if (groupEndTime) {
setEndTime(groupEndTime);
}
else {
const interval = setInterval(() => setEndTime(Date.now()), 50);
return () => clearInterval(interval);
}
}, [groupEndTime]);
return endTime;
};

View File

@@ -0,0 +1,8 @@
import { Trace } from '@/types/observability';
import React from 'react';
type Props = {
trace: Trace;
onClose: () => void;
};
export declare const TraceItemDetail: React.FC<Props>;
export {};

View File

@@ -0,0 +1,10 @@
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
import { Sidebar, Badge } from '@motiadev/ui';
import { formatDuration } from '@/lib/utils';
import { X } from 'lucide-react';
import { memo } from 'react';
import { EventIcon } from '../events/event-icon';
import { TraceEvent } from '../events/trace-event';
export const TraceItemDetail = memo(({ trace, onClose }) => {
return (_jsxs(Sidebar, { onClose: onClose, title: "Trace Details", subtitle: `Viewing details from step ${trace.name}`, actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }], children: [_jsxs("div", { className: "px-2 w-[800px] overflow-auto", children: [_jsxs("div", { className: "flex items-center gap-4 text-sm text-muted-foreground mb-4", children: [trace.endTime && _jsxs("span", { children: ["Duration: ", formatDuration(trace.endTime - trace.startTime)] }), _jsx("div", { className: "bg-blue-500 font-bold text-xs px-[4px] py-[2px] rounded-sm text-blue-100", children: trace.entryPoint.type }), trace.correlationId && _jsxs(Badge, { variant: "outline", children: ["Correlated: ", trace.correlationId] })] }), _jsx("div", { className: "pl-6 border-l-1 border-gray-500/40 font-mono text-xs flex flex-col gap-3", children: trace.events.map((event, index) => (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute -left-[26px] top-[8px] w-1 h-1 rounded-full bg-emerald-500 outline outline-2 outline-emerald-500/50" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(EventIcon, { event: event }), _jsxs("span", { className: "text-sm font-mono text-muted-foreground", children: ["+", Math.floor(event.timestamp - trace.startTime), "ms"] }), _jsx(TraceEvent, { event: event })] })] }, index))) })] }), trace.error && (_jsxs("div", { className: "p-4 bg-red-800/10", children: [_jsx("div", { className: "text-sm text-red-800 dark:text-red-400 font-semibold", children: trace.error.message }), _jsx("div", { className: "text-sm text-red-800 dark:text-red-400 pl-4", children: trace.error.stack })] }))] }));
});

View File

@@ -0,0 +1,10 @@
import { Trace, TraceGroup } from '@/types/observability';
import React from 'react';
type Props = {
trace: Trace;
group: TraceGroup;
groupEndTime: number;
onExpand: (traceId: string) => void;
};
export declare const TraceItem: React.FC<Props>;
export {};

View File

@@ -0,0 +1,14 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from '@motiadev/ui';
export const TraceItem = ({ trace, group, groupEndTime, onExpand }) => {
return (_jsxs("div", { className: "flex hover:bg-muted-foreground/10 relative cursor-pointer", onClick: () => onExpand(trace.id), "data-testid": "trace-timeline-item", children: [_jsx("div", { className: "flex items-center min-w-[200px] max-w-[200px] h-[32px] max-h-[32px] py-4 px-2 text-sm font-semibold text-foreground truncate sticky left-0 bg-card z-9", children: trace.name }), _jsx("div", { className: "flex w-full flex-row items-center hover:bg-muted/50 rounded-md", children: _jsx("div", { className: "relative w-full h-[32px] flex items-center", children: _jsx("div", { className: cn('h-[24px] rounded-[4px] hover:opacity-80 transition-all duration-200', {
'bg-[repeating-linear-gradient(140deg,#BEFE29,#BEFE29_8px,#ABE625_8px,#ABE625_16px)]': trace.status === 'running',
'bg-[repeating-linear-gradient(140deg,#2862FE,#2862FE_8px,#2358E5_8px,#2358E5_16px)]': trace.status === 'completed',
'bg-[repeating-linear-gradient(140deg,#EA2069,#EA2069_8px,#D41E60_8px,#D41E60_16px)]': trace.status === 'failed',
}), style: {
marginLeft: `${((trace.startTime - group.startTime) / (groupEndTime - group.startTime)) * 100}%`,
width: trace.endTime
? `${((trace.endTime - trace.startTime) / (groupEndTime - group.startTime)) * 100}%`
: `${((Date.now() - trace.startTime) / (groupEndTime - group.startTime)) * 100}%`,
} }) }) })] }));
};

View File

@@ -0,0 +1,8 @@
import { TraceGroup } from '@/types/observability';
import React from 'react';
type Props = {
status: TraceGroup['status'];
duration?: string;
};
export declare const TraceStatusBadge: React.FC<Props>;
export {};

View File

@@ -0,0 +1,18 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { Badge } from '@motiadev/ui';
import { useMemo } from 'react';
export const TraceStatusBadge = ({ status, duration }) => {
const variant = useMemo(() => {
if (status === 'running') {
return 'info';
}
if (status === 'completed') {
return 'success';
}
if (status === 'failed') {
return 'error';
}
return 'default';
}, [status]);
return _jsx(Badge, { variant: variant, children: duration && status !== 'failed' ? duration : status });
};

View File

@@ -0,0 +1,6 @@
import React from 'react';
type Props = {
groupId: string;
};
export declare const TraceTimeline: React.FC<Props>;
export {};

View File

@@ -0,0 +1,29 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useGlobalStore } from '@/stores/use-global-store';
import { useStreamGroup, useStreamItem } from '@motiadev/stream-client-react';
import { Button } from '@motiadev/ui';
import { Minus, Plus } from 'lucide-react';
import { memo, useMemo, useState } from 'react';
import { useGetEndTime } from './hooks/use-get-endtime';
import { TraceItem } from './trace-item/trace-item';
import { TraceItemDetail } from './trace-item/trace-item-detail';
export const TraceTimeline = memo(({ groupId }) => {
const { data: group } = useStreamItem({
streamName: 'motia-trace-group',
groupId: 'default',
id: groupId,
});
const { data } = useStreamGroup({ streamName: 'motia-trace', groupId });
const endTime = useGetEndTime(group);
const [zoom, setZoom] = useState(1);
const selectedTraceId = useGlobalStore((state) => state.selectedTraceId);
const selectTraceId = useGlobalStore((state) => state.selectTraceId);
const selectedTrace = useMemo(() => data?.find((trace) => trace.id === selectedTraceId), [data, selectedTraceId]);
const zoomMinus = () => {
if (zoom > 0.5)
setZoom(zoom - 0.1);
};
if (!group)
return null;
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col flex-1 overflow-x-auto h-full relative", children: _jsxs("div", { className: "flex flex-col items-center min-w-full sticky top-0", style: { width: `${zoom * 1000}px` }, children: [_jsxs("div", { className: "flex flex-1 w-full sticky top-0 bg-background z-10", children: [_jsxs("div", { className: "w-full min-h-[37px] h-[37px] min-w-[200px] max-w-[200px] flex items-center justify-center gap-2 sticky left-0 top-0 bg-card backdrop-blur-[4px] backdrop-filter", children: [_jsx(Button, { variant: "icon", size: "sm", className: "px-2", onClick: zoomMinus, children: _jsx(Minus, { className: "w-4 h-4 cursor-pointer" }) }), _jsxs("span", { className: "text-sm font-bold text-muted-foreground", children: [Math.floor(zoom * 100), "%"] }), _jsx(Button, { variant: "icon", size: "sm", className: "px-2", onClick: () => setZoom(zoom + 0.1), children: _jsx(Plus, { className: "w-4 h-4 cursor-pointer" }) })] }), _jsxs("div", { className: "flex justify-between font-mono p-2 w-full text-xs text-muted-foreground bg-card", children: [_jsx("span", { children: "0ms" }), _jsxs("span", { children: [Math.floor((endTime - group.startTime) * 0.25), "ms"] }), _jsxs("span", { children: [Math.floor((endTime - group.startTime) * 0.5), "ms"] }), _jsxs("span", { children: [Math.floor((endTime - group.startTime) * 0.75), "ms"] }), _jsxs("span", { children: [Math.floor(endTime - group.startTime), "ms"] }), _jsxs("div", { className: "absolute bottom-[-4px] w-full flex justify-between", children: [_jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" }), _jsx("span", { className: "w-[1px] h-full bg-blue-500" })] })] })] }), _jsx("div", { className: "flex flex-col w-full h-full", children: data?.map((trace) => (_jsx(TraceItem, { trace: trace, group: group, groupEndTime: endTime, onExpand: selectTraceId }, trace.id))) })] }) }), selectedTrace && _jsx(TraceItemDetail, { trace: selectedTrace, onClose: () => selectTraceId(undefined) })] }));
});

View File

@@ -0,0 +1,9 @@
import { TraceGroup } from '@/types/observability';
import React from 'react';
interface Props {
groups: TraceGroup[];
selectedGroupId?: string;
onGroupSelect: (group: TraceGroup) => void;
}
export declare const TracesGroups: React.FC<Props>;
export {};

View File

@@ -0,0 +1,15 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { cn } from '@motiadev/ui';
import { formatDistanceToNow } from 'date-fns';
import { memo } from 'react';
import { TraceStatusBadge } from './trace-status';
export const TracesGroups = memo(({ groups, selectedGroupId, onGroupSelect }) => {
const formatDuration = (duration) => {
if (!duration)
return 'N/A';
if (duration < 1000)
return `${duration}ms`;
return `${(duration / 1000).toFixed(1)}s`;
};
return (_jsx("div", { className: "overflow-auto", children: groups.length > 0 && (_jsx("div", { children: [...groups].reverse().map((group) => (_jsx("div", { "data-testid": `trace-${group.id}`, className: cn('motia-trace-group cursor-pointer transition-colors', selectedGroupId === group.id ? 'bg-muted-foreground/10' : 'hover:bg-muted/70'), onClick: () => onGroupSelect(group), children: _jsxs("div", { className: "p-3 flex flex-col gap-1", children: [_jsxs("div", { className: "flex flex-row justify-between items-center gap-2", children: [_jsx("span", { className: "font-semibold text-lg", children: group.name }), _jsx(TraceStatusBadge, { status: group.status, duration: group.endTime ? formatDuration(group.endTime - group.startTime) : undefined })] }), _jsxs("div", { className: "text-xs text-muted-foreground space-y-1", children: [_jsxs("div", { className: "flex justify-between", children: [_jsx("div", { "data-testid": "trace-id", className: "text-xs text-muted-foreground font-mono tracking-[1px]", children: group.id }), _jsxs("span", { children: [group.metadata.totalSteps, " steps"] })] }), _jsxs("div", { className: "flex justify-between", children: [formatDistanceToNow(group.startTime), " ago"] }), group.metadata.activeSteps > 0 && (_jsxs("div", { className: "text-blue-600", children: [group.metadata.activeSteps, " active"] }))] })] }) }, group.id))) })) }));
});

View File

@@ -0,0 +1 @@
export declare const TracesPage: () => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,33 @@
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { TraceTimeline } from '@/components/observability/trace-timeline';
import { useStreamGroup } from '@motiadev/stream-client-react';
import { TracesGroups } from '@/components/observability/traces-groups';
import { useGlobalStore } from '../../stores/use-global-store';
import { useEffect, useMemo, useState } from 'react';
import { Button, cn, Input } from '@motiadev/ui';
import { Search, Trash, X } from 'lucide-react';
export const TracesPage = () => {
const selectedGroupId = useGlobalStore((state) => state.selectedTraceGroupId);
const selectTraceGroupId = useGlobalStore((state) => state.selectTraceGroupId);
const { data } = useStreamGroup({ streamName: 'motia-trace-group', groupId: 'default' });
const handleGroupSelect = (group) => selectTraceGroupId(group.id);
const [search, setSearch] = useState('');
const clearTraces = () => fetch('/__motia/trace/clear', { method: 'POST' });
const traceGroups = useMemo(() => data?.filter((group) => group.name.toLowerCase().includes(search.toLowerCase()) ||
group.id.toLowerCase().includes(search.toLowerCase())), [data, search]);
useEffect(() => {
if (traceGroups && traceGroups.length > 0) {
const group = traceGroups[traceGroups.length - 1];
if (group && group.status === 'running' && group.id !== selectedGroupId) {
selectTraceGroupId(group.id);
}
}
else if (selectedGroupId) {
selectTraceGroupId(undefined);
}
}, [traceGroups]);
return (_jsxs("div", { className: "grid grid-rows-[auto_1fr] h-full", children: [_jsxs("div", { className: "flex p-2 border-b gap-2", "data-testid": "logs-search-container", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(Input, { variant: "shade", value: search, onChange: (e) => setSearch(e.target.value), className: "px-9 font-medium", placeholder: "Search by Trace ID or Step Name" }), _jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50" }), _jsx(X, { className: cn('cursor-pointer absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50 hover:text-muted-foreground', {
visible: search !== '',
invisible: search === '',
}), onClick: () => setSearch('') })] }), _jsxs(Button, { variant: "default", onClick: clearTraces, className: "h-[34px]", children: [_jsx(Trash, {}), " Clear"] })] }), _jsxs("div", { className: "grid grid-cols-[300px_1fr] overflow-hidden", children: [_jsx("div", { className: "w-[300px] border-r border-border overflow-auto h-full", "data-testid": "traces-container", children: _jsx(TracesGroups, { groups: traceGroups, selectedGroupId: selectedGroupId, onGroupSelect: handleGroupSelect }) }), _jsxs("div", { className: "overflow-auto", "data-testid": "trace-details", children: [selectedGroupId && _jsx(TraceTimeline, { groupId: selectedGroupId }), !selectedGroupId && (_jsx("div", { className: "flex items-center justify-center h-full text-muted-foreground", children: "Select a trace or trace group to view the timeline" }))] })] })] }));
};

View File

@@ -0,0 +1,2 @@
import React, { PropsWithChildren } from 'react';
export declare const RootMotia: React.FC<PropsWithChildren>;

View File

@@ -0,0 +1,7 @@
import { useAnalytics } from '@/lib/analytics';
import { useLogListener } from '@/hooks/use-log-listener';
export const RootMotia = ({ children }) => {
useLogListener();
useAnalytics();
return children;
};

View File

@@ -0,0 +1,13 @@
export interface StateItem {
groupId: string;
key: string;
type: 'string' | 'number' | 'boolean' | 'object' | 'array' | 'null';
value: string | number | boolean | object | unknown[] | null;
}
type Output = {
items: StateItem[];
deleteItems: (ids: string[]) => void;
refetch: () => void;
};
export declare const useGetStateItems: () => Output;
export {};

View File

@@ -0,0 +1,26 @@
import { useEffect, useState, useCallback } from 'react';
export const useGetStateItems = () => {
const [items, setItems] = useState([]);
const refetch = useCallback(() => {
fetch('/__motia/state')
.then(async (res) => {
if (res.ok) {
return res.json();
}
else {
throw await res.json();
}
})
.then(setItems)
.catch((err) => console.error(err));
}, []);
const deleteItems = (ids) => {
fetch('/__motia/state/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids }),
}).then(() => refetch());
};
useEffect(refetch, [refetch]);
return { items, deleteItems, refetch };
};

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { StateItem } from './hooks/states-hooks';
type Props = {
state: StateItem;
};
export declare const StateDetails: React.FC<Props>;
export {};

View File

@@ -0,0 +1,3 @@
import { jsx as _jsx } from "react/jsx-runtime";
import JsonView from 'react18-json-view';
export const StateDetails = ({ state }) => _jsx(JsonView, { src: state.value, theme: "default" });

View File

@@ -0,0 +1,7 @@
import React from 'react';
import { StateItem } from './hooks/states-hooks';
type Props = {
state: StateItem;
};
export declare const StateEditor: React.FC<Props>;
export {};

View File

@@ -0,0 +1,71 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { Button } from '@motiadev/ui';
import { AlertCircle, Check, Loader2, Save } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { JsonEditor } from '../ui/json-editor';
export const StateEditor = ({ state }) => {
const [isRequestLoading, setIsRequestLoading] = useState(false);
const [isValid, setIsValid] = useState(true);
const [jsonValue, setJsonValue] = useState(JSON.stringify(state.value, null, 2));
const [hasChanges, setHasChanges] = useState(false);
const [saveStatus, setSaveStatus] = useState('idle');
const lastSavedValue = useRef(JSON.stringify(state.value, null, 2));
useEffect(() => {
setJsonValue(JSON.stringify(state.value, null, 2));
}, [state.value]);
const handleJsonChange = useCallback((value) => {
setHasChanges(value !== lastSavedValue.current);
setJsonValue(value);
setSaveStatus('idle');
}, []);
const handleSave = async () => {
if (!isValid || !hasChanges)
return;
try {
setIsRequestLoading(true);
setSaveStatus('idle');
const response = await fetch('/__motia/state', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: state.key,
groupId: state.groupId,
value: JSON.parse(jsonValue),
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
lastSavedValue.current = jsonValue;
setSaveStatus('success');
setHasChanges(false);
setTimeout(() => {
setSaveStatus('idle');
}, 3000);
}
catch (error) {
console.error('Failed to save state:', error);
setSaveStatus('error');
}
finally {
setIsRequestLoading(false);
}
};
const resetChanges = useCallback(() => {
setJsonValue(JSON.stringify(state.value, null, 2));
setHasChanges(false);
setSaveStatus('idle');
}, [state.value]);
const statusView = useMemo(() => {
if (saveStatus === 'success') {
return (_jsx("div", { className: "bg-green-50 dark:bg-green-950/20 border border-green-200 dark:border-green-800 rounded-lg p-2", 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!"] }) }));
}
if (saveStatus === 'error') {
return (_jsx("div", { className: "bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-800 rounded-lg p-2", 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."] }) }));
}
return (_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"] })) }));
}, [saveStatus, hasChanges]);
return (_jsxs("div", { className: "flex flex-col gap-2 h-full", children: [_jsx("p", { className: "text-xs text-muted-foreground", children: "Modify the state value using the JSON editor below." }), _jsx("div", { className: "space-y-3 pt-2 flex flex-col", children: _jsxs("div", { className: "relative flex-1", children: [_jsx(JsonEditor, { value: jsonValue, onChange: handleJsonChange, onValidate: setIsValid, height: 'calc(100vh - 300px)' }), !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"] }))] }) }), _jsxs("div", { className: "flex items-center justify-between pt-2", children: [statusView, _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"] })) })] })] })] }));
};

View File

@@ -0,0 +1,8 @@
import React from 'react';
import { StateItem } from './hooks/states-hooks';
type Props = {
state: StateItem;
onClose: () => void;
};
export declare const StateSidebar: React.FC<Props>;
export {};

View File

@@ -0,0 +1,17 @@
import { jsx as _jsx } from "react/jsx-runtime";
import { Sidebar } from '@motiadev/ui';
import { X } from 'lucide-react';
import { StateDetails } from './state-details';
import { StateEditor } from './state-editor';
export const StateSidebar = ({ state, onClose }) => {
return (_jsx(Sidebar, { onClose: onClose, title: "State Details", initialWidth: 500, tabs: [
{
label: 'Overview',
content: _jsx(StateDetails, { state: state }),
},
{
label: 'Editor',
content: _jsx(StateEditor, { state: state }),
},
], actions: [{ icon: _jsx(X, {}), onClick: onClose, label: 'Close' }] }));
};

View File

@@ -0,0 +1 @@
export declare const StatesPage: () => import("react/jsx-runtime").JSX.Element;

View File

@@ -0,0 +1,56 @@
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useGlobalStore } from '@/stores/use-global-store';
import { Checkbox, Button, cn, Input } from '@motiadev/ui';
import { RefreshCw, Search, Trash, X } from 'lucide-react';
import { useMemo, useState } from 'react';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table';
import { useGetStateItems } from './hooks/states-hooks';
import { StateSidebar } from './state-sidebar';
export const StatesPage = () => {
const selectedStateId = useGlobalStore((state) => state.selectedStateId);
const selectStateId = useGlobalStore((state) => state.selectStateId);
const { items, deleteItems, refetch } = useGetStateItems();
const [search, setSearch] = useState('');
const filteredItems = useMemo(() => {
return items.filter((item) => {
return (item.groupId.toLowerCase().includes(search.toLowerCase()) ||
item.key.toLowerCase().includes(search.toLowerCase()));
});
}, [items, search]);
const selectedItem = useMemo(() => (selectedStateId ? filteredItems.find((item) => `${item.groupId}:${item.key}` === selectedStateId) : null), [filteredItems, selectedStateId]);
const [checkedItems, setCheckedItems] = useState(new Set());
const handleRowClick = (item) => selectStateId(`${item.groupId}:${item.key}`);
const onClose = () => selectStateId(undefined);
const deleteStates = () => {
deleteItems(Array.from(checkedItems));
setCheckedItems(new Set());
};
const handleCheckboxChange = (item) => {
const isChecked = checkedItems.has(`${item.groupId}:${item.key}`);
setCheckedItems((prev) => {
const newSet = new Set(prev);
if (isChecked) {
newSet.delete(`${item.groupId}:${item.key}`);
}
else {
newSet.add(`${item.groupId}:${item.key}`);
}
return newSet;
});
};
const toggleSelectAll = (checked) => {
setCheckedItems((prev) => {
const newSet = new Set(prev);
if (checked) {
filteredItems.forEach((item) => newSet.add(`${item.groupId}:${item.key}`));
}
else {
filteredItems.forEach((item) => newSet.delete(`${item.groupId}:${item.key}`));
}
return newSet;
});
};
return (_jsxs(_Fragment, { children: [selectedItem && _jsx(StateSidebar, { state: selectedItem, onClose: onClose }), _jsxs("div", { className: "grid grid-rows-[auto_1fr] h-full", "data-testid": "states-container", children: [_jsxs("div", { className: "flex p-2 border-b gap-2", "data-testid": "logs-search-container", children: [_jsxs("div", { className: "flex-1 relative", children: [_jsx(Input, { variant: "shade", value: search, onChange: (e) => setSearch(e.target.value), className: "px-9 font-medium", placeholder: "Search by Group ID or Key" }), _jsx(Search, { className: "absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50" }), _jsx(X, { className: "cursor-pointer absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground/50 hover:text-muted-foreground", onClick: () => setSearch('') })] }), _jsxs(Button, { variant: "default", className: "h-[34px]", disabled: checkedItems.size === 0, onClick: deleteStates, children: [_jsx(Trash, {}), " Delete"] }), _jsx(Button, { variant: "default", className: "h-[34px]", onClick: refetch, children: _jsx(RefreshCw, { className: "w-4 h-4 text-muted-foreground" }) })] }), _jsxs(Table, { children: [_jsx(TableHeader, { className: "sticky top-0 bg-background/20 backdrop-blur-sm", children: _jsxs(TableRow, { children: [_jsx(TableHead, { children: _jsx(Checkbox, { onClick: (evt) => evt.stopPropagation(), onCheckedChange: toggleSelectAll }) }), _jsx(TableHead, { className: "rounded-0", children: "Group ID" }), _jsx(TableHead, { children: "Key" }), _jsx(TableHead, { children: "Type" })] }) }), _jsx(TableBody, { children: filteredItems.map((item) => (_jsxs(TableRow, { "data-testid": `item-${item}`, onClick: () => handleRowClick(item), className: cn('font-mono font-semibold cursor-pointer border-0', selectedItem === item
? 'bg-muted-foreground/10 hover:bg-muted-foreground/20'
: 'hover:bg-muted-foreground/10'), children: [_jsx(TableCell, { onClick: (evt) => evt.stopPropagation(), children: _jsx(Checkbox, { checked: checkedItems.has(`${item.groupId}:${item.key}`), onClick: () => handleCheckboxChange(item) }) }), _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}`))) })] })] })] }));
};

View File

@@ -0,0 +1,12 @@
import { TutorialStep } from './tutorial-types';
declare class Tutorial {
steps: TutorialStep[];
private onStepsRegisteredCallbacks;
private onOpenCallbacks;
register(steps: TutorialStep[]): void;
onStepsRegistered(callback: () => void): void;
onOpen(callback: () => void): void;
open(): void;
}
export declare const MotiaTutorial: Tutorial;
export {};

View File

@@ -0,0 +1,36 @@
class Tutorial {
constructor() {
Object.defineProperty(this, "steps", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "onStepsRegisteredCallbacks", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
Object.defineProperty(this, "onOpenCallbacks", {
enumerable: true,
configurable: true,
writable: true,
value: []
});
}
register(steps) {
this.steps = steps;
this.onStepsRegisteredCallbacks.forEach((callback) => callback());
}
onStepsRegistered(callback) {
this.onStepsRegisteredCallbacks.push(callback);
}
onOpen(callback) {
this.onOpenCallbacks.push(callback);
}
open() {
this.onOpenCallbacks.forEach((callback) => callback());
}
}
export const MotiaTutorial = new Tutorial();

View File

@@ -0,0 +1,22 @@
export type TutorialActionClick = {
type: 'click';
selector: string;
optional?: boolean;
};
export type TutorialActionEditor = {
type: 'fill-editor';
content: Record<string, any>;
};
export type TutorialAction = TutorialActionClick | TutorialActionEditor;
export type TutorialImage = {
height: number;
src: string;
};
export type TutorialStep = {
title: string;
description: React.FC<void>;
image?: TutorialImage;
link?: string;
elementXpath?: string;
before?: TutorialAction[];
};

View File

@@ -0,0 +1,45 @@
export declare const workbenchXPath: {
sidebarContainer: string;
closePanelButton: string;
bottomPanel: string;
flows: {
dropdownFlow: (flowId: string) => string;
feature: (featureId: string) => string;
previewButton: (stepId: string) => string;
node: (stepId: string) => string;
};
endpoints: {
endpointsList: string;
endpoint: (method: string, path: string) => string;
callPanel: string;
specButton: string;
bodyTab: string;
headersTab: string;
paramsTab: string;
callTab: string;
response: string;
playButton: string;
};
tracing: {
trace: (index: number) => string;
details: string;
timeline: (index: number) => string;
};
logs: {
container: string;
searchContainer: string;
traceColumn: (index: number) => string;
row: string;
};
states: {
container: string;
row: (index: number) => string;
};
links: {
flows: string;
endpoints: string;
tracing: string;
logs: string;
states: string;
};
};

View File

@@ -0,0 +1,45 @@
export const workbenchXPath = {
sidebarContainer: '//div[@data-testid="sidebar-panel"]',
closePanelButton: '//div[@id="app-sidebar-container"]//button[@data-testid="close-panel"]',
bottomPanel: '//div[@id="bottom-panel"]',
flows: {
dropdownFlow: (flowId) => `//div[@data-testid="dropdown-${flowId}"]`,
feature: (featureId) => `//div[@data-feature-id="${featureId}"]`,
previewButton: (stepId) => `//button[@data-testid="open-code-preview-button-${stepId}"]`,
node: (stepId) => `//div[@data-testid="node-${stepId}"]`,
},
endpoints: {
endpointsList: '//div[@data-testid="endpoints-list"]',
endpoint: (method, path) => `//div[@data-testid="endpoint-${method}-${path}"]`,
callPanel: '//div[@data-testid="endpoint-details-panel"]',
specButton: '//button[@data-testid="endpoint-spec-button"]',
bodyTab: '//button[@data-testid="endpoint-body-tab"]',
headersTab: '//button[@data-testid="endpoint-headers-tab"]',
paramsTab: '//button[@data-testid="endpoint-params-tab"]',
callTab: '//button[@data-testid="endpoint-body-tab"]', // deprecated
response: '//div[@data-testid="endpoint-response-container"]',
playButton: '//button[@data-testid="endpoint-play-button"]',
},
tracing: {
trace: (index) => `(//div[contains(@class, 'motia-trace-group')])[${index}]`,
details: '//div[@data-testid="trace-details"]',
timeline: (index) => `(//div[@data-testid="trace-timeline-item"])[${index}]`,
},
logs: {
container: '//div[@data-testid="logs-container"]',
searchContainer: '//div[@data-testid="logs-search-container"]',
traceColumn: (index) => `(//td[starts-with(@data-testid, 'trace')])[${index}]`,
row: '//div[@data-testid="log-row"]',
},
states: {
container: '//div[@data-testid="states-container"]',
row: (index) => `(//tr[starts-with(@data-testid, 'item-')])[${index}]`,
},
links: {
flows: '//div[@data-testid="flows-dropdown-trigger"]',
endpoints: '//button[@data-testid="endpoints-link"]',
tracing: '//button[@data-testid="traces-link"]',
logs: '//button[@data-testid="logs-link"]',
states: '//button[@data-testid="states-link"]',
},
};

View File

@@ -0,0 +1 @@
export declare const waitForElementByXPath: (xpath: string, optional?: boolean, maxAttempts?: number, delayMs?: number) => Promise<HTMLElement | null>;

View File

@@ -0,0 +1,17 @@
export const waitForElementByXPath = async (xpath, optional = false, maxAttempts = 50, delayMs = 50) => {
let attempts = 0;
while (attempts < maxAttempts) {
const result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
const targetElement = result?.singleNodeValue;
if (targetElement) {
return targetElement;
}
else if (optional) {
return null;
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
attempts++;
}
console.warn(`Element not found after maximum attempts: ${xpath}`);
return null;
};

Some files were not shown because too many files have changed in this diff Show More