Files
router-studio/web/src/apps/studio/index.tsx
abearxiong 231caa3b9a feat: Implement view management features with a new UI for editing and listing views
- Added a resizable panel layout in the studio app to display the view list alongside the main application.
- Refactored the studio store to include new methods for fetching and managing route views.
- Introduced a new DataItemForm component for configuring data items in views.
- Created a ViewEditor component for adding and editing views, including data items and queries.
- Enhanced the ViewList component to support searching, adding, editing, and deleting views.
- Updated UI components (Button, Checkbox, Dialog, Input, Label, Table) for better styling and functionality.
- Added environment configuration for API URL.
- Introduced a new workspace configuration for pnpm.
2025-12-31 17:54:11 +08:00

169 lines
6.3 KiB
TypeScript

import { toast, ToastContainer } from 'react-toastify';
import { useStudioStore } from './store.ts';
import { useEffect, useState } from 'react';
import { MonitorPlay, Play } from 'lucide-react';
import { Panel, Group } from 'react-resizable-panels'
import { ViewList } from '../view/list.tsx';
export const AppProvider = () => {
return <main className='w-full h-screen flex flex-col overflow-hidden'>
<Group className="h-full flex-1 overflow-hidden">
<Panel defaultSize={300} minSize={250} maxSize={500} className="border-r overflow-auto">
<ViewList />
</Panel>
<Panel>
<App />
</Panel>
</Group>
<ToastContainer
position="top-right"
autoClose={3000}
hideProgressBar
newestOnTop
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="light" />
</main>
}
interface RouteItem {
id: string;
path?: string;
key?: string;
description?: string;
metadata?: Record<string, any>;
}
export const App = () => {
const { routes, getRouteList, run } = useStudioStore();
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [visibleIds, setVisibleIds] = useState<Set<string>>(new Set());
useEffect(() => {
getRouteList();
}, []);
const toggleDescription = (id: string) => {
const newExpanded = new Set(expandedIds);
if (newExpanded.has(id)) {
newExpanded.delete(id);
} else {
newExpanded.add(id);
}
setExpandedIds(newExpanded);
};
const toggleIdVisibility = (e: React.MouseEvent, id: string) => {
e.stopPropagation();
const newVisible = new Set(visibleIds);
if (newVisible.has(id)) {
newVisible.delete(id);
} else {
newVisible.add(id);
}
setVisibleIds(newVisible);
};
return (
<div className="max-w-5xl mx-auto p-6 h-full overflow-auto">
<div className="space-y-1">
{routes.map((route: RouteItem) => {
const isExpanded = expandedIds.has(route.id);
const isIdVisible = visibleIds.has(route.id);
const len = route.description?.length || 0;
const isLongDescription = len > 20;
return (
<div
key={route.id}
className="px-4 py-3 border-b border-gray-100 hover:bg-gray-50/50 transition-all duration-200"
>
<div className="flex flex-col gap-2.5">
{/* ID and Path/Key in one line */}
<div className="flex gap-2.5 flex-wrap items-center justify-between">
<div className="flex gap-2.5 flex-wrap items-center">
<span
onClick={(e) => toggleIdVisibility(e, route.id)}
className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-semibold bg-gray-900 text-white cursor-pointer hover:bg-gray-700 transition-all duration-200 shadow-sm"
>
{isIdVisible ? route.id : 'id'}
</span>
{(route.path || route.key) && (
<div className="bg-gray-100 px-3 py-1.5 rounded-md font-mono text-sm text-gray-900 border border-gray-200">
{route.path}{route.key && ` / ${route.key}`}
</div>
)}
</div>
<div className='inline-flex items-center justify-center gap-1'>
<button
className="p-1.5 rounded-md text-gray-500 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
title="直接运行"
onClick={() => run(route)}
>
<Play size={14} strokeWidth={2.5} />
</button>
<button className="p-1.5 rounded-md text-gray-20 hover:text-gray-900 hover:bg-gray-200 transition-all duration-200 cursor-pointer"
title="高级运行"
onClick={() => run(route)}>
<MonitorPlay size={14} strokeWidth={2.5} />
</button>
</div>
</div>
{/* Description with expand/collapse */}
{route.description && (
<div
className="cursor-pointer group"
>
<div
className={`text-gray-700 transition-colors duration-200 cursor-pointer ${isExpanded ? 'text-gray-900' : 'group-hover:text-gray-900'
}`}
>
{isExpanded ? (
<p className="text-sm leading-relaxed whitespace-pre-wrap">
{route.description}
</p>
) : (
<p className="text-sm leading-relaxed overflow-hidden text-ellipsis whitespace-nowrap">
{route.description}
</p>
)}
</div>
{isLongDescription && (
<p className="text-xs text-gray-400 mt-1 group-hover:text-gray-500 transition-colors duration-200"
onClick={() => toggleDescription(route.id)}
>
{isExpanded ? '点击收起' : '点击展开'}
</p>
)}
</div>
)}
{/* Metadata */}
{route.metadata && Object.keys(route.metadata).length > 0 && (
<div className="mt-0.5">
<span className="text-xs text-gray-500 mr-2 font-medium">Metadata:</span>
<div className="flex gap-1.5 flex-wrap">
{Object.entries(route.metadata).map(([k, v]) => (
<span
key={k}
className="inline-flex items-center px-2 py-1 rounded text-xs bg-white text-gray-700 border border-gray-200 shadow-sm"
>
<span className="font-semibold text-gray-900">{k}:</span> {String(v)}
</span>
))}
</div>
</div>
)}
</div>
</div>
);
})}
</div>
</div>
);
}