@@ -319,7 +325,9 @@ function CommunitiesManager() {
{loading ? (
diff --git a/frontend/src/Dashboard.jsx b/frontend/src/Dashboard.jsx
index 1b03118..92bb712 100644
--- a/frontend/src/Dashboard.jsx
+++ b/frontend/src/Dashboard.jsx
@@ -16,6 +16,8 @@ import {
IconCreditCard,
IconSearch
} from '@tabler/icons-react';
+import PageHeader from './components/PageHeader.jsx';
+import Breadcrumbs from './components/Breadcrumbs.jsx';
function StatCard({ icon: Icon, color, value, title, subtitle, to }) {
return (
@@ -172,19 +174,15 @@ function Dashboard() {
{/* Глобальный поиск удалён */}
{/* Заголовок страницы */}
-
-
-
- Панель
- Краткая сводка по данным
-
-
-
-
-
-
+
+ )}
+ actions={( )}
+ />
{/* Основные метрики */}
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx
index 5f4d823..6cbbbe9 100644
--- a/frontend/src/DomainsNewManager.jsx
+++ b/frontend/src/DomainsNewManager.jsx
@@ -17,7 +17,10 @@ import {
IconClock,
IconFileText
} from '@tabler/icons-react';
-import TableSkeleton from './components/TableSkeleton.jsx';
+import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
+import PageHeader from './components/PageHeader.jsx';
+import EmptyState from './components/EmptyState.jsx';
+import Breadcrumbs from './components/Breadcrumbs.jsx';
import S3MetaBar from './components/S3MetaBar.jsx';
import PageHeaderActions from './components/PageHeaderActions.jsx';
import ConfirmDialog from './components/ConfirmDialog.jsx';
@@ -414,14 +417,17 @@ function DomainsNewManager() {
)}
-
-
-
- Домены
- Главная / Данные / Домены
-
-
-
-
+ />
+ )}
+ />
{/* Add New Item Card */}
@@ -557,6 +562,10 @@ function DomainsNewManager() {
{/* Мета-информация будет показана внизу карточки */}
{loading ? (
+ ) : paginatedItems.length === 0 ? (
+
+ Импорт} />
+
) : (
diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx
index d0c911d..d8cdb71 100644
--- a/frontend/src/IPRangesManager.jsx
+++ b/frontend/src/IPRangesManager.jsx
@@ -17,9 +17,12 @@ import {
IconClock,
IconFileText
} from '@tabler/icons-react';
-import TableSkeleton from './components/TableSkeleton.jsx';
+import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
import S3MetaBar from './components/S3MetaBar.jsx';
import PageHeaderActions from './components/PageHeaderActions.jsx';
+import PageHeader from './components/PageHeader.jsx';
+import EmptyState from './components/EmptyState.jsx';
+import Breadcrumbs from './components/Breadcrumbs.jsx';
import ConfirmDialog from './components/ConfirmDialog.jsx';
import WsUpdateModal from './components/WsUpdateModal.jsx';
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
@@ -668,14 +671,17 @@ function IPRangesManager() {
)}
-
-
-
- IP-диапазоны
- Главная / Данные / IP ranges
-
-
-
-
+ />
+ )}
+ />
{/* Add New Item Card */}
@@ -814,6 +819,10 @@ function IPRangesManager() {
{/* Мета-информация будет показана внизу карточки */}
{loading ? (
+ ) : paginatedItems.length === 0 ? (
+
+ Импорт} />
+
) : (
diff --git a/frontend/src/components/Breadcrumbs.jsx b/frontend/src/components/Breadcrumbs.jsx
new file mode 100644
index 0000000..6e89d59
--- /dev/null
+++ b/frontend/src/components/Breadcrumbs.jsx
@@ -0,0 +1,25 @@
+import React from 'react'
+import { Link } from 'react-router-dom'
+
+function Breadcrumbs({ items = [] }) {
+ if (!Array.isArray(items) || items.length === 0) return null
+ return (
+
+ )
+}
+
+export default Breadcrumbs
+
+
diff --git a/frontend/src/components/EmptyState.jsx b/frontend/src/components/EmptyState.jsx
new file mode 100644
index 0000000..a88e604
--- /dev/null
+++ b/frontend/src/components/EmptyState.jsx
@@ -0,0 +1,31 @@
+import React from 'react'
+
+function EmptyState({
+ icon: Icon,
+ title = 'Нет данных',
+ description,
+ action,
+}) {
+ return (
+
+
+ {Icon ? : null}
+
+ {title}
+ {description && (
+
+ {description}
+
+ )}
+ {action && (
+
+ {action}
+
+ )}
+
+ )
+}
+
+export default EmptyState
+
+
diff --git a/frontend/src/components/NotifyProvider.jsx b/frontend/src/components/NotifyProvider.jsx
index 714baff..e470e37 100644
--- a/frontend/src/components/NotifyProvider.jsx
+++ b/frontend/src/components/NotifyProvider.jsx
@@ -90,9 +90,12 @@ export function useNotify() {
// Единый тост-успех для мутаций (POST/DELETE/PUT/PATCH)
export function notifyMutationSuccess(message, details) {
+ // Тихий режим: по умолчанию успех показываем не тостом, а через inline-баннер на страницах.
+ // Оставляем возможность вручную вызвать тост при необходимости, если передан details?.forceToast === true
try {
const text = message || 'Операция выполнена';
- if (typeof window !== 'undefined' && window.notify?.success) {
+ const forceToast = Boolean(details && details.forceToast);
+ if (forceToast && typeof window !== 'undefined' && window.notify?.success) {
window.notify.success(text, details);
}
} catch {}
diff --git a/frontend/src/components/PageHeader.jsx b/frontend/src/components/PageHeader.jsx
new file mode 100644
index 0000000..0affaa7
--- /dev/null
+++ b/frontend/src/components/PageHeader.jsx
@@ -0,0 +1,28 @@
+import React from 'react'
+
+function PageHeader({ title, pretitle, actions, meta }) {
+ return (
+
+
+
+ {pretitle && (
+ {pretitle}
+ )}
+ {title}
+ {meta && (
+ {meta}
+ )}
+
+ {actions && (
+
+ {actions}
+
+ )}
+
+
+ )
+}
+
+export default PageHeader
+
+
diff --git a/frontend/src/components/PageHeaderActions.jsx b/frontend/src/components/PageHeaderActions.jsx
index 00343b5..5993cbc 100644
--- a/frontend/src/components/PageHeaderActions.jsx
+++ b/frontend/src/components/PageHeaderActions.jsx
@@ -5,7 +5,8 @@ import {
IconDeviceFloppy,
IconPlayerPlay,
IconBolt,
- IconEraser
+ IconEraser,
+ IconDots
} from '@tabler/icons-react';
import PortalDropdown from './PortalDropdown.jsx';
import { IconClock } from '@tabler/icons-react';
@@ -31,6 +32,7 @@ function PageHeaderActions({
onOnlineUpdate,
onBackgroundUpdate,
}) {
+ const secondaryButtons = Boolean(onImport || onExport || onClear || onClearCommunities || onHistory);
// Рендер плейсхолдеров во время загрузки/перезагрузки страницы
if (loading) {
const showUpdateGroup = Boolean(onBackgroundUpdate || onOnlineUpdate);
@@ -95,7 +97,7 @@ function PageHeaderActions({
)}
-
+
{onRefresh && (
)}
+ {/* Компактное меню для маленьких экранов */}
+ {secondaryButtons && (
+
+ Ещё>}
+ >
+ {onRefresh && (
+
+ )}
+ {onImport && (
+
+ )}
+ {onExport && (
+
+ )}
+ {onClear && (
+
+ )}
+ {onClearCommunities && (
+
+ )}
+ {onHistory && (
+
+ )}
+
+
+ )}
);
}
diff --git a/frontend/src/components/TableSkeleton.jsx b/frontend/src/components/TableSkeleton.jsx
index 2b193a4..115a085 100644
--- a/frontend/src/components/TableSkeleton.jsx
+++ b/frontend/src/components/TableSkeleton.jsx
@@ -26,4 +26,20 @@ export default function TableSkeleton({ rows = 10, cols = 3 }) {
);
}
+export function TableEmpty({ cols = 3, children }) {
+ return (
+
+
+
+
+ |
+ {children}
+ |
+
+
+
+
+ );
+}
+
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 216b671..4fca928 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1 +1,5 @@
/* Using Bootstrap styles, this file can be empty or used for custom global styles. */
+
+/* Sticky footer helpers */
+.page { min-height: 100vh; display: flex; flex-direction: column; }
+.footer { margin-top: auto; }
\ No newline at end of file
|