diff --git a/web/app/components/app/annotation/index.tsx b/web/app/components/app/annotation/index.tsx index 0783c3fa66..418079abe8 100644 --- a/web/app/components/app/annotation/index.tsx +++ b/web/app/components/app/annotation/index.tsx @@ -2,19 +2,17 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pagination } from 'react-headless-pagination' import { useDebounce } from 'ahooks' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import Toast from '../../base/toast' import Filter from './filter' import type { QueryParam } from './filter' import List from './list' import EmptyElement from './empty-element' import HeaderOpts from './header-opts' -import s from './style.module.css' import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type' import ViewAnnotationModal from './view-annotation-modal' import cn from '@/utils/classnames' +import Pagination from '@/app/components/base/pagination' import Switch from '@/app/components/base/switch' import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation' import Loading from '@/app/components/base/loading' @@ -69,9 +67,10 @@ const Annotation: FC = ({ const [queryParams, setQueryParams] = useState({}) const [currPage, setCurrPage] = React.useState(0) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) + const [limit, setLimit] = React.useState(APP_PAGE_LIMIT) const query = { page: currPage + 1, - limit: APP_PAGE_LIMIT, + limit, keyword: debouncedQueryParams.keyword || '', } @@ -228,35 +227,12 @@ const Annotation: FC = ({ {/* Show Pagination only if the total is more than the limit */} {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + limit={limit} + onLimitChange={setLimit} + /> : null} {isShowViewModal && ( diff --git a/web/app/components/app/annotation/style.module.css b/web/app/components/app/annotation/style.module.css deleted file mode 100644 index 24179c1ca1..0000000000 --- a/web/app/components/app/annotation/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.pagination li { - list-style: none; -} \ No newline at end of file diff --git a/web/app/components/app/annotation/view-annotation-modal/index.tsx b/web/app/components/app/annotation/view-annotation-modal/index.tsx index daa8434ff7..0fb8bbc31e 100644 --- a/web/app/components/app/annotation/view-annotation-modal/index.tsx +++ b/web/app/components/app/annotation/view-annotation-modal/index.tsx @@ -2,13 +2,12 @@ import type { FC } from 'react' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -import { Pagination } from 'react-headless-pagination' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import EditItem, { EditItemType } from '../edit-annotation-modal/edit-item' import type { AnnotationItem, HitHistoryItem } from '../type' import s from './style.module.css' import HitHistoryNoData from './hit-history-no-data' import cn from '@/utils/classnames' +import Pagination from '@/app/components/base/pagination' import Drawer from '@/app/components/base/drawer-plus' import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' import Confirm from '@/app/components/base/confirm' @@ -150,35 +149,10 @@ const ViewAnnotationModal: FC = ({ {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + /> : null} diff --git a/web/app/components/app/log-annotation/index.tsx b/web/app/components/app/log-annotation/index.tsx index c84d941143..3fa13019f9 100644 --- a/web/app/components/app/log-annotation/index.tsx +++ b/web/app/components/app/log-annotation/index.tsx @@ -52,7 +52,7 @@ const LogAnnotation: FC = ({ options={options} /> )} -
+
{pageType === PageType.log && appDetail.mode !== 'workflow' && ()} {pageType === PageType.annotation && ()} {pageType === PageType.log && appDetail.mode === 'workflow' && ()} diff --git a/web/app/components/app/log/index.tsx b/web/app/components/app/log/index.tsx index e076f587ea..592233facd 100644 --- a/web/app/components/app/log/index.tsx +++ b/web/app/components/app/log/index.tsx @@ -2,17 +2,15 @@ import type { FC, SVGProps } from 'react' import React, { useState } from 'react' import useSWR from 'swr' +import Link from 'next/link' import { usePathname } from 'next/navigation' -import { Pagination } from 'react-headless-pagination' import { useDebounce } from 'ahooks' import { omit } from 'lodash-es' import dayjs from 'dayjs' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import { Trans, useTranslation } from 'react-i18next' -import Link from 'next/link' import List from './list' import Filter, { TIME_PERIOD_MAPPING } from './filter' -import s from './style.module.css' +import Pagination from '@/app/components/base/pagination' import Loading from '@/app/components/base/loading' import { fetchChatConversations, fetchCompletionConversations } from '@/service/log' import { APP_PAGE_LIMIT } from '@/config' @@ -60,6 +58,7 @@ const Logs: FC = ({ appDetail }) => { sort_by: '-created_at', }) const [currPage, setCurrPage] = React.useState(0) + const [limit, setLimit] = React.useState(APP_PAGE_LIMIT) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) // Get the app type first @@ -67,7 +66,7 @@ const Logs: FC = ({ appDetail }) => { const query = { page: currPage + 1, - limit: APP_PAGE_LIMIT, + limit, ...((debouncedQueryParams.period !== '9') ? { start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), @@ -102,9 +101,9 @@ const Logs: FC = ({ appDetail }) => { const total = isChatMode ? chatConversations?.total : completionConversations?.total return ( -
-

{t('appLog.description')}

-
+
+

{t('appLog.description')}

+
{total === undefined ? @@ -115,35 +114,12 @@ const Logs: FC = ({ appDetail }) => { {/* Show Pagination only if the total is more than the limit */} {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + limit={limit} + onLimitChange={setLimit} + /> : null}
diff --git a/web/app/components/app/log/style.module.css b/web/app/components/app/log/style.module.css deleted file mode 100644 index adb32a39db..0000000000 --- a/web/app/components/app/log/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.pagination li { - list-style: none; -} diff --git a/web/app/components/app/workflow-log/index.tsx b/web/app/components/app/workflow-log/index.tsx index 7a891f5895..5a8fa41a14 100644 --- a/web/app/components/app/workflow-log/index.tsx +++ b/web/app/components/app/workflow-log/index.tsx @@ -3,14 +3,12 @@ import type { FC, SVGProps } from 'react' import React, { useState } from 'react' import useSWR from 'swr' import { usePathname } from 'next/navigation' -import { Pagination } from 'react-headless-pagination' import { useDebounce } from 'ahooks' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import { Trans, useTranslation } from 'react-i18next' import Link from 'next/link' import List from './list' import Filter from './filter' -import s from './style.module.css' +import Pagination from '@/app/components/base/pagination' import Loading from '@/app/components/base/loading' import { fetchWorkflowLogs } from '@/service/log' import { APP_PAGE_LIMIT } from '@/config' @@ -53,10 +51,11 @@ const Logs: FC = ({ appDetail }) => { const [queryParams, setQueryParams] = useState({ status: 'all' }) const [currPage, setCurrPage] = React.useState(0) const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) + const [limit, setLimit] = React.useState(APP_PAGE_LIMIT) const query = { page: currPage + 1, - limit: APP_PAGE_LIMIT, + limit, ...(debouncedQueryParams.status !== 'all' ? { status: debouncedQueryParams.status } : {}), ...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}), } @@ -89,35 +88,12 @@ const Logs: FC = ({ appDetail }) => { {/* Show Pagination only if the total is more than the limit */} {(total && total > APP_PAGE_LIMIT) ? - - - {t('appLog.table.pagination.previous')} - -
- -
- - {t('appLog.table.pagination.next')} - - -
+ current={currPage} + onChange={setCurrPage} + total={total} + limit={limit} + onLimitChange={setLimit} + /> : null}
diff --git a/web/app/components/app/workflow-log/list.tsx b/web/app/components/app/workflow-log/list.tsx index e43d95d5ad..e3de4a957f 100644 --- a/web/app/components/app/workflow-log/list.tsx +++ b/web/app/components/app/workflow-log/list.tsx @@ -2,9 +2,7 @@ import type { FC } from 'react' import React, { useState } from 'react' import { useTranslation } from 'react-i18next' -// import s from './style.module.css' import DetailPanel from './detail' -import cn from '@/utils/classnames' import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log' import type { App } from '@/types/app' import Loading from '@/app/components/base/loading' @@ -12,6 +10,7 @@ import Drawer from '@/app/components/base/drawer' import Indicator from '@/app/components/header/indicator' import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import useTimestamp from '@/hooks/use-timestamp' +import cn from '@/utils/classnames' type ILogs = { logs?: WorkflowLogsResponse diff --git a/web/app/components/app/workflow-log/style.module.css b/web/app/components/app/workflow-log/style.module.css deleted file mode 100644 index adb32a39db..0000000000 --- a/web/app/components/app/workflow-log/style.module.css +++ /dev/null @@ -1,3 +0,0 @@ -.pagination li { - list-style: none; -} diff --git a/web/app/components/base/pagination/hook.ts b/web/app/components/base/pagination/hook.ts new file mode 100644 index 0000000000..6501d6f457 --- /dev/null +++ b/web/app/components/base/pagination/hook.ts @@ -0,0 +1,95 @@ +import React, { useCallback } from 'react' +import type { IPaginationProps, IUsePagination } from './type' + +const usePagination = ({ + currentPage, + setCurrentPage, + truncableText = '...', + truncableClassName = '', + totalPages, + edgePageCount, + middlePagesSiblingCount, +}: IPaginationProps): IUsePagination => { + const pages = Array(totalPages) + .fill(0) + .map((_, i) => i + 1) + + const hasPreviousPage = currentPage > 1 + const hasNextPage = currentPage < totalPages + + const isReachedToFirst = currentPage <= middlePagesSiblingCount + const isReachedToLast = currentPage + middlePagesSiblingCount >= totalPages + + const middlePages = React.useMemo(() => { + const middlePageCount = middlePagesSiblingCount * 2 + 1 + if (isReachedToFirst) + return pages.slice(0, middlePageCount) + + if (isReachedToLast) + return pages.slice(-middlePageCount) + + return pages.slice( + currentPage - middlePagesSiblingCount, + currentPage + middlePagesSiblingCount + 1, + ) + }, [currentPage, isReachedToFirst, isReachedToLast, middlePagesSiblingCount, pages]) + + const getAllPreviousPages = useCallback(() => { + return pages.slice(0, middlePages[0] - 1) + }, [middlePages, pages]) + + const previousPages = React.useMemo(() => { + if (isReachedToFirst || getAllPreviousPages().length < 1) + return [] + + return pages + .slice(0, edgePageCount) + .filter(p => !middlePages.includes(p)) + }, [edgePageCount, getAllPreviousPages, isReachedToFirst, middlePages, pages]) + + const getAllNextPages = React.useMemo(() => { + return pages.slice( + middlePages[middlePages.length - 1], + pages[pages.length], + ) + }, [pages, middlePages]) + + const nextPages = React.useMemo(() => { + if (isReachedToLast) + return [] + + if (getAllNextPages.length < 1) + return [] + + return pages + .slice(pages.length - edgePageCount, pages.length) + .filter(p => !middlePages.includes(p)) + }, [edgePageCount, getAllNextPages.length, isReachedToLast, middlePages, pages]) + + const isPreviousTruncable = React.useMemo(() => { + // Is truncable if first value of middlePage is larger than last value of previousPages + return middlePages[0] > previousPages[previousPages.length - 1] + 1 + }, [previousPages, middlePages]) + + const isNextTruncable = React.useMemo(() => { + // Is truncable if last value of middlePage is larger than first value of previousPages + return middlePages[middlePages.length - 1] + 1 < nextPages[0] + }, [nextPages, middlePages]) + + return { + currentPage, + setCurrentPage, + truncableText, + truncableClassName, + pages, + hasPreviousPage, + hasNextPage, + previousPages, + isPreviousTruncable, + middlePages, + isNextTruncable, + nextPages, + } +} + +export default usePagination diff --git a/web/app/components/base/pagination/index.tsx b/web/app/components/base/pagination/index.tsx index f8c5684b55..b64c712425 100644 --- a/web/app/components/base/pagination/index.tsx +++ b/web/app/components/base/pagination/index.tsx @@ -1,50 +1,165 @@ import type { FC } from 'react' import React from 'react' -import { Pagination } from 'react-headless-pagination' -import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' import { useTranslation } from 'react-i18next' -import s from './style.module.css' +import { RiArrowLeftLine, RiArrowRightLine } from '@remixicon/react' +import { useDebounceFn } from 'ahooks' +import { Pagination } from './pagination' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import cn from '@/utils/classnames' type Props = { + className?: string current: number onChange: (cur: number) => void total: number limit?: number + onLimitChange?: (limit: number) => void } -const CustomizedPagination: FC = ({ current, onChange, total, limit = 10 }) => { +const CustomizedPagination: FC = ({ + className, + current, + onChange, + total, + limit = 10, + onLimitChange, +}) => { const { t } = useTranslation() const totalPages = Math.ceil(total / limit) + const inputRef = React.useRef(null) + const [showInput, setShowInput] = React.useState(false) + const [inputValue, setInputValue] = React.useState(current + 1) + const [showPerPageTip, setShowPerPageTip] = React.useState(false) + + const { run: handlePaging } = useDebounceFn((value: string) => { + if (parseInt(value) > totalPages) { + setInputValue(totalPages) + onChange(totalPages - 1) + setShowInput(false) + return + } + if (parseInt(value) < 1) { + setInputValue(1) + onChange(0) + setShowInput(false) + return + } + onChange(parseInt(value) - 1) + setInputValue(parseInt(value)) + setShowInput(false) + }, { wait: 500 }) + + const handleInputChange = (e: React.ChangeEvent) => { + const value = e.target.value + if (!value) + return setInputValue('') + if (isNaN(parseInt(value))) + return setInputValue('') + setInputValue(parseInt(value)) + handlePaging(value) + } + return ( - - - {t('appLog.table.pagination.previous')} - -
+
+
} + disabled={current === 0} + > + + + {!showInput && ( +
setShowInput(true)} + > +
{current + 1}
+
/
+
{totalPages}
+
+ )} + {showInput && ( + setShowInput(false)} + /> + )} +
} + disabled={current === totalPages - 1} + > + + +
+
- - {t('appLog.table.pagination.next')} - - + {onLimitChange && ( +
+
{showPerPageTip ? t('common.pagination.perPage') : ''}
+
setShowPerPageTip(true)} + onMouseLeave={() => setShowPerPageTip(false)} + > +
onLimitChange?.(10)} + >10
+
onLimitChange?.(25)} + >25
+
onLimitChange?.(50)} + >50
+
+
+ )} ) } diff --git a/web/app/components/base/pagination/pagination.tsx b/web/app/components/base/pagination/pagination.tsx new file mode 100644 index 0000000000..5898c4e924 --- /dev/null +++ b/web/app/components/base/pagination/pagination.tsx @@ -0,0 +1,189 @@ +import React from 'react' +import clsx from 'clsx' +import usePagination from './hook' +import type { + ButtonProps, + IPagination, + IPaginationProps, + PageButtonProps, +} from './type' + +const defaultState: IPagination = { + currentPage: 0, + setCurrentPage: () => {}, + truncableText: '...', + truncableClassName: '', + pages: [], + hasPreviousPage: false, + hasNextPage: false, + previousPages: [], + isPreviousTruncable: false, + middlePages: [], + isNextTruncable: false, + nextPages: [], +} + +const PaginationContext: React.Context = React.createContext(defaultState) + +export const PrevButton = ({ + className, + children, + dataTestId, + as =