feat: Добавить уведомления об успешных мутациях и глобальные горячие клавиши для сохранения изменений в менеджерах ASNs, Domains и IPRanges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s
This commit is contained in:
@@ -17,7 +17,6 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import LockBanner from './components/LockBanner.jsx';
|
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton 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';
|
||||||
@@ -25,6 +24,8 @@ 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';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
|
import ErrorAlert from './components/ErrorAlert.jsx';
|
||||||
|
import { notifyMutationSuccess } from './components/NotifyProvider.jsx';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -261,6 +262,7 @@ function ASNsNewManager() {
|
|||||||
setItems(unique);
|
setItems(unique);
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
notifyMutationSuccess('Изменения сохранены');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving changes:', error);
|
console.error('Error saving changes:', error);
|
||||||
setError('Не удалось сохранить изменения.');
|
setError('Не удалось сохранить изменения.');
|
||||||
@@ -370,6 +372,18 @@ function ASNsNewManager() {
|
|||||||
}
|
}
|
||||||
}, [editingAsn]);
|
}, [editingAsn]);
|
||||||
|
|
||||||
|
// Глобальные хоткеи: Ctrl+S — сохранить изменения
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!loading) handleSaveChanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
|
||||||
|
|
||||||
const handleEditKeyDown = (e, asn) => {
|
const handleEditKeyDown = (e, asn) => {
|
||||||
if (e.key === 'Enter') handleSaveEdit(asn);
|
if (e.key === 'Enter') handleSaveEdit(asn);
|
||||||
if (e.key === 'Escape') handleCancelEdit();
|
if (e.key === 'Escape') handleCancelEdit();
|
||||||
@@ -378,17 +392,7 @@ function ASNsNewManager() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
<ErrorAlert message={error} onClose={() => setError('')} />
|
||||||
<div className="d-flex">
|
|
||||||
<div>
|
|
||||||
<IconX className="icon alert-icon" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
@@ -434,7 +438,7 @@ function ASNsNewManager() {
|
|||||||
if (!res.ok || data.ok === false) {
|
if (!res.ok || data.ok === false) {
|
||||||
throw new Error(data?.message || `HTTP ${res.status}`);
|
throw new Error(data?.message || `HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
window.notify?.success('Фоновое обновление запущено', data);
|
notifyMutationSuccess('Фоновое обновление запущено', data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||||
}
|
}
|
||||||
@@ -445,7 +449,6 @@ function ASNsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-3">
|
<div className="col-lg-3">
|
||||||
<LockBanner resource="asns" className="mb-3" />
|
|
||||||
{/* Add New ASN Card */}
|
{/* Add New ASN Card */}
|
||||||
<div className="card card-md">
|
<div className="card card-md">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@@ -466,6 +469,7 @@ function ASNsNewManager() {
|
|||||||
onChange={(e) => setNewItem({ ...newItem, asn: e.target.value })}
|
onChange={(e) => setNewItem({ ...newItem, asn: e.target.value })}
|
||||||
/>
|
/>
|
||||||
{newInvalid.asn && <div className="invalid-feedback">Только цифры</div>}
|
{newInvalid.asn && <div className="invalid-feedback">Только цифры</div>}
|
||||||
|
<div className="form-text">Формат: только цифры, например 12345</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">Community</label>
|
<label className="form-label">Community</label>
|
||||||
@@ -477,6 +481,7 @@ function ASNsNewManager() {
|
|||||||
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
||||||
/>
|
/>
|
||||||
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
||||||
|
<div className="form-text">Формат: N или N:N (например 65000:100)</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const match = communities.find(c => c.value === String(newItem.community).trim());
|
const match = communities.find(c => c.value === String(newItem.community).trim());
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@@ -582,6 +587,7 @@ function ASNsNewManager() {
|
|||||||
value={editingValue}
|
value={editingValue}
|
||||||
onChange={setEditingValue}
|
onChange={setEditingValue}
|
||||||
communities={communities}
|
communities={communities}
|
||||||
|
onKeyDown={(e) => handleEditKeyDown(e, item.asn)}
|
||||||
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.asn)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
|
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.asn)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import LockBanner from './components/LockBanner.jsx';
|
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton 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';
|
||||||
@@ -25,6 +24,8 @@ 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';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
|
import ErrorAlert from './components/ErrorAlert.jsx';
|
||||||
|
import { notifyMutationSuccess } from './components/NotifyProvider.jsx';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -265,6 +266,7 @@ function DomainsNewManager() {
|
|||||||
setItems(unique);
|
setItems(unique);
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
notifyMutationSuccess('Изменения сохранены');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving changes:', error);
|
console.error('Error saving changes:', error);
|
||||||
setError('Не удалось сохранить изменения.');
|
setError('Не удалось сохранить изменения.');
|
||||||
@@ -375,6 +377,18 @@ function DomainsNewManager() {
|
|||||||
}
|
}
|
||||||
}, [editingDomain]);
|
}, [editingDomain]);
|
||||||
|
|
||||||
|
// Глобальные хоткеи: Ctrl+S — сохранить изменения
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!loading) handleSaveChanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
|
||||||
|
|
||||||
const handleEditKeyDown = (e, domainName) => {
|
const handleEditKeyDown = (e, domainName) => {
|
||||||
if (e.key === 'Enter') handleSaveEdit(domainName);
|
if (e.key === 'Enter') handleSaveEdit(domainName);
|
||||||
if (e.key === 'Escape') handleCancelEdit();
|
if (e.key === 'Escape') handleCancelEdit();
|
||||||
@@ -383,17 +397,7 @@ function DomainsNewManager() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
<ErrorAlert message={error} onClose={() => setError('')} />
|
||||||
<div className="d-flex">
|
|
||||||
<div>
|
|
||||||
<IconX className="icon alert-icon" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
@@ -439,7 +443,7 @@ function DomainsNewManager() {
|
|||||||
if (!res.ok || data.ok === false) {
|
if (!res.ok || data.ok === false) {
|
||||||
throw new Error(data?.message || `HTTP ${res.status}`);
|
throw new Error(data?.message || `HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
window.notify?.success('Фоновое обновление запущено', data);
|
notifyMutationSuccess('Фоновое обновление запущено', data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||||
}
|
}
|
||||||
@@ -450,7 +454,6 @@ function DomainsNewManager() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-3">
|
<div className="col-lg-3">
|
||||||
<LockBanner resource="domains-new" className="mb-3" />
|
|
||||||
{/* Add New Item Card */}
|
{/* Add New Item Card */}
|
||||||
<div className="card card-md">
|
<div className="card card-md">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@@ -472,6 +475,7 @@ function DomainsNewManager() {
|
|||||||
/>
|
/>
|
||||||
{newInvalid.domain && <div className="invalid-feedback">Неверный формат домена</div>}
|
{newInvalid.domain && <div className="invalid-feedback">Неверный формат домена</div>}
|
||||||
<div className="form-text">Без http/https, только доменное имя</div>
|
<div className="form-text">Без http/https, только доменное имя</div>
|
||||||
|
<div className="form-text">Формат: sub.domain.tld (например foo.example.org)</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">Community</label>
|
<label className="form-label">Community</label>
|
||||||
@@ -483,6 +487,7 @@ function DomainsNewManager() {
|
|||||||
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
|
||||||
/>
|
/>
|
||||||
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
|
||||||
|
<div className="form-text">Формат: N или N:N (например 65000:100)</div>
|
||||||
{(() => {
|
{(() => {
|
||||||
const match = communities.find(c => c.value === String(newItem.community).trim());
|
const match = communities.find(c => c.value === String(newItem.community).trim());
|
||||||
if (!match) return null;
|
if (!match) return null;
|
||||||
@@ -588,6 +593,7 @@ function DomainsNewManager() {
|
|||||||
value={editingValue}
|
value={editingValue}
|
||||||
onChange={setEditingValue}
|
onChange={setEditingValue}
|
||||||
communities={communities}
|
communities={communities}
|
||||||
|
onKeyDown={(e) => handleEditKeyDown(e, item.domain)}
|
||||||
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.domain)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
|
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.domain)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
IconClock,
|
IconClock,
|
||||||
IconFileText
|
IconFileText
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import LockBanner from './components/LockBanner.jsx';
|
|
||||||
import TableSkeleton from './components/TableSkeleton.jsx';
|
import TableSkeleton 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';
|
||||||
@@ -25,6 +24,8 @@ 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';
|
||||||
import HistoryModal from './components/HistoryModal.jsx';
|
import HistoryModal from './components/HistoryModal.jsx';
|
||||||
|
import ErrorAlert from './components/ErrorAlert.jsx';
|
||||||
|
import { notifyMutationSuccess } from './components/NotifyProvider.jsx';
|
||||||
|
|
||||||
const API_URL = '/api';
|
const API_URL = '/api';
|
||||||
|
|
||||||
@@ -282,6 +283,7 @@ function IPRangesManager() {
|
|||||||
setItems(unique);
|
setItems(unique);
|
||||||
setSuccess('Изменения успешно сохранены!');
|
setSuccess('Изменения успешно сохранены!');
|
||||||
setTimeout(() => setSuccess(''), 3000);
|
setTimeout(() => setSuccess(''), 3000);
|
||||||
|
notifyMutationSuccess('Изменения сохранены');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error saving changes:', error);
|
console.error('Error saving changes:', error);
|
||||||
setError('Не удалось сохранить изменения.');
|
setError('Не удалось сохранить изменения.');
|
||||||
@@ -390,6 +392,17 @@ function IPRangesManager() {
|
|||||||
editInputRef.current.focus();
|
editInputRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [editingIp]);
|
}, [editingIp]);
|
||||||
|
// Глобальные хоткеи: Ctrl+S — сохранить изменения
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e) => {
|
||||||
|
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!loading) handleSaveChanges();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener('keydown', onKey);
|
||||||
|
return () => window.removeEventListener('keydown', onKey);
|
||||||
|
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
|
||||||
|
|
||||||
const handleEditKeyDown = (e, ipRange) => {
|
const handleEditKeyDown = (e, ipRange) => {
|
||||||
if (e.key === 'Enter') handleSaveEdit(ipRange);
|
if (e.key === 'Enter') handleSaveEdit(ipRange);
|
||||||
@@ -638,17 +651,7 @@ function IPRangesManager() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{error && (
|
{error && (
|
||||||
<div className="alert alert-danger alert-dismissible" role="alert">
|
<ErrorAlert message={error} onClose={() => setError('')} />
|
||||||
<div className="d-flex">
|
|
||||||
<div>
|
|
||||||
<IconX className="icon alert-icon" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button type="button" className="btn-close" onClick={() => setError('')}></button>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{success && (
|
{success && (
|
||||||
@@ -694,7 +697,7 @@ function IPRangesManager() {
|
|||||||
if (!res.ok || data.ok === false) {
|
if (!res.ok || data.ok === false) {
|
||||||
throw new Error(data?.message || `HTTP ${res.status}`);
|
throw new Error(data?.message || `HTTP ${res.status}`);
|
||||||
}
|
}
|
||||||
window.notify?.success('Фоновое обновление запущено', data);
|
notifyMutationSuccess('Фоновое обновление запущено', data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
|
||||||
}
|
}
|
||||||
@@ -705,7 +708,6 @@ function IPRangesManager() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-lg-3">
|
<div className="col-lg-3">
|
||||||
<LockBanner resource="ip-ranges" className="mb-3" />
|
|
||||||
{/* Add New Item Card */}
|
{/* Add New Item Card */}
|
||||||
<div className="card card-md">
|
<div className="card card-md">
|
||||||
<div className="card-header">
|
<div className="card-header">
|
||||||
@@ -726,6 +728,7 @@ function IPRangesManager() {
|
|||||||
onChange={(e) => setNewItem({ ...newItem, ipRange: e.target.value })}
|
onChange={(e) => setNewItem({ ...newItem, ipRange: e.target.value })}
|
||||||
/>
|
/>
|
||||||
{newInvalid.ipRange && <div className="invalid-feedback">Некорректный CIDR</div>}
|
{newInvalid.ipRange && <div className="invalid-feedback">Некорректный CIDR</div>}
|
||||||
|
<div className="form-text">Формат: A.B.C.D/Маска, например 10.0.0.0/24</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<label className="form-label">Community</label>
|
<label className="form-label">Community</label>
|
||||||
@@ -848,6 +851,7 @@ function IPRangesManager() {
|
|||||||
onChange={setEditingValue}
|
onChange={setEditingValue}
|
||||||
communities={communities}
|
communities={communities}
|
||||||
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||||
|
onKeyDown={(e) => handleEditKeyDown(e, item.ipRange)}
|
||||||
/>
|
/>
|
||||||
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.ipRange)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
|
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.ipRange)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>
|
||||||
<button className="btn btn-secondary btn-icon btn-sm" onClick={handleCancelEdit}><IconX size={16} /></button>
|
<button className="btn btn-secondary btn-icon btn-sm" onClick={handleCancelEdit}><IconX size={16} /></button>
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { IconAlertTriangle, IconX } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
function ErrorAlert({ message, details, onClose }) {
|
||||||
|
if (!message) return null;
|
||||||
|
return (
|
||||||
|
<div className="alert alert-danger alert-dismissible" role="alert">
|
||||||
|
<div className="d-flex">
|
||||||
|
<div>
|
||||||
|
<IconAlertTriangle className="icon alert-icon" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow-1">
|
||||||
|
{String(message)}
|
||||||
|
{details ? (
|
||||||
|
<details className="small mt-1">
|
||||||
|
<summary>Показать детали</summary>
|
||||||
|
<pre className="mb-0 mt-1" style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{typeof details === 'string' ? details : JSON.stringify(details, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</details>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="button" className="btn-close" onClick={onClose} aria-label="Закрыть">
|
||||||
|
<IconX size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorAlert;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -88,6 +88,16 @@ export function useNotify() {
|
|||||||
return useContext(NotifyContext);
|
return useContext(NotifyContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Единый тост-успех для мутаций (POST/DELETE/PUT/PATCH)
|
||||||
|
export function notifyMutationSuccess(message, details) {
|
||||||
|
try {
|
||||||
|
const text = message || 'Операция выполнена';
|
||||||
|
if (typeof window !== 'undefined' && window.notify?.success) {
|
||||||
|
window.notify.success(text, details);
|
||||||
|
}
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
function colorByType(type) {
|
function colorByType(type) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'success': return 'success';
|
case 'success': return 'success';
|
||||||
|
|||||||
Reference in New Issue
Block a user