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