ProjectCardStatus.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  1. import { AlertTriangle, Info, PauseCircle, RefreshCcw } from 'lucide-react'
  2. import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  3. import { InferredProjectStatus } from './ProjectCard.utils'
  4. import { RESOURCE_WARNING_MESSAGES } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants'
  5. import { getWarningContent } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils'
  6. import type { ResourceWarning } from '@/data/usage/resource-warnings-query'
  7. export interface ProjectCardWarningsProps {
  8. resourceWarnings?: ResourceWarning
  9. projectStatus: InferredProjectStatus
  10. renderMode?: 'alert' | 'badge'
  11. }
  12. export const ProjectCardStatus = ({
  13. resourceWarnings: allResourceWarnings,
  14. projectStatus,
  15. renderMode = 'alert',
  16. }: ProjectCardWarningsProps) => {
  17. // [Terry] temp to remove auth_restricted_email_sending property from resourceWarnings
  18. // set auth_restricted_email_sending from 'warning' to null so it doesn't show up in the warning banner
  19. // [Joshen] Can remove this eventually once the auth email thing is resolved (Nov 2024)
  20. const resourceWarnings = allResourceWarnings
  21. ? {
  22. ...allResourceWarnings,
  23. auth_restricted_email_sending: null,
  24. }
  25. : undefined
  26. // [Joshen] Read only takes higher precedence over multiple resource warnings
  27. const activeWarnings = resourceWarnings?.is_readonly_mode_enabled
  28. ? ['is_readonly_mode_enabled']
  29. : Object.keys(resourceWarnings || {}).filter(
  30. (property) =>
  31. property !== 'project' &&
  32. property !== 'is_readonly_mode_enabled' &&
  33. resourceWarnings?.[property as keyof typeof resourceWarnings] !== null
  34. )
  35. const hasCriticalWarning = activeWarnings.some(
  36. (x) => resourceWarnings?.[x as keyof typeof resourceWarnings] === 'critical'
  37. )
  38. const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning
  39. const warningContent =
  40. resourceWarnings !== undefined
  41. ? getWarningContent(resourceWarnings, activeWarnings[0], 'cardContent')
  42. : undefined
  43. const getTitle = () => {
  44. switch (projectStatus) {
  45. case 'isPaused':
  46. return renderMode === 'badge' ? 'Paused' : 'Project is paused'
  47. case 'isPausing':
  48. return renderMode === 'badge' ? 'Pausing' : 'Project is pausing'
  49. case 'isRestarting':
  50. return renderMode === 'badge' ? 'Restarting' : 'Project is restarting'
  51. case 'isResizing':
  52. return renderMode === 'badge' ? 'Resizing' : 'Project is resizing'
  53. case 'isComingUp':
  54. return renderMode === 'badge' ? 'Starting' : 'Project is coming up'
  55. case 'isRestoring':
  56. return renderMode === 'badge' ? 'Restoring' : 'Project is restoring'
  57. case 'isUpgrading':
  58. return renderMode === 'badge' ? 'Upgrading' : 'Project is upgrading'
  59. case 'isRestoreFailed':
  60. return renderMode === 'badge' ? 'Restore Failed' : 'Project restore failed'
  61. case 'isPauseFailed':
  62. return renderMode === 'badge' ? 'Pause Failed' : 'Project pause failed'
  63. }
  64. if (!resourceWarnings) return undefined
  65. // If none of the paused/restoring states match, proceed with the default logic
  66. return activeWarnings.length > 1
  67. ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
  68. hasCriticalWarning ? 'critical' : 'warning'
  69. ].title
  70. : warningContent?.title
  71. }
  72. const getDescription = () => {
  73. switch (projectStatus) {
  74. case 'isPaused':
  75. return 'This project will not accept requests until resumed'
  76. case 'isPausing':
  77. return 'The pause process will complete in a few minutes'
  78. case 'isRestarting':
  79. case 'isResizing':
  80. case 'isComingUp':
  81. case 'isRestoring':
  82. case 'isUpgrading':
  83. return 'Your project will be ready in a few minutes'
  84. case 'isRestoreFailed':
  85. case 'isPauseFailed':
  86. return 'Please contact support for assistance'
  87. }
  88. if (!resourceWarnings) return undefined
  89. // If none of the paused/restoring states match, proceed with the default logic
  90. return activeWarnings.length > 1
  91. ? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
  92. hasCriticalWarning ? 'critical' : 'warning'
  93. ].description
  94. : warningContent?.description
  95. }
  96. const alertTitle = getTitle()
  97. const alertDescription = getDescription()
  98. const alertType = isCritical
  99. ? 'destructive'
  100. : projectStatus === 'isPaused'
  101. ? 'default'
  102. : 'warning'
  103. if (
  104. (activeWarnings.length === 0 || warningContent === undefined) &&
  105. projectStatus === 'isHealthy'
  106. ) {
  107. if (renderMode === 'badge') {
  108. return (
  109. // Badge must be wrapped in a div in order to be centered in table cell
  110. <div className="flex items-center">
  111. <Badge variant="success">Active</Badge>
  112. </div>
  113. )
  114. }
  115. return null
  116. }
  117. if (renderMode === 'badge') {
  118. // Render a fallback en dash if no title is available
  119. if (!alertTitle) return <span className="text-xs text-foreground-muted">–</span>
  120. const badgeVariant = isCritical
  121. ? 'destructive'
  122. : activeWarnings.length > 0 ||
  123. projectStatus === 'isPauseFailed' ||
  124. projectStatus === 'isRestoreFailed'
  125. ? 'warning'
  126. : projectStatus === 'isHealthy'
  127. ? 'success'
  128. : 'default'
  129. return (
  130. // Badge must be wrapped in a div in order to be centered in table cell
  131. <div className="flex items-center">
  132. {alertDescription ? (
  133. <Tooltip>
  134. <TooltipTrigger asChild>
  135. <Badge variant={badgeVariant}>{alertTitle}</Badge>
  136. </TooltipTrigger>
  137. <TooltipContent side="bottom">{alertDescription}</TooltipContent>
  138. </Tooltip>
  139. ) : (
  140. <Badge variant={badgeVariant}>{alertTitle}</Badge>
  141. )}
  142. </div>
  143. )
  144. }
  145. // Only render if an alert title is available
  146. if (!alertTitle) return null
  147. return (
  148. <div role="alert" className={cn('w-full p-5 pb-5 flex flex-row gap-x-2 items-center')}>
  149. {/* Icon */}
  150. <div
  151. className={cn(
  152. 'shrink-0 w-6 h-6 border rounded-md flex items-center justify-center',
  153. alertType === 'destructive' && 'border-destructive-400 [&>svg]:text-destructive-600',
  154. alertType === 'warning' && 'border-warning-400 [&>svg]:text-warning-600',
  155. alertType === 'default' && 'border-strong [&>svg]:text-foreground'
  156. )}
  157. >
  158. {['isPaused', 'isPausing'].includes(projectStatus ?? '') ? (
  159. <PauseCircle strokeWidth={1.5} size={14} />
  160. ) : ['isRestoring', 'isComingUp', 'isRestarting', 'isResizing'].includes(
  161. projectStatus ?? ''
  162. ) ? (
  163. <RefreshCcw strokeWidth={1.5} size={14} />
  164. ) : (
  165. <AlertTriangle strokeWidth={1.5} size={14} />
  166. )}
  167. </div>
  168. {/* Text and tooltip icon */}
  169. <div className="flex items-center w-full gap-x-2">
  170. <p className="text-xs">{alertTitle}</p>
  171. <Tooltip>
  172. <TooltipTrigger>
  173. <Info
  174. size={12}
  175. className="text-foreground-lighter hover:text-foreground transition-colors"
  176. />
  177. </TooltipTrigger>
  178. <TooltipContent side="bottom">{alertDescription}</TooltipContent>
  179. </Tooltip>
  180. </div>
  181. </div>
  182. )
  183. }