diff --git a/apps/studio/components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown.tsx b/apps/studio/components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown.tsx new file mode 100644 index 00000000000..2c1368d4b7f --- /dev/null +++ b/apps/studio/components/interfaces/EdgeFunctions/EdgeFunctionsSortDropdown.tsx @@ -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 ( + + + + + + onChange(val as EdgeFunctionsSort)} + > + + Sort by name + + Ascending + Descending + + + + Sort by created at + + Ascending + Descending + + + + Sort by updated at + + Ascending + Descending + + + + + + ) +} diff --git a/apps/studio/pages/project/[ref]/functions/index.tsx b/apps/studio/pages/project/[ref]/functions/index.tsx index 90a558c8459..3601eb75a6e 100644 --- a/apps/studio/pages/project/[ref]/functions/index.tsx +++ b/apps/studio/pages/project/[ref]/functions/index.tsx @@ -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(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 ? ( - - - - - Name - URL - Created - Last updated - Deployments - - +
+
+
+
+ } + 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 && ( +
+
+
+ +
+
+ +
+ + + Name + URL + Created + Last updated + Deployments + + - - <> - {functions.length > 0 && - functions.map((item) => ( - - ))} - - -
-
+ + <> + {filteredFunctions.length > 0 ? ( + filteredFunctions.map((item) => ( + + )) + ) : ( + + +

No results found

+

+ Your search for "{search}" did not return any results +

+
+
+ )} + +
+ + + ) : ( )} @@ -89,46 +178,36 @@ const EdgeFunctionsPage: NextPageWithLayout = () => { } EdgeFunctionsPage.getLayout = (page: React.ReactElement) => { - const EdgeFunctionsPageLayout = () => { - const secondaryActions = [ - , - , - ] - - return ( -
- - - - Edge Functions - - Deploy edge functions to handle complex business logic - - - - {secondaryActions.map((action) => action)} - {IS_PLATFORM && } - - - - - {page} -
- ) - } - return ( - +
+ + + + Edge Functions + + Deploy edge functions to handle complex business logic + + + + + + {IS_PLATFORM && } + + + + + {page} +
)