feat: Integrate NotifyProvider into App component and update ASNs, Domains, and IPRanges managers to display S3MetaBar with improved comments for clarity
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m41s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m41s
This commit is contained in:
@@ -547,7 +547,7 @@ function ASNsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<S3MetaBar keys={["asns"]} />
|
{/* Мета-информация будет показана внизу карточки */}
|
||||||
<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">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -641,21 +641,7 @@ function ASNsNewManager() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(etag || lastModified || contentLength !== null) && (
|
<S3MetaBar keys={["asns"]} />
|
||||||
<div className="card-footer">
|
|
||||||
<div className="d-flex flex-wrap gap-2 align-items-center small">
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconHash size={14} className="me-1" /> {etag || '—'}
|
|
||||||
</span>
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconClock size={14} className="me-1" /> {lastModified || '—'}
|
|
||||||
</span>
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconFileText size={14} className="me-1" /> {(contentLength ?? '—') + (contentLength !== null ? ' байт' : '')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import BillingManager from './BillingManager';
|
|||||||
import CommunitiesManager from './CommunitiesManager';
|
import CommunitiesManager from './CommunitiesManager';
|
||||||
import Dashboard from './Dashboard';
|
import Dashboard from './Dashboard';
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
import { NotifyProvider } from './components/NotifyProvider.jsx';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// --- Simple i18n (RU/EN) ---
|
// --- Simple i18n (RU/EN) ---
|
||||||
@@ -81,7 +82,9 @@ function App() {
|
|||||||
<Router>
|
<Router>
|
||||||
<LanguageProvider>
|
<LanguageProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<MainLayout />
|
<NotifyProvider>
|
||||||
|
<MainLayout />
|
||||||
|
</NotifyProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</LanguageProvider>
|
</LanguageProvider>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -555,7 +555,7 @@ function DomainsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<S3MetaBar keys={["domainsNew"]} />
|
{/* Мета-информация будет показана внизу карточки */}
|
||||||
<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">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -649,21 +649,7 @@ function DomainsNewManager() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(etag || lastModified || contentLength !== null) && (
|
<S3MetaBar keys={["domainsNew"]} />
|
||||||
<div className="card-footer">
|
|
||||||
<div className="d-flex flex-wrap gap-2 align-items-center small">
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconHash size={14} className="me-1" /> {etag || '—'}
|
|
||||||
</span>
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconClock size={14} className="me-1" /> {lastModified || '—'}
|
|
||||||
</span>
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconFileText size={14} className="me-1" /> {(contentLength ?? '—') + (contentLength !== null ? ' байт' : '')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -563,7 +563,7 @@ function IPRangesManager() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<S3MetaBar keys={["ipRanges"]} />
|
{/* Мета-информация будет показана внизу карточки */}
|
||||||
<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">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -662,21 +662,7 @@ function IPRangesManager() {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{(etag || lastModified || contentLength !== null) && (
|
<S3MetaBar keys={["ipRanges"]} />
|
||||||
<div className="card-footer">
|
|
||||||
<div className="d-flex flex-wrap gap-2 align-items-center small">
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconHash size={14} className="me-1" /> {etag || '—'}
|
|
||||||
</span>
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconClock size={14} className="me-1" /> {lastModified || '—'}
|
|
||||||
</span>
|
|
||||||
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center">
|
|
||||||
<IconFileText size={14} className="me-1" /> {(contentLength ?? '—') + (contentLength !== null ? ' байт' : '')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import { IconCheck, IconAlertTriangle, IconInfoCircle, IconX } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
const NotifyContext = createContext({
|
||||||
|
add: () => {},
|
||||||
|
success: () => {},
|
||||||
|
error: () => {},
|
||||||
|
info: () => {},
|
||||||
|
warning: () => {},
|
||||||
|
remove: () => {},
|
||||||
|
clear: () => {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const DEFAULT_TIMEOUT = {
|
||||||
|
success: 3000,
|
||||||
|
info: 4000,
|
||||||
|
warning: 5000,
|
||||||
|
error: 7000,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function NotifyProvider({ children }) {
|
||||||
|
const [items, setItems] = useState([]); // { id, type, message, count, createdAt }
|
||||||
|
const timeouts = useRef(new Map());
|
||||||
|
const idSeq = useRef(1);
|
||||||
|
|
||||||
|
const remove = useCallback((id) => {
|
||||||
|
setItems((prev) => prev.filter((n) => n.id !== id));
|
||||||
|
const t = timeouts.current.get(id);
|
||||||
|
if (t) { clearTimeout(t); timeouts.current.delete(id); }
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const schedule = useCallback((id, type) => {
|
||||||
|
const dur = DEFAULT_TIMEOUT[type] ?? 4000;
|
||||||
|
const old = timeouts.current.get(id);
|
||||||
|
if (old) clearTimeout(old);
|
||||||
|
const t = setTimeout(() => remove(id), dur);
|
||||||
|
timeouts.current.set(id, t);
|
||||||
|
}, [remove]);
|
||||||
|
|
||||||
|
const add = useCallback((type, message) => {
|
||||||
|
const text = String(message || '').trim();
|
||||||
|
if (!text) return;
|
||||||
|
setItems((prev) => {
|
||||||
|
const dup = prev.find((n) => n.type === type && n.message === text);
|
||||||
|
if (dup) {
|
||||||
|
const updated = prev.map((n) => n.id === dup.id ? { ...n, count: (n.count || 1) + 1, createdAt: Date.now() } : n);
|
||||||
|
schedule(dup.id, type);
|
||||||
|
return updated;
|
||||||
|
}
|
||||||
|
const id = idSeq.current++;
|
||||||
|
const next = [...prev, { id, type, message: text, count: 1, createdAt: Date.now() }];
|
||||||
|
schedule(id, type);
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}, [schedule]);
|
||||||
|
|
||||||
|
const clear = useCallback(() => {
|
||||||
|
setItems([]);
|
||||||
|
for (const t of timeouts.current.values()) clearTimeout(t);
|
||||||
|
timeouts.current.clear();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const api = useMemo(() => ({
|
||||||
|
add,
|
||||||
|
success: (m) => add('success', m),
|
||||||
|
error: (m) => add('error', m),
|
||||||
|
info: (m) => add('info', m),
|
||||||
|
warning: (m) => add('warning', m),
|
||||||
|
remove,
|
||||||
|
clear,
|
||||||
|
}), [add, remove, clear]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Опционально доступ из консоли / старого кода
|
||||||
|
window.notify = api;
|
||||||
|
return () => { if (window.notify === api) delete window.notify; };
|
||||||
|
}, [api]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NotifyContext.Provider value={api}>
|
||||||
|
{children}
|
||||||
|
<NotifyViewport items={items} onClose={remove} />
|
||||||
|
</NotifyContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotify() {
|
||||||
|
return useContext(NotifyContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
function colorByType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return 'success';
|
||||||
|
case 'error': return 'danger';
|
||||||
|
case 'warning': return 'warning';
|
||||||
|
default: return 'info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function iconByType(type) {
|
||||||
|
switch (type) {
|
||||||
|
case 'success': return <IconCheck className="me-2" />;
|
||||||
|
case 'warning': return <IconAlertTriangle className="me-2" />;
|
||||||
|
case 'error': return <IconAlertTriangle className="me-2" />;
|
||||||
|
default: return <IconInfoCircle className="me-2" />;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function NotifyViewport({ items, onClose }) {
|
||||||
|
return (
|
||||||
|
<div className="position-fixed top-0 end-0 p-3" style={{ zIndex: 1080, pointerEvents: 'none' }}>
|
||||||
|
<div className="d-flex flex-column gap-2 align-items-end">
|
||||||
|
{items.map((n) => (
|
||||||
|
<div key={n.id} className={`alert alert-${colorByType(n.type)} alert-dismissible shadow-sm`} role="alert" style={{ minWidth: 320, pointerEvents: 'auto' }}>
|
||||||
|
<div className="d-flex align-items-start">
|
||||||
|
<div className="me-1 mt-1">{iconByType(n.type)}</div>
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
{n.message}
|
||||||
|
{n.count > 1 && (
|
||||||
|
<span className="badge bg-white text-body border ms-2">×{n.count}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button type="button" className="btn-close" aria-label="Close" onClick={() => onClose(n.id)}>
|
||||||
|
<IconX size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user