fix(admin): UX Control Center — даты Inbox, bulk, фильтры событий, палитра по ID
Refs EventHub/EventHubFrontAdmin#32 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Search } from 'lucide-react';
|
||||
import { useAuthStore } from '@/store/authStore';
|
||||
import { NAV_GROUPS, filterNavGroupsByRole } from '@/config/adminAccess';
|
||||
import { NAV_GROUPS, filterNavGroupsByRole, canAccessRoute } from '@/config/adminAccess';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -24,6 +24,9 @@ interface CommandPaletteProps {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
/** Looks like EventHub entity ids (base64url-ish). */
|
||||
const ENTITY_ID_RE = /^[A-Za-z0-9_-]{16,40}$/;
|
||||
|
||||
export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
@@ -54,15 +57,38 @@ export function CommandPalette({ open, onOpenChange }: CommandPaletteProps) {
|
||||
}, [role, t]);
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const q = query.trim().toLowerCase();
|
||||
if (!q) return items;
|
||||
return items.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(q) ||
|
||||
item.group.toLowerCase().includes(q) ||
|
||||
item.path.toLowerCase().includes(q)
|
||||
);
|
||||
}, [items, query]);
|
||||
const q = query.trim();
|
||||
const qLower = q.toLowerCase();
|
||||
const navHits = !qLower
|
||||
? items
|
||||
: items.filter(
|
||||
(item) =>
|
||||
item.label.toLowerCase().includes(qLower) ||
|
||||
item.group.toLowerCase().includes(qLower) ||
|
||||
item.path.toLowerCase().includes(qLower)
|
||||
);
|
||||
|
||||
if (!ENTITY_ID_RE.test(q)) return navHits;
|
||||
|
||||
const jumps: PaletteItem[] = [];
|
||||
const pushIf = (path: string, labelKey: string, id: string) => {
|
||||
if (canAccessRoute(path.split('?')[0], role)) {
|
||||
jumps.push({
|
||||
id,
|
||||
label: `${t(labelKey)} · ${q}`,
|
||||
group: t('layout.paletteJumpGroup'),
|
||||
path,
|
||||
});
|
||||
}
|
||||
};
|
||||
pushIf(`/inbox/reports?selected=${encodeURIComponent(q)}`, 'layout.paletteOpenReportInbox', `jump-report-inbox-${q}`);
|
||||
pushIf(`/reports/${encodeURIComponent(q)}`, 'layout.paletteOpenReport', `jump-report-${q}`);
|
||||
pushIf(`/inbox/tickets?selected=${encodeURIComponent(q)}`, 'layout.paletteOpenTicketInbox', `jump-ticket-inbox-${q}`);
|
||||
pushIf(`/tickets/${encodeURIComponent(q)}`, 'layout.paletteOpenTicket', `jump-ticket-${q}`);
|
||||
pushIf(`/events/${encodeURIComponent(q)}`, 'layout.paletteOpenEvent', `jump-event-${q}`);
|
||||
pushIf(`/users/${encodeURIComponent(q)}`, 'layout.paletteOpenUser', `jump-user-${q}`);
|
||||
return [...jumps, ...navHits];
|
||||
}, [items, query, role, t]);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex(0);
|
||||
|
||||
@@ -80,7 +80,10 @@ export function HeaderNodeMetrics({ className }: { className?: string }) {
|
||||
const showArrows = canScroll.left || canScroll.right;
|
||||
|
||||
return (
|
||||
<div className={cn('flex min-w-0 max-w-full items-center gap-0.5', className)}>
|
||||
<div className={cn('flex min-w-0 max-w-full items-center gap-1.5', className)}>
|
||||
<span className="hidden shrink-0 text-[10px] font-medium uppercase tracking-wide text-muted-foreground sm:inline">
|
||||
{t('layout.nodesLabel')}
|
||||
</span>
|
||||
{showArrows && (
|
||||
<Button
|
||||
type="button"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { NodeMetric } from '@/store/metricsStore';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
@@ -63,6 +64,7 @@ const MetricIndicator: React.FC<Props> = ({
|
||||
onClick,
|
||||
className,
|
||||
}) => {
|
||||
const { t } = useTranslation();
|
||||
const cpu = stats.cpu_utilization ?? 0;
|
||||
const totalMemory = (stats.memory_total ?? 0) + (stats.memory_available ?? 0);
|
||||
const usedMemory = stats.memory_total ?? 0;
|
||||
@@ -104,15 +106,20 @@ const MetricIndicator: React.FC<Props> = ({
|
||||
{cpu.toFixed(0)}%
|
||||
</div>
|
||||
<div className="pointer-events-none absolute bottom-full left-1/2 z-50 mb-2 hidden w-max max-w-[240px] -translate-x-1/2 rounded-md border bg-popover px-2 py-1.5 text-left text-xs text-popover-foreground shadow-md group-hover:block group-focus-visible:block">
|
||||
<div className="font-medium">{node}</div>
|
||||
<div className="font-medium">{t('layout.nodeTooltipTitle', { node })}</div>
|
||||
<div>
|
||||
CPU: {cpu.toFixed(1)}%{' '}
|
||||
{prevCpu !== undefined ? `(было ${prevCpu.toFixed(1)}%)` : ''}
|
||||
{t('layout.nodeCpu', { value: cpu.toFixed(1) })}
|
||||
{prevCpu !== undefined ? ` (${t('layout.nodeWas', { value: prevCpu.toFixed(1) })})` : ''}
|
||||
</div>
|
||||
<div>
|
||||
Память: {(usedMemory / 1048576).toFixed(0)} / {(totalMemory / 1048576).toFixed(0)} МБ (
|
||||
{memoryPercent.toFixed(1)}%)
|
||||
{prevMemoryPercent !== null ? ` (было ${prevMemoryPercent.toFixed(1)}%)` : ''}
|
||||
{t('layout.nodeMemory', {
|
||||
used: (usedMemory / 1048576).toFixed(0),
|
||||
total: (totalMemory / 1048576).toFixed(0),
|
||||
percent: memoryPercent.toFixed(1),
|
||||
})}
|
||||
{prevMemoryPercent !== null
|
||||
? ` (${t('layout.nodeWas', { value: prevMemoryPercent.toFixed(1) })})`
|
||||
: ''}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -125,7 +132,11 @@ const MetricIndicator: React.FC<Props> = ({
|
||||
className={shellClass}
|
||||
style={{ width: size, height: size }}
|
||||
onClick={onClick}
|
||||
aria-label={`${node}: CPU ${cpu.toFixed(0)}%, память ${memoryPercent.toFixed(0)}%`}
|
||||
aria-label={t('layout.nodeAria', {
|
||||
node,
|
||||
cpu: cpu.toFixed(0),
|
||||
memory: memoryPercent.toFixed(0),
|
||||
})}
|
||||
>
|
||||
{body}
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user