mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-27 16:04:21 +00:00
perf: ui
This commit is contained in:
@@ -4,22 +4,12 @@ import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import PipelineFormComponent from '@/app/home/pipelines/components/pipeline-form/PipelineFormComponent';
|
||||
import DebugDialog from '@/app/home/pipelines/components/debug-dialog/DebugDialog';
|
||||
import PipelineExtension from '@/app/home/pipelines/components/pipeline-extensions/PipelineExtension';
|
||||
import PipelineMonitoringTab from '@/app/home/pipelines/components/monitoring-tab/PipelineMonitoringTab';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { useSidebarData } from '@/app/home/components/home-sidebar/SidebarDataContext';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Settings, Puzzle, Bug, BarChart3 } from 'lucide-react';
|
||||
import { Settings, Bug, BarChart3 } from 'lucide-react';
|
||||
|
||||
export default function PipelineDetailContent({ id }: { id: string }) {
|
||||
const isCreateMode = id === 'new';
|
||||
@@ -39,35 +29,29 @@ export default function PipelineDetailContent({ id }: { id: string }) {
|
||||
}, [id, isCreateMode, pipelines, setDetailEntityName, t]);
|
||||
|
||||
const [activeTab, setActiveTab] = useState('config');
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
const [isWebSocketConnected, setIsWebSocketConnected] = useState(false);
|
||||
const [formDirty, setFormDirty] = useState(false);
|
||||
|
||||
function handleFinish() {
|
||||
refreshPipelines();
|
||||
}
|
||||
|
||||
function handleDeletePipeline() {
|
||||
httpClient.deletePipeline(id).then(() => {
|
||||
refreshPipelines();
|
||||
setShowDeleteConfirm(false);
|
||||
router.push('/home/pipelines');
|
||||
});
|
||||
}
|
||||
|
||||
function handleNewPipelineCreated(newPipelineId: string) {
|
||||
refreshPipelines();
|
||||
// Navigate to the newly created pipeline's detail view via query param
|
||||
router.push(`/home/pipelines?id=${encodeURIComponent(newPipelineId)}`);
|
||||
}
|
||||
|
||||
// Create mode: simple form layout
|
||||
// ==================== Create Mode ====================
|
||||
if (isCreateMode) {
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center gap-3 pb-4 shrink-0">
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('pipelines.createPipeline')}
|
||||
</h1>
|
||||
<Button type="submit" form="pipeline-form">
|
||||
{t('common.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto min-h-0">
|
||||
@@ -76,7 +60,7 @@ export default function PipelineDetailContent({ id }: { id: string }) {
|
||||
pipelineId={undefined}
|
||||
isEditMode={false}
|
||||
disableForm={false}
|
||||
showButtons={true}
|
||||
showButtons={false}
|
||||
onFinish={handleFinish}
|
||||
onNewPipelineCreated={handleNewPipelineCreated}
|
||||
onDeletePipeline={() => {}}
|
||||
@@ -87,115 +71,85 @@ export default function PipelineDetailContent({ id }: { id: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// Edit mode: tabbed layout with config, extensions, debug, monitoring
|
||||
// ==================== Edit Mode ====================
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center gap-3 pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">
|
||||
{t('pipelines.editPipeline')}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<Tabs
|
||||
key={id}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex flex-1 flex-col min-h-0"
|
||||
>
|
||||
<TabsList className="shrink-0">
|
||||
<TabsTrigger value="config" className="gap-1.5">
|
||||
<Settings className="size-3.5" />
|
||||
{t('pipelines.configuration')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="extensions" className="gap-1.5">
|
||||
<Puzzle className="size-3.5" />
|
||||
{t('pipelines.extensions.title')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="debug" className="gap-1.5">
|
||||
<Bug className="size-3.5" />
|
||||
{t('pipelines.debugChat')}
|
||||
<span
|
||||
className={`inline-block size-2 rounded-full ${
|
||||
isWebSocketConnected ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="monitoring" className="gap-1.5">
|
||||
<BarChart3 className="size-3.5" />
|
||||
{t('pipelines.monitoring.title')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent
|
||||
value="config"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<PipelineFormComponent
|
||||
pipelineId={id}
|
||||
isEditMode={true}
|
||||
disableForm={false}
|
||||
showButtons={true}
|
||||
onFinish={handleFinish}
|
||||
onNewPipelineCreated={handleNewPipelineCreated}
|
||||
onDeletePipeline={() => setShowDeleteConfirm(true)}
|
||||
onCancel={() => router.push('/home/pipelines')}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="extensions"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<PipelineExtension pipelineId={id} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="debug" className="flex-1 min-h-0 mt-4">
|
||||
<DebugDialog
|
||||
open={activeTab === 'debug'}
|
||||
pipelineId={id}
|
||||
isEmbedded={true}
|
||||
onConnectionStatusChange={setIsWebSocketConnected}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent
|
||||
value="monitoring"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<PipelineMonitoringTab
|
||||
pipelineId={id}
|
||||
onNavigateToMonitoring={() => {
|
||||
router.push('/home/monitoring');
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Sticky Header: title + save button */}
|
||||
<div className="flex items-center justify-between pb-4 shrink-0">
|
||||
<h1 className="text-xl font-semibold">{t('pipelines.editPipeline')}</h1>
|
||||
<Button type="submit" form="pipeline-form" disabled={!formDirty}>
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Delete confirmation dialog */}
|
||||
<Dialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('common.confirmDelete')}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{t('pipelines.deleteConfirmation')}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-4">{t('pipelines.deleteConfirmation')}</div>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowDeleteConfirm(false)}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleDeletePipeline}>
|
||||
{t('common.confirmDelete')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
{/* Horizontal Tabs */}
|
||||
<Tabs
|
||||
key={id}
|
||||
value={activeTab}
|
||||
onValueChange={setActiveTab}
|
||||
className="flex flex-1 flex-col min-h-0"
|
||||
>
|
||||
<TabsList className="shrink-0">
|
||||
<TabsTrigger value="config" className="gap-1.5">
|
||||
<Settings className="size-3.5" />
|
||||
{t('pipelines.configuration')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="debug" className="gap-1.5">
|
||||
<Bug className="size-3.5" />
|
||||
{t('pipelines.debugChat')}
|
||||
<span
|
||||
className={`inline-block size-2 rounded-full ${
|
||||
isWebSocketConnected ? 'bg-green-500' : 'bg-red-500'
|
||||
}`}
|
||||
/>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="monitoring" className="gap-1.5">
|
||||
<BarChart3 className="size-3.5" />
|
||||
{t('pipelines.monitoring.title')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Tab: Configuration */}
|
||||
<TabsContent
|
||||
value="config"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<PipelineFormComponent
|
||||
pipelineId={id}
|
||||
isEditMode={true}
|
||||
disableForm={false}
|
||||
showButtons={false}
|
||||
onFinish={handleFinish}
|
||||
onNewPipelineCreated={handleNewPipelineCreated}
|
||||
onDeletePipeline={() => {}}
|
||||
onCancel={() => router.push('/home/pipelines')}
|
||||
onDirtyChange={setFormDirty}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab: Debug */}
|
||||
<TabsContent value="debug" className="flex-1 min-h-0 mt-4">
|
||||
<DebugDialog
|
||||
open={activeTab === 'debug'}
|
||||
pipelineId={id}
|
||||
isEmbedded={true}
|
||||
onConnectionStatusChange={setIsWebSocketConnected}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* Tab: Monitoring */}
|
||||
<TabsContent
|
||||
value="monitoring"
|
||||
className="flex-1 min-h-0 overflow-y-auto mt-4"
|
||||
>
|
||||
<PipelineMonitoringTab
|
||||
pipelineId={id}
|
||||
onNavigateToMonitoring={() => {
|
||||
router.push('/home/monitoring');
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,24 @@ import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { extractI18nObject } from '@/i18n/I18nProvider';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Info, Brain, Zap, Shield, FileOutput } from 'lucide-react';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@/components/ui/card';
|
||||
import {
|
||||
Info,
|
||||
Brain,
|
||||
Zap,
|
||||
Shield,
|
||||
FileOutput,
|
||||
Puzzle,
|
||||
Trash2,
|
||||
Copy,
|
||||
} from 'lucide-react';
|
||||
import PipelineExtension from '@/app/home/pipelines/components/pipeline-extensions/PipelineExtension';
|
||||
|
||||
export default function PipelineFormComponent({
|
||||
onFinish,
|
||||
@@ -42,6 +59,7 @@ export default function PipelineFormComponent({
|
||||
showButtons = true,
|
||||
onDeletePipeline,
|
||||
onCancel,
|
||||
onDirtyChange,
|
||||
}: {
|
||||
pipelineId?: string;
|
||||
isEditMode: boolean;
|
||||
@@ -51,6 +69,7 @@ export default function PipelineFormComponent({
|
||||
onNewPipelineCreated: (pipelineId: string) => void;
|
||||
onDeletePipeline: () => void;
|
||||
onCancel?: () => void;
|
||||
onDirtyChange?: (dirty: boolean) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||
@@ -93,6 +112,7 @@ export default function PipelineFormComponent({
|
||||
trigger: Zap,
|
||||
safety: Shield,
|
||||
output: FileOutput,
|
||||
extensions: Puzzle,
|
||||
};
|
||||
|
||||
const formLabelList: SectionItem[] = isEditMode
|
||||
@@ -122,6 +142,11 @@ export default function PipelineFormComponent({
|
||||
name: 'output',
|
||||
icon: SECTION_ICONS.output,
|
||||
},
|
||||
{
|
||||
label: t('pipelines.extensions.title'),
|
||||
name: 'extensions',
|
||||
icon: SECTION_ICONS.extensions,
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
@@ -165,6 +190,11 @@ export default function PipelineFormComponent({
|
||||
return JSON.stringify(watchedValues) !== savedSnapshotRef.current;
|
||||
}, [isEditMode, watchedValues]);
|
||||
|
||||
// Notify parent when dirty state changes
|
||||
useEffect(() => {
|
||||
onDirtyChange?.(hasUnsavedChanges);
|
||||
}, [hasUnsavedChanges, onDirtyChange]);
|
||||
|
||||
useEffect(() => {
|
||||
// get config schema from metadata
|
||||
httpClient.getGeneralPipelineMetadata().then((resp) => {
|
||||
@@ -314,27 +344,29 @@ export default function PipelineFormComponent({
|
||||
// If this is the runner selector stage, render it directly
|
||||
if (stage.name === 'runner') {
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">
|
||||
{extractI18nObject(stage.label)}
|
||||
</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Card key={stage.name}>
|
||||
<CardHeader>
|
||||
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
|
||||
{stage.description && (
|
||||
<CardDescription>
|
||||
{extractI18nObject(stage.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -346,52 +378,56 @@ export default function PipelineFormComponent({
|
||||
// For n8n-service-api config, use N8nAuthFormComponent for form linkage
|
||||
if (stage.name === 'n8n-service-api') {
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">
|
||||
{extractI18nObject(stage.label)}
|
||||
</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<N8nAuthFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Card key={stage.name}>
|
||||
<CardHeader>
|
||||
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
|
||||
{stage.description && (
|
||||
<CardDescription>
|
||||
{extractI18nObject(stage.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<N8nAuthFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] ||
|
||||
{}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={stage.name} className="space-y-4 mb-6">
|
||||
<div className="text-lg font-medium">
|
||||
{extractI18nObject(stage.label)}
|
||||
</div>
|
||||
{stage.description && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{extractI18nObject(stage.description)}
|
||||
</div>
|
||||
)}
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Card key={stage.name}>
|
||||
<CardHeader>
|
||||
<CardTitle>{extractI18nObject(stage.label)}</CardTitle>
|
||||
{stage.description && (
|
||||
<CardDescription>
|
||||
{extractI18nObject(stage.description)}
|
||||
</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<DynamicFormComponent
|
||||
itemConfigList={stage.config}
|
||||
initialValues={
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(form.watch(formName) as Record<string, any>)?.[stage.name] || {}
|
||||
}
|
||||
onSubmit={(values) => {
|
||||
handleDynamicFormEmit(formName, stage.name, values);
|
||||
}}
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -477,58 +513,130 @@ export default function PipelineFormComponent({
|
||||
{/* Basic info section */}
|
||||
{activeSection === 'basic' && (
|
||||
<div className="space-y-6">
|
||||
{/* Name and Emoji in same row */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
{t('common.name')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.emoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.icon')}</FormLabel>
|
||||
<FormControl>
|
||||
<EmojiPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{/* Basic Information Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{t('pipelines.basicInfo')}</CardTitle>
|
||||
<CardDescription>
|
||||
{t('pipelines.basicInfoDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{/* Name and Emoji in same row */}
|
||||
<div className="flex gap-4 items-start">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.name"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>
|
||||
{t('common.name')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.emoji"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('common.icon')}</FormLabel>
|
||||
<FormControl>
|
||||
<EmojiPicker
|
||||
value={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('common.description')}
|
||||
<span className="text-red-500">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="basic.description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('common.description')}
|
||||
<span className="text-destructive">*</span>
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Copy pipeline (edit mode only) */}
|
||||
{isEditMode && (
|
||||
<div className="flex items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5">
|
||||
<p className="text-sm font-medium">
|
||||
{t('pipelines.copyPipelineAction')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t('pipelines.copyPipelineHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
<Copy className="size-4 mr-1.5" />
|
||||
{t('common.copy')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Danger Zone (edit mode only) */}
|
||||
{isEditMode && (
|
||||
<Card className="border-destructive/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-destructive">
|
||||
{t('pipelines.dangerZone')}
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
{t('pipelines.dangerZoneDescription')}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{t('pipelines.deletePipelineAction')}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{isDefaultPipeline
|
||||
? t('pipelines.defaultPipelineCannotDelete')
|
||||
: t('pipelines.deletePipelineHint')}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
disabled={isDefaultPipeline}
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<Trash2 className="size-4 mr-1.5" />
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -566,6 +674,10 @@ export default function PipelineFormComponent({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'extensions' && pipelineId && (
|
||||
<PipelineExtension pipelineId={pipelineId} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user