ProjectTableRow.tsx201 lines · main
1import { Check, Copy, Github, MoreVertical, Settings } from 'lucide-react'
2import { useRouter } from 'next/router'
3import { useState } from 'react'
4import InlineSVG from 'react-inlinesvg'
5import { toast } from 'sonner'
6import {
7 Button,
8 copyToClipboard,
9 DropdownMenu,
10 DropdownMenuContent,
11 DropdownMenuItem,
12 DropdownMenuTrigger,
13 TableCell,
14 TableRow,
15} from 'ui'
16import { TimestampInfo } from 'ui-patterns'
17
18import { inferProjectStatus } from './ProjectCard.utils'
19import { ProjectCardStatus } from './ProjectCardStatus'
20import { ComputeBadgeWrapper } from '@/components/ui/ComputeBadgeWrapper'
21import PartnerIcon from '@/components/ui/PartnerIcon'
22import type { IntegrationProjectConnection } from '@/data/integrations/integrations.types'
23import { getManagedByFromOrganizationPartner } from '@/data/organizations/managed-by-utils'
24import { getComputeSize, OrgProject } from '@/data/projects/org-projects-infinite-query'
25import type { ResourceWarning } from '@/data/usage/resource-warnings-query'
26import { BASE_PATH } from '@/lib/constants'
27import { MANAGED_BY } from '@/lib/constants/infrastructure'
28import { createNavigationHandler } from '@/lib/navigation'
29import type { Organization } from '@/types'
30
31export interface ProjectTableRowProps {
32 project: OrgProject
33 organization?: Organization
34 rewriteHref?: string
35 githubIntegration?: IntegrationProjectConnection
36 vercelIntegration?: IntegrationProjectConnection
37 resourceWarnings?: ResourceWarning
38}
39
40export const ProjectTableRow = ({
41 project,
42 organization,
43 rewriteHref,
44 githubIntegration,
45 vercelIntegration,
46 resourceWarnings,
47}: ProjectTableRowProps) => {
48 const router = useRouter()
49 const { name, ref: projectRef } = project
50 const projectStatus = inferProjectStatus(project.status)
51 const [isCopied, setIsCopied] = useState(false)
52
53 const url = rewriteHref ?? `/project/${project.ref}`
54 const isGithubIntegrated = githubIntegration !== undefined
55 const isVercelIntegrated = vercelIntegration !== undefined
56 const githubRepository = githubIntegration?.metadata.name ?? undefined
57 const projectManagedBy = getManagedByFromOrganizationPartner(
58 undefined,
59 project.integration_source
60 )
61 const hasPartnerIcon = projectManagedBy !== MANAGED_BY.BRIVEN
62 const handleNavigation = createNavigationHandler(url, router)
63
64 const handleCopyProjectRef = (e: React.SyntheticEvent) => {
65 e.stopPropagation()
66 copyToClipboard(projectRef)
67 setIsCopied(true)
68 toast.success('Copied project ID to clipboard')
69 setTimeout(() => setIsCopied(false), 2000)
70 }
71
72 return (
73 <>
74 <TableRow
75 className="cursor-pointer hover:bg-surface-200 inset-focus"
76 onClick={handleNavigation}
77 onAuxClick={handleNavigation}
78 onKeyDown={handleNavigation}
79 tabIndex={0}
80 >
81 <TableCell>
82 <div className="flex flex-col gap-y-2">
83 <div>
84 <h5 className="text-sm">{name}</h5>
85 <button
86 tabIndex={0}
87 onClick={handleCopyProjectRef}
88 onKeyDown={(e) => {
89 if (e.key === 'Enter' || e.key === ' ') {
90 e.preventDefault()
91 handleCopyProjectRef(e)
92 }
93 }}
94 className="inline-flex items-center gap-x-1 cursor-pointer border border-transparent border-dashed rounded-sm transition-colors hover:bg-surface-100 hover:border hover:border-strong group font-mono text-xs text-foreground-lighter hover:text-foreground-light px-1 -ml-1"
95 >
96 {projectRef}
97 {isCopied ? (
98 <Check size={12} strokeWidth={1.25} className="text-brand" />
99 ) : (
100 <Copy
101 size={12}
102 strokeWidth={1.25}
103 className="opacity-0 group-hover:opacity-100 transition-opacity"
104 />
105 )}
106 </button>
107 </div>
108 {(isGithubIntegrated || isVercelIntegrated || hasPartnerIcon) && (
109 <div className="flex items-center gap-x-1.5">
110 {isVercelIntegrated && (
111 <div className="bg-surface-100 w-5 h-5 p-1 border border-strong rounded-md flex items-center text-black dark:text-white">
112 <InlineSVG
113 src={`${BASE_PATH}/img/icons/vercel-icon.svg`}
114 title="Vercel Icon"
115 className="w-3"
116 />
117 </div>
118 )}
119 <PartnerIcon organization={{ managed_by: projectManagedBy }} />
120 {isGithubIntegrated && (
121 <div className="bg-surface-100 flex items-center gap-x-0.5 h-5 pr-1 border border-strong rounded-md">
122 <div className="w-5 h-5 p-1 flex items-center">
123 <Github size={12} strokeWidth={1.5} />
124 </div>
125 {githubRepository && (
126 <p className="text-xs text-foreground-light truncate">{githubRepository}</p>
127 )}
128 </div>
129 )}
130 </div>
131 )}
132 </div>
133 </TableCell>
134 <TableCell>
135 <ProjectCardStatus
136 projectStatus={projectStatus}
137 resourceWarnings={resourceWarnings}
138 renderMode="badge"
139 />
140 </TableCell>
141 <TableCell>
142 <div className="w-fit">
143 {project.status !== 'INACTIVE' ? (
144 <ComputeBadgeWrapper
145 slug={organization?.slug}
146 projectRef={project.ref}
147 cloudProvider={project.cloud_provider}
148 computeSize={getComputeSize(project)}
149 resourceWarnings={resourceWarnings}
150 />
151 ) : (
152 <span className="text-xs text-foreground-muted">–</span>
153 )}
154 </div>
155 </TableCell>
156 <TableCell>
157 <span className="lowercase text-sm text-foreground-light">
158 {project.cloud_provider} | {project.region || 'N/A'}
159 </span>
160 </TableCell>
161 <TableCell>
162 {project.inserted_at ? (
163 <TimestampInfo
164 className="text-sm text-foreground-light"
165 utcTimestamp={project.inserted_at}
166 />
167 ) : (
168 <span className="text-sm text-foreground-light">N/A</span>
169 )}
170 </TableCell>
171 <TableCell className="text-right">
172 <div onClick={(e) => e.stopPropagation()}>
173 <DropdownMenu>
174 <DropdownMenuTrigger asChild>
175 <Button
176 type="default"
177 icon={<MoreVertical />}
178 aria-label="More actions"
179 className="w-7"
180 onClick={(e) => e.stopPropagation()}
181 />
182 </DropdownMenuTrigger>
183 <DropdownMenuContent align="end" className="w-48">
184 <DropdownMenuItem
185 className="gap-x-2"
186 onClick={(e) => {
187 e.stopPropagation()
188 router.push(`/project/${projectRef}/settings/general`)
189 }}
190 >
191 <Settings size={14} />
192 <span>Settings</span>
193 </DropdownMenuItem>
194 </DropdownMenuContent>
195 </DropdownMenu>
196 </div>
197 </TableCell>
198 </TableRow>
199 </>
200 )
201}