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 <hzj94@hotmail.com>
This commit is contained in:
Vinod Kiran 2025-07-10 20:29:24 +05:30 committed by GitHub
parent 6baec93860
commit bf05f25f7e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 2595 additions and 1560 deletions

View File

@ -290,6 +290,9 @@ export class DocumentStoreDTO {
}
static fromEntities(entities: DocumentStore[]): DocumentStoreDTO[] {
if (entities.length === 0) {
return []
}
return entities.map((entity) => this.fromEntity(entity))
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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' })

View File

@ -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
}

View File

@ -39,7 +39,9 @@ const getAllChatMessages = async (
messageId?: string,
feedback?: boolean,
feedbackTypes?: ChatMessageRatingType[],
activeWorkspaceId?: string
activeWorkspaceId?: string,
page?: number,
pageSize?: number
): Promise<ChatMessage[]> => {
try {
const dbResponse = await utilGetChatMessage({
@ -54,7 +56,9 @@ const getAllChatMessages = async (
messageId,
feedback,
feedbackTypes,
activeWorkspaceId
activeWorkspaceId,
page,
pageSize
})
return dbResponse
} catch (error) {

View File

@ -127,21 +127,36 @@ const deleteChatflow = async (chatflowId: string, orgId: string, workspaceId: st
}
}
const getAllChatflows = async (type?: ChatflowType, workspaceId?: string): Promise<ChatFlow[]> => {
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,

View File

@ -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)}`)

View File

@ -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,

View File

@ -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<string, Evaluation[]>()
// 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,

View File

@ -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,

View File

@ -65,7 +65,7 @@ const getPublicExecutionById = async (executionId: string): Promise<Execution |
const getAllExecutions = async (filters: ExecutionFilters = {}): Promise<{ data: Execution[]; total: number }> => {
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

View File

@ -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,

View File

@ -14,7 +14,8 @@ const getChatflowStats = async (
endDate?: string,
messageId?: string,
feedback?: boolean,
feedbackTypes?: ChatMessageRatingType[]
feedbackTypes?: ChatMessageRatingType[],
activeWorkspaceId?: string
): Promise<any> => {
try {
const chatmessages = (await utilGetChatMessage({
@ -24,15 +25,20 @@ const getChatflowStats = async (
endDate,
messageId,
feedback,
feedbackTypes
feedbackTypes,
activeWorkspaceId
})) as Array<ChatMessage & { feedback?: ChatMessageFeedback }>
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

View File

@ -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<any> => {
}
}
const getAllTools = async (workspaceId?: string): Promise<Tool[]> => {
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)}`)
}

View File

@ -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<any> => {
}
}
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,

View File

@ -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`)
}

View File

@ -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<ChatMessage[]> => {
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<ChatMessage & { feedback: ChatMessageFeedback }>
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<ChatMessage[]> {
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<ChatMessage[]> {
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<ChatMessage & { feedback: ChatMessageFeedback }>
// Apply feedback type filtering with previous message inclusion
if (feedbackTypes && feedbackTypes.length > 0) {
return filterMessagesWithFeedback(messages, feedbackTypes)
}
return messages
}
function filterMessagesWithFeedback(
messages: Array<ChatMessage & { feedback: ChatMessageFeedback }>,
feedbackTypes: ChatMessageRatingType[]
): ChatMessage[] {
// Group messages by session for proper filtering
const sessionGroups = new Map<string, Array<ChatMessage & { feedback: ChatMessageFeedback }>>()
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<number>()
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())
}

View File

@ -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 }
}

View File

@ -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`)
}

View File

@ -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<boolean> => {
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
}

View File

@ -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)

View File

@ -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}`)

View File

@ -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}`)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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}`)

View File

@ -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)

View File

@ -8,8 +8,14 @@ import Typography from '@mui/material/Typography'
const StatsCard = ({ title, stat }) => {
const customization = useSelector((state) => state.customization)
return (
<Card sx={{ border: '1px solid #e0e0e0', borderRadius: `${customization.borderRadius}px` }}>
<CardContent>
<Card
sx={{
border: customization.isDarkMode ? 'none' : '1px solid #e0e0e0',
boxShadow: customization.isDarkMode ? '0px 3px 8px rgba(255, 255, 255, 0.5)' : 'none',
borderRadius: `${customization.borderRadius}px`
}}
>
<CardContent sx={{ padding: '12px', '&:last-child': { paddingBottom: '12px', paddingLeft: '18px', paddingRight: '8px' } }}>
<Typography sx={{ fontSize: '0.875rem' }} color='text.primary' gutterBottom>
{title}
</Typography>

View File

@ -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) => (
<Menu
elevation={0}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'right'
}}
transformOrigin={{
vertical: 'top',
horizontal: 'right'
}}
{...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 (
<ListItemButton style={{ borderRadius: 15, border: '1px solid #e0e0e0' }} onClick={onClick} ref={ref}>
@ -104,10 +146,12 @@ const ConfirmDeleteMessageDialog = ({ show, dialogProps, onCancel, onConfirm })
</DialogTitle>
<DialogContent>
<span style={{ marginTop: '20px', marginBottom: '20px' }}>{dialogProps.description}</span>
<FormControlLabel
control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />}
label='Remove messages from 3rd party Memory Node'
/>
{dialogProps.isChatflow && (
<FormControlLabel
control={<Checkbox checked={hardDelete} onChange={(event) => setHardDelete(event.target.checked)} />}
label='Remove messages from 3rd party Memory Node'
/>
)}
</DialogContent>
<DialogActions>
<Button onClick={onCancel}>{dialogProps.cancelButtonName}</Button>
@ -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'
>
<DialogTitle sx={{ fontSize: '1rem' }} id='alert-dialog-title'>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{dialogProps.title}
<div style={{ flex: 1 }} />
<Button variant='outlined' onClick={() => exportMessages()} startIcon={<IconFileExport />}>
Export
</Button>
</div>
</DialogTitle>
<DialogContent>
<>
<div
@ -912,7 +957,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
<b style={{ marginRight: 10 }}>Feedback</b>
<MultiDropdown
key={JSON.stringify(feedbackTypeFilter)}
name='chatType'
name='feedbackType'
options={[
{
label: 'Positive',
@ -929,31 +974,81 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
/>
</div>
<div style={{ flex: 1 }}></div>
{stats.totalMessages > 0 && (
<Button color='error' variant='outlined' onClick={() => onDeleteMessages()} startIcon={<IconEraser />}>
Delete Messages
</Button>
)}
<Button
id='messages-dialog-action-button'
aria-controls={open ? 'messages-dialog-action-menu' : undefined}
aria-haspopup='true'
aria-expanded={open ? 'true' : undefined}
variant={customization.isDarkMode ? 'contained' : 'outlined'}
disableElevation
color='secondary'
onClick={handleClick}
sx={{
minWidth: 150,
'&:hover': {
backgroundColor: customization.isDarkMode ? alpha(theme.palette.secondary.main, 0.8) : undefined
}
}}
endIcon={
<KeyboardArrowDownIcon style={{ backgroundColor: customization.isDarkMode ? 'transparent' : 'inherit' }} />
}
>
More Actions
</Button>
<StyledMenu
id='messages-dialog-action-menu'
MenuListProps={{
'aria-labelledby': 'messages-dialog-action-button'
}}
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
<MenuItem
onClick={() => {
handleClose()
exportMessages()
}}
disableRipple
>
<IconFileExport style={{ marginRight: 8 }} />
Export to JSON
</MenuItem>
{(stats.totalMessages ?? 0) > 0 && (
<MenuItem
onClick={() => {
handleClose()
onDeleteMessages()
}}
disableRipple
>
<IconEraser style={{ marginRight: 8 }} />
Delete All
</MenuItem>
)}
</StyledMenu>
</div>
<div
style={{
display: 'grid',
gridTemplateColumns: 'repeat(3, minmax(0, 1fr))',
gridTemplateColumns: 'repeat(4, minmax(0, 1fr))',
gap: 10,
marginBottom: 16,
marginBottom: 25,
marginLeft: 8,
marginRight: 8
marginRight: 8,
marginTop: 20
}}
>
<StatsCard title='Total Messages' stat={`${stats.totalMessages}`} />
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback}`} />
<StatsCard title='Total Sessions' stat={`${stats.totalSessions ?? 0}`} />
<StatsCard title='Total Messages' stat={`${stats.totalMessages ?? 0}`} />
<StatsCard title='Total Feedback Received' stat={`${stats.totalFeedback ?? 0}`} />
<StatsCard
title='Positive Feedback'
stat={`${((stats.positiveFeedback / stats.totalFeedback) * 100 || 0).toFixed(2)}%`}
stat={`${(((stats.positiveFeedback ?? 0) / (stats.totalFeedback ?? 1)) * 100 || 0).toFixed(2)}%`}
/>
</div>
<div style={{ display: 'flex', flexDirection: 'row' }}>
{chatlogs && chatlogs.length == 0 && (
<div style={{ display: 'flex', flexDirection: 'row', overflow: 'hidden', minWidth: 0 }}>
{chatlogs && chatlogs.length === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center', width: '100%' }} flexDirection='column'>
<Box sx={{ p: 5, height: 'auto' }}>
<img
@ -966,7 +1061,7 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</Stack>
)}
{chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '40%' }}>
<div style={{ flexBasis: '40%', minWidth: 0, overflow: 'hidden' }}>
<Box
sx={{
overflowY: 'auto',
@ -976,6 +1071,28 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
maxHeight: 'calc(100vh - 260px)'
}}
>
<div
style={{
display: 'flex',
marginLeft: '15px',
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: 10
}}
>
<Typography variant='h5'>
Sessions {pageLimit * (currentPage - 1) + 1} - {Math.min(pageLimit * currentPage, total)} of{' '}
{total}
</Typography>
<Pagination
style={{ justifyItems: 'right', justifyContent: 'center' }}
count={Math.ceil(total / pageLimit)}
onChange={onChange}
page={currentPage}
color='primary'
/>
</div>
{chatlogs.map((chatmsg, index) => (
<ListItemButton
key={index}
@ -1018,9 +1135,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
</div>
)}
{chatlogs && chatlogs.length > 0 && (
<div style={{ flexBasis: '60%', paddingRight: '30px' }}>
<div style={{ flexBasis: '60%', paddingRight: '30px', minWidth: 0, overflow: 'hidden' }}>
{chatMessages && chatMessages.length > 1 && (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<div style={{ marginBottom: 10, display: 'flex', flexDirection: 'row', alignItems: 'center' }}>
<div style={{ flex: 1, marginLeft: '20px', marginBottom: '15px', marginTop: '10px' }}>
{chatMessages[1].sessionId && (
<div>
@ -1046,31 +1163,26 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
<div
style={{
display: 'flex',
flexDirection: 'column',
flexDirection: 'row',
alignContent: 'center',
alignItems: 'end'
}}
>
<StyledButton
sx={{ height: 'max-content', width: 'max-content' }}
variant='outlined'
color='error'
title='Clear Message'
onClick={() => clearChat(chatMessages[1])}
startIcon={<IconEraser />}
>
Clear
</StyledButton>
<Tooltip title='Clear Message'>
<IconButton color='error' onClick={() => clearChat(chatMessages[1])}>
<IconEraser />
</IconButton>
</Tooltip>
{chatMessages[1].sessionId && (
<Tooltip
title={
'At your left 👈 you will see the Memory node that was used in this conversation. You need to have the matching Memory node with same parameters in the canvas, in order to delete the session conversations stored on the Memory node'
'On the left 👈, youll see the Memory node used in this conversation. To delete the session conversations stored on that Memory node, you must have a matching Memory node with identical parameters in the canvas.'
}
placement='bottom'
>
<h5 style={{ cursor: 'pointer', color: theme.palette.primary.main }}>
Why my session is not deleted?
</h5>
<IconButton color='primary'>
<IconBulb />
</IconButton>
</Tooltip>
)}
</div>
@ -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'
>
<div style={{ width: '100%', height: '100%' }}>
<div style={{ width: '100%', height: '100%', overflowY: 'auto' }}>
{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 }) => {
})}
</div>
)}
<div className='markdownanswer'>
<div
className='markdownanswer'
style={{ wordBreak: 'break-word', overflowWrap: 'break-word' }}
>
<MemoizedReactMarkdown chatflowid={dialogProps.chatflow.id}>
{message.message}
</MemoizedReactMarkdown>
@ -1486,7 +1606,9 @@ const ViewMessagesDialog = ({ show, dialogProps, onCancel }) => {
return (
<Box
sx={{
background: theme.palette.timeMessage.main,
background: customization.isDarkMode
? theme.palette.divider
: theme.palette.timeMessage.main,
p: 2
}}
key={index}

View File

@ -0,0 +1,85 @@
import { Box, FormControl, MenuItem, Pagination, Select, Typography } from '@mui/material'
import { useEffect, useState } from 'react'
import { useTheme } from '@mui/material/styles'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
export const DEFAULT_ITEMS_PER_PAGE = 12
const TablePagination = ({ currentPage, limit, total, onChange }) => {
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 (
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant='body2'>Items per page:</Typography>
<FormControl
variant='outlined'
size='small'
sx={{
minWidth: 80,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: borderColor
},
'& .MuiSvgIcon-root': {
color: customization.isDarkMode ? '#fff' : 'inherit'
}
}}
>
<Select value={itemsPerPage} onChange={handleLimitChange} displayEmpty>
<MenuItem value={12}>12</MenuItem>
<MenuItem value={24}>24</MenuItem>
<MenuItem value={48}>48</MenuItem>
<MenuItem value={100}>100</MenuItem>
</Select>
</FormControl>
</Box>
{totalItems > 0 && (
<Typography variant='body2'>
Items {activePage * itemsPerPage - itemsPerPage + 1} to{' '}
{activePage * itemsPerPage > totalItems ? totalItems : activePage * itemsPerPage} of {totalItems}
</Typography>
)}
<Pagination count={Math.ceil(totalItems / itemsPerPage)} onChange={handlePageChange} page={activePage} color='primary' />
</Box>
)
}
TablePagination.propTypes = {
onChange: PropTypes.func.isRequired,
currentPage: PropTypes.number,
limit: PropTypes.number,
total: PropTypes.number
}
export default TablePagination

View File

@ -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 (
<>
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table sx={{ minWidth: 650 }} size='small' aria-label='document_store_table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>&nbsp;</StyledTableCell>
<StyledTableCell>
<TableSortLabel active={orderBy === 'name'} direction={order} onClick={() => handleRequestSort('name')}>
Name
</TableSortLabel>
</StyledTableCell>
<StyledTableCell>Description</StyledTableCell>
<StyledTableCell>Connected flows</StyledTableCell>
<StyledTableCell>Total characters</StyledTableCell>
<StyledTableCell>Total chunks</StyledTableCell>
<StyledTableCell>Loader Types</StyledTableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{sortedData.map((row, index) => {
return (
<StyledTableRow
onClick={() => onRowClick(row)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<StyledTableCell>
<DocumentStoreStatus isTableView={true} status={row.status} />
</StyledTableCell>
<StyledTableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{row.name}
</Typography>
</StyledTableCell>
<StyledTableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{row?.description}
</Typography>
</StyledTableCell>
<StyledTableCell>{row.whereUsed?.length ?? 0}</StyledTableCell>
<StyledTableCell>{row.totalChars}</StyledTableCell>
<StyledTableCell>{row.totalChunks}</StyledTableCell>
<StyledTableCell>
{images && images[row.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[row.id]
.slice(0, images[row.id].length > 3 ? 3 : images[row.id].length)
.map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images?.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</StyledTableCell>
</StyledTableRow>
)
})}
</>
)}
</TableBody>
</Table>
</TableContainer>
</>
)
}
DocumentStoreTable.propTypes = {
data: PropTypes.array,
isLoading: PropTypes.bool,
images: PropTypes.object,
onRowClick: PropTypes.func
}
DocumentStoreTable.displayName = 'DocumentStoreTable'

View File

@ -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) {

View File

@ -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 = () => {
</Grid>
<Grid item xs={12} md={4}>
<Stack direction='row' spacing={1}>
<Button variant='contained' color='primary' onClick={applyFilters} size='small'>
<Button
variant='contained'
color='primary'
onClick={() => applyFilters(currentPage, pageLimit)}
size='small'
>
Apply
</Button>
<Button variant='outlined' onClick={resetFilters} size='small'>
@ -366,69 +360,47 @@ const AgentExecutions = () => {
</Grid>
</Box>
<ExecutionsListTable
data={executions}
isLoading={isLoading}
onSelectionChange={handleExecutionSelectionChange}
onExecutionRowClick={(execution) => {
setOpenDrawer(true)
const executionDetails =
typeof execution.executionData === 'string' ? JSON.parse(execution.executionData) : execution.executionData
setSelectedExecutionData(executionDetails)
setSelectedMetadata(omit(execution, ['executionData']))
}}
/>
{/* Pagination and Page Size Controls */}
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', mt: 2 }}>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
<Typography variant='body2'>Items per page:</Typography>
<FormControl
variant='outlined'
size='small'
sx={{
minWidth: 80,
'& .MuiOutlinedInput-notchedOutline': {
borderColor: borderColor
},
'& .MuiSvgIcon-root': {
color: customization.isDarkMode ? '#fff' : 'inherit'
}
{executions?.length > 0 && (
<>
<ExecutionsListTable
data={executions}
isLoading={isLoading}
onSelectionChange={handleExecutionSelectionChange}
onExecutionRowClick={(execution) => {
setOpenDrawer(true)
const executionDetails =
typeof execution.executionData === 'string'
? JSON.parse(execution.executionData)
: execution.executionData
setSelectedExecutionData(executionDetails)
setSelectedMetadata(omit(execution, ['executionData']))
}}
>
<Select value={pagination.limit} onChange={handleLimitChange} displayEmpty>
<MenuItem value={10}>10</MenuItem>
<MenuItem value={50}>50</MenuItem>
<MenuItem value={100}>100</MenuItem>
<MenuItem value={1000}>1000</MenuItem>
</Select>
</FormControl>
</Box>
<Pagination
count={Math.ceil(pagination.total / pagination.limit)}
page={pagination.page}
onChange={handlePageChange}
color='primary'
/>
</Box>
/>
<ExecutionDetails
open={openDrawer}
execution={selectedExecutionData}
metadata={selectedMetadata}
onClose={() => setOpenDrawer(false)}
onProceedSuccess={() => {
setOpenDrawer(false)
getAllExecutions.request()
}}
onUpdateSharing={() => {
getAllExecutions.request()
}}
onRefresh={(executionId) => {
getAllExecutions.request()
getExecutionByIdApi.request(executionId)
}}
/>
{/* Pagination and Page Size Controls */}
{!isLoading && total > 0 && (
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
)}
<ExecutionDetails
open={openDrawer}
execution={selectedExecutionData}
metadata={selectedMetadata}
onClose={() => setOpenDrawer(false)}
onProceedSuccess={() => {
setOpenDrawer(false)
getAllExecutions.request()
}}
onUpdateSharing={() => {
getAllExecutions.request()
}}
onRefresh={(executionId) => {
getAllExecutions.request()
getExecutionByIdApi.request(executionId)
}}
/>
</>
)}
{/* Delete Confirmation Dialog */}
<Dialog

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
// material-ui
import { Chip, Box, Skeleton, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { Chip, Box, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
@ -15,6 +15,7 @@ import { FlowListTable } from '@/ui-component/table/FlowListTable'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import chatflowsApi from '@/api/chatflows'
@ -45,6 +46,25 @@ const Agentflows = () => {
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
const [agentflowVersion, setAgentflowVersion] = useState(localStorage.getItem('agentFlowVersion') || 'v2')
/* 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)
refresh(page, pageLimit, agentflowVersion)
}
const refresh = (page, limit, nextView) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT', params)
}
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView)
@ -55,7 +75,7 @@ const Agentflows = () => {
if (nextView === null) return
localStorage.setItem('agentFlowVersion', nextView)
setAgentflowVersion(nextView)
getAllAgentflows.request(nextView === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT')
refresh(1, pageLimit, nextView)
}
const onSearchChange = (event) => {
@ -87,7 +107,7 @@ const Agentflows = () => {
}
useEffect(() => {
getAllAgentflows.request(agentflowVersion === 'v2' ? 'AGENTFLOW' : 'MULTIAGENT')
refresh(currentPage, pageLimit, agentflowVersion)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -107,7 +127,8 @@ const Agentflows = () => {
useEffect(() => {
if (getAllAgentflows.data) {
try {
const agentflows = getAllAgentflows.data
const agentflows = getAllAgentflows.data?.data
setTotal(getAllAgentflows.data?.total)
const images = {}
const icons = {}
for (let i = 0; i < agentflows.length; i += 1) {
@ -189,6 +210,7 @@ const Agentflows = () => {
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
disabled={total === 0}
color='primary'
exclusive
onChange={handleChange}
@ -228,17 +250,11 @@ const Agentflows = () => {
Add New
</StyledPermissionButton>
</ViewHeader>
{!view || view === 'card' ? (
{!isLoading && total > 0 && (
<>
{isLoading && !getAllAgentflows.data ? (
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllAgentflows.data?.filter(filterFlows).map((data, index) => (
{getAllAgentflows.data?.data.filter(filterFlows).map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
@ -248,22 +264,25 @@ const Agentflows = () => {
/>
))}
</Box>
) : (
<FlowListTable
isAgentCanvas={true}
isAgentflowV2={agentflowVersion === 'v2'}
data={getAllAgentflows.data?.data}
images={images}
icons={icons}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllAgentflows}
setError={setError}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
) : (
<FlowListTable
isAgentCanvas={true}
isAgentflowV2={agentflowVersion === 'v2'}
data={getAllAgentflows.data}
images={images}
icons={icons}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllAgentflows}
setError={setError}
/>
)}
{!isLoading && (!getAllAgentflows.data || getAllAgentflows.data.length === 0) && (
{!isLoading && total === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img

View File

@ -33,6 +33,8 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import apiKeyApi from '@/api/apikey'
@ -59,7 +61,6 @@ import {
IconFileUpload
} from '@tabler/icons-react'
import APIEmptySVG from '@/assets/images/api_empty.svg'
import UploadJSONFileDialog from '@/views/apikey/UploadJSONFileDialog'
// ==============================|| APIKey ||============================== //
@ -222,6 +223,26 @@ const APIKey = () => {
const [uploadDialogProps, setUploadDialogProps] = useState({})
const [search, setSearch] = useState('')
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllAPIKeysApi.request(params)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
@ -341,12 +362,11 @@ const APIKey = () => {
const onConfirm = () => {
setShowDialog(false)
setShowUploadDialog(false)
getAllAPIKeysApi.request()
refresh(currentPage, pageLimit)
}
useEffect(() => {
getAllAPIKeysApi.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -356,7 +376,8 @@ const APIKey = () => {
useEffect(() => {
if (getAllAPIKeysApi.data) {
setAPIKeys(getAllAPIKeysApi.data)
setAPIKeys(getAllAPIKeysApi.data?.data)
setTotal(getAllAPIKeysApi.data?.total)
}
}, [getAllAPIKeysApi.data])
@ -395,7 +416,7 @@ const APIKey = () => {
Create Key
</StyledPermissionButton>
</ViewHeader>
{!isLoading && apiKeys.length <= 0 ? (
{!isLoading && apiKeys?.length <= 0 ? (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
@ -407,104 +428,108 @@ const APIKey = () => {
<div>No API Keys Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>Key Name</StyledTableCell>
<StyledTableCell>API Key</StyledTableCell>
<StyledTableCell>Usage</StyledTableCell>
<StyledTableCell>Updated</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</StyledTableRow>
</>
) : (
<>
{apiKeys.filter(filterKeys).map((key, index) => (
<APIKeyRow
key={index}
apiKey={key}
showApiKeys={showApiKeys}
onCopyClick={(event) => {
navigator.clipboard.writeText(key.apiKey)
setAnchorEl(event.currentTarget)
setTimeout(() => {
handleClosePopOver()
}, 1500)
}}
onShowAPIClick={() => onShowApiKeyClick(key.apiKey)}
open={openPopOver}
anchorEl={anchorEl}
onClose={handleClosePopOver}
theme={theme}
onEditClick={() => edit(key)}
onDeleteClick={() => deleteKey(key)}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>Key Name</StyledTableCell>
<StyledTableCell>API Key</StyledTableCell>
<StyledTableCell>Usage</StyledTableCell>
<StyledTableCell>Updated</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'apikeys:update,apikeys:create'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permission={'apikeys:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</StyledTableRow>
</>
) : (
<>
{apiKeys?.filter(filterKeys).map((key, index) => (
<APIKeyRow
key={index}
apiKey={key}
showApiKeys={showApiKeys}
onCopyClick={(event) => {
navigator.clipboard.writeText(key.apiKey)
setAnchorEl(event.currentTarget)
setTimeout(() => {
handleClosePopOver()
}, 1500)
}}
onShowAPIClick={() => onShowApiKeyClick(key.apiKey)}
open={openPopOver}
anchorEl={anchorEl}
onClose={handleClosePopOver}
theme={theme}
onEditClick={() => edit(key)}
onDeleteClick={() => deleteKey(key)}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}

View File

@ -510,7 +510,8 @@ const CustomAssistantConfigurePreview = () => {
} else if (setting === 'viewMessages') {
setViewMessagesDialogProps({
title: 'View Messages',
chatflow: canvas.chatflow
chatflow: canvas.chatflow,
isChatflow: false
})
setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') {

View File

@ -76,7 +76,8 @@ const CanvasHeader = ({ chatflow, isAgentCanvas, isAgentflowV2, handleSaveFlow,
} else if (setting === 'viewMessages') {
setViewMessagesDialogProps({
title: 'View Messages',
chatflow: chatflow
chatflow: chatflow,
isChatflow: isAgentflowV2 ? false : true
})
setViewMessagesDialogOpen(true)
} else if (setting === 'viewLeads') {

View File

@ -15,6 +15,7 @@ import { FlowListTable } from '@/ui-component/table/FlowListTable'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import chatflowsApi from '@/api/chatflows'
@ -43,6 +44,25 @@ const Chatflows = () => {
const getAllChatflowsApi = useApi(chatflowsApi.getAllChatflows)
const [view, setView] = useState(localStorage.getItem('flowDisplayStyle') || 'card')
/* 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 applyFilters = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllChatflowsApi.request(params)
}
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('flowDisplayStyle', nextView)
@ -70,7 +90,7 @@ const Chatflows = () => {
}
useEffect(() => {
getAllChatflowsApi.request()
applyFilters(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -81,7 +101,9 @@ const Chatflows = () => {
useEffect(() => {
if (getAllChatflowsApi.data) {
try {
const chatflows = getAllChatflowsApi.data
const chatflows = getAllChatflowsApi.data?.data
const total = getAllChatflowsApi.data?.total
setTotal(total)
const images = {}
for (let i = 0; i < chatflows.length; i += 1) {
const flowDataStr = chatflows[i].flowData
@ -123,6 +145,7 @@ const Chatflows = () => {
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
disabled={total === 0}
exclusive
onChange={handleChange}
>
@ -161,41 +184,37 @@ const Chatflows = () => {
Add New
</StyledPermissionButton>
</ViewHeader>
{!view || view === 'card' ? (
{isLoading && (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
)}
{!isLoading && total > 0 && (
<>
{isLoading && !getAllChatflowsApi.data ? (
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
{getAllChatflowsApi.data?.data?.filter(filterFlows).map((data, index) => (
<ItemCard key={index} onClick={() => goToCanvas(data)} data={data} images={images[data.id]} />
))}
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllChatflowsApi.data &&
getAllChatflowsApi.data
?.filter(filterFlows)
.map((data, index) => (
<ItemCard
key={index}
onClick={() => goToCanvas(data)}
data={data}
images={images[data.id]}
/>
))}
</Box>
<FlowListTable
data={getAllChatflowsApi.data?.data}
images={images}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi}
setError={setError}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
) : (
<FlowListTable
data={getAllChatflowsApi.data}
images={images}
isLoading={isLoading}
filterFunction={filterFlows}
updateFlowsApi={getAllChatflowsApi}
setError={setError}
/>
)}
{!isLoading && (!getAllChatflowsApi.data || getAllChatflowsApi.data.length === 0) && (
{!isLoading && (!getAllChatflowsApi.data?.data || getAllChatflowsApi.data?.data.length === 0) && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img

View File

@ -26,9 +26,11 @@ import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import AddEditDatasetRowDialog from './AddEditDatasetRowDialog'
import UploadCSVFileDialog from '@/views/datasets/UploadCSVFileDialog'
import ErrorBoundary from '@/ErrorBoundary'
import { useError } from '@/store/context/ErrorContext'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import AddEditDatasetDialog from '@/views/datasets/AddEditDatasetDialog'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import datasetsApi from '@/api/dataset'
@ -45,8 +47,6 @@ import empty_datasetSVG from '@/assets/images/empty_datasets.svg'
import { IconTrash, IconPlus, IconX, IconUpload, IconArrowsDownUp } from '@tabler/icons-react'
import DragIndicatorIcon from '@mui/icons-material/DragIndicator'
import { useError } from '@/store/context/ErrorContext'
// ==============================|| Dataset Items ||============================== //
const EvalDatasetRows = () => {
@ -85,6 +85,25 @@ const EvalDatasetRows = () => {
const [startDragPos, setStartDragPos] = useState(-1)
const [endDragPos, setEndDragPos] = useState(-1)
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getDatasetRows.request(datasetId, params)
}
const handleDragStart = (e, position) => {
draggingItem.current = position
setStartDragPos(position)
@ -242,11 +261,11 @@ const EvalDatasetRows = () => {
setShowRowDialog(false)
setShowUploadDialog(false)
setShowDatasetDialog(false)
getDatasetRows.request(datasetId)
refresh(currentPage, pageLimit)
}
useEffect(() => {
getDatasetRows.request(datasetId)
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -254,6 +273,7 @@ const EvalDatasetRows = () => {
if (getDatasetRows.data) {
const dataset = getDatasetRows.data
setDataset(dataset)
setTotal(dataset.total)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getDatasetRows.data])
@ -449,9 +469,11 @@ const EvalDatasetRows = () => {
</TableBody>
</Table>
</TableContainer>
<Typography sx={{ color: theme.palette.grey[600] }} variant='subtitle2'>
<Typography sx={{ color: theme.palette.grey[600], marginTop: -2 }} variant='subtitle2'>
<i>Use the drag icon at (extreme right) to reorder the dataset items</i>
</Typography>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</React.Fragment>
)}
</Stack>

View File

@ -29,6 +29,7 @@ import ErrorBoundary from '@/ErrorBoundary'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import { closeSnackbar as closeSnackbarAction, enqueueSnackbar as enqueueSnackbarAction } from '@/store/actions'
@ -70,8 +71,27 @@ const EvalDatasets = () => {
const [datasetDialogProps, setDatasetDialogProps] = useState({})
const getAllDatasets = useApi(datasetsApi.getAllDatasets)
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllDatasets.request(params)
}
const goToRows = (selectedDataset) => {
navigate(`/dataset_rows/${selectedDataset.id}`)
navigate(`/dataset_rows/${selectedDataset.id}?page=1&limit=10`)
}
const onSearchChange = (event) => {
@ -149,7 +169,7 @@ const EvalDatasets = () => {
const onConfirm = () => {
setShowDatasetDialog(false)
getAllDatasets.request()
refresh()
}
function filterDatasets(data) {
@ -157,13 +177,14 @@ const EvalDatasets = () => {
}
useEffect(() => {
getAllDatasets.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllDatasets.data) {
setDatasets(getAllDatasets.data)
setDatasets(getAllDatasets.data?.data)
setTotal(getAllDatasets.data?.total)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllDatasets.data])
@ -209,118 +230,126 @@ const EvalDatasets = () => {
<div>No Datasets Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Rows</TableCell>
<TableCell>Last Updated</TableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell> </TableCell>
</Available>
<Available permission={'datasets:delete'}>
<TableCell> </TableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'datasets:update,datasets:create'}>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'datasets:update,datasets:create'}>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
</>
) : (
<>
{datasets.filter(filterDatasets).map((ds, index) => (
<StyledTableRow
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell onClick={() => goToRows(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell
onClick={() => goToRows(ds)}
style={{ wordWrap: 'break-word', flexWrap: 'wrap', width: '40%' }}
>
{truncateString(ds?.description, 200)}
</TableCell>
<TableCell onClick={() => goToRows(ds)}>{ds?.rowCount}</TableCell>
<TableCell onClick={() => goToRows(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Rows</TableCell>
<TableCell>Last Updated</TableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell> </TableCell>
</Available>
<Available permission={'datasets:delete'}>
<TableCell> </TableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(ds)}>
<IconEdit />
</IconButton>
</TableCell>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<TableCell>
<IconButton title='Delete' color='error' onClick={() => deleteDataset(ds)}>
<IconTrash />
</IconButton>
</TableCell>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'datasets:update,datasets:create'}>
<Skeleton variant='text' />
</Available>
<Available permission={'datasets:delete'}>
<Skeleton variant='text' />
</Available>
</StyledTableRow>
</>
) : (
<>
{datasets.filter(filterDatasets).map((ds, index) => (
<StyledTableRow
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell onClick={() => goToRows(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell
onClick={() => goToRows(ds)}
style={{ wordWrap: 'break-word', flexWrap: 'wrap', width: '40%' }}
>
{truncateString(ds?.description, 200)}
</TableCell>
<TableCell onClick={() => goToRows(ds)}>{ds?.rowCount}</TableCell>
<TableCell onClick={() => goToRows(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<Available permission={'datasets:update,datasets:create'}>
<TableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(ds)}>
<IconEdit />
</IconButton>
</TableCell>
</Available>
<Available permission={'datasets:delete'}>
<TableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteDataset(ds)}
>
<IconTrash />
</IconButton>
</TableCell>
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}

View File

@ -1,32 +1,18 @@
import { useEffect, useState } from 'react'
import React, { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useSelector } from 'react-redux'
// material-ui
import {
Box,
Paper,
Skeleton,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
ToggleButton,
ToggleButtonGroup,
Typography
} from '@mui/material'
import { Box, Stack, ToggleButton, ToggleButtonGroup } from '@mui/material'
import { useTheme } from '@mui/material/styles'
// project imports
import ErrorBoundary from '@/ErrorBoundary'
import { useError } from '@/store/context/ErrorContext'
import MainCard from '@/ui-component/cards/MainCard'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import DocumentStoreCard from '@/ui-component/cards/DocumentStoreCard'
import AddDocStoreDialog from '@/views/docstore/AddDocStoreDialog'
import ErrorBoundary from '@/ErrorBoundary'
import ViewHeader from '@/layout/MainLayout/ViewHeader'
import DocumentStoreStatus from '@/views/docstore/DocumentStoreStatus'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
// API
@ -39,13 +25,12 @@ import doc_store_empty from '@/assets/images/doc_store_empty.svg'
// const
import { baseURL, gridSpacing } from '@/store/constant'
import { useError } from '@/store/context/ErrorContext'
import { DocumentStoreTable } from '@/ui-component/table/DocumentStoreTable'
// ==============================|| DOCUMENTS ||============================== //
const Documents = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
const navigate = useNavigate()
const getAllDocumentStores = useApi(documentsApi.getAllDocumentStores)
@ -66,7 +51,9 @@ const Documents = () => {
}
function filterDocStores(data) {
return data.name.toLowerCase().indexOf(search.toLowerCase()) > -1
return (
data.name.toLowerCase().indexOf(search.toLowerCase()) > -1 || data.description.toLowerCase().indexOf(search.toLowerCase()) > -1
)
}
const onSearchChange = (event) => {
@ -90,41 +77,61 @@ const Documents = () => {
const onConfirm = () => {
setShowDialog(false)
getAllDocumentStores.request()
applyFilters(currentPage, pageLimit)
}
useEffect(() => {
getAllDocumentStores.request()
applyFilters(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
/* 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 applyFilters = (page, limit) => {
setLoading(true)
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllDocumentStores.request(params)
}
useEffect(() => {
if (getAllDocumentStores.data) {
try {
const docStores = getAllDocumentStores.data
if (!Array.isArray(docStores)) return
const { data, total } = getAllDocumentStores.data
if (!Array.isArray(data)) return
const loaderImages = {}
for (let i = 0; i < docStores.length; i += 1) {
const loaders = docStores[i].loaders ?? []
for (let i = 0; i < data.length; i += 1) {
const loaders = data[i].loaders ?? []
let totalChunks = 0
let totalChars = 0
loaderImages[docStores[i].id] = []
loaderImages[data[i].id] = []
for (let j = 0; j < loaders.length; j += 1) {
const imageSrc = `${baseURL}/api/v1/node-icon/${loaders[j].loaderId}`
if (!loaderImages[docStores[i].id].includes(imageSrc)) {
loaderImages[docStores[i].id].push(imageSrc)
if (!loaderImages[data[i].id].includes(imageSrc)) {
loaderImages[data[i].id].push(imageSrc)
}
totalChunks += loaders[j]?.totalChunks ?? 0
totalChars += loaders[j]?.totalChars ?? 0
}
docStores[i].totalDocs = loaders?.length ?? 0
docStores[i].totalChunks = totalChunks
docStores[i].totalChars = totalChars
data[i].totalDocs = loaders?.length ?? 0
data[i].totalChunks = totalChunks
data[i].totalChars = totalChars
}
setDocStores(docStores)
setDocStores(data)
setTotal(total)
setImages(loaderImages)
} catch (e) {
console.error(e)
@ -136,6 +143,8 @@ const Documents = () => {
setLoading(getAllDocumentStores.loading)
}, [getAllDocumentStores.loading])
const hasDocStores = docStores && docStores.length > 0
return (
<MainCard>
{error ? (
@ -144,43 +153,45 @@ const Documents = () => {
<Stack flexDirection='column' sx={{ gap: 3 }}>
<ViewHeader
onSearchChange={onSearchChange}
search={true}
search={hasDocStores}
searchPlaceholder='Search Name'
title='Document Store'
description='Store and upsert documents for LLM retrieval (RAG)'
>
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleChange}
>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='card'
title='Card View'
{hasDocStores && (
<ToggleButtonGroup
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
exclusive
onChange={handleChange}
>
<IconLayoutGrid />
</ToggleButton>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='card'
title='Card View'
>
<IconLayoutGrid />
</ToggleButton>
<ToggleButton
sx={{
borderColor: theme.palette.grey[900] + 25,
borderRadius: 2,
color: theme?.customization?.isDarkMode ? 'white' : 'inherit'
}}
variant='contained'
value='list'
title='List View'
>
<IconList />
</ToggleButton>
</ToggleButtonGroup>
)}
<StyledPermissionButton
permissionId={'documentStores:create'}
variant='contained'
@ -192,142 +203,7 @@ const Documents = () => {
Add New
</StyledPermissionButton>
</ViewHeader>
{!view || view === 'card' ? (
<>
{isLoading && !docStores ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
)}
</>
) : (
<TableContainer sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }} component={Paper}>
<Table aria-label='documents table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode ? theme.palette.common.black : theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>&nbsp;</TableCell>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Connected flows</TableCell>
<TableCell>Total characters</TableCell>
<TableCell>Total chunks</TableCell>
<TableCell>Loader types</TableCell>
</TableRow>
</TableHead>
<TableBody>
{docStores?.filter(filterDocStores).map((data, index) => (
<TableRow
onClick={() => goToDocumentStore(data.id)}
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell align='center'>
<DocumentStoreStatus isTableView={true} status={data.status} />
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data.name}
</Typography>
</TableCell>
<TableCell>
<Typography
sx={{
display: '-webkit-box',
WebkitLineClamp: 5,
WebkitBoxOrient: 'vertical',
textOverflow: 'ellipsis',
overflow: 'hidden'
}}
>
{data?.description}
</Typography>
</TableCell>
<TableCell>{data.whereUsed?.length ?? 0}</TableCell>
<TableCell>{data.totalChars}</TableCell>
<TableCell>{data.totalChunks}</TableCell>
<TableCell>
{images[data.id] && (
<Box
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'start',
gap: 1
}}
>
{images[data.id].slice(0, images.length > 3 ? 3 : images.length).map((img) => (
<Box
key={img}
sx={{
width: 30,
height: 30,
borderRadius: '50%',
backgroundColor: customization.isDarkMode
? theme.palette.common.white
: theme.palette.grey[300] + 75
}}
>
<img
style={{
width: '100%',
height: '100%',
padding: 5,
objectFit: 'contain'
}}
alt=''
src={img}
/>
</Box>
))}
{images.length > 3 && (
<Typography
sx={{
alignItems: 'center',
display: 'flex',
fontSize: '.9rem',
fontWeight: 200
}}
>
+ {images.length - 3} More
</Typography>
)}
</Box>
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{!isLoading && (!docStores || docStores.length === 0) && (
{!hasDocStores ? (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img
@ -338,6 +214,30 @@ const Documents = () => {
</Box>
<div>No Document Stores Created Yet</div>
</Stack>
) : (
<React.Fragment>
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{docStores?.filter(filterDocStores).map((data, index) => (
<DocumentStoreCard
key={index}
images={images[data.id]}
data={data}
onClick={() => goToDocumentStore(data.id)}
/>
))}
</Box>
) : (
<DocumentStoreTable
isLoading={isLoading}
data={docStores?.filter(filterDocStores)}
images={images}
onRowClick={(row) => goToDocumentStore(row.id)}
/>
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</React.Fragment>
)}
</Stack>
)}

View File

@ -33,6 +33,7 @@ import useApi from '@/hooks/useApi'
// Hooks
import useConfirm from '@/hooks/useConfirm'
import useNotifier from '@/utils/useNotifier'
import { useError } from '@/store/context/ErrorContext'
// project
import MainCard from '@/ui-component/cards/MainCard'
@ -43,6 +44,7 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import CreateEvaluationDialog from '@/views/evaluations/CreateEvaluationDialog'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// icons
import {
@ -59,8 +61,6 @@ import {
} from '@tabler/icons-react'
import empty_evalSVG from '@/assets/images/empty_evals.svg'
import { useError } from '@/store/context/ErrorContext'
const EvalsEvaluation = () => {
const theme = useTheme()
const customization = useSelector((state) => state.customization)
@ -83,6 +83,24 @@ const EvalsEvaluation = () => {
const [selected, setSelected] = useState([])
const [autoRefresh, setAutoRefresh] = useState(false)
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllEvaluations.request(params)
}
const onSelectAllClick = (event) => {
if (event.target.checked) {
const newSelected = rows.filter((item) => item?.latestEval).map((n) => n.id)
@ -171,13 +189,14 @@ const EvalsEvaluation = () => {
}
useEffect(() => {
getAllEvaluations.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllEvaluations.data) {
const evalRows = getAllEvaluations.data
const evalRows = getAllEvaluations.data.data
setTotal(getAllEvaluations.data.total)
if (evalRows) {
// Prepare the data for the table
for (let i = 0; i < evalRows.length; i++) {
@ -244,7 +263,8 @@ const EvalsEvaluation = () => {
}, [createNewEvaluation.error])
const onRefresh = useCallback(() => {
getAllEvaluations.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllEvaluations])
useEffect(() => {
@ -358,111 +378,115 @@ const EvalsEvaluation = () => {
<div>No Evaluations Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell padding='checkbox'>
<Checkbox
color='primary'
checked={selected.length === (rows.filter((item) => item?.latestEval) || []).length}
onChange={onSelectAllClick}
inputProps={{
'aria-label': 'select all'
}}
/>
</TableCell>
<TableCell width={10}> </TableCell>
<TableCell>Name</TableCell>
<TableCell>Latest Version</TableCell>
<TableCell>Average Metrics</TableCell>
<TableCell>Last Evaluated</TableCell>
<TableCell>Flow(s)</TableCell>
<TableCell>Dataset</TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{isTableLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{rows
.filter((item) => item?.latestEval)
.map((item, index) => (
<EvaluationRunRow
rows={rows.filter((row) => row.name === item.name)}
item={item}
key={index}
theme={theme}
selected={selected}
customization={customization}
onRefresh={onRefresh}
handleSelect={handleSelect}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell padding='checkbox'>
<Checkbox
color='primary'
checked={selected.length === (rows.filter((item) => item?.latestEval) || []).length}
onChange={onSelectAllClick}
inputProps={{
'aria-label': 'select all'
}}
/>
</TableCell>
<TableCell width={10}> </TableCell>
<TableCell>Name</TableCell>
<TableCell>Latest Version</TableCell>
<TableCell>Average Metrics</TableCell>
<TableCell>Last Evaluated</TableCell>
<TableCell>Flow(s)</TableCell>
<TableCell>Dataset</TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{isTableLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{rows
.filter((item) => item?.latestEval)
.map((item, index) => (
<EvaluationRunRow
rows={rows.filter((row) => row.name === item.name)}
item={item}
key={index}
theme={theme}
selected={selected}
customization={customization}
onRefresh={onRefresh}
handleSelect={handleSelect}
/>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}

View File

@ -14,6 +14,8 @@ import ConfirmDialog from '@/ui-component/dialog/ConfirmDialog'
import AddEditEvaluatorDialog from '@/views/evaluators/AddEditEvaluatorDialog'
import { StyledTableCell, StyledTableRow } from '@/ui-component/table/TableStyles'
import { PermissionIconButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
import { truncateString } from '@/utils/genericHelper'
// API
import evaluatorsApi from '@/api/evaluators'
@ -23,15 +25,14 @@ import moment from 'moment/moment'
import useNotifier from '@/utils/useNotifier'
import useConfirm from '@/hooks/useConfirm'
import useApi from '@/hooks/useApi'
import { useError } from '@/store/context/ErrorContext'
// icons
import empty_evaluatorSVG from '@/assets/images/empty_evaluators.svg'
import { IconTrash, IconPlus, IconJson, IconX, IconNumber123, IconAbc, IconAugmentedReality } from '@tabler/icons-react'
import { truncateString } from '@/utils/genericHelper'
// const
import { evaluators as evaluatorsOptions, numericOperators } from '../evaluators/evaluatorConstant'
import { useError } from '@/store/context/ErrorContext'
// ==============================|| Evaluators ||============================== //
@ -54,6 +55,24 @@ const Evaluators = () => {
const getAllEvaluators = useApi(evaluatorsApi.getAllEvaluators)
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllEvaluators.request(params)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
@ -129,7 +148,7 @@ const Evaluators = () => {
const onConfirm = () => {
setShowEvaluatorDialog(false)
getAllEvaluators.request()
refresh(currentPage, pageLimit)
}
function filterDatasets(data) {
@ -137,13 +156,14 @@ const Evaluators = () => {
}
useEffect(() => {
getAllEvaluators.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
if (getAllEvaluators.data) {
setEvaluators(getAllEvaluators.data)
setEvaluators(getAllEvaluators.data.data)
setTotal(getAllEvaluators.data.total)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [getAllEvaluators.data])
@ -189,327 +209,352 @@ const Evaluators = () => {
<div>No Evaluators Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell>Details</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{evaluators.filter(filterDatasets).map((ds, index) => (
<>
<StyledTableRow
hover
key={index}
sx={{ cursor: 'pointer', '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconNumber123 />} label='Numeric' variant='outlined' />
</Stack>
)}
{ds?.type === 'text' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconAbc />} label='Text Based' variant='outlined' />
</Stack>
)}
{ds?.type === 'json' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip icon={<IconJson />} label='JSON Based' variant='outlined' />
</Stack>
)}
{ds?.type === 'llm' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconAugmentedReality />}
label='LLM Based'
variant='outlined'
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell style={{ width: '40%' }} onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }}>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<TableCell>Type</TableCell>
<TableCell>Name</TableCell>
<TableCell>Details</TableCell>
<TableCell>Last Updated</TableCell>
<TableCell> </TableCell>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</StyledTableRow>
</>
) : (
<>
{evaluators.filter(filterDatasets).map((ds, index) => (
<>
<StyledTableRow
hover
key={index}
sx={{
cursor: 'pointer',
'&:last-child td, &:last-child th': { border: 0 }
}}
>
<TableCell onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconNumber123 />}
label='Numeric'
variant='outlined'
/>
</Stack>
)}
{ds?.type === 'text' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconAbc />}
label='Text Based'
variant='outlined'
/>
</Stack>
)}
{ds?.type === 'json' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconJson />}
label='JSON Based'
variant='outlined'
/>
</Stack>
)}
{ds?.type === 'llm' && (
<Stack flexDirection='row' sx={{ alignItems: 'center' }}>
<Chip
icon={<IconAugmentedReality />}
label='LLM Based'
variant='outlined'
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)} component='th' scope='row'>
{ds.name}
</TableCell>
<TableCell style={{ width: '40%' }} onClick={() => edit(ds)}>
{ds?.type === 'numeric' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Measure</b>:{' '}
{
[
...evaluatorsOptions,
...numericOperators
].find((item) => item.name === ds?.measure)
?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[
...evaluatorsOptions,
...numericOperators
].find((item) => item.name === ds?.operator)
?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'text' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[
...evaluatorsOptions,
...numericOperators
].find((item) => item.name === ds?.operator)
?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'json' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
</Stack>
)}
{ds?.type === 'llm' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Prompt</b>: {truncateString(ds?.prompt, 100)}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Output Schema Elements</b>:{' '}
{ds?.outputSchema.length > 0
? ds?.outputSchema
.map((item) => item.property)
.join(', ')
: 'None'}
</span>
}
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<TableCell>
<PermissionIconButton
permissionId={'evaluators:delete'}
title='Delete'
color='error'
onClick={() => deleteEvaluator(ds)}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Measure</b>:{' '}
{
[...evaluatorsOptions, ...numericOperators].find(
(item) => item.name === ds?.measure
)?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions, ...numericOperators].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'text' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions, ...numericOperators].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Value</b>: {ds?.value}
</span>
}
/>
</Stack>
)}
{ds?.type === 'json' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Operator</b>:{' '}
{
[...evaluatorsOptions].find(
(item) => item.name === ds?.operator
)?.label
}
</span>
}
/>
</Stack>
)}
{ds?.type === 'llm' && (
<Stack
flexDirection='row'
gap={1}
sx={{ alignItems: 'center', flexWrap: 'wrap' }}
>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Prompt</b>: {truncateString(ds?.prompt, 100)}
</span>
}
/>
<Chip
variant='outlined'
size='small'
color='default'
sx={{
height: 'auto',
'& .MuiChip-label': {
display: 'block',
whiteSpace: 'normal'
},
p: 0.5
}}
label={
<span>
<b>Output Schema Elements</b>:{' '}
{ds?.outputSchema.length > 0
? ds?.outputSchema
.map((item) => item.property)
.join(', ')
: 'None'}
</span>
}
/>
</Stack>
)}
</TableCell>
<TableCell onClick={() => edit(ds)}>
{moment(ds.updatedDate).format('MMMM Do YYYY, hh:mm A')}
</TableCell>
<TableCell>
<PermissionIconButton
permissionId={'evaluators:delete'}
title='Delete'
color='error'
onClick={() => deleteEvaluator(ds)}
>
<IconTrash />
</PermissionIconButton>
</TableCell>
</StyledTableRow>
</>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<IconTrash />
</PermissionIconButton>
</TableCell>
</StyledTableRow>
</>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}

View File

@ -12,6 +12,7 @@ import ViewHeader from '@/layout/MainLayout/ViewHeader'
import ErrorBoundary from '@/ErrorBoundary'
import { ToolsTable } from '@/ui-component/table/ToolsListTable'
import { PermissionButton, StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import toolsApi from '@/api/tools'
@ -39,6 +40,25 @@ const Tools = () => {
const inputRef = useRef(null)
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllToolsApi.request(params)
}
const handleChange = (event, nextView) => {
if (nextView === null) return
localStorage.setItem('toolsDisplayStyle', nextView)
@ -102,7 +122,7 @@ const Tools = () => {
const onConfirm = () => {
setShowDialog(false)
getAllToolsApi.request()
refresh(currentPage, pageLimit)
}
const [search, setSearch] = useState('')
@ -117,8 +137,7 @@ const Tools = () => {
}
useEffect(() => {
getAllToolsApi.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -126,6 +145,12 @@ const Tools = () => {
setLoading(getAllToolsApi.loading)
}, [getAllToolsApi.loading])
useEffect(() => {
if (getAllToolsApi.data) {
setTotal(getAllToolsApi.data.total)
}
}, [getAllToolsApi.data])
return (
<>
<MainCard>
@ -144,6 +169,7 @@ const Tools = () => {
sx={{ borderRadius: 2, maxHeight: 40 }}
value={view}
color='primary'
disabled={total === 0}
exclusive
onChange={handleChange}
>
@ -203,27 +229,29 @@ const Tools = () => {
</StyledPermissionButton>
</ButtonGroup>
</ViewHeader>
{!view || view === 'card' ? (
{isLoading && (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
</Box>
)}
{!isLoading && total > 0 && (
<>
{isLoading ? (
{!view || view === 'card' ? (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
<Skeleton variant='rounded' height={160} />
{getAllToolsApi.data?.data?.filter(filterTools).map((data, index) => (
<ItemCard data={data} key={index} onClick={() => edit(data)} />
))}
</Box>
) : (
<Box display='grid' gridTemplateColumns='repeat(3, 1fr)' gap={gridSpacing}>
{getAllToolsApi.data &&
getAllToolsApi.data
?.filter(filterTools)
.map((data, index) => <ItemCard data={data} key={index} onClick={() => edit(data)} />)}
</Box>
<ToolsTable data={getAllToolsApi.data.data} isLoading={isLoading} onSelect={edit} />
)}
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
) : (
<ToolsTable data={getAllToolsApi.data} isLoading={isLoading} onSelect={edit} />
)}
{!isLoading && (!getAllToolsApi.data || getAllToolsApi.data.length === 0) && (
{!isLoading && total === 0 && (
<Stack sx={{ alignItems: 'center', justifyContent: 'center' }} flexDirection='column'>
<Box sx={{ p: 2, height: 'auto' }}>
<img

View File

@ -33,6 +33,7 @@ import ErrorBoundary from '@/ErrorBoundary'
import { StyledPermissionButton } from '@/ui-component/button/RBACButtons'
import { Available } from '@/ui-component/rbac/available'
import { refreshVariablesCache } from '@/ui-component/input/suggestionOption'
import TablePagination, { DEFAULT_ITEMS_PER_PAGE } from '@/ui-component/pagination/TablePagination'
// API
import variablesApi from '@/api/variables'
@ -91,8 +92,27 @@ const Variables = () => {
const { confirm } = useConfirm()
const getAllVariables = useApi(variablesApi.getAllVariables)
const [search, setSearch] = useState('')
/* 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)
refresh(page, pageLimit)
}
const refresh = (page, limit) => {
const params = {
page: page || currentPage,
limit: limit || pageLimit
}
getAllVariables.request(params)
}
const onSearchChange = (event) => {
setSearch(event.target.value)
}
@ -172,12 +192,12 @@ const Variables = () => {
const onConfirm = () => {
setShowVariableDialog(false)
getAllVariables.request()
refresh(currentPage, pageLimit)
refreshVariablesCache()
}
useEffect(() => {
getAllVariables.request()
refresh(currentPage, pageLimit)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
@ -187,7 +207,8 @@ const Variables = () => {
useEffect(() => {
if (getAllVariables.data) {
setVariables(getAllVariables.data)
setVariables(getAllVariables.data.data)
setTotal(getAllVariables.data.total)
}
}, [getAllVariables.data])
@ -231,162 +252,169 @@ const Variables = () => {
<div>No Variables Yet</div>
</Stack>
) : (
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>Name</StyledTableCell>
<StyledTableCell>Value</StyledTableCell>
<StyledTableCell>Type</StyledTableCell>
<StyledTableCell>Last Updated</StyledTableCell>
<StyledTableCell>Created</StyledTableCell>
<Available permissionId={'variables:update'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permissionId={'variables:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<>
<TableContainer
sx={{ border: 1, borderColor: theme.palette.grey[900] + 25, borderRadius: 2 }}
component={Paper}
>
<Table sx={{ minWidth: 650 }} aria-label='simple table'>
<TableHead
sx={{
backgroundColor: customization.isDarkMode
? theme.palette.common.black
: theme.palette.grey[100],
height: 56
}}
>
<TableRow>
<StyledTableCell>Name</StyledTableCell>
<StyledTableCell>Value</StyledTableCell>
<StyledTableCell>Type</StyledTableCell>
<StyledTableCell>Last Updated</StyledTableCell>
<StyledTableCell>Created</StyledTableCell>
<Available permissionId={'variables:update'}>
<StyledTableCell> </StyledTableCell>
</Available>
<Available permissionId={'variables:delete'}>
<StyledTableCell> </StyledTableCell>
</Available>
</TableRow>
</TableHead>
<TableBody>
{isLoading ? (
<>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</StyledTableRow>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</StyledTableRow>
</>
) : (
<>
{variables.filter(filterVariables).map((variable, index) => (
<StyledTableRow key={index} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
<StyledTableCell component='th' scope='row'>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<div
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: '50%'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{variable.name}
</div>
</StyledTableCell>
<StyledTableCell>{variable.value}</StyledTableCell>
<StyledTableCell>
<Chip
color={variable.type === 'static' ? 'info' : 'secondary'}
size='small'
label={variable.type}
/>
</StyledTableCell>
<StyledTableCell>
{moment(variable.updatedDate).format('MMMM Do, YYYY HH:mm:ss')}
</StyledTableCell>
<StyledTableCell>
{moment(variable.createdDate).format('MMMM Do, YYYY HH:mm:ss')}
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteVariable(variable)}
>
<IconTrash />
</IconButton>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
<StyledTableRow>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<Skeleton variant='text' />
</StyledTableCell>
</Available>
</StyledTableRow>
</>
) : (
<>
{variables.filter(filterVariables).map((variable, index) => (
<StyledTableRow
key={index}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<StyledTableCell component='th' scope='row'>
<div
style={{
display: 'flex',
flexDirection: 'row',
alignItems: 'center'
}}
>
<div
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: '50%'
}}
>
<IconVariable
style={{
width: '100%',
height: '100%',
borderRadius: '50%',
objectFit: 'contain'
}}
/>
</div>
{variable.name}
</div>
</StyledTableCell>
<StyledTableCell>{variable.value}</StyledTableCell>
<StyledTableCell>
<Chip
color={variable.type === 'static' ? 'info' : 'secondary'}
size='small'
label={variable.type}
/>
</StyledTableCell>
<StyledTableCell>
{moment(variable.updatedDate).format('MMMM Do, YYYY HH:mm:ss')}
</StyledTableCell>
<StyledTableCell>
{moment(variable.createdDate).format('MMMM Do, YYYY HH:mm:ss')}
</StyledTableCell>
<Available permission={'variables:create,variables:update'}>
<StyledTableCell>
<IconButton title='Edit' color='primary' onClick={() => edit(variable)}>
<IconEdit />
</IconButton>
</StyledTableCell>
</Available>
<Available permission={'variables:delete'}>
<StyledTableCell>
<IconButton
title='Delete'
color='error'
onClick={() => deleteVariable(variable)}
>
<IconTrash />
</IconButton>
</StyledTableCell>
</Available>
</StyledTableRow>
))}
</>
)}
</TableBody>
</Table>
</TableContainer>
{/* Pagination and Page Size Controls */}
<TablePagination currentPage={currentPage} limit={pageLimit} total={total} onChange={onChange} />
</>
)}
</Stack>
)}