InfrastructureActivity.tsx457 lines · main
1import { useParams } from 'common'
2import dayjs from 'dayjs'
3import { capitalize } from 'lodash'
4import { BarChart2, ChartLine, ExternalLink } from 'lucide-react'
5import Link from 'next/link'
6import { Fragment, useMemo, useState } from 'react'
7import { Button } from 'ui'
8import { Admonition } from 'ui-patterns/admonition'
9import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
10
11import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
12import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
13import { CPUWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings'
14import { DiskIOBandwidthWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings'
15import { RAMWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/RAMWarnings'
16import UsageBarChart from '@/components/interfaces/Organization/Usage/UsageBarChart'
17import {
18 ScaffoldContainer,
19 ScaffoldDivider,
20 ScaffoldSection,
21 ScaffoldSectionContent,
22 ScaffoldSectionDetail,
23} from '@/components/layouts/Scaffold'
24import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
25import { DateRangePicker } from '@/components/ui/DateRangePicker'
26import { DocsButton } from '@/components/ui/DocsButton'
27import Panel from '@/components/ui/Panel'
28import { DataPoint } from '@/data/analytics/constants'
29import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
30import {
31 InfraMonitoringAttribute,
32 useInfraMonitoringAttributesQuery,
33} from '@/data/analytics/infra-monitoring-query'
34import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
35import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
36import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
37import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
38import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
39import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
40import { DOCS_URL, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS, InstanceSpecs } from '@/lib/constants'
41import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
42import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
43
44const NON_DEDICATED_IO_RESOURCES = [
45 'ci_micro',
46 'ci_small',
47 'ci_medium',
48 'ci_large',
49 'ci_xlarge',
50 'ci_2xlarge',
51]
52
53const INFRA_ATTRIBUTES: InfraMonitoringAttribute[] = [
54 'max_cpu_usage',
55 'ram_usage',
56 'disk_io_consumption',
57]
58
59export const InfrastructureActivity = () => {
60 const { ref: projectRef } = useParams()
61 const { data: project } = useSelectedProjectQuery()
62 const { data: organization } = useSelectedOrganizationQuery()
63 const state = useDatabaseSelectorStateSnapshot()
64 const [dateRange, setDateRange] = useState<any>()
65
66 const { data: subscription, isPending: isLoadingSubscription } = useOrgSubscriptionQuery({
67 orgSlug: organization?.slug,
68 })
69 const { hasAccess: hasAccessToComputeSizes } = useCheckEntitlements(
70 'instances.compute_update_available_sizes'
71 )
72
73 const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
74 // [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
75 const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
76
77 const { data: addons } = useProjectAddonsQuery({ projectRef })
78 const selectedAddons = addons?.selected_addons ?? []
79
80 const { computeInstance } = getAddons(selectedAddons)
81 const hasDedicatedIOResources =
82 computeInstance !== undefined &&
83 !NON_DEDICATED_IO_RESOURCES.includes(computeInstance.variant.identifier)
84
85 function getCurrentComputeInstanceSpecs() {
86 if (computeInstance?.variant.meta) {
87 // If user has a compute instance (called addons) return that
88 return computeInstance?.variant.meta as InstanceSpecs
89 } else {
90 // Otherwise, return the default specs
91 return project?.infra_compute_size === 'nano' ? INSTANCE_NANO_SPECS : INSTANCE_MICRO_SPECS
92 }
93 }
94
95 const currentComputeInstanceSpecs = getCurrentComputeInstanceSpecs()
96
97 const currentBillingCycleSelected = useMemo(() => {
98 // Selected by default
99 if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true
100
101 const { current_period_start, current_period_end } = subscription
102
103 return (
104 dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) &&
105 dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000))
106 )
107 }, [dateRange, subscription])
108
109 const upgradeUrl =
110 organization === undefined
111 ? `/`
112 : hasAccessToComputeSizes
113 ? `/org/${organization?.slug ?? '[slug]'}/billing#subscription`
114 : `/project/${projectRef}/settings/addons`
115
116 const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
117
118 const startDate = useMemo(() => {
119 if (dateRange?.period_start?.date === 'Invalid Date') return undefined
120
121 // If end date is in future, set end date to now
122 if (!dateRange?.period_start?.date) {
123 return undefined
124 } else {
125 // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
126 return new Date(dateRange?.period_start?.date ?? 0).toISOString().slice(0, -5) + 'Z'
127 }
128 }, [dateRange])
129
130 const endDate = useMemo(() => {
131 if (dateRange?.period_end?.date === 'Invalid Date') return undefined
132
133 // If end date is in future, set end date to end of current day
134 if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
135 // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
136 // In order to have full days from Prometheus metrics when using 1d interval,
137 // the time needs to be greater or equal than the time of the start date
138 return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
139 } else if (dateRange?.period_end?.date) {
140 // LF seems to have an issue with the milliseconds, causes infinite loading sometimes
141 return new Date(dateRange.period_end.date ?? 0).toISOString().slice(0, -5) + 'Z'
142 }
143 }, [dateRange])
144
145 // Switch to hourly interval, if the timeframe is <48 hours
146 let interval: '1d' | '1h' = '1d'
147 let dateFormat = 'DD MMM'
148 if (startDate && endDate) {
149 const diffInHours = dayjs(endDate).diff(startDate, 'hours')
150
151 if (diffInHours <= 48) {
152 interval = '1h'
153 dateFormat = 'h a'
154 }
155 }
156
157 const { data: infraMonitoringData, isPending: isLoadingInfraData } =
158 useInfraMonitoringAttributesQuery({
159 projectRef,
160 attributes: INFRA_ATTRIBUTES,
161 interval,
162 startDate,
163 endDate,
164 databaseIdentifier: state.selectedDatabaseId,
165 })
166
167 const transformedData = useMemo(() => {
168 if (!infraMonitoringData) return undefined
169 return mapMultiResponseToAnalyticsData(infraMonitoringData, INFRA_ATTRIBUTES, dateFormat)
170 }, [infraMonitoringData, dateFormat])
171
172 const cpuUsageData = transformedData?.max_cpu_usage
173 const memoryUsageData = transformedData?.ram_usage
174 const ioBudgetData = transformedData?.disk_io_consumption
175
176 const hasLatest = dayjs(endDate!).isAfter(dayjs().startOf('day'))
177
178 const latestIoBudgetConsumption =
179 hasLatest && ioBudgetData?.data?.slice(-1)?.[0]
180 ? Number(ioBudgetData.data.slice(-1)[0].disk_io_consumption)
181 : 0
182
183 const highestIoBudgetConsumption = (ioBudgetData?.data || [])
184 .map((x) => Number(x.disk_io_consumption) ?? 0)
185 .reduce((a, b) => Math.max(a, b), 0)
186
187 const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = {
188 max_cpu_usage: {
189 isLoading: isLoadingInfraData,
190 data: cpuUsageData?.data ?? [],
191 },
192 ram_usage: {
193 isLoading: isLoadingInfraData,
194 data: memoryUsageData?.data ?? [],
195 },
196 disk_io_consumption: {
197 isLoading: isLoadingInfraData,
198 data: ioBudgetData?.data ?? [],
199 },
200 }
201
202 return (
203 <>
204 <ScaffoldContainer id="infrastructure-activity">
205 <div className="mx-auto flex flex-col gap-10 pt-6">
206 <div>
207 <p className="text-xl">Infrastructure Activity</p>
208 <p className="text-sm text-foreground-light">
209 Activity statistics related to your server instance
210 </p>
211 </div>
212 </div>
213 </ScaffoldContainer>
214 <ScaffoldContainer className="sticky top-0 py-6 border-b bg-studio z-10">
215 <div className="flex items-center gap-x-4">
216 <DatabaseSelector />
217 {!isLoadingSubscription && (
218 <>
219 <DateRangePicker
220 onChange={setDateRange}
221 value={TIME_PERIODS_REPORTS[0].key}
222 options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
223 loading={isLoadingSubscription}
224 currentBillingPeriodStart={subscription?.current_period_start}
225 currentBillingPeriodEnd={subscription?.current_period_end}
226 />
227 <p className="text-sm text-foreground-light">
228 {dayjs(startDate).format('DD MMM YYYY')} - {dayjs(endDate).format('DD MMM YYYY')}
229 </p>
230 </>
231 )}
232 </div>
233 </ScaffoldContainer>
234
235 {categoryMeta?.attributes.map((attribute) => {
236 const chartData = chartMeta[attribute.key]?.data ?? []
237
238 return (
239 <Fragment key={attribute.key}>
240 <ScaffoldContainer id={attribute.anchor}>
241 <ScaffoldSection>
242 <ScaffoldSectionDetail>
243 <div className="sticky top-32 space-y-6">
244 <div className="space-y-1">
245 <div className="flex items-center space-x-2">
246 <h4 className="text-base capitalize m-0">{attribute.name}</h4>
247 </div>
248 <div className="grid gap-4">
249 {attribute.description.split('\n').map((value, idx) => (
250 <p key={`desc-${idx}`} className="text-sm text-foreground-light pr-8 m-0">
251 {value}
252 </p>
253 ))}
254 </div>
255 </div>
256 <div className="space-y-2">
257 <p className="text-sm text-foreground mb-2">More information</p>
258 {attribute.links.map((link) => (
259 <div key={link.url}>
260 <Link href={link.url} target="_blank" rel="noreferrer">
261 <div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
262 <p className="text-sm m-0">{link.name}</p>
263 <ExternalLink size={16} strokeWidth={1.5} />
264 </div>
265 </Link>
266 </div>
267 ))}
268 </div>
269 </div>
270 </ScaffoldSectionDetail>
271 <ScaffoldSectionContent>
272 {attribute.key === 'disk_io_consumption' && (
273 <>
274 <DiskIOBandwidthWarnings
275 upgradeUrl={upgradeUrl}
276 hasAccessToComputeSizes={hasAccessToComputeSizes}
277 hasLatest={hasLatest}
278 currentBillingCycleSelected={currentBillingCycleSelected}
279 latestIoBudgetConsumption={latestIoBudgetConsumption}
280 highestIoBudgetConsumption={highestIoBudgetConsumption}
281 />
282 <div className="space-y-1">
283 <p>Disk IO Bandwidth</p>
284
285 {currentComputeInstanceSpecs.baseline_disk_io_mbs ===
286 currentComputeInstanceSpecs.max_disk_io_mbs ? (
287 <p className="text-sm text-foreground-light">
288 Your current compute has a baseline and maximum disk throughput of{' '}
289 {currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps.
290 </p>
291 ) : (
292 <p className="text-sm text-foreground-light">
293 Your current compute can burst above the baseline disk throughput of{' '}
294 {currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
295 Mbps for short periods of time.
296 </p>
297 )}
298 </div>
299 <div>
300 <p className="text-sm mb-2">Overview</p>
301 <div className="flex items-center justify-between border-b py-1">
302 <p className="text-xs text-foreground-light">Current compute instance</p>
303 <p className="text-xs">
304 {computeInstance?.variant?.name ??
305 capitalize(project?.infra_compute_size) ??
306 'Micro'}
307 </p>
308 </div>
309 <div className="flex items-center justify-between border-b py-1">
310 <p className="text-xs text-foreground-light">Baseline IO Bandwidth</p>
311 <p className="text-xs">
312 {currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
313 Mbps
314 </p>
315 </div>
316 <div className="flex items-center justify-between border-b py-1">
317 <p className="text-xs text-foreground-light">
318 Maximum IO Bandwidth (burst limit)
319 </p>
320 <p className="text-xs">
321 {currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps
322 </p>
323 </div>
324 {currentComputeInstanceSpecs.max_disk_io_mbs !==
325 currentComputeInstanceSpecs?.baseline_disk_io_mbs && (
326 <div className="flex items-center justify-between py-1">
327 <p className="text-xs text-foreground-light">Daily burst time limit</p>
328 <p className="text-xs">30 mins</p>
329 </div>
330 )}
331 </div>
332 </>
333 )}
334 {attribute.key === 'max_cpu_usage' && (
335 <CPUWarnings
336 upgradeUrl={upgradeUrl}
337 hasAccessToComputeSizes={hasAccessToComputeSizes}
338 severity={projectResourceWarnings?.cpu_exhaustion}
339 />
340 )}
341 {attribute.key === 'ram_usage' && (
342 <RAMWarnings
343 upgradeUrl={upgradeUrl}
344 hasAccessToComputeSizes={hasAccessToComputeSizes}
345 severity={projectResourceWarnings?.memory_and_swap_exhaustion}
346 />
347 )}
348
349 <div className="space-y-1">
350 <div className="flex flex-row justify-between">
351 {attribute.key === 'disk_io_consumption' ? (
352 <p>Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}</p>
353 ) : (
354 <p>
355 Max{' '}
356 <span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
357 {attribute.name}
358 </span>{' '}
359 utilization per {interval === '1d' ? 'day' : 'hour'}
360 </p>
361 )}
362 </div>
363
364 {attribute.key === 'ram_usage' && (
365 <div className="text-sm text-foreground-light">
366 <p>
367 Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of
368 memory.
369 </p>
370 {currentComputeInstanceSpecs.memory_gb === 1 && (
371 <p>
372 As your project is running on the smallest compute instance, it is not
373 unusual for your project to have a base memory usage of ~50%.
374 </p>
375 )}
376 </div>
377 )}
378
379 {attribute.key === 'max_cpu_usage' && (
380 <p className="text-sm text-foreground-light">
381 Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.
382 </p>
383 )}
384
385 {attribute.chartDescription.split('\n').map((paragraph, idx) => (
386 <p key={`para-${idx}`} className="text-sm text-foreground-light">
387 {paragraph}
388 </p>
389 ))}
390 </div>
391 {attribute.key === 'disk_io_consumption' && hasDedicatedIOResources ? (
392 <>
393 <Admonition
394 type="note"
395 title={`Your compute instance of ${computeInstance.variant.name} comes with dedicated I/O resources`}
396 description="Your project thus does not rely on I/O balance or burst capacity as larger
397 add-ons are designed for sustained, high performance with specific IOPS and
398 throughput limits without needing to burst."
399 >
400 <DocsButton
401 abbrev={false}
402 href={`${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`}
403 />
404 </Admonition>
405 </>
406 ) : chartMeta[attribute.key].isLoading ? (
407 <div className="space-y-2">
408 <ShimmeringLoader />
409 <ShimmeringLoader className="w-3/4" />
410 <ShimmeringLoader className="w-1/2" />
411 </div>
412 ) : chartData.length ? (
413 <UsageBarChart
414 name={`${attribute.chartPrefix || ''} ${attribute.name}`}
415 unit={attribute.unit}
416 attributes={attribute.attributes}
417 data={chartData}
418 yFormatter={(value) => `${Math.round(Number(value))}%`}
419 tooltipFormatter={(value) => `${value}%`}
420 yLimit={100}
421 />
422 ) : (
423 <Panel>
424 <Panel.Content>
425 <div className="flex flex-col items-center justify-center space-y-2">
426 <BarChart2 size={18} className="text-foreground-light mb-2" />
427 <p className="text-sm">No data in period</p>
428 <p className="text-sm text-foreground-light">
429 May take a few minutes to show
430 </p>
431 </div>
432 </Panel.Content>
433 </Panel>
434 )}
435 {attribute.key === 'disk_io_consumption' && !hasDedicatedIOResources && (
436 <Admonition
437 type="default"
438 title="Looking for actual disk activity?"
439 description="The chart above shows your remaining burst budget, not real disk throughput. For detailed read/write IOPS and throughput charts, head to the Database Observability page."
440 >
441 <Button asChild type="default" icon={<ChartLine size={14} />}>
442 <Link href={`/project/${projectRef}/observability/database`}>
443 View detailed IOPS and throughput
444 </Link>
445 </Button>
446 </Admonition>
447 )}
448 </ScaffoldSectionContent>
449 </ScaffoldSection>
450 </ScaffoldContainer>
451 <ScaffoldDivider />
452 </Fragment>
453 )
454 })}
455 </>
456 )
457}