Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 35 additions & 71 deletions src/main/frontend/package-lock.json

Large diffs are not rendered by default.

13 changes: 11 additions & 2 deletions src/main/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:report": "playwright show-report"
},
"dependencies": {
"@monaco-editor/react": "^4.7.0",
Expand All @@ -16,19 +19,25 @@
"@radix-ui/react-select": "^2.1.6",
"@radix-ui/react-tooltip": "^1.1.8",
"@types/d3": "^7.4.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"d3": "^7.9.0",
"lucide-react": "^0.474.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-resizable-panels": "^4.8.0",
"react-router-dom": "^7.1.5",
"swagger-ui-react": "^5.21.0",
"tailwind-merge": "^3.0.2"
"tailwind-merge": "^3.0.2",
"tailwindcss-animate": "^1.0.7"
},
"overrides": {
"dompurify": "^3.3.3"
},
"devDependencies": {
"@axe-core/playwright": "^4.10.1",
"@playwright/test": "^1.51.1",
"@types/node": "^22.0.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/swagger-ui-react": "^4.18.3",
Expand Down
73 changes: 73 additions & 0 deletions src/main/frontend/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { defineConfig, devices } from '@playwright/test';

/**
* Playwright E2E test configuration for Code IQ UI Redesign (Phase 7).
*
* Prerequisites:
* - `code-iq serve` running on localhost:8080 (Neo4j graph loaded)
* - Or use `webServer` below to start the Vite dev server pointing at a real backend
*
* Run all tests: npm run test:e2e
* Run headed: npm run test:e2e:headed
* Show HTML report: npm run test:e2e:report
*/
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [['html', { outputFolder: 'playwright-report' }], ['line']],

use: {
baseURL: process.env.BASE_URL ?? 'http://localhost:8080',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},

// Performance threshold constants (ms) shared via env so specs can read them
// Actual assertions live in performance.spec.ts
// PERF_THRESHOLD_100 = 500
// PERF_THRESHOLD_1K = 2000
// PERF_THRESHOLD_10K = 3000

projects: [
// P0 — required for release sign-off
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},

// P1 — run in CI when available
{
name: 'edge',
use: { ...devices['Desktop Edge'], channel: 'msedge' },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},

// Responsive breakpoints (chromium only — layout logic is shared)
{
name: 'desktop-1920',
use: { ...devices['Desktop Chrome'], viewport: { width: 1920, height: 1080 } },
testMatch: '**/responsive.spec.ts',
},
{
name: 'laptop-1440',
use: { ...devices['Desktop Chrome'], viewport: { width: 1440, height: 900 } },
testMatch: '**/responsive.spec.ts',
},
{
name: 'tablet-768',
use: { ...devices['Desktop Chrome'], viewport: { width: 768, height: 1024 } },
testMatch: '**/responsive.spec.ts',
},
],
});
116 changes: 83 additions & 33 deletions src/main/frontend/src/components/CodeGraphView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import * as d3 from 'd3';
import { useApi } from '@/hooks/useApi';
import { api } from '@/lib/api';
import type { KindEntry, NodeResponse, NodesListResponse } from '@/types/api';
import { ChevronRight, Home } from 'lucide-react';
import { ChevronRight, Home, X, FileCode } from 'lucide-react';
import { useFileSelection } from '@/contexts/FileSelectionContext';

// Reuse the kind-color mapping from ExplorerView for consistency
const KIND_COLORS: Record<string, string> = {
Expand Down Expand Up @@ -231,6 +232,8 @@ export default function CodeGraphView() {
const [drillTotal, setDrillTotal] = useState(0);
const [drillLoading, setDrillLoading] = useState(false);

const { selectedPath, selectedType, clearSelection } = useFileSelection();

const { data: kindsData, loading: kindsLoading } = useApi(() => api.getKinds(), []);

// Observe container size
Expand Down Expand Up @@ -261,16 +264,34 @@ export default function CodeGraphView() {
setDrillNodes([]);
setDrillTotal(0);
try {
const result: NodesListResponse = await api.getNodesByKind(kind, 200, 0);
setDrillNodes(result.nodes ?? []);
setDrillTotal(result.total ?? result.count ?? (result.nodes ?? []).length);
const result: NodesListResponse = await api.getNodesByKind(kind, 500, 0);
const nodes = result.nodes ?? [];
// If a file/directory is selected, filter nodes to that path
const filtered = selectedPath
? nodes.filter(n => {
if (!n.file_path) return false;
return selectedType === 'directory'
? n.file_path.startsWith(selectedPath)
: n.file_path === selectedPath;
})
: nodes;
setDrillNodes(filtered);
setDrillTotal(filtered.length);
} catch {
setDrillNodes([]);
setDrillTotal(0);
} finally {
setDrillLoading(false);
}
}, []);
}, [selectedPath, selectedType]);

// Re-run drill if file selection changes while already drilled into a kind
useEffect(() => {
if (selectedKind) {
handleDrillDown(selectedKind);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [selectedPath]);

const handleDrillUp = useCallback(() => {
setSelectedKind(null);
Expand All @@ -280,52 +301,81 @@ export default function CodeGraphView() {

const kinds: KindEntry[] = kindsData?.kinds ?? [];

// Friendly display name for the selected path
const selectedLabel = selectedPath
? selectedPath.split('/').pop() ?? selectedPath
: null;

return (
<div className="flex flex-col h-full space-y-3">
{/* Header + breadcrumb */}
<div className="flex items-center justify-between">
<div className="flex items-center justify-between flex-wrap gap-2">
<div>
<h1 className="text-2xl font-bold gradient-text">Code Graph</h1>
<p className="text-sm text-surface-400 mt-0.5">
{selectedKind
? drillTotal > drillNodes.length
? `Showing ${drillNodes.length} of ${drillTotal} "${selectedKind}" nodes`
: `${drillNodes.length} nodes of kind "${selectedKind}"`
? drillTotal > 0
? `${drillTotal} "${selectedKind}" nodes${selectedPath ? ` in ${selectedLabel}` : ''}`
: `No "${selectedKind}" nodes${selectedPath ? ` in ${selectedLabel}` : ''}`
: `${kinds.length} node kinds — click a tile to explore`}
</p>
</div>

{/* Breadcrumb */}
<nav className="flex items-center gap-1 text-sm">
<button
onClick={handleDrillUp}
className={`flex items-center gap-1 px-2 py-1 rounded transition-colors ${
selectedKind
? 'text-brand-400 hover:text-brand-300 hover:bg-surface-800/50 cursor-pointer'
: 'text-surface-500 cursor-default'
}`}
disabled={!selectedKind}
>
<Home className="w-3.5 h-3.5" />
<span>All Kinds</span>
</button>
{selectedKind && (
<>
<ChevronRight className="w-3.5 h-3.5 text-surface-600" />
<span
className="px-2 py-1 rounded text-brand-300 font-medium"
style={{ color: getKindColor(selectedKind) }}
>
{selectedKind.toUpperCase()}
<div className="flex items-center gap-2">
{/* Active file filter badge */}
{selectedPath && (
<div
className="flex items-center gap-1.5 px-2 py-1 rounded-md text-xs
bg-surface-800/70 border border-surface-700/50 text-surface-300"
data-testid="file-filter-badge"
>
<FileCode className="w-3 h-3 text-brand-400 shrink-0" />
<span className="truncate max-w-[200px]" title={selectedPath}>
{selectedLabel}
</span>
</>
<button
onClick={clearSelection}
aria-label="Clear file filter"
className="text-surface-500 hover:text-surface-300 ml-0.5"
>
<X className="w-3 h-3" />
</button>
</div>
)}
</nav>

{/* Breadcrumb */}
<nav className="flex items-center gap-1 text-sm" aria-label="Graph breadcrumb">
<button
onClick={handleDrillUp}
className={`flex items-center gap-1 px-2 py-1 rounded transition-colors ${
selectedKind
? 'text-brand-400 hover:text-brand-300 hover:bg-surface-800/50 cursor-pointer'
: 'text-surface-500 cursor-default'
}`}
disabled={!selectedKind}
>
<Home className="w-3.5 h-3.5" />
<span>All Kinds</span>
</button>
{selectedKind && (
<>
<ChevronRight className="w-3.5 h-3.5 text-surface-600" />
<span
className="px-2 py-1 rounded text-brand-300 font-medium"
style={{ color: getKindColor(selectedKind) }}
>
{selectedKind.toUpperCase()}
</span>
</>
)}
</nav>
</div>
</div>

{/* Treemap container */}
<div
ref={containerRef}
data-testid="graph-container"
className="flex-1 rounded-xl border border-surface-800/50 bg-surface-900/40 overflow-hidden"
>
{(kindsLoading || drillLoading) && (
Expand Down
Loading
Loading