Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"reactflow": "^11.11.4",
"tailwind-merge": "^3.3.1",
"zod": "^4.1.11",
"zustand": "^5.0.8"
"zustand": "^5.0.8",
"ansi_up": "^6.0.6"
}
}
17 changes: 14 additions & 3 deletions frontend/src/components/timeline/ExecutionInspector.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useMemo, useState } from 'react'
import { AnsiUp } from 'ansi_up'
import { RunSelector } from '@/components/timeline/RunSelector'
import { ExecutionTimeline } from '@/components/timeline/ExecutionTimeline'
import { EventInspector } from '@/components/timeline/EventInspector'
Expand Down Expand Up @@ -234,6 +235,9 @@ export function ExecutionInspector() {
const previewText = preview.truncated
? `${preview.text.trimEnd()}\n…`
: preview.text
const hasAnsi = /\u001b\[[0-9;]*m/.test(previewText)
const au = hasAnsi ? new AnsiUp() : null
const ansiHtml = hasAnsi && au ? au.ansi_to_html(previewText) : ''

return (
<div key={log.id} className="border rounded-md bg-background px-3 py-2 space-y-1 min-w-0">
Expand All @@ -247,9 +251,16 @@ export function ExecutionInspector() {
<div className="text-[11px] text-muted-foreground">Node: {log.nodeId}</div>
)}
<div className="text-[11px] max-w-full">
<pre className="whitespace-pre-wrap break-words font-mono text-[11px]">
{previewText}
</pre>
{hasAnsi ? (
<div
className="font-mono text-[11px] whitespace-pre-wrap break-words"
dangerouslySetInnerHTML={{ __html: ansiHtml }}
/>
) : (
<pre className="whitespace-pre-wrap break-words font-mono text-[11px]">
{previewText}
</pre>
)}
{preview.truncated && (
<button
className="text-[10px] text-blue-500 hover:text-blue-700 mt-1"
Expand Down
88 changes: 73 additions & 15 deletions frontend/src/components/ui/MessageModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Copy } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { AnsiUp } from 'ansi_up'

interface MessageModalProps {
open: boolean
Expand All @@ -16,36 +18,92 @@ interface MessageModalProps {
}

export function MessageModal({ open, onOpenChange, title, message }: MessageModalProps) {
const hasAnsi = /\u001b\[[0-9;]*m/.test(message)
const [wrap, setWrap] = useState(true)
const [colorize, setColorize] = useState(true)

// Load persisted prefs on mount; default colorize to hasAnsi if unset
useEffect(() => {
try {
const w = localStorage.getItem('messageModal.wrap')
if (w !== null) setWrap(w === '1')
const c = localStorage.getItem('messageModal.color')
if (c !== null) setColorize(c === '1')
else setColorize(hasAnsi)
} catch {}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

useEffect(() => {
try { localStorage.setItem('messageModal.wrap', wrap ? '1' : '0') } catch {}
}, [wrap])
useEffect(() => {
try { localStorage.setItem('messageModal.color', colorize ? '1' : '0') } catch {}
}, [colorize])

const ansiHtml = useMemo(() => {
if (!(colorize && hasAnsi)) return ''
const au = new AnsiUp()
return au.ansi_to_html(message)
}, [colorize, hasAnsi, message])
const copyToClipboard = () => {
navigator.clipboard.writeText(message)
}

return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogContent className="max-w-4xl max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>
Full message content
</DialogDescription>
</DialogHeader>

<div className="flex-1 overflow-auto">
<pre className="text-xs font-mono whitespace-pre-wrap break-words bg-muted/30 rounded p-3 border">
{message}
</pre>
<div className="flex-1 overflow-y-auto">
{colorize && hasAnsi ? (
<div
className={`text-xs font-mono bg-muted/30 rounded p-3 border min-h-[200px] ${wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre overflow-x-auto'}`}
dangerouslySetInnerHTML={{ __html: ansiHtml }}
/>
) : (
<pre className={`text-xs font-mono bg-muted/30 rounded p-3 border min-h-[200px] ${wrap ? 'whitespace-pre-wrap break-words' : 'whitespace-pre overflow-x-auto'}`}>
{message}
</pre>
)}
</div>

<div className="flex justify-between items-center pt-4">
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
className="flex items-center gap-2"
>
<Copy className="h-4 w-4" />
Copy to clipboard
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={copyToClipboard}
className="flex items-center gap-2"
>
<Copy className="h-4 w-4" />
Copy to clipboard
</Button>
<Button
variant={wrap ? 'default' : 'outline'}
size="sm"
onClick={() => setWrap((v) => !v)}
aria-pressed={wrap}
title="Toggle word wrap"
>
Wrap: {wrap ? 'On' : 'Off'}
</Button>
<Button
variant={colorize ? 'default' : 'outline'}
size="sm"
onClick={() => setColorize((v) => !v)}
aria-pressed={colorize}
title="Toggle ANSI colorization"
disabled={!hasAnsi}
>
Colorize: {colorize && hasAnsi ? 'On' : 'Off'}
</Button>
</div>

<Button
variant="outline"
Expand All @@ -58,4 +116,4 @@ export function MessageModal({ open, onOpenChange, title, message }: MessageModa
</DialogContent>
</Dialog>
)
}
}
17 changes: 15 additions & 2 deletions packages/component-sdk/src/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ async function runComponentInDocker<I, O>(
params: I,
context: ExecutionContext,
): Promise<O> {
const { image, command, entrypoint, env = {}, network = 'none', timeoutSeconds = 300 } = runner;
const { image, command, entrypoint, env = {}, network = 'none', platform, volumes, timeoutSeconds = 300 } = runner;

context.logger.info(`[Docker] Running ${image} with command: ${command.join(' ')}`);
context.emitProgress(`Starting Docker container: ${image}`);
Expand All @@ -34,6 +34,20 @@ async function runComponentInDocker<I, O>(
'--network', network, // Network mode (default: none for security)
];

// Set target platform when requested (enables emulation on Apple Silicon/ARM hosts)
if (platform && platform.trim().length > 0) {
dockerArgs.push('--platform', platform);
}

// Add volume mounts
if (Array.isArray(volumes)) {
for (const vol of volumes) {
if (!vol || !vol.source || !vol.target) continue;
const mode = vol.readOnly ? ':ro' : '';
dockerArgs.push('-v', `${vol.source}:${vol.target}${mode}`);
}
}

// Add environment variables
for (const [key, value] of Object.entries(env)) {
dockerArgs.push('-e', `${key}=${value}`);
Expand Down Expand Up @@ -170,4 +184,3 @@ export async function runComponentWithRunner<I, O>(
throw new Error(`Unsupported runner type ${(runner as any).kind}`);
}
}

6 changes: 6 additions & 0 deletions packages/component-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface DockerRunnerConfig {
entrypoint?: string; // Override container's default entrypoint
env?: Record<string, string>;
network?: 'none' | 'bridge' | 'host'; // Network mode (default: none for security)
platform?: string; // Optional platform to run under (e.g., 'linux/amd64')
volumes?: Array<{
source: string; // host path
target: string; // container path
readOnly?: boolean;
}>; // Optional volume mounts
timeoutSeconds?: number;
}

Expand Down
1 change: 1 addition & 0 deletions worker/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import './security/naabu';
import './security/dnsx';
import './security/httpx';
import './security/notify';
import './security/prowler-scan';

// IT Automation components
import './it-automation/google-workspace-license-unassign';
Expand Down
Loading
Loading