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