feat: Добавить отображение версии UI в нижнем колонтитуле и улучшить отображение ASN и CIDR с помощью моноширинного шрифта в менеджерах ASNs и IPRanges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m51s
This commit is contained in:
@@ -578,7 +578,7 @@ function ASNsNewManager() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{paginatedItems.map((item) => (
|
{paginatedItems.map((item) => (
|
||||||
<tr key={item.asn} className={editingAsn === item.asn ? 'table-info' : ''}>
|
<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>{renderCommunityBadge(item.community)}</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
{editingAsn === item.asn ? (
|
{editingAsn === item.asn ? (
|
||||||
|
|||||||
@@ -336,6 +336,11 @@ function MainLayout() {
|
|||||||
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</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)} />
|
<SettingsModal open={settingsOpen} onClose={() => setSettingsOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -841,7 +841,7 @@ function IPRangesManager() {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{paginatedItems.map((item) => (
|
{paginatedItems.map((item) => (
|
||||||
<tr key={item.ipRange} className={editingIp === item.ipRange ? 'table-info' : ''}>
|
<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>{renderCommunityBadge(item.community)}</td>
|
||||||
<td className="text-end">
|
<td className="text-end">
|
||||||
{editingIp === item.ipRange ? (
|
{editingIp === item.ipRange ? (
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
import PortalDropdown from './PortalDropdown.jsx';
|
import PortalDropdown from './PortalDropdown.jsx';
|
||||||
import { IconClock } from '@tabler/icons-react';
|
import { IconClock } from '@tabler/icons-react';
|
||||||
|
import { formatDateTimeWithRelative } from '../lib/datetime.js';
|
||||||
|
|
||||||
function PageHeaderActions({
|
function PageHeaderActions({
|
||||||
loading = false,
|
loading = false,
|
||||||
@@ -149,10 +150,10 @@ function PageHeaderActions({
|
|||||||
<div className="btn-group">
|
<div className="btn-group">
|
||||||
<button className="btn btn-outline-secondary" type="button" onClick={onHistory} title="История версий">История</button>
|
<button className="btn btn-outline-secondary" type="button" onClick={onHistory} title="История версий">История</button>
|
||||||
{lastModified && (
|
{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" />
|
<IconClock className="me-1" />
|
||||||
<span className="d-none d-sm-inline">{new Date(lastModified).toLocaleTimeString()}</span>
|
<span className="d-none d-sm-inline">{formatDateTimeWithRelative(lastModified)}</span>
|
||||||
<span className="d-inline d-sm-none">{new Date(lastModified).toLocaleTimeString()}</span>
|
<span className="d-inline d-sm-none">{formatDateTimeWithRelative(lastModified)}</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import api from '../lib/api.js';
|
import api from '../lib/api.js';
|
||||||
import { IconHash, IconClock, IconFileText, IconRefresh } from '@tabler/icons-react';
|
import { IconHash, IconClock, IconFileText, IconRefresh } from '@tabler/icons-react';
|
||||||
|
import { formatDateTimeWithRelative } from '../lib/datetime.js';
|
||||||
|
|
||||||
function S3MetaBar({ keys = [], className = '' }) {
|
function S3MetaBar({ keys = [], className = '' }) {
|
||||||
const [meta, setMeta] = useState({});
|
const [meta, setMeta] = useState({});
|
||||||
@@ -35,8 +36,8 @@ function S3MetaBar({ keys = [], className = '' }) {
|
|||||||
const it = meta[k];
|
const it = meta[k];
|
||||||
return (
|
return (
|
||||||
<div key={k} className="d-flex align-items-center gap-2 flex-wrap">
|
<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" 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"><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="Время последнего изменения"><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>
|
<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">
|
<button className="btn btn-outline-secondary btn-sm" type="button" onClick={() => copyMeta(k)} title="Копировать ETag/Last-Modified">
|
||||||
<IconFileText size={14} className="me-1" />Копировать
|
<IconFileText size={14} className="me-1" />Копировать
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user