{toolbarLeft}
{filters?.map((filter) => {
+ const currentValue = filterState[filter.id]
+ if (filter.render) {
+ return (
+
+ {filter.render({
+ value: currentValue,
+ onChange: (value) => updateFilter(filter.id, value),
+ filterState,
+ })}
+
+ )
+ }
+
if (filter.type === 'search') {
return (
- updateFilter(filter.id, e.target.value)}
- className={cn(
- 'h-8 rounded-md border border-control bg-surface-100',
- 'pl-7 pr-3 text-xs text-foreground placeholder:text-foreground-lighter',
- 'focus:outline-none focus:ring-1 focus:ring-brand',
- 'min-w-[180px]'
- )}
+ size="small"
+ className={cn('h-8 bg-surface-100', 'pl-7 pr-3 text-xs', 'min-w-[180px]')}
/>
)
}
- if (filter.type === 'select' || filter.type === 'multi-select') {
+ if (filter.type === 'select') {
+ const value = (filterState[filter.id] as string) ?? '__all__'
return (
-
+
+
+
+
+
+ {filter.placeholder ?? `All ${filter.label}`}
+
+ {filter.options?.map((opt) => (
+
+ {opt.label}
+
+ ))}
+
+
+ )
+ }
+
+ if (filter.type === 'multi-select') {
+ const selected = Array.isArray(filterState[filter.id])
+ ? (filterState[filter.id] as string[])
+ : []
+ return (
+
+
+
+
+
+ {filter.options?.map((opt) => (
+ {
+ const next = checked
+ ? [...selected, opt.value]
+ : selected.filter((v) => v !== opt.value)
+ updateFilter(filter.id, next)
+ }}
+ >
+ {opt.label}
+
+ ))}
+
+
)
}
diff --git a/apps/studio/components/v2/DataTableRenderer/cells/AvatarCell.tsx b/apps/studio/components/v2/DataTableRenderer/cells/AvatarCell.tsx
index 4ffc2aa000a..af6234d9c36 100644
--- a/apps/studio/components/v2/DataTableRenderer/cells/AvatarCell.tsx
+++ b/apps/studio/components/v2/DataTableRenderer/cells/AvatarCell.tsx
@@ -33,11 +33,11 @@ export function AvatarCell({ value }: AvatarCellProps) {
return (
{initials}
- {str}
+ {str}
)
}
diff --git a/apps/studio/components/v2/DataTableRenderer/cells/DateCell.tsx b/apps/studio/components/v2/DataTableRenderer/cells/DateCell.tsx
index 4cb6219e1a7..802faa6c611 100644
--- a/apps/studio/components/v2/DataTableRenderer/cells/DateCell.tsx
+++ b/apps/studio/components/v2/DataTableRenderer/cells/DateCell.tsx
@@ -1,3 +1,6 @@
+import { getColumnType } from 'components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/DateTimeInput/DateTimeInput.utils'
+import dayjs from 'dayjs'
+import { DATETIME_FORMAT } from 'lib/constants'
import { Tooltip, TooltipContent, TooltipTrigger } from 'ui'
interface DateCellProps {
@@ -5,28 +8,11 @@ interface DateCellProps {
showTime?: boolean
}
-function formatRelative(date: Date): string {
- const diff = Date.now() - date.getTime()
- const seconds = Math.floor(diff / 1000)
- const minutes = Math.floor(seconds / 60)
- const hours = Math.floor(minutes / 60)
- const days = Math.floor(hours / 24)
-
- if (seconds < 60) return 'just now'
- if (minutes < 60) return `${minutes}m ago`
- if (hours < 24) return `${hours}h ago`
- if (days < 30) return `${days}d ago`
- return date.toLocaleDateString()
-}
-
function formatFull(date: Date, showTime: boolean): string {
- if (showTime) {
- return date
- .toISOString()
- .replace('T', ' ')
- .replace(/\.\d{3}Z$/, ' UTC')
- }
- return date.toDateString()
+ const valueType = getColumnType(showTime ? 'timestamp' : 'date')
+ if (valueType === 'date') return dayjs(date).format('YYYY-MM-DD')
+ if (valueType === 'time') return dayjs(date).format('HH:mm:ss')
+ return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}
export function DateCell({ value, showTime = false }: DateCellProps) {
@@ -42,7 +28,7 @@ export function DateCell({ value, showTime = false }: DateCellProps) {
return (
- {formatRelative(date)}
+ {dayjs(date).format(DATETIME_FORMAT)}
{formatFull(date, showTime)}
diff --git a/apps/studio/components/v2/DataTableRenderer/index.tsx b/apps/studio/components/v2/DataTableRenderer/index.tsx
index 830bde493a1..0178b2e2be3 100644
--- a/apps/studio/components/v2/DataTableRenderer/index.tsx
+++ b/apps/studio/components/v2/DataTableRenderer/index.tsx
@@ -24,6 +24,7 @@ export function DataTableRenderer>({
onRetry,
sort: controlledSort,
onSortChange: controlledOnSortChange,
+ renderSortControl,
filters,
filterState: controlledFilterState,
onFilterChange: controlledOnFilterChange,
@@ -172,7 +173,8 @@ export function DataTableRenderer>({
const hasPagination = !!pagination && !!onPageChange
const hasRowActions = !!rowActions && rowActions.length > 0
const showChromeToolbar =
- !hideToolbar && (hasBulkBar || Boolean(filters?.length) || toolbarLeft || toolbarRight)
+ !hideToolbar &&
+ (hasBulkBar || Boolean(filters?.length) || toolbarLeft || toolbarRight || renderSortControl)
return (
@@ -190,7 +192,15 @@ export function DataTableRenderer
>({
filterState={activeFilterState}
onFilterChange={handleFilterChange}
toolbarLeft={toolbarLeft}
- toolbarRight={toolbarRight}
+ toolbarRight={
+ <>
+ {renderSortControl?.({
+ sort: activeSort,
+ onSortChange: handleSortChange,
+ })}
+ {toolbarRight}
+ >
+ }
/>
) : null}
diff --git a/apps/studio/components/v2/DataTableRenderer/types.ts b/apps/studio/components/v2/DataTableRenderer/types.ts
index 93b98f78f7c..0152d79463c 100644
--- a/apps/studio/components/v2/DataTableRenderer/types.ts
+++ b/apps/studio/components/v2/DataTableRenderer/types.ts
@@ -49,6 +49,11 @@ export interface FilterDefinition {
type: 'search' | 'select' | 'multi-select' | 'toggle'
options?: Array<{ value: string; label: string }>
placeholder?: string
+ render?: (args: {
+ value: string | string[] | boolean | undefined
+ onChange: (value: string | string[] | boolean) => void
+ filterState: FilterState
+ }) => ReactNode
}
export type FilterState = Record
@@ -106,6 +111,10 @@ export interface DataTableRendererProps = any> {
// If omitted, sorting is handled client-side internally
sort?: SortState | null
onSortChange?: (sort: SortState | null) => void
+ renderSortControl?: (args: {
+ sort: SortState | null
+ onSortChange: (sort: SortState | null) => void
+ }) => ReactNode
// ── Filtering (controlled) ──
// If omitted, filtering is handled client-side when filters is defined
diff --git a/apps/studio/components/v2/hooks/useEntityPanelParams.ts b/apps/studio/components/v2/hooks/useEntityPanelParams.ts
new file mode 100644
index 00000000000..fd17263a91b
--- /dev/null
+++ b/apps/studio/components/v2/hooks/useEntityPanelParams.ts
@@ -0,0 +1,34 @@
+'use client'
+
+import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
+
+export function useEntityPanelParams() {
+ const [isCreating, setIsCreating] = useQueryState(
+ 'new',
+ parseAsBoolean.withDefault(false).withOptions({ history: 'push', clearOnDefault: true })
+ )
+ const [editingId, setEditingId] = useQueryState(
+ 'edit',
+ parseAsString.withOptions({ history: 'push', clearOnDefault: true })
+ )
+ const [duplicatingId, setDuplicatingId] = useQueryState(
+ 'duplicate',
+ parseAsString.withOptions({ history: 'push', clearOnDefault: true })
+ )
+
+ const closePanels = () => {
+ setIsCreating(false)
+ setEditingId(null)
+ setDuplicatingId(null)
+ }
+
+ return {
+ isCreating,
+ setIsCreating,
+ editingId,
+ setEditingId,
+ duplicatingId,
+ setDuplicatingId,
+ closePanels,
+ }
+}
diff --git a/apps/studio/components/v2/views/V2EdgeFunctionsList.tsx b/apps/studio/components/v2/views/V2EdgeFunctionsList.tsx
index b1d4650f677..b858d41f8f2 100644
--- a/apps/studio/components/v2/views/V2EdgeFunctionsList.tsx
+++ b/apps/studio/components/v2/views/V2EdgeFunctionsList.tsx
@@ -2,10 +2,12 @@
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
import type { EdgeFunctionsResponse } from 'data/edge-functions/edge-functions-query'
+import { useRouter } from 'next/navigation'
import { useV2Params } from '@/app/v2/V2ParamsContext'
import { DataTableRenderer } from '@/components/v2/DataTableRenderer'
import type { DataTableColumn } from '@/components/v2/DataTableRenderer'
+import { useV2DashboardStore } from '@/stores/v2-dashboard'
const EDGE_FUNCTIONS_COLUMNS: DataTableColumn[] = [
{
@@ -54,7 +56,9 @@ const EDGE_FUNCTIONS_COLUMNS: DataTableColumn[] = [
]
export function V2EdgeFunctionsList() {
+ const router = useRouter()
const { projectRef } = useV2Params()
+ const openDataTab = useV2DashboardStore((s) => s.openDataTab)
const {
data: functions,
@@ -73,6 +77,19 @@ export function V2EdgeFunctionsList() {
filters={[
{ id: 'search', label: 'Search', type: 'search', placeholder: 'Filter edge functions…' },
]}
+ onRowClick={(row) => {
+ if (!projectRef) return
+ const path = `/v2/project/${projectRef}/data/edge-functions/${row.slug}`
+ openDataTab({
+ id: `edge-function:${row.slug}`,
+ label: row.name || row.slug,
+ type: 'detail',
+ category: 'edge-functions',
+ domain: 'fn',
+ path,
+ })
+ router.push(path)
+ }}
emptyState={{
title: 'No edge functions yet',
description: 'Deploy your first edge function to get started.',
diff --git a/apps/studio/components/v2/views/V2FunctionsList.tsx b/apps/studio/components/v2/views/V2FunctionsList.tsx
index 511501a0234..8eef843003c 100644
--- a/apps/studio/components/v2/views/V2FunctionsList.tsx
+++ b/apps/studio/components/v2/views/V2FunctionsList.tsx
@@ -5,8 +5,10 @@ import type { DatabaseFunction } from 'data/database-functions/database-function
import { useSchemasQuery } from 'data/database/schemas-query'
import { isValidConnString } from 'data/fetchers'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { CopyPlus, Pencil, Plus } from 'lucide-react'
import { useState } from 'react'
import {
+ Button,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
@@ -15,8 +17,10 @@ import {
} from 'ui'
import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { CreateFunction } from '@/components/interfaces/Database/Functions/CreateFunction'
import { DataTableRenderer } from '@/components/v2/DataTableRenderer'
import type { DataTableColumn } from '@/components/v2/DataTableRenderer'
+import { useEntityPanelParams } from '@/components/v2/hooks/useEntityPanelParams'
const FUNCTIONS_COLUMNS: DataTableColumn[] = [
{
@@ -77,6 +81,15 @@ const FUNCTIONS_COLUMNS: DataTableColumn[] = [
export function V2FunctionsList() {
const { projectRef } = useV2Params()
const [schema, setSchema] = useState('public')
+ const {
+ isCreating,
+ setIsCreating,
+ editingId,
+ setEditingId,
+ duplicatingId,
+ setDuplicatingId,
+ closePanels,
+ } = useEntityPanelParams()
const { data: project, isPending: isProjectPending } = useProjectDetailQuery(
{ ref: projectRef },
@@ -103,6 +116,8 @@ export function V2FunctionsList() {
const schemaFunctions = (functions ?? []).filter(
(f) => schema === '__all__' || f.schema === schema
)
+ const functionToEdit = schemaFunctions.find((f) => f.id.toString() === editingId)
+ const functionToDuplicate = schemaFunctions.find((f) => f.id.toString() === duplicatingId)
const schemaSelector = (
@@ -121,25 +136,64 @@ export function V2FunctionsList() {
)
return (
-
- columns={FUNCTIONS_COLUMNS}
- rows={schemaFunctions}
- rowKey="id"
- isLoading={isProjectPending || (shouldFetch && isFunctionsPending)}
- error={isError ? (error as Error) : null}
- filters={[
- {
- id: 'search',
- label: 'Search',
- type: 'search',
- placeholder: 'Filter functions…',
- },
- ]}
- toolbarLeft={schemaSelector}
- emptyState={{
- title: 'No functions found',
- description: 'Create a database function to get started.',
- }}
- />
+ <>
+
+ columns={FUNCTIONS_COLUMNS}
+ rows={schemaFunctions}
+ rowKey="id"
+ isLoading={isProjectPending || (shouldFetch && isFunctionsPending)}
+ error={isError ? (error as Error) : null}
+ filters={[
+ {
+ id: 'search',
+ label: 'Search',
+ type: 'search',
+ placeholder: 'Filter functions…',
+ },
+ ]}
+ toolbarLeft={schemaSelector}
+ toolbarRight={
+ }
+ onClick={() => setIsCreating(true)}
+ >
+ Create function
+
+ }
+ rowActions={[
+ {
+ id: 'edit',
+ label: 'Edit function',
+ icon: ,
+ onClick: (row) => {
+ setDuplicatingId(null)
+ setEditingId(row.id.toString())
+ },
+ },
+ {
+ id: 'duplicate',
+ label: 'Duplicate function',
+ icon: ,
+ onClick: (row) => {
+ setEditingId(null)
+ setDuplicatingId(row.id.toString())
+ },
+ },
+ ]}
+ emptyState={{
+ title: 'No functions found',
+ description: 'Create a database function to get started.',
+ }}
+ />
+
+
+ >
)
}
diff --git a/apps/studio/components/v2/views/V2RolesList.tsx b/apps/studio/components/v2/views/V2RolesList.tsx
index 64dbcd64556..acd1bf076a8 100644
--- a/apps/studio/components/v2/views/V2RolesList.tsx
+++ b/apps/studio/components/v2/views/V2RolesList.tsx
@@ -4,10 +4,14 @@ import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import type { PgRole } from 'data/database-roles/database-roles-query'
import { isValidConnString } from 'data/fetchers'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { Plus } from 'lucide-react'
+import { Button } from 'ui'
import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { CreateRolePanel } from '@/components/interfaces/Database/Roles/CreateRolePanel'
import { DataTableRenderer } from '@/components/v2/DataTableRenderer'
import type { DataTableColumn } from '@/components/v2/DataTableRenderer'
+import { useEntityPanelParams } from '@/components/v2/hooks/useEntityPanelParams'
const ROLES_COLUMNS: DataTableColumn[] = [
{
@@ -93,6 +97,7 @@ const ROLES_COLUMNS: DataTableColumn[] = [
export function V2RolesList() {
const { projectRef } = useV2Params()
+ const { isCreating, setIsCreating } = useEntityPanelParams()
const { data: project, isPending: isProjectPending } = useProjectDetailQuery(
{ ref: projectRef },
@@ -112,25 +117,39 @@ export function V2RolesList() {
)
return (
-
- columns={ROLES_COLUMNS}
- rows={(roles as PgRole[]) ?? []}
- rowKey="name"
- isLoading={isProjectPending || (shouldFetch && isRolesPending)}
- error={isError ? (error as Error) : null}
- compact
- filters={[
- {
- id: 'search',
- label: 'Search',
- type: 'search',
- placeholder: 'Filter roles…',
- },
- ]}
- emptyState={{
- title: 'No roles found',
- description: 'Database roles will appear here.',
- }}
- />
+ <>
+
+ columns={ROLES_COLUMNS}
+ rows={(roles as PgRole[]) ?? []}
+ rowKey="name"
+ isLoading={isProjectPending || (shouldFetch && isRolesPending)}
+ error={isError ? (error as Error) : null}
+ compact
+ filters={[
+ {
+ id: 'search',
+ label: 'Search',
+ type: 'search',
+ placeholder: 'Filter roles…',
+ },
+ ]}
+ toolbarRight={
+ }
+ onClick={() => setIsCreating(true)}
+ >
+ Add role
+
+ }
+ emptyState={{
+ title: 'No roles found',
+ description: 'Database roles will appear here.',
+ }}
+ />
+
+ setIsCreating(false)} />
+ >
)
}
diff --git a/apps/studio/components/v2/views/V2TypesList.tsx b/apps/studio/components/v2/views/V2TypesList.tsx
index 7718a0e2117..e364181ffdb 100644
--- a/apps/studio/components/v2/views/V2TypesList.tsx
+++ b/apps/studio/components/v2/views/V2TypesList.tsx
@@ -4,10 +4,15 @@ import { useEnumeratedTypesQuery } from 'data/enumerated-types/enumerated-types-
import type { EnumeratedType } from 'data/enumerated-types/enumerated-types-query'
import { isValidConnString } from 'data/fetchers'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { Pencil, Plus } from 'lucide-react'
+import { Button } from 'ui'
import { useV2Params } from '@/app/v2/V2ParamsContext'
+import CreateEnumeratedTypeSidePanel from '@/components/interfaces/Database/EnumeratedTypes/CreateEnumeratedTypeSidePanel'
+import EditEnumeratedTypeSidePanel from '@/components/interfaces/Database/EnumeratedTypes/EditEnumeratedTypeSidePanel'
import { DataTableRenderer } from '@/components/v2/DataTableRenderer'
import type { DataTableColumn } from '@/components/v2/DataTableRenderer'
+import { useEntityPanelParams } from '@/components/v2/hooks/useEntityPanelParams'
const TYPES_COLUMNS: DataTableColumn[] = [
{
@@ -47,6 +52,7 @@ const TYPES_COLUMNS: DataTableColumn[] = [
export function V2TypesList() {
const { projectRef } = useV2Params()
+ const { isCreating, setIsCreating, editingId, setEditingId } = useEntityPanelParams()
const { data: project, isPending: isProjectPending } = useProjectDetailQuery(
{ ref: projectRef },
@@ -64,17 +70,49 @@ export function V2TypesList() {
{ projectRef, connectionString: project?.connectionString },
{ enabled: shouldFetch }
)
+ const selectedType = (types ?? []).find((type) => type.id.toString() === editingId)
return (
-
- columns={TYPES_COLUMNS}
- rows={(types as EnumeratedType[]) ?? []}
- rowKey="id"
- isLoading={isProjectPending || (shouldFetch && isTypesPending)}
- error={isError ? (error as Error) : null}
- compact
- filters={[{ id: 'search', label: 'Search', type: 'search', placeholder: 'Filter types…' }]}
- emptyState={{ title: 'No enumerated types found' }}
- />
+ <>
+
+ columns={TYPES_COLUMNS}
+ rows={(types as EnumeratedType[]) ?? []}
+ rowKey="id"
+ isLoading={isProjectPending || (shouldFetch && isTypesPending)}
+ error={isError ? (error as Error) : null}
+ compact
+ filters={[{ id: 'search', label: 'Search', type: 'search', placeholder: 'Filter types…' }]}
+ toolbarRight={
+ }
+ onClick={() => setIsCreating(true)}
+ >
+ Create type
+
+ }
+ rowActions={[
+ {
+ id: 'edit',
+ label: 'Edit type',
+ icon: ,
+ onClick: (row) => setEditingId(row.id.toString()),
+ },
+ ]}
+ emptyState={{ title: 'No enumerated types found' }}
+ />
+
+ setIsCreating(false)}
+ schema="public"
+ />
+ setEditingId(null)}
+ />
+ >
)
}
diff --git a/apps/studio/components/v2/views/V2UsersList.tsx b/apps/studio/components/v2/views/V2UsersList.tsx
index 1ec8e977dc6..f581fe1e88f 100644
--- a/apps/studio/components/v2/views/V2UsersList.tsx
+++ b/apps/studio/components/v2/views/V2UsersList.tsx
@@ -1,15 +1,19 @@
'use client'
+import { SortDropdown } from 'components/interfaces/Auth/Users/SortDropdown'
import { useUsersCountQuery } from 'data/auth/users-count-query'
import { useUsersInfiniteQuery } from 'data/auth/users-infinite-query'
-import type { User } from 'data/auth/users-infinite-query'
+import type { User, Filter as UserTypeFilter } from 'data/auth/users-infinite-query'
import { isValidConnString } from 'data/fetchers'
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
+import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useState } from 'react'
import { useV2Params } from '@/app/v2/V2ParamsContext'
+import { AddUserDropdown } from '@/components/interfaces/Auth/Users/AddUserDropdown'
+import { PROVIDER_FILTER_OPTIONS } from '@/components/interfaces/Auth/Users/Users.constants'
import { DataTableRenderer } from '@/components/v2/DataTableRenderer'
-import type { DataTableColumn, SortState } from '@/components/v2/DataTableRenderer'
+import type { DataTableColumn, FilterState, SortState } from '@/components/v2/DataTableRenderer'
const PAGE_SIZE = 50
@@ -85,7 +89,22 @@ const USERS_COLUMNS: DataTableColumn[] = [
export function V2UsersList() {
const { projectRef } = useV2Params()
const [search, setSearch] = useState('')
+ const [filterUserType, setFilterUserType] = useState<'all' | UserTypeFilter>('all')
+ const [selectedProviders, setSelectedProviders] = useState([])
const [sort, setSort] = useState(null)
+ const {
+ authenticationShowSortByEmail: showSortByEmail,
+ authenticationShowSortByPhone: showSortByPhone,
+ } = useIsFeatureEnabled([
+ 'authentication:show_sort_by_email',
+ 'authentication:show_sort_by_phone',
+ ])
+
+ const sortColumn =
+ (sort?.columnId as 'created_at' | 'email' | 'id' | 'phone' | 'last_sign_in_at') ?? 'created_at'
+ const sortOrder = sort?.direction ?? 'asc'
+ const sortByValue = `${sortColumn}:${sortOrder}`
+
const [page, setPage] = useState(1)
const { data: project, isLoading: isProjectLoading } = useProjectDetailQuery(
@@ -95,7 +114,16 @@ export function V2UsersList() {
const shouldFetch = Boolean(projectRef) && isValidConnString(project?.connectionString)
- const { data: countData } = useUsersCountQuery({ projectRef }, { enabled: shouldFetch })
+ const { data: countData } = useUsersCountQuery(
+ {
+ projectRef,
+ connectionString: project?.connectionString,
+ keywords: search || undefined,
+ filter: filterUserType === 'all' ? undefined : filterUserType,
+ providers: selectedProviders,
+ },
+ { enabled: shouldFetch }
+ )
const total = countData?.count ?? 0
const {
@@ -110,10 +138,10 @@ export function V2UsersList() {
projectRef,
connectionString: project?.connectionString,
keywords: search || undefined,
- sort:
- (sort?.columnId as 'created_at' | 'email' | 'id' | 'phone' | 'last_sign_in_at') ??
- 'created_at',
- order: sort?.direction ?? 'asc',
+ filter: filterUserType === 'all' ? undefined : filterUserType,
+ providers: selectedProviders,
+ sort: sortColumn,
+ order: sortOrder,
},
{ enabled: shouldFetch }
)
@@ -133,16 +161,38 @@ export function V2UsersList() {
}
const handleSortChange = (s: SortState | null) => {
- setSort(s)
+ setSort(s ?? { columnId: 'created_at', direction: 'asc' })
setPage(1)
}
- const handleFilterChange = (state: Record) => {
+ const handleSortByValue = (value: string) => {
+ const [columnId, direction] = value.split(':') as [
+ SortState['columnId'],
+ SortState['direction'],
+ ]
+ setSort({ columnId, direction })
+ setPage(1)
+ }
+
+ const handleFilterChange = (state: FilterState) => {
const newSearch = (state['search'] as string) ?? ''
+ const newUserType = (state['userType'] as 'all' | UserTypeFilter) ?? 'all'
+ const newProviders = Array.isArray(state['providers'])
+ ? (state['providers'] as string[])
+ : ([] as string[])
+
if (newSearch !== search) {
setSearch(newSearch)
setPage(1)
}
+ if (newUserType !== filterUserType) {
+ setFilterUserType(newUserType)
+ setPage(1)
+ }
+ if (newProviders.join(',') !== selectedProviders.join(',')) {
+ setSelectedProviders(newProviders)
+ setPage(1)
+ }
}
return (
@@ -154,6 +204,18 @@ export function V2UsersList() {
error={isError ? (error as Error) : null}
sort={sort}
onSortChange={handleSortChange}
+ renderSortControl={() => (
+
+ )}
filters={[
{
id: 'search',
@@ -161,9 +223,30 @@ export function V2UsersList() {
type: 'search',
placeholder: 'Search by email or phone…',
},
+ {
+ id: 'userType',
+ label: 'User type',
+ type: 'select',
+ options: [
+ { value: 'all', label: 'All users' },
+ { value: 'verified', label: 'Verified users' },
+ { value: 'unverified', label: 'Unverified users' },
+ { value: 'anonymous', label: 'Anonymous users' },
+ ],
+ },
+ {
+ id: 'providers',
+ label: 'Provider',
+ type: 'multi-select',
+ options: PROVIDER_FILTER_OPTIONS.map((provider) => ({
+ value: provider.value,
+ label: provider.name,
+ })),
+ },
]}
- filterState={{ search }}
+ filterState={{ search, userType: filterUserType, providers: selectedProviders }}
onFilterChange={handleFilterChange}
+ toolbarRight={}
pagination={{
page,
pageSize: PAGE_SIZE,
diff --git a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
index dc2d55a6d0f..aaa02f88543 100644
--- a/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
+++ b/apps/studio/pages/project/[ref]/functions/[functionSlug]/index.tsx
@@ -1,432 +1,10 @@
-import { PermissionAction } from '@supabase/shared-types/out/constants'
-import { IS_PLATFORM, useParams } from 'common'
-import { EdgeFunctionRecentInvocations } from 'components/interfaces/Functions/EdgeFunctionRecentInvocations'
-import ReportWidget from 'components/interfaces/Reports/ReportWidget'
+import { EdgeFunctionOverviewPageContent } from 'components/interfaces/Functions/EdgeFunctionDetails/EdgeFunctionOverviewPageContent'
import DefaultLayout from 'components/layouts/DefaultLayout'
import EdgeFunctionDetailsLayout from 'components/layouts/EdgeFunctionsLayout/EdgeFunctionDetailsLayout'
-import AreaChart from 'components/ui/Charts/AreaChart'
-import StackedBarChart from 'components/ui/Charts/StackedBarChart'
-import NoPermission from 'components/ui/NoPermission'
-import {
- FunctionsCombinedStatsVariables,
- useFunctionsCombinedStatsQuery,
-} from 'data/analytics/functions-combined-stats-query'
-import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
-import dayjs, { Dayjs } from 'dayjs'
-import { useFillTimeseriesSorted } from 'hooks/analytics/useFillTimeseriesSorted'
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
-import maxBy from 'lodash/maxBy'
-import meanBy from 'lodash/meanBy'
-import sumBy from 'lodash/sumBy'
-import { useRouter } from 'next/router'
-import { useMemo, useState } from 'react'
-import type { ChartIntervals, NextPageWithLayout } from 'types'
-import {
- Alert_Shadcn_,
- AlertDescription_Shadcn_,
- AlertTitle_Shadcn_,
- Button,
- WarningIcon,
-} from 'ui'
-import { PageContainer } from 'ui-patterns/PageContainer'
-import { PageSection, PageSectionContent } from 'ui-patterns/PageSection'
-
-const CHART_INTERVALS: ChartIntervals[] = [
- {
- key: '15min',
- label: '15 min',
- startValue: 15,
- startUnit: 'minute',
- format: 'MMM D, h:mm:ssa',
- },
- {
- key: '1hr',
- label: '1 hour',
- startValue: 1,
- startUnit: 'hour',
- format: 'MMM D, h:mma',
- },
- {
- key: '3hr',
- label: '3 hours',
- startValue: 3,
- startUnit: 'hour',
- format: 'MMM D, h:mma',
- },
- {
- key: '1day',
- label: '1 day',
- startValue: 1,
- startUnit: 'hour',
- format: 'MMM D, h:mma',
- },
-]
+import type { NextPageWithLayout } from 'types'
const PageLayout: NextPageWithLayout = () => {
- const router = useRouter()
- const { ref: projectRef, functionSlug } = useParams()
-
- const [interval, setInterval] = useState('15min')
- const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
- const { data: selectedFunction } = useEdgeFunctionQuery({
- projectRef,
- slug: functionSlug,
- })
- const id = selectedFunction?.id
- const combinedStatsResults = useFunctionsCombinedStatsQuery({
- projectRef,
- functionId: id,
- interval: selectedInterval.key as FunctionsCombinedStatsVariables['interval'],
- })
-
- const combinedStatsData = useMemo(() => {
- const result = combinedStatsResults.data?.result as
- | Record[]
- | undefined
- return result || []
- }, [combinedStatsResults.data])
-
- const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => {
- const start = dayjs()
- .subtract(selectedInterval.startValue, selectedInterval.startUnit as dayjs.ManipulateType)
- .startOf(selectedInterval.startUnit as dayjs.ManipulateType)
-
- const end = dayjs().startOf(selectedInterval.startUnit as dayjs.ManipulateType)
- return [start, end]
- }, [selectedInterval])
-
- const {
- data: combinedStatsChartData,
- error: combinedStatsError,
- isError: isErrorCombinedStats,
- } = useFillTimeseriesSorted({
- data: combinedStatsData,
- timestampKey: 'timestamp',
- valueKey: [
- 'requests_count',
- 'log_count',
- 'log_info_count',
- 'log_warn_count',
- 'log_error_count',
- 'success_count',
- 'redirect_count',
- 'client_err_count',
- 'server_err_count',
- 'avg_cpu_time_used',
- 'avg_memory_used',
- 'avg_execution_time',
- 'max_execution_time',
- 'avg_heap_memory_used',
- 'avg_external_memory_used',
- 'max_cpu_time_used',
- ],
- defaultValue: 0,
- startDate: startDate.toISOString(),
- endDate: endDate.toISOString(),
- })
-
- const { isLoading: permissionsLoading, can: canReadFunction } = useAsyncCheckPermissions(
- PermissionAction.FUNCTIONS_READ,
- functionSlug as string
- )
- if (!canReadFunction && !permissionsLoading) {
- return
- }
-
- return (
-
-
-
- {IS_PLATFORM && id && (
-
-
-
- )}
-
-
- {CHART_INTERVALS.map((item, i) => {
- const classes = []
-
- if (i === 0) {
- classes.push('rounded-tr-none rounded-br-none')
- } else if (i === CHART_INTERVALS.length - 1) {
- classes.push('rounded-tl-none rounded-bl-none')
- } else {
- classes.push('rounded-none')
- }
-
- return (
-
- )
- })}
-
-
-
- Statistics for past {selectedInterval.label}
-
-
-
-
-
{
- return isErrorCombinedStats ? (
-
-
- Failed to reterieve execution time
-
- {combinedStatsError?.message ?? 'Unknown error'}
-
-
- ) : (
-
- )
- }}
- />
- {
- if (isErrorCombinedStats) {
- return (
-
-
- Failed to reterieve invocations
-
- {combinedStatsError?.message ?? 'Unknown error'}
-
-
- )
- } else {
- const requestData = props.data
- .map((d: any) => [
- {
- status: '2xx',
- count: d.success_count,
- timestamp: d.timestamp,
- },
- {
- status: '3xx',
- count: d.redirect_count,
- timestamp: d.timestamp,
- },
- {
- status: '4xx',
- count: d.client_err_count,
- timestamp: d.timestamp,
- },
- {
- status: '5xx',
- count: d.server_err_count,
- timestamp: d.timestamp,
- },
- ])
- .flat()
-
- const logsData = props.data
- .map((d: any) => [
- {
- status: 'error',
- count: d.log_error_count,
- timestamp: d.timestamp,
- },
- {
- status: 'info',
- count: d.log_info_count,
- timestamp: d.timestamp,
- },
- {
- status: 'warn',
- count: d.log_warn_count,
- timestamp: d.timestamp,
- },
- ])
- .flat()
-
- return (
-
- {
- router.push(
- `/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}`
- )
- }}
- />
- {
- router.push(
- `/project/${projectRef}/functions/${functionSlug}/logs?its=${startDate.toISOString()}`
- )
- }}
- />
-
- )
- }
- }}
- />
- {
- return isErrorCombinedStats ? (
-
-
- Failed to retrieve CPU time
-
- {combinedStatsError?.message ?? 'Unknown error'}
-
-
- ) : (
-
- )
- }}
- />
- {
- if (isErrorCombinedStats) {
- return (
-
-
- Failed to retrieve memory usage
-
- {combinedStatsError?.message ?? 'Unknown error'}
-
-
- )
- }
-
- const memoryData = props.data
- .map((d: any) => [
- {
- type: 'heap',
- count: d.avg_heap_memory_used,
- timestamp: d.timestamp,
- },
- {
- type: 'external',
- count: d.avg_external_memory_used,
- timestamp: d.timestamp,
- },
- ])
- .flat()
-
- return (
-
- )
- }}
- />
-
-
-
-
-
- )
+ return
}
PageLayout.getLayout = (page) => (