mirror of
https://github.com/supabase/supabase.git
synced 2026-07-06 12:44:31 +08:00
230 lines
8.4 KiB
TypeScript
230 lines
8.4 KiB
TypeScript
import * as Tooltip from '@radix-ui/react-tooltip'
|
|
import dayjs from 'dayjs'
|
|
import { ArchiveRestoreIcon } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { useEffect, useRef } from 'react'
|
|
import { useInView } from 'react-intersection-observer'
|
|
import { Button, IconArchive, IconExternalLink, cn } from 'ui'
|
|
|
|
import { Markdown } from 'components/interfaces/Markdown'
|
|
import { CriticalIcon, WarningIcon } from 'ui-patterns/Icons/StatusIcons'
|
|
import type { ItemRenderer } from 'components/ui/InfiniteList'
|
|
import { Notification, NotificationData } from 'data/notifications/notifications-v2-query'
|
|
import { ProjectInfo } from 'data/projects/projects-query'
|
|
import type { Organization } from 'types'
|
|
|
|
interface NotificationRowProps {
|
|
setRowHeight: (idx: number, height: number) => void
|
|
getProject: (ref: string) => ProjectInfo
|
|
getOrganizationById: (id: number) => Organization
|
|
getOrganizationBySlug: (slug: string) => Organization
|
|
onUpdateNotificationStatus: (id: string, status: 'archived' | 'seen') => void
|
|
queueMarkRead: (id: string) => void
|
|
}
|
|
|
|
const NotificationRow: ItemRenderer<Notification, NotificationRowProps> = ({
|
|
index,
|
|
listRef,
|
|
item: notification,
|
|
setRowHeight,
|
|
getProject,
|
|
getOrganizationById,
|
|
getOrganizationBySlug,
|
|
onUpdateNotificationStatus,
|
|
queueMarkRead,
|
|
}) => {
|
|
const ref = useRef<HTMLDivElement>(null)
|
|
const { ref: viewRef, inView } = useInView()
|
|
const { status, priority } = notification
|
|
|
|
const data = notification.data as NotificationData
|
|
const project = data.project_ref !== undefined ? getProject(data.project_ref) : undefined
|
|
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 (ref.current) {
|
|
listRef?.current?.resetAfterIndex(0)
|
|
setRowHeight(index, ref.current.clientHeight)
|
|
}
|
|
}, [ref])
|
|
|
|
useEffect(() => {
|
|
if (inView && notification.status === 'new') {
|
|
queueMarkRead(notification.id)
|
|
}
|
|
}, [inView])
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
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={<IconExternalLink strokeWidth={1.5} size={14} />}
|
|
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' ? (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger asChild>
|
|
<Button
|
|
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.Trigger>
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
|
'border border-background',
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-foreground">Unarchive</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
) : (
|
|
<Tooltip.Root delayDuration={0}>
|
|
<Tooltip.Trigger asChild>
|
|
<Button
|
|
type="outline"
|
|
icon={<IconArchive 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.Trigger>
|
|
<Tooltip.Portal>
|
|
<Tooltip.Content side="bottom">
|
|
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
|
<div
|
|
className={[
|
|
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
|
'border border-background',
|
|
].join(' ')}
|
|
>
|
|
<span className="text-xs text-foreground">Archive</span>
|
|
</div>
|
|
</Tooltip.Content>
|
|
</Tooltip.Portal>
|
|
</Tooltip.Root>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default NotificationRow
|