feat: Implement date formatting utility functions and update components to use formatted timestamps for improved readability and consistency across the application.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m47s

This commit is contained in:
2025-12-24 15:52:01 +07:00
parent 6e7a63dd28
commit 27dade3eda
20 changed files with 1832 additions and 1045 deletions
@@ -0,0 +1,267 @@
import { useState, useEffect, useRef } from 'react';
import { IconPlus } from '@tabler/icons-react';
import {
makeGateway,
normalizeGateways,
needsGateways as NEEDS_GATEWAYS,
SERVER_TYPE_OPTIONS
} from '../../utils/serverUtils.js';
/**
* Модальное окно для редактирования сервера
*/
function EditServerModal({ show, server, onChange, onSave, onClose }) {
const modalRef = useRef(null);
const initializedRef = useRef(false);
const [localServer, setLocalServer] = useState({});
const ensureGateways = (srv) => {
if (!srv) return { gateways: [] };
const needs = NEEDS_GATEWAYS(srv.type);
let gateways = [];
if (needs) {
gateways = normalizeGateways(srv.gateways, srv.gateway || srv.dns || srv.ip);
if (gateways.length === 0) {
gateways = [makeGateway({ primary: true })];
}
}
return { ...srv, gateways };
};
useEffect(() => {
if (show && server && !initializedRef.current) {
const prepared = ensureGateways(server);
setLocalServer(prepared);
initializedRef.current = true;
}
if (!show) {
initializedRef.current = false;
}
}, [show, server]);
useEffect(() => {
if (window.Tabler && window.Tabler.Modal && modalRef.current) {
const modalInstance = window.Tabler.Modal.getOrCreateInstance(modalRef.current);
if (show) {
modalInstance.show();
} else {
modalInstance.hide();
}
const handler = () => onClose && onClose();
modalRef.current.addEventListener('hide.bs.modal', handler);
return () => {
if (modalRef.current) {
modalRef.current.removeEventListener('hide.bs.modal', handler);
}
};
}
}, [show, onClose]);
const handleFieldChange = (field, value) => {
setLocalServer(prev => {
const updated = { ...prev, [field]: value };
onChange && onChange(updated);
return updated;
});
};
const handleGatewayChange = (idx, field, value) => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
const updatedGateways = currentGateways.map((gw, i) =>
i === idx ? { ...gw, [field]: value } : gw
);
const updated = { ...prev, gateways: updatedGateways };
onChange && onChange(updated);
return updated;
});
};
const handlePrimaryChange = (idx, checked) => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
const updatedGateways = currentGateways.map((gw, i) => ({
...gw,
primary: i === idx ? checked : false
}));
if (!updatedGateways.some(g => g.primary) && updatedGateways.length > 0) {
updatedGateways[idx].primary = true;
}
const updated = { ...prev, gateways: updatedGateways };
onChange && onChange(updated);
return updated;
});
};
const handleAddGateway = () => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
const newGw = makeGateway({ primary: currentGateways.length === 0 });
const updatedGateways = [...currentGateways, newGw];
const updated = { ...prev, gateways: updatedGateways };
onChange && onChange(updated);
return updated;
});
};
const handleRemoveGateway = (idx) => {
setLocalServer(prev => {
const currentGateways = Array.isArray(prev.gateways) ? prev.gateways : [];
if (currentGateways.length <= 1) return prev;
const updatedGateways = currentGateways.filter((_, i) => i !== idx);
if (!updatedGateways.some(g => g.primary) && updatedGateways.length > 0) {
updatedGateways[0].primary = true;
}
const updated = { ...prev, gateways: updatedGateways };
onChange && onChange(updated);
return updated;
});
};
const currentGateways = Array.isArray(localServer.gateways) ? localServer.gateways : [];
const showGateways = NEEDS_GATEWAYS(localServer.type);
return (
<div className="modal" tabIndex="-1" ref={modalRef}>
<div className="modal-dialog">
<div className="modal-content">
<div className="modal-header">
<h5 className="modal-title">Редактировать сервер</h5>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
<form onSubmit={e => { e.preventDefault(); onSave && onSave(localServer); }}>
<div className="mb-3">
<label className="form-label">ID (опционально)</label>
<input type="text" className="form-control" value={localServer.id || ''} onChange={e => handleFieldChange('id', e.target.value)} placeholder="srv-1" />
</div>
<div className="mb-3">
<label className="form-label">IP адрес</label>
<input type="text" className="form-control" value={localServer.ip || ''} onChange={e => handleFieldChange('ip', e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">Внешний IP (опционально)</label>
<input type="text" className="form-control" value={localServer.extIp || ''} onChange={e => handleFieldChange('extIp', e.target.value)} placeholder="публичный IP, если отличается" />
</div>
<div className="mb-3">
<label className="form-label">Внутренний IP (опционально)</label>
<input type="text" className="form-control" value={localServer.internalIp || ''} onChange={e => handleFieldChange('internalIp', e.target.value)} placeholder="10.x.x.x / 192.168.x.x" />
</div>
<div className="mb-3">
<label className="form-label">DNS имя</label>
<input type="text" className="form-control" value={localServer.dns || ''} onChange={e => handleFieldChange('dns', e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">Страна</label>
<input type="text" className="form-control" value={localServer.country || ''} onChange={e => handleFieldChange('country', e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">Провайдер</label>
<input type="text" className="form-control" value={localServer.provider || ''} onChange={e => handleFieldChange('provider', e.target.value)} />
</div>
<div className="mb-3">
<label className="form-label">Тип сервера</label>
<select className="form-select" value={localServer.type || 'jumphost'} onChange={e => handleFieldChange('type', e.target.value)}>
{SERVER_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>{opt.label}</option>
))}
</select>
</div>
<div className="mb-3">
<label className="form-label">Тип туннеля</label>
<input type="text" className="form-control" value={localServer.tunnel || ''} onChange={e => handleFieldChange('tunnel', e.target.value)} />
</div>
{showGateways && (
<>
<div className="form-label">Шлюзы ({currentGateways.length})</div>
<div className="list-group list-group-flush border rounded mb-2">
{currentGateways.map((gw, idx) => (
<div key={gw.id || `gw-${idx}`} className="list-group-item">
<div className="row g-2 align-items-end">
<div className="col-12 col-md-4">
<label className="form-label small mb-1">Имя *</label>
<input
type="text"
className="form-control"
value={gw.name || ''}
onChange={(e) => handleGatewayChange(idx, 'name', e.target.value)}
placeholder="GW-MSK"
required
/>
</div>
<div className="col-12 col-md-4">
<label className="form-label small mb-1">IP</label>
<input
type="text"
className="form-control"
value={gw.ip || ''}
onChange={(e) => handleGatewayChange(idx, 'ip', e.target.value)}
placeholder="10.0.0.1"
/>
</div>
<div className="col-12 col-md-3">
<label className="form-label small mb-1">Комментарий</label>
<input
type="text"
className="form-control"
value={gw.comment || ''}
onChange={(e) => handleGatewayChange(idx, 'comment', e.target.value)}
placeholder="Основной шлюз"
/>
</div>
<div className="col-12 col-md-1 d-flex align-items-center justify-content-center">
<div className="form-check form-switch m-0">
<input
className="form-check-input"
type="checkbox"
checked={!!gw.primary}
onChange={(e) => handlePrimaryChange(idx, e.target.checked)}
title="Основной шлюз"
/>
</div>
</div>
</div>
<div className="d-flex justify-content-between align-items-center mt-2">
<div className="text-muted small">{gw.primary ? 'Основной шлюз' : 'Резервный шлюз'}</div>
{currentGateways.length > 1 && (
<button
type="button"
className="btn btn-link text-danger px-0"
onClick={() => handleRemoveGateway(idx)}
>
Удалить
</button>
)}
</div>
</div>
))}
</div>
<button
type="button"
className="btn btn-outline-primary btn-sm"
onClick={handleAddGateway}
>
<IconPlus className="icon" />
Добавить шлюз
</button>
<div className="form-text">Для jumphost/exit должен быть один основной gateway.</div>
</>
)}
</form>
</div>
<div className="modal-footer">
<button type="button" className="btn btn-secondary d-flex align-items-center" data-bs-dismiss="modal">
<span className="fw-bold">Отмена</span>
</button>
<button type="button" className="btn btn-primary d-flex align-items-center" onClick={() => onSave && onSave(localServer)}>
<span className="fw-bold">Сохранить</span>
</button>
</div>
</div>
</div>
</div>
);
}
export default EditServerModal;