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

This commit is contained in:
2025-08-11 16:37:10 +07:00
parent 3340571a6e
commit 415f05f6aa
5 changed files with 144 additions and 49 deletions
+134
View File
@@ -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>
);
}