mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
Chore/fix layout header alignment right buttons (#21225)
* Fix NotificationV2 popover trigger alignment * Remove unnecessary alt prop on FeedbackDropdown * Deprecate old NotificationsPopover folder
This commit is contained in:
1 parent
700a7a106d
commit
71305867b2
9 files changed
+212
-964
No files matched your search
@@ -1,11 +1,11 @@
|
||||
import { useParams } from 'common'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import { useFlag, useSelectedOrganization, useSelectedProject } from 'hooks'
|
||||
import FeedbackDropdown from '../ProjectLayout/LayoutHeader/FeedbackDropdown'
|
||||
import HelpPopover from '../ProjectLayout/LayoutHeader/HelpPopover'
|
||||
import NotificationsPopover from '../ProjectLayout/LayoutHeader/NotificationsPopover'
|
||||
import NotificationsPopoverV2 from '../ProjectLayout/LayoutHeader/NotificationsPopoverV2/NotificationsPopover'
|
||||
import BranchDropdown from './BranchDropdown'
|
||||
import EnableBranchingButton from './EnableBranchingButton/EnableBranchingButton'
|
||||
import OrganizationDropdown from './OrganizationDropdown'
|
||||
@@ -13,6 +13,9 @@ import ProjectDropdown from './ProjectDropdown'
|
||||
import SettingsButton from './SettingsButton'
|
||||
import UserSettingsDropdown from './UserSettingsDropdown'
|
||||
|
||||
// [Joshen] Just FYI this is only for Nav V2 which is still going through design iteration
|
||||
// Component is not currently in use
|
||||
|
||||
const AppHeader = () => {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
@@ -47,9 +50,9 @@ const AppHeader = () => {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<FeedbackDropdown alt />
|
||||
<NotificationsPopover alt />
|
||||
<HelpPopover alt />
|
||||
<FeedbackDropdown />
|
||||
<NotificationsPopoverV2 />
|
||||
<HelpPopover />
|
||||
<SettingsButton />
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
|
||||
+3
-22
@@ -1,18 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
IconMessageCircle,
|
||||
PopoverContent_Shadcn_,
|
||||
PopoverTrigger_Shadcn_,
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
import { Button, PopoverContent_Shadcn_, PopoverTrigger_Shadcn_, Popover_Shadcn_ } from 'ui'
|
||||
import FeedbackWidget from './FeedbackWidget'
|
||||
|
||||
interface FeedbackDropdownProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => {
|
||||
const FeedbackDropdown = () => {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [feedback, setFeedback] = useState('')
|
||||
const [screenshot, setScreenshot] = useState<string>()
|
||||
@@ -37,16 +27,7 @@ const FeedbackDropdown = ({ alt = false }: FeedbackDropdownProps) => {
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
asChild
|
||||
onClick={onOpen}
|
||||
type={alt ? 'outline' : 'default'}
|
||||
icon={
|
||||
alt ? null : (
|
||||
<IconMessageCircle size={16} strokeWidth={1.5} className="text-foreground-lighter" />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Button asChild onClick={onOpen} type="outline">
|
||||
<span className="hidden md:flex">Feedback</span>
|
||||
</Button>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
Popover_Shadcn_,
|
||||
} from 'ui'
|
||||
|
||||
interface HelpPopoverProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const HelpPopover = ({ alt = false }: HelpPopoverProps) => {
|
||||
const HelpPopover = () => {
|
||||
const router = useRouter()
|
||||
const projectRef = router.query.ref
|
||||
const supportUrl = `/support/new${projectRef ? `?ref=${projectRef}` : ''}`
|
||||
@@ -34,36 +30,28 @@ const HelpPopover = ({ alt = false }: HelpPopoverProps) => {
|
||||
<div className="relative flex items-center">
|
||||
<Button
|
||||
id="help-popover-button"
|
||||
type={alt ? 'text' : 'default'}
|
||||
className={alt ? 'px-1' : ''}
|
||||
type="text"
|
||||
className="px-1"
|
||||
icon={
|
||||
<IconHelpCircle
|
||||
size={alt ? 18 : 16}
|
||||
strokeWidth={1.5}
|
||||
className={alt ? 'text-foreground-light' : 'text-foreground-lighter'}
|
||||
/>
|
||||
<IconHelpCircle size={16} strokeWidth={1.5} className="text-foreground-light" />
|
||||
}
|
||||
>
|
||||
{!alt && <span>Help</span>}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</Tooltip.Trigger>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
{alt ? (
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'space-y-2 border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-foreground">Help</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
) : null}
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content side="bottom">
|
||||
<Tooltip.Arrow className="radix-tooltip-arrow" />
|
||||
<div
|
||||
className={[
|
||||
'rounded bg-alternative py-1 px-2 leading-none shadow',
|
||||
'space-y-2 border border-background',
|
||||
].join(' ')}
|
||||
>
|
||||
<p className="text-xs text-foreground">Help</p>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
<PopoverContent_Shadcn_ className="w-[400px] space-y-4 p-0 py-5" align="end" side="bottom">
|
||||
<div className="mb-5 space-y-4 px-5">
|
||||
|
||||
@@ -15,7 +15,6 @@ import { IS_PLATFORM } from 'lib/constants'
|
||||
import BreadcrumbsView from './BreadcrumbsView'
|
||||
import FeedbackDropdown from './FeedbackDropdown'
|
||||
import HelpPopover from './HelpPopover'
|
||||
import NotificationsPopover from './NotificationsPopover'
|
||||
import NotificationsPopoverV2 from './NotificationsPopoverV2/NotificationsPopover'
|
||||
|
||||
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
|
||||
@@ -24,7 +23,6 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
const selectedOrganization = useSelectedOrganization()
|
||||
|
||||
const enableBranchManagement = useFlag('branchManagement')
|
||||
const notificationsV2 = useFlag('notificationsV2')
|
||||
|
||||
const isBranchingEnabled =
|
||||
selectedProject?.is_branch_enabled === true || selectedProject?.parent_project_ref !== undefined
|
||||
@@ -117,19 +115,13 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
|
||||
{/* Additional breadcrumbs are supplied */}
|
||||
<BreadcrumbsView defaultValue={breadcrumbs} />
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center gap-x-2">
|
||||
{customHeaderComponents && customHeaderComponents}
|
||||
{IS_PLATFORM && <FeedbackDropdown alt={notificationsV2} />}
|
||||
{IS_PLATFORM && !notificationsV2 && (
|
||||
<>
|
||||
<HelpPopover />
|
||||
<NotificationsPopover />
|
||||
</>
|
||||
)}
|
||||
{IS_PLATFORM && notificationsV2 && (
|
||||
{IS_PLATFORM && (
|
||||
<>
|
||||
<FeedbackDropdown />
|
||||
<NotificationsPopoverV2 />
|
||||
<HelpPopover alt />
|
||||
<HelpPopover />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
import { Action, ActionReason, ActionType } from '@supabase/shared-types/out/notifications'
|
||||
import Link from 'next/link'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Fragment } from 'react'
|
||||
import { Button, IconExternalLink } from 'ui'
|
||||
|
||||
import { Organization } from 'types'
|
||||
|
||||
interface NotificationActionsProps {
|
||||
organization: Organization
|
||||
changelogLink?: string
|
||||
availableActions: Action[]
|
||||
onSelectRestartProject: () => void
|
||||
onSelectApplyMigration: () => void
|
||||
onSelectRollbackMigration: () => void
|
||||
}
|
||||
|
||||
const NotificationActions = ({
|
||||
organization,
|
||||
changelogLink,
|
||||
availableActions,
|
||||
onSelectRestartProject,
|
||||
onSelectApplyMigration,
|
||||
onSelectRollbackMigration,
|
||||
}: NotificationActionsProps) => {
|
||||
const router = useRouter()
|
||||
|
||||
const onSelectUpgradePlan = () => {
|
||||
return router.push(`/org/${organization.slug}/billing?panel=subscriptionPlan`)
|
||||
}
|
||||
|
||||
const renderActionButton = (action: Action) => {
|
||||
switch (action.action_type) {
|
||||
case ActionType.UpgradeProjectToPro:
|
||||
return (
|
||||
<Button type="default" onClick={onSelectUpgradePlan}>
|
||||
Upgrade plan
|
||||
</Button>
|
||||
)
|
||||
case ActionType.SchedulePostgresRestart:
|
||||
return (
|
||||
<Button type="default" onClick={onSelectRestartProject}>
|
||||
Restart project
|
||||
</Button>
|
||||
)
|
||||
case ActionType.MigratePostgresSchema:
|
||||
if (action.reason === ActionReason.Rollback) {
|
||||
return (
|
||||
<Button type="default" onClick={onSelectRollbackMigration}>
|
||||
Rollback
|
||||
</Button>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Button type="default" onClick={onSelectApplyMigration}>
|
||||
Apply now
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-2">
|
||||
{availableActions.map((action) => {
|
||||
return (
|
||||
<Fragment key={`${action.action_type}_${action.reason}`}>
|
||||
{renderActionButton(action)}
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
{changelogLink && (
|
||||
<Button asChild type="default" icon={<IconExternalLink size={12} strokeWidth={2} />}>
|
||||
<Link href={changelogLink} target="_blank" rel="noreferrer">
|
||||
More info
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationActions
|
||||
-104
@@ -1,104 +0,0 @@
|
||||
import { Notification, NotificationStatus } from '@supabase/shared-types/out/notifications'
|
||||
import dayjs from 'dayjs'
|
||||
import { Button, IconX } from 'ui'
|
||||
|
||||
import { useNotificationsDismissMutation } from 'data/notifications/notifications-dismiss-mutation'
|
||||
import { useOrganizationsQuery } from 'data/organizations/organizations-query'
|
||||
import { ProjectInfo, useProjectsQuery } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
import NotificationActions from './NotificationActions'
|
||||
import { formatNotificationCTAText, formatNotificationText } from './NotificationRows.utils'
|
||||
|
||||
export interface NotificationRowProps {
|
||||
notification: Notification
|
||||
onSelectRestartProject: (project: ProjectInfo, notification: Notification) => void
|
||||
onSelectApplyMigration: (project: ProjectInfo, notification: Notification) => void
|
||||
onSelectRollbackMigration: (project: ProjectInfo, notification: Notification) => void
|
||||
}
|
||||
|
||||
const NotificationRow = ({
|
||||
notification,
|
||||
onSelectRestartProject,
|
||||
onSelectApplyMigration,
|
||||
onSelectRollbackMigration,
|
||||
}: NotificationRowProps) => {
|
||||
const { ui } = useStore()
|
||||
const { data: projects } = useProjectsQuery()
|
||||
const { data: organizations } = useOrganizationsQuery()
|
||||
const project = projects?.find((project) => project.id === notification.project_id)
|
||||
const organization = organizations?.find((org) => org.id === project?.organization_id)
|
||||
|
||||
const insertedAt = dayjs(notification.inserted_at).format('DD MMM YYYY, HH:mma')
|
||||
const changelogLink = (notification.data as any).changelog_link
|
||||
const availableActions = notification.meta?.actions_available ?? []
|
||||
|
||||
const { mutate: dismissNotifications, isLoading: isDismissing } = useNotificationsDismissMutation(
|
||||
{
|
||||
onError: (error) => {
|
||||
ui.setNotification({
|
||||
error,
|
||||
category: 'error',
|
||||
message: `Failed to dismiss notification: ${error.message}`,
|
||||
duration: 4000,
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const dismissNotification = async (notificationId: string) => {
|
||||
if (!notificationId) return
|
||||
dismissNotifications({ ids: [notificationId] })
|
||||
}
|
||||
|
||||
if (!project || !organization) return null
|
||||
|
||||
return (
|
||||
<div className="flex py-2">
|
||||
<div className="flex min-w-[50px] justify-center">
|
||||
{notification.notification_status !== NotificationStatus.Seen && (
|
||||
<div className="mt-1.5 h-2 w-2 rounded-full bg-green-900" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-grow mr-8 flex flex-col gap-4">
|
||||
<div className="w-full flex justify-between">
|
||||
<div className="w-9/10 space-y-2">
|
||||
{formatNotificationText(project.name, notification)}
|
||||
{formatNotificationCTAText(availableActions)}
|
||||
<p className="text-foreground-light text-sm !mt-2">{insertedAt}</p>
|
||||
</div>
|
||||
<div className="w-1/10 flex justify-end">
|
||||
<div>
|
||||
<Button
|
||||
className="!px-1 group"
|
||||
type="text"
|
||||
loading={isDismissing}
|
||||
icon={
|
||||
<IconX
|
||||
size={14}
|
||||
strokeWidth={2}
|
||||
className="text-foreground-light group-hover:text-foreground transition"
|
||||
/>
|
||||
}
|
||||
onClick={() => dismissNotification(notification.id)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{(availableActions.length > 0 || changelogLink !== undefined) && (
|
||||
<div className="flex items-center">
|
||||
<NotificationActions
|
||||
organization={organization}
|
||||
changelogLink={changelogLink}
|
||||
availableActions={availableActions}
|
||||
onSelectRestartProject={() => onSelectRestartProject(project, notification)}
|
||||
onSelectApplyMigration={() => onSelectApplyMigration(project, notification)}
|
||||
onSelectRollbackMigration={() => onSelectRollbackMigration(project, notification)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationRow
|
||||
-178
@@ -1,178 +0,0 @@
|
||||
import {
|
||||
Action,
|
||||
ActionType,
|
||||
ExtensionsUpgrade,
|
||||
Notification,
|
||||
NotificationName,
|
||||
ServerUpgrade,
|
||||
ServiceUpgrade,
|
||||
ViolatedLimit,
|
||||
} from '@supabase/shared-types/out/notifications'
|
||||
import dayjs from 'dayjs'
|
||||
import Link from 'next/link'
|
||||
import { Button, IconArrowRight, IconExternalLink } from 'ui'
|
||||
|
||||
import { Markdown } from 'components/interfaces/Markdown'
|
||||
|
||||
export const formatNotificationText = (projectName: string, notification: Notification) => {
|
||||
if (notification.data.name === NotificationName.ProjectExceedingTierLimit) {
|
||||
const { violations } = notification.data
|
||||
const violationsText = violations
|
||||
.map((violation: ViolatedLimit) => violation.dimension as string)
|
||||
.reduce((a: string, b: string) => `${a}, ${b}`)
|
||||
|
||||
return (
|
||||
<p className="text-sm">
|
||||
Your project "{projectName}" has exceeded its limits in the following areas:{' '}
|
||||
{violationsText}.
|
||||
</p>
|
||||
)
|
||||
} else if (notification.data.name === NotificationName.PostgresqlUpgradeAvailable) {
|
||||
const { upgrade_type, additional } = notification.data
|
||||
|
||||
if (upgrade_type === 'postgresql-server') {
|
||||
const { version_to } = additional as ServerUpgrade
|
||||
return (
|
||||
<p className="text-sm">
|
||||
New version of Postgres ({version_to}) is now available for project "{projectName}".
|
||||
</p>
|
||||
)
|
||||
} else if (upgrade_type === 'extensions') {
|
||||
const { name, version_to } = additional as ExtensionsUpgrade
|
||||
return (
|
||||
<p className="text-sm">
|
||||
New version of "{name}" ({version_to}) is now available for project "{projectName}".
|
||||
</p>
|
||||
)
|
||||
} else if (upgrade_type === 'schema-migration') {
|
||||
const { name, version_to } = additional as ExtensionsUpgrade
|
||||
return (
|
||||
<div className="text-sm space-y-1">
|
||||
<p>A new schema migration is available for your project "{projectName}".</p>
|
||||
<ol className="list-disc pl-6">
|
||||
<li>
|
||||
<div className="flex items-center space-x-1">
|
||||
<p>{name}</p>
|
||||
<IconArrowRight size={12} strokeWidth={2} />
|
||||
<p>{version_to}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
return ''
|
||||
} else if (notification.data.name === NotificationName.PostgresqlUpgradeCompleted) {
|
||||
const { upgrade_type, additional } = notification.data
|
||||
|
||||
if (upgrade_type === 'postgresql-server') {
|
||||
const { version_to } = additional as ServerUpgrade
|
||||
return (
|
||||
<p className="text-sm">
|
||||
Postgres ({version_to}) has been successfully updated to {version_to} for project "
|
||||
{projectName}".
|
||||
</p>
|
||||
)
|
||||
} else if (upgrade_type === 'extensions') {
|
||||
const { name, version_to } = additional as ExtensionsUpgrade
|
||||
return (
|
||||
<p className="text-sm">
|
||||
The extension "{name}" has been successfully updated to {version_to} for project "
|
||||
{projectName}".
|
||||
</p>
|
||||
)
|
||||
} else if (upgrade_type === 'schema-migration') {
|
||||
const { version_to } = additional
|
||||
return (
|
||||
<p className="text-sm">
|
||||
The schema migration "{version_to}" has been successfully applied for project "
|
||||
{projectName}".
|
||||
</p>
|
||||
)
|
||||
}
|
||||
} else if (notification.data.name === NotificationName.ProjectUpdateCompleted) {
|
||||
const { upgrades } = notification.data
|
||||
return (
|
||||
<div>
|
||||
<p className="text-sm">
|
||||
The following services have been successfully updated for project "{projectName}":
|
||||
</p>
|
||||
<ol className="list-disc pl-6">
|
||||
{upgrades.map((upgrade: ServiceUpgrade) => (
|
||||
<li key={upgrade.name}>
|
||||
<div className="flex items-center space-x-1">
|
||||
<p className="text-sm">{upgrade.name}</p>
|
||||
<IconArrowRight size={12} strokeWidth={2} />
|
||||
<p className="text-sm">{upgrade.version_to}</p>
|
||||
{upgrade.changelog_link && (
|
||||
<div className="!ml-4">
|
||||
<Link href={upgrade.changelog_link} target="_blank" rel="noreferrer">
|
||||
<IconExternalLink
|
||||
className="cursor-pointer text-foreground-light hover:text-foreground transition"
|
||||
size={12}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</div>
|
||||
)
|
||||
} else if (notification.data.name === NotificationName.ProjectInformational) {
|
||||
const buttons = notification.data.linked_buttons ?? []
|
||||
return (
|
||||
<>
|
||||
<Markdown content={notification.data.message} className="text-foreground" />
|
||||
{buttons.map((button, index) => {
|
||||
return (
|
||||
<a href={button.url} key={index} target="_blank">
|
||||
<Button
|
||||
asChild
|
||||
type="default"
|
||||
className="mr-2 mt-2 mb-2"
|
||||
icon={<IconExternalLink size={12} strokeWidth={2} />}
|
||||
>
|
||||
<span>{button.text}</span>
|
||||
</Button>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<p className="text-sm">
|
||||
Unknown notification type: {notification.notification_name} - Please reach out to support
|
||||
for more information.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export const formatNotificationCTAText = (availableActions: Action[]) => {
|
||||
const [action] = availableActions
|
||||
if (!action) return <p className="text-sm"></p>
|
||||
|
||||
switch (action.action_type) {
|
||||
case ActionType.SchedulePostgresRestart:
|
||||
return <p className="text-sm">Restart your project to get the latest updates.</p>
|
||||
case ActionType.UpgradeProjectToPro:
|
||||
return <p className="text-sm">Upgrade your project to ensure continued availability.</p>
|
||||
case ActionType.PgBouncerRestart:
|
||||
return <p className="text-sm">Restart your connection pooler to get the latest updates.</p>
|
||||
case ActionType.MigratePostgresSchema:
|
||||
if (action.deadline) {
|
||||
return (
|
||||
<p className="text-sm space-x-1">
|
||||
This patch will be automatically applied after{' '}
|
||||
{dayjs(new Date(action.deadline)).format('DD MMM YYYY, HH:mma')}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
-347
@@ -1,347 +0,0 @@
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import {
|
||||
ActionType,
|
||||
Notification,
|
||||
NotificationStatus,
|
||||
PostgresqlUpgradeData,
|
||||
} from '@supabase/shared-types/out/notifications'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import dayjs from 'dayjs'
|
||||
import { useRouter } from 'next/router'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Button, IconArrowRight, IconBell, IconInbox, Popover } from 'ui'
|
||||
|
||||
import ConfirmModal from 'components/ui/Dialogs/ConfirmDialog'
|
||||
import { useNotificationsQuery } from 'data/notifications/notifications-query'
|
||||
import { useNotificationsUpdateMutation } from 'data/notifications/notifications-update-mutation'
|
||||
import { getProjectDetail } from 'data/projects/project-detail-query'
|
||||
import { useProjectRestartServicesMutation } from 'data/projects/project-restart-services-mutation'
|
||||
import { ProjectInfo, setProjectPostgrestStatus } from 'data/projects/projects-query'
|
||||
import { useStore } from 'hooks'
|
||||
import { delete_, post } from 'lib/common/fetch'
|
||||
import { API_URL } from 'lib/constants'
|
||||
import NotificationRow from './NotificationRow'
|
||||
|
||||
interface NotificationsPopoverProps {
|
||||
alt?: boolean
|
||||
}
|
||||
|
||||
const NotificationsPopover = ({ alt = false }: NotificationsPopoverProps) => {
|
||||
const router = useRouter()
|
||||
const { meta, ui } = useStore()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: notifications } = useNotificationsQuery()
|
||||
const { mutate: updateNotifications } = useNotificationsUpdateMutation({
|
||||
onError: () => console.error('Failed to update notifications'),
|
||||
})
|
||||
|
||||
const [projectToRestart, setProjectToRestart] = useState<ProjectInfo>()
|
||||
const [projectToApplyMigration, setProjectToApplyMigration] = useState<ProjectInfo>()
|
||||
const [projectToRollbackMigration, setProjectToRollbackMigration] = useState<ProjectInfo>()
|
||||
const [targetNotification, setTargetNotification] = useState<Notification>()
|
||||
|
||||
const { mutate: restartProjectServices } = useProjectRestartServicesMutation({
|
||||
onSuccess: (res, variables) => {
|
||||
setProjectToRestart(undefined)
|
||||
setTargetNotification(undefined)
|
||||
|
||||
setProjectPostgrestStatus(queryClient, variables.ref, 'OFFLINE')
|
||||
ui.setNotification({ category: 'success', message: `Restarting services` })
|
||||
router.push(`/project/${variables.ref}`)
|
||||
},
|
||||
})
|
||||
|
||||
const newNotifications =
|
||||
notifications?.filter(
|
||||
(notification) => notification.notification_status === NotificationStatus.New
|
||||
) ?? []
|
||||
const hasNewNotifications = newNotifications.length > 0
|
||||
|
||||
const onOpenChange = async (open: boolean) => {
|
||||
// Mark notifications as seen
|
||||
if (!open) {
|
||||
const notificationIds =
|
||||
notifications
|
||||
?.filter((notification) => notification.notification_status === NotificationStatus.New)
|
||||
.map((notification) => notification.id) ?? []
|
||||
if (notificationIds.length > 0) updateNotifications({ ids: notificationIds })
|
||||
}
|
||||
}
|
||||
|
||||
const onConfirmProjectRestart = async () => {
|
||||
if (!projectToRestart || !targetNotification) return
|
||||
|
||||
const { id } = targetNotification
|
||||
const { ref, region } = projectToRestart
|
||||
if (!ref || !region) return console.error('Ref and region required')
|
||||
|
||||
const serviceNamesByActionName: Record<string, string> = {
|
||||
[ActionType.PgBouncerRestart]: 'pgbouncer',
|
||||
[ActionType.SchedulePostgresRestart]: 'postgresql',
|
||||
[ActionType.MigratePostgresSchema]: 'postgresql',
|
||||
}
|
||||
const services: string[] = targetNotification.meta.actions_available
|
||||
.map((action) => action.action_type)
|
||||
.filter((actionName) => Object.keys(serviceNamesByActionName).indexOf(actionName) !== -1)
|
||||
.map((actionName) => serviceNamesByActionName[actionName])
|
||||
|
||||
restartProjectServices({ ref, region, services: services as any, source_notification_id: id })
|
||||
}
|
||||
|
||||
const onConfirmProjectApplyMigration = async () => {
|
||||
if (!projectToApplyMigration) return
|
||||
|
||||
const data = targetNotification?.data as PostgresqlUpgradeData
|
||||
const { resource } = data.additional as any
|
||||
if (!resource) return
|
||||
|
||||
// [Joshen] Leaving this as an exception for RQ due to dynamic URL
|
||||
const res = await post(`${API_URL}/database/${projectToApplyMigration.ref}/${resource}`, {})
|
||||
if (!res.error) {
|
||||
const project = await getProjectDetail({ ref: projectToApplyMigration.ref })
|
||||
if (project) meta.setProjectDetails(project)
|
||||
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully applied migration for project "${projectToApplyMigration.name}"`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({
|
||||
error: res.error,
|
||||
category: 'error',
|
||||
message: `Failed to apply migration: ${res.error.message}`,
|
||||
})
|
||||
}
|
||||
setProjectToApplyMigration(undefined)
|
||||
}
|
||||
|
||||
const onConfirmProjectRollbackMigration = async () => {
|
||||
if (!projectToRollbackMigration) return
|
||||
|
||||
const data = targetNotification?.data as PostgresqlUpgradeData
|
||||
const { resource } = data.additional as any
|
||||
if (!resource) return
|
||||
|
||||
// [Joshen] Leaving this as an exception for RQ due to dynamic URL
|
||||
const res = await delete_(
|
||||
`${API_URL}/database/${projectToRollbackMigration.ref}/${resource}`,
|
||||
{}
|
||||
)
|
||||
if (!res.error) {
|
||||
const project = await getProjectDetail({ ref: projectToRollbackMigration.ref })
|
||||
if (project) meta.setProjectDetails(project)
|
||||
|
||||
ui.setNotification({
|
||||
category: 'success',
|
||||
message: `Successfully rolled back migration for project "${projectToRollbackMigration.name}"`,
|
||||
})
|
||||
} else {
|
||||
ui.setNotification({
|
||||
error: res.error,
|
||||
category: 'error',
|
||||
message: `Failed to roll back migration: ${res.error.message}`,
|
||||
})
|
||||
}
|
||||
setProjectToRollbackMigration(undefined)
|
||||
}
|
||||
|
||||
if (!notifications || !Array.isArray(notifications)) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
size="content"
|
||||
align="end"
|
||||
side="bottom"
|
||||
sideOffset={8}
|
||||
onOpenChange={onOpenChange}
|
||||
overlay={
|
||||
<div className="w-[400px] lg:w-[700px]">
|
||||
<div className="flex items-center justify-between border-b border-default bg-surface-200 px-4 py-2">
|
||||
<p className="text-sm">Notifications</p>
|
||||
{/* Area for improvement: Paginate notifications and show in a side panel */}
|
||||
{/* <p className="text-foreground-light hover:text-foreground cursor-pointer text-sm transition">
|
||||
See all{' '}
|
||||
{notifications.length > MAX_NOTIFICATIONS_TO_SHOW && `(${notifications.length})`}
|
||||
</p> */}
|
||||
</div>
|
||||
<div className="max-h-[380px] overflow-y-auto py-2">
|
||||
{notifications.length === 0 ? (
|
||||
<div className="py-2 px-4">
|
||||
<p className="text-sm text-foreground-light">No notifications available</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{notifications.map((notification, i: number) => (
|
||||
<Fragment key={notification.id}>
|
||||
<NotificationRow
|
||||
notification={notification}
|
||||
onSelectRestartProject={(project, notification) => {
|
||||
setProjectToRestart(project)
|
||||
setTargetNotification(notification)
|
||||
}}
|
||||
onSelectApplyMigration={(project, notification) => {
|
||||
setProjectToApplyMigration(project)
|
||||
setTargetNotification(notification)
|
||||
}}
|
||||
onSelectRollbackMigration={(project, notification) => {
|
||||
setProjectToRollbackMigration(project)
|
||||
setTargetNotification(notification)
|
||||
}}
|
||||
/>
|
||||
{i !== notifications.length - 1 && <Popover.Separator />}
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Tooltip.Root delayDuration={0}>
|
||||
<Tooltip.Trigger asChild>
|
||||
<div className="relative flex items-center">
|
||||
{hasNewNotifications && (
|
||||
<>
|
||||
{alt ? null : (
|
||||
<div className="absolute -top-1 -right-1 z-50 flex h-3 w-3 items-center justify-center">
|
||||
<div className="h-full w-full animate-ping rounded-full bg-green-800 opacity-60"></div>
|
||||
<div className="z-60 absolute top-0 right-0 h-full w-full rounded-full bg-green-900 opacity-80"></div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Button
|
||||
asChild
|
||||
id="notification-button"
|
||||
type={alt ? 'text' : 'default'}
|
||||
className={alt ? 'px-1' : ''}
|
||||
icon={
|
||||
hasNewNotifications ? (
|
||||
<div className="-mr-3.5 z-10 h-4 w-4 flex items-center justify-center rounded-full bg-foreground">
|
||||
<p className="text-xs text-background-alternative">
|
||||
{newNotifications.length}
|
||||
</p>
|
||||
</div>
|
||||
) : alt ? (
|
||||
<IconInbox size={18} strokeWidth={1.5} className="text-foreground-light" />
|
||||
) : (
|
||||
<IconBell size={16} strokeWidth={1.5} className="text-foreground" />
|
||||
)
|
||||
}
|
||||
iconRight={
|
||||
hasNewNotifications ? (
|
||||
<>
|
||||
{alt ? (
|
||||
<IconInbox size={18} strokeWidth={1.5} className="text-foreground-light" />
|
||||
) : (
|
||||
<IconBell size={16} strokeWidth={1.5} className="text-foreground" />
|
||||
)}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<span></span>
|
||||
</Button>
|
||||
</div>
|
||||
</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 flex items-center space-x-1',
|
||||
].join(' ')}
|
||||
>
|
||||
<span className="text-xs text-foreground">Notifications</span>
|
||||
</div>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Popover>
|
||||
|
||||
<ConfirmModal
|
||||
danger
|
||||
visible={projectToRestart !== undefined}
|
||||
title={`Restart project "${projectToRestart?.name}"`}
|
||||
description={`Are you sure you want to restart the project? There will be a few minutes of downtime.`}
|
||||
buttonLabel="Restart"
|
||||
buttonLoadingLabel="Restarting"
|
||||
onSelectCancel={() => setProjectToRestart(undefined)}
|
||||
onSelectConfirm={onConfirmProjectRestart}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
size="large"
|
||||
visible={projectToApplyMigration !== undefined}
|
||||
title={`Apply schema migration for "${projectToApplyMigration?.name}"`}
|
||||
// @ts-ignore
|
||||
description={
|
||||
<div className="text-foreground space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p>The following schema migration will be applied to the project</p>
|
||||
<ol className="list-disc pl-6">
|
||||
<li>
|
||||
<div className="flex items-center space-x-1">
|
||||
<p>{(targetNotification?.data as any)?.additional?.name}</p>
|
||||
<IconArrowRight size={12} strokeWidth={2} />
|
||||
<p>{(targetNotification?.data as any)?.additional?.version_to}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
This change can be rolled back anytime up till{' '}
|
||||
{dayjs(
|
||||
new Date(targetNotification?.meta.actions_available?.[0]?.deadline ?? 0)
|
||||
).format('DD MMM YYYY, HH:mma ZZ')}
|
||||
, after which the changes will be finalized and can no longer be undone.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonLabel="Confirm"
|
||||
buttonLoadingLabel="Confirm"
|
||||
onSelectCancel={() => setProjectToApplyMigration(undefined)}
|
||||
onSelectConfirm={onConfirmProjectApplyMigration}
|
||||
/>
|
||||
|
||||
<ConfirmModal
|
||||
size="medium"
|
||||
visible={projectToRollbackMigration !== undefined}
|
||||
title={`Rollback schema migration for "${projectToRollbackMigration?.name}"`}
|
||||
// @ts-ignore
|
||||
description={
|
||||
<div className="text-foreground space-y-2">
|
||||
<div className="space-y-1">
|
||||
<p>The following schema migration will be rolled back for the project</p>
|
||||
<ol className="list-disc pl-6">
|
||||
<li>
|
||||
<div className="flex items-center space-x-1">
|
||||
<p>{(targetNotification?.data as any)?.additional?.name}</p>
|
||||
<IconArrowRight size={12} strokeWidth={2} />
|
||||
<p>{(targetNotification?.data as any)?.additional?.version_to}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<p>
|
||||
This migration however will still be applied and finalized after{' '}
|
||||
{dayjs(
|
||||
new Date(targetNotification?.meta.actions_available?.[0]?.deadline ?? 0)
|
||||
).format('DD MMM YYYY, HH:mma ZZ')}
|
||||
, after which the changes can no longer be undone.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
buttonLabel="Confirm"
|
||||
buttonLoadingLabel="Confirm"
|
||||
onSelectCancel={() => setProjectToRollbackMigration(undefined)}
|
||||
onSelectConfirm={onConfirmProjectRollbackMigration}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationsPopover
|
||||
+179
-183
@@ -65,7 +65,7 @@ const NotificationsPopoverV2 = () => {
|
||||
projects: snap.filterProjects,
|
||||
},
|
||||
})
|
||||
const { data: summary, isSuccess: isSuccessSummary } = useNotificationsSummaryQuery()
|
||||
const { data: summary } = useNotificationsSummaryQuery()
|
||||
const { mutate: updateNotifications } = useNotificationsV2UpdateMutation()
|
||||
const { mutate: archiveAllNotifications, isLoading: isArchiving } =
|
||||
useNotificationsArchiveAllMutation({
|
||||
@@ -84,192 +84,188 @@ const NotificationsPopoverV2 = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* wrapped in a div to stop space-x class impacting popover content */}
|
||||
<Popover_Shadcn_
|
||||
modal={false}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open)
|
||||
if (!open) markNotificationsRead()
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type={hasNewNotifications ? 'outline' : 'text'}
|
||||
className={clsx(
|
||||
'h-[26px]',
|
||||
// !hasCritical || !hasWarning || !hasNewNotifications ? 'w-[26px]' : '',
|
||||
'group',
|
||||
hasNewNotifications ? 'rounded-full px-1.5' : 'px-1',
|
||||
hasCritical
|
||||
? 'border-destructive-500 hover:border-destructive-600 hover:bg-destructive-300'
|
||||
: hasWarning
|
||||
? 'border-warning-500 hover:border-warning-600 hover:bg-warning-300'
|
||||
: ''
|
||||
)}
|
||||
icon={
|
||||
hasCritical ? (
|
||||
<CriticalIcon className="!w-3.5 !h-3.5 transition-all -mr-3.5 group-hover:-mr-1 z-10" />
|
||||
) : hasWarning ? (
|
||||
<WarningIcon className="!w-3.5 !h-3.5 transition-all -mr-3.5 group-hover:-mr-1 z-10" />
|
||||
) : hasNewNotifications ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'transition-all -mr-3 group-hover:-mr-1',
|
||||
'z-10 h-4 flex items-center justify-center rounded-full bg-black dark:bg-white',
|
||||
(summary?.unread_count ?? 0) > 9 ? 'px-0.5 w-auto' : 'w-4'
|
||||
)}
|
||||
>
|
||||
<p className="text-xs text-background-alternative">{summary?.unread_count}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
iconRight={
|
||||
<IconInbox
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className="transition group-hover:text-foreground text-foreground-light"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="p-0 w-[450px] overflow-hidden" side="bottom" align="end">
|
||||
<div className="px-4">
|
||||
<p className="pt-4 pb-1 text-sm">Notifications</p>
|
||||
<div className="flex items-center">
|
||||
<Tabs_Shadcn_
|
||||
className="w-full"
|
||||
onValueChange={(tab: string) => {
|
||||
setActiveTab(tab as 'inbox' | 'archived')
|
||||
if (tab === 'archived' && snap.filterStatuses.includes('unread')) {
|
||||
snap.setFilters('unread', 'status')
|
||||
}
|
||||
}}
|
||||
value={activeTab}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TabsList_Shadcn_ className="flex gap-5 grow border-none">
|
||||
<TabsTrigger_Shadcn_
|
||||
id="inbox"
|
||||
value="inbox"
|
||||
className="px-0 data-[state=active]:bg-transparent flex gap-2"
|
||||
>
|
||||
Inbox
|
||||
<div
|
||||
className={clsx([
|
||||
'flex items-center justify-center text-xs rounded-full bg-surface-300 h-4',
|
||||
(summary?.unread_count ?? 0) > 9 ? 'px-0.5 w-auto' : 'w-4',
|
||||
])}
|
||||
>
|
||||
{summary?.unread_count}
|
||||
</div>
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_
|
||||
id="archived"
|
||||
value="archived"
|
||||
className="px-0 data-[state=active]:bg-transparent"
|
||||
>
|
||||
Archived
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<NotificationsFilter activeTab={activeTab} />
|
||||
</div>
|
||||
</Tabs_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t">
|
||||
{isLoading && (
|
||||
<div className="p-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)}
|
||||
{isError && (
|
||||
<div className="p-4">
|
||||
<AlertError subject="Failed to retrieve notifications" error={error} />
|
||||
</div>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<div className="flex flex-1 h-[400px] bg-background">
|
||||
{notifications.length > 0 &&
|
||||
!(activeTab === 'archived' && snap.filterStatuses.includes('unread')) ? (
|
||||
<InfiniteList
|
||||
items={notifications}
|
||||
ItemComponent={NotificationRow}
|
||||
LoaderComponent={
|
||||
<div className="p-4">
|
||||
<ShimmeringLoader />
|
||||
</div>
|
||||
}
|
||||
itemProps={{
|
||||
setRowHeight: (idx: number, height: number) => {
|
||||
if (rowHeights.current) {
|
||||
rowHeights.current = { ...rowHeights.current, [idx]: height }
|
||||
}
|
||||
},
|
||||
getProject: (ref: string) =>
|
||||
projects?.find((project) => project.ref === ref)!,
|
||||
getOrganization: (id: number) => organizations?.find((org) => org.id === id)!,
|
||||
onUpdateNotificationStatus: (id: string, status: 'archived' | 'seen') => {
|
||||
updateNotifications({ ids: [id], status })
|
||||
},
|
||||
queueMarkRead: (id: string) => {
|
||||
if (markedRead.current && !markedRead.current.includes(id)) {
|
||||
markedRead.current.push(id)
|
||||
}
|
||||
},
|
||||
}}
|
||||
getItemSize={(idx: number) => rowHeights?.current?.[idx] ?? 56}
|
||||
hasNextPage={hasNextPage}
|
||||
isLoadingNextPage={isFetchingNextPage}
|
||||
onLoadNextPage={() => fetchNextPage()}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-4 items-center flex-grow justify-center">
|
||||
<IconInbox size={32} className="text-foreground-light" />
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className="text-foreground-light text-sm mx-auto text-center">
|
||||
{activeTab === 'archived'
|
||||
? `No archived notifications${
|
||||
snap.numFiltersApplied > 0
|
||||
? ` based on the ${snap.numFiltersApplied} filter${
|
||||
snap.numFiltersApplied > 1 ? 's' : ''
|
||||
} applied`
|
||||
: ''
|
||||
}`
|
||||
: snap.numFiltersApplied > 0
|
||||
? `No notifications based on the ${snap.numFiltersApplied} filter${
|
||||
snap.numFiltersApplied > 1 ? 's' : ''
|
||||
} applied`
|
||||
: 'All caught up'}
|
||||
</p>
|
||||
<p className="text-foreground-lighter text-xs w-60 mx-auto text-center">
|
||||
{activeTab === 'archived'
|
||||
? 'Notifications that you have previously archived will be shown here'
|
||||
: 'You will be notified here for any notices on your organizations and projects'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Popover_Shadcn_
|
||||
modal={false}
|
||||
open={open}
|
||||
onOpenChange={(open) => {
|
||||
setOpen(open)
|
||||
if (!open) markNotificationsRead()
|
||||
}}
|
||||
>
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<Button
|
||||
type={hasNewNotifications ? 'outline' : 'text'}
|
||||
className={clsx(
|
||||
'h-[26px]',
|
||||
// !hasCritical || !hasWarning || !hasNewNotifications ? 'w-[26px]' : '',
|
||||
'group',
|
||||
hasNewNotifications ? 'rounded-full px-1.5' : 'px-1',
|
||||
hasCritical
|
||||
? 'border-destructive-500 hover:border-destructive-600 hover:bg-destructive-300'
|
||||
: hasWarning
|
||||
? 'border-warning-500 hover:border-warning-600 hover:bg-warning-300'
|
||||
: ''
|
||||
)}
|
||||
icon={
|
||||
hasCritical ? (
|
||||
<CriticalIcon className="!w-3.5 !h-3.5 transition-all -mr-3.5 group-hover:-mr-1 z-10" />
|
||||
) : hasWarning ? (
|
||||
<WarningIcon className="!w-3.5 !h-3.5 transition-all -mr-3.5 group-hover:-mr-1 z-10" />
|
||||
) : hasNewNotifications ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'transition-all -mr-3 group-hover:-mr-1',
|
||||
'z-10 h-4 flex items-center justify-center rounded-full bg-black dark:bg-white',
|
||||
(summary?.unread_count ?? 0) > 9 ? 'px-0.5 w-auto' : 'w-4'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{notifications.length > 0 && activeTab === 'inbox' && (
|
||||
<div className="flex items-center justify-center p-1.5 border-t">
|
||||
<Button
|
||||
disabled={isArchiving}
|
||||
loading={isArchiving}
|
||||
type="text"
|
||||
icon={<IconArchive />}
|
||||
onClick={() => archiveAllNotifications()}
|
||||
>
|
||||
Archive all
|
||||
</Button>
|
||||
<p className="text-xs text-background-alternative">{summary?.unread_count}</p>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
iconRight={
|
||||
<IconInbox
|
||||
size={18}
|
||||
strokeWidth={1.5}
|
||||
className="transition group-hover:text-foreground text-foreground-light"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="p-0 w-[450px] overflow-hidden" side="bottom" align="end">
|
||||
<div className="px-4">
|
||||
<p className="pt-4 pb-1 text-sm">Notifications</p>
|
||||
<div className="flex items-center">
|
||||
<Tabs_Shadcn_
|
||||
className="w-full"
|
||||
onValueChange={(tab: string) => {
|
||||
setActiveTab(tab as 'inbox' | 'archived')
|
||||
if (tab === 'archived' && snap.filterStatuses.includes('unread')) {
|
||||
snap.setFilters('unread', 'status')
|
||||
}
|
||||
}}
|
||||
value={activeTab}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<TabsList_Shadcn_ className="flex gap-5 grow border-none">
|
||||
<TabsTrigger_Shadcn_
|
||||
id="inbox"
|
||||
value="inbox"
|
||||
className="px-0 data-[state=active]:bg-transparent flex gap-2"
|
||||
>
|
||||
Inbox
|
||||
<div
|
||||
className={clsx([
|
||||
'flex items-center justify-center text-xs rounded-full bg-surface-300 h-4',
|
||||
(summary?.unread_count ?? 0) > 9 ? 'px-0.5 w-auto' : 'w-4',
|
||||
])}
|
||||
>
|
||||
{summary?.unread_count}
|
||||
</div>
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_
|
||||
id="archived"
|
||||
value="archived"
|
||||
className="px-0 data-[state=active]:bg-transparent"
|
||||
>
|
||||
Archived
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<NotificationsFilter activeTab={activeTab} />
|
||||
</div>
|
||||
</Tabs_Shadcn_>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-t">
|
||||
{isLoading && (
|
||||
<div className="p-4">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
</div>
|
||||
{isError && (
|
||||
<div className="p-4">
|
||||
<AlertError subject="Failed to retrieve notifications" error={error} />
|
||||
</div>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<div className="flex flex-1 h-[400px] bg-background">
|
||||
{notifications.length > 0 &&
|
||||
!(activeTab === 'archived' && snap.filterStatuses.includes('unread')) ? (
|
||||
<InfiniteList
|
||||
items={notifications}
|
||||
ItemComponent={NotificationRow}
|
||||
LoaderComponent={
|
||||
<div className="p-4">
|
||||
<ShimmeringLoader />
|
||||
</div>
|
||||
}
|
||||
itemProps={{
|
||||
setRowHeight: (idx: number, height: number) => {
|
||||
if (rowHeights.current) {
|
||||
rowHeights.current = { ...rowHeights.current, [idx]: height }
|
||||
}
|
||||
},
|
||||
getProject: (ref: string) => projects?.find((project) => project.ref === ref)!,
|
||||
getOrganization: (id: number) => organizations?.find((org) => org.id === id)!,
|
||||
onUpdateNotificationStatus: (id: string, status: 'archived' | 'seen') => {
|
||||
updateNotifications({ ids: [id], status })
|
||||
},
|
||||
queueMarkRead: (id: string) => {
|
||||
if (markedRead.current && !markedRead.current.includes(id)) {
|
||||
markedRead.current.push(id)
|
||||
}
|
||||
},
|
||||
}}
|
||||
getItemSize={(idx: number) => rowHeights?.current?.[idx] ?? 56}
|
||||
hasNextPage={hasNextPage}
|
||||
isLoadingNextPage={isFetchingNextPage}
|
||||
onLoadNextPage={() => fetchNextPage()}
|
||||
/>
|
||||
) : (
|
||||
<div className="flex flex-col gap-y-4 items-center flex-grow justify-center">
|
||||
<IconInbox size={32} className="text-foreground-light" />
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<p className="text-foreground-light text-sm mx-auto text-center">
|
||||
{activeTab === 'archived'
|
||||
? `No archived notifications${
|
||||
snap.numFiltersApplied > 0
|
||||
? ` based on the ${snap.numFiltersApplied} filter${
|
||||
snap.numFiltersApplied > 1 ? 's' : ''
|
||||
} applied`
|
||||
: ''
|
||||
}`
|
||||
: snap.numFiltersApplied > 0
|
||||
? `No notifications based on the ${snap.numFiltersApplied} filter${
|
||||
snap.numFiltersApplied > 1 ? 's' : ''
|
||||
} applied`
|
||||
: 'All caught up'}
|
||||
</p>
|
||||
<p className="text-foreground-lighter text-xs w-60 mx-auto text-center">
|
||||
{activeTab === 'archived'
|
||||
? 'Notifications that you have previously archived will be shown here'
|
||||
: 'You will be notified here for any notices on your organizations and projects'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{notifications.length > 0 && activeTab === 'inbox' && (
|
||||
<div className="flex items-center justify-center p-1.5 border-t">
|
||||
<Button
|
||||
disabled={isArchiving}
|
||||
loading={isArchiving}
|
||||
type="text"
|
||||
icon={<IconArchive />}
|
||||
onClick={() => archiveAllNotifications()}
|
||||
>
|
||||
Archive all
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user