index.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import {
  2. useMemo,
  3. useState,
  4. } from 'react'
  5. import {
  6. RiCheckboxCircleFill,
  7. RiErrorWarningFill,
  8. RiInstallLine,
  9. } from '@remixicon/react'
  10. import { useTranslation } from 'react-i18next'
  11. import { usePluginTaskStatus } from './hooks'
  12. import {
  13. PortalToFollowElem,
  14. PortalToFollowElemContent,
  15. PortalToFollowElemTrigger,
  16. } from '@/app/components/base/portal-to-follow-elem'
  17. import Tooltip from '@/app/components/base/tooltip'
  18. import Button from '@/app/components/base/button'
  19. import ProgressCircle from '@/app/components/base/progress-bar/progress-circle'
  20. import CardIcon from '@/app/components/plugins/card/base/card-icon'
  21. import cn from '@/utils/classnames'
  22. import { useGetLanguage } from '@/context/i18n'
  23. import useGetIcon from '@/app/components/plugins/install-plugin/base/use-get-icon'
  24. import DownloadingIcon from '@/app/components/header/plugins-nav/downloading-icon'
  25. const PluginTasks = () => {
  26. const { t } = useTranslation()
  27. const language = useGetLanguage()
  28. const [open, setOpen] = useState(false)
  29. const {
  30. errorPlugins,
  31. runningPluginsLength,
  32. successPluginsLength,
  33. errorPluginsLength,
  34. totalPluginsLength,
  35. isInstalling,
  36. isInstallingWithSuccess,
  37. isInstallingWithError,
  38. isSuccess,
  39. isFailed,
  40. handleClearErrorPlugin,
  41. handleClearAllErrorPlugin,
  42. opacity,
  43. } = usePluginTaskStatus()
  44. const { getIconUrl } = useGetIcon()
  45. const tip = useMemo(() => {
  46. if (isInstalling)
  47. return t('plugin.task.installing', { installingLength: runningPluginsLength })
  48. if (isInstallingWithSuccess)
  49. return t('plugin.task.installingWithSuccess', { installingLength: runningPluginsLength, successLength: successPluginsLength })
  50. if (isInstallingWithError)
  51. return t('plugin.task.installingWithError', { installingLength: runningPluginsLength, successLength: successPluginsLength, errorLength: errorPluginsLength })
  52. if (isFailed)
  53. return t('plugin.task.installError', { errorLength: errorPluginsLength })
  54. }, [isInstalling, isInstallingWithSuccess, isInstallingWithError, isFailed, errorPluginsLength, runningPluginsLength, successPluginsLength, t])
  55. if (!totalPluginsLength)
  56. return null
  57. return (
  58. <div
  59. className='flex items-center'
  60. style={{ opacity }}
  61. >
  62. <PortalToFollowElem
  63. open={open}
  64. onOpenChange={setOpen}
  65. placement='bottom-start'
  66. offset={{
  67. mainAxis: 4,
  68. crossAxis: 79,
  69. }}
  70. >
  71. <PortalToFollowElemTrigger
  72. onClick={() => {
  73. if (isFailed)
  74. setOpen(v => !v)
  75. }}
  76. >
  77. <Tooltip popupContent={tip}>
  78. <div
  79. className={cn(
  80. 'relative flex h-8 w-8 items-center justify-center rounded-lg border-[0.5px] border-components-button-secondary-border bg-components-button-secondary-bg shadow-xs hover:bg-components-button-secondary-bg-hover',
  81. (isInstallingWithError || isFailed) && 'cursor-pointer border-components-button-destructive-secondary-border-hover bg-state-destructive-hover hover:bg-state-destructive-hover-alt',
  82. )}
  83. id="plugin-task-trigger"
  84. >
  85. {
  86. (isInstalling || isInstallingWithError) && (
  87. <DownloadingIcon />
  88. )
  89. }
  90. {
  91. !(isInstalling || isInstallingWithError) && (
  92. <RiInstallLine
  93. className={cn(
  94. 'h-4 w-4 text-components-button-secondary-text',
  95. (isInstallingWithError || isFailed) && 'text-components-button-destructive-secondary-text',
  96. )}
  97. />
  98. )
  99. }
  100. <div className='absolute -right-1 -top-1'>
  101. {
  102. (isInstalling || isInstallingWithSuccess) && (
  103. <ProgressCircle
  104. percentage={successPluginsLength / totalPluginsLength * 100}
  105. circleFillColor='fill-components-progress-brand-bg'
  106. />
  107. )
  108. }
  109. {
  110. isInstallingWithError && (
  111. <ProgressCircle
  112. percentage={runningPluginsLength / totalPluginsLength * 100}
  113. circleFillColor='fill-components-progress-brand-bg'
  114. sectorFillColor='fill-components-progress-error-border'
  115. circleStrokeColor='stroke-components-progress-error-border'
  116. />
  117. )
  118. }
  119. {
  120. isSuccess && (
  121. <RiCheckboxCircleFill className='h-3.5 w-3.5 text-text-success' />
  122. )
  123. }
  124. {
  125. isFailed && (
  126. <RiErrorWarningFill className='h-3.5 w-3.5 text-text-destructive' />
  127. )
  128. }
  129. </div>
  130. </div>
  131. </Tooltip>
  132. </PortalToFollowElemTrigger>
  133. <PortalToFollowElemContent className='z-[11]'>
  134. <div className='w-[320px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 pb-2 shadow-lg'>
  135. <div className='system-sm-semibold-uppercase sticky top-0 flex h-7 items-center justify-between px-2 pt-1'>
  136. {t('plugin.task.installedError', { errorLength: errorPluginsLength })}
  137. <Button
  138. className='shrink-0'
  139. size='small'
  140. variant='ghost'
  141. onClick={() => handleClearAllErrorPlugin()}
  142. >
  143. {t('plugin.task.clearAll')}
  144. </Button>
  145. </div>
  146. <div className='max-h-[400px] overflow-y-auto'>
  147. {
  148. errorPlugins.map(errorPlugin => (
  149. <div
  150. key={errorPlugin.plugin_unique_identifier}
  151. className='flex rounded-lg p-2 hover:bg-state-base-hover'
  152. >
  153. <div className='relative mr-2 flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-components-panel-border-subtle bg-background-default-dodge'>
  154. <RiErrorWarningFill className='absolute -bottom-0.5 -right-0.5 z-10 h-3 w-3 text-text-destructive' />
  155. <CardIcon
  156. size='tiny'
  157. src={getIconUrl(errorPlugin.icon)}
  158. />
  159. </div>
  160. <div className='grow'>
  161. <div className='system-md-regular truncate text-text-secondary'>
  162. {errorPlugin.labels[language]}
  163. </div>
  164. <div className='system-xs-regular break-all text-text-destructive'>
  165. {errorPlugin.message}
  166. </div>
  167. </div>
  168. <Button
  169. className='shrink-0'
  170. size='small'
  171. variant='ghost'
  172. onClick={() => handleClearErrorPlugin(errorPlugin.taskId, errorPlugin.plugin_unique_identifier)}
  173. >
  174. {t('common.operation.clear')}
  175. </Button>
  176. </div>
  177. ))
  178. }
  179. </div>
  180. </div>
  181. </PortalToFollowElemContent>
  182. </PortalToFollowElem>
  183. </div>
  184. )
  185. }
  186. export default PluginTasks