| @@ -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<Props> = ({ | |||
| const [queryParams, setQueryParams] = useState<QueryParam>({}) | |||
| const [currPage, setCurrPage] = React.useState<number>(0) | |||
| const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | |||
| const [limit, setLimit] = React.useState<number>(APP_PAGE_LIMIT) | |||
| const query = { | |||
| page: currPage + 1, | |||
| limit: APP_PAGE_LIMIT, | |||
| limit, | |||
| keyword: debouncedQueryParams.keyword || '', | |||
| } | |||
| @@ -228,35 +227,12 @@ const Annotation: FC<Props> = ({ | |||
| {/* Show Pagination only if the total is more than the limit */} | |||
| {(total && total > APP_PAGE_LIMIT) | |||
| ? <Pagination | |||
| className="flex items-center w-full h-10 text-sm select-none mt-8" | |||
| currentPage={currPage} | |||
| edgePageCount={2} | |||
| middlePagesSiblingCount={1} | |||
| setCurrentPage={setCurrPage} | |||
| totalPages={Math.ceil(total / APP_PAGE_LIMIT)} | |||
| truncableClassName="w-8 px-0.5 text-center" | |||
| truncableText="..." | |||
| > | |||
| <Pagination.PrevButton | |||
| disabled={currPage === 0} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| <ArrowLeftIcon className="mr-3 h-3 w-3" /> | |||
| {t('appLog.table.pagination.previous')} | |||
| </Pagination.PrevButton> | |||
| <div className={`flex items-center justify-center flex-grow ${s.pagination}`}> | |||
| <Pagination.PageButton | |||
| activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white" | |||
| className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer" | |||
| inactiveClassName="text-gray-500" | |||
| /> | |||
| </div> | |||
| <Pagination.NextButton | |||
| disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| {t('appLog.table.pagination.next')} | |||
| <ArrowRightIcon className="ml-3 h-3 w-3" /> | |||
| </Pagination.NextButton> | |||
| </Pagination> | |||
| current={currPage} | |||
| onChange={setCurrPage} | |||
| total={total} | |||
| limit={limit} | |||
| onLimitChange={setLimit} | |||
| /> | |||
| : null} | |||
| {isShowViewModal && ( | |||
| @@ -1,3 +0,0 @@ | |||
| .pagination li { | |||
| list-style: none; | |||
| } | |||
| @@ -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<Props> = ({ | |||
| </table> | |||
| {(total && total > APP_PAGE_LIMIT) | |||
| ? <Pagination | |||
| className="flex items-center w-full h-10 text-sm select-none mt-8" | |||
| currentPage={currPage} | |||
| edgePageCount={2} | |||
| middlePagesSiblingCount={1} | |||
| setCurrentPage={setCurrPage} | |||
| totalPages={Math.ceil(total / APP_PAGE_LIMIT)} | |||
| truncatableClassName="w-8 px-0.5 text-center" | |||
| truncatableText="..." | |||
| > | |||
| <Pagination.PrevButton | |||
| disabled={currPage === 0} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| <ArrowLeftIcon className="mr-3 h-3 w-3" /> | |||
| {t('appLog.table.pagination.previous')} | |||
| </Pagination.PrevButton> | |||
| <div className={`flex items-center justify-center flex-grow ${s.pagination}`}> | |||
| <Pagination.PageButton | |||
| activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white" | |||
| className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer" | |||
| inactiveClassName="text-gray-500" | |||
| /> | |||
| </div> | |||
| <Pagination.NextButton | |||
| disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| {t('appLog.table.pagination.next')} | |||
| <ArrowRightIcon className="ml-3 h-3 w-3" /> | |||
| </Pagination.NextButton> | |||
| </Pagination> | |||
| current={currPage} | |||
| onChange={setCurrPage} | |||
| total={total} | |||
| /> | |||
| : null} | |||
| </div> | |||
| @@ -52,7 +52,7 @@ const LogAnnotation: FC<Props> = ({ | |||
| options={options} | |||
| /> | |||
| )} | |||
| <div className={cn('grow', appDetail.mode !== 'workflow' && 'mt-3')}> | |||
| <div className={cn('grow h-0', appDetail.mode !== 'workflow' && 'mt-3')}> | |||
| {pageType === PageType.log && appDetail.mode !== 'workflow' && (<Log appDetail={appDetail} />)} | |||
| {pageType === PageType.annotation && (<Annotation appDetail={appDetail} />)} | |||
| {pageType === PageType.log && appDetail.mode === 'workflow' && (<WorkflowLog appDetail={appDetail} />)} | |||
| @@ -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<ILogsProps> = ({ appDetail }) => { | |||
| sort_by: '-created_at', | |||
| }) | |||
| const [currPage, setCurrPage] = React.useState<number>(0) | |||
| const [limit, setLimit] = React.useState<number>(APP_PAGE_LIMIT) | |||
| const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | |||
| // Get the app type first | |||
| @@ -67,7 +66,7 @@ const Logs: FC<ILogsProps> = ({ 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<ILogsProps> = ({ appDetail }) => { | |||
| const total = isChatMode ? chatConversations?.total : completionConversations?.total | |||
| return ( | |||
| <div className='flex flex-col h-full'> | |||
| <p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p> | |||
| <div className='flex flex-col py-4 flex-1'> | |||
| <div className='grow flex flex-col h-full'> | |||
| <p className='shrink-0 text-text-tertiary system-sm-regular'>{t('appLog.description')}</p> | |||
| <div className='grow max-h-[calc(100%-16px)] flex flex-col py-4 flex-1'> | |||
| <Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} /> | |||
| {total === undefined | |||
| ? <Loading type='app' /> | |||
| @@ -115,35 +114,12 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => { | |||
| {/* Show Pagination only if the total is more than the limit */} | |||
| {(total && total > APP_PAGE_LIMIT) | |||
| ? <Pagination | |||
| className="flex items-center w-full h-10 text-sm select-none mt-8" | |||
| currentPage={currPage} | |||
| edgePageCount={2} | |||
| middlePagesSiblingCount={1} | |||
| setCurrentPage={setCurrPage} | |||
| totalPages={Math.ceil(total / APP_PAGE_LIMIT)} | |||
| truncableClassName="w-8 px-0.5 text-center" | |||
| truncableText="..." | |||
| > | |||
| <Pagination.PrevButton | |||
| disabled={currPage === 0} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| <ArrowLeftIcon className="mr-3 h-3 w-3" /> | |||
| {t('appLog.table.pagination.previous')} | |||
| </Pagination.PrevButton> | |||
| <div className={`flex items-center justify-center flex-grow ${s.pagination}`}> | |||
| <Pagination.PageButton | |||
| activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white" | |||
| className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer" | |||
| inactiveClassName="text-gray-500" | |||
| /> | |||
| </div> | |||
| <Pagination.NextButton | |||
| disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| {t('appLog.table.pagination.next')} | |||
| <ArrowRightIcon className="ml-3 h-3 w-3" /> | |||
| </Pagination.NextButton> | |||
| </Pagination> | |||
| current={currPage} | |||
| onChange={setCurrPage} | |||
| total={total} | |||
| limit={limit} | |||
| onLimitChange={setLimit} | |||
| /> | |||
| : null} | |||
| </div> | |||
| </div> | |||
| @@ -1,3 +0,0 @@ | |||
| .pagination li { | |||
| list-style: none; | |||
| } | |||
| @@ -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<ILogsProps> = ({ appDetail }) => { | |||
| const [queryParams, setQueryParams] = useState<QueryParam>({ status: 'all' }) | |||
| const [currPage, setCurrPage] = React.useState<number>(0) | |||
| const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | |||
| const [limit, setLimit] = React.useState<number>(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<ILogsProps> = ({ appDetail }) => { | |||
| {/* Show Pagination only if the total is more than the limit */} | |||
| {(total && total > APP_PAGE_LIMIT) | |||
| ? <Pagination | |||
| className="flex items-center w-full h-10 text-sm select-none mt-8" | |||
| currentPage={currPage} | |||
| edgePageCount={2} | |||
| middlePagesSiblingCount={1} | |||
| setCurrentPage={setCurrPage} | |||
| totalPages={Math.ceil(total / APP_PAGE_LIMIT)} | |||
| truncableClassName="w-8 px-0.5 text-center" | |||
| truncableText="..." | |||
| > | |||
| <Pagination.PrevButton | |||
| disabled={currPage === 0} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| <ArrowLeftIcon className="mr-3 h-3 w-3" /> | |||
| {t('appLog.table.pagination.previous')} | |||
| </Pagination.PrevButton> | |||
| <div className={`flex items-center justify-center flex-grow ${s.pagination}`}> | |||
| <Pagination.PageButton | |||
| activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white" | |||
| className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer" | |||
| inactiveClassName="text-gray-500" | |||
| /> | |||
| </div> | |||
| <Pagination.NextButton | |||
| disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600 dark:hover:text-gray-200'}`} > | |||
| {t('appLog.table.pagination.next')} | |||
| <ArrowRightIcon className="ml-3 h-3 w-3" /> | |||
| </Pagination.NextButton> | |||
| </Pagination> | |||
| current={currPage} | |||
| onChange={setCurrPage} | |||
| total={total} | |||
| limit={limit} | |||
| onLimitChange={setLimit} | |||
| /> | |||
| : null} | |||
| </div> | |||
| </div> | |||
| @@ -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 | |||
| @@ -1,3 +0,0 @@ | |||
| .pagination li { | |||
| list-style: none; | |||
| } | |||
| @@ -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 | |||
| @@ -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<Props> = ({ current, onChange, total, limit = 10 }) => { | |||
| const CustomizedPagination: FC<Props> = ({ | |||
| className, | |||
| current, | |||
| onChange, | |||
| total, | |||
| limit = 10, | |||
| onLimitChange, | |||
| }) => { | |||
| const { t } = useTranslation() | |||
| const totalPages = Math.ceil(total / limit) | |||
| const inputRef = React.useRef<HTMLDivElement>(null) | |||
| const [showInput, setShowInput] = React.useState(false) | |||
| const [inputValue, setInputValue] = React.useState<string | number>(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<HTMLInputElement>) => { | |||
| const value = e.target.value | |||
| if (!value) | |||
| return setInputValue('') | |||
| if (isNaN(parseInt(value))) | |||
| return setInputValue('') | |||
| setInputValue(parseInt(value)) | |||
| handlePaging(value) | |||
| } | |||
| return ( | |||
| <Pagination | |||
| className="flex items-center w-full h-10 text-sm select-none mt-8" | |||
| className={cn('flex items-center w-full px-6 py-3 select-none', className)} | |||
| currentPage={current} | |||
| edgePageCount={2} | |||
| middlePagesSiblingCount={1} | |||
| setCurrentPage={onChange} | |||
| totalPages={totalPages} | |||
| truncatableClassName="w-8 px-0.5 text-center" | |||
| truncatableText="..." | |||
| truncableClassName='flex items-center justify-center w-8 px-1 py-2 system-sm-medium text-text-tertiary' | |||
| truncableText='...' | |||
| > | |||
| <Pagination.PrevButton | |||
| disabled={current === 0} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${current === 0 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600'}`} > | |||
| <ArrowLeftIcon className="mr-3 h-3 w-3" /> | |||
| {t('appLog.table.pagination.previous')} | |||
| </Pagination.PrevButton> | |||
| <div className={`flex items-center justify-center flex-grow ${s.pagination}`}> | |||
| <div className='flex items-center gap-0.5 p-0.5 rounded-[10px] bg-background-section-burn'> | |||
| <Pagination.PrevButton | |||
| as={<div></div>} | |||
| disabled={current === 0} | |||
| > | |||
| <Button | |||
| variant='secondary' | |||
| className='w-7 h-7 px-1.5' | |||
| disabled={current === 0} | |||
| > | |||
| <RiArrowLeftLine className='h-4 w-4' /> | |||
| </Button> | |||
| </Pagination.PrevButton> | |||
| {!showInput && ( | |||
| <div | |||
| ref={inputRef} | |||
| className='flex items-center gap-0.5 px-2 py-1.5 rounded-lg hover:bg-state-base-hover-alt hover:cursor-text' | |||
| onClick={() => setShowInput(true)} | |||
| > | |||
| <div className='system-xs-medium text-text-secondary'>{current + 1}</div> | |||
| <div className='system-xs-medium text-text-quaternary'>/</div> | |||
| <div className='system-xs-medium text-text-secondary'>{totalPages}</div> | |||
| </div> | |||
| )} | |||
| {showInput && ( | |||
| <Input | |||
| styleCss={{ | |||
| height: '28px', | |||
| width: `${inputRef.current?.clientWidth}px`, | |||
| }} | |||
| placeholder='' | |||
| autoFocus | |||
| value={inputValue} | |||
| onChange={handleInputChange} | |||
| onBlur={() => setShowInput(false)} | |||
| /> | |||
| )} | |||
| <Pagination.NextButton | |||
| as={<div></div>} | |||
| disabled={current === totalPages - 1} | |||
| > | |||
| <Button | |||
| variant='secondary' | |||
| className='w-7 h-7 px-1.5' | |||
| disabled={current === totalPages - 1} | |||
| > | |||
| <RiArrowRightLine className='h-4 w-4' /> | |||
| </Button> | |||
| </Pagination.NextButton> | |||
| </div> | |||
| <div className={cn('grow flex items-center justify-center gap-1 list-none')}> | |||
| <Pagination.PageButton | |||
| activeClassName="bg-primary-50 text-primary-600" | |||
| className="flex items-center justify-center h-8 w-8 rounded-lg cursor-pointer" | |||
| inactiveClassName="text-gray-500" | |||
| className='flex items-center justify-center min-w-8 px-1 py-2 rounded-lg system-sm-medium cursor-pointer hover:bg-components-button-ghost-bg-hover' | |||
| activeClassName='bg-components-button-tertiary-bg text-components-button-tertiary-text hover:bg-components-button-ghost-bg-hover' | |||
| inactiveClassName='text-text-tertiary' | |||
| /> | |||
| </div> | |||
| <Pagination.NextButton | |||
| disabled={current === totalPages - 1} | |||
| className={`flex items-center mr-2 text-gray-500 focus:outline-none ${current === totalPages - 1 ? 'cursor-not-allowed opacity-50' : 'cursor-pointer hover:text-gray-600'}`} > | |||
| {t('appLog.table.pagination.next')} | |||
| <ArrowRightIcon className="ml-3 h-3 w-3" /> | |||
| </Pagination.NextButton> | |||
| {onLimitChange && ( | |||
| <div className='shrink-0 flex items-center gap-2'> | |||
| <div className='shrink-0 w-[51px] text-end text-text-tertiary system-2xs-regular-uppercase'>{showPerPageTip ? t('common.pagination.perPage') : ''}</div> | |||
| <div | |||
| className='flex items-center gap-[1px] p-0.5 rounded-[10px] bg-components-segmented-control-bg-normal' | |||
| onMouseEnter={() => setShowPerPageTip(true)} | |||
| onMouseLeave={() => setShowPerPageTip(false)} | |||
| > | |||
| <div | |||
| className={cn( | |||
| 'px-2.5 py-1.5 rounded-lg border-[0.5px] border-transparent system-sm-medium text-text-tertiary cursor-pointer hover:bg-state-base-hover hover:text-text-secondary', | |||
| limit === 10 && 'shadow-xs border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-secondary hover:bg-components-segmented-control-item-active-bg', | |||
| )} | |||
| onClick={() => onLimitChange?.(10)} | |||
| >10</div> | |||
| <div | |||
| className={cn( | |||
| 'px-2.5 py-1.5 rounded-lg border-[0.5px] border-transparent system-sm-medium text-text-tertiary cursor-pointer hover:bg-state-base-hover hover:text-text-secondary', | |||
| limit === 25 && 'shadow-xs border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-secondary hover:bg-components-segmented-control-item-active-bg', | |||
| )} | |||
| onClick={() => onLimitChange?.(25)} | |||
| >25</div> | |||
| <div | |||
| className={cn( | |||
| 'px-2.5 py-1.5 rounded-lg border-[0.5px] border-transparent system-sm-medium text-text-tertiary cursor-pointer hover:bg-state-base-hover hover:text-text-secondary', | |||
| limit === 50 && 'shadow-xs border-components-segmented-control-item-active-border bg-components-segmented-control-item-active-bg text-text-secondary hover:bg-components-segmented-control-item-active-bg', | |||
| )} | |||
| onClick={() => onLimitChange?.(50)} | |||
| >50</div> | |||
| </div> | |||
| </div> | |||
| )} | |||
| </Pagination> | |||
| ) | |||
| } | |||
| @@ -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<IPagination> = React.createContext<IPagination>(defaultState) | |||
| export const PrevButton = ({ | |||
| className, | |||
| children, | |||
| dataTestId, | |||
| as = <button />, | |||
| ...buttonProps | |||
| }: ButtonProps) => { | |||
| const pagination = React.useContext(PaginationContext) | |||
| const previous = () => { | |||
| if (pagination.currentPage + 1 > 1) | |||
| pagination.setCurrentPage(pagination.currentPage - 1) | |||
| } | |||
| const disabled = pagination.currentPage === 0 | |||
| return ( | |||
| <as.type | |||
| {...buttonProps} | |||
| {...as.props} | |||
| className={clsx(className, as.props.className)} | |||
| onClick={() => previous()} | |||
| tabIndex={disabled ? '-1' : 0} | |||
| disabled={disabled} | |||
| data-testid={dataTestId} | |||
| onKeyPress={(event: React.KeyboardEvent) => { | |||
| event.preventDefault() | |||
| if (event.key === 'Enter' && !disabled) | |||
| previous() | |||
| }} | |||
| > | |||
| {as.props.children ?? children} | |||
| </as.type> | |||
| ) | |||
| } | |||
| export const NextButton = ({ | |||
| className, | |||
| children, | |||
| dataTestId, | |||
| as = <button />, | |||
| ...buttonProps | |||
| }: ButtonProps) => { | |||
| const pagination = React.useContext(PaginationContext) | |||
| const next = () => { | |||
| if (pagination.currentPage + 1 < pagination.pages.length) | |||
| pagination.setCurrentPage(pagination.currentPage + 1) | |||
| } | |||
| const disabled = pagination.currentPage === pagination.pages.length - 1 | |||
| return ( | |||
| <as.type | |||
| {...buttonProps} | |||
| {...as.props} | |||
| className={clsx(className, as.props.className)} | |||
| onClick={() => next()} | |||
| tabIndex={disabled ? '-1' : 0} | |||
| disabled={disabled} | |||
| data-testid={dataTestId} | |||
| onKeyPress={(event: React.KeyboardEvent) => { | |||
| event.preventDefault() | |||
| if (event.key === 'Enter' && !disabled) | |||
| next() | |||
| }} | |||
| > | |||
| {as.props.children ?? children} | |||
| </as.type> | |||
| ) | |||
| } | |||
| type ITruncableElementProps = { | |||
| prev?: boolean | |||
| } | |||
| const TruncableElement = ({ prev }: ITruncableElementProps) => { | |||
| const pagination: IPagination = React.useContext(PaginationContext) | |||
| const { | |||
| isPreviousTruncable, | |||
| isNextTruncable, | |||
| truncableText, | |||
| truncableClassName, | |||
| } = pagination | |||
| return ((isPreviousTruncable && prev === true) || (isNextTruncable && !prev)) | |||
| ? ( | |||
| <li className={truncableClassName || undefined}>{truncableText}</li> | |||
| ) | |||
| : null | |||
| } | |||
| export const PageButton = ({ | |||
| as = <a />, | |||
| className, | |||
| dataTestIdActive, | |||
| dataTestIdInactive, | |||
| activeClassName, | |||
| inactiveClassName, | |||
| renderExtraProps, | |||
| }: PageButtonProps) => { | |||
| const pagination: IPagination = React.useContext(PaginationContext) | |||
| const renderPageButton = (page: number) => ( | |||
| <li key={page}> | |||
| <as.type | |||
| data-testid={ | |||
| clsx({ | |||
| [`${dataTestIdActive}`]: | |||
| dataTestIdActive && pagination.currentPage + 1 === page, | |||
| [`${dataTestIdInactive}-${page}`]: | |||
| dataTestIdActive && pagination.currentPage + 1 !== page, | |||
| }) || undefined | |||
| } | |||
| tabIndex={0} | |||
| onKeyPress={(event: React.KeyboardEvent) => { | |||
| if (event.key === 'Enter') | |||
| pagination.setCurrentPage(page - 1) | |||
| }} | |||
| onClick={() => pagination.setCurrentPage(page - 1)} | |||
| className={clsx( | |||
| className, | |||
| pagination.currentPage + 1 === page | |||
| ? activeClassName | |||
| : inactiveClassName, | |||
| )} | |||
| {...as.props} | |||
| {...(renderExtraProps ? renderExtraProps(page) : {})} | |||
| > | |||
| {page} | |||
| </as.type> | |||
| </li> | |||
| ) | |||
| return ( | |||
| <> | |||
| {pagination.previousPages.map(renderPageButton)} | |||
| <TruncableElement prev /> | |||
| {pagination.middlePages.map(renderPageButton)} | |||
| <TruncableElement /> | |||
| {pagination.nextPages.map(renderPageButton)} | |||
| </> | |||
| ) | |||
| } | |||
| export const Pagination = ({ | |||
| dataTestId, | |||
| ...paginationProps | |||
| }: IPaginationProps & { dataTestId?: string }) => { | |||
| const pagination = usePagination(paginationProps) | |||
| return ( | |||
| <PaginationContext.Provider value={pagination}> | |||
| <div className={paginationProps.className} data-testid={dataTestId}> | |||
| {paginationProps.children} | |||
| </div> | |||
| </PaginationContext.Provider> | |||
| ) | |||
| } | |||
| Pagination.PrevButton = PrevButton | |||
| Pagination.NextButton = NextButton | |||
| Pagination.PageButton = PageButton | |||
| @@ -1,3 +0,0 @@ | |||
| .pagination li { | |||
| list-style: none; | |||
| } | |||
| @@ -0,0 +1,58 @@ | |||
| import type { ButtonHTMLAttributes } from 'react' | |||
| type IBasePaginationProps = { | |||
| currentPage: number | |||
| setCurrentPage: (page: number) => void | |||
| truncableText?: string | |||
| truncableClassName?: string | |||
| } | |||
| type IPaginationProps = IBasePaginationProps & { | |||
| totalPages: number | |||
| edgePageCount: number | |||
| middlePagesSiblingCount: number | |||
| className?: string | |||
| children?: React.ReactNode | |||
| } | |||
| type IUsePagination = IBasePaginationProps & { | |||
| pages: number[] | |||
| hasPreviousPage: boolean | |||
| hasNextPage: boolean | |||
| previousPages: number[] | |||
| isPreviousTruncable: boolean | |||
| middlePages: number[] | |||
| isNextTruncable: boolean | |||
| nextPages: number[] | |||
| } | |||
| type IPagination = IUsePagination & { | |||
| setCurrentPage: (page: number) => void | |||
| } | |||
| type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & { | |||
| as?: React.ReactElement | |||
| children?: string | React.ReactNode | |||
| className?: string | |||
| dataTestId?: string | |||
| } | |||
| type PageButtonProps = ButtonProps & { | |||
| /** | |||
| * Provide a custom ReactElement (e.g. Next/Link) | |||
| */ | |||
| as?: React.ReactElement | |||
| activeClassName?: string | |||
| inactiveClassName?: string | |||
| dataTestIdActive?: string | |||
| dataTestIdInactive?: string | |||
| renderExtraProps?: (pageNum: number) => {} | |||
| } | |||
| export type { | |||
| IPaginationProps, | |||
| IUsePagination, | |||
| IPagination, | |||
| ButtonProps, | |||
| PageButtonProps, | |||
| } | |||
| @@ -595,6 +595,9 @@ const translation = { | |||
| expiring: 'Expiring in one day', | |||
| expiring_plural: 'Expiring in {{count}} days', | |||
| }, | |||
| pagination: { | |||
| perPage: 'Items per page', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -595,6 +595,9 @@ const translation = { | |||
| expiring: '许可证还有 1 天到期', | |||
| expiring_plural: '许可证还有 {{count}} 天到期', | |||
| }, | |||
| pagination: { | |||
| perPage: '每页显示', | |||
| }, | |||
| } | |||
| export default translation | |||
| @@ -77,7 +77,6 @@ | |||
| "react-dom": "~18.2.0", | |||
| "react-easy-crop": "^5.0.8", | |||
| "react-error-boundary": "^4.0.2", | |||
| "react-headless-pagination": "^1.1.4", | |||
| "react-hook-form": "^7.51.4", | |||
| "react-i18next": "^12.2.0", | |||
| "react-infinite-scroll-component": "^6.1.0", | |||
| @@ -11665,13 +11665,6 @@ react-error-boundary@^4.0.2: | |||
| dependencies: | |||
| "@babel/runtime" "^7.12.5" | |||
| react-headless-pagination@^1.1.4: | |||
| version "1.1.4" | |||
| resolved "https://registry.npmjs.org/react-headless-pagination/-/react-headless-pagination-1.1.4.tgz" | |||
| integrity sha512-Z5d55g3gM2BQMvHJUGm1jbbQ5Bgtq54kNlI5ca1NTwdVR8ZNunN0EdOtNKNobsFRKuZGkQ24VTIu6ulNq190Iw== | |||
| dependencies: | |||
| classnames "2.3.1" | |||
| react-hook-form@^7.51.4: | |||
| version "7.51.4" | |||
| resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz" | |||