| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { AlertCircle, Info, Search } from 'lucide-react'
- import Link from 'next/link'
- import { useRef, useState } from 'react'
- import { toast } from 'sonner'
- import {
- Button,
- Card,
- Switch,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import { Input } from 'ui-patterns/DataInputs/Input'
- import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
- import { PublicationSkeleton } from './PublicationSkeleton'
- import AlertError from '@/components/ui/AlertError'
- import InformationBox from '@/components/ui/InformationBox'
- import { NoSearchResults } from '@/components/ui/NoSearchResults'
- import { useDatabasePublicationsQuery } from '@/data/database-publications/database-publications-query'
- import { useDatabasePublicationUpdateMutation } from '@/data/database-publications/database-publications-update-mutation'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { onSearchInputEscape } from '@/lib/keyboard'
- import { SHORTCUT_IDS } from '@/state/shortcuts/registry'
- import { useShortcut } from '@/state/shortcuts/useShortcut'
- interface PublicationEvent {
- event: string
- key: string
- }
- export const PublicationsList = () => {
- const { ref } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const [filterString, setFilterString] = useState<string>('')
- const searchInputRef = useRef<HTMLInputElement>(null)
- useShortcut(
- SHORTCUT_IDS.LIST_PAGE_FOCUS_SEARCH,
- () => {
- searchInputRef.current?.focus()
- searchInputRef.current?.select()
- },
- { label: 'Search publications' }
- )
- useShortcut(SHORTCUT_IDS.LIST_PAGE_RESET_FILTERS, () => {
- setFilterString('')
- })
- const {
- data = [],
- error,
- isPending: isLoading,
- isSuccess,
- isError,
- } = useDatabasePublicationsQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const { mutate: updatePublications } = useDatabasePublicationUpdateMutation({
- onSuccess: () => {
- toast.success('Successfully updated event')
- setToggleListenEventValue(null)
- },
- })
- const { can: canUpdatePublications, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- 'publications'
- )
- const publicationEvents: PublicationEvent[] = [
- { event: 'Insert', key: 'publish_insert' },
- { event: 'Update', key: 'publish_update' },
- { event: 'Delete', key: 'publish_delete' },
- { event: 'Truncate', key: 'publish_truncate' },
- ]
- const publications = (
- filterString.length === 0
- ? data
- : data.filter((publication) => publication.name.includes(filterString))
- ).sort((a, b) => a.id - b.id)
- const [toggleListenEventValue, setToggleListenEventValue] = useState<{
- publication: any
- event: PublicationEvent
- currentStatus: any
- } | null>(null)
- const toggleListenEvent = async () => {
- if (!toggleListenEventValue || !project) return
- const { publication, event, currentStatus } = toggleListenEventValue
- const payload = {
- projectRef: project.ref,
- connectionString: project.connectionString,
- id: publication.id,
- } as any
- payload[`publish_${event.event.toLowerCase()}`] = !currentStatus
- updatePublications(payload)
- }
- return (
- <>
- <div className="flex items-center justify-between">
- <div className="flex items-center">
- <Input
- ref={searchInputRef}
- size="tiny"
- icon={<Search />}
- className="w-48"
- placeholder="Search for a publication"
- value={filterString}
- onChange={(e) => setFilterString(e.target.value)}
- onKeyDown={onSearchInputEscape(filterString, setFilterString)}
- />
- </div>
- {isPermissionsLoaded && !canUpdatePublications && (
- <div className="w-[500px]">
- <InformationBox
- icon={<AlertCircle className="text-foreground-light" strokeWidth={2} />}
- title="You need additional permissions to update database publications"
- />
- </div>
- )}
- </div>
- <div className="w-full overflow-hidden overflow-x-auto">
- <Card>
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead>Name</TableHead>
- <TableHead>System ID</TableHead>
- <TableHead>Insert</TableHead>
- <TableHead>Update</TableHead>
- <TableHead>Delete</TableHead>
- <TableHead>Truncate</TableHead>
- <TableHead />
- </TableRow>
- </TableHeader>
- <TableBody>
- {isLoading &&
- Array.from({ length: 2 }).map((_, i) => <PublicationSkeleton key={i} index={i} />)}
- {isError && (
- <TableRow>
- <TableCell colSpan={7}>
- <AlertError error={error} subject="Failed to retrieve publications" />
- </TableCell>
- </TableRow>
- )}
- {!isLoading && publications.length === 0 && (
- <TableRow>
- <TableCell colSpan={7}>
- <NoSearchResults
- searchString={filterString}
- onResetFilter={() => setFilterString('')}
- className="border-none !p-0"
- />
- </TableCell>
- </TableRow>
- )}
- {isSuccess &&
- publications.map((x) => (
- <TableRow key={x.name}>
- <TableCell>
- <div className="flex items-center gap-x-2">
- {x.name}
- {/* [Joshen] Making this tooltip very specific for these 2 publications */}
- {['briven_realtime', 'briven_realtime_messages_publication'].includes(
- x.name
- ) && (
- <Tooltip>
- <TooltipTrigger>
- <Info size={14} className="text-foreground-light" />
- </TooltipTrigger>
- <TooltipContent side="bottom">
- {x.name === 'briven_realtime'
- ? 'Managed by Briven and handles Postgres changes'
- : x.name === 'briven_realtime_messages_publication'
- ? 'Managed by Briven and handles broadcasts from the database'
- : undefined}
- </TooltipContent>
- </Tooltip>
- )}
- </div>
- </TableCell>
- <TableCell>{x.id}</TableCell>
- {publicationEvents.map((event) => (
- <TableCell key={event.key}>
- <Switch
- size="small"
- checked={(x as any)[event.key]}
- disabled={!canUpdatePublications}
- onClick={() => {
- setToggleListenEventValue({
- publication: x,
- event,
- currentStatus: (x as any)[event.key],
- })
- }}
- />
- </TableCell>
- ))}
- <TableCell>
- <div className="flex justify-end">
- <Button asChild type="default" style={{ paddingTop: 3, paddingBottom: 3 }}>
- <Link href={`/project/${ref}/database/publications/${x.id}`}>
- {x.tables === null
- ? 'All tables'
- : `${x.tables.length} ${x.tables.length === 1 ? 'table' : 'tables'}`}
- </Link>
- </Button>
- </div>
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </Card>
- </div>
- <ConfirmationModal
- visible={toggleListenEventValue !== null}
- title={`Confirm to toggle sending ${toggleListenEventValue?.event.event.toLowerCase()} events`}
- confirmLabel="Confirm"
- confirmLabelLoading="Updating"
- onCancel={() => setToggleListenEventValue(null)}
- onConfirm={() => {
- toggleListenEvent()
- }}
- >
- <p className="text-sm text-foreground-light">
- Are you sure you want to {toggleListenEventValue?.currentStatus ? 'stop' : 'start'}{' '}
- sending {toggleListenEventValue?.event.event.toLowerCase()} events for{' '}
- {toggleListenEventValue?.publication.name}?
- </p>
- </ConfirmationModal>
- </>
- )
- }
|