NotificationPanelContent
Контейнер ленты уведомлений. Содержит заголовок, опциональные фильтры (segmented control, chip toggle), кнопку «прочитать все», слот-список карточек и состояния loading / blank.
Namespace NotificationPanelContent.{Blank, Group, Stack} — субкомпоненты-композиции для слота content.
Когда использовать
- Лента уведомлений в центре экрана или на отдельной странице.
- Контент внутри
NotificationPanel.
Анатомия
Panel state
Состояния панели (соответствуют Figma variant content):
content— стандартный список карточек через слотcontent.loading—loading={true}показывает скелетоны (skeletonsAmount, default2).blank—content={<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'
Примеры использования
Базовая лента
Уведомления
Непрочитанное уведомление
INC-4821
Кластер k8s-prod-1 деградирован
3 из 5 нод недоступны последние 6 минут.
Непрочитанное уведомление
Storage
Лимит дисковой квоты
Использовано 92% /var/data — осталось 38 ГБ из 480 ГБ.
Непрочитанное уведомление
api-gateway · v2.18.3
Деплой не прошёл health-check
Readiness probe вернул 503 на 4 из 6 подов.
Backup
Резервная копия завершена
Бэкап БД prod-1 (412 ГБ) загружен в s3://backups-prod/.
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'
/>
</>
}
/>
);
}Загрузка
Уведомления
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: () => {} }}
/>
);
}Ошибка загрузки
Уведомления
Сервис временно недоступен
Восстановление уже идёт. Обновите страницу через минуту.
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='Восстановление уже идёт. Обновите страницу через минуту.'
/>
}
/>
);
}Полный сценарий продовой ленты
Уведомления
Требуют внимания
Непрочитанное уведомление
INC-4821
Кластер k8s-prod-1 деградирован
3 из 5 нод недоступны последние 6 минут.
Непрочитанное уведомление
api-gateway · v2.18.3
Деплой не прошёл health-check
Readiness probe вернул 503 на 4 из 6 подов.
Лимит дисковой квоты · 3 хоста
Непрочитанное уведомление
Storage
prod-1 · /var/data
Использовано 92% — осталось 38 ГБ из 480 ГБ.
Непрочитанное уведомление
@you · billing/PR-1402
А. Иванов упомянул вас в PR
Пересмотрите расчёт grace-period в BillingScheduler.tsx:142
Backup
Резервная копия завершена
prod-1 (412 ГБ) → s3://backups-prod/2026-05-26/
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
Props
NotificationPanelContentProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
chipToggle | { label: string; checked: boolean; onChange(checked: boolean): void; } | — | no | Переключатель для фильтрации |
className | string | — | no | |
content | ReactNode | — | no | Контент для отрисовки (e.g NotificationCard | NotificationPanelContent.Blank) |
data-test-id | string | — | no | |
loading | boolean | — | no | Состояние загрузки |
readAllButton | (Omit<ButtonProps, "data-test-id" | "size"> & { tooltip?: TooltipProps; }) | — | no | Кнопка в "шапке" панели |
scrollContainerRef | RefObject<HTMLElement> | — | no | Ссылка на контейнер, который скроллится |
scrollEndRef | RefObject<HTMLDivElement> | — | no | Ссылка на элемент, обозначающий самый конец прокручиваемого списка |
segments | Omit<SegmentControlProps, "data-test-id" | "size"> | — | no | Сегменты для фильтрации |
settings | NotificationPanelSettingsProps | — | no | Кнопка настроек и выпадающий список |
skeletonsAmount | number | 2 | no | Количество скелетонов карточек для отображения при загрузке |
title | string | — | yes | Заголовок панели |
Types
NotificationPanelContentProps
NotificationPanelSettingsProps
Related props
ButtonProps
SegmentControlProps
TooltipProps
NotificationPanelContent.Stack
Types
Props
NotificationCardStackProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
actions | Action[] | — | no | Список действий в выпадающем меню |
children | Iterable<ReactNode> | — | yes | Карточки в стопке, видна первая карточка, остальные схлопываются под нее. |
data-test-id | string | — | no | |
defaultOpen | boolean | — | no | Состояние открыт/закрыт по умолчанию |
onOpenChanged | ((open: boolean) => void) |