From bf05f25f7e6ac2c31a331399f09f29955118996b Mon Sep 17 00:00:00 2001 From: Vinod Kiran Date: Thu, 10 Jul 2025 20:29:24 +0530 Subject: [PATCH] New Feature Pagination (#4704) * common pagination component * Pagination for Doc Store Dashboard * Pagination for Executions Dashboard * Pagination Support for Tables * lint fixes * update view message dialog UI * initial loading was ignoring the pagination counts * 1) default page size change 2) ensure page limits are passed on load 3) co-pilot review comments (n+1 query) 4) * 1) default page size change 2) ensure page limits are passed on load 3) co-pilot review comments (n+1 query) 4) refresh lists after insert/delete. * Enhancement: Improve handling of empty responses in DocumentStore and API key services - Added check for empty entities in DocumentStoreDTO.fromEntities to return an empty array. - Updated condition in getAllDocumentStores to handle total count correctly, allowing for zero total. - Refined logic in getAllApiKeys to check for empty keys and ensure correct API key retrieval. - Adjusted UI components to safely handle potential undefined apiKeys array. * Refresh API key list on pagination change * Enhancement: Update pagination and filter handling across components - Increased default items per page in AgentExecutions from 10 to 12. - Improved JSON parsing for chat type and feedback type filters in ViewMessagesDialog. - Enhanced execution filtering logic in AgentExecutions to ensure proper pagination and state management. - Refactored filter section in AgentExecutions for better readability and functionality. - Updated refresh logic in Agentflows to use the correct agentflow version. * add workspaceId to removeAllChatMessages * Refactor chat message retrieval logic for improved efficiency and maintainability - Introduced a new `handleFeedbackQuery` function to streamline feedback-related queries. - Enhanced pagination handling for session-based queries in `getMessagesWithFeedback`. - Updated `ViewMessagesDialog` to sort messages in descending order by default. - Simplified image rendering logic in `DocumentStoreTable` for better readability. * - Update `validateChatflowAPIKey` and `validateAPIKey` functions to get the correct keys array - Enhanced error handling in the `sanitizeExecution` function to ensure safe access to nested properties * Refactor API key validation logic for improved accuracy and error handling - Consolidated API key validation in `validateAPIKey` to return detailed validation results. - Updated `validateFlowAPIKey` to streamline flow API key validation. - Introduced `getApiKeyById` function in the API key service for better key retrieval. - Removed unused function `getAllChatSessionsFromChatflow` from the chat message API. --------- Co-authored-by: Henry --- .../server/src/Interface.DocumentStore.ts | 3 + .../server/src/controllers/apikey/index.ts | 4 +- .../src/controllers/chat-messages/index.ts | 11 +- .../server/src/controllers/chatflows/index.ts | 10 +- .../server/src/controllers/dataset/index.ts | 7 +- .../src/controllers/documentstore/index.ts | 14 +- .../src/controllers/evaluations/index.ts | 4 +- .../src/controllers/evaluators/index.ts | 4 +- .../server/src/controllers/stats/index.ts | 11 +- .../server/src/controllers/tools/index.ts | 4 +- .../server/src/controllers/variables/index.ts | 4 +- packages/server/src/index.ts | 93 ++- packages/server/src/services/apikey/index.ts | 44 +- .../src/services/chat-messages/index.ts | 8 +- .../server/src/services/chatflows/index.ts | 29 +- packages/server/src/services/dataset/index.ts | 54 +- .../src/services/documentstore/index.ts | 21 +- .../server/src/services/evaluations/index.ts | 92 ++- .../server/src/services/evaluator/index.ts | 20 +- .../server/src/services/executions/index.ts | 2 +- .../src/services/export-import/index.ts | 29 +- packages/server/src/services/stats/index.ts | 12 +- packages/server/src/services/tools/index.ts | 19 +- .../server/src/services/variables/index.ts | 22 +- packages/server/src/utils/buildChatflow.ts | 4 +- packages/server/src/utils/getChatMessage.ts | 307 ++++++-- packages/server/src/utils/pagination.ts | 29 + packages/server/src/utils/upsertVector.ts | 4 +- packages/server/src/utils/validateKey.ts | 73 +- packages/ui/src/api/apikey.js | 2 +- packages/ui/src/api/chatflows.js | 4 +- packages/ui/src/api/dataset.js | 4 +- packages/ui/src/api/documentstore.js | 2 +- packages/ui/src/api/evaluations.js | 2 +- packages/ui/src/api/evaluators.js | 2 +- packages/ui/src/api/tools.js | 2 +- packages/ui/src/api/variables.js | 2 +- .../ui/src/ui-component/cards/StatsCard.jsx | 10 +- .../dialog/ViewMessagesDialog.jsx | 402 ++++++---- .../pagination/TablePagination.jsx | 85 +++ .../ui-component/table/DocumentStoreTable.jsx | 255 +++++++ packages/ui/src/utils/exportImport.js | 2 +- .../ui/src/views/agentexecutions/index.jsx | 162 ++-- packages/ui/src/views/agentflows/index.jsx | 71 +- packages/ui/src/views/apikey/index.jsx | 233 +++--- .../CustomAssistantConfigurePreview.jsx | 3 +- packages/ui/src/views/canvas/CanvasHeader.jsx | 3 +- packages/ui/src/views/chatflows/index.jsx | 79 +- .../ui/src/views/datasets/DatasetItems.jsx | 32 +- packages/ui/src/views/datasets/index.jsx | 251 ++++--- packages/ui/src/views/docstore/index.jsx | 306 +++----- packages/ui/src/views/evaluations/index.jsx | 244 +++--- packages/ui/src/views/evaluators/index.jsx | 695 ++++++++++-------- packages/ui/src/views/tools/index.jsx | 62 +- packages/ui/src/views/variables/index.jsx | 302 ++++---- 55 files changed, 2595 insertions(+), 1560 deletions(-) create mode 100644 packages/server/src/utils/pagination.ts create mode 100644 packages/ui/src/ui-component/pagination/TablePagination.jsx create mode 100644 packages/ui/src/ui-component/table/DocumentStoreTable.jsx diff --git a/packages/server/src/Interface.DocumentStore.ts b/packages/server/src/Interface.DocumentStore.ts index 93ec640cb..dd1e4263f 100644 --- a/packages/server/src/Interface.DocumentStore.ts +++ b/packages/server/src/Interface.DocumentStore.ts @@ -290,6 +290,9 @@ export class DocumentStoreDTO { } static fromEntities(entities: DocumentStore[]): DocumentStoreDTO[] { + if (entities.length === 0) { + return [] + } return entities.map((entity) => this.fromEntity(entity)) } diff --git a/packages/server/src/controllers/apikey/index.ts b/packages/server/src/controllers/apikey/index.ts index 029af1e5e..340ff27b2 100644 --- a/packages/server/src/controllers/apikey/index.ts +++ b/packages/server/src/controllers/apikey/index.ts @@ -2,12 +2,14 @@ import { Request, Response, NextFunction } from 'express' import { StatusCodes } from 'http-status-codes' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import apikeyService from '../../services/apikey' +import { getPageAndLimitParams } from '../../utils/pagination' // Get api keys const getAllApiKeys = async (req: Request, res: Response, next: NextFunction) => { try { const autoCreateNewKey = true - const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, autoCreateNewKey) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await apikeyService.getAllApiKeys(req.user?.activeWorkspaceId, autoCreateNewKey, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/chat-messages/index.ts b/packages/server/src/controllers/chat-messages/index.ts index f5d3dd044..b000c9ea3 100644 --- a/packages/server/src/controllers/chat-messages/index.ts +++ b/packages/server/src/controllers/chat-messages/index.ts @@ -9,6 +9,7 @@ import { ChatMessage } from '../../database/entities/ChatMessage' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import { utilGetChatMessage } from '../../utils/getChatMessage' +import { getPageAndLimitParams } from '../../utils/pagination' const getFeedbackTypeFilters = (_feedbackTypeFilters: ChatMessageRatingType[]): ChatMessageRatingType[] | undefined => { try { @@ -71,6 +72,9 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio const startDate = req.query?.startDate as string | undefined const endDate = req.query?.endDate as string | undefined const feedback = req.query?.feedback as boolean | undefined + + const { page, limit } = getPageAndLimitParams(req) + let feedbackTypeFilters = req.query?.feedbackType as ChatMessageRatingType[] | undefined if (feedbackTypeFilters) { feedbackTypeFilters = getFeedbackTypeFilters(feedbackTypeFilters) @@ -93,7 +97,9 @@ const getAllChatMessages = async (req: Request, res: Response, next: NextFunctio messageId, feedback, feedbackTypeFilters, - activeWorkspaceId + activeWorkspaceId, + page, + limit ) return res.json(parseAPIResponse(apiResponse)) } catch (error) { @@ -202,7 +208,8 @@ const removeAllChatMessages = async (req: Request, res: Response, next: NextFunc startDate, endDate, feedback: isFeedback, - feedbackTypes: feedbackTypeFilters + feedbackTypes: feedbackTypeFilters, + activeWorkspaceId: workspaceId }) const messageIds = messages.map((message) => message.id) diff --git a/packages/server/src/controllers/chatflows/index.ts b/packages/server/src/controllers/chatflows/index.ts index 7d5fe04df..88921444c 100644 --- a/packages/server/src/controllers/chatflows/index.ts +++ b/packages/server/src/controllers/chatflows/index.ts @@ -8,6 +8,7 @@ import chatflowsService from '../../services/chatflows' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { checkUsageLimit } from '../../utils/quotaUsage' import { RateLimiterManager } from '../../utils/rateLimit' +import { getPageAndLimitParams } from '../../utils/pagination' const checkIfChatflowIsValidForStreaming = async (req: Request, res: Response, next: NextFunction) => { try { @@ -67,7 +68,14 @@ const deleteChatflow = async (req: Request, res: Response, next: NextFunction) = const getAllChatflows = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await chatflowsService.getAllChatflows(req.query?.type as ChatflowType, req.user?.activeWorkspaceId) + const { page, limit } = getPageAndLimitParams(req) + + const apiResponse = await chatflowsService.getAllChatflows( + req.query?.type as ChatflowType, + req.user?.activeWorkspaceId, + page, + limit + ) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/dataset/index.ts b/packages/server/src/controllers/dataset/index.ts index f4d08de97..d6c753118 100644 --- a/packages/server/src/controllers/dataset/index.ts +++ b/packages/server/src/controllers/dataset/index.ts @@ -2,10 +2,12 @@ import { Request, Response, NextFunction } from 'express' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import datasetService from '../../services/dataset' import { StatusCodes } from 'http-status-codes' +import { getPageAndLimitParams } from '../../utils/pagination' const getAllDatasets = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await datasetService.getAllDatasets(req.user?.activeWorkspaceId) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await datasetService.getAllDatasets(req.user?.activeWorkspaceId, page, limit) return res.json(apiResponse) } catch (error) { next(error) @@ -17,7 +19,8 @@ const getDataset = async (req: Request, res: Response, next: NextFunction) => { if (typeof req.params === 'undefined' || !req.params.id) { throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: datasetService.getDataset - id not provided!`) } - const apiResponse = await datasetService.getDataset(req.params.id) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await datasetService.getDataset(req.params.id, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/documentstore/index.ts b/packages/server/src/controllers/documentstore/index.ts index 04f1f339a..9d19373b4 100644 --- a/packages/server/src/controllers/documentstore/index.ts +++ b/packages/server/src/controllers/documentstore/index.ts @@ -6,6 +6,7 @@ import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { DocumentStoreDTO } from '../../Interface' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { FLOWISE_COUNTER_STATUS, FLOWISE_METRIC_COUNTERS } from '../../Interface.Metrics' +import { getPageAndLimitParams } from '../../utils/pagination' const createDocumentStore = async (req: Request, res: Response, next: NextFunction) => { try { @@ -37,8 +38,17 @@ const createDocumentStore = async (req: Request, res: Response, next: NextFuncti const getAllDocumentStores = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await documentStoreService.getAllDocumentStores(req.user?.activeWorkspaceId) - return res.json(DocumentStoreDTO.fromEntities(apiResponse)) + const { page, limit } = getPageAndLimitParams(req) + + const apiResponse: any = await documentStoreService.getAllDocumentStores(req.user?.activeWorkspaceId, page, limit) + if (apiResponse?.total >= 0) { + return res.json({ + total: apiResponse.total, + data: DocumentStoreDTO.fromEntities(apiResponse.data) + }) + } else { + return res.json(DocumentStoreDTO.fromEntities(apiResponse)) + } } catch (error) { next(error) } diff --git a/packages/server/src/controllers/evaluations/index.ts b/packages/server/src/controllers/evaluations/index.ts index 9ae7cb000..4cde76158 100644 --- a/packages/server/src/controllers/evaluations/index.ts +++ b/packages/server/src/controllers/evaluations/index.ts @@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from 'express' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import evaluationsService from '../../services/evaluations' +import { getPageAndLimitParams } from '../../utils/pagination' const createEvaluation = async (req: Request, res: Response, next: NextFunction) => { try { @@ -81,7 +82,8 @@ const deleteEvaluation = async (req: Request, res: Response, next: NextFunction) const getAllEvaluations = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await evaluationsService.getAllEvaluations(req.user?.activeWorkspaceId) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await evaluationsService.getAllEvaluations(req.user?.activeWorkspaceId, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/evaluators/index.ts b/packages/server/src/controllers/evaluators/index.ts index 3f4e29133..1f151c37c 100644 --- a/packages/server/src/controllers/evaluators/index.ts +++ b/packages/server/src/controllers/evaluators/index.ts @@ -2,10 +2,12 @@ import { Request, Response, NextFunction } from 'express' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' import evaluatorService from '../../services/evaluator' +import { getPageAndLimitParams } from '../../utils/pagination' const getAllEvaluators = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await evaluatorService.getAllEvaluators(req.user?.activeWorkspaceId) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await evaluatorService.getAllEvaluators(req.user?.activeWorkspaceId, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/stats/index.ts b/packages/server/src/controllers/stats/index.ts index c86bd544a..e159bf053 100644 --- a/packages/server/src/controllers/stats/index.ts +++ b/packages/server/src/controllers/stats/index.ts @@ -45,7 +45,16 @@ const getChatflowStats = async (req: Request, res: Response, next: NextFunction) return res.status(500).send(e) } } - const apiResponse = await statsService.getChatflowStats(chatflowid, chatTypes, startDate, endDate, '', true, feedbackTypeFilters) + const apiResponse = await statsService.getChatflowStats( + chatflowid, + chatTypes, + startDate, + endDate, + '', + true, + feedbackTypeFilters, + req.user?.activeWorkspaceId + ) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/tools/index.ts b/packages/server/src/controllers/tools/index.ts index e5eda2ee7..8b5772a4f 100644 --- a/packages/server/src/controllers/tools/index.ts +++ b/packages/server/src/controllers/tools/index.ts @@ -2,6 +2,7 @@ import { NextFunction, Request, Response } from 'express' import toolsService from '../../services/tools' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' +import { getPageAndLimitParams } from '../../utils/pagination' const createTool = async (req: Request, res: Response, next: NextFunction) => { try { @@ -40,7 +41,8 @@ const deleteTool = async (req: Request, res: Response, next: NextFunction) => { const getAllTools = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await toolsService.getAllTools(req.user?.activeWorkspaceId) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await toolsService.getAllTools(req.user?.activeWorkspaceId, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/controllers/variables/index.ts b/packages/server/src/controllers/variables/index.ts index 3f1417b91..42b58603e 100644 --- a/packages/server/src/controllers/variables/index.ts +++ b/packages/server/src/controllers/variables/index.ts @@ -3,6 +3,7 @@ import variablesService from '../../services/variables' import { Variable } from '../../database/entities/Variable' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { StatusCodes } from 'http-status-codes' +import { getPageAndLimitParams } from '../../utils/pagination' const createVariable = async (req: Request, res: Response, next: NextFunction) => { try { @@ -45,7 +46,8 @@ const deleteVariable = async (req: Request, res: Response, next: NextFunction) = const getAllVariables = async (req: Request, res: Response, next: NextFunction) => { try { - const apiResponse = await variablesService.getAllVariables(req.user?.activeWorkspaceId) + const { page, limit } = getPageAndLimitParams(req) + const apiResponse = await variablesService.getAllVariables(req.user?.activeWorkspaceId, page, limit) return res.json(apiResponse) } catch (error) { next(error) diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 9460bb26f..38c7fb8c9 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -21,7 +21,7 @@ import { WHITELIST_URLS } from './utils/constants' import { initializeJwtCookieMiddleware, verifyToken } from './enterprise/middleware/passport' import { IdentityManager } from './IdentityManager' import { SSEStreamer } from './utils/SSEStreamer' -import { getAPIKeyWorkspaceID, validateAPIKey } from './utils/validateKey' +import { validateAPIKey } from './utils/validateKey' import { LoggedInUser } from './enterprise/Interface.Enterprise' import { IMetricsProvider } from './Interface.Metrics' import { Prometheus } from './metrics/Prometheus' @@ -217,58 +217,55 @@ export class App { return res.status(401).json({ error: 'Unauthorized Access' }) } } - const isKeyValidated = await validateAPIKey(req) - if (!isKeyValidated) { + + const { isValid, workspaceId: apiKeyWorkSpaceId } = await validateAPIKey(req) + if (!isValid) { return res.status(401).json({ error: 'Unauthorized Access' }) } - const apiKeyWorkSpaceId = await getAPIKeyWorkspaceID(req) - if (apiKeyWorkSpaceId) { - // Find workspace - const workspace = await this.AppDataSource.getRepository(Workspace).findOne({ - where: { id: apiKeyWorkSpaceId } - }) - if (!workspace) { - return res.status(401).json({ error: 'Unauthorized Access' }) - } - // Find owner role - const ownerRole = await this.AppDataSource.getRepository(Role).findOne({ - where: { name: GeneralRole.OWNER, organizationId: IsNull() } - }) - if (!ownerRole) { - return res.status(401).json({ error: 'Unauthorized Access' }) - } - - // Find organization - const activeOrganizationId = workspace.organizationId as string - const org = await this.AppDataSource.getRepository(Organization).findOne({ - where: { id: activeOrganizationId } - }) - if (!org) { - return res.status(401).json({ error: 'Unauthorized Access' }) - } - const subscriptionId = org.subscriptionId as string - const customerId = org.customerId as string - const features = await this.identityManager.getFeaturesByPlan(subscriptionId) - const productId = await this.identityManager.getProductIdFromSubscription(subscriptionId) - - // @ts-ignore - req.user = { - permissions: [...JSON.parse(ownerRole.permissions)], - features, - activeOrganizationId: activeOrganizationId, - activeOrganizationSubscriptionId: subscriptionId, - activeOrganizationCustomerId: customerId, - activeOrganizationProductId: productId, - isOrganizationAdmin: true, - activeWorkspaceId: apiKeyWorkSpaceId, - activeWorkspace: workspace.name, - isApiKeyValidated: true - } - next() - } else { + // Find workspace + const workspace = await this.AppDataSource.getRepository(Workspace).findOne({ + where: { id: apiKeyWorkSpaceId } + }) + if (!workspace) { return res.status(401).json({ error: 'Unauthorized Access' }) } + + // Find owner role + const ownerRole = await this.AppDataSource.getRepository(Role).findOne({ + where: { name: GeneralRole.OWNER, organizationId: IsNull() } + }) + if (!ownerRole) { + return res.status(401).json({ error: 'Unauthorized Access' }) + } + + // Find organization + const activeOrganizationId = workspace.organizationId as string + const org = await this.AppDataSource.getRepository(Organization).findOne({ + where: { id: activeOrganizationId } + }) + if (!org) { + return res.status(401).json({ error: 'Unauthorized Access' }) + } + const subscriptionId = org.subscriptionId as string + const customerId = org.customerId as string + const features = await this.identityManager.getFeaturesByPlan(subscriptionId) + const productId = await this.identityManager.getProductIdFromSubscription(subscriptionId) + + // @ts-ignore + req.user = { + permissions: [...JSON.parse(ownerRole.permissions)], + features, + activeOrganizationId: activeOrganizationId, + activeOrganizationSubscriptionId: subscriptionId, + activeOrganizationCustomerId: customerId, + activeOrganizationProductId: productId, + isOrganizationAdmin: true, + activeWorkspaceId: apiKeyWorkSpaceId!, + activeWorkspace: workspace.name, + isApiKeyValidated: true + } + next() } } else { return res.status(401).json({ error: 'Unauthorized Access' }) diff --git a/packages/server/src/services/apikey/index.ts b/packages/server/src/services/apikey/index.ts index a63d315a5..a8b603633 100644 --- a/packages/server/src/services/apikey/index.ts +++ b/packages/server/src/services/apikey/index.ts @@ -9,19 +9,31 @@ import { Not, IsNull } from 'typeorm' import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { v4 as uuidv4 } from 'uuid' -const getAllApiKeysFromDB = async (workspaceId?: string) => { +const getAllApiKeysFromDB = async (workspaceId?: string, page: number = -1, limit: number = -1) => { const appServer = getRunningExpressApp() - const keys = await appServer.AppDataSource.getRepository(ApiKey).findBy(getWorkspaceSearchOptions(workspaceId)) - const keysWithChatflows = await addChatflowsCount(keys) - return keysWithChatflows + const queryBuilder = appServer.AppDataSource.getRepository(ApiKey).createQueryBuilder('api_key').orderBy('api_key.updatedDate', 'DESC') + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + if (workspaceId) queryBuilder.andWhere('api_key.workspaceId = :workspaceId', { workspaceId }) + const [data, total] = await queryBuilder.getManyAndCount() + const keysWithChatflows = await addChatflowsCount(data) + + if (page > 0 && limit > 0) { + return { total, data: keysWithChatflows } + } else { + return keysWithChatflows + } } -const getAllApiKeys = async (workspaceId?: string, autoCreateNewKey?: boolean) => { +const getAllApiKeys = async (workspaceId?: string, autoCreateNewKey?: boolean, page: number = -1, limit: number = -1) => { try { - let keys = await getAllApiKeysFromDB(workspaceId) - if (keys.length === 0 && autoCreateNewKey) { + let keys = await getAllApiKeysFromDB(workspaceId, page, limit) + const isEmpty = keys?.total === 0 || (Array.isArray(keys) && keys?.length === 0) + if (isEmpty && autoCreateNewKey) { await createApiKey('DefaultKey', workspaceId) - keys = await getAllApiKeysFromDB(workspaceId) + keys = await getAllApiKeysFromDB(workspaceId, page, limit) } return keys } catch (error) { @@ -44,6 +56,21 @@ const getApiKey = async (apiKey: string) => { } } +const getApiKeyById = async (apiKeyId: string) => { + try { + const appServer = getRunningExpressApp() + const currentKey = await appServer.AppDataSource.getRepository(ApiKey).findOneBy({ + id: apiKeyId + }) + if (!currentKey) { + return undefined + } + return currentKey + } catch (error) { + throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: apikeyService.getApiKeyById - ${getErrorMessage(error)}`) + } +} + const createApiKey = async (keyName: string, workspaceId?: string) => { try { const apiKey = generateAPIKey() @@ -231,5 +258,6 @@ export default { updateApiKey, verifyApiKey, getApiKey, + getApiKeyById, importKeys } diff --git a/packages/server/src/services/chat-messages/index.ts b/packages/server/src/services/chat-messages/index.ts index b2b2c00a3..40565de29 100644 --- a/packages/server/src/services/chat-messages/index.ts +++ b/packages/server/src/services/chat-messages/index.ts @@ -39,7 +39,9 @@ const getAllChatMessages = async ( messageId?: string, feedback?: boolean, feedbackTypes?: ChatMessageRatingType[], - activeWorkspaceId?: string + activeWorkspaceId?: string, + page?: number, + pageSize?: number ): Promise => { try { const dbResponse = await utilGetChatMessage({ @@ -54,7 +56,9 @@ const getAllChatMessages = async ( messageId, feedback, feedbackTypes, - activeWorkspaceId + activeWorkspaceId, + page, + pageSize }) return dbResponse } catch (error) { diff --git a/packages/server/src/services/chatflows/index.ts b/packages/server/src/services/chatflows/index.ts index 9900b7c1b..a6ba3371b 100644 --- a/packages/server/src/services/chatflows/index.ts +++ b/packages/server/src/services/chatflows/index.ts @@ -127,21 +127,36 @@ const deleteChatflow = async (chatflowId: string, orgId: string, workspaceId: st } } -const getAllChatflows = async (type?: ChatflowType, workspaceId?: string): Promise => { +const getAllChatflows = async (type?: ChatflowType, workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() - const dbResponse = await appServer.AppDataSource.getRepository(ChatFlow).findBy(getWorkspaceSearchOptions(workspaceId)) + + const queryBuilder = appServer.AppDataSource.getRepository(ChatFlow) + .createQueryBuilder('chat_flow') + .orderBy('chat_flow.updatedDate', 'DESC') + + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } if (type === 'MULTIAGENT') { - return dbResponse.filter((chatflow) => chatflow.type === 'MULTIAGENT') + queryBuilder.andWhere('chat_flow.type = :type', { type: 'MULTIAGENT' }) } else if (type === 'AGENTFLOW') { - return dbResponse.filter((chatflow) => chatflow.type === 'AGENTFLOW') + queryBuilder.andWhere('chat_flow.type = :type', { type: 'AGENTFLOW' }) } else if (type === 'ASSISTANT') { - return dbResponse.filter((chatflow) => chatflow.type === 'ASSISTANT') + queryBuilder.andWhere('chat_flow.type = :type', { type: 'ASSISTANT' }) } else if (type === 'CHATFLOW') { // fetch all chatflows that are not agentflow - return dbResponse.filter((chatflow) => chatflow.type === 'CHATFLOW' || !chatflow.type) + queryBuilder.andWhere('chat_flow.type = :type', { type: 'CHATFLOW' }) + } + if (workspaceId) queryBuilder.andWhere('chat_flow.workspaceId = :workspaceId', { workspaceId }) + const [data, total] = await queryBuilder.getManyAndCount() + + if (page > 0 && limit > 0) { + return { data, total } + } else { + return data } - return dbResponse } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/packages/server/src/services/dataset/index.ts b/packages/server/src/services/dataset/index.ts index 4b4913226..351919e6b 100644 --- a/packages/server/src/services/dataset/index.ts +++ b/packages/server/src/services/dataset/index.ts @@ -8,22 +8,33 @@ import { Readable } from 'stream' import { In } from 'typeorm' import csv from 'csv-parser' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' -const getAllDatasets = async (workspaceId?: string) => { +const getAllDatasets = async (workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() + const queryBuilder = appServer.AppDataSource.getRepository(Dataset).createQueryBuilder('ds').orderBy('ds.updatedDate', 'DESC') + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + if (workspaceId) queryBuilder.andWhere('ds.workspaceId = :workspaceId', { workspaceId }) + + const [data, total] = await queryBuilder.getManyAndCount() + const returnObj: Dataset[] = [] - const datasets = await appServer.AppDataSource.getRepository(Dataset).findBy(getWorkspaceSearchOptions(workspaceId)) // TODO: This is a hack to get the row count for each dataset. Need to find a better way to do this - for (const dataset of datasets) { + for (const dataset of data) { ;(dataset as any).rowCount = await appServer.AppDataSource.getRepository(DatasetRow).count({ where: { datasetId: dataset.id } }) returnObj.push(dataset) } - return returnObj + if (page > 0 && limit > 0) { + return { total, data: returnObj } + } else { + return returnObj + } } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, @@ -32,36 +43,45 @@ const getAllDatasets = async (workspaceId?: string) => { } } -const getDataset = async (id: string) => { +const getDataset = async (id: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() const dataset = await appServer.AppDataSource.getRepository(Dataset).findOneBy({ id: id }) - let items = await appServer.AppDataSource.getRepository(DatasetRow).find({ - where: { datasetId: id }, - order: { sequenceNo: 'asc' } - }) + const queryBuilder = appServer.AppDataSource.getRepository(DatasetRow).createQueryBuilder('dsr').orderBy('dsr.sequenceNo', 'ASC') + queryBuilder.andWhere('dsr.datasetId = :datasetId', { datasetId: id }) + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + let [data, total] = await queryBuilder.getManyAndCount() // special case for sequence numbers == -1 (this happens when the update script is run and all rows are set to -1) // check if there are any sequence numbers == -1, if so set them to the max sequence number + 1 - const missingSequenceNumbers = items.filter((item) => item.sequenceNo === -1) + const missingSequenceNumbers = data.filter((item) => item.sequenceNo === -1) if (missingSequenceNumbers.length > 0) { - const maxSequenceNumber = items.reduce((prev, current) => (prev.sequenceNo > current.sequenceNo ? prev : current)) + const maxSequenceNumber = data.reduce((prev, current) => (prev.sequenceNo > current.sequenceNo ? prev : current)) let sequenceNo = maxSequenceNumber.sequenceNo + 1 for (const zeroSequenceNumber of missingSequenceNumbers) { zeroSequenceNumber.sequenceNo = sequenceNo++ } await appServer.AppDataSource.getRepository(DatasetRow).save(missingSequenceNumbers) // now get the items again - items = await appServer.AppDataSource.getRepository(DatasetRow).find({ - where: { datasetId: id }, - order: { sequenceNo: 'asc' } - }) + const queryBuilder2 = appServer.AppDataSource.getRepository(DatasetRow) + .createQueryBuilder('dsr') + .orderBy('dsr.sequenceNo', 'ASC') + queryBuilder2.andWhere('dsr.datasetId = :datasetId', { datasetId: id }) + if (page > 0 && limit > 0) { + queryBuilder2.skip((page - 1) * limit) + queryBuilder2.take(limit) + } + ;[data, total] = await queryBuilder2.getManyAndCount() } return { ...dataset, - rows: items + rows: data, + total } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: datasetService.getDataset - ${getErrorMessage(error)}`) diff --git a/packages/server/src/services/documentstore/index.ts b/packages/server/src/services/documentstore/index.ts index 355bc5512..a9a12fd47 100644 --- a/packages/server/src/services/documentstore/index.ts +++ b/packages/server/src/services/documentstore/index.ts @@ -77,11 +77,26 @@ const createDocumentStore = async (newDocumentStore: DocumentStore, orgId: strin } } -const getAllDocumentStores = async (workspaceId?: string) => { +const getAllDocumentStores = async (workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() - const entities = await appServer.AppDataSource.getRepository(DocumentStore).findBy(getWorkspaceSearchOptions(workspaceId)) - return entities + const queryBuilder = appServer.AppDataSource.getRepository(DocumentStore) + .createQueryBuilder('doc_store') + .orderBy('doc_store.updatedDate', 'DESC') + + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + if (workspaceId) queryBuilder.andWhere('doc_store.workspaceId = :workspaceId', { workspaceId }) + + const [data, total] = await queryBuilder.getManyAndCount() + + if (page > 0 && limit > 0) { + return { data, total } + } else { + return data + } } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/packages/server/src/services/evaluations/index.ts b/packages/server/src/services/evaluations/index.ts index 2d44b3ce2..9195ac26f 100644 --- a/packages/server/src/services/evaluations/index.ts +++ b/packages/server/src/services/evaluations/index.ts @@ -338,42 +338,80 @@ const createEvaluation = async (body: ICommonObject, baseURL: string, orgId: str } } -const getAllEvaluations = async (workspaceId?: string) => { +const getAllEvaluations = async (workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() - const findAndOrderBy: any = { - where: getWorkspaceSearchOptions(workspaceId), - order: { - runDate: 'DESC' - } - } - const evaluations = await appServer.AppDataSource.getRepository(Evaluation).find(findAndOrderBy) + // First, get the count of distinct evaluation names for the total + // needed as the The getCount() method in TypeORM doesn't respect the GROUP BY clause and will return the total count of records + const countQuery = appServer.AppDataSource.getRepository(Evaluation) + .createQueryBuilder('ev') + .select('COUNT(DISTINCT(ev.name))', 'count') + .where('ev.workspaceId = :workspaceId', { workspaceId: workspaceId }) + + const totalResult = await countQuery.getRawOne() + const total = totalResult ? parseInt(totalResult.count) : 0 + + // Then get the distinct evaluation names with their counts and latest run date + const namesQueryBuilder = appServer.AppDataSource.getRepository(Evaluation) + .createQueryBuilder('ev') + .select('DISTINCT(ev.name)', 'name') + .addSelect('COUNT(ev.name)', 'count') + .addSelect('MAX(ev.runDate)', 'latestRunDate') + .andWhere('ev.workspaceId = :workspaceId', { workspaceId: workspaceId }) + .groupBy('ev.name') + .orderBy('max(ev.runDate)', 'DESC') // Order by the latest run date + + if (page > 0 && limit > 0) { + namesQueryBuilder.skip((page - 1) * limit) + namesQueryBuilder.take(limit) + } + + const evaluationNames = await namesQueryBuilder.getRawMany() + // Get all evaluations for all names at once in a single query const returnResults: IEvaluationResult[] = [] - // mark the first evaluation with a unique name as the latestEval and then reset the version number - for (let i = 0; i < evaluations.length; i++) { - const evaluation = evaluations[i] as IEvaluationResult - returnResults.push(evaluation) - // find the first index with this name in the evaluations array - // as it is sorted desc, make the first evaluation with this name as the latestEval - const currentIndex = evaluations.indexOf(evaluation) - if (evaluations.findIndex((e) => e.name === evaluation.name) === currentIndex) { - returnResults[i].latestEval = true - } - } - for (let i = 0; i < returnResults.length; i++) { - const evaluation = returnResults[i] - if (evaluation.latestEval) { - const versions = returnResults.filter((e) => e.name === evaluation.name) - let descVersion = versions.length - for (let j = 0; j < versions.length; j++) { - versions[j].version = descVersion-- + if (evaluationNames.length > 0) { + const names = evaluationNames.map((item) => item.name) + // Fetch all evaluations for these names in a single query + const allEvaluations = await appServer.AppDataSource.getRepository(Evaluation) + .createQueryBuilder('ev') + .where('ev.name IN (:...names)', { names }) + .andWhere('ev.workspaceId = :workspaceId', { workspaceId }) + .orderBy('ev.name', 'ASC') + .addOrderBy('ev.runDate', 'DESC') + .getMany() + + // Process the results by name + const evaluationsByName = new Map() + // Group evaluations by name + for (const evaluation of allEvaluations) { + if (!evaluationsByName.has(evaluation.name)) { + evaluationsByName.set(evaluation.name, []) + } + evaluationsByName.get(evaluation.name)!.push(evaluation) + } + + // Process each name's evaluations + for (const item of evaluationNames) { + const evaluationsForName = evaluationsByName.get(item.name) || [] + for (let i = 0; i < evaluationsForName.length; i++) { + const evaluation = evaluationsForName[i] as IEvaluationResult + evaluation.latestEval = i === 0 + evaluation.version = parseInt(item.count) - i + returnResults.push(evaluation) } } } - return returnResults + if (page > 0 && limit > 0) { + return { + total: total, + data: returnResults + } + } else { + return returnResults + } } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/packages/server/src/services/evaluator/index.ts b/packages/server/src/services/evaluator/index.ts index ce52cf758..3cfbcc6f3 100644 --- a/packages/server/src/services/evaluator/index.ts +++ b/packages/server/src/services/evaluator/index.ts @@ -4,13 +4,25 @@ import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' import { Evaluator } from '../../database/entities/Evaluator' import { EvaluatorDTO } from '../../Interface.Evaluation' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' -const getAllEvaluators = async (workspaceId?: string) => { +const getAllEvaluators = async (workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() - const results: Evaluator[] = await appServer.AppDataSource.getRepository(Evaluator).findBy(getWorkspaceSearchOptions(workspaceId)) - return EvaluatorDTO.fromEntities(results) + const queryBuilder = appServer.AppDataSource.getRepository(Evaluator).createQueryBuilder('ev').orderBy('ev.updatedDate', 'DESC') + if (workspaceId) queryBuilder.andWhere('ev.workspaceId = :workspaceId', { workspaceId }) + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + const [data, total] = await queryBuilder.getManyAndCount() + if (page > 0 && limit > 0) { + return { + total, + data: EvaluatorDTO.fromEntities(data) + } + } else { + return EvaluatorDTO.fromEntities(data) + } } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/packages/server/src/services/executions/index.ts b/packages/server/src/services/executions/index.ts index 0973e710b..6acae3494 100644 --- a/packages/server/src/services/executions/index.ts +++ b/packages/server/src/services/executions/index.ts @@ -65,7 +65,7 @@ const getPublicExecutionById = async (executionId: string): Promise => { try { const appServer = getRunningExpressApp() - const { id, agentflowId, sessionId, state, startDate, endDate, page = 1, limit = 10, workspaceId } = filters + const { id, agentflowId, sessionId, state, startDate, endDate, page = 1, limit = 12, workspaceId } = filters // Handle UUID fields properly using raw parameters to avoid type conversion issues // This uses the query builder instead of direct objects for compatibility with UUID fields diff --git a/packages/server/src/services/export-import/index.ts b/packages/server/src/services/export-import/index.ts index 696981a0d..e2e39ead2 100644 --- a/packages/server/src/services/export-import/index.ts +++ b/packages/server/src/services/export-import/index.ts @@ -90,16 +90,20 @@ const convertExportInput = (body: any): ExportInput => { const FileDefaultName = 'ExportData.json' const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string): Promise<{ FileDefaultName: string } & ExportData> => { try { - let AgentFlow: ChatFlow[] = + let AgentFlow: ChatFlow[] | { data: ChatFlow[]; total: number } = exportInput.agentflow === true ? await chatflowService.getAllChatflows('MULTIAGENT', activeWorkspaceId) : [] + AgentFlow = 'data' in AgentFlow ? AgentFlow.data : AgentFlow - let AgentFlowV2: ChatFlow[] = + let AgentFlowV2: ChatFlow[] | { data: ChatFlow[]; total: number } = exportInput.agentflowv2 === true ? await chatflowService.getAllChatflows('AGENTFLOW', activeWorkspaceId) : [] + AgentFlowV2 = 'data' in AgentFlowV2 ? AgentFlowV2.data : AgentFlowV2 let AssistantCustom: Assistant[] = exportInput.assistantCustom === true ? await assistantService.getAllAssistants('CUSTOM', activeWorkspaceId) : [] - let AssistantFlow: ChatFlow[] = + + let AssistantFlow: ChatFlow[] | { data: ChatFlow[]; total: number } = exportInput.assistantCustom === true ? await chatflowService.getAllChatflows('ASSISTANT', activeWorkspaceId) : [] + AssistantFlow = 'data' in AssistantFlow ? AssistantFlow.data : AssistantFlow let AssistantOpenAI: Assistant[] = exportInput.assistantOpenAI === true ? await assistantService.getAllAssistants('OPENAI', activeWorkspaceId) : [] @@ -107,12 +111,15 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string): let AssistantAzure: Assistant[] = exportInput.assistantAzure === true ? await assistantService.getAllAssistants('AZURE', activeWorkspaceId) : [] - let ChatFlow: ChatFlow[] = exportInput.chatflow === true ? await chatflowService.getAllChatflows('CHATFLOW', activeWorkspaceId) : [] + let ChatFlow: ChatFlow[] | { data: ChatFlow[]; total: number } = + exportInput.chatflow === true ? await chatflowService.getAllChatflows('CHATFLOW', activeWorkspaceId) : [] + ChatFlow = 'data' in ChatFlow ? ChatFlow.data : ChatFlow - const allChatflow: ChatFlow[] = + let allChatflow: ChatFlow[] | { data: ChatFlow[]; total: number } = exportInput.chat_message === true || exportInput.chat_feedback === true ? await chatflowService.getAllChatflows(undefined, activeWorkspaceId) : [] + allChatflow = 'data' in allChatflow ? allChatflow.data : allChatflow const chatflowIds = allChatflow.map((chatflow) => chatflow.id) let ChatMessage: ChatMessage[] = @@ -124,8 +131,10 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string): let CustomTemplate: CustomTemplate[] = exportInput.custom_template === true ? await marketplacesService.getAllCustomTemplates(activeWorkspaceId) : [] - let DocumentStore: DocumentStore[] = + let DocumentStore: DocumentStore[] | { data: DocumentStore[]; total: number } = exportInput.document_store === true ? await documenStoreService.getAllDocumentStores(activeWorkspaceId) : [] + DocumentStore = 'data' in DocumentStore ? DocumentStore.data : DocumentStore + const documentStoreIds = DocumentStore.map((documentStore) => documentStore.id) let DocumentStoreFileChunk: DocumentStoreFileChunk[] = @@ -137,9 +146,13 @@ const exportData = async (exportInput: ExportInput, activeWorkspaceId?: string): const { data: totalExecutions } = exportInput.execution === true ? await executionService.getAllExecutions(filters) : { data: [] } let Execution: Execution[] = exportInput.execution === true ? totalExecutions : [] - let Tool: Tool[] = exportInput.tool === true ? await toolsService.getAllTools(activeWorkspaceId) : [] + let Tool: Tool[] | { data: Tool[]; total: number } = + exportInput.tool === true ? await toolsService.getAllTools(activeWorkspaceId) : [] + Tool = 'data' in Tool ? Tool.data : Tool - let Variable: Variable[] = exportInput.variable === true ? await variableService.getAllVariables(activeWorkspaceId) : [] + let Variable: Variable[] | { data: Variable[]; total: number } = + exportInput.variable === true ? await variableService.getAllVariables(activeWorkspaceId) : [] + Variable = 'data' in Variable ? Variable.data : Variable return { FileDefaultName, diff --git a/packages/server/src/services/stats/index.ts b/packages/server/src/services/stats/index.ts index 8b28c4f95..6ce6d4a10 100644 --- a/packages/server/src/services/stats/index.ts +++ b/packages/server/src/services/stats/index.ts @@ -14,7 +14,8 @@ const getChatflowStats = async ( endDate?: string, messageId?: string, feedback?: boolean, - feedbackTypes?: ChatMessageRatingType[] + feedbackTypes?: ChatMessageRatingType[], + activeWorkspaceId?: string ): Promise => { try { const chatmessages = (await utilGetChatMessage({ @@ -24,15 +25,20 @@ const getChatflowStats = async ( endDate, messageId, feedback, - feedbackTypes + feedbackTypes, + activeWorkspaceId })) as Array const totalMessages = chatmessages.length const totalFeedback = chatmessages.filter((message) => message?.feedback).length const positiveFeedback = chatmessages.filter((message) => message?.feedback?.rating === 'THUMBS_UP').length + // count the number of unique sessions in the chatmessages - count unique sessionId + const uniqueSessions = new Set(chatmessages.map((message) => message.sessionId)) + const totalSessions = uniqueSessions.size const dbResponse = { totalMessages, totalFeedback, - positiveFeedback + positiveFeedback, + totalSessions } return dbResponse diff --git a/packages/server/src/services/tools/index.ts b/packages/server/src/services/tools/index.ts index c30a50501..e31a0f461 100644 --- a/packages/server/src/services/tools/index.ts +++ b/packages/server/src/services/tools/index.ts @@ -3,7 +3,6 @@ import { Tool } from '../../database/entities/Tool' import { getAppVersion } from '../../utils' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { getRunningExpressApp } from '../../utils/getRunningExpressApp' import { FLOWISE_METRIC_COUNTERS, FLOWISE_COUNTER_STATUS } from '../../Interface.Metrics' import { QueryRunner } from 'typeorm' @@ -44,11 +43,23 @@ const deleteTool = async (toolId: string): Promise => { } } -const getAllTools = async (workspaceId?: string): Promise => { +const getAllTools = async (workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() - const dbResponse = await appServer.AppDataSource.getRepository(Tool).findBy(getWorkspaceSearchOptions(workspaceId)) - return dbResponse + const queryBuilder = appServer.AppDataSource.getRepository(Tool).createQueryBuilder('tool').orderBy('tool.updatedDate', 'DESC') + + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + if (workspaceId) queryBuilder.andWhere('tool.workspaceId = :workspaceId', { workspaceId }) + const [data, total] = await queryBuilder.getManyAndCount() + + if (page > 0 && limit > 0) { + return { data, total } + } else { + return data + } } catch (error) { throw new InternalFlowiseError(StatusCodes.INTERNAL_SERVER_ERROR, `Error: toolsService.getAllTools - ${getErrorMessage(error)}`) } diff --git a/packages/server/src/services/variables/index.ts b/packages/server/src/services/variables/index.ts index dcccddb6b..0a0099842 100644 --- a/packages/server/src/services/variables/index.ts +++ b/packages/server/src/services/variables/index.ts @@ -4,7 +4,6 @@ import { Variable } from '../../database/entities/Variable' import { InternalFlowiseError } from '../../errors/internalFlowiseError' import { getErrorMessage } from '../../errors/utils' import { getAppVersion } from '../../utils' -import { getWorkspaceSearchOptions } from '../../enterprise/utils/ControllerServiceUtils' import { QueryRunner } from 'typeorm' import { validate } from 'uuid' @@ -44,11 +43,26 @@ const deleteVariable = async (variableId: string): Promise => { } } -const getAllVariables = async (workspaceId?: string) => { +const getAllVariables = async (workspaceId?: string, page: number = -1, limit: number = -1) => { try { const appServer = getRunningExpressApp() - const dbResponse = await appServer.AppDataSource.getRepository(Variable).findBy(getWorkspaceSearchOptions(workspaceId)) - return dbResponse + const queryBuilder = appServer.AppDataSource.getRepository(Variable) + .createQueryBuilder('variable') + .orderBy('variable.updatedDate', 'DESC') + + if (page > 0 && limit > 0) { + queryBuilder.skip((page - 1) * limit) + queryBuilder.take(limit) + } + if (workspaceId) queryBuilder.andWhere('variable.workspaceId = :workspaceId', { workspaceId }) + + const [data, total] = await queryBuilder.getManyAndCount() + + if (page > 0 && limit > 0) { + return { data, total } + } else { + return data + } } catch (error) { throw new InternalFlowiseError( StatusCodes.INTERNAL_SERVER_ERROR, diff --git a/packages/server/src/utils/buildChatflow.ts b/packages/server/src/utils/buildChatflow.ts index 0805308d1..195c54b97 100644 --- a/packages/server/src/utils/buildChatflow.ts +++ b/packages/server/src/utils/buildChatflow.ts @@ -57,7 +57,7 @@ import { constructGraphs, getAPIOverrideConfig } from '../utils' -import { validateChatflowAPIKey } from './validateKey' +import { validateFlowAPIKey } from './validateKey' import logger from './logger' import { utilAddChatMessage } from './addChatMesage' import { checkPredictions, checkStorage, updatePredictionsUsage, updateStorageUsage } from './quotaUsage' @@ -923,7 +923,7 @@ export const utilBuildChatflow = async (req: Request, isInternal: boolean = fals try { // Validate API Key if its external API request if (!isInternal) { - const isKeyValidated = await validateChatflowAPIKey(req, chatflow) + const isKeyValidated = await validateFlowAPIKey(req, chatflow) if (!isKeyValidated) { throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) } diff --git a/packages/server/src/utils/getChatMessage.ts b/packages/server/src/utils/getChatMessage.ts index 171bba14c..1e40e0681 100644 --- a/packages/server/src/utils/getChatMessage.ts +++ b/packages/server/src/utils/getChatMessage.ts @@ -4,7 +4,6 @@ import { ChatMessage } from '../database/entities/ChatMessage' import { ChatMessageFeedback } from '../database/entities/ChatMessageFeedback' import { ChatFlow } from '../database/entities/ChatFlow' import { getRunningExpressApp } from '../utils/getRunningExpressApp' -import { aMonthAgo } from '.' /** * Method that get chat messages. @@ -19,6 +18,7 @@ import { aMonthAgo } from '.' * @param {boolean} feedback * @param {ChatMessageRatingType[]} feedbackTypes */ + interface GetChatMessageParams { chatflowid: string chatTypes?: ChatType[] @@ -32,6 +32,8 @@ interface GetChatMessageParams { feedback?: boolean feedbackTypes?: ChatMessageRatingType[] activeWorkspaceId?: string + page?: number + pageSize?: number } export const utilGetChatMessage = async ({ @@ -46,72 +48,44 @@ export const utilGetChatMessage = async ({ messageId, feedback, feedbackTypes, - activeWorkspaceId + activeWorkspaceId, + page = -1, + pageSize = -1 }: GetChatMessageParams): Promise => { + if (!page) page = -1 + if (!pageSize) pageSize = -1 + const appServer = getRunningExpressApp() // Check if chatflow workspaceId is same as activeWorkspaceId if (activeWorkspaceId) { const chatflow = await appServer.AppDataSource.getRepository(ChatFlow).findOneBy({ - id: chatflowid + id: chatflowid, + workspaceId: activeWorkspaceId }) - if (chatflow?.workspaceId !== activeWorkspaceId) { + if (!chatflow) { throw new Error('Unauthorized access') } + } else { + throw new Error('Unauthorized access') } if (feedback) { - const query = await appServer.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message') - - // do the join with chat message feedback based on messageId for each chat message in the chatflow - query - .leftJoinAndSelect('chat_message.execution', 'execution') - .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') - .where('chat_message.chatflowid = :chatflowid', { chatflowid }) - - // based on which parameters are available add `andWhere` clauses to the query - if (chatTypes && chatTypes.length > 0) { - query.andWhere('chat_message.chatType IN (:...chatTypes)', { chatTypes }) - } - if (chatId) { - query.andWhere('chat_message.chatId = :chatId', { chatId }) - } - if (memoryType) { - query.andWhere('chat_message.memoryType = :memoryType', { memoryType }) - } - if (sessionId) { - query.andWhere('chat_message.sessionId = :sessionId', { sessionId }) - } - - // set date range - if (startDate) { - query.andWhere('chat_message.createdDate >= :startDateTime', { startDateTime: startDate ? new Date(startDate) : aMonthAgo() }) - } - if (endDate) { - query.andWhere('chat_message.createdDate <= :endDateTime', { endDateTime: endDate ? new Date(endDate) : new Date() }) - } - - // sort - query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC') - - const messages = (await query.getMany()) as Array - - if (feedbackTypes && feedbackTypes.length > 0) { - // just applying a filter to the messages array will only return the messages that have feedback, - // but we also want the message before the feedback message which is the user message. - const indicesToKeep = new Set() - - messages.forEach((message, index) => { - if (message.role === 'apiMessage' && message.feedback && feedbackTypes.includes(message.feedback.rating)) { - if (index > 0) indicesToKeep.add(index - 1) - indicesToKeep.add(index) - } - }) - - return messages.filter((_, index) => indicesToKeep.has(index)) - } - - return messages + // Handle feedback queries with improved efficiency + return await handleFeedbackQuery({ + chatflowid, + chatTypes, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate, + messageId, + feedbackTypes, + page, + pageSize + }) } let createdDateQuery @@ -146,3 +120,226 @@ export const utilGetChatMessage = async ({ return messages } + +async function handleFeedbackQuery(params: { + chatflowid: string + chatTypes?: ChatType[] + sortOrder: string + chatId?: string + memoryType?: string + sessionId?: string + startDate?: string + endDate?: string + messageId?: string + feedbackTypes?: ChatMessageRatingType[] + page: number + pageSize: number +}): Promise { + const { + chatflowid, + chatTypes, + sortOrder, + chatId, + memoryType, + sessionId, + startDate, + endDate, + messageId, + feedbackTypes, + page, + pageSize + } = params + + const appServer = getRunningExpressApp() + + // For specific session/message queries, no pagination needed + if (sessionId || messageId) { + return await getMessagesWithFeedback(params, false) + } + + // For paginated queries, handle session-based pagination efficiently + if (page > -1 && pageSize > -1) { + // First get session IDs with pagination + const sessionQuery = appServer.AppDataSource.getRepository(ChatMessage) + .createQueryBuilder('chat_message') + .select('DISTINCT chat_message.sessionId', 'sessionId') + .where('chat_message.chatflowid = :chatflowid', { chatflowid }) + + // Apply basic filters + if (chatTypes && chatTypes.length > 0) { + sessionQuery.andWhere('chat_message.chatType IN (:...chatTypes)', { chatTypes }) + } + if (chatId) { + sessionQuery.andWhere('chat_message.chatId = :chatId', { chatId }) + } + if (memoryType) { + sessionQuery.andWhere('chat_message.memoryType = :memoryType', { memoryType }) + } + if (startDate && typeof startDate === 'string') { + sessionQuery.andWhere('chat_message.createdDate >= :startDateTime', { + startDateTime: new Date(startDate) + }) + } + if (endDate && typeof endDate === 'string') { + sessionQuery.andWhere('chat_message.createdDate <= :endDateTime', { + endDateTime: new Date(endDate) + }) + } + + // If feedback types are specified, only get sessions with those feedback types + if (feedbackTypes && feedbackTypes.length > 0) { + sessionQuery + .leftJoin(ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') + .andWhere('feedback.rating IN (:...feedbackTypes)', { feedbackTypes }) + } + + const startIndex = pageSize * (page - 1) + const sessionIds = await sessionQuery + .orderBy('MAX(chat_message.createdDate)', sortOrder === 'DESC' ? 'DESC' : 'ASC') + .groupBy('chat_message.sessionId') + .offset(startIndex) + .limit(pageSize) + .getRawMany() + + if (sessionIds.length === 0) { + return [] + } + + // Get all messages for these sessions + const sessionIdList = sessionIds.map((s) => s.sessionId) + return await getMessagesWithFeedback( + { + ...params, + sessionId: undefined // Clear specific sessionId since we're using list + }, + true, + sessionIdList + ) + } + + // No pagination - get all feedback messages + return await getMessagesWithFeedback(params, false) +} + +async function getMessagesWithFeedback( + params: { + chatflowid: string + chatTypes?: ChatType[] + sortOrder: string + chatId?: string + memoryType?: string + sessionId?: string + startDate?: string + endDate?: string + messageId?: string + feedbackTypes?: ChatMessageRatingType[] + }, + useSessionList: boolean = false, + sessionIdList?: string[] +): Promise { + const { chatflowid, chatTypes, sortOrder, chatId, memoryType, sessionId, startDate, endDate, messageId, feedbackTypes } = params + + const appServer = getRunningExpressApp() + const query = appServer.AppDataSource.getRepository(ChatMessage).createQueryBuilder('chat_message') + + query + .leftJoinAndSelect('chat_message.execution', 'execution') + .leftJoinAndMapOne('chat_message.feedback', ChatMessageFeedback, 'feedback', 'feedback.messageId = chat_message.id') + .where('chat_message.chatflowid = :chatflowid', { chatflowid }) + + // Apply filters + if (useSessionList && sessionIdList && sessionIdList.length > 0) { + query.andWhere('chat_message.sessionId IN (:...sessionIds)', { sessionIds: sessionIdList }) + } + + if (chatTypes && chatTypes.length > 0) { + query.andWhere('chat_message.chatType IN (:...chatTypes)', { chatTypes }) + } + if (chatId) { + query.andWhere('chat_message.chatId = :chatId', { chatId }) + } + if (memoryType) { + query.andWhere('chat_message.memoryType = :memoryType', { memoryType }) + } + if (sessionId) { + query.andWhere('chat_message.sessionId = :sessionId', { sessionId }) + } + if (messageId) { + query.andWhere('chat_message.id = :messageId', { messageId }) + } + if (startDate && typeof startDate === 'string') { + query.andWhere('chat_message.createdDate >= :startDateTime', { + startDateTime: new Date(startDate) + }) + } + if (endDate && typeof endDate === 'string') { + query.andWhere('chat_message.createdDate <= :endDateTime', { + endDateTime: new Date(endDate) + }) + } + + // Pre-filter by feedback types if specified (more efficient than post-processing) + if (feedbackTypes && feedbackTypes.length > 0) { + query.andWhere('(feedback.rating IN (:...feedbackTypes) OR feedback.rating IS NULL)', { feedbackTypes }) + } + + query.orderBy('chat_message.createdDate', sortOrder === 'DESC' ? 'DESC' : 'ASC') + + const messages = (await query.getMany()) as Array + + // Apply feedback type filtering with previous message inclusion + if (feedbackTypes && feedbackTypes.length > 0) { + return filterMessagesWithFeedback(messages, feedbackTypes) + } + + return messages +} + +function filterMessagesWithFeedback( + messages: Array, + feedbackTypes: ChatMessageRatingType[] +): ChatMessage[] { + // Group messages by session for proper filtering + const sessionGroups = new Map>() + + messages.forEach((message) => { + const sessionId = message.sessionId + if (!sessionId) return // Skip messages without sessionId + + if (!sessionGroups.has(sessionId)) { + sessionGroups.set(sessionId, []) + } + sessionGroups.get(sessionId)!.push(message) + }) + + const result: ChatMessage[] = [] + + // Process each session group + sessionGroups.forEach((sessionMessages) => { + // Sort by creation date to ensure proper order + sessionMessages.sort((a, b) => new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime()) + + const toInclude = new Set() + + sessionMessages.forEach((message, index) => { + if (message.role === 'apiMessage' && message.feedback && feedbackTypes.includes(message.feedback.rating)) { + // Include the feedback message + toInclude.add(index) + // Include the previous message (user message) if it exists + if (index > 0) { + toInclude.add(index - 1) + } + } + }) + + // Add filtered messages to result + sessionMessages.forEach((message, index) => { + if (toInclude.has(index)) { + result.push(message) + } + }) + }) + + // Sort final result by creation date + return result.sort((a, b) => new Date(a.createdDate).getTime() - new Date(b.createdDate).getTime()) +} diff --git a/packages/server/src/utils/pagination.ts b/packages/server/src/utils/pagination.ts new file mode 100644 index 000000000..ae81933cd --- /dev/null +++ b/packages/server/src/utils/pagination.ts @@ -0,0 +1,29 @@ +import { InternalFlowiseError } from '../errors/internalFlowiseError' +import { StatusCodes } from 'http-status-codes' +import { Request } from 'express' + +type Pagination = { + page: number + limit: number +} + +export const getPageAndLimitParams = (req: Request): Pagination => { + // by default assume no pagination + let page = -1 + let limit = -1 + if (req.query.page) { + // if page is provided, make sure it's a positive number + page = parseInt(req.query.page as string) + if (page < 0) { + throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: page cannot be negative!`) + } + } + if (req.query.limit) { + // if limit is provided, make sure it's a positive number + limit = parseInt(req.query.limit as string) + if (limit < 0) { + throw new InternalFlowiseError(StatusCodes.PRECONDITION_FAILED, `Error: limit cannot be negative!`) + } + } + return { page, limit } +} diff --git a/packages/server/src/utils/upsertVector.ts b/packages/server/src/utils/upsertVector.ts index 4df5546e0..63dbfbdc0 100644 --- a/packages/server/src/utils/upsertVector.ts +++ b/packages/server/src/utils/upsertVector.ts @@ -21,7 +21,7 @@ import { getStartingNodes, getAPIOverrideConfig } from '../utils' -import { validateChatflowAPIKey } from './validateKey' +import { validateFlowAPIKey } from './validateKey' import { IncomingInput, INodeDirectedGraph, IReactFlowObject, ChatType, IExecuteFlowParams, MODE } from '../Interface' import { ChatFlow } from '../database/entities/ChatFlow' import { getRunningExpressApp } from '../utils/getRunningExpressApp' @@ -251,7 +251,7 @@ export const upsertVector = async (req: Request, isInternal: boolean = false) => const files = (req.files as Express.Multer.File[]) || [] if (!isInternal) { - const isKeyValidated = await validateChatflowAPIKey(req, chatflow) + const isKeyValidated = await validateFlowAPIKey(req, chatflow) if (!isKeyValidated) { throw new InternalFlowiseError(StatusCodes.UNAUTHORIZED, `Unauthorized`) } diff --git a/packages/server/src/utils/validateKey.ts b/packages/server/src/utils/validateKey.ts index 2eb539de9..840735895 100644 --- a/packages/server/src/utils/validateKey.ts +++ b/packages/server/src/utils/validateKey.ts @@ -1,14 +1,15 @@ import { Request } from 'express' import { ChatFlow } from '../database/entities/ChatFlow' +import { ApiKey } from '../database/entities/ApiKey' import { compareKeys } from './apiKey' import apikeyService from '../services/apikey' /** - * Validate Chatflow API Key + * Validate flow API Key, this is needed because Prediction/Upsert API is public * @param {Request} req * @param {ChatFlow} chatflow */ -export const validateChatflowAPIKey = async (req: Request, chatflow: ChatFlow) => { +export const validateFlowAPIKey = async (req: Request, chatflow: ChatFlow): Promise => { const chatFlowApiKeyId = chatflow?.apikeyid if (!chatFlowApiKeyId) return true @@ -16,48 +17,52 @@ export const validateChatflowAPIKey = async (req: Request, chatflow: ChatFlow) = if (chatFlowApiKeyId && !authorizationHeader) return false const suppliedKey = authorizationHeader.split(`Bearer `).pop() - if (suppliedKey) { - const keys = await apikeyService.getAllApiKeys() - const apiSecret = keys.find((key: any) => key.id === chatFlowApiKeyId)?.apiSecret - if (!apiSecret) return false - if (!compareKeys(apiSecret, suppliedKey)) return false + if (!suppliedKey) return false + + try { + const apiKey = await apikeyService.getApiKeyById(chatFlowApiKeyId) + if (!apiKey) return false + + const apiKeyWorkSpaceId = apiKey.workspaceId + if (!apiKeyWorkSpaceId) return false + + if (apiKeyWorkSpaceId !== chatflow.workspaceId) return false + + const apiSecret = apiKey.apiSecret + if (!apiSecret || !compareKeys(apiSecret, suppliedKey)) return false + return true + } catch (error) { + return false } - return false } /** - * Validate API Key + * Validate and Get API Key Information * @param {Request} req + * @returns {Promise<{isValid: boolean, apiKey?: ApiKey, workspaceId?: string}>} */ -export const validateAPIKey = async (req: Request) => { +export const validateAPIKey = async (req: Request): Promise<{ isValid: boolean; apiKey?: ApiKey; workspaceId?: string }> => { const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' - if (!authorizationHeader) return false + if (!authorizationHeader) return { isValid: false } const suppliedKey = authorizationHeader.split(`Bearer `).pop() + if (!suppliedKey) return { isValid: false } - if (suppliedKey) { - const keys = await apikeyService.getAllApiKeys() - const apiSecret = keys.find((key: any) => key.apiKey === suppliedKey)?.apiSecret - if (!apiSecret) return false - if (!compareKeys(apiSecret, suppliedKey)) return false - return true + try { + const apiKey = await apikeyService.getApiKey(suppliedKey) + if (!apiKey) return { isValid: false } + + const apiKeyWorkSpaceId = apiKey.workspaceId + if (!apiKeyWorkSpaceId) return { isValid: false } + + const apiSecret = apiKey.apiSecret + if (!apiSecret || !compareKeys(apiSecret, suppliedKey)) { + return { isValid: false, apiKey, workspaceId: apiKey.workspaceId } + } + + return { isValid: true, apiKey, workspaceId: apiKey.workspaceId } + } catch (error) { + return { isValid: false } } - return false -} - -/** - * Get API Key WorkspaceID - * @param {Request} req - */ -export const getAPIKeyWorkspaceID = async (req: Request) => { - const authorizationHeader = (req.headers['Authorization'] as string) ?? (req.headers['authorization'] as string) ?? '' - if (!authorizationHeader) return false - - const suppliedKey = authorizationHeader.split(`Bearer `).pop() - if (suppliedKey) { - const key = await apikeyService.getApiKey(suppliedKey) - return key?.workspaceId - } - return undefined } diff --git a/packages/ui/src/api/apikey.js b/packages/ui/src/api/apikey.js index ca554d574..a8483e43d 100644 --- a/packages/ui/src/api/apikey.js +++ b/packages/ui/src/api/apikey.js @@ -1,6 +1,6 @@ import client from './client' -const getAllAPIKeys = () => client.get('/apikey') +const getAllAPIKeys = (params) => client.get('/apikey', { params }) const createNewAPI = (body) => client.post(`/apikey`, body) diff --git a/packages/ui/src/api/chatflows.js b/packages/ui/src/api/chatflows.js index 3176947d1..ae8a8d1f7 100644 --- a/packages/ui/src/api/chatflows.js +++ b/packages/ui/src/api/chatflows.js @@ -1,8 +1,8 @@ import client from './client' -const getAllChatflows = () => client.get('/chatflows?type=CHATFLOW') +const getAllChatflows = (params) => client.get('/chatflows?type=CHATFLOW', { params }) -const getAllAgentflows = (type) => client.get(`/chatflows?type=${type}`) +const getAllAgentflows = (type, params) => client.get(`/chatflows?type=${type}`, { params }) const getSpecificChatflow = (id) => client.get(`/chatflows/${id}`) diff --git a/packages/ui/src/api/dataset.js b/packages/ui/src/api/dataset.js index 211064583..125dc035c 100644 --- a/packages/ui/src/api/dataset.js +++ b/packages/ui/src/api/dataset.js @@ -1,9 +1,9 @@ import client from './client' -const getAllDatasets = () => client.get('/datasets') +const getAllDatasets = (params) => client.get('/datasets', { params }) //dataset -const getDataset = (id) => client.get(`/datasets/set/${id}`) +const getDataset = (id, params) => client.get(`/datasets/set/${id}`, { params }) const createDataset = (body) => client.post(`/datasets/set`, body) const updateDataset = (id, body) => client.put(`/datasets/set/${id}`, body) const deleteDataset = (id) => client.delete(`/datasets/set/${id}`) diff --git a/packages/ui/src/api/documentstore.js b/packages/ui/src/api/documentstore.js index cb6211b97..ac0c4bbff 100644 --- a/packages/ui/src/api/documentstore.js +++ b/packages/ui/src/api/documentstore.js @@ -1,6 +1,6 @@ import client from './client' -const getAllDocumentStores = () => client.get('/document-store/store') +const getAllDocumentStores = (params) => client.get('/document-store/store', { params }) const getDocumentLoaders = () => client.get('/document-store/components/loaders') const getSpecificDocumentStore = (id) => client.get(`/document-store/store/${id}`) const createDocumentStore = (body) => client.post(`/document-store/store`, body) diff --git a/packages/ui/src/api/evaluations.js b/packages/ui/src/api/evaluations.js index e85942a03..e338bc3e9 100644 --- a/packages/ui/src/api/evaluations.js +++ b/packages/ui/src/api/evaluations.js @@ -1,7 +1,7 @@ import client from './client' //evaluation -const getAllEvaluations = () => client.get('/evaluations') +const getAllEvaluations = (params) => client.get('/evaluations', { params }) const getIsOutdated = (id) => client.get(`/evaluations/is-outdated/${id}`) const getEvaluation = (id) => client.get(`/evaluations/${id}`) const createEvaluation = (body) => client.post(`/evaluations`, body) diff --git a/packages/ui/src/api/evaluators.js b/packages/ui/src/api/evaluators.js index 9effe862e..f496b3921 100644 --- a/packages/ui/src/api/evaluators.js +++ b/packages/ui/src/api/evaluators.js @@ -1,6 +1,6 @@ import client from './client' -const getAllEvaluators = () => client.get('/evaluators') +const getAllEvaluators = (params) => client.get('/evaluators', { params }) //evaluators const createEvaluator = (body) => client.post(`/evaluators`, body) diff --git a/packages/ui/src/api/tools.js b/packages/ui/src/api/tools.js index 77992a2ab..5e9b7b559 100644 --- a/packages/ui/src/api/tools.js +++ b/packages/ui/src/api/tools.js @@ -1,6 +1,6 @@ import client from './client' -const getAllTools = () => client.get('/tools') +const getAllTools = (params) => client.get('/tools', { params }) const getSpecificTool = (id) => client.get(`/tools/${id}`) diff --git a/packages/ui/src/api/variables.js b/packages/ui/src/api/variables.js index 944b83198..4285f184c 100644 --- a/packages/ui/src/api/variables.js +++ b/packages/ui/src/api/variables.js @@ -1,6 +1,6 @@ import client from './client' -const getAllVariables = () => client.get('/variables') +const getAllVariables = (params) => client.get('/variables', { params }) const createVariable = (body) => client.post(`/variables`, body) diff --git a/packages/ui/src/ui-component/cards/StatsCard.jsx b/packages/ui/src/ui-component/cards/StatsCard.jsx index 42b5c4576..acd019374 100644 --- a/packages/ui/src/ui-component/cards/StatsCard.jsx +++ b/packages/ui/src/ui-component/cards/StatsCard.jsx @@ -8,8 +8,14 @@ import Typography from '@mui/material/Typography' const StatsCard = ({ title, stat }) => { const customization = useSelector((state) => state.customization) return ( - - + + {title} diff --git a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx index 7e336bfa4..77e398b21 100644 --- a/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx +++ b/packages/ui/src/ui-component/dialog/ViewMessagesDialog.jsx @@ -24,9 +24,14 @@ import { CardContent, FormControlLabel, Checkbox, - DialogActions + DialogActions, + Pagination, + Typography, + Menu, + MenuItem, + IconButton } from '@mui/material' -import { useTheme } from '@mui/material/styles' +import { useTheme, styled, alpha } from '@mui/material/styles' import DatePicker from 'react-datepicker' import robotPNG from '@/assets/images/robot.png' @@ -34,7 +39,8 @@ import userPNG from '@/assets/images/account.png' import msgEmptySVG from '@/assets/images/message_empty.svg' import multiagent_supervisorPNG from '@/assets/images/multiagent_supervisor.png' import multiagent_workerPNG from '@/assets/images/multiagent_worker.png' -import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip } from '@tabler/icons-react' +import { IconTool, IconDeviceSdCard, IconFileExport, IconEraser, IconX, IconDownload, IconPaperclip, IconBulb } from '@tabler/icons-react' +import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown' // Project import import { MemoizedReactMarkdown } from '@/ui-component/markdown/MemoizedReactMarkdown' @@ -63,6 +69,42 @@ import { enqueueSnackbar as enqueueSnackbarAction, closeSnackbar as closeSnackba import '@/views/chatmessage/ChatMessage.css' import 'react-datepicker/dist/react-datepicker.css' +const StyledMenu = styled((props) => ( + +))(({ theme }) => ({ + '& .MuiPaper-root': { + borderRadius: 6, + marginTop: theme.spacing(1), + minWidth: 180, + boxShadow: + 'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px', + '& .MuiMenu-list': { + padding: '4px 0' + }, + '& .MuiMenuItem-root': { + '& .MuiSvgIcon-root': { + fontSize: 18, + color: theme.palette.text.secondary, + marginRight: theme.spacing(1.5) + }, + '&:active': { + backgroundColor: alpha(theme.palette.primary.main, theme.palette.action.selectedOpacity) + } + } + } +})) + const DatePickerCustomInput = forwardRef(function DatePickerCustomInput({ value, onClick }, ref) { return ( @@ -104,10 +146,12 @@ const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm }) {dialogProps.description} - setHardDelete(event.target.checked)} />} - label='Remove messages from 3rd party Memory Node' - /> + {dialogProps.isChatflow && ( + setHardDelete(event.target.checked)} />} + label='Remove messages from 3rd party Memory Node' + /> + )} @@ -142,7 +186,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [chatlogs, setChatLogs] = useState([]) const [allChatlogs, setAllChatLogs] = useState([]) const [chatMessages, setChatMessages] = useState([]) - const [stats, setStats] = useState([]) + const [stats, setStats] = useState({}) const [selectedMessageIndex, setSelectedMessageIndex] = useState(0) const [selectedChatId, setSelectedChatId] = useState('') const [sourceDialogOpen, setSourceDialogOpen] = useState(false) @@ -154,6 +198,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const [startDate, setStartDate] = useState(new Date(new Date().setMonth(new Date().getMonth() - 1))) const [endDate, setEndDate] = useState(new Date()) const [leadEmail, setLeadEmail] = useState('') + const [anchorEl, setAnchorEl] = useState(null) + const open = Boolean(anchorEl) const getChatmessageApi = useApi(chatmessageApi.getAllChatmessageFromChatflow) const getChatmessageFromPKApi = useApi(chatmessageApi.getChatmessageFromPK) @@ -161,74 +207,70 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { const getStoragePathFromServer = useApi(chatmessageApi.getStoragePath) let storagePath = '' + /* Table Pagination */ + const [currentPage, setCurrentPage] = useState(1) + const [pageLimit, setPageLimit] = useState(10) + const [total, setTotal] = useState(0) + const onChange = (event, page) => { + setCurrentPage(page) + refresh(page, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter) + } + + const refresh = (page, limit, startDate, endDate, chatTypes, feedbackTypes) => { + getChatmessageApi.request(dialogProps.chatflow.id, { + chatType: chatTypes.length ? chatTypes : undefined, + feedbackType: feedbackTypes.length ? feedbackTypes : undefined, + startDate: startDate, + endDate: endDate, + order: 'DESC', + page: page, + limit: limit + }) + getStatsApi.request(dialogProps.chatflow.id, { + chatType: chatTypes.length ? chatTypes : undefined, + feedbackType: feedbackTypes.length ? feedbackTypes : undefined, + startDate: startDate, + endDate: endDate + }) + setCurrentPage(page) + } + const onStartDateSelected = (date) => { const updatedDate = new Date(date) updatedDate.setHours(0, 0, 0, 0) setStartDate(updatedDate) - getChatmessageApi.request(dialogProps.chatflow.id, { - startDate: updatedDate, - endDate: endDate, - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) - getStatsApi.request(dialogProps.chatflow.id, { - startDate: updatedDate, - endDate: endDate, - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) + refresh(1, pageLimit, updatedDate, endDate, chatTypeFilter, feedbackTypeFilter) } const onEndDateSelected = (date) => { const updatedDate = new Date(date) updatedDate.setHours(23, 59, 59, 999) setEndDate(updatedDate) - getChatmessageApi.request(dialogProps.chatflow.id, { - endDate: updatedDate, - startDate: startDate, - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) - getStatsApi.request(dialogProps.chatflow.id, { - endDate: updatedDate, - startDate: startDate, - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) + refresh(1, pageLimit, startDate, updatedDate, chatTypeFilter, feedbackTypeFilter) } const onChatTypeSelected = (chatTypes) => { - setChatTypeFilter(chatTypes) - getChatmessageApi.request(dialogProps.chatflow.id, { - chatType: chatTypes.length ? chatTypes : undefined, - startDate: startDate, - endDate: endDate, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) - getStatsApi.request(dialogProps.chatflow.id, { - chatType: chatTypes.length ? chatTypes : undefined, - startDate: startDate, - endDate: endDate, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) + // Parse the JSON string from MultiDropdown back to an array + let parsedChatTypes = [] + if (chatTypes && typeof chatTypes === 'string' && chatTypes.startsWith('[') && chatTypes.endsWith(']')) { + parsedChatTypes = JSON.parse(chatTypes) + } else if (Array.isArray(chatTypes)) { + parsedChatTypes = chatTypes + } + setChatTypeFilter(parsedChatTypes) + refresh(1, pageLimit, startDate, endDate, parsedChatTypes, feedbackTypeFilter) } const onFeedbackTypeSelected = (feedbackTypes) => { - setFeedbackTypeFilter(feedbackTypes) - - getChatmessageApi.request(dialogProps.chatflow.id, { - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - feedbackType: feedbackTypes.length ? feedbackTypes : undefined, - startDate: startDate, - endDate: endDate, - order: 'ASC' - }) - getStatsApi.request(dialogProps.chatflow.id, { - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - feedbackType: feedbackTypes.length ? feedbackTypes : undefined, - startDate: startDate, - endDate: endDate - }) + // Parse the JSON string from MultiDropdown back to an array + let parsedFeedbackTypes = [] + if (feedbackTypes && typeof feedbackTypes === 'string' && feedbackTypes.startsWith('[') && feedbackTypes.endsWith(']')) { + parsedFeedbackTypes = JSON.parse(feedbackTypes) + } else if (Array.isArray(feedbackTypes)) { + parsedFeedbackTypes = feedbackTypes + } + setFeedbackTypeFilter(parsedFeedbackTypes) + refresh(1, pageLimit, startDate, endDate, chatTypeFilter, parsedFeedbackTypes) } const onDeleteMessages = () => { @@ -236,7 +278,8 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { title: 'Delete Messages', description: 'Are you sure you want to delete messages? This action cannot be undone.', confirmButtonName: 'Delete', - cancelButtonName: 'Cancel' + cancelButtonName: 'Cancel', + isChatflow: dialogProps.isChatflow }) setHardDeleteDialogOpen(true) } @@ -280,18 +323,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { ) } }) - getChatmessageApi.request(chatflowid, { - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - startDate: startDate, - endDate: endDate, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) - getStatsApi.request(chatflowid, { - chatType: chatTypeFilter.length ? chatTypeFilter : undefined, - startDate: startDate, - endDate: endDate, - feedbackType: feedbackTypeFilter.length ? feedbackTypeFilter : undefined - }) + refresh(1, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter) } catch (error) { console.error(error) enqueueSnackbar({ @@ -555,20 +587,42 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { item: allChatMessages[i] } } else if (Object.prototype.hasOwnProperty.call(seen, PK) && seen[PK].counter === 1) { + // Properly identify user and API messages regardless of order + const firstMessage = seen[PK].item + const secondMessage = item + + let userContent = '' + let apiContent = '' + + // Check both messages and assign based on role, not order + if (firstMessage.role === 'userMessage') { + userContent = `User: ${firstMessage.content}` + } else if (firstMessage.role === 'apiMessage') { + apiContent = `Bot: ${firstMessage.content}` + } + + if (secondMessage.role === 'userMessage') { + userContent = `User: ${secondMessage.content}` + } else if (secondMessage.role === 'apiMessage') { + apiContent = `Bot: ${secondMessage.content}` + } + seen[PK] = { counter: 2, item: { ...seen[PK].item, - apiContent: - seen[PK].item.role === 'apiMessage' ? `Bot: ${seen[PK].item.content}` : `User: ${seen[PK].item.content}`, - userContent: item.role === 'apiMessage' ? `Bot: ${item.content}` : `User: ${item.content}` + apiContent, + userContent } } filteredChatLogs.push(seen[PK].item) } } - setChatLogs(filteredChatLogs) - if (filteredChatLogs.length) return getChatPK(filteredChatLogs[0]) + + // Sort by date to maintain chronological order + const sortedChatLogs = filteredChatLogs.sort((a, b) => new Date(b.createdDate) - new Date(a.createdDate)) + setChatLogs(sortedChatLogs) + if (sortedChatLogs.length) return getChatPK(sortedChatLogs[0]) return undefined } @@ -613,6 +667,14 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setSourceDialogOpen(true) } + const handleClick = (event) => { + setAnchorEl(event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + const renderFileUploads = (item, index) => { if (item?.mime?.startsWith('image/')) { return ( @@ -706,15 +768,13 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { useEffect(() => { if (getStatsApi.data) { setStats(getStatsApi.data) + setTotal(getStatsApi.data?.totalSessions ?? 0) } }, [getStatsApi.data]) useEffect(() => { if (dialogProps.chatflow) { - getChatmessageApi.request(dialogProps.chatflow.id, { - startDate: startDate, - endDate: endDate - }) + refresh(currentPage, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter) getStatsApi.request(dialogProps.chatflow.id, { startDate: startDate, endDate: endDate @@ -733,6 +793,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { setEndDate(new Date()) setStats([]) setLeadEmail('') + setTotal(0) + setCurrentPage(1) + setPageLimit(10) } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -748,16 +811,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { if (dialogProps.chatflow) { // when the filter is cleared fetch all messages if (feedbackTypeFilter.length === 0) { - getChatmessageApi.request(dialogProps.chatflow.id, { - startDate: startDate, - endDate: endDate, - chatType: chatTypeFilter.length ? chatTypeFilter : undefined - }) - getStatsApi.request(dialogProps.chatflow.id, { - startDate: startDate, - endDate: endDate, - chatType: chatTypeFilter.length ? chatTypeFilter : undefined - }) + refresh(currentPage, pageLimit, startDate, endDate, chatTypeFilter, feedbackTypeFilter) } } // eslint-disable-next-line react-hooks/exhaustive-deps @@ -819,19 +873,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { onClose={onCancel} open={show} fullWidth - maxWidth={'lg'} + maxWidth={'xl'} aria-labelledby='alert-dialog-title' aria-describedby='alert-dialog-description' > - -
- {dialogProps.title} -
- -
- <>
{ Feedback { />
- {stats.totalMessages > 0 && ( - - )} + + + { + handleClose() + exportMessages() + }} + disableRipple + > + + Export to JSON + + {(stats.totalMessages ?? 0) > 0 && ( + { + handleClose() + onDeleteMessages() + }} + disableRipple + > + + Delete All + + )} +
- - + + +
-
- {chatlogs && chatlogs.length == 0 && ( +
+ {chatlogs && chatlogs.length === 0 && ( { )} {chatlogs && chatlogs.length > 0 && ( -
+
{ maxHeight: 'calc(100vh - 260px)' }} > +
+ + Sessions {pageLimit * (currentPage - 1) + 1} - {Math.min(pageLimit * currentPage, total)} of{' '} + {total} + + +
{chatlogs.map((chatmsg, index) => ( {
)} {chatlogs && chatlogs.length > 0 && ( -
+
{chatMessages && chatMessages.length > 1 && ( -
+
{chatMessages[1].sessionId && (
@@ -1046,31 +1163,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
- clearChat(chatMessages[1])} - startIcon={} - > - Clear - + + clearChat(chatMessages[1])}> + + + {chatMessages[1].sessionId && ( -
- Why my session is not deleted? -
+ + +
)}
@@ -1081,12 +1193,15 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { display: 'flex', flexDirection: 'column', marginLeft: '20px', - border: '1px solid #e0e0e0', - borderRadius: `${customization.borderRadius}px` + marginBottom: '5px', + border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0', + boxShadow: customization.isDarkMode ? '0 0 5px 0 rgba(255, 255, 255, 0.5)' : 'none', + borderRadius: `10px`, + overflow: 'hidden' }} className='cloud-message' > -
+
{chatMessages && chatMessages.map((message, index) => { if (message.type === 'apiMessage' || message.type === 'userMessage') { @@ -1125,7 +1240,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { style={{ display: 'flex', flexDirection: 'column', - width: '100%' + width: '100%', + minWidth: 0, + overflow: 'hidden' }} > {message.fileUploads && message.fileUploads.length > 0 && ( @@ -1412,7 +1529,10 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { })}
)} -
+
{message.message} @@ -1486,7 +1606,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => { return ( { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + const borderColor = theme.palette.grey[900] + 25 + + const [itemsPerPage, setItemsPerPage] = useState(DEFAULT_ITEMS_PER_PAGE) + const [activePage, setActivePage] = useState(1) + const [totalItems, setTotalItems] = useState(0) + + useEffect(() => { + setTotalItems(total) + }, [total]) + + useEffect(() => { + setItemsPerPage(limit) + }, [limit]) + + useEffect(() => { + setActivePage(currentPage) + }, [currentPage]) + + const handlePageChange = (event, value) => { + setActivePage(value) + onChange(value, itemsPerPage) + } + + const handleLimitChange = (event) => { + const itemsPerPage = parseInt(event.target.value, 10) + setItemsPerPage(itemsPerPage) + setActivePage(1) + onChange(1, itemsPerPage) + } + + return ( + + + Items per page: + + + + + {totalItems > 0 && ( + + Items {activePage * itemsPerPage - itemsPerPage + 1} to{' '} + {activePage * itemsPerPage > totalItems ? totalItems : activePage * itemsPerPage} of {totalItems} + + )} + + + ) +} + +TablePagination.propTypes = { + onChange: PropTypes.func.isRequired, + currentPage: PropTypes.number, + limit: PropTypes.number, + total: PropTypes.number +} + +export default TablePagination diff --git a/packages/ui/src/ui-component/table/DocumentStoreTable.jsx b/packages/ui/src/ui-component/table/DocumentStoreTable.jsx new file mode 100644 index 000000000..0f5df0089 --- /dev/null +++ b/packages/ui/src/ui-component/table/DocumentStoreTable.jsx @@ -0,0 +1,255 @@ +import { useState } from 'react' +import PropTypes from 'prop-types' +import { useSelector } from 'react-redux' +import { styled } from '@mui/material/styles' +import { + Box, + Paper, + Skeleton, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TableSortLabel, + useTheme, + Typography +} from '@mui/material' +import { tableCellClasses } from '@mui/material/TableCell' +import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus' + +const StyledTableCell = styled(TableCell)(({ theme }) => ({ + borderColor: theme.palette.grey[900] + 25, + + [`&.${tableCellClasses.head}`]: { + color: theme.palette.grey[900] + }, + [`&.${tableCellClasses.body}`]: { + fontSize: 14, + height: 64 + } +})) + +const StyledTableRow = styled(TableRow)(() => ({ + // hide last border + '&:last-child td, &:last-child th': { + border: 0 + } +})) + +export const DocumentStoreTable = ({ data, isLoading, onRowClick, images }) => { + const theme = useTheme() + const customization = useSelector((state) => state.customization) + + const localStorageKeyOrder = 'doc_store_order' + const localStorageKeyOrderBy = 'doc_store_orderBy' + + const [order, setOrder] = useState(localStorage.getItem(localStorageKeyOrder) || 'desc') + const [orderBy, setOrderBy] = useState(localStorage.getItem(localStorageKeyOrderBy) || 'name') + + const handleRequestSort = (property) => { + const isAsc = orderBy === property && order === 'asc' + const newOrder = isAsc ? 'desc' : 'asc' + setOrder(newOrder) + setOrderBy(property) + localStorage.setItem(localStorageKeyOrder, newOrder) + localStorage.setItem(localStorageKeyOrderBy, property) + } + + const sortedData = data + ? [...data].sort((a, b) => { + if (orderBy === 'name') { + return order === 'asc' ? (a.name || '').localeCompare(b.name || '') : (b.name || '').localeCompare(a.name || '') + } + return 0 + }) + : [] + + return ( + <> + + + + +   + + handleRequestSort('name')}> + Name + + + Description + Connected flows + Total characters + Total chunks + Loader Types + + + + {isLoading ? ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + <> + {sortedData.map((row, index) => { + return ( + onRowClick(row)} + hover + key={index} + sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }} + > + + + + + + {row.name} + + + + + {row?.description} + + + {row.whereUsed?.length ?? 0} + {row.totalChars} + {row.totalChunks} + + {images && images[row.id] && ( + + {images[row.id] + .slice(0, images[row.id].length > 3 ? 3 : images[row.id].length) + .map((img) => ( + + + + ))} + {images?.length > 3 && ( + + + {images.length - 3} More + + )} + + )} + + + ) + })} + + )} + +
+
+ + ) +} + +DocumentStoreTable.propTypes = { + data: PropTypes.array, + isLoading: PropTypes.bool, + images: PropTypes.object, + onRowClick: PropTypes.func +} + +DocumentStoreTable.displayName = 'DocumentStoreTable' diff --git a/packages/ui/src/utils/exportImport.js b/packages/ui/src/utils/exportImport.js index 0eafc9f3a..a9361a7fa 100644 --- a/packages/ui/src/utils/exportImport.js +++ b/packages/ui/src/utils/exportImport.js @@ -89,7 +89,7 @@ const sanitizeDocumentStore = (DocumentStore) => { const sanitizeExecution = (Execution) => { try { return Execution.map((execution) => { - execution.agentflow.workspaceId = undefined + if (execution.agentflow) execution.agentflow.workspaceId = undefined return { ...execution, workspaceId: undefined } }) } catch (error) { diff --git a/packages/ui/src/views/agentexecutions/index.jsx b/packages/ui/src/views/agentexecutions/index.jsx index 8d12dd706..ccc18b364 100644 --- a/packages/ui/src/views/agentexecutions/index.jsx +++ b/packages/ui/src/views/agentexecutions/index.jsx @@ -4,7 +4,6 @@ import 'react-datepicker/dist/react-datepicker.css' // material-ui import { - Pagination, Box, Stack, TextField, @@ -21,7 +20,6 @@ import { DialogTitle, IconButton, Tooltip, - Typography, useTheme } from '@mui/material' @@ -44,6 +42,7 @@ import { IconTrash } from '@tabler/icons-react' import { ExecutionsListTable } from '@/ui-component/table/ExecutionsListTable' import { ExecutionDetails } from './ExecutionDetails' import { omit } from 'lodash' +import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination' // ==============================|| AGENT EXECUTIONS ||============================== // @@ -71,11 +70,6 @@ const AgentExecutions = () => { agentflowId: '', sessionId: '' }) - const [pagination, setPagination] = useState({ - page: 1, - limit: 10, - total: 0 - }) const handleFilterChange = (field, value) => { setFilters({ @@ -94,26 +88,25 @@ const AgentExecutions = () => { }) } - const handlePageChange = (event, newPage) => { - setPagination({ - ...pagination, - page: newPage - }) + /* Table Pagination */ + const [currentPage, setCurrentPage] = useState(1) + const [pageLimit, setPageLimit] = useState(DEFAULT_ITEMS_PER_PAGE) + const [total, setTotal] = useState(0) + const onChange = (page, pageLimit) => { + setCurrentPage(page) + setPageLimit(pageLimit) + applyFilters(page, pageLimit) } - const handleLimitChange = (event) => { - setPagination({ - ...pagination, - page: 1, // Reset to first page when changing items per page - limit: parseInt(event.target.value, 10) - }) - } - - const applyFilters = () => { + const applyFilters = (page, limit) => { setLoading(true) + // Ensure page and limit are numbers, not objects + const pageNum = typeof page === 'number' ? page : currentPage + const limitNum = typeof limit === 'number' ? limit : pageLimit + const params = { - page: pagination.page, - limit: pagination.limit + page: pageNum, + limit: limitNum } if (filters.state) params.state = filters.state @@ -152,7 +145,8 @@ const AgentExecutions = () => { agentflowId: '', sessionId: '' }) - getAllExecutions.request() + setCurrentPage(1) + getAllExecutions.request({ page: 1, limit: pageLimit }) } const handleExecutionSelectionChange = (selectedIds) => { @@ -175,7 +169,7 @@ const AgentExecutions = () => { } useEffect(() => { - getAllExecutions.request() + getAllExecutions.request({ page: 1, limit: DEFAULT_ITEMS_PER_PAGE }) // eslint-disable-next-line react-hooks/exhaustive-deps }, []) @@ -186,7 +180,7 @@ const AgentExecutions = () => { const { data, total } = getAllExecutions.data if (!Array.isArray(data)) return setExecutions(data) - setPagination((prev) => ({ ...prev, total })) + setTotal(total) } catch (e) { console.error(e) } @@ -201,17 +195,12 @@ const AgentExecutions = () => { setError(getAllExecutions.error) }, [getAllExecutions.error]) - useEffect(() => { - applyFilters() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pagination.page, pagination.limit]) - useEffect(() => { if (deleteExecutionsApi.data) { // Refresh the executions list getAllExecutions.request({ - page: pagination.page, - limit: pagination.limit + page: currentPage, + limit: pageLimit }) setSelectedExecutionIds([]) } @@ -339,7 +328,12 @@ const AgentExecutions = () => { -