From b2b7e82e281512a7249aabc754dbe4d65f825ad9 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Wed, 24 Dec 2025 15:25:28 +0800 Subject: [PATCH] refactor(web): migrate log service to TanStack Query (#30065) --- .claude/skills/frontend-testing/SKILL.md | 6 +- .../assets/component-test.template.tsx | 2 +- .../frontend-testing/references/checklist.md | 14 +- .../frontend-testing/references/mocking.md | 25 +- .../frontend-testing/references/workflow.md | 12 +- .github/workflows/web-tests.yml | 2 +- .../components/app/annotation/filter.spec.tsx | 350 +++++++++++-- web/app/components/app/annotation/filter.tsx | 8 +- web/app/components/app/log/filter.tsx | 7 +- web/app/components/app/log/index.tsx | 23 +- web/app/components/app/log/list.tsx | 10 +- .../app/workflow-log/index.spec.tsx | 494 ++++++++++-------- web/app/components/app/workflow-log/index.tsx | 9 +- web/app/components/base/skeleton/index.tsx | 3 +- .../model-parameter-modal/model-display.tsx | 30 +- web/models/log.ts | 15 - web/package.json | 1 + web/service/log.ts | 54 +- web/service/use-log.ts | 89 ++++ web/testing/testing.md | 4 +- 20 files changed, 741 insertions(+), 417 deletions(-) create mode 100644 web/service/use-log.ts diff --git a/.claude/skills/frontend-testing/SKILL.md b/.claude/skills/frontend-testing/SKILL.md index 7475513ba0..65602c92eb 100644 --- a/.claude/skills/frontend-testing/SKILL.md +++ b/.claude/skills/frontend-testing/SKILL.md @@ -49,10 +49,10 @@ pnpm test pnpm test:watch # Run specific file -pnpm test -- path/to/file.spec.tsx +pnpm test path/to/file.spec.tsx # Generate coverage report -pnpm test -- --coverage +pnpm test:coverage # Analyze component complexity pnpm analyze-component @@ -155,7 +155,7 @@ describe('ComponentName', () => { For each file: ┌────────────────────────────────────────┐ │ 1. Write test │ - │ 2. Run: pnpm test -- .spec.tsx │ + │ 2. Run: pnpm test .spec.tsx │ │ 3. PASS? → Mark complete, next file │ │ FAIL? → Fix first, then continue │ └────────────────────────────────────────┘ diff --git a/.claude/skills/frontend-testing/assets/component-test.template.tsx b/.claude/skills/frontend-testing/assets/component-test.template.tsx index 92dd797c83..c39baff916 100644 --- a/.claude/skills/frontend-testing/assets/component-test.template.tsx +++ b/.claude/skills/frontend-testing/assets/component-test.template.tsx @@ -198,7 +198,7 @@ describe('ComponentName', () => { }) // -------------------------------------------------------------------------- - // Async Operations (if component fetches data - useSWR, useQuery, fetch) + // Async Operations (if component fetches data - useQuery, fetch) // -------------------------------------------------------------------------- // WHY: Async operations have 3 states users experience: loading, success, error describe('Async Operations', () => { diff --git a/.claude/skills/frontend-testing/references/checklist.md b/.claude/skills/frontend-testing/references/checklist.md index aad80b120e..1ff2b27bbb 100644 --- a/.claude/skills/frontend-testing/references/checklist.md +++ b/.claude/skills/frontend-testing/references/checklist.md @@ -114,15 +114,15 @@ For the current file being tested: **Run these checks after EACH test file, not just at the end:** -- [ ] Run `pnpm test -- path/to/file.spec.tsx` - **MUST PASS before next file** +- [ ] Run `pnpm test path/to/file.spec.tsx` - **MUST PASS before next file** - [ ] Fix any failures immediately - [ ] Mark file as complete in todo list - [ ] Only then proceed to next file ### After All Files Complete -- [ ] Run full directory test: `pnpm test -- path/to/directory/` -- [ ] Check coverage report: `pnpm test -- --coverage` +- [ ] Run full directory test: `pnpm test path/to/directory/` +- [ ] Check coverage report: `pnpm test:coverage` - [ ] Run `pnpm lint:fix` on all test files - [ ] Run `pnpm type-check:tsgo` @@ -186,16 +186,16 @@ Always test these scenarios: ```bash # Run specific test -pnpm test -- path/to/file.spec.tsx +pnpm test path/to/file.spec.tsx # Run with coverage -pnpm test -- --coverage path/to/file.spec.tsx +pnpm test:coverage path/to/file.spec.tsx # Watch mode -pnpm test:watch -- path/to/file.spec.tsx +pnpm test:watch path/to/file.spec.tsx # Update snapshots (use sparingly) -pnpm test -- -u path/to/file.spec.tsx +pnpm test -u path/to/file.spec.tsx # Analyze component pnpm analyze-component path/to/component.tsx diff --git a/.claude/skills/frontend-testing/references/mocking.md b/.claude/skills/frontend-testing/references/mocking.md index 51920ebc64..23889c8d3d 100644 --- a/.claude/skills/frontend-testing/references/mocking.md +++ b/.claude/skills/frontend-testing/references/mocking.md @@ -242,32 +242,9 @@ describe('Component with Context', () => { }) ``` -### 7. SWR / React Query +### 7. React Query ```typescript -// SWR -vi.mock('swr', () => ({ - __esModule: true, - default: vi.fn(), -})) - -import useSWR from 'swr' -const mockedUseSWR = vi.mocked(useSWR) - -describe('Component with SWR', () => { - it('should show loading state', () => { - mockedUseSWR.mockReturnValue({ - data: undefined, - error: undefined, - isLoading: true, - }) - - render() - expect(screen.getByText(/loading/i)).toBeInTheDocument() - }) -}) - -// React Query import { QueryClient, QueryClientProvider } from '@tanstack/react-query' const createTestQueryClient = () => new QueryClient({ diff --git a/.claude/skills/frontend-testing/references/workflow.md b/.claude/skills/frontend-testing/references/workflow.md index b0f2994bde..009c3e013b 100644 --- a/.claude/skills/frontend-testing/references/workflow.md +++ b/.claude/skills/frontend-testing/references/workflow.md @@ -35,7 +35,7 @@ When testing a **single component, hook, or utility**: 2. Run `pnpm analyze-component ` (if available) 3. Check complexity score and features detected 4. Write the test file -5. Run test: `pnpm test -- .spec.tsx` +5. Run test: `pnpm test .spec.tsx` 6. Fix any failures 7. Verify coverage meets goals (100% function, >95% branch) ``` @@ -80,7 +80,7 @@ Process files in this recommended order: ``` ┌─────────────────────────────────────────────┐ │ 1. Write test file │ -│ 2. Run: pnpm test -- .spec.tsx │ +│ 2. Run: pnpm test .spec.tsx │ │ 3. If FAIL → Fix immediately, re-run │ │ 4. If PASS → Mark complete in todo list │ │ 5. ONLY THEN proceed to next file │ @@ -95,10 +95,10 @@ After all individual tests pass: ```bash # Run all tests in the directory together -pnpm test -- path/to/directory/ +pnpm test path/to/directory/ # Check coverage -pnpm test -- --coverage path/to/directory/ +pnpm test:coverage path/to/directory/ ``` ## Component Complexity Guidelines @@ -201,9 +201,9 @@ Run pnpm test ← Multiple failures, hard to debug ``` # GOOD: Incremental with verification Write component-a.spec.tsx -Run pnpm test -- component-a.spec.tsx ✅ +Run pnpm test component-a.spec.tsx ✅ Write component-b.spec.tsx -Run pnpm test -- component-b.spec.tsx ✅ +Run pnpm test component-b.spec.tsx ✅ ...continue... ``` diff --git a/.github/workflows/web-tests.yml b/.github/workflows/web-tests.yml index 8eba0f084b..adf52a1362 100644 --- a/.github/workflows/web-tests.yml +++ b/.github/workflows/web-tests.yml @@ -42,7 +42,7 @@ jobs: run: pnpm run check:i18n-types - name: Run tests - run: pnpm test --coverage + run: pnpm test:coverage - name: Coverage Summary if: always() diff --git a/web/app/components/app/annotation/filter.spec.tsx b/web/app/components/app/annotation/filter.spec.tsx index 9b733a8c10..7bb39bd444 100644 --- a/web/app/components/app/annotation/filter.spec.tsx +++ b/web/app/components/app/annotation/filter.spec.tsx @@ -1,72 +1,332 @@ +import type { UseQueryResult } from '@tanstack/react-query' import type { Mock } from 'vitest' import type { QueryParam } from './filter' +import type { AnnotationsCountResponse } from '@/models/log' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { fireEvent, render, screen } from '@testing-library/react' import * as React from 'react' -import useSWR from 'swr' +import * as useLogModule from '@/service/use-log' import Filter from './filter' -vi.mock('swr', () => ({ - __esModule: true, - default: vi.fn(), -})) +vi.mock('@/service/use-log') -vi.mock('@/service/log', () => ({ - fetchAnnotationsCount: vi.fn(), -})) +const mockUseAnnotationsCount = useLogModule.useAnnotationsCount as Mock -const mockUseSWR = useSWR as unknown as Mock +// ============================================================================ +// Test Utilities +// ============================================================================ + +const createQueryClient = () => new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) + +const renderWithQueryClient = (ui: React.ReactElement) => { + const queryClient = createQueryClient() + return render( + + {ui} + , + ) +} + +// ============================================================================ +// Mock Return Value Factory +// ============================================================================ + +type MockQueryResult = Pick, 'data' | 'isLoading' | 'error' | 'refetch'> + +const createMockQueryResult = ( + overrides: Partial> = {}, +): MockQueryResult => ({ + data: undefined, + isLoading: false, + error: null, + refetch: vi.fn(), + ...overrides, +}) + +// ============================================================================ +// Tests +// ============================================================================ describe('Filter', () => { const appId = 'app-1' const childContent = 'child-content' + const defaultQueryParams: QueryParam = { keyword: '' } beforeEach(() => { vi.clearAllMocks() }) - it('should render nothing until annotation count is fetched', () => { - mockUseSWR.mockReturnValue({ data: undefined }) + // -------------------------------------------------------------------------- + // Rendering Tests (REQUIRED) + // -------------------------------------------------------------------------- + describe('Rendering', () => { + it('should render nothing when data is loading', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ isLoading: true }), + ) - const { container } = render( - -
{childContent}
-
, - ) + // Act + const { container } = renderWithQueryClient( + +
{childContent}
+
, + ) - expect(container.firstChild).toBeNull() - expect(mockUseSWR).toHaveBeenCalledWith( - { url: `/apps/${appId}/annotations/count` }, - expect.any(Function), - ) + // Assert + expect(container.firstChild).toBeNull() + }) + + it('should render nothing when data is undefined', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ data: undefined, isLoading: false }), + ) + + // Act + const { container } = renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(container.firstChild).toBeNull() + }) + + it('should render filter and children when data is available', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 20 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + expect(screen.getByText(childContent)).toBeInTheDocument() + }) }) - it('should propagate keyword changes and clearing behavior', () => { - mockUseSWR.mockReturnValue({ data: { total: 20 } }) - const queryParams: QueryParam = { keyword: 'prefill' } - const setQueryParams = vi.fn() + // -------------------------------------------------------------------------- + // Props Tests (REQUIRED) + // -------------------------------------------------------------------------- + describe('Props', () => { + it('should call useAnnotationsCount with appId', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 10 }, + isLoading: false, + }), + ) - const { container } = render( - -
{childContent}
-
, - ) + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) - const input = screen.getByPlaceholderText('common.operation.search') as HTMLInputElement - fireEvent.change(input, { target: { value: 'updated' } }) - expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: 'updated' }) + // Assert + expect(mockUseAnnotationsCount).toHaveBeenCalledWith(appId) + }) - const clearButton = input.parentElement?.querySelector('div.cursor-pointer') as HTMLElement - fireEvent.click(clearButton) - expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' }) + it('should display keyword value in input', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 10 }, + isLoading: false, + }), + ) + const queryParams: QueryParam = { keyword: 'test-keyword' } - expect(container).toHaveTextContent(childContent) + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toHaveValue('test-keyword') + }) + }) + + // -------------------------------------------------------------------------- + // User Interactions + // -------------------------------------------------------------------------- + describe('User Interactions', () => { + it('should call setQueryParams when typing in search input', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 20 }, + isLoading: false, + }), + ) + const queryParams: QueryParam = { keyword: '' } + const setQueryParams = vi.fn() + + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Act + const input = screen.getByPlaceholderText('common.operation.search') + fireEvent.change(input, { target: { value: 'updated' } }) + + // Assert + expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: 'updated' }) + }) + + it('should call setQueryParams with empty keyword when clearing input', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 20 }, + isLoading: false, + }), + ) + const queryParams: QueryParam = { keyword: 'prefill' } + const setQueryParams = vi.fn() + + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Act + const input = screen.getByPlaceholderText('common.operation.search') + const clearButton = input.parentElement?.querySelector('div.cursor-pointer') + if (clearButton) + fireEvent.click(clearButton) + + // Assert + expect(setQueryParams).toHaveBeenCalledWith({ ...queryParams, keyword: '' }) + }) + }) + + // -------------------------------------------------------------------------- + // Edge Cases (REQUIRED) + // -------------------------------------------------------------------------- + describe('Edge Cases', () => { + it('should handle empty keyword in queryParams', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 5 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toHaveValue('') + }) + + it('should handle undefined keyword in queryParams', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 5 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + }) + + it('should handle zero count', () => { + // Arrange + mockUseAnnotationsCount.mockReturnValue( + createMockQueryResult({ + data: { count: 0 }, + isLoading: false, + }), + ) + + // Act + renderWithQueryClient( + +
{childContent}
+
, + ) + + // Assert - should still render when count is 0 + expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() + }) }) }) diff --git a/web/app/components/app/annotation/filter.tsx b/web/app/components/app/annotation/filter.tsx index 76f33d2f1b..b64a033793 100644 --- a/web/app/components/app/annotation/filter.tsx +++ b/web/app/components/app/annotation/filter.tsx @@ -2,9 +2,8 @@ import type { FC } from 'react' import * as React from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import Input from '@/app/components/base/input' -import { fetchAnnotationsCount } from '@/service/log' +import { useAnnotationsCount } from '@/service/use-log' export type QueryParam = { keyword?: string @@ -23,10 +22,9 @@ const Filter: FC = ({ setQueryParams, children, }) => { - // TODO: change fetch list api - const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount) + const { data, isLoading } = useAnnotationsCount(appId) const { t } = useTranslation() - if (!data) + if (isLoading || !data) return null return (
diff --git a/web/app/components/app/log/filter.tsx b/web/app/components/app/log/filter.tsx index 8984ff3494..4a0103449f 100644 --- a/web/app/components/app/log/filter.tsx +++ b/web/app/components/app/log/filter.tsx @@ -6,11 +6,10 @@ import dayjs from 'dayjs' import quarterOfYear from 'dayjs/plugin/quarterOfYear' import * as React from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import Chip from '@/app/components/base/chip' import Input from '@/app/components/base/input' import Sort from '@/app/components/base/sort' -import { fetchAnnotationsCount } from '@/service/log' +import { useAnnotationsCount } from '@/service/use-log' dayjs.extend(quarterOfYear) @@ -36,9 +35,9 @@ type IFilterProps = { } const Filter: FC = ({ isChatMode, appId, queryParams, setQueryParams }: IFilterProps) => { - const { data } = useSWR({ url: `/apps/${appId}/annotations/count` }, fetchAnnotationsCount) + const { data, isLoading } = useAnnotationsCount(appId) const { t } = useTranslation() - if (!data) + if (isLoading || !data) return null return (
diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index 183826464f..0e6e4bba2d 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -8,11 +8,10 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import Loading from '@/app/components/base/loading' import Pagination from '@/app/components/base/pagination' import { APP_PAGE_LIMIT } from '@/config' -import { fetchChatConversations, fetchCompletionConversations } from '@/service/log' +import { useChatConversations, useCompletionConversations } from '@/service/use-log' import { AppModeEnum } from '@/types/app' import EmptyElement from './empty-element' import Filter, { TIME_PERIOD_MAPPING } from './filter' @@ -88,19 +87,15 @@ const Logs: FC = ({ appDetail }) => { } // When the details are obtained, proceed to the next request - const { data: chatConversations, mutate: mutateChatList } = useSWR(() => isChatMode - ? { - url: `/apps/${appDetail.id}/chat-conversations`, - params: query, - } - : null, fetchChatConversations) + const { data: chatConversations, refetch: mutateChatList } = useChatConversations({ + appId: isChatMode ? appDetail.id : '', + params: query, + }) - const { data: completionConversations, mutate: mutateCompletionList } = useSWR(() => !isChatMode - ? { - url: `/apps/${appDetail.id}/completion-conversations`, - params: query, - } - : null, fetchCompletionConversations) + const { data: completionConversations, refetch: mutateCompletionList } = useCompletionConversations({ + appId: !isChatMode ? appDetail.id : '', + params: query, + }) const total = isChatMode ? chatConversations?.total : completionConversations?.total diff --git a/web/app/components/app/log/list.tsx b/web/app/components/app/log/list.tsx index 06cd20b323..2b13a09b3a 100644 --- a/web/app/components/app/log/list.tsx +++ b/web/app/components/app/log/list.tsx @@ -17,7 +17,6 @@ import { usePathname, useRouter, useSearchParams } from 'next/navigation' import * as React from 'react' import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import { createContext, useContext } from 'use-context-selector' import { useShallow } from 'zustand/react/shallow' import ModelInfo from '@/app/components/app/log/model-info' @@ -38,7 +37,8 @@ import { WorkflowContextProvider } from '@/app/components/workflow/context' import { useAppContext } from '@/context/app-context' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' -import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversationDetail, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log' +import { fetchChatMessages, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log' +import { useChatConversationDetail, useCompletionConversationDetail } from '@/service/use-log' import { AppModeEnum } from '@/types/app' import { cn } from '@/utils/classnames' import PromptLogModal from '../../base/prompt-log-modal' @@ -825,8 +825,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) { */ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { // Text Generator App Session Details Including Message List - const detailParams = ({ url: `/apps/${appId}/completion-conversations/${conversationId}` }) - const { data: conversationDetail, mutate: conversationDetailMutate } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchCompletionConversationDetail) + const { data: conversationDetail, refetch: conversationDetailMutate } = useCompletionConversationDetail(appId, conversationId) const { notify } = useContext(ToastContext) const { t } = useTranslation() @@ -875,8 +874,7 @@ const CompletionConversationDetailComp: FC<{ appId?: string, conversationId?: st * Chat App Conversation Detail Component */ const ChatConversationDetailComp: FC<{ appId?: string, conversationId?: string }> = ({ appId, conversationId }) => { - const detailParams = { url: `/apps/${appId}/chat-conversations/${conversationId}` } - const { data: conversationDetail } = useSWR(() => (appId && conversationId) ? detailParams : null, fetchChatConversationDetail) + const { data: conversationDetail } = useChatConversationDetail(appId, conversationId) const { notify } = useContext(ToastContext) const { t } = useTranslation() diff --git a/web/app/components/app/workflow-log/index.spec.tsx b/web/app/components/app/workflow-log/index.spec.tsx index 8d87a6426b..d689758b30 100644 --- a/web/app/components/app/workflow-log/index.spec.tsx +++ b/web/app/components/app/workflow-log/index.spec.tsx @@ -1,9 +1,9 @@ -import type { MockedFunction } from 'vitest' +import type { UseQueryResult } from '@tanstack/react-query' /** * Logs Container Component Tests * * Tests the main Logs container component which: - * - Fetches workflow logs via useSWR + * - Fetches workflow logs via TanStack Query * - Manages query parameters (status, period, keyword) * - Handles pagination * - Renders Filter, List, and Empty states @@ -15,14 +15,16 @@ import type { MockedFunction } from 'vitest' * - trigger-by-display.spec.tsx */ +import type { MockedFunction } from 'vitest' import type { ILogsProps } from './index' import type { WorkflowAppLogDetail, WorkflowLogsResponse, WorkflowRunDetail } from '@/models/log' import type { App, AppIconType, AppModeEnum } from '@/types/app' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { render, screen, waitFor } from '@testing-library/react' import userEvent from '@testing-library/user-event' -import useSWR from 'swr' import { APP_PAGE_LIMIT } from '@/config' import { WorkflowRunTriggeredFrom } from '@/models/log' +import * as useLogModule from '@/service/use-log' import { TIME_PERIOD_MAPPING } from './filter' import Logs from './index' @@ -30,7 +32,7 @@ import Logs from './index' // Mocks // ============================================================================ -vi.mock('swr') +vi.mock('@/service/use-log') vi.mock('ahooks', () => ({ useDebounce: (value: T) => value, @@ -72,10 +74,6 @@ vi.mock('@/app/components/base/amplitude/utils', () => ({ trackEvent: (...args: unknown[]) => mockTrackEvent(...args), })) -vi.mock('@/service/log', () => ({ - fetchWorkflowLogs: vi.fn(), -})) - vi.mock('@/hooks/use-theme', () => ({ __esModule: true, default: () => { @@ -89,38 +87,76 @@ vi.mock('@/context/app-context', () => ({ }), })) -// Mock useTimestamp -vi.mock('@/hooks/use-timestamp', () => ({ - __esModule: true, - default: () => ({ - formatTime: (timestamp: number, _format: string) => `formatted-${timestamp}`, - }), -})) - -// Mock useBreakpoints -vi.mock('@/hooks/use-breakpoints', () => ({ - __esModule: true, - default: () => 'pc', - MediaType: { - mobile: 'mobile', - pc: 'pc', - }, -})) - -// Mock BlockIcon -vi.mock('@/app/components/workflow/block-icon', () => ({ - __esModule: true, - default: () =>
BlockIcon
, -})) - // Mock WorkflowContextProvider vi.mock('@/app/components/workflow/context', () => ({ WorkflowContextProvider: ({ children }: { children: React.ReactNode }) => ( -
{children}
+ <>{children} ), })) -const mockedUseSWR = useSWR as unknown as MockedFunction +const mockedUseWorkflowLogs = useLogModule.useWorkflowLogs as MockedFunction + +// ============================================================================ +// Test Utilities +// ============================================================================ + +const createQueryClient = () => new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}) + +const renderWithQueryClient = (ui: React.ReactElement) => { + const queryClient = createQueryClient() + return render( + + {ui} + , + ) +} + +// ============================================================================ +// Mock Return Value Factory +// ============================================================================ + +const createMockQueryResult = ( + overrides: { data?: T, isLoading?: boolean, error?: Error | null } = {}, +): UseQueryResult => { + const isLoading = overrides.isLoading ?? false + const error = overrides.error ?? null + const data = overrides.data + + return { + data, + isLoading, + error, + refetch: vi.fn(), + isError: !!error, + isPending: isLoading, + isSuccess: !isLoading && !error && data !== undefined, + isFetching: isLoading, + isRefetching: false, + isLoadingError: false, + isRefetchError: false, + isInitialLoading: isLoading, + isPaused: false, + isEnabled: true, + status: isLoading ? 'pending' : error ? 'error' : 'success', + fetchStatus: isLoading ? 'fetching' : 'idle', + dataUpdatedAt: Date.now(), + errorUpdatedAt: 0, + failureCount: 0, + failureReason: null, + errorUpdateCount: 0, + isFetched: !isLoading, + isFetchedAfterMount: !isLoading, + isPlaceholderData: false, + isStale: false, + promise: Promise.resolve(data as T), + } as UseQueryResult +} // ============================================================================ // Test Data Factories @@ -195,6 +231,20 @@ const createMockLogsResponse = ( page: 1, }) +// ============================================================================ +// Type-safe Mock Helper +// ============================================================================ + +type WorkflowLogsParams = { + appId: string + params?: Record +} + +const getMockCallParams = (): WorkflowLogsParams | undefined => { + const lastCall = mockedUseWorkflowLogs.mock.calls.at(-1) + return lastCall?.[0] +} + // ============================================================================ // Tests // ============================================================================ @@ -213,45 +263,48 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Rendering', () => { it('should render without crashing', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('appLog.workflowTitle')).toBeInTheDocument() }) it('should render title and subtitle', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('appLog.workflowTitle')).toBeInTheDocument() expect(screen.getByText('appLog.workflowSubtitle')).toBeInTheDocument() }) it('should render Filter component', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByPlaceholderText('common.operation.search')).toBeInTheDocument() }) }) @@ -261,30 +314,33 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Loading State', () => { it('should show loading spinner when data is undefined', () => { - mockedUseSWR.mockReturnValue({ - data: undefined, - mutate: vi.fn(), - isValidating: true, - isLoading: true, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: undefined, + isLoading: true, + }), + ) - const { container } = render() + // Act + const { container } = renderWithQueryClient() + // Assert expect(container.querySelector('.spin-animation')).toBeInTheDocument() }) it('should not show loading spinner when data is available', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) - const { container } = render() + // Act + const { container } = renderWithQueryClient() + // Assert expect(container.querySelector('.spin-animation')).not.toBeInTheDocument() }) }) @@ -294,16 +350,17 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Empty State', () => { it('should render empty element when total is 0', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('appLog.table.empty.element.title')).toBeInTheDocument() expect(screen.queryByRole('table')).not.toBeInTheDocument() }) @@ -313,20 +370,21 @@ describe('Logs Container', () => { // Data Fetching Tests // -------------------------------------------------------------------------- describe('Data Fetching', () => { - it('should call useSWR with correct URL and default params', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + it('should call useWorkflowLogs with correct appId and default params', () => { + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { url: string, params: Record } - expect(keyArg).toMatchObject({ - url: `/apps/${defaultProps.appDetail.id}/workflow-app-logs`, + // Assert + const callArg = getMockCallParams() + expect(callArg).toMatchObject({ + appId: defaultProps.appDetail.id, params: expect.objectContaining({ page: 1, detail: true, @@ -336,34 +394,36 @@ describe('Logs Container', () => { }) it('should include date filters for non-allTime periods', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } - expect(keyArg?.params).toHaveProperty('created_at__after') - expect(keyArg?.params).toHaveProperty('created_at__before') + // Assert + const callArg = getMockCallParams() + expect(callArg?.params).toHaveProperty('created_at__after') + expect(callArg?.params).toHaveProperty('created_at__before') }) it('should not include status param when status is all', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } - expect(keyArg?.params).not.toHaveProperty('status') + // Assert + const callArg = getMockCallParams() + expect(callArg?.params).not.toHaveProperty('status') }) }) @@ -372,24 +432,23 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Filter Integration', () => { it('should update query when selecting status filter', async () => { + // Arrange const user = userEvent.setup() - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + renderWithQueryClient() - // Click status filter + // Act await user.click(screen.getByText('All')) await user.click(await screen.findByText('Success')) - // Check that useSWR was called with updated params + // Assert await waitFor(() => { - const lastCall = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } + const lastCall = getMockCallParams() expect(lastCall?.params).toMatchObject({ status: 'succeeded', }) @@ -397,46 +456,46 @@ describe('Logs Container', () => { }) it('should update query when selecting period filter', async () => { + // Arrange const user = userEvent.setup() - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + renderWithQueryClient() - // Click period filter + // Act await user.click(screen.getByText('appLog.filter.period.last7days')) await user.click(await screen.findByText('appLog.filter.period.allTime')) - // When period is allTime (9), date filters should be removed + // Assert await waitFor(() => { - const lastCall = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } + const lastCall = getMockCallParams() expect(lastCall?.params).not.toHaveProperty('created_at__after') expect(lastCall?.params).not.toHaveProperty('created_at__before') }) }) it('should update query when typing keyword', async () => { + // Arrange const user = userEvent.setup() - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) - render() + renderWithQueryClient() + // Act const searchInput = screen.getByPlaceholderText('common.operation.search') await user.type(searchInput, 'test-keyword') + // Assert await waitFor(() => { - const lastCall = mockedUseSWR.mock.calls.at(-1)?.[0] as { params?: Record } + const lastCall = getMockCallParams() expect(lastCall?.params).toMatchObject({ keyword: 'test-keyword', }) @@ -449,36 +508,35 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Pagination', () => { it('should not render pagination when total is less than limit', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) - render() + // Act + renderWithQueryClient() - // Pagination component should not be rendered + // Assert expect(screen.queryByRole('navigation')).not.toBeInTheDocument() }) it('should render pagination when total exceeds limit', () => { + // Arrange const logs = Array.from({ length: APP_PAGE_LIMIT }, (_, i) => createMockWorkflowLog({ id: `log-${i}` })) - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse(logs, APP_PAGE_LIMIT + 10), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse(logs, APP_PAGE_LIMIT + 10), + }), + ) - render() + // Act + renderWithQueryClient() - // Should show pagination - checking for any pagination-related element - // The Pagination component renders page controls + // Assert expect(screen.getByRole('table')).toBeInTheDocument() }) }) @@ -488,37 +546,39 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('List Rendering', () => { it('should render List component when data is available', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByRole('table')).toBeInTheDocument() }) it('should display log data in table', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([ - createMockWorkflowLog({ - workflow_run: createMockWorkflowRun({ - status: 'succeeded', - total_tokens: 500, + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([ + createMockWorkflowLog({ + workflow_run: createMockWorkflowRun({ + status: 'succeeded', + total_tokens: 500, + }), }), - }), - ], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + ], 1), + }), + ) - render() + // Act + renderWithQueryClient() + // Assert expect(screen.getByText('Success')).toBeInTheDocument() expect(screen.getByText('500')).toBeInTheDocument() }) @@ -541,52 +601,54 @@ describe('Logs Container', () => { // -------------------------------------------------------------------------- describe('Edge Cases', () => { it('should handle different app modes', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([createMockWorkflowLog()], 1), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([createMockWorkflowLog()], 1), + }), + ) const chatApp = createMockApp({ mode: 'advanced-chat' as AppModeEnum }) - render() + // Act + renderWithQueryClient() - // Should render without trigger column + // Assert expect(screen.queryByText('appLog.table.header.triggered_from')).not.toBeInTheDocument() }) - it('should handle error state from useSWR', () => { - mockedUseSWR.mockReturnValue({ - data: undefined, - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: new Error('Failed to fetch'), - }) + it('should handle error state from useWorkflowLogs', () => { + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: undefined, + error: new Error('Failed to fetch'), + }), + ) - const { container } = render() + // Act + const { container } = renderWithQueryClient() - // Should show loading state when data is undefined + // Assert - should show loading state when data is undefined expect(container.querySelector('.spin-animation')).toBeInTheDocument() }) it('should handle app with different ID', () => { - mockedUseSWR.mockReturnValue({ - data: createMockLogsResponse([], 0), - mutate: vi.fn(), - isValidating: false, - isLoading: false, - error: undefined, - }) + // Arrange + mockedUseWorkflowLogs.mockReturnValue( + createMockQueryResult({ + data: createMockLogsResponse([], 0), + }), + ) const customApp = createMockApp({ id: 'custom-app-123' }) - render() + // Act + renderWithQueryClient() - const keyArg = mockedUseSWR.mock.calls.at(-1)?.[0] as { url: string } - expect(keyArg?.url).toBe('/apps/custom-app-123/workflow-app-logs') + // Assert + const callArg = getMockCallParams() + expect(callArg?.appId).toBe('custom-app-123') }) }) }) diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 1390f2d435..12438d6d17 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -9,13 +9,12 @@ import { omit } from 'lodash-es' import * as React from 'react' import { useState } from 'react' import { useTranslation } from 'react-i18next' -import useSWR from 'swr' import EmptyElement from '@/app/components/app/log/empty-element' import Loading from '@/app/components/base/loading' import Pagination from '@/app/components/base/pagination' import { APP_PAGE_LIMIT } from '@/config' import { useAppContext } from '@/context/app-context' -import { fetchWorkflowLogs } from '@/service/log' +import { useWorkflowLogs } from '@/service/use-log' import Filter, { TIME_PERIOD_MAPPING } from './filter' import List from './list' @@ -55,10 +54,10 @@ const Logs: FC = ({ appDetail }) => { ...omit(debouncedQueryParams, ['period', 'status']), } - const { data: workflowLogs, mutate } = useSWR({ - url: `/apps/${appDetail.id}/workflow-app-logs`, + const { data: workflowLogs, refetch: mutate } = useWorkflowLogs({ + appId: appDetail.id, params: query, - }, fetchWorkflowLogs) + }) const total = workflowLogs?.total return ( diff --git a/web/app/components/base/skeleton/index.tsx b/web/app/components/base/skeleton/index.tsx index 9cd7e3f09c..cbb5a3d7c3 100644 --- a/web/app/components/base/skeleton/index.tsx +++ b/web/app/components/base/skeleton/index.tsx @@ -36,7 +36,8 @@ export const SkeletonPoint: FC = (props) => {
·
) } -/** Usage +/** + * Usage * * * diff --git a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx index e9e8ec7525..d8a4fabac0 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-parameter-modal/model-display.tsx @@ -6,20 +6,22 @@ type ModelDisplayProps = { } const ModelDisplay = ({ currentModel, modelId }: ModelDisplayProps) => { - return currentModel ? ( - - ) : ( -
-
- {modelId} -
-
- ) + return currentModel + ? ( + + ) + : ( +
+
+ {modelId} +
+
+ ) } export default ModelDisplay diff --git a/web/models/log.ts b/web/models/log.ts index 8c022ee6b2..d15d6d6688 100644 --- a/web/models/log.ts +++ b/web/models/log.ts @@ -6,21 +6,6 @@ import type { } from '@/app/components/workflow/types' import type { VisionFile } from '@/types/app' -// Log type contains key:string conversation_id:string created_at:string question:string answer:string -export type Conversation = { - id: string - key: string - conversationId: string - question: string - answer: string - userRate: number - adminRate: number -} - -export type ConversationListResponse = { - logs: Conversation[] -} - export const CompletionParams = ['temperature', 'top_p', 'presence_penalty', 'max_token', 'stop', 'frequency_penalty'] as const export type CompletionParamType = typeof CompletionParams[number] diff --git a/web/package.json b/web/package.json index ce2e59e022..baecd2c5c7 100644 --- a/web/package.json +++ b/web/package.json @@ -38,6 +38,7 @@ "gen:i18n-types": "node ./i18n-config/generate-i18n-types.js", "check:i18n-types": "node ./i18n-config/check-i18n-sync.js", "test": "vitest run", + "test:coverage": "vitest run --coverage", "test:watch": "vitest --watch", "analyze-component": "node testing/analyze-component.js", "storybook": "storybook dev -p 6006", diff --git a/web/service/log.ts b/web/service/log.ts index aa0be7ac3b..a540cea22c 100644 --- a/web/service/log.ts +++ b/web/service/log.ts @@ -1,80 +1,38 @@ -import type { Fetcher } from 'swr' import type { AgentLogDetailRequest, AgentLogDetailResponse, - AnnotationsCountResponse, - ChatConversationFullDetailResponse, - ChatConversationsRequest, - ChatConversationsResponse, ChatMessagesRequest, ChatMessagesResponse, - CompletionConversationFullDetailResponse, - CompletionConversationsRequest, - CompletionConversationsResponse, - ConversationListResponse, LogMessageAnnotationsRequest, LogMessageAnnotationsResponse, LogMessageFeedbacksRequest, LogMessageFeedbacksResponse, - WorkflowLogsResponse, WorkflowRunDetailResponse, } from '@/models/log' import type { NodeTracingListResponse } from '@/types/workflow' import { get, post } from './base' -export const fetchConversationList: Fetcher }> = ({ appId, params }) => { - return get(`/console/api/apps/${appId}/messages`, params) -} - -// (Text Generation Application) Session List -export const fetchCompletionConversations: Fetcher = ({ url, params }) => { - return get(url, { params }) -} - -// (Text Generation Application) Session Detail -export const fetchCompletionConversationDetail: Fetcher = ({ url }) => { - return get(url, {}) -} - -// (Chat Application) Session List -export const fetchChatConversations: Fetcher = ({ url, params }) => { - return get(url, { params }) -} - -// (Chat Application) Session Detail -export const fetchChatConversationDetail: Fetcher = ({ url }) => { - return get(url, {}) -} - // (Chat Application) Message list in one session -export const fetchChatMessages: Fetcher = ({ url, params }) => { +export const fetchChatMessages = ({ url, params }: { url: string, params: ChatMessagesRequest }): Promise => { return get(url, { params }) } -export const updateLogMessageFeedbacks: Fetcher = ({ url, body }) => { +export const updateLogMessageFeedbacks = ({ url, body }: { url: string, body: LogMessageFeedbacksRequest }): Promise => { return post(url, { body }) } -export const updateLogMessageAnnotations: Fetcher = ({ url, body }) => { +export const updateLogMessageAnnotations = ({ url, body }: { url: string, body: LogMessageAnnotationsRequest }): Promise => { return post(url, { body }) } -export const fetchAnnotationsCount: Fetcher = ({ url }) => { - return get(url) -} - -export const fetchWorkflowLogs: Fetcher }> = ({ url, params }) => { - return get(url, { params }) -} - -export const fetchRunDetail = (url: string) => { +export const fetchRunDetail = (url: string): Promise => { return get(url) } -export const fetchTracingList: Fetcher = ({ url }) => { +export const fetchTracingList = ({ url }: { url: string }): Promise => { return get(url) } -export const fetchAgentLogDetail = ({ appID, params }: { appID: string, params: AgentLogDetailRequest }) => { +export const fetchAgentLogDetail = ({ appID, params }: { appID: string, params: AgentLogDetailRequest }): Promise => { return get(`/apps/${appID}/agent/logs`, { params }) } diff --git a/web/service/use-log.ts b/web/service/use-log.ts new file mode 100644 index 0000000000..b120adda2f --- /dev/null +++ b/web/service/use-log.ts @@ -0,0 +1,89 @@ +import type { + AnnotationsCountResponse, + ChatConversationFullDetailResponse, + ChatConversationsRequest, + ChatConversationsResponse, + CompletionConversationFullDetailResponse, + CompletionConversationsRequest, + CompletionConversationsResponse, + WorkflowLogsResponse, +} from '@/models/log' +import { useQuery } from '@tanstack/react-query' +import { get } from './base' + +const NAME_SPACE = 'log' + +// ============ Annotations Count ============ + +export const useAnnotationsCount = (appId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'annotations-count', appId], + queryFn: () => get(`/apps/${appId}/annotations/count`), + enabled: !!appId, + }) +} + +// ============ Chat Conversations ============ + +type ChatConversationsParams = { + appId: string + params?: Partial +} + +export const useChatConversations = ({ appId, params }: ChatConversationsParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'chat-conversations', appId, params], + queryFn: () => get(`/apps/${appId}/chat-conversations`, { params }), + enabled: !!appId, + }) +} + +// ============ Completion Conversations ============ + +type CompletionConversationsParams = { + appId: string + params?: Partial +} + +export const useCompletionConversations = ({ appId, params }: CompletionConversationsParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'completion-conversations', appId, params], + queryFn: () => get(`/apps/${appId}/completion-conversations`, { params }), + enabled: !!appId, + }) +} + +// ============ Chat Conversation Detail ============ + +export const useChatConversationDetail = (appId?: string, conversationId?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'chat-conversation-detail', appId, conversationId], + queryFn: () => get(`/apps/${appId}/chat-conversations/${conversationId}`), + enabled: !!appId && !!conversationId, + }) +} + +// ============ Completion Conversation Detail ============ + +export const useCompletionConversationDetail = (appId?: string, conversationId?: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'completion-conversation-detail', appId, conversationId], + queryFn: () => get(`/apps/${appId}/completion-conversations/${conversationId}`), + enabled: !!appId && !!conversationId, + }) +} + +// ============ Workflow Logs ============ + +type WorkflowLogsParams = { + appId: string + params?: Record +} + +export const useWorkflowLogs = ({ appId, params }: WorkflowLogsParams) => { + return useQuery({ + queryKey: [NAME_SPACE, 'workflow-logs', appId, params], + queryFn: () => get(`/apps/${appId}/workflow-app-logs`, { params }), + enabled: !!appId, + }) +} diff --git a/web/testing/testing.md b/web/testing/testing.md index a2c8399d45..08fc716cf3 100644 --- a/web/testing/testing.md +++ b/web/testing/testing.md @@ -21,10 +21,10 @@ pnpm test pnpm test:watch # Generate coverage report -pnpm test -- --coverage +pnpm test:coverage # Run specific file -pnpm test -- path/to/file.spec.tsx +pnpm test path/to/file.spec.tsx ``` ## Project Test Setup