feat(automation): own automation page with tree sidebar, separate from agents

This commit is contained in:
Agent Zero
2026-08-17 12:11:14 +02:00
parent 93a53a43f6
commit aa20aba2e7
5 changed files with 158 additions and 1 deletions
+101
View File
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
import { useUIStore } from '@/store/uiStore';
import { ChevronRight, ChevronDown, Zap, Play, History, GitBranch, Settings2, Activity, Clock } from 'lucide-react';
interface AutomationNode {
title: string;
path?: string;
icon?: React.ReactNode;
children?: AutomationNode[];
}
const AUTOMATION_TREE: AutomationNode[] = [
{
title: 'Automation',
icon: <Zap className="w-4 h-4" />,
children: [
{ title: 'Übersicht', path: '/automation/overview' },
{ title: 'Ausführungen', path: '/automation/runs' },
{ title: 'Versionen', path: '/automation/versions' },
],
},
{
title: 'Einstellungen',
icon: <Settings2 className="w-4 h-4" />,
children: [
{ title: 'Trigger', path: '/automation/triggers' },
{ title: 'Aktionen', path: '/automation/actions' },
{ title: 'Bedingungen', path: '/automation/conditions' },
],
},
];
function TreeItem({ node, depth }: { node: AutomationNode; depth: number }) {
const [expanded, setExpanded] = useState(depth < 1);
const hasChildren = node.children && node.children.length > 0;
const paddingLeft = depth * 16 + 12;
if (hasChildren) {
return (
<div>
<button
onClick={() => setExpanded(!expanded)}
className="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm font-medium text-secondary-700 hover:bg-secondary-100 transition-colors min-h-touch"
style={{ paddingLeft }}
aria-expanded={expanded}
>
{expanded ? <ChevronDown className="w-3.5 h-3.5 flex-shrink-0" /> : <ChevronRight className="w-3.5 h-3.5 flex-shrink-0" />}
{node.icon}
<span>{node.title}</span>
</button>
{expanded && (
<div>
{node.children!.map((child) => (
<TreeItem key={child.path || child.title} node={child} depth={depth + 1} />
))}
</div>
)}
</div>
);
}
return (
<NavLink
to={node.path!}
className={({ isActive }) =>
`flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors min-h-touch ${
isActive
? 'bg-primary-100 text-primary-700 font-medium'
: 'text-secondary-600 hover:bg-secondary-100 hover:text-secondary-900'
}`
}
style={{ paddingLeft: paddingLeft + 20 }}
>
{node.icon}
<span>{node.title}</span>
</NavLink>
);
}
export function AutomationPage() {
const sidebarOpen = useUIStore((state) => state.sidebarOpen);
return (
<div className="flex h-full overflow-hidden" data-testid="automation-page">
<aside className={`${sidebarOpen ? 'w-64 border-r border-secondary-200' : 'w-0'} flex-shrink-0 min-h-[calc(100vh-4rem)] transition-all duration-200 overflow-hidden`}>
<div className="w-64 flex-shrink-0 p-4">
<h1 className="text-xl font-bold text-secondary-900 mb-4">Automation</h1>
<nav className="space-y-0.5 overflow-y-auto" role="navigation" aria-label="Automation Navigation">
{AUTOMATION_TREE.map((node) => (
<TreeItem key={node.title} node={node} depth={0} />
))}
</nav>
</div>
</aside>
<main className="flex-1 p-6 overflow-y-auto" data-testid="automation-content">
<Outlet />
</main>
</div>
);
}
+1 -1
View File
@@ -41,7 +41,7 @@ export function StartPage() {
{ id: 'workspaces', label: 'Workspaces', icon: <LayoutGrid className="w-4 h-4" />, active: true },
{ id: 'settings', label: 'Einstellungen', icon: <Settings className="w-4 h-4" />, onClick: () => navigate('/settings') },
{ id: 'agents', label: 'Agenten', icon: <Bot className="w-4 h-4" />, onClick: () => navigate('/agents/overview') },
{ id: 'automation', label: 'Automation', icon: <Zap className="w-4 h-4" />, onClick: () => navigate('/agents/automation') },
{ id: 'automation', label: 'Automation', icon: <Zap className="w-4 h-4" />, onClick: () => navigate('/automation/overview') },
{ id: 'help', label: 'Hilfe', icon: <HelpCircle className="w-4 h-4" />, onClick: () => navigate('/help/welcome') },
];
@@ -0,0 +1,28 @@
import React from 'react';
export function AutomationOverviewPage() {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Automation Übersicht</h1>
<p className="text-secondary-600 mb-6">
Erstellen und verwalten Sie automatisierte Workflows. Definieren Sie Trigger, Bedingungen und Aktionen.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="p-6 bg-white rounded-xl border border-secondary-200">
<div className="w-12 h-12 bg-warning-100 rounded-lg flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-warning-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
</div>
<h3 className="font-semibold text-secondary-900">Automations</h3>
<p className="text-sm text-secondary-500 mt-1">Workflows erstellen und verwalten</p>
</div>
<div className="p-6 bg-white rounded-xl border border-secondary-200">
<div className="w-12 h-12 bg-primary-100 rounded-lg flex items-center justify-center mb-3">
<svg className="w-6 h-6 text-primary-600" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
</div>
<h3 className="font-semibold text-secondary-900">Einstellungen</h3>
<p className="text-sm text-secondary-500 mt-1">Trigger, Aktionen und Bedingungen konfigurieren</p>
</div>
</div>
</div>
);
}
@@ -0,0 +1,12 @@
import React from 'react';
export function AutomationPlaceholderPage() {
return (
<div className="max-w-4xl mx-auto">
<h1 className="text-2xl font-bold text-secondary-900 mb-4">Automation</h1>
<p className="text-secondary-600">
Diese Seite wird gerade erstellt. Wählen Sie ein Thema aus dem Menü links.
</p>
</div>
);
}
+16
View File
@@ -80,6 +80,9 @@ const HelpPlaceholderPage = React.lazy(() => import('@/pages/help/HelpPlaceholde
const AgentsPage = React.lazy(() => import('@/pages/Agents').then(m => ({ default: m.AgentsPage })));
const AgentsOverviewPage = React.lazy(() => import('@/pages/agents/AgentsOverview').then(m => ({ default: m.AgentsOverviewPage })));
const AgentsPlaceholderPage = React.lazy(() => import('@/pages/agents/AgentsPlaceholder').then(m => ({ default: m.AgentsPlaceholderPage })));
const AutomationPage = React.lazy(() => import('@/pages/Automation').then(m => ({ default: m.AutomationPage })));
const AutomationOverviewPage = React.lazy(() => import('@/pages/automation/AutomationOverview').then(m => ({ default: m.AutomationOverviewPage })));
const AutomationPlaceholderPage = React.lazy(() => import('@/pages/automation/AutomationPlaceholder').then(m => ({ default: m.AutomationPlaceholderPage })));
const ApiDocsPage = React.lazy(() => import('@/pages/ApiDocs').then(m => ({ default: m.ApiDocsPage })));
/** Centered spinner fallback for lazy-loaded routes */
@@ -174,6 +177,19 @@ const router = createBrowserRouter([
{ path: '*', element: withSuspense(<AgentsPlaceholderPage />) },
],
},
{
path: '/automation',
element: withSuspense(<AutomationPage />),
children: [
{ path: 'overview', element: withSuspense(<AutomationOverviewPage />) },
{ path: 'runs', element: withSuspense(<AutomationDashboardPage />) },
{ path: 'versions', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: 'triggers', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: 'actions', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: 'conditions', element: withSuspense(<AgentsPlaceholderPage />) },
{ path: '*', element: withSuspense(<AutomationPlaceholderPage />) },
],
},
{
path: '/settings',
element: <PermissionRoute permission="settings:read">{withSuspense(<SettingsPage />)}</PermissionRoute>,