ActivityStats.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import { useParams } from 'common'
  2. import dayjs from 'dayjs'
  3. import { Archive, Cpu, Database, GitBranch, Github } from 'lucide-react'
  4. import { useMemo } from 'react'
  5. import { cn, Skeleton } from 'ui'
  6. import { TimestampInfo } from 'ui-patterns'
  7. import { HighAvailabilityBadge } from './HighAvailabilityBadge'
  8. import { ServiceStatus } from './ServiceStatus'
  9. import { ComputeBadgeWrapper } from '@/components/ui/ComputeBadgeWrapper'
  10. import { SingleStat } from '@/components/ui/SingleStat'
  11. import { useBranchesQuery } from '@/data/branches/branches-query'
  12. import { useBackupsQuery } from '@/data/database/backups-query'
  13. import { DatabaseMigration, useMigrationsQuery } from '@/data/database/migrations-query'
  14. import { useGitHubConnectionsQuery } from '@/data/integrations/github-connections-query'
  15. import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
  16. import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
  17. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  18. import { PROJECT_STATUS } from '@/lib/constants'
  19. import { EMPTY_ARR } from '@/lib/void'
  20. export const ActivityStats = () => {
  21. const { ref } = useParams()
  22. const { data: project } = useSelectedProjectQuery()
  23. const { data: organization } = useSelectedOrganizationQuery()
  24. const { data: resourceWarnings } = useResourceWarningsQuery({ slug: organization?.slug })
  25. const projectResourceWarnings = resourceWarnings?.find((warning) => warning.project === ref)
  26. const parentProjectRef = project?.parent_project_ref ?? project?.ref
  27. const { data: branchesData, isPending: isLoadingBranches } = useBranchesQuery({
  28. projectRef: parentProjectRef,
  29. })
  30. const isDefaultProject = project?.parent_project_ref === undefined
  31. const currentBranch = useMemo(
  32. () => (branchesData ?? []).find((b) => b.project_ref === ref),
  33. [branchesData, ref]
  34. )
  35. const latestNonDefaultBranch = useMemo(() => {
  36. const list = (branchesData ?? []).filter((b) => !b.is_default)
  37. if (list.length === 0) return undefined
  38. return list
  39. .slice()
  40. .sort(
  41. (a, b) =>
  42. new Date(b.created_at ?? b.updated_at).valueOf() -
  43. new Date(a.created_at ?? a.updated_at).valueOf()
  44. )[0]
  45. }, [branchesData])
  46. const { data: migrationsData = EMPTY_ARR, isPending: isLoadingMigrations } = useMigrationsQuery({
  47. projectRef: project?.ref,
  48. projectStatus: project?.status,
  49. connectionString: project?.connectionString,
  50. })
  51. const latestMigration = useMemo<DatabaseMigration | undefined>(
  52. () => migrationsData[0],
  53. [migrationsData]
  54. )
  55. const migrationLabelText =
  56. migrationsData.length === 0 ? 'No migrations' : (latestMigration?.name ?? 'Unknown')
  57. const { data: backupsData, isPending: isLoadingBackups } = useBackupsQuery({
  58. projectRef: project?.ref,
  59. projectStatus: project?.status,
  60. })
  61. const latestBackup = useMemo(() => {
  62. const list = backupsData?.backups ?? []
  63. if (list.length === 0) return undefined
  64. return list
  65. .slice()
  66. .sort((a, b) => new Date(b.inserted_at).valueOf() - new Date(a.inserted_at).valueOf())[0]
  67. }, [backupsData])
  68. const { data: githubConnections, isPending: isLoadingGithubConnections } =
  69. useGitHubConnectionsQuery({ organizationId: organization?.id }, { enabled: !!organization?.id })
  70. const githubConnection = githubConnections?.find(
  71. (connection) => connection.project.ref === parentProjectRef
  72. )
  73. const isProjectComingUp = [PROJECT_STATUS.COMING_UP, PROJECT_STATUS.UNKNOWN].includes(
  74. project?.status ?? PROJECT_STATUS.UNKNOWN
  75. )
  76. const githubLabelText = githubConnection?.repository.name
  77. ? githubConnection.repository.name
  78. : isProjectComingUp
  79. ? 'Waiting for project...'
  80. : 'No repository connected'
  81. const integrationsPath = parentProjectRef
  82. ? `/project/${parentProjectRef}/settings/integrations`
  83. : undefined
  84. return (
  85. <div className="@container">
  86. <div className="grid grid-cols-1 @md:grid-cols-2 gap-2 @md:gap-6 flex-wrap">
  87. <ServiceStatus />
  88. <SingleStat
  89. icon={<Cpu size={18} strokeWidth={1.5} className="text-foreground" />}
  90. label={<span>Compute</span>}
  91. value={
  92. <div className="flex items-center gap-2 flex-wrap">
  93. {project?.infra_compute_size ? (
  94. <ComputeBadgeWrapper
  95. projectRef={project?.ref}
  96. slug={organization?.slug}
  97. cloudProvider={project?.cloud_provider}
  98. computeSize={project.infra_compute_size}
  99. resourceWarnings={projectResourceWarnings}
  100. />
  101. ) : (
  102. <p className="text-foreground-lighter">Unknown</p>
  103. )}
  104. {project?.high_availability && <HighAvailabilityBadge />}
  105. </div>
  106. }
  107. />
  108. <SingleStat
  109. href={integrationsPath}
  110. icon={<Github size={18} strokeWidth={1.5} className="text-foreground" />}
  111. label={<span>GitHub</span>}
  112. value={
  113. isLoadingGithubConnections ? (
  114. <Skeleton className="h-6 w-24" />
  115. ) : (
  116. <p
  117. className={cn('truncate', !githubConnection && 'text-foreground-lighter')}
  118. title={githubLabelText}
  119. >
  120. {githubLabelText}
  121. </p>
  122. )
  123. }
  124. />
  125. <SingleStat
  126. href={`/project/${ref}/branches`}
  127. icon={<GitBranch size={18} strokeWidth={1.5} className="text-foreground" />}
  128. label={<span>{isDefaultProject ? 'Recent branch' : 'Branch Created'}</span>}
  129. trackingProperties={{
  130. stat_type: 'branches',
  131. stat_value: branchesData?.length ?? 0,
  132. }}
  133. value={
  134. isLoadingBranches ? (
  135. <Skeleton className="h-6 w-24" />
  136. ) : isDefaultProject ? (
  137. <p
  138. className={cn(
  139. 'truncate',
  140. !latestNonDefaultBranch && 'text-foreground-lighter truncate'
  141. )}
  142. title={latestNonDefaultBranch?.name ?? 'No branches'}
  143. >
  144. {latestNonDefaultBranch?.name ?? 'No branches'}
  145. </p>
  146. ) : currentBranch?.created_at ? (
  147. <TimestampInfo
  148. className="text-base"
  149. label={dayjs(currentBranch.created_at).fromNow()}
  150. utcTimestamp={currentBranch.created_at}
  151. />
  152. ) : (
  153. <p className="text-foreground-lighter">Unknown</p>
  154. )
  155. }
  156. />
  157. <SingleStat
  158. href={`/project/${ref}/database/migrations`}
  159. icon={<Database size={18} strokeWidth={1.5} className="text-foreground" />}
  160. label={<span>Last migration</span>}
  161. trackingProperties={{
  162. stat_type: 'migrations',
  163. stat_value: migrationsData?.length ?? 0,
  164. }}
  165. value={
  166. isLoadingMigrations ? (
  167. <Skeleton className="h-6 w-24" />
  168. ) : (
  169. <p className={!!latestMigration ? 'text-foreground' : 'text-foreground-lighter'}>
  170. {migrationLabelText}
  171. </p>
  172. )
  173. }
  174. />
  175. <SingleStat
  176. href={`/project/${ref}/database/backups/scheduled`}
  177. icon={<Archive size={18} strokeWidth={1.5} className="text-foreground" />}
  178. label={<span>Last backup</span>}
  179. trackingProperties={{
  180. stat_type: 'backups',
  181. stat_value: backupsData?.backups?.length ?? 0,
  182. }}
  183. value={
  184. isLoadingBackups ? (
  185. <Skeleton className="h-6 w-24" />
  186. ) : backupsData?.pitr_enabled ? (
  187. <p>PITR enabled</p>
  188. ) : latestBackup ? (
  189. <TimestampInfo
  190. className="text-base"
  191. displayAs="utc"
  192. label={dayjs(latestBackup.inserted_at).fromNow()}
  193. utcTimestamp={latestBackup.inserted_at}
  194. />
  195. ) : (
  196. <p className="text-foreground-lighter">No backups</p>
  197. )
  198. }
  199. />
  200. </div>
  201. </div>
  202. )
  203. }