Files
LangBot/web/src/app/home/pipelines/page.tsx
Junyan Chin 42caae1bcf feat: Implement extension and bot limitations across services and UI (#1991)
- Added checks for maximum allowed extensions, bots, and pipelines in the backend services (PluginsRouterGroup, BotService, MCPService, PipelineService).
- Updated system configuration to include limitation settings for max_bots, max_pipelines, and max_extensions.
- Enhanced frontend components to handle limitations, providing user feedback when limits are reached.
- Added internationalization support for limitation messages in English, Japanese, Simplified Chinese, and Traditional Chinese.
2026-02-22 17:25:45 +08:00

196 lines
6.2 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import CreateCardComponent from '@/app/infra/basic-component/create-card-component/CreateCardComponent';
import { httpClient } from '@/app/infra/http/HttpClient';
import { PipelineCardVO } from '@/app/home/pipelines/components/pipeline-card/PipelineCardVO';
import PipelineCard from '@/app/home/pipelines/components/pipeline-card/PipelineCard';
import styles from './pipelineConfig.module.css';
import { toast } from 'sonner';
import { useTranslation } from 'react-i18next';
import PipelineDialog from './PipelineDetailDialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { systemInfo } from '@/app/infra/http';
export default function PluginConfigPage() {
const { t } = useTranslation();
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const [isEditForm, setIsEditForm] = useState(false);
const [pipelineList, setPipelineList] = useState<PipelineCardVO[]>([]);
const [selectedPipelineId, setSelectedPipelineId] = useState('');
const [sortByValue, setSortByValue] = useState<string>('created_at');
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
useEffect(() => {
// Load sort preference from localStorage
const savedSortBy = localStorage.getItem('pipeline_sort_by');
const savedSortOrder = localStorage.getItem('pipeline_sort_order');
if (savedSortBy && savedSortOrder) {
setSortByValue(savedSortBy);
setSortOrderValue(savedSortOrder);
getPipelines(savedSortBy, savedSortOrder);
} else {
getPipelines();
}
}, []);
function getPipelines(
sortBy: string = sortByValue,
sortOrder: string = sortOrderValue,
) {
httpClient
.getPipelines(sortBy, sortOrder)
.then((value) => {
const currentTime = new Date();
const pipelineList = value.pipelines.map((pipeline) => {
const lastUpdatedTimeAgo = Math.floor(
(currentTime.getTime() -
new Date(
pipeline.updated_at ?? currentTime.getTime(),
).getTime()) /
1000 /
60 /
60 /
24,
);
const lastUpdatedTimeAgoText =
lastUpdatedTimeAgo > 0
? ` ${lastUpdatedTimeAgo} ${t('pipelines.daysAgo')}`
: t('pipelines.today');
return new PipelineCardVO({
lastUpdatedTimeAgo: lastUpdatedTimeAgoText,
description: pipeline.description,
id: pipeline.uuid ?? '',
name: pipeline.name,
emoji: pipeline.emoji,
isDefault: pipeline.is_default ?? false,
});
});
setPipelineList(pipelineList);
})
.catch((error) => {
toast.error(t('pipelines.getPipelineListError') + error.message);
});
}
const handlePipelineClick = (pipelineId: string) => {
setSelectedPipelineId(pipelineId);
setIsEditForm(true);
setDialogOpen(true);
};
const handleCreateNew = () => {
const maxPipelines = systemInfo.limitation?.max_pipelines ?? -1;
if (maxPipelines >= 0 && pipelineList.length >= maxPipelines) {
toast.error(t('limitation.maxPipelinesReached', { max: maxPipelines }));
return;
}
setIsEditForm(false);
setSelectedPipelineId('');
setDialogOpen(true);
};
function handleSortChange(value: string) {
const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
setSortByValue(newSortBy);
setSortOrderValue(newSortOrder);
// Save sort preference to localStorage
localStorage.setItem('pipeline_sort_by', newSortBy);
localStorage.setItem('pipeline_sort_order', newSortOrder);
getPipelines(newSortBy, newSortOrder);
}
return (
<div className={styles.configPageContainer}>
<PipelineDialog
open={dialogOpen}
onOpenChange={setDialogOpen}
pipelineId={selectedPipelineId || undefined}
isEditMode={isEditForm}
onFinish={() => {
getPipelines();
}}
onNewPipelineCreated={(pipelineId) => {
getPipelines();
setSelectedPipelineId(pipelineId);
setIsEditForm(true);
setDialogOpen(true);
}}
onDeletePipeline={() => {
getPipelines();
setDialogOpen(false);
}}
onCancel={() => {
setDialogOpen(false);
}}
/>
<div className="flex flex-row justify-between items-center mb-4 px-[0.8rem]">
<Select
value={`${sortByValue},${sortOrderValue}`}
onValueChange={handleSortChange}
>
<SelectTrigger className="w-[180px] cursor-pointer bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectValue placeholder={t('pipelines.sortBy')} />
</SelectTrigger>
<SelectContent className="bg-[#ffffff] dark:bg-[#2a2a2e]">
<SelectItem
value="created_at,DESC"
className="text-gray-900 dark:text-gray-100"
>
{t('pipelines.newestCreated')}
</SelectItem>
<SelectItem
value="created_at,ASC"
className="text-gray-900 dark:text-gray-100"
>
{t('pipelines.earliestCreated')}
</SelectItem>
<SelectItem
value="updated_at,DESC"
className="text-gray-900 dark:text-gray-100"
>
{t('pipelines.recentlyEdited')}
</SelectItem>
<SelectItem
value="updated_at,ASC"
className="text-gray-900 dark:text-gray-100"
>
{t('pipelines.earliestEdited')}
</SelectItem>
</SelectContent>
</Select>
</div>
<div className={styles.pipelineListContainer}>
<CreateCardComponent
width={'100%'}
height={'10rem'}
plusSize={'90px'}
onClick={handleCreateNew}
/>
{pipelineList.map((pipeline) => {
return (
<div
key={pipeline.id}
onClick={() => handlePipelineClick(pipeline.id)}
>
<PipelineCard cardVO={pipeline} />
</div>
);
})}
</div>
</div>
);
}