mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Table consistency (#37924)
* update project tables * org team table * Minor fixes --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
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
|
||||
|
||||
+91
-99
@@ -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>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
+23
-20
@@ -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>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
|
||||
+39
-30
@@ -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
|
||||
})
|
||||
}
|
||||
+141
-117
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user