UpcomingInvoice.tsx516 lines · main
| 1 | import React from 'react' |
| 2 | import { Table, TableBody, TableCell, TableFooter, TableRow } from 'ui' |
| 3 | import { InfoTooltip } from 'ui-patterns/info-tooltip' |
| 4 | import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' |
| 5 | |
| 6 | import { billingMetricUnit, formatUsage } from '../helpers' |
| 7 | import AlertError from '@/components/ui/AlertError' |
| 8 | import { InlineLink } from '@/components/ui/InlineLink' |
| 9 | import { PricingMetric } from '@/data/analytics/org-daily-stats-query' |
| 10 | import { |
| 11 | UpcomingInvoiceResponse, |
| 12 | useOrgUpcomingInvoiceQuery, |
| 13 | } from '@/data/invoices/org-invoice-upcoming-query' |
| 14 | import { useOrganizationQuery } from '@/data/organizations/organization-query' |
| 15 | import { DOCS_URL } from '@/lib/constants' |
| 16 | import { formatCurrency } from '@/lib/helpers' |
| 17 | |
| 18 | export interface UpcomingInvoiceProps { |
| 19 | slug?: string |
| 20 | } |
| 21 | |
| 22 | const usageBillingDocsLink: { [K in PricingMetric]?: string } = { |
| 23 | [PricingMetric.MONTHLY_ACTIVE_USERS]: `${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users`, |
| 24 | [PricingMetric.MONTHLY_ACTIVE_SSO_USERS]: `${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users-sso`, |
| 25 | [PricingMetric.MONTHLY_ACTIVE_THIRD_PARTY_USERS]: `${DOCS_URL}/guides/platform/manage-your-usage/monthly-active-users-third-party`, |
| 26 | [PricingMetric.AUTH_MFA_PHONE]: `${DOCS_URL}/guides/platform/manage-your-usage/advanced-mfa-phone`, |
| 27 | |
| 28 | [PricingMetric.EGRESS]: `${DOCS_URL}/guides/platform/manage-your-usage/egress`, |
| 29 | [PricingMetric.CACHED_EGRESS]: `${DOCS_URL}/guides/platform/manage-your-usage/egress`, |
| 30 | |
| 31 | [PricingMetric.FUNCTION_INVOCATIONS]: `${DOCS_URL}/guides/platform/manage-your-usage/edge-function-invocations`, |
| 32 | |
| 33 | [PricingMetric.STORAGE_SIZE]: `${DOCS_URL}/guides/platform/manage-your-usage/storage-size`, |
| 34 | [PricingMetric.STORAGE_IMAGES_TRANSFORMED]: `${DOCS_URL}/guides/platform/manage-your-usage/storage-image-transformations`, |
| 35 | |
| 36 | [PricingMetric.REALTIME_MESSAGE_COUNT]: `${DOCS_URL}/guides/platform/manage-your-usage/realtime-messages`, |
| 37 | [PricingMetric.REALTIME_PEAK_CONNECTIONS]: `${DOCS_URL}/guides/platform/manage-your-usage/realtime-peak-connections`, |
| 38 | |
| 39 | [PricingMetric.CUSTOM_DOMAIN]: `${DOCS_URL}/guides/platform/manage-your-usage/custom-domains`, |
| 40 | [PricingMetric.IPV4]: `${DOCS_URL}/guides/platform/manage-your-usage/ipv4`, |
| 41 | [PricingMetric.PITR_7]: `${DOCS_URL}/guides/platform/manage-your-usage/point-in-time-recovery`, |
| 42 | [PricingMetric.PITR_14]: `${DOCS_URL}/guides/platform/manage-your-usage/point-in-time-recovery`, |
| 43 | [PricingMetric.PITR_28]: `${DOCS_URL}/guides/platform/manage-your-usage/point-in-time-recovery`, |
| 44 | [PricingMetric.DISK_SIZE_GB_HOURS_GP3]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`, |
| 45 | [PricingMetric.DISK_SIZE_GB_HOURS_IO2]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-size`, |
| 46 | [PricingMetric.DISK_IOPS_GP3]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-iops`, |
| 47 | [PricingMetric.DISK_IOPS_IO2]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-iops`, |
| 48 | [PricingMetric.DISK_THROUGHPUT_GP3]: `${DOCS_URL}/guides/platform/manage-your-usage/disk-throughput`, |
| 49 | [PricingMetric.LOG_DRAIN]: `${DOCS_URL}/guides/platform/manage-your-usage/log-drains`, |
| 50 | } |
| 51 | |
| 52 | export const UpcomingInvoice = ({ slug }: UpcomingInvoiceProps) => { |
| 53 | const { |
| 54 | data: upcomingInvoice, |
| 55 | error: error, |
| 56 | isPending: isLoading, |
| 57 | isError, |
| 58 | isSuccess, |
| 59 | } = useOrgUpcomingInvoiceQuery({ orgSlug: slug }) |
| 60 | |
| 61 | const { data: organization } = useOrganizationQuery({ slug }) |
| 62 | |
| 63 | // For non-platform customers, compute is broken down per project and contains a breakdown array |
| 64 | const computeItems = |
| 65 | upcomingInvoice?.lines?.filter( |
| 66 | (item) => |
| 67 | item.description?.toLowerCase().includes('compute') && |
| 68 | !item.description.startsWith('Compute Credits') |
| 69 | ) || [] |
| 70 | |
| 71 | const computeCreditsItem = |
| 72 | upcomingInvoice?.lines?.find((item) => item.description.startsWith('Compute Credits')) ?? null |
| 73 | |
| 74 | const planItem = upcomingInvoice?.lines?.find((item) => |
| 75 | item.description?.toLowerCase().includes('plan') |
| 76 | ) |
| 77 | |
| 78 | const regularComputeItems = computeItems.filter( |
| 79 | (it) => !it.metadata?.is_branch && !it.metadata?.is_read_replica |
| 80 | ) |
| 81 | const branchingComputeItems = computeItems.filter((it) => it.metadata?.is_branch) |
| 82 | const replicaComputeItems = computeItems.filter((it) => it.metadata?.is_read_replica) |
| 83 | |
| 84 | const branchingComputeItemsDisplay = branchingComputeItems |
| 85 | .flatMap((it) => it.breakdown) |
| 86 | .sort((a, b) => (a?.project_name ?? '').localeCompare(b?.project_name ?? '')) |
| 87 | |
| 88 | const otherItems = |
| 89 | upcomingInvoice?.lines |
| 90 | ?.filter( |
| 91 | (item) => |
| 92 | !item.description?.toLowerCase().includes('compute') && |
| 93 | !item.description?.toLowerCase().includes('plan') && |
| 94 | item.amount_before_discount > 0 |
| 95 | ) |
| 96 | .sort((a, b) => b.amount_before_discount - a.amount_before_discount) || [] |
| 97 | |
| 98 | const prepaidCreditsItem = |
| 99 | upcomingInvoice?.lines?.find((item) => item.item_name === 'Prepaid Credits') ?? null |
| 100 | |
| 101 | const hasTax = |
| 102 | upcomingInvoice?.tax_status === 'calculated' && (upcomingInvoice?.tax?.tax_amount ?? 0) > 0 |
| 103 | const taxFailed = upcomingInvoice?.tax_status === 'failed' |
| 104 | |
| 105 | const planFeePaidInAdvance = |
| 106 | !planItem && upcomingInvoice?.fixed_fees_billing_mode === 'in_arrears' |
| 107 | |
| 108 | return ( |
| 109 | <> |
| 110 | {isLoading && ( |
| 111 | <div className="space-y-2"> |
| 112 | <ShimmeringLoader /> |
| 113 | <ShimmeringLoader className="w-3/4" /> |
| 114 | <ShimmeringLoader className="w-1/2" /> |
| 115 | </div> |
| 116 | )} |
| 117 | |
| 118 | {isError && <AlertError subject="Failed to retrieve upcoming invoice" error={error} />} |
| 119 | |
| 120 | {isSuccess && ( |
| 121 | <div> |
| 122 | <div> |
| 123 | <Table className="w-full text-sm"> |
| 124 | <TableBody> |
| 125 | {planItem && !planFeePaidInAdvance && ( |
| 126 | <TableRow> |
| 127 | <TableCell className="py-2! px-0">{planItem?.description}</TableCell> |
| 128 | <TableCell className="text-right py-2 px-0"> |
| 129 | {!planItem ? ( |
| 130 | '-' |
| 131 | ) : ( |
| 132 | <InvoiceLineItemAmount |
| 133 | amount={planItem.amount} |
| 134 | amountBeforeDiscount={planItem.amount_before_discount} |
| 135 | /> |
| 136 | )} |
| 137 | </TableCell> |
| 138 | </TableRow> |
| 139 | )} |
| 140 | |
| 141 | {/* Compute section */} |
| 142 | <ComputeLineItem |
| 143 | computeItems={regularComputeItems} |
| 144 | title="Compute" |
| 145 | computeCredits={computeCreditsItem} |
| 146 | tooltip={ |
| 147 | <p className="prose text-xs"> |
| 148 | The first project is covered by Compute Credits. Additional projects incur |
| 149 | compute costs starting at <span translate="no">$10</span>/month, independent |
| 150 | of activity. See{' '} |
| 151 | <InlineLink href={`${DOCS_URL}/guides/platform/manage-your-usage/compute`}> |
| 152 | docs |
| 153 | </InlineLink> |
| 154 | . |
| 155 | </p> |
| 156 | } |
| 157 | /> |
| 158 | |
| 159 | {/* Read Replica compute */} |
| 160 | <ComputeLineItem |
| 161 | title="Replica Compute" |
| 162 | computeItems={replicaComputeItems} |
| 163 | tooltip={ |
| 164 | <p className="prose text-xs"> |
| 165 | Each Read Replica is a dedicated database. You are charged for its resources: |
| 166 | Compute, Disk Size, provisioned Disk IOPS, provisioned Disk Throughput, and |
| 167 | IPv4. See{' '} |
| 168 | <InlineLink |
| 169 | href={`${DOCS_URL}/guides/platform/manage-your-usage/read-replicas`} |
| 170 | > |
| 171 | docs |
| 172 | </InlineLink> |
| 173 | . |
| 174 | </p> |
| 175 | } |
| 176 | /> |
| 177 | |
| 178 | {/* Branching compute */} |
| 179 | {branchingComputeItems.length > 0 && ( |
| 180 | <TableRow> |
| 181 | <TableCell className="py-2 px-0"> |
| 182 | <div className="flex items-center gap-1"> |
| 183 | <span>Branching</span> |
| 184 | <InfoTooltip className="max-w-sm"> |
| 185 | <ul className="ml-6 list-disc"> |
| 186 | {branchingComputeItemsDisplay.map((breakdown) => ( |
| 187 | <li key={`branching-breakdown-${breakdown!.project_ref}`}> |
| 188 | {breakdown!.project_name} ({breakdown!.usage} Hours) |
| 189 | </li> |
| 190 | ))} |
| 191 | </ul> |
| 192 | |
| 193 | <p className="mt-2"> |
| 194 | See{' '} |
| 195 | <InlineLink |
| 196 | href={`${DOCS_URL}/guides/platform/manage-your-usage/branching`} |
| 197 | > |
| 198 | docs |
| 199 | </InlineLink>{' '} |
| 200 | on how billing for Branching works. |
| 201 | </p> |
| 202 | </InfoTooltip> |
| 203 | </div> |
| 204 | </TableCell> |
| 205 | <TableCell className="text-right py-2 px-0"> |
| 206 | <InvoiceLineItemAmount |
| 207 | amount={branchingComputeItems.reduce((prev, cur) => prev + cur.amount, 0)} |
| 208 | amountBeforeDiscount={branchingComputeItems.reduce( |
| 209 | (prev, cur) => prev + (cur.amount_before_discount ?? 0), |
| 210 | 0 |
| 211 | )} |
| 212 | /> |
| 213 | </TableCell> |
| 214 | </TableRow> |
| 215 | )} |
| 216 | |
| 217 | {/* Non-compute items */} |
| 218 | {otherItems.map((item) => { |
| 219 | const usageMetric = item.usage_metric as PricingMetric |
| 220 | const sortedBreakdown = (item.breakdown ?? []).sort((a, b) => |
| 221 | a.project_name.localeCompare(b.project_name) |
| 222 | ) |
| 223 | |
| 224 | return ( |
| 225 | <TableRow key={item.description}> |
| 226 | <TableCell className="py-2 px-0"> |
| 227 | <div className="gap-1 flex items-center"> |
| 228 | <span>{item.description ?? 'Unknown'}</span> |
| 229 | {(sortedBreakdown.length > 0 || item.usage_metric !== null) && ( |
| 230 | <InfoTooltip className="max-w-sm"> |
| 231 | {item.item_name === 'minimum_amount' && ( |
| 232 | <p className="mb-2" translate="no"> |
| 233 | Minimum Fee - If your cost is below the minimum fee, you will be |
| 234 | charged the difference as a floor fee |
| 235 | </p> |
| 236 | )} |
| 237 | |
| 238 | {item.unit_price_desc && ( |
| 239 | <p className="mb-2" translate="no"> |
| 240 | Pricing: {item.unit_price_desc} |
| 241 | </p> |
| 242 | )} |
| 243 | |
| 244 | {sortedBreakdown.length > 0 && ( |
| 245 | <> |
| 246 | <p>Projects using {item.description}:</p> |
| 247 | <ul className="ml-6 list-disc"> |
| 248 | {sortedBreakdown.map((breakdown) => { |
| 249 | const unit = billingMetricUnit(usageMetric) |
| 250 | return ( |
| 251 | <li |
| 252 | key={`${item.description}-breakdown-${breakdown.project_ref}`} |
| 253 | > |
| 254 | <InlineLink |
| 255 | target="_blank" |
| 256 | href={`/project/${breakdown.project_ref}`} |
| 257 | > |
| 258 | {breakdown.project_name} |
| 259 | </InlineLink>{' '} |
| 260 | {usageMetric && ( |
| 261 | <span> |
| 262 | ({formatUsage(usageMetric, breakdown)} |
| 263 | {!!unit ? ` ${unit}` : ''}) |
| 264 | </span> |
| 265 | )} |
| 266 | </li> |
| 267 | ) |
| 268 | })} |
| 269 | </ul> |
| 270 | </> |
| 271 | )} |
| 272 | |
| 273 | {usageMetric && usageBillingDocsLink[usageMetric] != null && ( |
| 274 | <p className="mt-2"> |
| 275 | See{' '} |
| 276 | <InlineLink href={usageBillingDocsLink[usageMetric]!}> |
| 277 | docs |
| 278 | </InlineLink>{' '} |
| 279 | on how billing for {item.description} works and{' '} |
| 280 | <InlineLink href={`/organization/${slug}/usage`}> |
| 281 | usage page |
| 282 | </InlineLink>{' '} |
| 283 | for a detailed breakdown. |
| 284 | </p> |
| 285 | )} |
| 286 | </InfoTooltip> |
| 287 | )} |
| 288 | </div> |
| 289 | </TableCell> |
| 290 | <TableCell className="text-right py-2 px-0"> |
| 291 | <InvoiceLineItemAmount |
| 292 | amount={item.amount} |
| 293 | amountBeforeDiscount={item.amount_before_discount} |
| 294 | /> |
| 295 | </TableCell> |
| 296 | </TableRow> |
| 297 | ) |
| 298 | })} |
| 299 | </TableBody> |
| 300 | |
| 301 | <TableFooter> |
| 302 | {prepaidCreditsItem && ( |
| 303 | <TableRow> |
| 304 | <TableCell className="py-2 px-0 flex items-center"> |
| 305 | <span className="mr-2">{prepaidCreditsItem.item_name}</span> |
| 306 | <InfoTooltip className="max-w-xs"> |
| 307 | Prepaid credits purchased upfront, applied automatically against your |
| 308 | invoice. Any remaining balance rolls over to the next billing cycle. |
| 309 | </InfoTooltip> |
| 310 | </TableCell> |
| 311 | <TableCell className="text-right py-2 px-0" translate="no"> |
| 312 | {formatCurrency(prepaidCreditsItem.amount) ?? '-'} |
| 313 | </TableCell> |
| 314 | </TableRow> |
| 315 | )} |
| 316 | <TableRow> |
| 317 | <TableCell className="font-medium py-2 px-0 flex items-center"> |
| 318 | <span className="mr-2">Current Costs</span> |
| 319 | <InfoTooltip> |
| 320 | Costs accumulated from the beginning of the billing cycle up to now. |
| 321 | </InfoTooltip> |
| 322 | </TableCell> |
| 323 | <TableCell className="text-right font-medium py-2 px-0" translate="no"> |
| 324 | {formatCurrency(upcomingInvoice?.amount_total) ?? '-'} |
| 325 | </TableCell> |
| 326 | </TableRow> |
| 327 | |
| 328 | {(!!upcomingInvoice.amount_projected || hasTax || taxFailed) && ( |
| 329 | <TableRow> |
| 330 | <TableCell className="font-medium py-2 px-0 flex items-center"> |
| 331 | <span className="mr-2">Projected Costs</span> |
| 332 | <InfoTooltip className="max-w-sm"> |
| 333 | <p className="mb-2"> |
| 334 | Projected costs at the end of the billing cycle. Includes predictable |
| 335 | costs for Compute Hours, IPv4, Custom Domain and Point-In-Time-Recovery, |
| 336 | but no costs for metrics like MAU, storage or function invocations. Final |
| 337 | amounts may vary depending on your usage. |
| 338 | </p> |
| 339 | |
| 340 | {hasTax && ( |
| 341 | <div className="mt-3 border-t border-muted pt-2 space-y-1"> |
| 342 | <div className="flex items-center justify-between gap-4 text-xs"> |
| 343 | <span>Subtotal</span> |
| 344 | <span translate="no"> |
| 345 | {formatCurrency(upcomingInvoice.tax!.total_amount_excluding_tax)} |
| 346 | </span> |
| 347 | </div> |
| 348 | <div className="flex items-center justify-between gap-4 text-xs"> |
| 349 | <span> |
| 350 | Tax |
| 351 | {upcomingInvoice.tax?.tax_rate_percentage != null && |
| 352 | ` (${upcomingInvoice.tax.tax_rate_percentage}%)`} |
| 353 | </span> |
| 354 | <span translate="no"> |
| 355 | {formatCurrency(upcomingInvoice.tax!.tax_amount)} |
| 356 | </span> |
| 357 | </div> |
| 358 | <p className="text-foreground-lighter pt-1 text-xs"> |
| 359 | Estimated based on your organization's billing address. The final |
| 360 | amount may be adjusted at the end of the billing cycle. |
| 361 | </p> |
| 362 | </div> |
| 363 | )} |
| 364 | |
| 365 | {taxFailed && ( |
| 366 | <p className="mt-3 border-t border-muted pt-2 text-warning"> |
| 367 | We were unable to estimate tax for your organization. Please verify your |
| 368 | billing address in your organization settings. |
| 369 | </p> |
| 370 | )} |
| 371 | </InfoTooltip> |
| 372 | </TableCell> |
| 373 | <TableCell className="text-right font-medium py-2 px-0" translate="no"> |
| 374 | {formatCurrency( |
| 375 | hasTax |
| 376 | ? upcomingInvoice.tax!.total_amount_including_tax |
| 377 | : upcomingInvoice.amount_projected |
| 378 | ) ?? '-'} |
| 379 | </TableCell> |
| 380 | </TableRow> |
| 381 | )} |
| 382 | |
| 383 | {planFeePaidInAdvance && ( |
| 384 | <TableRow className="border-0 hover:bg-transparent"> |
| 385 | <TableCell |
| 386 | className="pt-2! pb-0! px-0 text-foreground-light text-xs text-right" |
| 387 | colSpan={2} |
| 388 | > |
| 389 | Your {organization?.plan?.name && `${organization.plan.name} `}Plan fee for |
| 390 | this period has already been paid. This invoice will only reflect usage |
| 391 | charges. |
| 392 | </TableCell> |
| 393 | </TableRow> |
| 394 | )} |
| 395 | </TableFooter> |
| 396 | </Table> |
| 397 | </div> |
| 398 | </div> |
| 399 | )} |
| 400 | </> |
| 401 | ) |
| 402 | } |
| 403 | |
| 404 | function InvoiceLineItemAmount({ |
| 405 | amountBeforeDiscount, |
| 406 | amount, |
| 407 | }: { |
| 408 | amountBeforeDiscount?: number |
| 409 | amount: number |
| 410 | }) { |
| 411 | if (amountBeforeDiscount && amount < amountBeforeDiscount) { |
| 412 | return ( |
| 413 | <div> |
| 414 | <span className="text-foreground-light line-through mr-2" translate="no"> |
| 415 | {formatCurrency(amountBeforeDiscount)} |
| 416 | </span> |
| 417 | <span translate="no">{formatCurrency(amount)}</span> |
| 418 | </div> |
| 419 | ) |
| 420 | } else { |
| 421 | return <span translate="no">{formatCurrency(amount)}</span> |
| 422 | } |
| 423 | } |
| 424 | |
| 425 | function ComputeLineItem({ |
| 426 | computeItems, |
| 427 | tooltip, |
| 428 | title, |
| 429 | computeCredits, |
| 430 | }: { |
| 431 | title: string |
| 432 | tooltip: React.ReactElement |
| 433 | computeItems: UpcomingInvoiceResponse['lines'] |
| 434 | computeCredits?: UpcomingInvoiceResponse['lines'][number] | null |
| 435 | }) { |
| 436 | const computeProjects = computeItems |
| 437 | .flatMap((item) => |
| 438 | item.breakdown!.map((project) => ({ |
| 439 | ...project, |
| 440 | computeType: item.description, |
| 441 | computeCosts: project.amount!, |
| 442 | })) |
| 443 | ) |
| 444 | // descending by cost |
| 445 | .sort((a, b) => b.computeCosts - a.computeCosts) |
| 446 | |
| 447 | const computeItemsSortedByCost = computeItems |
| 448 | // descending by cost |
| 449 | .sort((a, b) => b.amount - a.amount) |
| 450 | |
| 451 | const computeCosts = Math.max( |
| 452 | 0, |
| 453 | computeItems.reduce((prev, cur) => prev + cur.amount_before_discount, 0) |
| 454 | ) |
| 455 | |
| 456 | const discountedComputeCosts = Math.max( |
| 457 | 0, |
| 458 | computeItems.reduce((prev, cur) => prev + (cur.amount ?? 0), 0) |
| 459 | ) |
| 460 | |
| 461 | if (!computeItems.length) return null |
| 462 | |
| 463 | return ( |
| 464 | <> |
| 465 | <TableRow> |
| 466 | <TableCell className="py-2! px-0 flex items-center gap-1"> |
| 467 | <span>{title}</span> |
| 468 | <InfoTooltip className="max-w-sm">{tooltip}</InfoTooltip> |
| 469 | </TableCell> |
| 470 | <TableCell className="text-right py-2 px-0"> |
| 471 | <InvoiceLineItemAmount |
| 472 | amount={discountedComputeCosts} |
| 473 | amountBeforeDiscount={computeCosts} |
| 474 | /> |
| 475 | </TableCell> |
| 476 | </TableRow> |
| 477 | {computeProjects.map((project) => ( |
| 478 | <TableRow key={project.project_ref} className="text-foreground-light text-xs"> |
| 479 | <TableCell className="py-2! px-0 pl-6"> |
| 480 | {project.project_name} ({project.computeType} - {project.usage} Hours) |
| 481 | </TableCell> |
| 482 | |
| 483 | <TableCell className="py-2! px-0 text-right" translate="no"> |
| 484 | {formatCurrency(project.computeCosts)} |
| 485 | </TableCell> |
| 486 | </TableRow> |
| 487 | ))} |
| 488 | |
| 489 | {/* Fallback to breakdown by instance size if project breakdown not available */} |
| 490 | {!computeProjects.length && |
| 491 | computeItemsSortedByCost.map((computeItem) => ( |
| 492 | <TableRow |
| 493 | key={title + computeItem.usage_metric} |
| 494 | className="text-foreground-light text-xs" |
| 495 | > |
| 496 | <TableCell className="py-2! px-0 pl-6"> |
| 497 | {computeItem.description} - {computeItem.usage_original} Hours |
| 498 | </TableCell> |
| 499 | |
| 500 | <TableCell className="py-2! px-0 text-right" translate="no"> |
| 501 | {formatCurrency(computeItem.amount)} |
| 502 | </TableCell> |
| 503 | </TableRow> |
| 504 | ))} |
| 505 | |
| 506 | {computeCredits && ( |
| 507 | <TableRow className="text-foreground-light text-xs"> |
| 508 | <TableCell className="py-2! px-0 pl-6">Compute Credits</TableCell> |
| 509 | <TableCell className="py-2! px-0 text-right" translate="no"> |
| 510 | {formatCurrency(computeCredits.amount)} |
| 511 | </TableCell> |
| 512 | </TableRow> |
| 513 | )} |
| 514 | </> |
| 515 | ) |
| 516 | } |