- 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.
169 lines
6.3 KiB
TypeScript
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>
|
|
);
|
|
} |