mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
refactor: Update UI components for consistency and improve layout
This commit is contained in:
parent
da619171cf
commit
77be7f10d9
74 changed files with 3186 additions and 618 deletions
|
|
@ -23,10 +23,10 @@ export function HorizontalBarChart({
|
|||
);
|
||||
}
|
||||
|
||||
const barH = 22;
|
||||
const gap = 4;
|
||||
const labelW = 110;
|
||||
const valueW = 45;
|
||||
const barH = 32;
|
||||
const gap = 12;
|
||||
const labelW = 120;
|
||||
const valueW = 48;
|
||||
const barAreaW = width - labelW - valueW - 16;
|
||||
const totalH = items.length * (barH + gap);
|
||||
const maxVal = maxValue ?? Math.max(...items.map((i) => i.value), 1);
|
||||
|
|
@ -49,7 +49,7 @@ export function HorizontalBarChart({
|
|||
x={labelW - 8}
|
||||
y={y + barH / 2 + 4}
|
||||
textAnchor="end"
|
||||
fontSize={11}
|
||||
fontSize={13}
|
||||
fontFamily="var(--font)"
|
||||
fill="var(--text-secondary)"
|
||||
>
|
||||
|
|
@ -68,7 +68,7 @@ export function HorizontalBarChart({
|
|||
x={labelW + barAreaW + 8}
|
||||
y={y + barH / 2 + 4}
|
||||
textAnchor="start"
|
||||
fontSize={11}
|
||||
fontSize={13}
|
||||
fontFamily="var(--font-mono)"
|
||||
fontWeight={600}
|
||||
fill="var(--text)"
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export function LineChart({
|
|||
points,
|
||||
color = 'var(--accent)',
|
||||
width = 400,
|
||||
height = 160,
|
||||
height = 240,
|
||||
}: LineChartProps) {
|
||||
if (!points || points.length < 2) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ export function HeaderBar({ onStartScan, onOpenPalette }: HeaderBarProps) {
|
|||
className="btn btn-primary btn-sm"
|
||||
onClick={onStartScan}
|
||||
>
|
||||
Start Scan
|
||||
Start scan
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
import type { FC } from 'react';
|
||||
import type { IconProps } from '../icons/Icons';
|
||||
import { useHealth } from '../../api/queries/health';
|
||||
import { useOverview } from '../../api/queries/overview';
|
||||
import { useSSE } from '../../contexts/SSEContext';
|
||||
|
||||
interface NavItem {
|
||||
|
|
@ -89,17 +90,19 @@ function navLinkClass({ isActive }: { isActive: boolean }) {
|
|||
|
||||
export function Sidebar() {
|
||||
const { data: health } = useHealth();
|
||||
const { data: overview } = useOverview();
|
||||
const { isScanRunning } = useSSE();
|
||||
|
||||
const primary = NAV_SECTIONS.filter((n) => n.group === 'primary');
|
||||
const secondary = NAV_SECTIONS.filter((n) => n.group === 'secondary');
|
||||
const footer = NAV_SECTIONS.filter((n) => n.group === 'footer');
|
||||
const findingsCount =
|
||||
overview && overview.state !== 'empty' ? overview.total_findings : null;
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
<div className="sidebar-header">
|
||||
<span className="logo">nyx</span>
|
||||
{health?.version && <span className="version">v{health.version}</span>}
|
||||
<img src="/logo.png" alt="Nyx" className="sidebar-logo-img" />
|
||||
</div>
|
||||
|
||||
<ul className="nav-list">
|
||||
|
|
@ -113,12 +116,15 @@ export function Sidebar() {
|
|||
<span className="nav-icon">
|
||||
<item.Icon />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
{item.id === 'findings' && findingsCount != null && (
|
||||
<span className="nav-badge">{findingsCount}</span>
|
||||
)}
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
||||
<li className="nav-separator" />
|
||||
<li className="nav-section-header">Tools</li>
|
||||
|
||||
{secondary.map((item) => (
|
||||
<li key={item.id}>
|
||||
|
|
@ -126,7 +132,7 @@ export function Sidebar() {
|
|||
<span className="nav-icon">
|
||||
<item.Icon />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -140,7 +146,7 @@ export function Sidebar() {
|
|||
<span className="nav-icon">
|
||||
<item.Icon />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
</NavLink>
|
||||
</li>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import type {
|
||||
HealthScore,
|
||||
|
|
@ -25,8 +26,17 @@ export function HealthScoreCard({
|
|||
posture?: PostureSummary;
|
||||
}) {
|
||||
const gradeClass = `grade-${health.grade.toLowerCase()}`;
|
||||
const gradeAccent =
|
||||
health.grade === 'A' || health.grade === 'B'
|
||||
? 'var(--green)'
|
||||
: health.grade === 'C'
|
||||
? 'var(--amber)'
|
||||
: 'var(--red)';
|
||||
return (
|
||||
<div className="card health-card">
|
||||
<div
|
||||
className="card health-card"
|
||||
style={{ '--health-accent': gradeAccent } as React.CSSProperties}
|
||||
>
|
||||
<div className="health-eyebrow">Health Score</div>
|
||||
<div className="health-headline">
|
||||
<div className={`health-grade-block ${gradeClass}`}>
|
||||
|
|
@ -44,12 +54,26 @@ export function HealthScoreCard({
|
|||
)}
|
||||
</div>
|
||||
<div className="health-components">
|
||||
{health.components.map((c) => (
|
||||
<div className="health-component" key={c.label} title={c.detail}>
|
||||
<div className="health-component-score">{c.score}</div>
|
||||
<div className="health-component-label">{c.label}</div>
|
||||
</div>
|
||||
))}
|
||||
{health.components.map((c) => {
|
||||
const barColor =
|
||||
c.score >= 70
|
||||
? 'var(--green)'
|
||||
: c.score >= 40
|
||||
? 'var(--amber)'
|
||||
: 'var(--red)';
|
||||
return (
|
||||
<div className="health-component" key={c.label} title={c.detail}>
|
||||
<div className="health-component-label">{c.label}</div>
|
||||
<div className="health-component-bar-track">
|
||||
<div
|
||||
className="health-component-fill"
|
||||
style={{ width: `${c.score}%`, background: barColor }}
|
||||
/>
|
||||
</div>
|
||||
<div className="health-component-score">{c.score}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -113,7 +137,13 @@ export function BacklogCard({ backlog }: { backlog: BacklogStats }) {
|
|||
function BucketBar({ buckets }: { buckets: OverviewCount[] }) {
|
||||
const total = buckets.reduce((s, b) => s + b.count, 0);
|
||||
if (total === 0) return null;
|
||||
const colors = ['#3498db', '#2ecc71', '#f1c40f', '#e67e22', '#e74c3c'];
|
||||
const colors = [
|
||||
'var(--accent)',
|
||||
'var(--green)',
|
||||
'var(--amber)',
|
||||
'var(--red)',
|
||||
'var(--muted)',
|
||||
];
|
||||
return (
|
||||
<div
|
||||
className="bucket-bar"
|
||||
|
|
@ -149,10 +179,10 @@ export function ConfidenceDistributionChart({
|
|||
);
|
||||
}
|
||||
const segments = [
|
||||
{ label: 'High', value: dist.high, color: '#27ae60' },
|
||||
{ label: 'Medium', value: dist.medium, color: '#f39c12' },
|
||||
{ label: 'Low', value: dist.low, color: '#95a5a6' },
|
||||
{ label: 'None', value: dist.none, color: '#bdc3c7' },
|
||||
{ label: 'High', value: dist.high, color: 'var(--green)' },
|
||||
{ label: 'Medium', value: dist.medium, color: 'var(--amber)' },
|
||||
{ label: 'Low', value: dist.low, color: 'var(--muted)' },
|
||||
{ label: 'None', value: dist.none, color: 'var(--subtle)' },
|
||||
];
|
||||
return (
|
||||
<div className="confidence-dist">
|
||||
|
|
@ -484,7 +514,7 @@ export function SuppressionHygieneCard({
|
|||
<div className="kv-row kv-row-emphasis">
|
||||
<dt
|
||||
className="kv-label"
|
||||
title="Share of suppressions that are not pinned to a specific finding fingerprint. Lower is better — it means triage is decisive rather than blanket-silencing whole rules or files."
|
||||
title="Share of suppressions that are not pinned to a specific finding fingerprint. Lower is better because triage is decisive rather than blanket-silencing whole rules or files."
|
||||
>
|
||||
Blanket rate
|
||||
<span className="kv-hint">Lower is better</span>
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ export interface PaletteCommand {
|
|||
id: string;
|
||||
/** Visible label. */
|
||||
label: string;
|
||||
/** Optional secondary line — section, hint, shortcut. */
|
||||
/** Optional secondary line such as section, hint, or shortcut. */
|
||||
hint?: string;
|
||||
/** Group label for visual separation. */
|
||||
group?: string;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ interface LoadingStateProps {
|
|||
/**
|
||||
* Suppresses the spinner for the first ~150ms so trivially-fast queries
|
||||
* don't flash a spinner on screen. The text shows instantly so there's
|
||||
* always *something* — but the visible spin only kicks in if work is
|
||||
* always something, but the visible spin only kicks in if work is
|
||||
* actually slow.
|
||||
*/
|
||||
delaySpinnerMs?: number;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue