mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-13 04:06:59 +00:00
feat: chart-led Security overview with sortable Images and History tables (#1364)
* feat: chart-led Security overview with sortable Images and History tables Refine the Security page around the existing design system and add the data the dashboard needs. - Overview leads with four charts (30-day risk trend, severity donut, top exposed images, findings by type); the signal-rail counts become a secondary summary, and the scanner and deploy-enforcement posture follow. - Images becomes a recessed table with search, a severity filter, sortable columns, a last-scan column, and inline scan actions; the findings cell is clickable into the scan sheet, and the per-row cursor tooltip is dropped where the columns already carry that information. - Policies puts deploy-enforcement first, collapses the policy packs into an accordion, and uses the standard primary button for Add policy. - Suppressions and acknowledgements move their titles and Add buttons outside the cards, matching the Fleet tab layout. - History switches from the detail sheet to an inline table (search, sortable columns, two-scan compare, pagination); the now-unreachable scan-history overlay is removed. - Add GET /api/security/overview/trend, a node-scoped daily critical/high rollup backing the risk-trend chart. - Extract the shared image-scan hook and the severity classifier, and harden the overview data fetch so a malformed non-critical response can never read as a clean security state. * fix: treat malformed Security responses as errors, not empty or clean states Address an independent review of the data-fetch paths so a 200 with an unexpected shape can never read as a benign "no findings" view. - SecurityView: validate that the image-summaries body is a scan-summary map; an unexpected shape now sets the error state instead of an empty map. Isolate the trend fetch in its own self-catching promise so a transport failure on the non-critical chart can no longer poison the overview or summaries error state. - useImageScan: only a "completed" poll counts as success (a malformed or unknown status now throws), and a failed post-scan summaries refresh is logged instead of silently dropped. - HistoryTab: a 200 whose body lacks an items array is treated as an error, not an empty "no completed scans" list.
This commit is contained in:
@@ -1,6 +1,4 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import BashExecModal from '../BashExecModal';
|
||||
import LazyBoundary from '../LazyBoundary';
|
||||
import { PolicyBlockDialog } from '../stack/PolicyBlockDialog';
|
||||
import { UpdateReadinessDialog } from '../stack/UpdateReadinessDialog';
|
||||
import { DeleteStackDialog } from './DeleteStackDialog';
|
||||
@@ -14,14 +12,6 @@ import type { OverlayState } from './hooks/useOverlayState';
|
||||
import type { StackActionsHook } from './hooks/useStackActions';
|
||||
import type { PermissionAction } from '@/context/AuthContext';
|
||||
|
||||
// SecurityHistoryView is the only lazy-loaded view that lives outside
|
||||
// the ViewRouter switch -- it renders as an overlay sheet wired into the
|
||||
// settings flow, not as a top-level tab. The other tab-level lazy views
|
||||
// (HostConsole, FleetView, AuditLogView, etc.) live inside ViewRouter.
|
||||
const SecurityHistoryView = lazy(() =>
|
||||
import('../SecurityHistoryView').then(m => ({ default: m.SecurityHistoryView })),
|
||||
);
|
||||
|
||||
interface ShellOverlaysProps {
|
||||
overlayState: OverlayState;
|
||||
stackActions: StackActionsHook;
|
||||
@@ -32,8 +22,6 @@ interface ShellOverlaysProps {
|
||||
stackName: string;
|
||||
gitSourceOpen: boolean;
|
||||
setGitSourceOpen: (open: boolean) => void;
|
||||
securityHistoryOpen: boolean;
|
||||
setSecurityHistoryOpen: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function ShellOverlays({
|
||||
@@ -46,8 +34,6 @@ export function ShellOverlays({
|
||||
stackName,
|
||||
gitSourceOpen,
|
||||
setGitSourceOpen,
|
||||
securityHistoryOpen,
|
||||
setSecurityHistoryOpen,
|
||||
}: ShellOverlaysProps) {
|
||||
const {
|
||||
deleteDialogOpen, closeDeleteDialog, stackToDelete,
|
||||
@@ -171,21 +157,6 @@ export function ShellOverlays({
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Scan history overlay. Conditionally mounted so the lazy chunk
|
||||
only fetches when the user opens the overlay; an always-mounted
|
||||
lazy component would fetch on EditorLayout's first render and
|
||||
defeat the split. The overlay has no internal state that needs
|
||||
to persist across opens. */}
|
||||
{securityHistoryOpen ? (
|
||||
<LazyBoundary>
|
||||
<Suspense fallback={null}>
|
||||
<SecurityHistoryView
|
||||
open
|
||||
onClose={() => setSecurityHistoryOpen(false)}
|
||||
/>
|
||||
</Suspense>
|
||||
</LazyBoundary>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,8 @@ import type { ScheduleTaskPrefill } from '../ScheduledOperationsView';
|
||||
import type { ActiveView } from './hooks/useViewNavigationState';
|
||||
import type { SecurityTab } from '@/lib/events';
|
||||
|
||||
// Paid-tier views and the security-history overlay are loaded on demand.
|
||||
// Their internal PaidGate / CapabilityGate wrappers render
|
||||
// Paid-tier views are loaded on demand. Their internal PaidGate /
|
||||
// CapabilityGate wrappers render
|
||||
// the upsell or capability-missing card with blurred children rather than
|
||||
// short-circuiting, so a tier-locked or capability-missing operator
|
||||
// opening one of these tabs still triggers the chunk fetch to render the
|
||||
|
||||
@@ -58,7 +58,6 @@ describe('useViewNavigationState', () => {
|
||||
const { result } = renderHook(() => useViewNavigationState());
|
||||
expect(result.current.activeView).toBe('dashboard');
|
||||
expect(result.current.settingsSection).toBe('appearance');
|
||||
expect(result.current.securityHistoryOpen).toBe(false);
|
||||
expect(result.current.filterNodeId).toBeNull();
|
||||
expect(result.current.schedulePrefill).toBeNull();
|
||||
expect(result.current.mobileNavOpen).toBe(false);
|
||||
@@ -156,18 +155,6 @@ describe('useViewNavigationState', () => {
|
||||
expect(result.current.filterNodeId).toBe(5);
|
||||
});
|
||||
|
||||
it('SENCHO_NAVIGATE_EVENT with security-history opens the sheet without changing activeView', () => {
|
||||
const { result } = renderHook(() => useViewNavigationState());
|
||||
act(() => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(SENCHO_NAVIGATE_EVENT, { detail: { view: 'security-history', nodeId: 3 } }),
|
||||
);
|
||||
});
|
||||
expect(result.current.securityHistoryOpen).toBe(true);
|
||||
expect(result.current.filterNodeId).toBe(3);
|
||||
expect(result.current.activeView).toBe('dashboard');
|
||||
});
|
||||
|
||||
it('SENCHO_NAVIGATE_EVENT with no nodeId sets filterNodeId to null', () => {
|
||||
const { result } = renderHook(() => useViewNavigationState());
|
||||
act(() => {
|
||||
|
||||
@@ -61,7 +61,6 @@ export function useViewNavigationState(options?: UseViewNavigationStateOptions)
|
||||
const [activeView, setActiveView] = useState<ActiveView>('dashboard');
|
||||
const [settingsSection, setSettingsSection] = useState<SectionId>('appearance');
|
||||
const [securityTab, setSecurityTab] = useState<SecurityTab>('overview');
|
||||
const [securityHistoryOpen, setSecurityHistoryOpen] = useState(false);
|
||||
const [filterNodeId, setFilterNodeId] = useState<number | null>(null);
|
||||
const [schedulePrefill, setSchedulePrefill] = useState<ScheduleTaskPrefill | null>(null);
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
@@ -89,11 +88,6 @@ export function useViewNavigationState(options?: UseViewNavigationStateOptions)
|
||||
const handler = (e: Event) => {
|
||||
const detail = (e as CustomEvent<SenchoNavigateDetail & { view: string }>).detail;
|
||||
if (!detail?.view) return;
|
||||
if (detail.view === 'security-history') {
|
||||
setSecurityHistoryOpen(true);
|
||||
setFilterNodeId(detail.nodeId ?? null);
|
||||
return;
|
||||
}
|
||||
if (detail.view === 'security') {
|
||||
// Set the target tab before switching the view so the controlled
|
||||
// SecurityView lands on it deterministically (no mount race).
|
||||
@@ -152,7 +146,6 @@ export function useViewNavigationState(options?: UseViewNavigationStateOptions)
|
||||
activeView, setActiveView,
|
||||
settingsSection, setSettingsSection,
|
||||
securityTab, setSecurityTab,
|
||||
securityHistoryOpen, setSecurityHistoryOpen,
|
||||
filterNodeId, setFilterNodeId,
|
||||
schedulePrefill, setSchedulePrefill,
|
||||
mobileNavOpen, setMobileNavOpen,
|
||||
|
||||
Reference in New Issue
Block a user