NotificationPanelContent

Контейнер ленты уведомлений. Содержит заголовок, опциональные фильтры (segmented control, chip toggle), кнопку «прочитать все», слот-список карточек и состояния loading / blank.

Namespace NotificationPanelContent.{Blank, Group, Stack} — субкомпоненты-композиции для слота content.

Когда использовать

  • Лента уведомлений в центре экрана или на отдельной странице.
  • Контент внутри NotificationPanel.

Анатомия

Panel state

Состояния панели (соответствуют Figma variant content):

  • content — стандартный список карточек через слот content.
  • loadingloading={true} показывает скелетоны (skeletonsAmount, default 2).
  • blankcontent={<NotificationPanelContent.Blank />} для пустого состояния и состояния ошибки.

Субкомпоненты слота content

  • NotificationPanelContent.Group — группа карточек с заголовком.
  • NotificationPanelContent.Stack — стек схлопывающихся карточек. Стек из одной карточки рендерится без обёртки.
  • NotificationPanelContent.Blank — пустое состояние (используется и для ошибки — с иконкой и текстом).

Установка

pnpm add @cloud-ru/ds-uikit-product-notification
import { NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification'

Примеры использования

Базовая лента

Базовая лентаСписок карточек через слот content, readAllButton помечает все прочитанными, onVisible — каждую при прокрутке.

Уведомления

Непрочитанное уведомление
INC-4821
Кластер k8s-prod-1 деградирован
3 из 5 нод недоступны последние 6 минут.
Непрочитанное уведомление
Storage
Лимит дисковой квоты
Использовано 92% /var/data — осталось 38 ГБ из 480 ГБ.
02:50
Непрочитанное уведомление
api-gateway · v2.18.3
Деплой не прошёл health-check
Readiness probe вернул 503 на 4 из 6 подов.
вчера · 23:11
Backup
Резервная копия завершена
Бэкап БД prod-1 (412 ГБ) загружен в s3://backups-prod/.
вчера · 14:32
tsx
import { APPEARANCE, NotificationCard, NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification';
import { useState } from 'react';

export function PanelBasic() {
  const [readIds, setReadIds] = useState<Set<string>>(new Set());
  const markRead = (id: string) => setReadIds(prev => (prev.has(id) ? prev : new Set(prev).add(id)));

  return (
    <NotificationPanelContent
      title='Уведомления'
      readAllButton={{
        label: 'Прочитать всё',
        onClick: () => setReadIds(new Set(['inc-4821', 'quota-var-data', 'deploy-9217', 'backup-1729'])),
      }}
      content={
        <>
          <NotificationCard
            id='inc-4821'
            label='INC-4821'
            appearance={APPEARANCE.Error}
            title='Кластер k8s-prod-1 деградирован'
            description='3 из 5 нод недоступны последние 6 минут.'
            date='03:14'
            link={{ label: 'Открыть инцидент', href: '/incidents/INC-4821' }}
            unread={!readIds.has('inc-4821')}
            onVisible={markRead}
          />
          <NotificationCard
            id='quota-var-data'
            label='Storage'
            appearance={APPEARANCE.Warning}
            title='Лимит дисковой квоты'
            description='Использовано 92% /var/data — осталось 38 ГБ из 480 ГБ.'
            date='02:50'
            unread={!readIds.has('quota-var-data')}
            onVisible={markRead}
          />
          <NotificationCard
            id='deploy-9217'
            label='api-gateway · v2.18.3'
            appearance={APPEARANCE.Error}
            title='Деплой не прошёл health-check'
            description='Readiness probe вернул 503 на 4 из 6 подов.'
            date='вчера · 23:11'
            unread={!readIds.has('deploy-9217')}
            onVisible={markRead}
          />
          <NotificationCard
            id='backup-1729'
            label='Backup'
            appearance={APPEARANCE.Success}
            title='Резервная копия завершена'
            description='Бэкап БД prod-1 (412 ГБ) загружен в s3://backups-prod/.'
            date='вчера · 14:32'
          />
        </>
      }
    />
  );
}

Загрузка

Загрузкаloading + skeletonsAmount={4} вместо слота content; фильтры и readAllButton остаются на месте.

Уведомления

tsx
import { NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification';

export function PanelLoading() {
  return (
    <NotificationPanelContent
      title='Уведомления'
      loading
      skeletonsAmount={4}
      segments={{
        items: [
          { value: 'all', label: 'Все' },
          { value: 'unread', label: 'Непрочитанные' },
          { value: 'mentions', label: 'Упоминания' },
        ],
        value: 'all',
        onChange: () => {},
      }}
      readAllButton={{ label: 'Прочитать всё', onClick: () => {} }}
    />
  );
}

Ошибка загрузки

Ошибка загрузкиСостояние ошибки через content={<NotificationPanelContent.Blank />} с иконкой и текстом — сервер недоступен.

Уведомления

Сервис временно недоступен
Восстановление уже идёт. Обновите страницу через минуту.
tsx
import { CrossSVG } from '@cloud-ru/ds-icons/interface/system';
import { NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification';

export function PanelError() {
  return (
    <NotificationPanelContent
      title='Уведомления'
      content={
        <NotificationPanelContent.Blank
          icon={{ icon: CrossSVG, appearance: 'neutral' }}
          title='Сервис временно недоступен'
          content='Восстановление уже идёт. Обновите страницу через минуту.'
        />
      }
    />
  );
}

Полный сценарий продовой ленты

Полный сценарий продовой лентыSegments + chipToggle + settings + readAll, Group для критичных, Stack для повторяющихся алертов квоты по нескольким хостам.

Уведомления

Требуют внимания
Непрочитанное уведомление
INC-4821
Кластер k8s-prod-1 деградирован
3 из 5 нод недоступны последние 6 минут.
Непрочитанное уведомление
api-gateway · v2.18.3
Деплой не прошёл health-check
Readiness probe вернул 503 на 4 из 6 подов.
02:50
Непрочитанное уведомление
Storage
prod-1 · /var/data
Использовано 92% — осталось 38 ГБ из 480 ГБ.
01:12
Непрочитанное уведомление
@you · billing/PR-1402
А. Иванов упомянул вас в PR
Пересмотрите расчёт grace-period в BillingScheduler.tsx:142
Открыть PR
вчера · 18:20
Backup
Резервная копия завершена
prod-1 (412 ГБ) → s3://backups-prod/2026-05-26/
вчера · 14:32
tsx
import { APPEARANCE, NotificationCard, NotificationPanelContent } from '@cloud-ru/ds-uikit-product-notification';
import { useMemo, useState } from 'react';

type Filter = 'all' | 'unread' | 'mentions';

type CardId = 'inc-4821' | 'deploy-9217' | 'stack-quota' | 'mention-1' | 'backup-1729';

export function PanelFull() {
  const [filter, setFilter] = useState<Filter>('all');
  const [importantOnly, setImportantOnly] = useState(false);
  const [muted, setMuted] = useState(false);
  const [readIds, setReadIds] = useState<Set<CardId>>(new Set());

  const markRead = (id: string) =>
    setReadIds(prev => (prev.has(id as CardId) ? prev : new Set(prev).add(id as CardId)));

  const isUnread = (id: CardId) => !readIds.has(id);

  // Какие карточки попадают под текущий фильтр. `important` — критичные алерты (error/warning).
  const visible = useMemo(() => {
    const matches = (id: CardId, important: boolean, mention: boolean) => {
      if (filter === 'unread' && !isUnread(id)) return false;
      if (filter === 'mentions' && !mention) return false;
      if (importantOnly && !important) return false;
      return true;
    };

    return {
      inc: matches('inc-4821', true, false),
      deploy: matches('deploy-9217', true, false),
      quota: matches('stack-quota', true, false),
      mention: matches('mention-1', false, true),
      backup: matches('backup-1729', false, false),
    };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [filter, importantOnly, readIds]);

  const attentionVisible = visible.inc || visible.deploy;

  if (muted) {
    return (
      <NotificationPanelContent
        title='Уведомления'
        settings={{
          button: { onClick: () => setMuted(false) },
          actions: [{ content: { label: 'Снять заглушение' }, onClick: () => setMuted(false) }],
        }}
        content={<NotificationPanelContent.Blank />}
      />
    );
  }

  return (
    <NotificationPanelContent
      title='Уведомления'
      segments={{
        items: [
          { value: 'all', label: 'Все' },
          { value: 'unread', label: 'Непрочитанные' },
          { value: 'mentions', label: 'Упоминания' },
        ],
        value: filter,
        onChange: value => setFilter(value as Filter),
      }}
      chipToggle={{
        label: 'Только важные',
        checked: importantOnly,
        onChange: setImportantOnly,
      }}
      settings={{
        button: {},
        actions: [
          { content: { label: 'Только непрочитанные' }, onClick: () => setFilter('unread') },
          { content: { label: 'Показать все' }, onClick: () => setFilter('all') },
          { content: { label: 'Заглушить на 1 час' }, onClick: () => setMuted(true) },
        ],
      }}
      readAllButton={{
        label: 'Прочитать всё',
        onClick: () => setReadIds(new Set(['inc-4821', 'deploy-9217', 'stack-quota', 'mention-1'])),
      }}
      content={
        <>
          {attentionVisible && (
            <NotificationPanelContent.Group title='Требуют внимания'>
              {visible.inc && (
                <NotificationCard
                  id='inc-4821'
                  label='INC-4821'
                  appearance={APPEARANCE.Error}
                  title='Кластер k8s-prod-1 деградирован'
                  description='3 из 5 нод недоступны последние 6 минут.'
                  date='03:14'
                  link={{ label: 'Открыть инцидент', href: '/incidents/INC-4821' }}
                  unread={isUnread('inc-4821')}
                  onVisible={markRead}
                />
              )}
              {visible.deploy && (
                <NotificationCard
                  id='deploy-9217'
                  label='api-gateway · v2.18.3'
                  appearance={APPEARANCE.Error}
                  title='Деплой не прошёл health-check'
                  description='Readiness probe вернул 503 на 4 из 6 подов.'
                  date='02:50'
                  primaryButton={{ label: 'Повторить', onClick: () => markRead('deploy-9217') }}
                  secondaryButton={{
                    label: 'Логи',
                    onClick: () => window.open('/logs/api-gateway/2.18.3', '_blank'),
                  }}
                  unread={isUnread('deploy-9217')}
                  onVisible={markRead}
                />
              )}
            </NotificationPanelContent.Group>
          )}

          {visible.quota && (
            <>
              <NotificationPanelContent.Stack
                title='Лимит дисковой квоты · 3 хоста'
                unread={isUnread('stack-quota')}
                actions={[
                  { content: { label: 'Прочитать все' }, onClick: () => markRead('stack-quota') },
                  { content: { label: 'Заглушить группу' }, onClick: () => setMuted(true) },
                ]}
                onOpenChanged={open => open && markRead('stack-quota')}
              >
                <NotificationCard
                  id='quota-prod-1'
                  label='Storage'
                  appearance={APPEARANCE.Warning}
                  title='prod-1 · /var/data'
                  description='Использовано 92% — осталось 38 ГБ из 480 ГБ.'
                  date='01:12'
                />
                <NotificationCard
                  id='quota-prod-2'
                  label='Storage'
                  appearance={APPEARANCE.Warning}
                  title='prod-2 · /var/log'
                  description='Использовано 87% — осталось 62 ГБ из 480 ГБ.'
                  date='00:48'
                />
                <NotificationCard
                  id='quota-stage-1'
                  label='Storage'
                  appearance={APPEARANCE.Warning}
                  title='stage-1 · /var/data'
                  description='Использовано 84% — осталось 76 ГБ из 480 ГБ.'
                  date='вчера · 23:50'
                />
              </NotificationPanelContent.Stack>
            </>
          )}

          {visible.mention && (
            <NotificationCard
              id='mention-1'
              label='@you · billing/PR-1402'
              appearance={APPEARANCE.Default}
              title='А. Иванов упомянул вас в PR'
              description='Пересмотрите расчёт grace-period в BillingScheduler.tsx:142'
              date='вчера · 18:20'
              link={{ label: 'Открыть PR', href: '/billing/pulls/1402' }}
              unread={isUnread('mention-1')}
              onVisible={markRead}
            />
          )}

          {visible.backup && (
            <>
              <NotificationCard
                id='backup-1729'
                label='Backup'
                appearance={APPEARANCE.Success}
                title='Резервная копия завершена'
                description='prod-1 (412 ГБ) → s3://backups-prod/2026-05-26/'
                date='вчера · 14:32'
              />
            </>
          )}
        </>
      }
    />
  );
}

Props

NotificationPanelContent

Types

PropsNotificationPanelContentProps
PropTypeDefaultRequiredDescription
chipToggle{ label: string; checked: boolean; onChange(checked: boolean): void; }noПереключатель для фильтрации
classNamestringno
contentReactNodenoКонтент для отрисовки (e.g NotificationCard | NotificationPanelContent.Blank)
data-test-idstringno
loadingbooleannoСостояние загрузки
readAllButton(Omit<ButtonProps, "data-test-id" | "size"> & { tooltip?: TooltipProps; })noКнопка в "шапке" панели
scrollContainerRefRefObject<HTMLElement>noСсылка на контейнер, который скроллится
scrollEndRefRefObject<HTMLDivElement>noСсылка на элемент, обозначающий самый конец прокручиваемого списка
segmentsOmit<SegmentControlProps, "data-test-id" | "size">noСегменты для фильтрации
settingsNotificationPanelSettingsPropsnoКнопка настроек и выпадающий список
skeletonsAmountnumber2noКоличество скелетонов карточек для отображения при загрузке
titlestringyesЗаголовок панели

Types

NotificationPanelContentProps

NotificationPanelContent.Stack

Types

PropsNotificationCardStackProps
PropTypeDefaultRequiredDescription
actionsAction[]noСписок действий в выпадающем меню
childrenIterable<ReactNode>yesКарточки в стопке, видна первая карточка, остальные схлопываются под нее.
data-test-idstringno
defaultOpenbooleannoСостояние открыт/закрыт по умолчанию
onOpenChanged((open: boolean) => void)noКолбек смены состояния открыт/закрыт
titlestringyesЗаголовок стопки карточек
unreadbooleannoСостояние непрочитанных карточек

Types

NotificationCardStackProps

NotificationPanelContent.Group

Types

PropsNotificationPanelGroupProps
PropTypeDefaultRequiredDescription
childrenReactNodeyesСодержимое группы
classNamestringnoCSS-класс
data-test-idstringno
titlestringyesЗаголовок группы

Types

NotificationPanelGroupProps

NotificationPanelContent.Blank

Types

PropsNotificationPanelBlankProps
PropTypeDefaultRequiredDescription
classNamestringnoДополнительный класс
contentReactNodenoПодзаголовок
data-test-idstringno
iconIconPredefinedPropsnoИконка
titlestringnoЗаголовок

Types

NotificationPanelBlankProps

Storybook

Figma