feat: Edge functions search and sorting (#40796)

* added sorting and search for edge functions

* updated logic

* updated types

* updated types

* updated search bar

* updated to remove unneeded map
This commit is contained in:
Ali Waseem authored and GitHub committed 2025-11-25 13:37:34 -07:00
1 parent 8de52c4eb3
commit 86755ba1fd
2 files changed
+228 -61

No files matched your search

@@ -0,0 +1,88 @@
import { ArrowDownNarrowWide, ArrowDownWideNarrow } from 'lucide-react'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from 'ui'
export const EDGE_FUNCTIONS_SORT_VALUES = [
'name:asc',
'name:desc',
'created_at:asc',
'created_at:desc',
'updated_at:asc',
'updated_at:desc',
] as const
export type EdgeFunctionsSort = (typeof EDGE_FUNCTIONS_SORT_VALUES)[number]
export type EdgeFunctionsSortColumn = EdgeFunctionsSort extends `${infer Column}:${string}`
? Column
: unknown
export type EdgeFunctionsSortOrder = EdgeFunctionsSort extends `${string}:${infer Order}`
? Order
: unknown
interface EdgeFunctionsSortDropdownProps {
value: EdgeFunctionsSort
onChange: (value: EdgeFunctionsSort) => void
}
function getSortLabel(value: EdgeFunctionsSort) {
const [sortCol] = value.split(':')
return sortCol.replace('_', ' ')
}
export const EdgeFunctionsSortDropdown = ({ value, onChange }: EdgeFunctionsSortDropdownProps) => {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
icon={
value.includes('desc') ? (
<ArrowDownWideNarrow size={14} />
) : (
<ArrowDownNarrowWide size={14} />
)
}
>
Sorted by {getSortLabel(value)}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuRadioGroup
value={value}
onValueChange={(val) => onChange(val as EdgeFunctionsSort)}
>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Sort by name</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value="name:asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="name:desc">Descending</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Sort by created at</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value="created_at:asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="created_at:desc">Descending</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Sort by updated at</DropdownMenuSubTrigger>
<DropdownMenuSubContent>
<DropdownMenuRadioItem value="updated_at:asc">Ascending</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="updated_at:desc">Descending</DropdownMenuRadioItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -1,8 +1,16 @@
import { ExternalLink } from 'lucide-react'
import React from 'react'
import { ExternalLink, Search, X } from 'lucide-react'
import React, { useMemo, useState } from 'react'
import { parseAsString, parseAsStringLiteral, useQueryState } from 'nuqs'
import { useParams } from 'common'
import { DeployEdgeFunctionButton } from 'components/interfaces/EdgeFunctions/DeployEdgeFunctionButton'
import {
EdgeFunctionsSort,
EdgeFunctionsSortColumn,
EdgeFunctionsSortDropdown,
EdgeFunctionsSortOrder,
EDGE_FUNCTIONS_SORT_VALUES,
} from 'components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown'
import { EdgeFunctionsListItem } from 'components/interfaces/Functions/EdgeFunctionsListItem'
import {
FunctionsEmptyState,
@@ -16,7 +24,17 @@ import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import { DOCS_URL, IS_PLATFORM } from 'lib/constants'
import type { NextPageWithLayout } from 'types'
import { Button, Card, Table, TableBody, TableHead, TableHeader, TableRow } from 'ui'
import {
Button,
Card,
Input_Shadcn_,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { PageContainer } from 'ui-patterns/PageContainer'
import {
PageHeader,
@@ -27,6 +45,7 @@ import {
PageHeaderTitle,
} from 'ui-patterns/PageHeader'
import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
import { Input } from 'ui-patterns/DataInputs/Input'
const EdgeFunctionsPage: NextPageWithLayout = () => {
const { ref } = useParams()
@@ -38,6 +57,36 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
isSuccess,
} = useEdgeFunctionsQuery({ projectRef: ref })
const [search, setSearch] = useQueryState('search', parseAsString.withDefault(''))
const [sort, setSortQueryParam] = useQueryState(
'sort',
parseAsStringLiteral<EdgeFunctionsSort>(EDGE_FUNCTIONS_SORT_VALUES).withDefault('name:asc')
)
const filteredFunctions = useMemo(() => {
const temp = (functions ?? []).filter((x) =>
x.name.toLowerCase().includes(search.toLowerCase())
)
const [sortCol, sortOrder] = sort.split(':') as [
EdgeFunctionsSortColumn,
EdgeFunctionsSortOrder,
]
const orderMultiplier = sortOrder === 'asc' ? 1 : -1
return temp.sort((a, b) => {
if (sortCol === 'name') {
return a.name.localeCompare(b.name) * orderMultiplier
}
if (sortCol === 'created_at') {
return (a.created_at - b.created_at) * orderMultiplier
}
if (sortCol === 'updated_at') {
return (a.updated_at - b.updated_at) * orderMultiplier
}
return 0
})
}, [functions, search, sort])
const hasFunctions = (functions ?? []).length > 0
return (
@@ -51,28 +100,68 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
{isSuccess && (
<>
{hasFunctions ? (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>URL</TableHead>
<TableHead className="hidden 2xl:table-cell">Created</TableHead>
<TableHead className="lg:table-cell">Last updated</TableHead>
<TableHead className="lg:table-cell">Deployments</TableHead>
</TableRow>
</TableHeader>
<div className="space-y-4">
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
<div className="relative">
<Input
placeholder="Search function names"
icon={<Search size={12} />}
size="tiny"
className="w-32 md:w-64 pl-8 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
value={search}
onChange={(event) => setSearch(event.target.value)}
actions={[
search && (
<Button
size="tiny"
type="text"
icon={<X />}
onClick={() => setSearch('')}
className="p-0 h-5 w-5"
/>
),
]}
/>
</div>
</div>
<div className="flex items-center gap-2">
<EdgeFunctionsSortDropdown value={sort} onChange={setSortQueryParam} />
</div>
</div>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>URL</TableHead>
<TableHead className="hidden 2xl:table-cell">Created</TableHead>
<TableHead className="lg:table-cell">Last updated</TableHead>
<TableHead className="lg:table-cell">Deployments</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<>
{functions.length > 0 &&
functions.map((item) => (
<EdgeFunctionsListItem key={item.id} function={item} />
))}
</>
</TableBody>
</Table>
</Card>
<TableBody>
<>
{filteredFunctions.length > 0 ? (
filteredFunctions.map((item) => (
<EdgeFunctionsListItem key={item.id} function={item} />
))
) : (
<TableRow>
<TableCell colSpan={5}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{search}" did not return any results
</p>
</TableCell>
</TableRow>
)}
</>
</TableBody>
</Table>
</Card>
</div>
) : (
<FunctionsEmptyState />
)}
@@ -89,46 +178,36 @@ const EdgeFunctionsPage: NextPageWithLayout = () => {
}
EdgeFunctionsPage.getLayout = (page: React.ReactElement) => {
const EdgeFunctionsPageLayout = () => {
const secondaryActions = [
<DocsButton key="docs" href={`${DOCS_URL}/guides/functions`} />,
<Button asChild key="edge-function-examples" type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href="https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions"
>
Examples
</a>
</Button>,
]
return (
<div className="w-full min-h-full flex flex-col items-stretch">
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Edge Functions</PageHeaderTitle>
<PageHeaderDescription>
Deploy edge functions to handle complex business logic
</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
{secondaryActions.map((action) => action)}
{IS_PLATFORM && <DeployEdgeFunctionButton />}
</PageHeaderAside>
</PageHeaderMeta>
</PageHeader>
{page}
</div>
)
}
return (
<DefaultLayout>
<EdgeFunctionsLayout>
<EdgeFunctionsPageLayout />
<div className="w-full min-h-full flex flex-col items-stretch">
<PageHeader size="large">
<PageHeaderMeta>
<PageHeaderSummary>
<PageHeaderTitle>Edge Functions</PageHeaderTitle>
<PageHeaderDescription>
Deploy edge functions to handle complex business logic
</PageHeaderDescription>
</PageHeaderSummary>
<PageHeaderAside>
<DocsButton href={`${DOCS_URL}/guides/functions`} />
<Button asChild type="default" icon={<ExternalLink />}>
<a
target="_blank"
rel="noreferrer"
href="https://github.com/supabase/supabase/tree/master/examples/edge-functions/supabase/functions"
>
Examples
</a>
</Button>
{IS_PLATFORM && <DeployEdgeFunctionButton />}
</PageHeaderAside>
</PageHeaderMeta>
</PageHeader>
{page}
</div>
</EdgeFunctionsLayout>
</DefaultLayout>
)