index.tsx274 lines · main
1import { useFlag, useParams } from 'common'
2import { ExternalLink, RefreshCw, Search, X } from 'lucide-react'
3import { useRouter } from 'next/router'
4import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
5import React, { useMemo, useRef } from 'react'
6import { Button, Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
7import { Admonition } from 'ui-patterns'
8import { Input } from 'ui-patterns/DataInputs/Input'
9import { PageContainer } from 'ui-patterns/PageContainer'
10import {
11 PageHeader,
12 PageHeaderAside,
13 PageHeaderDescription,
14 PageHeaderMeta,
15 PageHeaderSummary,
16 PageHeaderTitle,
17} from 'ui-patterns/PageHeader'
18import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
19import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
20
21import { DeployEdgeFunctionButton } from '@/components/interfaces/EdgeFunctions/DeployEdgeFunctionButton'
22import {
23 EDGE_FUNCTIONS_SORT_VALUES,
24 EdgeFunctionsSort,
25 EdgeFunctionsSortColumn,
26 EdgeFunctionsSortDropdown,
27 EdgeFunctionsSortOrder,
28} from '@/components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown'
29import { EdgeFunctionsListItem } from '@/components/interfaces/Functions/EdgeFunctionsListItem'
30import {
31 FunctionsEmptyState,
32 FunctionsInstructionsLocal,
33} from '@/components/interfaces/Functions/FunctionsEmptyState'
34import { TerminalInstructionsDialog } from '@/components/interfaces/Functions/TerminalInstructionsDialog'
35import { useFunctionsListShortcuts } from '@/components/interfaces/Functions/useFunctionsListShortcuts'
36import DefaultLayout from '@/components/layouts/DefaultLayout'
37import EdgeFunctionsLayout from '@/components/layouts/EdgeFunctionsLayout/EdgeFunctionsLayout'
38import AlertError from '@/components/ui/AlertError'
39import { DocsButton } from '@/components/ui/DocsButton'
40import { ShortcutTooltip } from '@/components/ui/ShortcutTooltip'
41import { useEdgeFunctionsQuery } from '@/data/edge-functions/edge-functions-query'
42import { useIsProjectActive } from '@/hooks/misc/useSelectedProject'
43import { DOCS_URL, IS_PLATFORM } from '@/lib/constants'
44import { onSearchInputEscape } from '@/lib/keyboard'
45import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
46import type { NextPageWithLayout } from '@/types'
47
48const EdgeFunctionsPage: NextPageWithLayout = () => {
49 const router = useRouter()
50 const { ref } = useParams()
51 const showLastHourStats = useFlag('edgeFunctionsRequestMetrics')
52 const isProjectActive = useIsProjectActive()
53
54 const searchInputRef = useRef<HTMLInputElement>(null)
55
56 const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
57 const [sort, setSortQueryParam] = useQueryState(
58 'sort',
59 parseAsStringLiteral<EdgeFunctionsSort>(EDGE_FUNCTIONS_SORT_VALUES).withDefault('name:asc')
60 )
61
62 const {
63 data: functions,
64 error,
65 isPending: isLoading,
66 isError,
67 isSuccess,
68 isFetching,
69 refetch,
70 } = useEdgeFunctionsQuery({ projectRef: ref })
71
72 useFunctionsListShortcuts({
73 searchInputRef,
74 setSearch,
75 sort,
76 setSort: setSortQueryParam,
77 canCreateNew: isProjectActive,
78 onCreateNew: () => router.push(`/project/${ref}/functions/new`),
79 onRefresh: () => {
80 refetch()
81 },
82 })
83
84 const filteredFunctions = useMemo(() => {
85 const temp = (functions ?? []).filter((x) =>
86 x.name.toLowerCase().includes(search.toLowerCase())
87 )
88 const [sortCol, sortOrder] = sort.split(':') as [
89 EdgeFunctionsSortColumn,
90 EdgeFunctionsSortOrder,
91 ]
92 const orderMultiplier = sortOrder === 'asc' ? 1 : -1
93
94 return temp.sort((a, b) => {
95 if (sortCol === 'name') {
96 return a.name.localeCompare(b.name) * orderMultiplier
97 }
98 if (sortCol === 'created_at') {
99 return (a.created_at - b.created_at) * orderMultiplier
100 }
101 if (sortCol === 'updated_at') {
102 return (a.updated_at - b.updated_at) * orderMultiplier
103 }
104 return 0
105 })
106 }, [functions, search, sort])
107
108 const hasFunctions = (functions ?? []).length > 0
109
110 return (
111 <PageContainer size="large">
112 <PageSection>
113 <PageSectionContent>
114 <div className="flex flex-col gap-6">
115 {isLoading && <GenericSkeletonLoader />}
116 {isError &&
117 (IS_PLATFORM ? (
118 <AlertError error={error} subject="Failed to retrieve edge functions" />
119 ) : (
120 <Admonition type="warning" title="Failed to retrieve edge functions">
121 <p className="prose [&>code]:text-xs text-sm">
122 Local functions can be found at <code>briven/functions</code> folder.
123 </p>
124 </Admonition>
125 ))}
126 {isSuccess && (
127 <>
128 {hasFunctions ? (
129 <div className="space-y-4">
130 <div className="flex items-center gap-2">
131 <div className="flex items-center gap-2">
132 <div className="relative">
133 <ShortcutTooltip
134 shortcutId={SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH}
135 label="Search functions"
136 side="bottom"
137 >
138 <Input
139 ref={searchInputRef}
140 placeholder="Search function names"
141 icon={<Search />}
142 size="tiny"
143 className="w-32 md:w-64"
144 value={search}
145 onChange={(event) => setSearch(event.target.value)}
146 onKeyDown={onSearchInputEscape(search, setSearch)}
147 actions={[
148 search && (
149 <Button
150 key="clear"
151 size="tiny"
152 type="text"
153 icon={<X />}
154 onClick={() => setSearch('')}
155 className="p-0 h-5 w-5"
156 />
157 ),
158 ]}
159 />
160 </ShortcutTooltip>
161 </div>
162 </div>
163 <div className="flex items-center gap-2">
164 <EdgeFunctionsSortDropdown value={sort} onChange={setSortQueryParam} />
165 </div>
166 <ShortcutTooltip
167 shortcutId={SHORTCUT_IDS.FUNCTIONS_LIST_REFRESH}
168 side="bottom"
169 >
170 <Button
171 type="default"
172 icon={<RefreshCw />}
173 loading={isFetching}
174 onClick={() => refetch()}
175 >
176 Refresh
177 </Button>
178 </ShortcutTooltip>
179 <span className="border-l border-default pl-2 text-xs text-foreground-light">
180 {search && filteredFunctions.length !== functions.length
181 ? `Viewing ${filteredFunctions.length} of ${functions.length} functions in total`
182 : `Viewing ${functions.length} ${functions.length === 1 ? 'function' : 'functions'} in total`}
183 </span>
184 </div>
185 <Card>
186 <Table>
187 <TableHeader>
188 <TableRow>
189 <TableHead>Name</TableHead>
190 <TableHead>URL</TableHead>
191 <TableHead className="hidden 2xl:table-cell">Created</TableHead>
192 <TableHead className="lg:table-cell">Updated</TableHead>
193 {showLastHourStats && (
194 <>
195 <TableHead className="lg:table-cell">Total requests (1h)</TableHead>
196 <TableHead className="lg:table-cell">5xx error rate (1h)</TableHead>
197 </>
198 )}
199 <TableHead className="hidden 2xl:table-cell">Deployments</TableHead>
200 </TableRow>
201 </TableHeader>
202
203 <TableBody>
204 <>
205 {filteredFunctions.length > 0 ? (
206 filteredFunctions.map((item) => (
207 <EdgeFunctionsListItem key={item.id} function={item} />
208 ))
209 ) : (
210 <TableRow>
211 <TableCell colSpan={showLastHourStats ? 8 : 6}>
212 <p className="text-sm text-foreground">No results found</p>
213 <p className="text-sm text-foreground-light">
214 Your search for "{search}" did not return any results
215 </p>
216 </TableCell>
217 </TableRow>
218 )}
219 </>
220 </TableBody>
221 </Table>
222 </Card>
223 </div>
224 ) : (
225 <FunctionsEmptyState />
226 )}
227 </>
228 )}
229 {!IS_PLATFORM && <FunctionsInstructionsLocal />}
230 </div>
231 </PageSectionContent>
232 </PageSection>
233 </PageContainer>
234 )
235}
236
237EdgeFunctionsPage.getLayout = (page: React.ReactElement) => {
238 return (
239 <DefaultLayout>
240 <EdgeFunctionsLayout title="Edge Functions">
241 <div className="w-full min-h-full flex flex-col items-stretch">
242 <PageHeader size="large">
243 <PageHeaderMeta>
244 <PageHeaderSummary>
245 <PageHeaderTitle>Edge Functions</PageHeaderTitle>
246 <PageHeaderDescription>
247 Run server-side logic close to your users
248 </PageHeaderDescription>
249 </PageHeaderSummary>
250 <PageHeaderAside>
251 <DocsButton href={`${DOCS_URL}/guides/functions`} />
252 <Button asChild type="default" icon={<ExternalLink />}>
253 <a
254 target="_blank"
255 rel="noreferrer"
256 href="https://github.com/briven/briven/tree/master/examples/edge-functions/briven/functions"
257 >
258 Examples
259 </a>
260 </Button>
261 {IS_PLATFORM && <DeployEdgeFunctionButton />}
262 </PageHeaderAside>
263 </PageHeaderMeta>
264 </PageHeader>
265
266 {page}
267 </div>
268 </EdgeFunctionsLayout>
269 <TerminalInstructionsDialog />
270 </DefaultLayout>
271 )
272}
273
274export default EdgeFunctionsPage