| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168 |
- import { AlertTriangle, ChevronRight, Inbox } from 'lucide-react'
- import { Badge, Button, cn } from 'ui'
- import { GenericSkeletonLoader } from 'ui-patterns'
- import type { AdvisorItem } from './AdvisorPanel.types'
- import {
- formatItemDate,
- getAdvisorItemSecondaryText,
- getAdvisorPanelItemDisplayTitle,
- severityBadgeVariants,
- severityColorClasses,
- severityLabels,
- tabIconMap,
- } from './AdvisorPanel.utils'
- import { EmptyAdvisor } from './EmptyAdvisor'
- import type { Notification } from '@/data/notifications/notifications-v2-query'
- import type { AdvisorSeverity, AdvisorTab } from '@/state/advisor-state'
- const NoProjectNotice = () => {
- return (
- <div className="absolute top-28 px-6 flex flex-col items-center justify-center w-full gap-y-2">
- <Inbox className="text-foreground-muted" strokeWidth={1} />
- <div className="text-center">
- <p className="heading-default">Project required</p>
- <p className="text-foreground-light text-sm">
- Select a project to view security and performance advisories
- </p>
- </div>
- </div>
- )
- }
- interface AdvisorPanelBodyProps {
- isLoading: boolean
- isError: boolean
- filteredItems: AdvisorItem[]
- activeTab: AdvisorTab
- severityFilters: AdvisorSeverity[]
- onItemClick: (item: AdvisorItem) => void
- onClearFilters: () => void
- hiddenItemsCount: number
- hasAnyFilters: boolean
- hasProjectRef?: boolean
- }
- export const AdvisorPanelBody = ({
- isLoading,
- isError,
- filteredItems,
- activeTab,
- severityFilters,
- onItemClick,
- onClearFilters,
- hiddenItemsCount,
- hasAnyFilters,
- hasProjectRef = true,
- }: AdvisorPanelBodyProps) => {
- // Show notice if no project ref and trying to view project-specific tabs
- if (!hasProjectRef && activeTab !== 'messages' && activeTab !== 'all') {
- return <NoProjectNotice />
- }
- if (isLoading) {
- return (
- <div>
- <GenericSkeletonLoader className="w-full p-4" />
- </div>
- )
- }
- if (isError) {
- return (
- <div className="h-full mx-4 flex flex-col items-center justify-center gap-y-2">
- <AlertTriangle className="text-destructive" />
- <div className="flex flex-col items-center justify-center">
- <h4 className="text-base font-normal text-foreground-light">Error loading advisories</h4>
- <p className="text-sm text-foreground-lighter">Please try again later.</p>
- </div>
- </div>
- )
- }
- if (filteredItems.length === 0) {
- return (
- <EmptyAdvisor
- activeTab={activeTab}
- hasFilters={hasAnyFilters}
- onClearFilters={onClearFilters}
- />
- )
- }
- return (
- <>
- <div className="flex flex-col">
- {filteredItems.map((item) => {
- const SeverityIcon = tabIconMap[item.tab as Exclude<AdvisorTab, 'all'>]
- const severityClass = severityColorClasses[item.severity]
- const isNotification = item.source === 'notification'
- const notification = isNotification ? (item.original as Notification) : null
- const isUnread = notification?.status === 'new'
- const primaryText = getAdvisorPanelItemDisplayTitle(item)
- const secondaryText = getAdvisorItemSecondaryText(item)
- const metadataText =
- secondaryText ?? (item.createdAt ? formatItemDate(item.createdAt) : undefined)
- // Date strings (e.g. "a few seconds ago") come from formatItemDate and
- // need sentence-case capitalisation; entity strings (lint / signal) don't.
- const metadataCapitalize = secondaryText === undefined && item.createdAt !== undefined
- return (
- <div key={`${item.source}-${item.id}`} className="border-b">
- <Button
- type="text"
- className={cn(
- 'justify-start w-full block rounded-none h-auto py-3 px-4 hover:text-foreground',
- isUnread && 'bg-surface-100/50'
- )}
- onClick={() => onItemClick(item)}
- >
- <div className="flex items-center justify-between gap-2">
- <div className="flex items-center gap-3 overflow-hidden">
- <SeverityIcon
- size={16}
- strokeWidth={1.5}
- className={cn('shrink-0', severityClass)}
- />
- <div className="text-left flex flex-col gap-0.5 truncate flex-1 min-w-0">
- <div className="truncate">{primaryText}</div>
- {metadataText && (
- <div className="flex items-center gap-1 text-xs text-foreground-light">
- <span
- className={cn('truncate', metadataCapitalize && 'capitalize-sentence')}
- >
- {metadataText}
- </span>
- </div>
- )}
- </div>
- </div>
- <div className="flex items-center gap-2 shrink-0">
- {item.severity === 'critical' && (
- <Badge variant={severityBadgeVariants[item.severity]}>
- {severityLabels[item.severity]}
- </Badge>
- )}
- <ChevronRight
- size={16}
- strokeWidth={1.5}
- className="shrink-0 text-foreground-lighter"
- />
- </div>
- </div>
- </Button>
- </div>
- )
- })}
- </div>
- {severityFilters.length > 0 && hiddenItemsCount > 0 && (
- <div className="px-4 py-3">
- <Button type="text" className="w-full" onClick={onClearFilters}>
- Show {hiddenItemsCount} more issue{hiddenItemsCount !== 1 ? 's' : ''}
- </Button>
- </div>
- )}
- </>
- )
- }
|