EdgeFunctionsListItem.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import { IS_PLATFORM, useFlag } from 'common'
  2. import { useParams } from 'common/hooks'
  3. import dayjs from 'dayjs'
  4. import { Check, Copy } from 'lucide-react'
  5. import { useRouter } from 'next/router'
  6. import { useMemo, useState, type MouseEvent } from 'react'
  7. import { cn, copyToClipboard, TableCell, TableRow } from 'ui'
  8. import { ShimmeringLoader, TimestampInfo } from 'ui-patterns'
  9. import { formatErrorRate } from './EdgeFunctionsListItem.utils'
  10. import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
  11. import { useEdgeFunctionsLastHourStatsQuery } from '@/data/edge-functions/edge-functions-last-hour-stats-query'
  12. import {
  13. useEdgeFunctionsQuery,
  14. type EdgeFunctionsResponse,
  15. } from '@/data/edge-functions/edge-functions-query'
  16. import { normalizeFunctionIds } from '@/data/edge-functions/keys'
  17. import { createNavigationHandler } from '@/lib/navigation'
  18. interface EdgeFunctionsListItemProps {
  19. function: EdgeFunctionsResponse
  20. }
  21. export const EdgeFunctionsListItem = ({ function: item }: EdgeFunctionsListItemProps) => {
  22. const router = useRouter()
  23. const { ref } = useParams()
  24. const [isCopied, setIsCopied] = useState(false)
  25. const showLastHourStats = useFlag('edgeFunctionsRequestMetrics')
  26. const { data: endpoint } = useProjectApiUrl({ projectRef: ref })
  27. const functionUrl = `${endpoint}/functions/v1/${item.slug}`
  28. const handleNavigation = createNavigationHandler(
  29. `/project/${ref}/functions/${item.slug}${IS_PLATFORM ? '' : `/details`}`,
  30. router
  31. )
  32. const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
  33. const functionIds = useMemo(() => {
  34. if (!showLastHourStats || !functions) return []
  35. return normalizeFunctionIds(functions.map((item) => item.id))
  36. }, [functions, showLastHourStats])
  37. // [Joshen] We may be paginating the edge functions query in the future
  38. // So this will eventually need to be a list of visibleFunctionIds instead + debounced
  39. const {
  40. data: lastHourStatsAll,
  41. isPending: isStatsPending,
  42. isError: isStatsError,
  43. } = useEdgeFunctionsLastHourStatsQuery(
  44. { projectRef: ref, functionIds },
  45. { enabled: showLastHourStats }
  46. )
  47. const lastHourStats = lastHourStatsAll?.[item.id]
  48. return (
  49. <TableRow
  50. key={item.id}
  51. onClick={handleNavigation}
  52. onAuxClick={handleNavigation}
  53. onKeyDown={handleNavigation}
  54. tabIndex={0}
  55. className="cursor-pointer inset-focus"
  56. >
  57. <TableCell>
  58. <p className="text-sm text-foreground whitespace-nowrap py-2">{item.name}</p>
  59. </TableCell>
  60. <TableCell>
  61. <div className="text-xs text-foreground-light flex gap-2 items-center truncate">
  62. <p title={functionUrl} className="font-mono truncate hidden md:inline max-w-120">
  63. {functionUrl}
  64. </p>
  65. <button
  66. type="button"
  67. className="text-foreground-lighter hover:text-foreground transition"
  68. onClick={(event: MouseEvent<HTMLButtonElement>) => {
  69. function onCopy(value: string) {
  70. setIsCopied(true)
  71. copyToClipboard(value)
  72. setTimeout(() => setIsCopied(false), 3000)
  73. }
  74. event.stopPropagation()
  75. onCopy(functionUrl)
  76. }}
  77. >
  78. {isCopied ? (
  79. <div className="text-brand">
  80. <Check size={14} strokeWidth={3} />
  81. </div>
  82. ) : (
  83. <div className="relative">
  84. <div className="block">
  85. <Copy size={14} strokeWidth={1.5} />
  86. </div>
  87. </div>
  88. )}
  89. </button>
  90. </div>
  91. </TableCell>
  92. <TableCell className="hidden 2xl:table-cell whitespace-nowrap">
  93. {item.created_at ? (
  94. <TimestampInfo
  95. className="text-sm text-foreground-light whitespace-nowrap"
  96. utcTimestamp={item.created_at}
  97. label={dayjs(item.created_at).fromNow()}
  98. />
  99. ) : (
  100. <span className="text-sm text-foreground-light">–</span>
  101. )}
  102. </TableCell>
  103. <TableCell className="lg:table-cell">
  104. {item.updated_at ? (
  105. <TimestampInfo
  106. className="text-sm text-foreground-light whitespace-nowrap"
  107. utcTimestamp={item.updated_at}
  108. label={dayjs(item.updated_at).fromNow()}
  109. />
  110. ) : (
  111. <span className="text-sm text-foreground-light">–</span>
  112. )}
  113. </TableCell>
  114. {showLastHourStats && (
  115. <>
  116. <TableCell className="lg:table-cell whitespace-nowrap">
  117. {isStatsPending ? (
  118. <ShimmeringLoader className="w-12" />
  119. ) : isStatsError ? (
  120. <p className="text-foreground-lighter" title="Failed to load stats">
  121. -
  122. </p>
  123. ) : (
  124. <p className="text-foreground-light">
  125. {lastHourStats !== undefined ? lastHourStats.requestsCount.toLocaleString() : '-'}
  126. </p>
  127. )}
  128. </TableCell>
  129. <TableCell className="lg:table-cell whitespace-nowrap">
  130. {isStatsPending ? (
  131. <ShimmeringLoader className="w-12" />
  132. ) : isStatsError ? (
  133. <p className="text-foreground-lighter" title="Failed to load stats">
  134. -
  135. </p>
  136. ) : lastHourStats !== undefined ? (
  137. <span
  138. className={cn(
  139. 'text-sm',
  140. lastHourStats.errorRate >= 1
  141. ? 'text-destructive'
  142. : lastHourStats.errorRate > 0.1
  143. ? 'text-warning'
  144. : 'text-foreground-light'
  145. )}
  146. >
  147. {formatErrorRate(lastHourStats.errorRate)}
  148. </span>
  149. ) : (
  150. <p className="text-foreground-lighter">-</p>
  151. )}
  152. </TableCell>
  153. </>
  154. )}
  155. <TableCell className="hidden 2xl:table-cell">
  156. <p className="text-foreground-light">{item.version}</p>
  157. <button tabIndex={-1} className="sr-only">
  158. Go to function details
  159. </button>
  160. </TableCell>
  161. </TableRow>
  162. )
  163. }