index.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. 'use client'
  2. import type { FC, SVGProps } from 'react'
  3. import React, { useState } from 'react'
  4. import useSWR from 'swr'
  5. import { usePathname } from 'next/navigation'
  6. import { Pagination } from 'react-headless-pagination'
  7. import { useDebounce } from 'ahooks'
  8. import { ArrowLeftIcon, ArrowRightIcon } from '@heroicons/react/24/outline'
  9. import { Trans, useTranslation } from 'react-i18next'
  10. import Link from 'next/link'
  11. import List from './list'
  12. import Filter from './filter'
  13. import s from './style.module.css'
  14. import Loading from '@/app/components/base/loading'
  15. import { fetchWorkflowLogs } from '@/service/log'
  16. import { APP_PAGE_LIMIT } from '@/config'
  17. import type { App, AppMode } from '@/types/app'
  18. export type ILogsProps = {
  19. appDetail: App
  20. }
  21. export type QueryParam = {
  22. status?: string
  23. keyword?: string
  24. }
  25. const ThreeDotsIcon = ({ className }: SVGProps<SVGElement>) => {
  26. return <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className={className ?? ''}>
  27. <path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
  28. </svg>
  29. }
  30. const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
  31. const { t } = useTranslation()
  32. const pathname = usePathname()
  33. const pathSegments = pathname.split('/')
  34. pathSegments.pop()
  35. return <div className='flex items-center justify-center h-full'>
  36. <div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
  37. <span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
  38. <div className='mt-2 text-gray-500 text-sm font-normal'>
  39. <Trans
  40. i18nKey="appLog.table.empty.element.content"
  41. components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
  42. />
  43. </div>
  44. </div>
  45. </div>
  46. }
  47. const Logs: FC<ILogsProps> = ({ appDetail }) => {
  48. const { t } = useTranslation()
  49. const [queryParams, setQueryParams] = useState<QueryParam>({ status: 'all' })
  50. const [currPage, setCurrPage] = React.useState<number>(0)
  51. const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })
  52. const query = {
  53. page: currPage + 1,
  54. limit: APP_PAGE_LIMIT,
  55. ...(debouncedQueryParams.status !== 'all' ? { status: debouncedQueryParams.status } : {}),
  56. ...(debouncedQueryParams.keyword ? { keyword: debouncedQueryParams.keyword } : {}),
  57. }
  58. const getWebAppType = (appType: AppMode) => {
  59. if (appType !== 'completion' && appType !== 'workflow')
  60. return 'chat'
  61. return appType
  62. }
  63. const { data: workflowLogs, mutate } = useSWR({
  64. url: `/apps/${appDetail.id}/workflow-app-logs`,
  65. params: query,
  66. }, fetchWorkflowLogs)
  67. const total = workflowLogs?.total
  68. return (
  69. <div className='flex flex-col h-full'>
  70. <h1 className='text-md font-semibold text-gray-900'>{t('appLog.workflowTitle')}</h1>
  71. <p className='flex text-sm font-normal text-gray-500'>{t('appLog.workflowSubtitle')}</p>
  72. <div className='flex flex-col py-4 flex-1'>
  73. <Filter queryParams={queryParams} setQueryParams={setQueryParams} />
  74. {/* workflow log */}
  75. {total === undefined
  76. ? <Loading type='app' />
  77. : total > 0
  78. ? <List logs={workflowLogs} appDetail={appDetail} onRefresh={mutate} />
  79. : <EmptyElement appUrl={`${appDetail.site.app_base_url}/${getWebAppType(appDetail.mode)}/${appDetail.site.access_token}`} />
  80. }
  81. {/* Show Pagination only if the total is more than the limit */}
  82. {(total && total > APP_PAGE_LIMIT)
  83. ? <Pagination
  84. className="flex items-center w-full h-10 text-sm select-none mt-8"
  85. currentPage={currPage}
  86. edgePageCount={2}
  87. middlePagesSiblingCount={1}
  88. setCurrentPage={setCurrPage}
  89. totalPages={Math.ceil(total / APP_PAGE_LIMIT)}
  90. truncableClassName="w-8 px-0.5 text-center"
  91. truncableText="..."
  92. >
  93. <Pagination.PrevButton
  94. disabled={currPage === 0}
  95. 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'}`} >
  96. <ArrowLeftIcon className="mr-3 h-3 w-3" />
  97. {t('appLog.table.pagination.previous')}
  98. </Pagination.PrevButton>
  99. <div className={`flex items-center justify-center flex-grow ${s.pagination}`}>
  100. <Pagination.PageButton
  101. activeClassName="bg-primary-50 dark:bg-opacity-0 text-primary-600 dark:text-white"
  102. className="flex items-center justify-center h-8 w-8 rounded-full cursor-pointer"
  103. inactiveClassName="text-gray-500"
  104. />
  105. </div>
  106. <Pagination.NextButton
  107. disabled={currPage === Math.ceil(total / APP_PAGE_LIMIT) - 1}
  108. 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'}`} >
  109. {t('appLog.table.pagination.next')}
  110. <ArrowRightIcon className="ml-3 h-3 w-3" />
  111. </Pagination.NextButton>
  112. </Pagination>
  113. : null}
  114. </div>
  115. </div>
  116. )
  117. }
  118. export default Logs