mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
* refactor(infinite list): use @tanstack/react-virtual for virtualization Swapping virtualization libraries from `react-window` to `@tanstack/react-virtual`. Motivation: we need a completely headless library for maximum flexibility. `react-window` injects extra DOM elements which makes it hard to customize styling and placement on elements that are very picky about their DOM structure, like tables. * refactor(table editor menu): use new infinite list * refactor(notifications): use new infinite list * cleanup(infinite list): remove old infinite list * refactor(storage menu): use new infinite list & remove react-window deps
190 lines
6.9 KiB
TypeScript
190 lines
6.9 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import { Archive, ArchiveRestoreIcon, ExternalLink } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useEffect } from 'react'
|
|
import { useInView } from 'react-intersection-observer'
|
|
import { Button, cn } from 'ui'
|
|
|
|
import { Markdown } from 'components/interfaces/Markdown'
|
|
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
|
|
import { useVirtualizerContext } from 'components/ui/InfiniteList'
|
|
import { NotificationData, type NotificationsData } from 'data/notifications/notifications-v2-query'
|
|
import { useProjectDetailQuery } from 'data/projects/project-detail-query'
|
|
import type { Organization } from 'types'
|
|
import { CriticalIcon, WarningIcon } from 'ui'
|
|
|
|
interface NotificationRowProps {
|
|
index: number
|
|
item: NotificationsData[number]
|
|
getOrganizationById: (id: number) => Organization
|
|
getOrganizationBySlug: (slug: string) => Organization
|
|
onUpdateNotificationStatus: (id: string, status: 'archived' | 'seen') => void
|
|
queueMarkRead: (id: string) => void
|
|
}
|
|
|
|
const NotificationRow = ({
|
|
index,
|
|
item: notification,
|
|
getOrganizationById,
|
|
getOrganizationBySlug,
|
|
onUpdateNotificationStatus,
|
|
queueMarkRead,
|
|
}: NotificationRowProps) => {
|
|
const { virtualizer } = useVirtualizerContext()
|
|
const { ref: viewRef, inView } = useInView()
|
|
|
|
const { status, priority } = notification
|
|
const data = notification.data as NotificationData
|
|
|
|
const { data: project } = useProjectDetailQuery({ ref: data.project_ref })
|
|
|
|
const organization =
|
|
data.org_slug !== undefined
|
|
? getOrganizationBySlug(data.org_slug)
|
|
: project !== undefined
|
|
? getOrganizationById(project.organization_id)
|
|
: undefined
|
|
|
|
const daysFromNow = dayjs().diff(dayjs(notification.inserted_at), 'day')
|
|
const formattedTimeFromNow = dayjs(notification.inserted_at).fromNow()
|
|
const formattedInsertedAt = dayjs(notification.inserted_at).format('MMM DD, YYYY')
|
|
|
|
const onButtonAction = (type?: string) => {
|
|
// [Joshen] Implement accordingly - BE team will need to give us a heads up on this
|
|
console.log('Action', type)
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (inView && notification.status === 'new') {
|
|
queueMarkRead(notification.id)
|
|
}
|
|
}, [inView])
|
|
|
|
return (
|
|
<div
|
|
ref={virtualizer.measureElement}
|
|
className={cn(
|
|
`p-4 flex justify-between gap-x-3 group`,
|
|
index !== 0 ? 'border-t border-overlay' : '',
|
|
status === 'new' ? 'bg-surface-100/50' : 'bg-background'
|
|
)}
|
|
>
|
|
<div ref={viewRef} className="flex flex-col gap-y-2.5 w-full py-0.5">
|
|
{(project !== undefined || organization !== undefined) && (
|
|
<div className="flex items-center max-w-[350px]">
|
|
{organization !== undefined && (
|
|
<Link
|
|
title={organization.name}
|
|
href={`/org/${organization.slug}/general`}
|
|
className="text-xs transition text-foreground-light hover:text-foreground underline truncate"
|
|
>
|
|
{organization.name}
|
|
</Link>
|
|
)}
|
|
{organization !== undefined && project !== undefined && (
|
|
<span className="text-foreground-lighter">
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width="16"
|
|
height="16"
|
|
stroke="currentColor"
|
|
strokeWidth="2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="none"
|
|
shapeRendering="geometricPrecision"
|
|
>
|
|
<path d="M16 3.549L7.12 20.600"></path>
|
|
</svg>
|
|
</span>
|
|
)}
|
|
{project !== undefined && (
|
|
<Link
|
|
title={project.name}
|
|
href={`/project/${project.ref}`}
|
|
className="text-xs transition text-foreground-light hover:text-foreground underline truncate"
|
|
>
|
|
{project.name}
|
|
</Link>
|
|
)}
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-x-2">
|
|
<p className="text-sm break-word">
|
|
{data.title}{' '}
|
|
<span className="ml-1 text-xs text-foreground-light capitalize-sentence">
|
|
{daysFromNow > 1 ? formattedInsertedAt : formattedTimeFromNow}
|
|
</span>
|
|
</p>
|
|
</div>
|
|
{data.message !== undefined && (
|
|
<Markdown className="text-foreground-light text-xs" content={data.message} />
|
|
)}
|
|
{(data.actions ?? []).length > 0 && (
|
|
<div className="flex items-center gap-x-2">
|
|
{data.actions.map((action, idx) => {
|
|
const key = `${notification.id}-action-${idx}`
|
|
if (action.url !== undefined) {
|
|
const url = action.url.includes('[ref]')
|
|
? action.url.replace('[ref]', project?.ref ?? '_')
|
|
: action.url.includes('[slug]')
|
|
? action.url.replace('[slug]', organization?.slug ?? '_')
|
|
: action.url
|
|
return (
|
|
<Button
|
|
key={key}
|
|
type="default"
|
|
icon={<ExternalLink strokeWidth={1.5} />}
|
|
asChild
|
|
>
|
|
<Link href={url} target="_blank" rel="noreferrer">
|
|
{action.label}
|
|
</Link>
|
|
</Button>
|
|
)
|
|
} else if (action.action_type !== undefined) {
|
|
return (
|
|
<Button
|
|
key={key}
|
|
type="default"
|
|
onClick={() => onButtonAction(action.action_type)}
|
|
>
|
|
{action.label}
|
|
</Button>
|
|
)
|
|
} else {
|
|
return null
|
|
}
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex flex-col items-center gap-y-2">
|
|
{priority === 'Warning' && <WarningIcon className="w-5 h-5" />}
|
|
{priority === 'Critical' && <CriticalIcon className="w-5 h-5" />}
|
|
{notification.status === 'archived' ? (
|
|
<ButtonTooltip
|
|
type="outline"
|
|
icon={
|
|
<ArchiveRestoreIcon size={13} strokeWidth={2} className="text-foreground-light" />
|
|
}
|
|
className="p-1.5 group-hover:opacity-100 opacity-0 transition rounded-full"
|
|
onClick={() => onUpdateNotificationStatus(notification.id, 'seen')}
|
|
tooltip={{ content: { text: 'Unarchive', side: 'bottom' } }}
|
|
/>
|
|
) : (
|
|
<ButtonTooltip
|
|
type="outline"
|
|
icon={<Archive size={13} strokeWidth={2} className="text-foreground-light" />}
|
|
className="p-1.5 group-hover:opacity-100 opacity-0 transition rounded-full"
|
|
onClick={() => onUpdateNotificationStatus(notification.id, 'archived')}
|
|
tooltip={{ content: { text: 'Archive', side: 'bottom' } }}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default NotificationRow
|