feat: Добавить отображение версии UI в нижнем колонтитуле и улучшить отображение ASN и CIDR с помощью моноширинного шрифта в менеджерах ASNs и IPRanges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s

This commit is contained in:
2025-08-27 17:05:36 +07:00
parent 449f44b499
commit 001b4d9d98
6 changed files with 56 additions and 7 deletions
+1 -1
View File
@@ -578,7 +578,7 @@ function ASNsNewManager() {
<tbody>
{paginatedItems.map((item) => (
<tr key={item.asn} className={editingAsn === item.asn ? 'table-info' : ''}>
<td>{item.asn}</td>
<td className="font-monospace" title="ASN">{item.asn}</td>
<td>{renderCommunityBadge(item.community)}</td>
<td className="text-end">
{editingAsn === item.asn ? (
+5
View File
@@ -336,6 +336,11 @@ function MainLayout() {
<Route path="/" element={<Navigate to="/dashboard" replace />} />
</Routes>
</div>
<footer className="footer mt-auto py-3">
<div className="container-xl text-muted small">
Версия UI: {import.meta?.env?.VITE_APP_VERSION || 'dev'}
</div>
</footer>
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
</div>
);
+1 -1
View File
@@ -841,7 +841,7 @@ function IPRangesManager() {
<tbody>
{paginatedItems.map((item) => (
<tr key={item.ipRange} className={editingIp === item.ipRange ? 'table-info' : ''}>
<td>{item.ipRange}</td>
<td className="font-monospace" title="CIDR">{item.ipRange}</td>
<td>{renderCommunityBadge(item.community)}</td>
<td className="text-end">
{editingIp === item.ipRange ? (
@@ -9,6 +9,7 @@ import {
} from '@tabler/icons-react';
import PortalDropdown from './PortalDropdown.jsx';
import { IconClock } from '@tabler/icons-react';
import { formatDateTimeWithRelative } from '../lib/datetime.js';
function PageHeaderActions({
loading = false,
@@ -149,10 +150,10 @@ function PageHeaderActions({
<div className="btn-group">
<button className="btn btn-outline-secondary" type="button" onClick={onHistory} title="История версий">История</button>
{lastModified && (
<button className="btn btn-outline-secondary" type="button" onClick={onHistory} title={`Последние изменения: ${new Date(lastModified).toLocaleString()}`}>
<button className="btn btn-outline-secondary" type="button" onClick={onHistory} title={`Последние изменения: ${formatDateTimeWithRelative(lastModified)}`}>
<IconClock className="me-1" />
<span className="d-none d-sm-inline">{new Date(lastModified).toLocaleTimeString()}</span>
<span className="d-inline d-sm-none">{new Date(lastModified).toLocaleTimeString()}</span>
<span className="d-none d-sm-inline">{formatDateTimeWithRelative(lastModified)}</span>
<span className="d-inline d-sm-none">{formatDateTimeWithRelative(lastModified)}</span>
</button>
)}
</div>
+3 -2
View File
@@ -1,6 +1,7 @@
import { useEffect, useState } from 'react';
import api from '../lib/api.js';
import { IconHash, IconClock, IconFileText, IconRefresh } from '@tabler/icons-react';
import { formatDateTimeWithRelative } from '../lib/datetime.js';
function S3MetaBar({ keys = [], className = '' }) {
const [meta, setMeta] = useState({});
@@ -35,8 +36,8 @@ function S3MetaBar({ keys = [], className = '' }) {
const it = meta[k];
return (
<div key={k} className="d-flex align-items-center gap-2 flex-wrap">
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center"><IconHash size={14} className="me-1" />{it?.etag || '—'}</span>
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center"><IconClock size={14} className="me-1" />{it?.lastModified ? new Date(it.lastModified).toLocaleString() : '—'}</span>
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center" title="ETag"><IconHash size={14} className="me-1" /><span className="font-monospace">{it?.etag || '—'}</span></span>
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center" title="Время последнего изменения"><IconClock size={14} className="me-1" />{it?.lastModified ? formatDateTimeWithRelative(it.lastModified) : '—'}</span>
<span className="badge bg-blue-lt text-blue d-inline-flex align-items-center"><IconFileText size={14} className="me-1" />{typeof it?.contentLength === 'number' ? `${it.contentLength} байт` : '—'}</span>
<button className="btn btn-outline-secondary btn-sm" type="button" onClick={() => copyMeta(k)} title="Копировать ETag/Last-Modified">
<IconFileText size={14} className="me-1" />Копировать
+42
View File
@@ -0,0 +1,42 @@
function pad(num) {
return String(num).padStart(2, '0');
}
export function formatDateTime(input) {
if (!input) return '';
const d = input instanceof Date ? input : new Date(input);
if (Number.isNaN(d.getTime())) return '';
const day = pad(d.getDate());
const month = pad(d.getMonth() + 1);
const year = d.getFullYear();
const h = pad(d.getHours());
const m = pad(d.getMinutes());
const s = pad(d.getSeconds());
return `${day}.${month}.${year} ${h}:${m}:${s}`;
}
export function formatRelative(input) {
if (!input) return '';
const d = input instanceof Date ? input : new Date(input);
if (Number.isNaN(d.getTime())) return '';
const diffMs = Date.now() - d.getTime();
const sec = Math.floor(diffMs / 1000);
if (sec < 5) return 'только что';
if (sec < 60) return `${sec} сек назад`;
const min = Math.floor(sec / 60);
if (min < 60) return `${min} мин назад`;
const hr = Math.floor(min / 60);
if (hr < 24) return `${hr} ч назад`;
const days = Math.floor(hr / 24);
return `${days} дн назад`;
}
export function formatDateTimeWithRelative(input) {
const abs = formatDateTime(input);
const rel = formatRelative(input);
if (!abs && !rel) return '';
if (abs && rel) return `${abs} (${rel})`;
return abs || rel;
}