feat(web): retrofit empty states to shared EmptyState primitive
Replace ad-hoc empty-state markup across Dashboard, TopologyList, LiveLogs, Attackers, Bounty, AttackerDetail, SwarmHosts, RemoteUpdates and CommandPalette with the new <EmptyState> component. Themed icons + hints improve discoverability; TopologyList and SwarmHosts gain CTAs to their respective creation flows.
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useParams, useNavigate } from 'react-router-dom';
|
import { useParams, useNavigate } from 'react-router-dom';
|
||||||
import { Activity, ArrowLeft, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Crosshair, Fingerprint, Shield, Clock, Wifi, Lock, FileKey, Radio, Timer, Paperclip } from 'lucide-react';
|
import { Activity, ArrowLeft, ChevronDown, ChevronLeft, ChevronRight, ChevronUp, Crosshair, Fingerprint, Shield, Clock, Wifi, Lock, FileKey, Radio, Timer, Paperclip, Terminal, Package, FileText } from 'lucide-react';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import ArtifactDrawer from './ArtifactDrawer';
|
import ArtifactDrawer from './ArtifactDrawer';
|
||||||
import SessionDrawer from './SessionDrawer';
|
import SessionDrawer from './SessionDrawer';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
|
|
||||||
interface AttackerBehavior {
|
interface AttackerBehavior {
|
||||||
@@ -964,9 +965,12 @@ const AttackerDetail: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
|
<EmptyState
|
||||||
NO BEHAVIORAL DATA YET — PROFILER HAS NOT RUN FOR THIS ATTACKER
|
icon={Activity}
|
||||||
</div>
|
title="NO BEHAVIORAL DATA YET"
|
||||||
|
hint="profiler has not run for this attacker"
|
||||||
|
size="compact"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -1028,9 +1032,11 @@ const AttackerDetail: React.FC = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
|
<EmptyState
|
||||||
{serviceFilter ? `NO ${serviceFilter.toUpperCase()} COMMANDS CAPTURED` : 'NO COMMANDS CAPTURED'}
|
icon={Terminal}
|
||||||
</div>
|
title={serviceFilter ? `NO ${serviceFilter.toUpperCase()} COMMANDS CAPTURED` : 'NO COMMANDS CAPTURED'}
|
||||||
|
size="compact"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
@@ -1177,9 +1183,11 @@ const AttackerDetail: React.FC = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
|
<EmptyState
|
||||||
NO ARTIFACTS CAPTURED FROM THIS ATTACKER
|
icon={Package}
|
||||||
</div>
|
title="NO ARTIFACTS CAPTURED"
|
||||||
|
size="compact"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
@@ -1258,9 +1266,11 @@ const AttackerDetail: React.FC = () => {
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ padding: '24px', textAlign: 'center', opacity: 0.5 }}>
|
<EmptyState
|
||||||
NO SESSION TRANSCRIPTS RECORDED FROM THIS ATTACKER
|
icon={FileText}
|
||||||
</div>
|
title="NO SESSION TRANSCRIPTS RECORDED"
|
||||||
|
size="compact"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import { Search, ChevronLeft, ChevronRight, UserX } from 'lucide-react';
|
import { Search, ChevronLeft, ChevronRight, Users } from 'lucide-react';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import './Attackers.css';
|
import './Attackers.css';
|
||||||
|
|
||||||
@@ -163,14 +164,13 @@ const Attackers: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="empty-state">
|
<EmptyState icon={Users} title="SCANNING THREAT PROFILES…" />
|
||||||
<span className="type-label">SCANNING THREAT PROFILES...</span>
|
|
||||||
</div>
|
|
||||||
) : attackers.length === 0 ? (
|
) : attackers.length === 0 ? (
|
||||||
<div className="empty-state">
|
<EmptyState
|
||||||
<UserX size={28} />
|
icon={Users}
|
||||||
<span className="type-label">NO ACTIVE THREATS PROFILED YET</span>
|
title="NO ACTIVE THREATS PROFILED YET"
|
||||||
</div>
|
hint="waiting on attacker traffic to correlate"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="ak-grid">
|
<div className="ak-grid">
|
||||||
{attackers.map(a => {
|
{attackers.map(a => {
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import React, { useEffect, useState } from 'react';
|
|||||||
import { useSearchParams, useNavigate } from 'react-router-dom';
|
import { useSearchParams, useNavigate } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Archive, Search, ChevronLeft, ChevronRight, Filter, Key, Package, ChevronRight as ChevR,
|
Archive, Search, ChevronLeft, ChevronRight, Filter, Key, Package, ChevronRight as ChevR,
|
||||||
ArchiveX,
|
Target,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import BountyInspector from './BountyInspector';
|
import BountyInspector from './BountyInspector';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import './Bounty.css';
|
import './Bounty.css';
|
||||||
|
|
||||||
@@ -220,12 +221,11 @@ const Bounty: React.FC = () => {
|
|||||||
}) : (
|
}) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={7}>
|
<td colSpan={7}>
|
||||||
<div className="empty-state">
|
<EmptyState
|
||||||
<ArchiveX size={30} />
|
icon={Target}
|
||||||
<span className="type-label">
|
title={loading ? 'RETRIEVING ARTIFACTS…' : 'THE VAULT IS EMPTY'}
|
||||||
{loading ? 'RETRIEVING ARTIFACTS...' : 'THE VAULT IS EMPTY'}
|
hint={loading ? undefined : 'attacker-dropped artifacts will land here'}
|
||||||
</span>
|
/>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import {
|
import {
|
||||||
LayoutDashboard, Server, Network, Terminal, Archive, Crosshair,
|
LayoutDashboard, Server, Network, Terminal, Archive, Crosshair,
|
||||||
PlusCircle, Pause, RefreshCw, Download, HardDrive, Package, UserPlus, Settings,
|
PlusCircle, Pause, RefreshCw, Download, HardDrive, Package, UserPlus, Settings,
|
||||||
|
SearchX,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
import EmptyState from '../EmptyState/EmptyState';
|
||||||
import './CommandPalette.css';
|
import './CommandPalette.css';
|
||||||
|
|
||||||
type IconComponent = React.ComponentType<{ size?: number; className?: string }>;
|
type IconComponent = React.ComponentType<{ size?: number; className?: string }>;
|
||||||
@@ -141,7 +143,7 @@ const CommandPalette: React.FC<Props> = ({ open, onClose, onNav, onAction }) =>
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{filtered.length === 0 && (
|
{filtered.length === 0 && (
|
||||||
<div className="cmd-empty">NO COMMAND MATCHES</div>
|
<EmptyState icon={SearchX} title="NO COMMAND MATCHES" size="compact" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="cmd-hint">
|
<div className="cmd-hint">
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import { Shield, Users, Activity, Clock, Paperclip, Crosshair, Flame, Archive } from 'lucide-react';
|
import { Shield, Users, Activity, Clock, Paperclip, Crosshair, Flame, Archive, ShieldOff, Server } from 'lucide-react';
|
||||||
import { parseEventBody } from '../utils/parseEventBody';
|
import { parseEventBody } from '../utils/parseEventBody';
|
||||||
import ArtifactDrawer from './ArtifactDrawer';
|
import ArtifactDrawer from './ArtifactDrawer';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
|
|
||||||
interface Stats {
|
interface Stats {
|
||||||
total_logs: number;
|
total_logs: number;
|
||||||
@@ -417,7 +418,13 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
|
|||||||
);
|
);
|
||||||
}) : (
|
}) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} style={{ textAlign: 'center', padding: '40px' }}>NO INTERACTION DETECTED</td>
|
<td colSpan={6} style={{ padding: 0 }}>
|
||||||
|
<EmptyState
|
||||||
|
icon={Activity}
|
||||||
|
title="NO INTERACTION DETECTED"
|
||||||
|
hint="waiting for the first decky hit"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -454,7 +461,7 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="panel-empty">NO ACTIVITY</div>
|
<EmptyState icon={Server} title="NO ACTIVITY" hint="all deckies quiet" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -486,7 +493,7 @@ const Dashboard: React.FC<DashboardProps> = ({ searchQuery }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="panel-empty">NO ATTACKERS YET</div>
|
<EmptyState icon={ShieldOff} title="NO ATTACKERS YET" hint="nothing on the radar" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import React, { useEffect, useState, useRef, useMemo } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
Terminal, Search, BarChart3, ChevronLeft, ChevronRight,
|
Terminal, Search, BarChart3, ChevronLeft, ChevronRight,
|
||||||
Play, Pause, Paperclip, Download, SearchX, X as XIcon,
|
Play, Pause, Paperclip, Download, Radio, X as XIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
import { parseEventBody } from '../utils/parseEventBody';
|
import { parseEventBody } from '../utils/parseEventBody';
|
||||||
import ArtifactDrawer from './ArtifactDrawer';
|
import ArtifactDrawer from './ArtifactDrawer';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import './LiveLogs.css';
|
import './LiveLogs.css';
|
||||||
|
|
||||||
@@ -357,12 +358,11 @@ const LiveLogs: React.FC = () => {
|
|||||||
}) : (
|
}) : (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5}>
|
<td colSpan={5}>
|
||||||
<div className="empty-state">
|
<EmptyState
|
||||||
<SearchX size={28} />
|
icon={Radio}
|
||||||
<span className="type-label">
|
title={loading ? 'RETRIEVING DATA…' : 'NO LOGS MATCHING CRITERIA'}
|
||||||
{loading ? 'RETRIEVING DATA...' : 'NO LOGS MATCHING CRITERIA'}
|
hint={loading ? undefined : 'adjust filters or wait for new events'}
|
||||||
</span>
|
/>
|
||||||
</div>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import {
|
import {
|
||||||
Upload, RefreshCw, RotateCcw, Package, AlertTriangle, CheckCircle,
|
Upload, RefreshCw, RotateCcw, Package, AlertTriangle, CheckCircle,
|
||||||
@@ -222,11 +223,11 @@ const RemoteUpdates: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{hosts.length === 0 ? (
|
{hosts.length === 0 ? (
|
||||||
<div style={{ padding: '24px', color: 'var(--dim-color)' }}>
|
<EmptyState
|
||||||
<Server size={16} style={{ verticalAlign: 'middle', marginRight: '8px' }} />
|
icon={Server}
|
||||||
No workers with an updater bundle are enrolled. Run{' '}
|
title="NO UPDATER-ENABLED WORKERS"
|
||||||
<code>decnet swarm enroll --host <name> --updater</code> to add one.
|
hint="run `decnet swarm enroll --host <name> --updater` to add one"
|
||||||
</div>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div style={{ display: 'grid', gap: '16px' }}>
|
<div style={{ display: 'grid', gap: '16px' }}>
|
||||||
{hosts.map((h) => {
|
{hosts.map((h) => {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
import api from '../utils/api';
|
import api from '../utils/api';
|
||||||
|
import EmptyState from './EmptyState/EmptyState';
|
||||||
import './Dashboard.css';
|
import './Dashboard.css';
|
||||||
import './Swarm.css';
|
import './Swarm.css';
|
||||||
import { HardDrive, PowerOff, RefreshCw, Trash2, Wifi, WifiOff } from 'lucide-react';
|
import { HardDrive, PowerOff, RefreshCw, Server, Trash2, Wifi, WifiOff } from 'lucide-react';
|
||||||
|
|
||||||
interface SwarmHost {
|
interface SwarmHost {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
@@ -20,6 +22,7 @@ interface SwarmHost {
|
|||||||
const shortFp = (fp: string): string => (fp ? fp.slice(0, 16) + '…' : '—');
|
const shortFp = (fp: string): string => (fp ? fp.slice(0, 16) + '…' : '—');
|
||||||
|
|
||||||
const SwarmHosts: React.FC = () => {
|
const SwarmHosts: React.FC = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
const [hosts, setHosts] = useState<SwarmHost[]>([]);
|
const [hosts, setHosts] = useState<SwarmHost[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [decommissioning, setDecommissioning] = useState<Set<string>>(new Set());
|
const [decommissioning, setDecommissioning] = useState<Set<string>>(new Set());
|
||||||
@@ -102,7 +105,12 @@ const SwarmHosts: React.FC = () => {
|
|||||||
{loading ? (
|
{loading ? (
|
||||||
<p>Loading hosts…</p>
|
<p>Loading hosts…</p>
|
||||||
) : hosts.length === 0 ? (
|
) : hosts.length === 0 ? (
|
||||||
<p>No swarm hosts enrolled yet. Head to <strong>SWARM → Agent Enrollment</strong> to onboard one.</p>
|
<EmptyState
|
||||||
|
icon={Server}
|
||||||
|
title="NO SWARM HOSTS ENROLLED"
|
||||||
|
hint="onboard an agent to expand the fleet"
|
||||||
|
cta={{ label: 'ENROLL HOST', onClick: () => navigate('/swarm/enroll') }}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<table className="data-table">
|
<table className="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Network, Plus, Power, Trash2, UploadCloud, RefreshCw, Skull } from 'luc
|
|||||||
import api from '../../utils/api';
|
import api from '../../utils/api';
|
||||||
import { clearLayout } from '../MazeNET/useMazeLayoutStore';
|
import { clearLayout } from '../MazeNET/useMazeLayoutStore';
|
||||||
import CreateTopologyWizard from './CreateTopologyWizard';
|
import CreateTopologyWizard from './CreateTopologyWizard';
|
||||||
|
import EmptyState from '../EmptyState/EmptyState';
|
||||||
import './TopologyList.css';
|
import './TopologyList.css';
|
||||||
|
|
||||||
interface TopologySummary {
|
interface TopologySummary {
|
||||||
@@ -237,9 +238,12 @@ const TopologyList: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!loading && rows.length === 0 && (
|
{!loading && rows.length === 0 && (
|
||||||
<div className="tlist-empty">
|
<EmptyState
|
||||||
No topologies yet. Click NEW TOPOLOGY to create one.
|
icon={Network}
|
||||||
</div>
|
title="NO TOPOLOGIES YET"
|
||||||
|
hint="spin one up to deploy a honeynet"
|
||||||
|
cta={{ label: 'NEW TOPOLOGY', icon: Plus, onClick: () => setCreating(true) }}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user