Table consistency (#37924)

* update project tables

* org team table

* Minor fixes

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Saxon FletcherandJoshen Lim authored and GitHub committed 2025-08-15 13:16:55 +10:00
1 parent ca4b3bc624
commit f9a6af7a68
22 files changed
+1157 -995

No files matched your search

@@ -1,7 +1,6 @@
import { Edit, MoreVertical, Search, Trash } from 'lucide-react'
import { useState } from 'react'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { DocsButton } from 'components/ui/DocsButton'
import SchemaSelector from 'components/ui/SchemaSelector'
@@ -15,11 +14,18 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
Card,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
Input,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import CreateEnumeratedTypeSidePanel from './CreateEnumeratedTypeSidePanel'
@@ -94,75 +100,79 @@ const EnumeratedTypes = () => {
)}
{isSuccess && (
<Table
head={[
<Table.th key="schema">Schema</Table.th>,
<Table.th key="name">Name</Table.th>,
<Table.th key="values">Values</Table.th>,
<Table.th key="actions" />,
]}
body={
<>
{filteredEnumeratedTypes.length === 0 && search.length === 0 && (
<Table.tr>
<Table.td colSpan={4}>
<p className="text-sm text-foreground">No enumerated types created yet</p>
<p className="text-sm text-foreground-light">
There are no enumerated types found in the schema "{selectedSchema}"
</p>
</Table.td>
</Table.tr>
)}
{filteredEnumeratedTypes.length === 0 && search.length > 0 && (
<Table.tr>
<Table.td colSpan={4}>
<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>
</Table.td>
</Table.tr>
)}
{filteredEnumeratedTypes.length > 0 &&
filteredEnumeratedTypes.map((type) => (
<Table.tr key={type.id}>
<Table.td className="w-20">
<p className="w-20 truncate">{type.schema}</p>
</Table.td>
<Table.td>{type.name}</Table.td>
<Table.td>{type.enums.join(', ')}</Table.td>
<Table.td>
{!isSchemaLocked && (
<div className="flex justify-end items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedTypeToEdit(type)}
>
<Edit size={14} />
<p>Update type</p>
</DropdownMenuItem>
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedTypeToDelete(type)}
>
<Trash size={14} />
<p>Delete type</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</Table.td>
</Table.tr>
))}
</>
}
/>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="schema">Schema</TableHead>
<TableHead key="name">Name</TableHead>
<TableHead key="values">Values</TableHead>
<TableHead key="actions" />
</TableRow>
</TableHeader>
<TableBody>
<>
{filteredEnumeratedTypes.length === 0 && search.length === 0 && (
<TableRow>
<TableCell colSpan={4}>
<p className="text-sm text-foreground">No enumerated types created yet</p>
<p className="text-sm text-foreground-light">
There are no enumerated types found in the schema "{selectedSchema}"
</p>
</TableCell>
</TableRow>
)}
{filteredEnumeratedTypes.length === 0 && search.length > 0 && (
<TableRow>
<TableCell colSpan={4}>
<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>
)}
{filteredEnumeratedTypes.length > 0 &&
filteredEnumeratedTypes.map((type) => (
<TableRow key={type.id}>
<TableCell className="w-20">
<p className="w-20 truncate">{type.schema}</p>
</TableCell>
<TableCell>{type.name}</TableCell>
<TableCell>{type.enums.join(', ')}</TableCell>
<TableCell>
{!isSchemaLocked && (
<div className="flex justify-end items-center space-x-2">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedTypeToEdit(type)}
>
<Edit size={14} />
<p>Update type</p>
</DropdownMenuItem>
<DropdownMenuItem
className="space-x-2"
onClick={() => setSelectedTypeToDelete(type)}
>
<Trash size={14} />
<p>Delete type</p>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)}
</TableCell>
</TableRow>
))}
</>
</TableBody>
</Table>
</Card>
)}
<CreateEnumeratedTypeSidePanel
@@ -1,5 +1,5 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AlertTriangle, Book, Github, Loader2, Settings } from 'lucide-react'
import { AlertTriangle, Book, Github, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'
@@ -10,17 +10,17 @@ import { DatabaseExtension } from 'data/database-extensions/database-extensions-
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsOrioleDb, useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { extensions } from 'shared-data'
import { Button, cn, Switch, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { Button, Switch, Tooltip, TooltipContent, TooltipTrigger, TableRow, TableCell } from 'ui'
import { Admonition } from 'ui-patterns'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import EnableExtensionModal from './EnableExtensionModal'
import { EXTENSION_DISABLE_WARNINGS } from './Extensions.constants'
interface ExtensionCardProps {
interface ExtensionRowProps {
extension: DatabaseExtension
}
const ExtensionCard = ({ extension }: ExtensionCardProps) => {
const ExtensionRow = ({ extension }: ExtensionRowProps) => {
const { data: project } = useSelectedProjectQuery()
const isOn = extension.installed_version !== null
const isOrioleDb = useIsOrioleDb()
@@ -35,7 +35,6 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
const orioleDbCheck = isOrioleDb && extension.name === 'orioledb'
const disabled = !canUpdateExtensions || orioleDbCheck
const X_PADDING = 'px-5'
const extensionMeta = extensions.find((item) => item.name === extension.name)
const docsUrl = extensionMeta?.link.startsWith('/guides')
? `https://supabase.com/docs${extensionMeta?.link}`
@@ -60,20 +59,93 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
return (
<>
<div className="bg-surface-100 border border-overlay flex flex-col overflow-hidden rounded shadow-sm">
<div className={cn('border-b border-overlay flex justify-between w-full py-3', X_PADDING)}>
<div className="max-w-[85%] flex items-center space-x-3 truncate">
<h3
title={extension.name}
className="h-5 m-0 text-sm truncate cursor-pointer text-foreground"
>
<TableRow>
<TableCell>
<div className="flex items-center gap-x-2">
<span title={extension.name} className="truncate inline-block max-w-48">
{extension.name}
</h3>
<p className="text-sm text-foreground-light font-mono tracking-tighter">
{extension?.installed_version ?? extension.default_version}
</p>
</span>
{extensionMeta?.deprecated && extensionMeta?.deprecated.length > 0 && (
<ButtonTooltip
type="warning"
icon={<AlertTriangle />}
className="rounded-full"
tooltip={{
content: {
text: `The extension is deprecated and will be removed in ${extensionMeta.deprecated.join(', ')}.`,
},
}}
>
Deprecated
</ButtonTooltip>
)}
</div>
</TableCell>
<TableCell className="font-mono tracking-tighter">
{extension?.installed_version ?? extension.default_version}
</TableCell>
<TableCell className="truncate">{isOn ? extension.schema : '-'}</TableCell>
<TableCell className="text-foreground-light">
<p className="block max-w-96" title={extension.comment ?? undefined}>
{extension.comment}
</p>
</TableCell>
<TableCell>
{extensionMeta?.product ? (
<div className="flex flex-col gap-1">
{extensionMeta.product_url ? (
<Link
href={extensionMeta.product_url.replace('{ref}', project?.ref ?? '')}
className="transition hover:text-foreground"
>
{extensionMeta.product}
</Link>
) : (
<span>{extensionMeta.product}</span>
)}
{!isOn && (
<span className="text-foreground-lighter text-xs">
Install extension to use {extensionMeta.product}
</span>
)}
</div>
) : (
<span className="text-foreground-lighter">-</span>
)}
</TableCell>
<TableCell className="flex gap-2 items-center">
{extensionMeta?.github_url && (
<Button asChild type="default" icon={<Github />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
href={extensionMeta.github_url}
className="font-mono tracking-tighter"
>
{extensionMeta.github_url.split('/').slice(-2).join('/')}
</a>
</Button>
)}
{docsUrl !== undefined && (
<Button asChild type="default" icon={<Book />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
className="font-mono tracking-tighter"
href={docsUrl}
>
Docs
</a>
</Button>
)}
</TableCell>
<TableCell className="w-20 sticky bg-surface-100 border-l right-0">
{isDisabling ? (
<Loader2 className="animate-spin" size={16} />
) : (
@@ -98,88 +170,8 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
)}
</Tooltip>
)}
</div>
{isOn && (
<div className={cn('border-b border-overlay py-2', X_PADDING)}>
<p className="text-foreground-light text-sm">
Installed in <span className="text-foreground">{extension.schema}</span> schema
</p>
</div>
)}
<div className={cn('flex h-full flex-col gap-y-3 py-3', X_PADDING)}>
<p className="text-sm text-foreground-light capitalize-sentence">{extension.comment}</p>
<div className="flex items-center gap-2 flex-wrap">
{extensionMeta?.github_url && (
<Button asChild type="default" icon={<Github />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
href={extensionMeta.github_url}
className="font-mono tracking-tighter"
>
{extensionMeta.github_url.split('/').slice(-2).join('/')}
</a>
</Button>
)}
{docsUrl !== undefined && (
<Button asChild type="default" icon={<Book />} className="rounded-full">
<a
target="_blank"
rel="noreferrer"
className="font-mono tracking-tighter"
href={docsUrl}
>
Docs
</a>
</Button>
)}
{extensionMeta?.deprecated && extensionMeta?.deprecated.length > 0 && (
<ButtonTooltip
type="warning"
icon={<AlertTriangle />}
className="rounded-full"
tooltip={{
content: {
text: `The extension is deprecated and will be removed in ${extensionMeta.deprecated.join(', ')}.`,
},
}}
>
Deprecated
</ButtonTooltip>
)}
</div>
</div>
{extensionMeta?.product && (
<div className={cn('border-t border-overlay py-3 flex items-center gap-x-3', X_PADDING)}>
<div className="min-w-5 w-5 h-5 border border-brand/50 rounded flex items-center justify-center">
<Settings className="text-brand" size={12} />
</div>
<div>
<p className="text-foreground-light text-xs">
<span className="text-foreground">{extension.name}</span> is used by{' '}
{extensionMeta.product_url ? (
<Link
href={extensionMeta.product_url.replace('{ref}', project?.ref ?? '')}
className="transition hover:text-foreground"
>
{extensionMeta.product}
</Link>
) : (
extensionMeta.product
)}
</p>
{!isOn && (
<p className="text-foreground-lighter text-xs">
Install extension to use {extensionMeta.product}
</p>
)}
</div>
</div>
)}
</div>
</TableCell>
</TableRow>
<EnableExtensionModal
visible={showConfirmEnableModal}
@@ -211,4 +203,4 @@ const ExtensionCard = ({ extension }: ExtensionCardProps) => {
)
}
export default ExtensionCard
export default ExtensionRow
@@ -7,13 +7,12 @@ import { useParams } from 'common'
import { DocsButton } from 'components/ui/DocsButton'
import InformationBox from 'components/ui/InformationBox'
import NoSearchResults from 'components/ui/NoSearchResults'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Input } from 'ui'
import ExtensionCard from './ExtensionCard'
import ExtensionCardSkeleton from './ExtensionCardSkeleton'
import { Card, Input, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import ExtensionRow from './ExtensionRow'
import { HIDDEN_EXTENSIONS, SEARCH_TERMS } from './Extensions.constants'
const Extensions = () => {
@@ -73,50 +72,46 @@ const Extensions = () => {
)}
{isLoading ? (
<div className="my-8 w-full space-y-12">
<div className="space-y-4">
<ShimmeringLoader className="h-[28px] w-40" />
<div className="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
{Array.from({ length: 6 }).map((_, index) => (
<ExtensionCardSkeleton key={index} index={index} />
))}
</div>
</div>
</div>
<GenericSkeletonLoader />
) : (
<>
{extensions.length === 0 && (
<NoSearchResults
searchString={filterString}
onResetFilter={() => setFilterString('')}
/>
)}
<div className="my-8 w-full space-y-12">
{enabledExtensions.length > 0 && (
<div className="space-y-4">
<h4 className="text-lg">Enabled extensions</h4>
<div className="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
{enabledExtensions.map((extension) => (
<ExtensionCard key={extension.name} extension={extension} />
))}
</div>
</div>
)}
{disabledExtensions.length > 0 && (
<div className="space-y-4">
<h4 className="text-lg">Available extensions</h4>
<div className="mb-4 grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
{disabledExtensions.map((extension) => (
<ExtensionCard key={extension.name} extension={extension} />
))}
</div>
</div>
)}
</div>
</>
<div className="w-full overflow-hidden overflow-x-auto">
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="name">Name</TableHead>
<TableHead key="version">Version</TableHead>
<TableHead key="schema">Schema</TableHead>
<TableHead key="description">Description</TableHead>
<TableHead key="used-by">Used by</TableHead>
<TableHead key="links">Links</TableHead>
<TableHead
key="enabled"
className="w-20 bg-background-200 border-l sticky right-0"
>
Enabled
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{[...enabledExtensions, ...disabledExtensions].map((extension) => (
<ExtensionRow key={extension.name} extension={extension} />
))}
{extensions.length === 0 && (
<TableRow>
<TableCell colSpan={7}>
<NoSearchResults
className="border-none !p-0 bg-transparent"
searchString={filterString}
onResetFilter={() => setFilterString('')}
/>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</div>
)}
</>
)
@@ -3,7 +3,6 @@ import { includes, noop, sortBy } from 'lodash'
import { Edit, Edit2, FileText, MoreVertical, Trash } from 'lucide-react'
import { useRouter } from 'next/router'
import Table from 'components/to-be-cleaned/Table'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useDatabaseFunctionsQuery } from 'data/database-functions/database-functions-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
@@ -16,6 +15,8 @@ import {
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
TableRow,
TableCell,
} from 'ui'
interface FunctionListProps {
@@ -57,27 +58,27 @@ const FunctionList = ({
if (_functions.length === 0 && filterString.length === 0) {
return (
<Table.tr key={schema}>
<Table.td colSpan={5}>
<TableRow key={schema}>
<TableCell colSpan={5}>
<p className="text-sm text-foreground">No functions created yet</p>
<p className="text-sm text-foreground-light">
There are no functions found in the schema "{schema}"
</p>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)
}
if (_functions.length === 0 && filterString.length > 0) {
return (
<Table.tr key={schema}>
<Table.td colSpan={5}>
<TableRow key={schema}>
<TableCell colSpan={5}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{filterString}" did not return any results
</p>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)
}
@@ -87,8 +88,8 @@ const FunctionList = ({
const isApiDocumentAvailable = schema == 'public' && x.return_type !== 'trigger'
return (
<Table.tr key={x.id}>
<Table.td className="truncate">
<TableRow key={x.id}>
<TableCell className="truncate">
<Button
type="text"
className="text-foreground text-sm p-0 hover:bg-transparent"
@@ -96,17 +97,19 @@ const FunctionList = ({
>
{x.name}
</Button>
</Table.td>
<Table.td className="table-cell overflow-auto">
</TableCell>
<TableCell className="table-cell overflow-auto">
<p title={x.argument_types} className="truncate">
{x.argument_types || '-'}
</p>
</Table.td>
<Table.td className="table-cell">
</TableCell>
<TableCell className="table-cell">
<p title={x.return_type}>{x.return_type}</p>
</Table.td>
<Table.td className="table-cell">{x.security_definer ? 'Definer' : 'Invoker'}</Table.td>
<Table.td className="text-right">
</TableCell>
<TableCell className="table-cell">
{x.security_definer ? 'Definer' : 'Invoker'}
</TableCell>
<TableCell className="text-right">
{!isLocked && (
<div className="flex items-center justify-end">
{canUpdateFunctions ? (
@@ -189,8 +192,8 @@ const FunctionList = ({
)}
</div>
)}
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)
})}
</>
@@ -6,7 +6,6 @@ import { useRouter } from 'next/router'
import { useParams } from 'common'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import SchemaSelector from 'components/ui/SchemaSelector'
@@ -18,7 +17,17 @@ import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Input } from 'ui'
import {
AiIconAnimation,
Input,
Table,
TableHeader,
TableHead,
TableBody,
TableRow,
TableCell,
Card,
} from 'ui'
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import FunctionList from './FunctionList'
@@ -168,34 +177,34 @@ const FunctionsList = ({
</div>
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="functions" />}
<Table
className="table-fixed overflow-x-auto"
head={
<>
<Table.th key="name">Name</Table.th>
<Table.th key="arguments" className="table-cell">
Arguments
</Table.th>
<Table.th key="return_type" className="table-cell">
Return type
</Table.th>
<Table.th key="security" className="table-cell w-[100px]">
Security
</Table.th>
<Table.th key="buttons" className="w-1/6"></Table.th>
</>
}
body={
<FunctionList
schema={selectedSchema}
filterString={filterString}
isLocked={isSchemaLocked}
editFunction={editFunction}
deleteFunction={deleteFunction}
/>
}
/>
<Card>
<Table className="table-fixed overflow-x-auto">
<TableHeader>
<TableRow>
<TableHead key="name">Name</TableHead>
<TableHead key="arguments" className="table-cell">
Arguments
</TableHead>
<TableHead key="return_type" className="table-cell">
Return type
</TableHead>
<TableHead key="security" className="table-cell w-[100px]">
Security
</TableHead>
<TableHead key="buttons" className="w-1/6"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<FunctionList
schema={selectedSchema}
filterString={filterString}
isLocked={isSchemaLocked}
editFunction={editFunction}
deleteFunction={deleteFunction}
/>
</TableBody>
</Table>
</Card>
</div>
)}
</>
@@ -4,7 +4,6 @@ import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
import SchemaSelector from 'components/ui/SchemaSelector'
@@ -15,7 +14,18 @@ import { useSchemasQuery } from 'data/database/schemas-query'
import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import { Button, Input, SidePanel } from 'ui'
import {
Button,
Input,
SidePanel,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
TableHeader,
Card,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
import CreateIndexSidePanel from './CreateIndexSidePanel'
@@ -139,49 +149,51 @@ const Indexes = () => {
)}
{isSuccessIndexes && (
<div className="w-full overflow-hidden overflow-x-auto">
<Table
head={[
<Table.th key="schema">Schema</Table.th>,
<Table.th key="table">Table</Table.th>,
<Table.th key="name">Name</Table.th>,
<Table.th key="buttons"></Table.th>,
]}
body={
<>
<div className="w-full overflow-hidden">
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="schema">Schema</TableHead>
<TableHead key="table">Table</TableHead>
<TableHead key="name">Name</TableHead>
<TableHead key="buttons"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedIndexes.length === 0 && search.length === 0 && (
<Table.tr>
<Table.td colSpan={4}>
<TableRow>
<TableCell colSpan={4}>
<p className="text-sm text-foreground">No indexes created yet</p>
<p className="text-sm text-foreground-light">
There are no indexes found in the schema "{selectedSchema}"
</p>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)}
{sortedIndexes.length === 0 && search.length > 0 && (
<Table.tr>
<Table.td colSpan={4}>
<TableRow>
<TableCell colSpan={4}>
<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>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)}
{indexes.length > 0 &&
indexes.map((index) => (
<Table.tr key={index.name}>
<Table.td>
<TableRow key={index.name}>
<TableCell>
<p title={index.schema}>{index.schema}</p>
</Table.td>
<Table.td>
</TableCell>
<TableCell>
<p title={index.table}>{index.table}</p>
</Table.td>
<Table.td>
</TableCell>
<TableCell>
<p title={index.name}>{index.name}</p>
</Table.td>
<Table.td>
</TableCell>
<TableCell>
<div className="flex justify-end items-center space-x-2">
<Button type="default" onClick={() => setSelectedIndex(index)}>
View definition
@@ -196,12 +208,12 @@ const Indexes = () => {
/>
)}
</div>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
))}
</>
}
/>
</TableBody>
</Table>
</Card>
</div>
)}
</div>
@@ -2,13 +2,23 @@ import dayjs from 'dayjs'
import Link from 'next/link'
import { useState } from 'react'
import Table from 'components/to-be-cleaned/Table'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import { DatabaseMigration, useMigrationsQuery } from 'data/database/migrations-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Search } from 'lucide-react'
import { Button, Input, SidePanel } from 'ui'
import {
Button,
Card,
Input,
SidePanel,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { Admonition } from 'ui-patterns'
import MigrationsEmptyState from './MigrationsEmptyState'
@@ -77,59 +87,63 @@ const Migrations = () => {
icon={<Search size="14" />}
/>
</div>
<Table
head={[
<Table.th key="version" style={{ width: '180px' }}>
Version
</Table.th>,
<Table.th key="version">Name</Table.th>,
<Table.th key="version">Inserted at (UTC)</Table.th>,
<Table.th key="buttons"></Table.th>,
]}
body={
migrations.length > 0 ? (
migrations.map((migration) => {
// [Joshen] LEFT OFF HERE
const insertedAt = dayjs(migration.version, 'YYYYMMDDHHmmss').format(
'DD MMM YYYY, HH:mm:ss'
)
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="version" style={{ width: '180px' }}>
Version
</TableHead>
<TableHead key="version">Name</TableHead>
<TableHead key="version">Inserted at (UTC)</TableHead>
<TableHead key="buttons"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{migrations.length > 0 ? (
migrations.map((migration) => {
// [Joshen] LEFT OFF HERE
const insertedAt = dayjs(migration.version, 'YYYYMMDDHHmmss').format(
'DD MMM YYYY, HH:mm:ss'
)
return (
<Table.tr key={migration.version}>
<Table.td>{migration.version}</Table.td>
<Table.td
className={
(migration?.name ?? '').length === 0
? '!text-foreground-lighter'
: ''
}
>
{migration?.name ?? 'Name not available'}
</Table.td>
<Table.td>{insertedAt}</Table.td>
<Table.td align="right">
<Button
type="default"
onClick={() => setSelectedMigration(migration)}
return (
<TableRow key={migration.version}>
<TableCell>{migration.version}</TableCell>
<TableCell
className={
(migration?.name ?? '').length === 0
? '!text-foreground-lighter'
: ''
}
>
View migration SQL
</Button>
</Table.td>
</Table.tr>
)
})
) : (
<Table.tr>
<Table.td colSpan={3}>
<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>
</Table.td>
</Table.tr>
)
}
/>
{migration?.name ?? 'Name not available'}
</TableCell>
<TableCell>{insertedAt}</TableCell>
<TableCell align="right">
<Button
type="default"
onClick={() => setSelectedMigration(migration)}
>
View migration SQL
</Button>
</TableCell>
</TableRow>
)
})
) : (
<TableRow>
<TableCell colSpan={3}>
<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>
@@ -14,7 +14,10 @@ interface PublicationsTableItemProps {
selectedPublication: PostgresPublication
}
const PublicationsTableItem = ({ table, selectedPublication }: PublicationsTableItemProps) => {
export const PublicationsTableItem = ({
table,
selectedPublication,
}: PublicationsTableItemProps) => {
const { data: project } = useSelectedProjectQuery()
const { data: protectedSchemas } = useProtectedSchemas()
const enabledForAllTables = selectedPublication.tables == null
@@ -108,5 +111,3 @@ const PublicationsTableItem = ({ table, selectedPublication }: PublicationsTable
</TableRow>
)
}
export default PublicationsTableItem
@@ -15,7 +15,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Card, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
import { Admonition } from 'ui-patterns'
import { Input } from 'ui-patterns/DataInputs/Input'
import PublicationsTableItem from './PublicationsTableItem'
import { PublicationsTableItem } from './PublicationsTableItem'
export const PublicationsTables = () => {
const { ref, id } = useParams()
@@ -95,44 +95,42 @@ export const PublicationsTables = () => {
(tables.length === 0 ? (
<NoSearchResults />
) : (
<div>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Schema</TableHead>
<TableHead>Description</TableHead>
{/*
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Schema</TableHead>
<TableHead>Description</TableHead>
{/*
We've disabled All tables toggle for publications.
See https://github.com/supabase/supabase/pull/7233.
*/}
<TableHead />
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{!!selectedPublication ? (
tables.map((table) => (
<PublicationsTableItem
key={table.id}
table={table}
selectedPublication={selectedPublication}
/>
))
) : (
<TableRow>
<TableCell colSpan={4}>
<p>The selected publication with ID {id} cannot be found</p>
<p className="text-foreground-light">
Head back to the list of publications to select one from there
</p>
</TableCell>
</TableRow>
</TableHeader>
<TableBody>
{!!selectedPublication ? (
tables.map((table) => (
<PublicationsTableItem
key={table.id}
table={table}
selectedPublication={selectedPublication}
/>
))
) : (
<TableRow>
<TableCell colSpan={4}>
<p>The selected publication with ID {id} cannot be found</p>
<p className="text-foreground-light">
Head back to the list of publications to select one from there
</p>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</Card>
</div>
)}
</TableBody>
</Table>
</Card>
))}
</>
)
@@ -20,7 +20,6 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
@@ -39,6 +38,7 @@ import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema } from 'hooks/useProtectedSchemas'
import {
Button,
Card,
Checkbox_Shadcn_,
DropdownMenu,
DropdownMenuContent,
@@ -53,6 +53,12 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
Table,
TableHeader,
TableHead,
TableBody,
TableRow,
TableCell,
cn,
} from 'ui'
import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning'
@@ -303,256 +309,265 @@ export const TableList = ({
{isSuccess && (
<div className="w-full">
<Table
head={[
<Table.th key="icon" className="!px-0" />,
<Table.th key="name">Name</Table.th>,
<Table.th key="description" className="hidden lg:table-cell">
Description
</Table.th>,
<Table.th key="rows" className="hidden text-right xl:table-cell">
Rows (Estimated)
</Table.th>,
<Table.th key="size" className="hidden text-right xl:table-cell">
Size (Estimated)
</Table.th>,
<Table.th key="realtime" className="hidden xl:table-cell text-center">
Realtime Enabled
</Table.th>,
<Table.th key="buttons"></Table.th>,
]}
body={
<>
{entities.length === 0 && filterString.length === 0 && (
<Table.tr key={selectedSchema}>
<Table.td colSpan={7}>
{visibleTypes.length === 0 ? (
<>
<p className="text-sm text-foreground">
Please select at least one entity type to filter with
</p>
<p className="text-sm text-foreground-light">
There are currently no results based on the filter that you have applied
</p>
</>
) : (
<>
<p className="text-sm text-foreground">No tables created yet</p>
<p className="text-sm text-foreground-light">
There are no{' '}
{visibleTypes.length === 5
? 'tables'
: visibleTypes.length === 1
? `${formatTooltipText(visibleTypes[0])}s`
: `${visibleTypes
.slice(0, -1)
.map((x) => `${formatTooltipText(x)}s`)
.join(
', '
)}, and ${formatTooltipText(visibleTypes[visibleTypes.length - 1])}s`}{' '}
found in the schema "{selectedSchema}"
</p>
</>
)}
</Table.td>
</Table.tr>
)}
{entities.length === 0 && filterString.length > 0 && (
<Table.tr key={selectedSchema}>
<Table.td colSpan={7}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{filterString}" did not return any results
</p>
</Table.td>
</Table.tr>
)}
{entities.length > 0 &&
entities.map((x) => (
<Table.tr key={x.id}>
<Table.td className="!pl-5 !pr-1">
<Tooltip>
<TooltipTrigger asChild>
{x.type === ENTITY_TYPE.TABLE ? (
<Table2
size={15}
strokeWidth={1.5}
className="text-foreground-lighter"
/>
) : x.type === ENTITY_TYPE.VIEW ? (
<Eye
size={15}
strokeWidth={1.5}
className="text-foreground-lighter"
/>
) : (
<div
className={cn(
'flex items-center justify-center text-xs h-4 w-4 rounded-[2px] font-bold',
x.type === ENTITY_TYPE.FOREIGN_TABLE &&
'text-yellow-900 bg-yellow-500',
x.type === ENTITY_TYPE.MATERIALIZED_VIEW &&
'text-purple-1000 bg-purple-500'
// [Alaister]: tables endpoint doesn't distinguish between tables and partitioned tables
// once we update the endpoint to include partitioned tables, we can uncomment this
// x.type === ENTITY_TYPE.PARTITIONED_TABLE &&
// 'text-foreground-light bg-border-stronger'
)}
>
{Object.entries(ENTITY_TYPE)
.find(([, value]) => value === x.type)?.[0]?.[0]
?.toUpperCase()}
</div>
)}
</TooltipTrigger>
<TooltipContent side="bottom" className="capitalize">
{formatTooltipText(x.type)}
</TooltipContent>
</Tooltip>
</Table.td>
<Table.td>
{/* only show tooltips if required, to reduce noise */}
{x.name.length > 20 ? (
<Tooltip disableHoverableContent={true}>
<TooltipTrigger
asChild
className="max-w-[95%] overflow-hidden text-ellipsis whitespace-nowrap"
>
<p>{x.name}</p>
</TooltipTrigger>
<TooltipContent side="bottom">{x.name}</TooltipContent>
</Tooltip>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="icon" className="!px-0" />
<TableHead key="name">Name</TableHead>
<TableHead key="description" className="hidden lg:table-cell">
Description
</TableHead>
<TableHead key="rows" className="hidden text-right xl:table-cell">
Rows (Estimated)
</TableHead>
<TableHead key="size" className="hidden text-right xl:table-cell">
Size (Estimated)
</TableHead>
<TableHead key="realtime" className="hidden xl:table-cell text-center">
Realtime Enabled
</TableHead>
<TableHead key="buttons"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<>
{entities.length === 0 && filterString.length === 0 && (
<TableRow key={selectedSchema}>
<TableCell colSpan={7}>
{visibleTypes.length === 0 ? (
<>
<p className="text-sm text-foreground">
Please select at least one entity type to filter with
</p>
<p className="text-sm text-foreground-light">
There are currently no results based on the filter that you have
applied
</p>
</>
) : (
<p>{x.name}</p>
<>
<p className="text-sm text-foreground">No tables created yet</p>
<p className="text-sm text-foreground-light">
There are no{' '}
{visibleTypes.length === 5
? 'tables'
: visibleTypes.length === 1
? `${formatTooltipText(visibleTypes[0])}s`
: `${visibleTypes
.slice(0, -1)
.map((x) => `${formatTooltipText(x)}s`)
.join(
', '
)}, and ${formatTooltipText(visibleTypes[visibleTypes.length - 1])}s`}{' '}
found in the schema "{selectedSchema}"
</p>
</>
)}
</Table.td>
<Table.td className="hidden lg:table-cell ">
{x.comment !== null ? (
<span className="lg:max-w-48 truncate inline-block" title={x.comment}>
{x.comment}
</span>
) : (
<p className="text-border-stronger">No description</p>
)}
</Table.td>
<Table.td className="hidden text-right xl:table-cell">
{x.rows !== undefined ? x.rows.toLocaleString() : '-'}
</Table.td>
<Table.td className="hidden text-right xl:table-cell">
{x.size !== undefined ? <code className="text-xs">{x.size}</code> : '-'}
</Table.td>
<Table.td className="hidden xl:table-cell text-center">
{(realtimePublication?.tables ?? []).find((table) => table.id === x.id) ? (
<div className="flex justify-center">
<Check size={18} strokeWidth={2} className="text-brand" />
</div>
) : (
<div className="flex justify-center">
<X size={18} strokeWidth={2} className="text-foreground-lighter" />
</div>
)}
</Table.td>
<Table.td>
<div className="flex justify-end gap-2">
<Button
asChild
type="default"
iconRight={<Columns size={14} className="text-foreground-light" />}
className="whitespace-nowrap hover:border-muted"
style={{ paddingTop: 3, paddingBottom: 3 }}
>
<Link href={`/project/${ref}/database/tables/${x.id}`}>
{x.columns.length} columns
</Link>
</Button>
{!isSchemaLocked && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem
className="flex items-center space-x-2"
onClick={() =>
router.push(`/project/${project?.ref}/editor/${x.id}`)
}
onMouseEnter={() =>
prefetchEditorTablePage({ id: x.id ? String(x.id) : undefined })
}
</TableCell>
</TableRow>
)}
{entities.length === 0 && filterString.length > 0 && (
<TableRow key={selectedSchema}>
<TableCell colSpan={7}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{filterString}" did not return any results
</p>
</TableCell>
</TableRow>
)}
{entities.length > 0 &&
entities.map((x) => (
<TableRow key={x.id}>
<TableCell className="!pl-5 !pr-1">
<Tooltip>
<TooltipTrigger asChild>
{x.type === ENTITY_TYPE.TABLE ? (
<Table2
size={15}
strokeWidth={1.5}
className="text-foreground-lighter"
/>
) : x.type === ENTITY_TYPE.VIEW ? (
<Eye
size={15}
strokeWidth={1.5}
className="text-foreground-lighter"
/>
) : (
<div
className={cn(
'flex items-center justify-center text-xs h-4 w-4 rounded-[2px] font-bold',
x.type === ENTITY_TYPE.FOREIGN_TABLE &&
'text-yellow-900 bg-yellow-500',
x.type === ENTITY_TYPE.MATERIALIZED_VIEW &&
'text-purple-1000 bg-purple-500'
// [Alaister]: tables endpoint doesn't distinguish between tables and partitioned tables
// once we update the endpoint to include partitioned tables, we can uncomment this
// x.type === ENTITY_TYPE.PARTITIONED_TABLE &&
// 'text-foreground-light bg-border-stronger'
)}
>
<Eye size={12} />
<p>View in Table Editor</p>
</DropdownMenuItem>
{Object.entries(ENTITY_TYPE)
.find(([, value]) => value === x.type)?.[0]?.[0]
?.toUpperCase()}
</div>
)}
</TooltipTrigger>
<TooltipContent side="bottom" className="capitalize">
{formatTooltipText(x.type)}
</TooltipContent>
</Tooltip>
</TableCell>
<TableCell>
{/* only show tooltips if required, to reduce noise */}
{x.name.length > 20 ? (
<Tooltip disableHoverableContent={true}>
<TooltipTrigger
asChild
className="max-w-[95%] overflow-hidden text-ellipsis whitespace-nowrap"
>
<p>{x.name}</p>
</TooltipTrigger>
{x.type === ENTITY_TYPE.TABLE && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canUpdateTables}
onClick={() => {
if (canUpdateTables) onEditTable(x)
}}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to edit this table',
},
}}
>
<Edit size={12} />
<p>Edit table</p>
</DropdownMenuItemTooltip>
<DropdownMenuItemTooltip
key="duplicate-table"
className="gap-x-2"
disabled={!canUpdateTables}
onClick={() => {
if (canUpdateTables) onDuplicateTable(x)
}}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to duplicate tables',
},
}}
>
<Copy size={12} />
<span>Duplicate Table</span>
</DropdownMenuItemTooltip>
<DropdownMenuSeparator />
<DropdownMenuItemTooltip
disabled={!canUpdateTables || isSchemaLocked}
className="gap-x-2"
onClick={() => {
if (canUpdateTables && !isSchemaLocked) {
onDeleteTable({ ...x, schema: selectedSchema })
}
}}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to delete tables',
},
}}
>
<Trash size={12} />
<p>Delete table</p>
</DropdownMenuItemTooltip>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
<TooltipContent side="bottom">{x.name}</TooltipContent>
</Tooltip>
) : (
<p>{x.name}</p>
)}
</div>
</Table.td>
</Table.tr>
))}
</>
}
/>
</TableCell>
<TableCell className="hidden lg:table-cell ">
{x.comment !== null ? (
<span className="lg:max-w-48 truncate inline-block" title={x.comment}>
{x.comment}
</span>
) : (
<p className="text-border-stronger">No description</p>
)}
</TableCell>
<TableCell className="hidden text-right xl:table-cell">
{x.rows !== undefined ? x.rows.toLocaleString() : '-'}
</TableCell>
<TableCell className="hidden text-right xl:table-cell">
{x.size !== undefined ? <code className="text-xs">{x.size}</code> : '-'}
</TableCell>
<TableCell className="hidden xl:table-cell text-center">
{(realtimePublication?.tables ?? []).find(
(table) => table.id === x.id
) ? (
<div className="flex justify-center">
<Check size={18} strokeWidth={2} className="text-brand" />
</div>
) : (
<div className="flex justify-center">
<X size={18} strokeWidth={2} className="text-foreground-lighter" />
</div>
)}
</TableCell>
<TableCell>
<div className="flex justify-end gap-2">
<Button
asChild
type="default"
iconRight={<Columns size={14} className="text-foreground-light" />}
className="whitespace-nowrap hover:border-muted"
style={{ paddingTop: 3, paddingBottom: 3 }}
>
<Link href={`/project/${ref}/database/tables/${x.id}`}>
{x.columns.length} columns
</Link>
</Button>
{!isSchemaLocked && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItem
className="flex items-center space-x-2"
onClick={() =>
router.push(`/project/${project?.ref}/editor/${x.id}`)
}
onMouseEnter={() =>
prefetchEditorTablePage({
id: x.id ? String(x.id) : undefined,
})
}
>
<Eye size={12} />
<p>View in Table Editor</p>
</DropdownMenuItem>
{x.type === ENTITY_TYPE.TABLE && (
<>
<DropdownMenuSeparator />
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canUpdateTables}
onClick={() => {
if (canUpdateTables) onEditTable(x)
}}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to edit this table',
},
}}
>
<Edit size={12} />
<p>Edit table</p>
</DropdownMenuItemTooltip>
<DropdownMenuItemTooltip
key="duplicate-table"
className="gap-x-2"
disabled={!canUpdateTables}
onClick={() => {
if (canUpdateTables) onDuplicateTable(x)
}}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to duplicate tables',
},
}}
>
<Copy size={12} />
<span>Duplicate Table</span>
</DropdownMenuItemTooltip>
<DropdownMenuSeparator />
<DropdownMenuItemTooltip
disabled={!canUpdateTables || isSchemaLocked}
className="gap-x-2"
onClick={() => {
if (canUpdateTables && !isSchemaLocked) {
onDeleteTable({ ...x, schema: selectedSchema })
}
}}
tooltip={{
content: {
side: 'left',
text: 'You need additional permissions to delete tables',
},
}}
>
<Trash size={12} />
<p>Delete table</p>
</DropdownMenuItemTooltip>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</TableCell>
</TableRow>
))}
</>
</TableBody>
</Table>
</Card>
</div>
)}
</div>
@@ -18,6 +18,8 @@ import {
Tooltip,
TooltipContent,
TooltipTrigger,
TableRow,
TableCell,
} from 'ui'
import { generateTriggerCreateSQL } from './TriggerList.utils'
@@ -58,35 +60,35 @@ const TriggerList = ({
if (_triggers.length === 0 && filterString.length === 0) {
return (
<Table.tr key={schema}>
<Table.td colSpan={7}>
<TableRow key={schema}>
<TableCell colSpan={7}>
<p className="text-sm text-foreground">No triggers created yet</p>
<p className="text-sm text-foreground-light">
There are no triggers found in the schema "{schema}"
</p>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)
}
if (_triggers.length === 0 && filterString.length > 0) {
return (
<Table.tr key={schema}>
<Table.td colSpan={7}>
<TableRow key={schema}>
<TableCell colSpan={7}>
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{filterString}" did not return any results
</p>
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)
}
return (
<>
{_triggers.map((x: any) => (
<Table.tr key={x.id}>
<Table.td className="space-x-2">
<TableRow key={x.id}>
<TableCell className="space-x-2">
<Tooltip>
<TooltipTrigger
onClick={() => editTrigger(x)}
@@ -98,35 +100,35 @@ const TriggerList = ({
{x.name}
</TooltipContent>
</Tooltip>
</Table.td>
</TableCell>
<Table.td className="break-all">
<TableCell className="break-all">
<p title={x.table} className="truncate">
{x.table}
</p>
</Table.td>
</TableCell>
<Table.td className="space-x-2">
<TableCell className="space-x-2">
<p title={x.function_name} className="truncate">
{x.function_name}
</p>
</Table.td>
</TableCell>
<Table.td>
<TableCell>
<div className="flex gap-2 flex-wrap">
{x.events.map((event: string) => (
<Badge key={event}>{`${x.activation} ${event}`}</Badge>
))}
</div>
</Table.td>
</TableCell>
<Table.td className="space-x-2">
<TableCell className="space-x-2">
<p title={x.orientation} className="truncate">
{x.orientation}
</p>
</Table.td>
</TableCell>
<Table.td>
<TableCell>
<div className="flex items-center justify-center">
{x.enabled_mode !== 'DISABLED' ? (
<Check strokeWidth={2} className="text-brand" />
@@ -134,9 +136,9 @@ const TriggerList = ({
<X strokeWidth={2} />
)}
</div>
</Table.td>
</TableCell>
<Table.td className="text-right">
<TableCell className="text-right">
{!isLocked && (
<div className="flex items-center justify-end">
{canUpdateTriggers ? (
@@ -216,8 +218,8 @@ const TriggerList = ({
)}
</div>
)}
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
))}
</>
)
@@ -6,7 +6,6 @@ import { useState } from 'react'
import AlphaPreview from 'components/to-be-cleaned/AlphaPreview'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import SchemaSelector from 'components/ui/SchemaSelector'
@@ -18,7 +17,17 @@ import { useQuerySchemaState } from 'hooks/misc/useSchemaQueryState'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useIsProtectedSchema, useProtectedSchemas } from 'hooks/useProtectedSchemas'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Input } from 'ui'
import {
AiIconAnimation,
Input,
Card,
Table,
TableHeader,
TableHead,
TableBody,
TableRow,
TableCell,
} from 'ui'
import { ProtectedSchemaWarning } from '../../ProtectedSchemaWarning'
import TriggerList from './TriggerList'
@@ -181,30 +190,32 @@ const TriggersList = ({
{isSchemaLocked && <ProtectedSchemaWarning schema={selectedSchema} entity="triggers" />}
<div className="w-full overflow-hidden overflow-x-auto">
<Table
head={
<>
<Table.th key="name">Name</Table.th>
<Table.th key="table">Table</Table.th>
<Table.th key="function">Function</Table.th>
<Table.th key="events">Events</Table.th>
<Table.th key="orientation">Orientation</Table.th>
<Table.th key="enabled" className="w-20">
Enabled
</Table.th>
<Table.th key="buttons" className="w-1/12"></Table.th>
</>
}
body={
<TriggerList
schema={selectedSchema}
filterString={filterString}
isLocked={isSchemaLocked}
editTrigger={editTrigger}
deleteTrigger={deleteTrigger}
/>
}
/>
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead key="name">Name</TableHead>
<TableHead key="table">Table</TableHead>
<TableHead key="function">Function</TableHead>
<TableHead key="events">Events</TableHead>
<TableHead key="orientation">Orientation</TableHead>
<TableHead key="enabled" className="w-20">
Enabled
</TableHead>
<TableHead key="buttons" className="w-1/12"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TriggerList
schema={selectedSchema}
filterString={filterString}
isLocked={isSchemaLocked}
editTrigger={editTrigger}
deleteTrigger={deleteTrigger}
/>
</TableBody>
</Table>
</Card>
</div>
</div>
)}
@@ -5,14 +5,11 @@ import { useMemo } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import ExtensionCard from 'components/interfaces/Database/Extensions/ExtensionCard'
import GraphiQL from 'components/interfaces/GraphQL/GraphiQL'
import { Loading } from 'components/ui/Loading'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useSessionAccessTokenQuery } from 'data/auth/session-access-token-query'
import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query'
import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { getRoleImpersonationJWT } from 'lib/role-impersonation'
import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
@@ -20,15 +17,8 @@ import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
export const GraphiQLTab = () => {
const { resolvedTheme } = useTheme()
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const currentTheme = resolvedTheme?.includes('dark') ? 'dark' : 'light'
const { data, isLoading: isExtensionsLoading } = useDatabaseExtensionsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})
const pgGraphqlExtension = (data ?? []).find((ext) => ext.name === 'pg_graphql')
const { data: accessToken } = useSessionAccessTokenQuery({ enabled: IS_PLATFORM })
const { data: apiKeys, isFetched } = useAPIKeysQuery({ projectRef, reveal: true })
@@ -80,29 +70,11 @@ export const GraphiQLTab = () => {
}
return customFetcher
}, [projectRef, getImpersonatedRoleState, jwtSecret, accessToken, serviceKey])
}, [projectRef, getImpersonatedRoleState, jwtSecret, accessToken, serviceKey, secretKey?.api_key])
if ((IS_PLATFORM && !accessToken) || !isFetched || (isExtensionsLoading && !pgGraphqlExtension)) {
if ((IS_PLATFORM && !accessToken) || !isFetched) {
return <Loading />
}
if (pgGraphqlExtension?.installed_version === null) {
return (
<div className="flex flex-col items-center justify-center flex-1 px-4">
<div className="w-full max-w-md">
<div className="mb-6">
<h1 className="mt-8 mb-2">Enable the GraphQL Extension</h1>
<p className="text-sm text-foreground-light">
Toggle the switch below to enable the GraphQL extension. You can then use the GraphQL
API with your Supabase Database.
</p>
</div>
<ExtensionCard extension={pgGraphqlExtension} />
</div>
</div>
)
}
return <GraphiQL fetcher={fetcher} theme={currentTheme} />
}
@@ -8,7 +8,6 @@ import {
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
Input,
} from 'ui'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
@@ -17,19 +16,22 @@ import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react'
import type { VaultSecret } from 'types'
import { Input } from 'ui-patterns/DataInputs/Input'
import EditSecretModal from './EditSecretModal'
import type { SecretTableColumn } from './Secrets.utils'
interface SecretRowProps {
secret: VaultSecret
row: VaultSecret
col: SecretTableColumn
onSelectRemove: (secret: VaultSecret) => void
}
const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
const SecretRow = ({ row, col, onSelectRemove }: SecretRowProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [modal, setModal] = useState<string | null>(null)
const [revealSecret, setRevealSecret] = useState(false)
const name = secret?.name ?? 'No name provided'
const name = row?.name ?? 'No name provided'
const { can: canManageSecrets } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
@@ -40,69 +42,23 @@ const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
{
projectRef: ref!,
connectionString: project?.connectionString,
id: secret.id,
id: row.id,
},
{
enabled: !!(ref! && secret.id) && revealSecret,
enabled: !!(ref! && row.id) && revealSecret,
}
)
const onCloseModal = () => setModal(null)
return (
<div className="px-6 py-4 flex items-center space-x-4">
<div className="space-y-1 min-w-[35%] max-w-[35%]">
<div>
<p className="text-sm text-foreground" title={name}>
{name}
</p>
{secret.description !== undefined && (
<p className="text-sm text-foreground-light" title={secret.description}>
{secret.description}
</p>
)}
</div>
<div className="flex items-center space-x-2 group">
<Key size={14} strokeWidth={2} className="text-foreground-light transition" />
<p className="text-foreground-light font-mono text-xs transition" title={secret.id}>
{secret.id}
</p>
</div>
</div>
<div className="flex items-center space-x-2 w-[40%]">
<Button
type="text"
className="px-1.5"
icon={
isFetching && revealedValue === undefined ? (
<Loader className="animate-spin" size={16} strokeWidth={1.5} />
) : !revealSecret ? (
<Eye size={16} strokeWidth={1.5} />
) : (
<EyeOff size={16} strokeWidth={1.5} />
)
}
onClick={() => setRevealSecret(!revealSecret)}
/>
<div className="flex-grow">
{revealSecret && revealedValue ? (
<Input copy size="small" className="font-mono" value={revealedValue} />
) : (
<p className="text-sm font-mono">••••••••••••••••••</p>
)}
</div>
</div>
<div className="flex items-center justify-end w-[25%] space-x-4">
<p className="text-sm text-foreground-light">
{secret.updated_at === secret.created_at ? 'Added' : 'Updated'} on{' '}
{dayjs(secret.updated_at).format('MMM D, YYYY')}
</p>
if (col.id === 'actions') {
return (
<div className="flex items-center justify-end w-full" onClick={(e) => e.stopPropagation()}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button title="Manage Secret" type="text" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<DropdownMenuContent side="bottom" align="end" className="w-40">
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canManageSecrets}
@@ -120,7 +76,7 @@ const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
<DropdownMenuItemTooltip
className="gap-x-2"
disabled={!canManageSecrets}
onClick={() => onSelectRemove(secret)}
onClick={() => onSelectRemove(row)}
tooltip={{
content: {
side: 'left',
@@ -134,8 +90,71 @@ const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
</DropdownMenuContent>
</DropdownMenu>
<EditSecretModal visible={modal === `edit`} secret={secret} onClose={onCloseModal} />
<EditSecretModal visible={modal === `edit`} secret={row} onClose={onCloseModal} />
</div>
)
}
if (col.id === 'secret_value') {
return (
<div className="flex items-center gap-2 w-full" onClick={(e) => e.stopPropagation()}>
<Button
type="text"
className="px-1.5"
icon={
isFetching && revealedValue === undefined ? (
<Loader className="animate-spin" size={16} strokeWidth={1.5} />
) : !revealSecret ? (
<Eye size={16} strokeWidth={1.5} />
) : (
<EyeOff size={16} strokeWidth={1.5} />
)
}
onClick={() => setRevealSecret(!revealSecret)}
/>
<div className="flex-grow min-w-0">
{revealSecret && revealedValue !== undefined ? (
<Input copy readOnly size="tiny" className="font-mono" value={revealedValue} />
) : (
<p className="text-sm font-mono text-foreground">••••••••••••••••••</p>
)}
</div>
</div>
)
}
if (col.id === 'updated_at') {
return (
<div className="w-full flex items-center justify-start">
<p className="text-xs text-foreground-light">
{row.updated_at === row.created_at ? 'Added' : 'Updated'} on{' '}
{dayjs(row.updated_at).format('MMM D, YYYY')}
</p>
</div>
)
}
if (col.id === 'id') {
return (
<div className="w-full flex items-center">
<Key size={12} strokeWidth={2} className="text-foreground-light mr-2" />
<p className="text-foreground-light text-xs font-mono truncate" title={row.id}>
{row.id}
</p>
</div>
)
}
return (
<div className="w-full flex flex-col justify-center">
<p className="text-xs text-foreground truncate" title={name}>
{name}
</p>
{row.description !== undefined && row.description !== '' && (
<div>
<p className="text-xs text-foreground-lighter w-full truncate">{row.description}</p>
</div>
)}
</div>
)
}
@@ -0,0 +1,57 @@
import type { Column } from 'react-data-grid'
import type { VaultSecret } from 'types'
import { cn } from 'ui'
import SecretRow from './SecretRow'
export type SecretColumnId = 'secret' | 'id' | 'secret_value' | 'updated_at' | 'actions'
export interface SecretTableColumn {
id: SecretColumnId
name: string
minWidth?: number
width?: number
maxWidth?: number
}
export const SECRET_TABLE_COLUMNS: SecretTableColumn[] = [
{ id: 'secret', name: 'Secret', minWidth: 300, width: 360 },
{ id: 'id', name: 'ID', minWidth: 220, width: 260 },
{ id: 'secret_value', name: 'Value', minWidth: 320, width: 420 },
{ id: 'updated_at', name: 'Last updated', minWidth: 180 },
{ id: 'actions', name: '', minWidth: 75, width: 75 },
]
export const formatSecretColumns = ({
onSelectRemove,
}: {
onSelectRemove: (secret: VaultSecret) => void
}): Column<VaultSecret>[] => {
return SECRET_TABLE_COLUMNS.map((col) => {
const result: Column<VaultSecret> = {
key: col.id,
name: col.name,
minWidth: col.minWidth ?? 100,
maxWidth: col.maxWidth,
width: col.width,
resizable: false,
sortable: false,
draggable: false,
headerCellClass: undefined,
renderHeaderCell: () => {
return (
<div
className={cn(
'flex items-center justify-between font-normal text-xs w-full',
col.id === 'secret' && 'ml-8'
)}
>
<p className="!text-foreground">{col.name}</p>
</div>
)
},
renderCell: ({ row }) => <SecretRow row={row} col={col} onSelectRemove={onSelectRemove} />,
}
return result
})
}
@@ -1,7 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import DataGrid, { Row } from 'react-data-grid'
import { sortBy } from 'lodash'
import { Loader, Search, X } from 'lucide-react'
import { Fragment, useEffect, useState } from 'react'
import { Loader, RefreshCw, Search, X } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
@@ -13,16 +14,17 @@ import type { VaultSecret } from 'types'
import {
Button,
Input,
LoadingLine,
cn,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Separator,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
} from 'ui'
import AddNewSecretModal from './AddNewSecretModal'
import DeleteSecretModal from './DeleteSecretModal'
import SecretRow from './SecretRow'
import { formatSecretColumns } from './Secrets.utils'
export const SecretsManagement = () => {
const { search } = useParams()
@@ -31,140 +33,162 @@ export const SecretsManagement = () => {
const [searchValue, setSearchValue] = useState<string>('')
const [showAddSecretModal, setShowAddSecretModal] = useState(false)
const [selectedSecretToRemove, setSelectedSecretToRemove] = useState<VaultSecret>()
const [selectedSort, setSelectedSort] = useState('updated_at')
const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at')
const { can: canManageSecrets } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
const { data, isLoading } = useVaultSecretsQuery({
const { data, isLoading, isRefetching, refetch, error, isError } = useVaultSecretsQuery({
projectRef: project?.ref!,
connectionString: project?.connectionString,
})
const allSecrets = data || []
const secrets = sortBy(
searchValue.length > 0
? allSecrets.filter(
(secret) =>
(secret?.name ?? '').toLowerCase().includes(searchValue.toLowerCase()) ||
(secret?.id ?? '').toLowerCase().includes(searchValue.toLowerCase())
)
: allSecrets,
(s) => {
if (selectedSort === 'updated_at') {
return Number(new Date(s.updated_at))
} else {
return s[selectedSort as keyof VaultSecret]
}
const allSecrets = useMemo(() => data || [], [data])
const secrets = useMemo(() => {
const filtered =
searchValue.length > 0
? allSecrets.filter(
(secret) =>
(secret?.name ?? '').toLowerCase().includes(searchValue.trim().toLowerCase()) ||
(secret?.id ?? '').toLowerCase().includes(searchValue.trim().toLowerCase())
)
: allSecrets
if (selectedSort === 'updated_at') {
return sortBy(filtered, (s) => Number(new Date(s.updated_at))).reverse()
}
)
return sortBy(filtered, (s) => (s.name || '').toLowerCase())
}, [allSecrets, searchValue, selectedSort])
useEffect(() => {
if (search !== undefined) setSearchValue(search)
}, [search])
const columns = useMemo(
() =>
formatSecretColumns({
onSelectRemove: (secret) => setSelectedSecretToRemove(secret),
}),
[]
)
return (
<>
<div className="space-y-4 p-4 md:p-10">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-2">
<div className="flex flex-col md:flex-row md:items-center gap-2">
<Input
className="md:w-52"
size="tiny"
placeholder="Search by name or key ID"
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
icon={<Search strokeWidth={2} size={16} />}
actions={
searchValue.length > 0
? [
<Button
key="clear"
size="tiny"
type="text"
icon={<X />}
className="px-1"
onClick={() => setSearchValue('')}
/>,
]
: []
}
/>
<Select_Shadcn_ value={selectedSort} onValueChange={setSelectedSort}>
<SelectTrigger_Shadcn_ size="tiny" className="md:w-44">
<SelectValue_Shadcn_ asChild>
<>Sort by {selectedSort}</>
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="updated_at" className="text-xs">
Updated at
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="name" className="text-xs">
Name
</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div className="flex items-center gap-x-2">
<DocsButton href="https://supabase.com/docs/guides/database/vault" />
<ButtonTooltip
type="primary"
disabled={!canManageSecrets}
onClick={() => setShowAddSecretModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canManageSecrets
? 'You need additional permissions to add secrets'
: undefined,
},
}}
>
Add new secret
</ButtonTooltip>
</div>
</div>
<div className="h-full w-full space-y-4">
<div className="h-full w-full flex flex-col relative">
<div className="bg-surface-200 py-3 px-10 flex items-center justify-between flex-wrap">
<div className="flex items-center gap-2">
<Input
size="tiny"
className="w-52"
placeholder="Search by name or key ID"
icon={<Search size={14} />}
value={searchValue ?? ''}
onChange={(e) => setSearchValue(e.target.value)}
actions={[
searchValue && (
<Button
size="tiny"
type="text"
icon={<X />}
onClick={() => {
setSearchValue('')
}}
className="p-0 h-5 w-5"
/>
),
]}
/>
{/* Table of secrets */}
<div className="border border-default rounded">
{isLoading ? (
<Select_Shadcn_ value={selectedSort} onValueChange={(v) => setSelectedSort(v as any)}>
<SelectTrigger_Shadcn_ size="tiny" className="w-44">
<SelectValue_Shadcn_ asChild>
<>Sort by {selectedSort}</>
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="updated_at" className="text-xs">
Updated at
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="name" className="text-xs">
Name
</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div className="flex items-center gap-x-2">
<Button
type="default"
icon={<RefreshCw />}
loading={isRefetching}
onClick={() => refetch()}
>
Refresh
</Button>
<DocsButton href="https://supabase.com/docs/guides/database/vault" />
<ButtonTooltip
type="primary"
disabled={!canManageSecrets}
onClick={() => setShowAddSecretModal(true)}
tooltip={{
content: {
side: 'bottom',
text: !canManageSecrets
? 'You need additional permissions to add secrets'
: undefined,
},
}}
>
Add new secret
</ButtonTooltip>
</div>
</div>
<LoadingLine loading={isLoading || isRefetching} />
{isError ? (
<div className="px-6 py-6 space-x-2 flex items-center justify-center">
<Loader className="animate-spin text-foreground-light" size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground">Loading secrets from the Vault</p>
<p className="text-sm text-foreground">Failed to load secrets</p>
</div>
) : (
<>
{secrets.map((secret, idx) => {
return (
<Fragment key={`secret-${idx}`}>
<SecretRow secret={secret} onSelectRemove={setSelectedSecretToRemove} />
{idx !== secrets.length - 1 && <Separator />}
</Fragment>
<DataGrid
className="flex-grow border-t-0"
rowHeight={52}
headerRowHeight={36}
columns={columns}
rows={secrets}
rowKeyGetter={(row: VaultSecret) => row.id}
rowClass={() => {
return cn(
'cursor-pointer',
'[&>.rdg-cell]:border-box [&>.rdg-cell]:outline-none [&>.rdg-cell]:shadow-none',
'[&>.rdg-cell:first-child>div]:pl-8'
)
})}
{secrets.length === 0 && (
<>
{searchValue.length === 0 ? (
<div className="px-6 py-6 space-y-1 flex flex-col items-center justify-center">
<p className="text-sm text-foreground">No secrets added yet</p>
<p className="text-sm text-foreground-light">
The Vault allows you to store sensitive information like API keys
</p>
</div>
) : (
<div className="px-6 py-4 space-y-1">
<p className="text-sm text-foreground">No results found</p>
<p className="text-sm text-foreground-light">
Your search for "{searchValue}" did not return any results
</p>
</div>
)}
</>
)}
</>
}}
renderers={{
renderRow(_, props) {
return <Row key={(props.row as VaultSecret).id} {...props} />
},
}}
/>
)}
{secrets.length === 0 && !isLoading && !isError ? (
<div className="absolute top-32 px-6 w-full">
<div className="text-center text-sm flex flex-col gap-y-1">
<p className="text-foreground">
{searchValue ? 'No secrets found' : 'No secrets added yet'}
</p>
<p className="text-foreground-light">
{searchValue
? `There are currently no secrets based on the search "${searchValue}"`
: 'The Vault allows you to store sensitive information like API keys'}
</p>
</div>
</div>
) : null}
</div>
</div>
@@ -1,7 +1,6 @@
import { ArrowRight, Check, Minus, User, X } from 'lucide-react'
import Link from 'next/link'
import Table from 'components/to-be-cleaned/Table'
import PartnerIcon from 'components/ui/PartnerIcon'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
@@ -17,6 +16,8 @@ import {
HoverCard_Shadcn_,
ScrollArea,
cn,
TableRow,
TableCell,
} from 'ui'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { isInviteExpired } from '../Organization.utils'
@@ -68,8 +69,8 @@ export const MemberRow = ({ member }: MemberRowProps) => {
}).length > 0
return (
<Table.tr>
<Table.td>
<TableRow>
<TableCell>
<div className="flex items-center gap-x-4">
<ProfileImage
alt={member.primary_email ?? member.username ?? ''}
@@ -103,18 +104,18 @@ export const MemberRow = ({ member }: MemberRowProps) => {
/>
)}
</div>
</Table.td>
</TableCell>
<Table.td>
<TableCell>
{isInvitedUser && member.invited_at && (
<Badge variant={isInviteExpired(member.invited_at) ? 'destructive' : 'warning'}>
{isInviteExpired(member.invited_at) ? 'Expired' : 'Invited'}
</Badge>
)}
{member.is_sso_user && <Badge variant="default">SSO</Badge>}
</Table.td>
</TableCell>
<Table.td>
<TableCell>
<div className="flex items-center justify-center">
{member.mfa_enabled ? (
<Check className="text-brand" strokeWidth={2} size={20} />
@@ -122,9 +123,9 @@ export const MemberRow = ({ member }: MemberRowProps) => {
<X className="text-foreground-light" strokeWidth={1.5} size={20} />
)}
</div>
</Table.td>
</TableCell>
<Table.td className="max-w-64">
<TableCell className="max-w-64">
{isLoadingRoles ? (
<ShimmeringLoader className="w-32" />
) : isObfuscated ? (
@@ -201,11 +202,11 @@ export const MemberRow = ({ member }: MemberRowProps) => {
)
})
)}
</Table.td>
</TableCell>
<Table.td>
<TableCell>
<MemberActions member={member} />
</Table.td>
</Table.tr>
</TableCell>
</TableRow>
)
}
@@ -1,7 +1,6 @@
import { AlertCircle, HelpCircle } from 'lucide-react'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useOrganizationRolesV2Query } from 'data/organization-members/organization-roles-query'
@@ -9,7 +8,20 @@ import { useOrganizationMembersQuery } from 'data/organizations/organization-mem
import { useProfile } from 'lib/profile'
import { partition } from 'lodash'
import { useMemo } from 'react'
import { Button, Loading, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import {
Button,
Loading,
Tooltip,
TooltipContent,
TooltipTrigger,
Table,
TableHeader,
TableHead,
TableBody,
TableCell,
TableRow,
Card,
} from 'ui'
import { Admonition } from 'ui-patterns'
import { MemberRow } from './MemberRow'
@@ -78,77 +90,86 @@ const MembersView = ({ searchString }: MembersViewProps) => {
{isSuccessMembers && (
<div className="rounded w-full overflow-hidden overflow-x-scroll">
<Loading active={!filteredMembers}>
<Table
head={[
<Table.th key="header-user">User</Table.th>,
<Table.th key="header-status" className="w-24" />,
<Table.th key="header-mfa" className="text-center w-32">
Enabled MFA
</Table.th>,
<Table.th key="header-role" className="flex items-center space-x-1">
<span>Role</span>
<Tooltip>
<TooltipTrigger asChild>
<Button asChild type="text" className="px-1">
<a
target="_blank"
rel="noreferrer"
href="https://supabase.com/docs/guides/platform/access-control"
>
<HelpCircle size={14} className="text-foreground-light" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">How to configure access control?</TooltipContent>
</Tooltip>
</Table.th>,
<Table.th key="header-action" />,
]}
body={[
...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole
? [
<Table.tr key="project-scope-notice">
<Table.td colSpan={12} className="!p-0">
<Admonition
type="note"
title="You are currently assigned with project scoped roles in this organization"
description="All the members within the organization will not be visible to you"
className="m-0 bg-alternative border-0 rounded-none"
/>
</Table.td>
</Table.tr>,
]
: []),
...(!!user ? [<MemberRow key={user.gotrue_id} member={user} />] : []),
...sortedMembers.map((member) => (
<MemberRow key={member.gotrue_id} member={member} />
)),
...(searchString.length > 0 && filteredMembers.length === 0
? [
<Table.tr key="no-results" className="bg-panel-secondary-light">
<Table.td colSpan={12}>
<div className="flex items-center space-x-3 opacity-75">
<AlertCircle size={16} strokeWidth={2} />
<p className="text-foreground-light">
No users matched the search query "{searchString}"
</p>
</div>
</Table.td>
</Table.tr>,
]
: []),
<Table.tr key="footer" className="bg-panel-secondary-light">
<Table.td colSpan={12}>
<p className="text-foreground-light">
{searchString ? `${filteredMembers.length} of ` : ''}
{members.length || '0'} {members.length == 1 ? 'user' : 'users'}
</p>
</Table.td>
</Table.tr>,
]}
/>
</Loading>
<Card>
<Loading active={!filteredMembers}>
<Table>
<TableHeader>
<TableRow>
<TableHead key="header-user">User</TableHead>
<TableHead key="header-status" className="w-24" />
<TableHead key="header-mfa" className="text-center w-32">
Enabled MFA
</TableHead>
<TableHead key="header-role" className="flex items-center space-x-1">
<span>Role</span>
<Tooltip>
<TooltipTrigger asChild>
<Button asChild type="text" className="px-1">
<a
target="_blank"
rel="noreferrer"
href="https://supabase.com/docs/guides/platform/access-control"
>
<HelpCircle size={14} className="text-foreground-light" />
</a>
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
How to configure access control?
</TooltipContent>
</Tooltip>
</TableHead>
<TableHead key="header-action" />
</TableRow>
</TableHeader>
<TableBody>
{[
...(isSuccessRoles && isSuccessMembers && !isOrgScopedRole
? [
<TableRow key="project-scope-notice">
<TableCell colSpan={12} className="!p-0">
<Admonition
type="note"
title="You are currently assigned with project scoped roles in this organization"
description="All the members within the organization will not be visible to you"
className="m-0 bg-alternative border-0 rounded-none"
/>
</TableCell>
</TableRow>,
]
: []),
...(!!user ? [<MemberRow key={user.gotrue_id} member={user} />] : []),
...sortedMembers.map((member) => (
<MemberRow key={member.gotrue_id} member={member} />
)),
...(searchString.length > 0 && filteredMembers.length === 0
? [
<TableRow key="no-results" className="bg-panel-secondary-light">
<TableCell colSpan={12}>
<div className="flex items-center space-x-3 opacity-75">
<AlertCircle size={16} strokeWidth={2} />
<p className="text-foreground-light">
No users matched the search query "{searchString}"
</p>
</div>
</TableCell>
</TableRow>,
]
: []),
<TableRow key="footer" className="bg-panel-secondary-light">
<TableCell colSpan={12}>
<p className="text-foreground-light">
{searchString ? `${filteredMembers.length} of ` : ''}
{members.length || '0'} {members.length == 1 ? 'user' : 'users'}
</p>
</TableCell>
</TableRow>,
]}
</TableBody>
</Table>
</Loading>
</Card>
</div>
)}
</>
@@ -4,7 +4,7 @@ import { Extensions } from 'components/interfaces/Database'
import DatabaseLayout from 'components/layouts/DatabaseLayout/DatabaseLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import NoPermission from 'components/ui/NoPermission'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import type { NextPageWithLayout } from 'types'
@@ -18,14 +18,17 @@ const DatabaseExtensions: NextPageWithLayout = () => {
}
return (
<ScaffoldContainer>
<ScaffoldSection>
<div className="col-span-12">
<FormHeader title="Database Extensions" />
<PageLayout
size="large"
title="Database Extensions"
subtitle="Manage what extensions are installed in your database"
>
<ScaffoldContainer size="large">
<ScaffoldSection isFullWidth>
<Extensions />
</div>
</ScaffoldSection>
</ScaffoldContainer>
</ScaffoldSection>
</ScaffoldContainer>
</PageLayout>
)
}
+7 -7
View File
@@ -35,32 +35,32 @@
@layer utilities {
/* Heading */
.heading-title {
@apply scroll-m-20 text-2xl tracking-tight text-foreground;
@apply scroll-m-20 text-2xl tracking-tight;
}
.heading-section {
@apply scroll-m-20 text-xl text-foreground;
@apply scroll-m-20 text-xl;
}
.heading-subSection {
@apply scroll-m-20 text-base text-foreground;
@apply scroll-m-20 text-base;
}
.heading-default {
@apply scroll-m-20 text-sm font-medium text-foreground;
@apply scroll-m-20 text-sm font-medium;
}
.heading-compact {
@apply scroll-m-20 text-xs font-medium text-foreground;
@apply scroll-m-20 text-xs font-medium;
}
.heading-meta {
@apply text-xs font-mono uppercase tracking-wider font-medium text-foreground-light;
@apply text-xs font-mono uppercase tracking-wider font-medium;
}
/* Text */
.text-default {
@apply text-base text-foreground-light;
@apply text-base;
}
.text-subTitle {
@@ -6,7 +6,10 @@ const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElemen
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn('rounded-lg border bg-surface-100 text-card-foreground shadow-sm', className)}
className={cn(
'overflow-hidden rounded-lg border bg-surface-100 text-card-foreground shadow-sm',
className
)}
{...props}
/>
)
@@ -19,7 +19,7 @@ const TableHeader = React.forwardRef<
HTMLTableSectionElement,
React.HTMLAttributes<HTMLTableSectionElement>
>(({ className, ...props }, ref) => (
<thead ref={ref} className={cn('[&_tr]:border-b', className)} {...props} />
<thead ref={ref} className={cn('[&_tr]:border-b [&>tr]:bg-200', className)} {...props} />
))
TableHeader.displayName = 'TableHeader'
@@ -64,7 +64,7 @@ const TableHead = React.forwardRef<
<th
ref={ref}
className={cn(
'h-12 px-4 text-left align-middle heading-meta [&:has([role=checkbox])]:pr-0',
'h-10 px-4 text-left align-middle heading-meta text-foreground-lighter [&:has([role=checkbox])]:pr-0',
className
)}
{...props}