feat: Обновить компоненты интерфейса, добавив новый заголовок страницы и состояние пустого контента в менеджерах ASNs, Domains и IPRanges, а также улучшить стили для адаптивного отображения
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m0s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 2m0s
This commit is contained in:
@@ -17,9 +17,12 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.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 ConfirmDialog from './components/ConfirmDialog.jsx';
|
||||||
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
@@ -409,14 +412,17 @@ function ASNsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="ASNs"
|
||||||
<div className="col">
|
pretitle={(
|
||||||
<h2 className="page-title">ASNs</h2>
|
<Breadcrumbs items={[
|
||||||
<div className="page-pretitle">Главная / Данные / ASNs</div>
|
{ label: 'Главная', to: '/dashboard' },
|
||||||
</div>
|
{ label: 'Данные' },
|
||||||
<div className="col-auto ms-auto d-print-none">
|
{ label: 'ASNs' },
|
||||||
<PageHeaderActions
|
]} />
|
||||||
|
)}
|
||||||
|
actions={(
|
||||||
|
<PageHeaderActions
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={fetchItems}
|
onRefresh={fetchItems}
|
||||||
disableRefresh={loading}
|
disableRefresh={loading}
|
||||||
@@ -443,10 +449,9 @@ function ASNsNewManager() {
|
|||||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-3">
|
<div className="col-lg-3">
|
||||||
{/* Add New ASN Card */}
|
{/* Add New ASN Card */}
|
||||||
@@ -551,6 +556,10 @@ function ASNsNewManager() {
|
|||||||
{/* Мета-информация будет показана внизу карточки */}
|
{/* Мета-информация будет показана внизу карточки */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableSkeleton rows={10} cols={3} />
|
<TableSkeleton rows={10} cols={3} />
|
||||||
|
) : paginatedItems.length === 0 ? (
|
||||||
|
<TableEmpty cols={3}>
|
||||||
|
<EmptyState title="Нет ASN" description="Импортируйте или добавьте записи, чтобы начать." action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>} />
|
||||||
|
</TableEmpty>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
|||||||
@@ -211,7 +211,7 @@ function MainLayout() {
|
|||||||
const activeTab = getActiveTab();
|
const activeTab = getActiveTab();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page">
|
<div className="page d-flex flex-column min-vh-100">
|
||||||
{/* Верхнее меню Tabler Admin */}
|
{/* Верхнее меню Tabler Admin */}
|
||||||
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{boxShadow: '0 1px 0 0 #e9ecef'}}>
|
<header className="navbar navbar-expand-md navbar-light d-print-none" style={{boxShadow: '0 1px 0 0 #e9ecef'}}>
|
||||||
<div className="container-xl">
|
<div className="container-xl">
|
||||||
@@ -322,7 +322,7 @@ function MainLayout() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div className="container-xl mt-4">
|
<div className="container-xl mt-4 flex-grow-1">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/dashboard" element={<Dashboard />} />
|
<Route path="/dashboard" element={<Dashboard />} />
|
||||||
<Route path="/domains" element={<DomainsNewManager />} />
|
<Route path="/domains" element={<DomainsNewManager />} />
|
||||||
@@ -336,7 +336,7 @@ function MainLayout() {
|
|||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
<footer className="footer mt-auto py-3">
|
<footer className="footer mt-auto py-2">
|
||||||
<div className="container-xl text-muted small">
|
<div className="container-xl text-muted small">
|
||||||
Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}
|
Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,6 +16,10 @@ import {
|
|||||||
IconFilter
|
IconFilter
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
|
import PageHeader from './components/PageHeader.jsx';
|
||||||
|
import EmptyState from './components/EmptyState.jsx';
|
||||||
|
import { TableEmpty } from './components/TableSkeleton.jsx';
|
||||||
|
import Breadcrumbs from './components/Breadcrumbs.jsx';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -224,14 +228,17 @@ function CommunitiesManager() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title={(<><IconFilter className="me-2" />Справочник Community</>)}
|
||||||
<div className="col">
|
pretitle={(
|
||||||
<h2 className="page-title"><IconFilter className="me-2" />Справочник Community</h2>
|
<Breadcrumbs items={[
|
||||||
<div className="page-pretitle">Главная / Данные / Community</div>
|
{ label: 'Главная', to: '/dashboard' },
|
||||||
</div>
|
{ label: 'Данные' },
|
||||||
<div className="col-auto ms-auto d-print-none">
|
{ label: 'Community' },
|
||||||
<PageHeaderActions
|
]} />
|
||||||
|
)}
|
||||||
|
actions={(
|
||||||
|
<PageHeaderActions
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={fetchItems}
|
onRefresh={fetchItems}
|
||||||
disableRefresh={loading}
|
disableRefresh={loading}
|
||||||
@@ -240,10 +247,9 @@ function CommunitiesManager() {
|
|||||||
disableExport={items.length === 0}
|
disableExport={items.length === 0}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
disableSave={loading}
|
disableSave={loading}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
<div className="alert alert-danger alert-dismissible" role="alert">
|
||||||
@@ -319,7 +325,9 @@ function CommunitiesManager() {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<tr><td colSpan={6} className="text-center text-muted py-4">Загрузка...</td></tr>
|
<tr><td colSpan={6} className="text-center text-muted py-4">Загрузка...</td></tr>
|
||||||
) : paginated.length === 0 ? (
|
) : paginated.length === 0 ? (
|
||||||
<tr><td colSpan={6} className="text-center text-muted py-4">Нет записей</td></tr>
|
<tr><td colSpan={6} className="text-center text-muted py-4">
|
||||||
|
<EmptyState title="Нет community" description="Добавьте запись, чтобы начать." action={<button className="btn btn-primary" onClick={addItem}>Добавить</button>} />
|
||||||
|
</td></tr>
|
||||||
) : paginated.map((item) => (
|
) : paginated.map((item) => (
|
||||||
<tr key={item.value} className={editingValue === item.value ? 'table-info' : ''}>
|
<tr key={item.value} className={editingValue === item.value ? 'table-info' : ''}>
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
+11
-13
@@ -16,6 +16,8 @@ import {
|
|||||||
IconCreditCard,
|
IconCreditCard,
|
||||||
IconSearch
|
IconSearch
|
||||||
} from '@tabler/icons-react';
|
} 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 }) {
|
function StatCard({ icon: Icon, color, value, title, subtitle, to }) {
|
||||||
return (
|
return (
|
||||||
@@ -172,19 +174,15 @@ function Dashboard() {
|
|||||||
<div>
|
<div>
|
||||||
{/* Глобальный поиск удалён */}
|
{/* Глобальный поиск удалён */}
|
||||||
{/* Заголовок страницы */}
|
{/* Заголовок страницы */}
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="Панель"
|
||||||
<div className="col">
|
pretitle={(
|
||||||
<h2 className="page-title">Панель</h2>
|
<Breadcrumbs items={[
|
||||||
<div className="page-pretitle">Краткая сводка по данным</div>
|
{ label: 'Главная' },
|
||||||
</div>
|
]} />
|
||||||
<div className="col-auto ms-auto d-print-none">
|
)}
|
||||||
<button className="btn btn-outline-primary" onClick={() => window.location.reload()}>
|
actions={(<button className="btn btn-outline-primary" onClick={() => window.location.reload()}><IconRefresh className="me-2" /> Обновить</button>)}
|
||||||
<IconRefresh className="me-2" /> Обновить
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Основные метрики */}
|
{/* Основные метрики */}
|
||||||
<div className="row g-3 mb-4">
|
<div className="row g-3 mb-4">
|
||||||
|
|||||||
@@ -17,7 +17,10 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText
|
||||||
} from '@tabler/icons-react';
|
} 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 S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
import PageHeaderActions from './components/PageHeaderActions.jsx';
|
||||||
import ConfirmDialog from './components/ConfirmDialog.jsx';
|
import ConfirmDialog from './components/ConfirmDialog.jsx';
|
||||||
@@ -414,14 +417,17 @@ function DomainsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="Домены"
|
||||||
<div className="col">
|
pretitle={(
|
||||||
<h2 className="page-title">Домены</h2>
|
<Breadcrumbs items={[
|
||||||
<div className="page-pretitle">Главная / Данные / Домены</div>
|
{ label: 'Главная', to: '/dashboard' },
|
||||||
</div>
|
{ label: 'Данные' },
|
||||||
<div className="col-auto ms-auto d-print-none">
|
{ label: 'Домены' },
|
||||||
<PageHeaderActions
|
]} />
|
||||||
|
)}
|
||||||
|
actions={(
|
||||||
|
<PageHeaderActions
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={fetchItems}
|
onRefresh={fetchItems}
|
||||||
disableRefresh={loading}
|
disableRefresh={loading}
|
||||||
@@ -448,10 +454,9 @@ function DomainsNewManager() {
|
|||||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-3">
|
<div className="col-lg-3">
|
||||||
{/* Add New Item Card */}
|
{/* Add New Item Card */}
|
||||||
@@ -557,6 +562,10 @@ function DomainsNewManager() {
|
|||||||
{/* Мета-информация будет показана внизу карточки */}
|
{/* Мета-информация будет показана внизу карточки */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableSkeleton rows={10} cols={3} />
|
<TableSkeleton rows={10} cols={3} />
|
||||||
|
) : paginatedItems.length === 0 ? (
|
||||||
|
<TableEmpty cols={3}>
|
||||||
|
<EmptyState title="Нет доменов" description="Импортируйте или добавьте записи, чтобы начать." action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>} />
|
||||||
|
</TableEmpty>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
|||||||
@@ -17,9 +17,12 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
|
||||||
import S3MetaBar from './components/S3MetaBar.jsx';
|
import S3MetaBar from './components/S3MetaBar.jsx';
|
||||||
import PageHeaderActions from './components/PageHeaderActions.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 ConfirmDialog from './components/ConfirmDialog.jsx';
|
||||||
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
import WsUpdateModal from './components/WsUpdateModal.jsx';
|
||||||
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
|
||||||
@@ -668,14 +671,17 @@ function IPRangesManager() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="page-header d-print-none mb-4">
|
<PageHeader
|
||||||
<div className="row align-items-center">
|
title="IP-диапазоны"
|
||||||
<div className="col">
|
pretitle={(
|
||||||
<h2 className="page-title">IP-диапазоны</h2>
|
<Breadcrumbs items={[
|
||||||
<div className="page-pretitle">Главная / Данные / IP ranges</div>
|
{ label: 'Главная', to: '/dashboard' },
|
||||||
</div>
|
{ label: 'Данные' },
|
||||||
<div className="col-auto ms-auto d-print-none">
|
{ label: 'IP ranges' },
|
||||||
<PageHeaderActions
|
]} />
|
||||||
|
)}
|
||||||
|
actions={(
|
||||||
|
<PageHeaderActions
|
||||||
loading={loading}
|
loading={loading}
|
||||||
onRefresh={fetchItems}
|
onRefresh={fetchItems}
|
||||||
disableRefresh={loading}
|
disableRefresh={loading}
|
||||||
@@ -702,10 +708,9 @@ function IPRangesManager() {
|
|||||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
)}
|
||||||
</div>
|
/>
|
||||||
</div>
|
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-3">
|
<div className="col-lg-3">
|
||||||
{/* Add New Item Card */}
|
{/* Add New Item Card */}
|
||||||
@@ -814,6 +819,10 @@ function IPRangesManager() {
|
|||||||
{/* Мета-информация будет показана внизу карточки */}
|
{/* Мета-информация будет показана внизу карточки */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<TableSkeleton rows={10} cols={3} />
|
<TableSkeleton rows={10} cols={3} />
|
||||||
|
) : paginatedItems.length === 0 ? (
|
||||||
|
<TableEmpty cols={3}>
|
||||||
|
<EmptyState title="Нет IP-диапазонов" description="Импортируйте или добавьте записи, чтобы начать." action={<button className="btn btn-primary" onClick={handleImport}>Импорт</button>} />
|
||||||
|
</TableEmpty>
|
||||||
) : (
|
) : (
|
||||||
<div className="table-responsive">
|
<div className="table-responsive">
|
||||||
<table className="table card-table table-vcenter table-nowrap mb-0">
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<nav aria-label="breadcrumb">
|
||||||
|
<ol className="breadcrumb">
|
||||||
|
{items.map((it, idx) => (
|
||||||
|
<li key={idx} className={`breadcrumb-item${idx === items.length - 1 ? ' active' : ''}`} aria-current={idx === items.length - 1 ? 'page' : undefined}>
|
||||||
|
{it.to && idx !== items.length - 1 ? (
|
||||||
|
<Link to={it.to}>{it.label}</Link>
|
||||||
|
) : (
|
||||||
|
<span>{it.label}</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Breadcrumbs
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
function EmptyState({
|
||||||
|
icon: Icon,
|
||||||
|
title = 'Нет данных',
|
||||||
|
description,
|
||||||
|
action,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="empty">
|
||||||
|
<div className="empty-icon">
|
||||||
|
{Icon ? <Icon size={48} className="text-muted" /> : null}
|
||||||
|
</div>
|
||||||
|
<p className="empty-title">{title}</p>
|
||||||
|
{description && (
|
||||||
|
<p className="empty-subtitle text-muted">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{action && (
|
||||||
|
<div className="empty-action">
|
||||||
|
{action}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmptyState
|
||||||
|
|
||||||
|
|
||||||
@@ -90,9 +90,12 @@ export function useNotify() {
|
|||||||
|
|
||||||
// Единый тост-успех для мутаций (POST/DELETE/PUT/PATCH)
|
// Единый тост-успех для мутаций (POST/DELETE/PUT/PATCH)
|
||||||
export function notifyMutationSuccess(message, details) {
|
export function notifyMutationSuccess(message, details) {
|
||||||
|
// Тихий режим: по умолчанию успех показываем не тостом, а через inline-баннер на страницах.
|
||||||
|
// Оставляем возможность вручную вызвать тост при необходимости, если передан details?.forceToast === true
|
||||||
try {
|
try {
|
||||||
const text = message || 'Операция выполнена';
|
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);
|
window.notify.success(text, details);
|
||||||
}
|
}
|
||||||
} catch {}
|
} catch {}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
function PageHeader({ title, pretitle, actions, meta }) {
|
||||||
|
return (
|
||||||
|
<div className="page-header d-print-none mb-4">
|
||||||
|
<div className="row align-items-center">
|
||||||
|
<div className="col">
|
||||||
|
{pretitle && (
|
||||||
|
<div className="page-pretitle">{pretitle}</div>
|
||||||
|
)}
|
||||||
|
<h2 className="page-title">{title}</h2>
|
||||||
|
{meta && (
|
||||||
|
<div className="text-muted small mt-1">{meta}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{actions && (
|
||||||
|
<div className="col-auto ms-auto d-print-none">
|
||||||
|
{actions}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageHeader
|
||||||
|
|
||||||
|
|
||||||
@@ -5,7 +5,8 @@ import {
|
|||||||
IconDeviceFloppy,
|
IconDeviceFloppy,
|
||||||
IconPlayerPlay,
|
IconPlayerPlay,
|
||||||
IconBolt,
|
IconBolt,
|
||||||
IconEraser
|
IconEraser,
|
||||||
|
IconDots
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import PortalDropdown from './PortalDropdown.jsx';
|
import PortalDropdown from './PortalDropdown.jsx';
|
||||||
import { IconClock } from '@tabler/icons-react';
|
import { IconClock } from '@tabler/icons-react';
|
||||||
@@ -31,6 +32,7 @@ function PageHeaderActions({
|
|||||||
onOnlineUpdate,
|
onOnlineUpdate,
|
||||||
onBackgroundUpdate,
|
onBackgroundUpdate,
|
||||||
}) {
|
}) {
|
||||||
|
const secondaryButtons = Boolean(onImport || onExport || onClear || onClearCommunities || onHistory);
|
||||||
// Рендер плейсхолдеров во время загрузки/перезагрузки страницы
|
// Рендер плейсхолдеров во время загрузки/перезагрузки страницы
|
||||||
if (loading) {
|
if (loading) {
|
||||||
const showUpdateGroup = Boolean(onBackgroundUpdate || onOnlineUpdate);
|
const showUpdateGroup = Boolean(onBackgroundUpdate || onOnlineUpdate);
|
||||||
@@ -95,7 +97,7 @@ function PageHeaderActions({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<span className="vr d-none d-lg-inline" />
|
<span className="vr d-none d-lg-inline" />
|
||||||
<div className="btn-group">
|
<div className="btn-group d-none d-sm-inline-flex">
|
||||||
{onRefresh && (
|
{onRefresh && (
|
||||||
<button className="btn btn-outline-secondary" type="button" onClick={onRefresh} disabled={disableRefresh} title="Обновить данные">
|
<button className="btn btn-outline-secondary" type="button" onClick={onRefresh} disabled={disableRefresh} title="Обновить данные">
|
||||||
<IconRefresh className={loading ? 'spin me-1' : 'me-1'} /> Обновить
|
<IconRefresh className={loading ? 'spin me-1' : 'me-1'} /> Обновить
|
||||||
@@ -159,6 +161,48 @@ function PageHeaderActions({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{/* Компактное меню для маленьких экранов */}
|
||||||
|
{secondaryButtons && (
|
||||||
|
<div className="btn-group d-inline-flex d-sm-none">
|
||||||
|
<PortalDropdown
|
||||||
|
buttonClassName="btn btn-outline-secondary"
|
||||||
|
buttonProps={{ title: 'Ещё действия' }}
|
||||||
|
align="left"
|
||||||
|
buttonContent={<><IconDots className="me-1" /> Ещё</>}
|
||||||
|
>
|
||||||
|
{onRefresh && (
|
||||||
|
<button className="dropdown-item" type="button" onClick={onRefresh} disabled={disableRefresh}>
|
||||||
|
<IconRefresh className="me-1" /> Обновить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onImport && (
|
||||||
|
<button className="dropdown-item" type="button" onClick={onImport}>
|
||||||
|
<IconUpload className="me-1" /> Импорт
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onExport && (
|
||||||
|
<button className="dropdown-item" type="button" onClick={onExport} disabled={disableExport}>
|
||||||
|
<IconDownload className="me-1" /> Экспорт
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onClear && (
|
||||||
|
<button className="dropdown-item" type="button" onClick={onClear} disabled={disableClear}>
|
||||||
|
Очистить
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onClearCommunities && (
|
||||||
|
<button className="dropdown-item" type="button" onClick={onClearCommunities} disabled={disableClearCommunities}>
|
||||||
|
<IconEraser className="me-1" /> Очистить community
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{onHistory && (
|
||||||
|
<button className="dropdown-item" type="button" onClick={onHistory}>
|
||||||
|
История
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</PortalDropdown>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,4 +26,20 @@ export default function TableSkeleton({ rows = 10, cols = 3 }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function TableEmpty({ cols = 3, children }) {
|
||||||
|
return (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table card-table table-vcenter table-nowrap mb-0">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td colSpan={cols} className="text-center text-muted py-5">
|
||||||
|
{children}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,5 @@
|
|||||||
/* Using Bootstrap styles, this file can be empty or used for custom global styles. */
|
/* 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; }
|
||||||
Reference in New Issue
Block a user