mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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>
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user