ProjectTableRow.tsx 7.3 KB

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