| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useEffect, useState } from 'react' | import React, { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { Pagination } from 'react-headless-pagination' | |||||
| import { useDebounce } from 'ahooks' | import { useDebounce } from 'ahooks' | ||||
| import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' | |||||
| import Toast from '../../base/toast' | import Toast from '../../base/toast' | ||||
| import Filter from './filter' | import Filter from './filter' | ||||
| import type { QueryParam } from './filter' | import type { QueryParam } from './filter' | ||||
| import List from './list' | import List from './list' | ||||
| import EmptyElement from './empty-element' | import EmptyElement from './empty-element' | ||||
| import HeaderOpts from './header-opts' | import HeaderOpts from './header-opts' | ||||
| import s from './style.module.css' | |||||
| import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type' | import { AnnotationEnableStatus, type AnnotationItem, type AnnotationItemBasic, JobStatus } from './type' | ||||
| import ViewAnnotationModal from './view-annotation-modal' | import ViewAnnotationModal from './view-annotation-modal' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Pagination from '@/app/components/base/pagination' | |||||
| import Switch from '@/app/components/base/switch' | import Switch from '@/app/components/base/switch' | ||||
| import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation' | import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| const [queryParams, setQueryParams] = useState<QueryParam>({}) | const [queryParams, setQueryParams] = useState<QueryParam>({}) | ||||
| const [currPage, setCurrPage] = React.useState<number>(0) | const [currPage, setCurrPage] = React.useState<number>(0) | ||||
| const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | ||||
| const [limit, setLimit] = React.useState<number>(APP_PAGE_LIMIT) | |||||
| const query = { | const query = { | ||||
| page: currPage + 1, | page: currPage + 1, | ||||
| limit: APP_PAGE_LIMIT, | |||||
| limit, | |||||
| keyword: debouncedQueryParams.keyword || '', | keyword: debouncedQueryParams.keyword || '', | ||||
| } | } | ||||
| {/* Show Pagination only if the total is more than the limit */} | {/* Show Pagination only if the total is more than the limit */} | ||||
| {(total && total > APP_PAGE_LIMIT) | {(total && total > APP_PAGE_LIMIT) | ||||
| ? <Pagination | ? <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} | : null} | ||||
| {isShowViewModal && ( | {isShowViewModal && ( |
| .pagination li { | |||||
| list-style: none; | |||||
| } |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useEffect, useState } from 'react' | import React, { useEffect, useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | 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 EditItem, { EditItemType } from '../edit-annotation-modal/edit-item' | ||||
| import type { AnnotationItem, HitHistoryItem } from '../type' | import type { AnnotationItem, HitHistoryItem } from '../type' | ||||
| import s from './style.module.css' | import s from './style.module.css' | ||||
| import HitHistoryNoData from './hit-history-no-data' | import HitHistoryNoData from './hit-history-no-data' | ||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import Pagination from '@/app/components/base/pagination' | |||||
| import Drawer from '@/app/components/base/drawer-plus' | import Drawer from '@/app/components/base/drawer-plus' | ||||
| import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' | import { MessageCheckRemove } from '@/app/components/base/icons/src/vender/line/communication' | ||||
| import Confirm from '@/app/components/base/confirm' | import Confirm from '@/app/components/base/confirm' | ||||
| </table> | </table> | ||||
| {(total && total > APP_PAGE_LIMIT) | {(total && total > APP_PAGE_LIMIT) | ||||
| ? <Pagination | ? <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} | : null} | ||||
| </div> | </div> | ||||
| options={options} | 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.log && appDetail.mode !== 'workflow' && (<Log appDetail={appDetail} />)} | ||||
| {pageType === PageType.annotation && (<Annotation appDetail={appDetail} />)} | {pageType === PageType.annotation && (<Annotation appDetail={appDetail} />)} | ||||
| {pageType === PageType.log && appDetail.mode === 'workflow' && (<WorkflowLog appDetail={appDetail} />)} | {pageType === PageType.log && appDetail.mode === 'workflow' && (<WorkflowLog appDetail={appDetail} />)} |
| import type { FC, SVGProps } from 'react' | import type { FC, SVGProps } from 'react' | ||||
| import React, { useState } from 'react' | import React, { useState } from 'react' | ||||
| import useSWR from 'swr' | import useSWR from 'swr' | ||||
| import Link from 'next/link' | |||||
| import { usePathname } from 'next/navigation' | import { usePathname } from 'next/navigation' | ||||
| import { Pagination } from 'react-headless-pagination' | |||||
| import { useDebounce } from 'ahooks' | import { useDebounce } from 'ahooks' | ||||
| import { omit } from 'lodash-es' | import { omit } from 'lodash-es' | ||||
| import dayjs from 'dayjs' | import dayjs from 'dayjs' | ||||
| import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' | |||||
| import { Trans, useTranslation } from 'react-i18next' | import { Trans, useTranslation } from 'react-i18next' | ||||
| import Link from 'next/link' | |||||
| import List from './list' | import List from './list' | ||||
| import Filter, { TIME_PERIOD_MAPPING } from './filter' | 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 Loading from '@/app/components/base/loading' | ||||
| import { fetchChatConversations, fetchCompletionConversations } from '@/service/log' | import { fetchChatConversations, fetchCompletionConversations } from '@/service/log' | ||||
| import { APP_PAGE_LIMIT } from '@/config' | import { APP_PAGE_LIMIT } from '@/config' | ||||
| sort_by: '-created_at', | sort_by: '-created_at', | ||||
| }) | }) | ||||
| const [currPage, setCurrPage] = React.useState<number>(0) | const [currPage, setCurrPage] = React.useState<number>(0) | ||||
| const [limit, setLimit] = React.useState<number>(APP_PAGE_LIMIT) | |||||
| const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | ||||
| // Get the app type first | // Get the app type first | ||||
| const query = { | const query = { | ||||
| page: currPage + 1, | page: currPage + 1, | ||||
| limit: APP_PAGE_LIMIT, | |||||
| limit, | |||||
| ...((debouncedQueryParams.period !== '9') | ...((debouncedQueryParams.period !== '9') | ||||
| ? { | ? { | ||||
| start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), | start: dayjs().subtract(TIME_PERIOD_MAPPING[debouncedQueryParams.period].value, 'day').startOf('day').format('YYYY-MM-DD HH:mm'), | ||||
| const total = isChatMode ? chatConversations?.total : completionConversations?.total | const total = isChatMode ? chatConversations?.total : completionConversations?.total | ||||
| return ( | 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} /> | <Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} /> | ||||
| {total === undefined | {total === undefined | ||||
| ? <Loading type='app' /> | ? <Loading type='app' /> | ||||
| {/* Show Pagination only if the total is more than the limit */} | {/* Show Pagination only if the total is more than the limit */} | ||||
| {(total && total > APP_PAGE_LIMIT) | {(total && total > APP_PAGE_LIMIT) | ||||
| ? <Pagination | ? <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} | : null} | ||||
| </div> | </div> | ||||
| </div> | </div> |
| .pagination li { | |||||
| list-style: none; | |||||
| } |
| import React, { useState } from 'react' | import React, { useState } from 'react' | ||||
| import useSWR from 'swr' | import useSWR from 'swr' | ||||
| import { usePathname } from 'next/navigation' | import { usePathname } from 'next/navigation' | ||||
| import { Pagination } from 'react-headless-pagination' | |||||
| import { useDebounce } from 'ahooks' | import { useDebounce } from 'ahooks' | ||||
| import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline' | |||||
| import { Trans, useTranslation } from 'react-i18next' | import { Trans, useTranslation } from 'react-i18next' | ||||
| import Link from 'next/link' | import Link from 'next/link' | ||||
| import List from './list' | import List from './list' | ||||
| import Filter from './filter' | 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 Loading from '@/app/components/base/loading' | ||||
| import { fetchWorkflowLogs } from '@/service/log' | import { fetchWorkflowLogs } from '@/service/log' | ||||
| import { APP_PAGE_LIMIT } from '@/config' | import { APP_PAGE_LIMIT } from '@/config' | ||||
| const [queryParams, setQueryParams] = useState<QueryParam>({ status: 'all' }) | const [queryParams, setQueryParams] = useState<QueryParam>({ status: 'all' }) | ||||
| const [currPage, setCurrPage] = React.useState<number>(0) | const [currPage, setCurrPage] = React.useState<number>(0) | ||||
| const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | const debouncedQueryParams = useDebounce(queryParams, { wait: 500 }) | ||||
| const [limit, setLimit] = React.useState<number>(APP_PAGE_LIMIT) | |||||
| const query = { | const query = { | ||||
| page: currPage + 1, | page: currPage + 1, | ||||
| limit: APP_PAGE_LIMIT, | |||||
| limit, | |||||
| ...(debouncedQueryParams.status !== 'all' ? { status: debouncedQueryParams.status } : {}), | ...(debouncedQueryParams.status !== 'all' ? { status: debouncedQueryParams.status } : {}), | ||||
| ...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}), | ...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}), | ||||
| } | } | ||||
| {/* Show Pagination only if the total is more than the limit */} | {/* Show Pagination only if the total is more than the limit */} | ||||
| {(total && total > APP_PAGE_LIMIT) | {(total && total > APP_PAGE_LIMIT) | ||||
| ? <Pagination | ? <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} | : null} | ||||
| </div> | </div> | ||||
| </div> | </div> |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React, { useState } from 'react' | import React, { useState } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| // import s from './style.module.css' | |||||
| import DetailPanel from './detail' | import DetailPanel from './detail' | ||||
| import cn from '@/utils/classnames' | |||||
| import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log' | import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log' | ||||
| import type { App } from '@/types/app' | import type { App } from '@/types/app' | ||||
| import Loading from '@/app/components/base/loading' | import Loading from '@/app/components/base/loading' | ||||
| import Indicator from '@/app/components/header/indicator' | import Indicator from '@/app/components/header/indicator' | ||||
| import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' | ||||
| import useTimestamp from '@/hooks/use-timestamp' | import useTimestamp from '@/hooks/use-timestamp' | ||||
| import cn from '@/utils/classnames' | |||||
| type ILogs = { | type ILogs = { | ||||
| logs?: WorkflowLogsResponse | logs?: WorkflowLogsResponse |
| .pagination li { | |||||
| list-style: none; | |||||
| } |
| 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 |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import React 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 { 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 = { | type Props = { | ||||
| className?: string | |||||
| current: number | current: number | ||||
| onChange: (cur: number) => void | onChange: (cur: number) => void | ||||
| total: number | total: number | ||||
| limit?: 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 { t } = useTranslation() | ||||
| const totalPages = Math.ceil(total / limit) | 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 ( | return ( | ||||
| <Pagination | <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} | currentPage={current} | ||||
| edgePageCount={2} | edgePageCount={2} | ||||
| middlePagesSiblingCount={1} | middlePagesSiblingCount={1} | ||||
| setCurrentPage={onChange} | setCurrentPage={onChange} | ||||
| totalPages={totalPages} | 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 | <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> | </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> | </Pagination> | ||||
| ) | ) | ||||
| } | } |
| 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 |
| .pagination li { | |||||
| list-style: none; | |||||
| } |
| 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, | |||||
| } |
| expiring: 'Expiring in one day', | expiring: 'Expiring in one day', | ||||
| expiring_plural: 'Expiring in {{count}} days', | expiring_plural: 'Expiring in {{count}} days', | ||||
| }, | }, | ||||
| pagination: { | |||||
| perPage: 'Items per page', | |||||
| }, | |||||
| } | } | ||||
| export default translation | export default translation |
| expiring: '许可证还有 1 天到期', | expiring: '许可证还有 1 天到期', | ||||
| expiring_plural: '许可证还有 {{count}} 天到期', | expiring_plural: '许可证还有 {{count}} 天到期', | ||||
| }, | }, | ||||
| pagination: { | |||||
| perPage: '每页显示', | |||||
| }, | |||||
| } | } | ||||
| export default translation | export default translation |
| "react-dom": "~18.2.0", | "react-dom": "~18.2.0", | ||||
| "react-easy-crop": "^5.0.8", | "react-easy-crop": "^5.0.8", | ||||
| "react-error-boundary": "^4.0.2", | "react-error-boundary": "^4.0.2", | ||||
| "react-headless-pagination": "^1.1.4", | |||||
| "react-hook-form": "^7.51.4", | "react-hook-form": "^7.51.4", | ||||
| "react-i18next": "^12.2.0", | "react-i18next": "^12.2.0", | ||||
| "react-infinite-scroll-component": "^6.1.0", | "react-infinite-scroll-component": "^6.1.0", |
| dependencies: | dependencies: | ||||
| "@babel/runtime" "^7.12.5" | "@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: | react-hook-form@^7.51.4: | ||||
| version "7.51.4" | version "7.51.4" | ||||
| resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz" | resolved "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz" |