Files
supabase/apps/studio/components/interfaces/Home/ProjectList/ProjectCardStatus.tsx
T
Joshen Lim 09a1f431bc Mass update for all lucide icon imports to use the component with Icon suffix (#51659)
## Context

This is largely just a mechanical change here:
Opting to use lucide icons with the `Icon` suffix across all icon
imports within `studio`
- so that it’s clearer which components are icons + reduces the
possibility of name collision with our own components
- e.g <User /> vs <UserIcon />

Also added a eslint rule for this:
<img width="1069" height="167" alt="image"
src="https://github.com/user-attachments/assets/7f36f443-5ca2-4bf6-8d22-86e774513bfc"
/>

And updated `AGENTS.md` as such:
```
Icons come from `lucide-react`, always imported with the `Icon` suffix (`import { CheckIcon, Trash2Icon } from 'lucide-react'`) — never the bare names (`Check`) or `Check as CheckIcon` aliases (lint-enforced).
```
2026-10-14 12:56:55 +02:00

202 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { AlertTriangleIcon, InfoIcon, PauseCircleIcon, RefreshCcwIcon } from 'lucide-react'
import { Badge, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { InferredProjectStatus } from './ProjectCard.utils'
import { RESOURCE_WARNING_MESSAGES } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.constants'
import { getWarningContent } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.utils'
import type { ResourceWarning } from '@/data/usage/resource-warnings-query'
export interface ProjectCardWarningsProps {
resourceWarnings?: ResourceWarning
projectStatus: InferredProjectStatus
renderMode?: 'alert' | 'badge'
}
export const ProjectCardStatus = ({
resourceWarnings: allResourceWarnings,
projectStatus,
renderMode = 'alert',
}: ProjectCardWarningsProps) => {
// [Terry] temp to remove auth_restricted_email_sending property from resourceWarnings
// set auth_restricted_email_sending from 'warning' to null so it doesn't show up in the warning banner
// [Joshen] Can remove this eventually once the auth email thing is resolved (Nov 2024)
const resourceWarnings = allResourceWarnings
? {
...allResourceWarnings,
auth_restricted_email_sending: null,
}
: undefined
// [Joshen] Read only takes higher precedence over multiple resource warnings
const activeWarnings = resourceWarnings?.is_readonly_mode_enabled
? ['is_readonly_mode_enabled']
: Object.keys(resourceWarnings || {}).filter(
(property) =>
property !== 'project' &&
property !== 'is_readonly_mode_enabled' &&
resourceWarnings?.[property as keyof typeof resourceWarnings] !== null
)
const hasCriticalWarning = activeWarnings.some(
(x) => resourceWarnings?.[x as keyof typeof resourceWarnings] === 'critical'
)
const isCritical = activeWarnings.includes('is_readonly_mode_enabled') || hasCriticalWarning
const warningContent =
resourceWarnings !== undefined
? getWarningContent(resourceWarnings, activeWarnings[0], 'cardContent')
: undefined
const getTitle = () => {
switch (projectStatus) {
case 'isPaused':
return renderMode === 'badge' ? 'Paused' : 'Project is paused'
case 'isPausing':
return renderMode === 'badge' ? 'Pausing' : 'Project is pausing'
case 'isRestarting':
return renderMode === 'badge' ? 'Restarting' : 'Project is restarting'
case 'isResizing':
return renderMode === 'badge' ? 'Resizing' : 'Project is resizing'
case 'isComingUp':
return renderMode === 'badge' ? 'Starting' : 'Project is coming up'
case 'isRestoring':
return renderMode === 'badge' ? 'Restoring' : 'Project is restoring'
case 'isUpgrading':
return renderMode === 'badge' ? 'Upgrading' : 'Project is upgrading'
case 'isRestoreFailed':
return renderMode === 'badge' ? 'Restore Failed' : 'Project restore failed'
case 'isPauseFailed':
return renderMode === 'badge' ? 'Pause Failed' : 'Project pause failed'
}
if (!resourceWarnings) return undefined
// If none of the paused/restoring states match, proceed with the default logic
return activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
hasCriticalWarning ? 'critical' : 'warning'
].title
: warningContent?.title
}
const getDescription = () => {
switch (projectStatus) {
case 'isPaused':
return 'This project will not accept requests until resumed'
case 'isPausing':
return 'The pause process will complete in a few minutes'
case 'isRestarting':
case 'isResizing':
case 'isComingUp':
case 'isRestoring':
case 'isUpgrading':
return 'Your project will be ready in a few minutes'
case 'isRestoreFailed':
case 'isPauseFailed':
return 'Please contact support for assistance'
}
if (!resourceWarnings) return undefined
// If none of the paused/restoring states match, proceed with the default logic
return activeWarnings.length > 1
? RESOURCE_WARNING_MESSAGES.multiple_resource_warnings.cardContent[
hasCriticalWarning ? 'critical' : 'warning'
].description
: warningContent?.description
}
const alertTitle = getTitle()
const alertDescription = getDescription()
const alertType = isCritical
? 'destructive'
: projectStatus === 'isPaused'
? 'default'
: 'warning'
if (
(activeWarnings.length === 0 || warningContent === undefined) &&
projectStatus === 'isHealthy'
) {
if (renderMode === 'badge') {
return (
// Badge must be wrapped in a div in order to be centered in table cell
<div className="flex items-center">
<Badge variant="success">Active</Badge>
</div>
)
}
return null
}
if (renderMode === 'badge') {
// Render a fallback en dash if no title is available
if (!alertTitle) return <span className="text-xs text-foreground-muted">–</span>
const badgeVariant = isCritical
? 'destructive'
: activeWarnings.length > 0 ||
projectStatus === 'isPauseFailed' ||
projectStatus === 'isRestoreFailed'
? 'warning'
: projectStatus === 'isHealthy'
? 'success'
: 'default'
return (
// Badge must be wrapped in a div in order to be centered in table cell
<div className="flex items-center">
{alertDescription ? (
<Tooltip>
<TooltipTrigger asChild>
<Badge variant={badgeVariant}>{alertTitle}</Badge>
</TooltipTrigger>
<TooltipContent side="bottom">{alertDescription}</TooltipContent>
</Tooltip>
) : (
<Badge variant={badgeVariant}>{alertTitle}</Badge>
)}
</div>
)
}
// Only render if an alert title is available
if (!alertTitle) return null
return (
<div role="alert" className={cn('w-full p-5 pb-5 flex flex-row gap-x-2 items-center')}>
{/* Icon */}
<div
className={cn(
'shrink-0 w-6 h-6 border rounded-md flex items-center justify-center',
alertType === 'destructive' && 'border-destructive-400 [&>svg]:text-destructive-600',
alertType === 'warning' && 'border-warning-400 [&>svg]:text-warning-600',
alertType === 'default' && 'border-strong [&>svg]:text-foreground'
)}
>
{['isPaused', 'isPausing'].includes(projectStatus ?? '') ? (
<PauseCircleIcon strokeWidth={1.5} size={14} />
) : ['isRestoring', 'isComingUp', 'isRestarting', 'isResizing'].includes(
projectStatus ?? ''
) ? (
<RefreshCcwIcon strokeWidth={1.5} size={14} />
) : (
<AlertTriangleIcon strokeWidth={1.5} size={14} />
)}
</div>
{/* Text and tooltip icon */}
<div className="flex items-center w-full gap-x-2">
<p className="text-xs">{alertTitle}</p>
<Tooltip>
<TooltipTrigger>
<InfoIcon
size={12}
className="text-foreground-lighter hover:text-foreground transition-colors"
/>
</TooltipTrigger>
<TooltipContent side="bottom">{alertDescription}</TooltipContent>
</Tooltip>
</div>
</div>
)
}