feat: table format

This commit is contained in:
dextmorgn
2025-07-08 14:24:47 +02:00
parent d72f9347b9
commit cea9ea693d
26 changed files with 1268 additions and 86 deletions

View File

@@ -93,7 +93,7 @@ class OrgToAsnScanner(Scanner):
def __get_asn_from_asnmap(self, name: str) -> Dict[str, Any]:
try:
# Properly run the shell pipeline using shell=True
command = f"echo {name} | asnmap -silent -json | jq"
command = f"echo {name} | asnmap -silent -json | jq -s"
result = subprocess.run(
command,
shell=True,
@@ -126,10 +126,12 @@ class OrgToAsnScanner(Scanner):
return combined_data if combined_data["as_number"] else None
except json.JSONDecodeError:
except json.JSONDecodeError as e:
Logger.error(self.sketch_id, {"message": f"An error occurred while parsing the JSON output from asnmap: {str(e)}"})
return None
except Exception as e:
Logger.error(self.sketch_id, {"message": f"An error occurred while running asnmap: {str(e)}"})
return None
def postprocess(self, results: OutputType, original_input: InputType) -> OutputType:

View File

@@ -48,13 +48,15 @@ class AsnmapTool(DockerTool):
def is_installed(self) -> bool:
return super().is_installed()
def launch(self, item: str, type: Literal["domain", "org", "ip", "asn"] = "domain") -> Any:
def launch(self, item: str, type: Literal["domain", "organization", "ip", "asn"] = "domain") -> Any:
flags = {
"domain" : '-d',
'org': '-org',
'ip' : '-i',
'asn': '-a'
}
if(flags.get(type) is None):
raise ValueError(f"Invalid type: {type}")
flag = flags[type]
try:
# Use the -target argument as asnmap expects

View File

@@ -1,2 +1,2 @@
index=101
resume_from=8.39.215.189
resume_from=35.207.115.44
index=5060

View File

@@ -21,7 +21,7 @@
"start": "electron-vite preview",
"dev": "electron-vite dev",
"build": "npm run typecheck && electron-vite build",
"postinstall": "electron-builder install-app-deps",
"postinstall": "npx electron-builder install-app-deps",
"build:unpack": "npm run build && electron-builder --dir",
"build:win": "npm run build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
@@ -62,6 +62,7 @@
"@react-sigma/core": "^5.0.4",
"@tailwindcss/vite": "^3.4.1",
"@tanstack/react-query": "^5.79.0",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.9",
"@tanstack/router-plugin": "^1.121.7",
"@tiptap/extension-code-block-lowlight": "^2.12.0",
@@ -97,6 +98,7 @@
"graphology-layout-force": "^0.2.4",
"graphology-layout-forceatlas2": "^0.10.1",
"input-otp": "^1.4.2",
"leaflet": "^1.9.4",
"lowlight": "^3.3.0",
"lucide-react": "^0.511.0",
"marked": "^15.0.12",
@@ -149,4 +151,4 @@
"vite": "^5.3.1",
"zustand": "^5.0.3"
}
}
}

View File

@@ -1,11 +1,11 @@
<!DOCTYPE html>
<html lang="en" theme="dark">
<head>
<!-- <script
<script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
>
</script> -->
</script>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

View File

@@ -1,5 +1,4 @@
"use client"
import { memo } from "react"
import { memo, useCallback } from "react"
import { cn } from "@/lib/utils"
import { CopyButton } from "@/components/copy"
import { Button } from "@/components/ui/button"
@@ -9,9 +8,9 @@ import { useGraphStore } from "@/stores/graph-store"
export default function DetailsPanel({ data }: { data: any }) {
const setOpenNodeEditorModal = useGraphStore(state => state.setOpenNodeEditorModal)
const handleEdit = () => {
const handleEdit = useCallback(() => {
setOpenNodeEditorModal(true)
}
}, [])
return (
<div className="overflow-y-auto overflow-x-hidden w-full min-w-0 h-full min-h-0">
@@ -32,7 +31,7 @@ export default function DetailsPanel({ data }: { data: any }) {
</div>
{data?.description && (
<div className="px-4 py-3 border-b border-border">
<div
<div
className="text-sm text-muted-foreground prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: data.description }}
/>

View File

@@ -8,11 +8,10 @@ import { CreateRelationDialog } from './create-relation'
import GraphLoader from './graph-loader'
import Loader from '../loader'
import WallEditor from './wall/wall'
import GraphReactForce from './graph-react-force'
// import GraphReactForce3D from './graph-react-force-3d'
import { useGraphControls } from '@/stores/graph-controls-store'
import { NodeEditorModal } from './wall/node-editor-modal'
// const GraphReactForce = lazy(() => import('./graph-react-force'))
import NodesTable from '../table'
const GraphReactForce = lazy(() => import('./graph-react-force'))
const Graph = lazy(() => import('./graph'))
@@ -118,22 +117,14 @@ const GraphPanel = ({ graphData, isLoading, isRefetching }: GraphPanelProps) =>
</div>
</div>
}>
{/* {nodes?.length > 500 ? (
<Graph />
) : nodes?.length > 500 ? (
view === "force3d" ? <GraphReactForce3D /> : <GraphReactForce />
) : (
view === "force3d" ? <GraphReactForce3D /> :
view === "force" ? <GraphReactForce /> :
<WallEditor isRefetching={isRefetching} isLoading={loading} />
)} */}
{/* <Graph /> */}
{nodes?.length > 500 ? (
<Graph />
) : (
view === "force" ? <GraphReactForce /> :
<WallEditor isRefetching={isRefetching} isLoading={loading} />
)}
{nodes?.length > 500? (
<>{view === "table" ? <NodesTable /> : <Graph />}</>
) : (<>
{view === "force" && <GraphReactForce />}
{view === "hierarchy" && <WallEditor isRefetching={isRefetching} isLoading={loading} />}
{view === "table" && <NodesTable />}
</>)}
</Suspense>
{/* <Graph /> */}
{/* <GraphSigma /> */}

View File

@@ -18,6 +18,7 @@ import { formatDistanceToNow } from "date-fns"
import { useQuery } from "@tanstack/react-query"
import { transformService } from "@/api/transfrom-service"
import { useParams } from "@tanstack/react-router"
import { capitalizeFirstLetter } from "@/lib/utils"
interface Transform {
id: string
@@ -36,9 +37,9 @@ const LaunchTransform = ({ values, type }: { values: string[], type: string }) =
const { data: transforms, isLoading } = useQuery({
queryKey: ["transforms", type],
queryFn: () => transformService.get(type),
refetchOnWindowFocus: true,
})
queryFn: () => transformService.get(capitalizeFirstLetter(type)),
// queryFn: () => transformService.get(),
});
const handleOpenModal = () => {

View File

@@ -0,0 +1,54 @@
import React from "react";
import { useQuery } from "@tanstack/react-query";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
type MapFromAddressProps = {
address: string;
height?: string;
zoom?: number;
};
export const MapFromAddress: React.FC<MapFromAddressProps> = ({
address,
height = "400px",
zoom = 15,
}) => {
const { data, isLoading, isError } = useQuery({
queryKey: ["geocode", address],
queryFn: async () => {
const res = await fetch(
`https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(address)}`
);
const json = await res.json();
if (!json || json.length === 0) throw new Error("Address not found");
return {
lat: parseFloat(json[0].lat),
lon: parseFloat(json[0].lon),
};
},
});
const mapId = `leaflet-map-${btoa(address).replace(/[^a-zA-Z0-9]/g, "")}`;
React.useEffect(() => {
if (!data) return;
const map = L.map(mapId).setView([data.lat, data.lon], zoom);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
}).addTo(map);
L.marker([data.lat, data.lon]).addTo(map).bindPopup(address).openPopup();
return () => {
map.remove(); // cleanup
};
}, [data, mapId, address, zoom]);
if (isLoading) return <div className="p-3"><p>Loading map</p></div>;
if (isError) return <div className="p-3"><p>Could not find the address.</p></div>;
return <div id={mapId} style={{ height }} />;
};

View File

@@ -61,14 +61,14 @@ const NodeRenderer = memo(
)
return (
<div className="flex items-center hover:bg-muted border-b h-full">
<div className="flex items-center overflow-hidden hover:bg-muted border-b h-full">
<div className="pl-2">
<Checkbox checked={isNodeChecked(node.id)} onCheckedChange={handleCheckboxChange} className="mr-1 border-border" />
</div>
<Button
variant={"ghost"}
className={cn(
"flex-1 flex truncate items-center justify-start p-4 !py-5 rounded-none text-left border-l-2 border-l-transparent h-full",
"flex-1 flex truncate mt-0 overflow-hidden items-center justify-start p-4 !py-5 rounded-none text-left border-l-2 border-l-transparent h-full",
)}
onClick={handleClick}
>

View File

@@ -14,6 +14,7 @@ import {
GitPullRequestCreate,
GitFork,
Waypoints,
Table
} from "lucide-react"
import { memo, useCallback } from "react"
import { sketchService } from "@/api/sketch-service"
@@ -59,6 +60,7 @@ const ToolbarButton = memo(function ToolbarButton({
export const Toolbar = memo(function Toolbar({ isLoading }: { isLoading: boolean }) {
const { id: sketchId } = useParams({ from: "/_auth/dashboard/investigations/$investigationId/$type/$id" })
const selectedNodes = useGraphStore((state) => state.selectedNodes || [])
const view = useGraphControls(s => s.view)
const setOpenAddRelationDialog = useGraphStore((state) => state.setOpenAddRelationDialog)
const setView = useGraphControls((s) => s.setView)
const removeNodes = useGraphStore((state) => state.removeNodes)
@@ -68,6 +70,7 @@ export const Toolbar = memo(function Toolbar({ isLoading }: { isLoading: boolean
const onLayout = useGraphControls((s) => s.onLayout);
const { confirm } = useConfirm()
const refetchGraph = useGraphControls((s) => s.refetchGraph)
const clearSelectedNodes = useGraphStore((s) => s.clearSelectedNodes)
const nodesLength = useGraphStore((s) => s.getNodesLength())
const handleRefresh = useCallback(() => {
@@ -83,13 +86,14 @@ export const Toolbar = memo(function Toolbar({ isLoading }: { isLoading: boolean
setOpenAddRelationDialog(true)
}, [])
const handleDeleteNodes = async () => {
const handleDeleteNodes = useCallback(async () => {
if (!selectedNodes.length) return
if (!await confirm({ title: `You are about to delete ${selectedNodes.length} node(s).`, message: "The action is irreversible." })) return
toast.promise(
(async () => {
removeNodes(selectedNodes.map((n) => n.id))
clearSelectedNodes()
return sketchService.deleteNodes(sketchId, JSON.stringify({ nodeIds: selectedNodes.map((n) => n.id) }))
})(),
{
@@ -98,19 +102,19 @@ export const Toolbar = memo(function Toolbar({ isLoading }: { isLoading: boolean
error: 'Failed to delete nodes.'
}
)
}
}, [selectedNodes, confirm, removeNodes, clearSelectedNodes, sketchId])
const isMoreThanZero = selectedNodes.length > 0
const isTwo = selectedNodes.length == 2
const isGraphOnly = nodesLength > 500
const isCosmoOnly = nodesLength > 3000
// const isCosmoOnly = nodesLength > 3000
const handleForceLayout = useCallback(() => {
setView("force")
}, [setView])
// const handleForce3DLayout = useCallback(() => {
// setView("force3d")
// }, [setView])
const handleTableLayout = useCallback(() => {
setView("table")
}, [setView])
const handleDagreLayoutTB = useCallback(() => {
setView("hierarchy")
@@ -145,27 +149,30 @@ export const Toolbar = memo(function Toolbar({ isLoading }: { isLoading: boolean
badge={isMoreThanZero ? selectedNodes.length : null}
/>
{/* <ToolbarButton disabled icon={<Filter className="h-4 w-4 opacity-70" />} tooltip="Filter" /> */}
<ToolbarButton
icon={<ZoomIn className="h-4 w-4 opacity-70" />}
tooltip="Zoom In"
onClick={zoomIn}
/>
<ToolbarButton
icon={<Minus className="h-4 w-4 opacity-70" />}
tooltip="Zoom Out"
onClick={zoomOut}
/>
<ToolbarButton
icon={<Maximize className="h-4 w-4 opacity-70" />}
tooltip="Fit to View"
onClick={zoomToFit}
/>
<ToolbarButton
icon={<GitFork className="h-4 w-4 opacity-70 rotate-180" />}
tooltip={isGraphOnly ? "Graph is too large to render in hierarchy layout" : `Hierarchy (${isMac ? '⌘' : 'ctrl'}+Y)`}
onClick={handleDagreLayoutTB}
disabled={isGraphOnly}
/>
{view !== "table" &&
<>
< ToolbarButton
icon={<ZoomIn className="h-4 w-4 opacity-70" />}
tooltip="Zoom In"
onClick={zoomIn}
/>
<ToolbarButton
icon={<Minus className="h-4 w-4 opacity-70" />}
tooltip="Zoom Out"
onClick={zoomOut}
/>
<ToolbarButton
icon={<Maximize className="h-4 w-4 opacity-70" />}
tooltip="Fit to View"
onClick={zoomToFit}
/>
<ToolbarButton
icon={<GitFork className="h-4 w-4 opacity-70 rotate-180" />}
tooltip={isGraphOnly ? "Graph is too large to render in hierarchy layout" : `Hierarchy (${isMac ? '⌘' : 'ctrl'}+Y)`}
onClick={handleDagreLayoutTB}
disabled={isGraphOnly}
/>
</>}
<ToolbarButton
icon={<GitFork className="h-4 w-4 opacity-70 rotate-90" />}
tooltip={isGraphOnly ? "Graph is too large to render in hierarchy layout" : `Hierarchy (${isMac ? '⌘' : 'ctrl'}+Y)`}
@@ -174,9 +181,14 @@ export const Toolbar = memo(function Toolbar({ isLoading }: { isLoading: boolean
/>
<ToolbarButton
icon={<Waypoints className="h-4 w-4 opacity-70" />}
tooltip={"Graph"}
tooltip={"Graph view"}
onClick={handleForceLayout}
disabled={isCosmoOnly}
// disabled={isCosmoOnly}
/>
<ToolbarButton
icon={<Table className="h-4 w-4 opacity-70" />}
tooltip={"Table view"}
onClick={handleTableLayout}
/>
{/* <ToolbarButton

View File

@@ -9,7 +9,7 @@ import { Transform } from '@/types';
import { GraphNode, useGraphStore } from '@/stores/graph-store';
import { useLaunchTransform } from '@/hooks/use-launch-transform';
import { useParams } from '@tanstack/react-router';
import { cn } from '@/lib/utils';
import { capitalizeFirstLetter, cn } from '@/lib/utils';
import { useConfirm } from '@/components/use-confirm-dialog';
import { toast } from 'sonner';
import { sketchService } from '@/api/sketch-service';
@@ -21,10 +21,6 @@ import {
} from '@/components/ui/tooltip';
import { useLayoutStore } from '@/stores/layout-store';
function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}
export default function ContextMenu({
node,
top,

View File

@@ -9,6 +9,7 @@ import { Badge } from "@/components/ui/badge"
import { Edit3, Save, X, Hash, Type, FileText, Tag, Check } from "lucide-react"
import type { NodeData } from "@/types"
import { useGraphStore } from "@/stores/graph-store"
import { MapFromAddress } from "../map"
export const NodeEditorModal: React.FC = () => {
const currentNode = useGraphStore(state => state.currentNode)
@@ -105,7 +106,7 @@ export const NodeEditorModal: React.FC = () => {
</CardTitle>
</CardHeader>
<CardContent>
<div
<div
className="text-sm text-muted-foreground prose prose-sm max-w-none"
dangerouslySetInnerHTML={{ __html: currentNode.data.description }}
/>
@@ -267,6 +268,19 @@ export const NodeEditorModal: React.FC = () => {
</div>
</CardContent>
</Card>
{formData.type === "location" && <Card className="border bg-muted/30">
<CardHeader className="pb-3">
<CardTitle className="text-base font-medium">
Location
</CardTitle>
</CardHeader>
<CardContent>
<div className="overflow-hidden rounded-lg bg-background border">
<MapFromAddress address={formData.label as string} />
</div>
</CardContent>
</Card>}
</div>
</div>

View File

@@ -0,0 +1,506 @@
import { ColumnDef } from "@tanstack/react-table"
import { useIcon } from "@/hooks/use-icon"
import { MoreVertical, AlertTriangle, CheckCircle, Clock, XCircle } from "lucide-react"
import { Button } from "@/components/ui/button"
import { ArrowUpDown } from "lucide-react"
import { TypeBadge } from "@/components/type-badge"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Checkbox } from "../ui/checkbox"
import { useCallback, memo } from "react"
import { GraphNode, useGraphStore } from "@/stores/graph-store"
import { Badge } from "@/components/ui/badge"
// Memoized icon component to prevent unnecessary re-renders
const MemoizedIcon = memo(({ type, size = 16 }: { type: string; size?: number }) => {
const IconComponent = useIcon(type)
return <IconComponent size={size} />
})
MemoizedIcon.displayName = 'MemoizedIcon'
// Helper function to get status styling
const getStatusStyle = (status: string) => {
switch (status?.toLowerCase()) {
case 'active':
return { bg: 'bg-emerald-50 dark:bg-emerald-950/20', text: 'text-emerald-700 dark:text-emerald-300', border: 'border-emerald-200 dark:border-emerald-800', icon: CheckCircle }
case 'pending':
return { bg: 'bg-amber-50 dark:bg-amber-950/20', text: 'text-amber-700 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800', icon: Clock }
case 'error':
return { bg: 'bg-red-50 dark:bg-red-950/20', text: 'text-red-700 dark:text-red-300', border: 'border-red-200 dark:border-red-800', icon: XCircle }
case 'warning':
return { bg: 'bg-orange-50 dark:bg-orange-950/20', text: 'text-orange-700 dark:text-orange-300', border: 'border-orange-200 dark:border-orange-800', icon: AlertTriangle }
default:
return { bg: 'bg-gray-50 dark:bg-gray-900', text: 'text-gray-700 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-700', icon: Clock }
}
}
// Helper function to get priority styling
const getPriorityStyle = (priority: string) => {
switch (priority?.toLowerCase()) {
case 'high':
return { bg: 'bg-red-50 dark:bg-red-950/20', text: 'text-red-700 dark:text-red-300', border: 'border-red-200 dark:border-red-800' }
case 'medium':
return { bg: 'bg-amber-50 dark:bg-amber-950/20', text: 'text-amber-700 dark:text-amber-300', border: 'border-amber-200 dark:border-amber-800' }
case 'low':
return { bg: 'bg-emerald-50 dark:bg-emerald-950/20', text: 'text-emerald-700 dark:text-emerald-300', border: 'border-emerald-200 dark:border-emerald-800' }
default:
return { bg: 'bg-gray-50 dark:bg-gray-900', text: 'text-gray-700 dark:text-gray-300', border: 'border-gray-200 dark:border-gray-700' }
}
}
// Helper function to get risk level styling
const getRiskStyle = (risk: string) => {
switch (risk?.toLowerCase()) {
case 'critical':
return { bg: 'bg-red-100 dark:bg-red-900/30', text: 'text-red-800 dark:text-red-200' }
case 'high':
return { bg: 'bg-orange-100 dark:bg-orange-900/30', text: 'text-orange-800 dark:text-orange-200' }
case 'medium':
return { bg: 'bg-amber-100 dark:bg-amber-900/30', text: 'text-amber-800 dark:text-amber-200' }
case 'low':
return { bg: 'bg-emerald-100 dark:bg-emerald-900/30', text: 'text-emerald-800 dark:text-emerald-200' }
default:
return { bg: 'bg-gray-100 dark:bg-gray-800', text: 'text-gray-800 dark:text-gray-200' }
}
}
// Helper function to get confidence color
const getConfidenceColor = (confidence: number) => {
if (confidence >= 80) return 'text-emerald-600 dark:text-emerald-400'
if (confidence >= 60) return 'text-amber-600 dark:text-amber-400'
if (confidence >= 40) return 'text-orange-600 dark:text-orange-400'
return 'text-red-600 dark:text-red-400'
}
export const columns: ColumnDef<GraphNode>[] = [
{
size: 50,
minSize: 50,
maxSize: 50,
enableResizing: false,
accessorKey: "icon",
header: () => {
const setSelectedNodes = useGraphStore(s => s.setSelectedNodes)
const nodes = useGraphStore(s => s.nodes)
const handleToggleCheckAll = useCallback(
(checked: boolean) => {
if (!checked) {
setSelectedNodes([])
return
}
else setSelectedNodes(nodes)
},
[nodes, setSelectedNodes],
)
return (<div className="flex items-center h-full"><Checkbox onCheckedChange={handleToggleCheckAll} /></div>)
},
cell: ({ row }) => {
const toggleNodeSelection = useGraphStore(s => s.toggleNodeSelection)
const selectedNodes = useGraphStore(s => s.selectedNodes)
const toggleNode = useCallback(() => toggleNodeSelection(row.original, true), [])
const isNodeChecked = useCallback(
(nodeId: string) => {
return selectedNodes.some((node) => node.id === nodeId)
},
[selectedNodes],
)
return <div className="flex items-center"><Checkbox checked={isNodeChecked(row.original.id)} onCheckedChange={toggleNode} /></div>
},
},
{
enableResizing: true,
accessorKey: "data.label",
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Label
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
cell: ({ row }) => {
const setCurrentNode = useGraphStore(s => s.setCurrentNode)
// const IconComponent = useIcon(row.original.data.type)
const setOpenNodeEditorModal = useGraphStore(s => s.setOpenNodeEditorModal)
const openEdit = useCallback((e: React.MouseEvent) => {
e.stopPropagation();
const typedNode = row.original as GraphNode
setCurrentNode(typedNode)
setOpenNodeEditorModal(true)
}, [row.original, setCurrentNode, setOpenNodeEditorModal])
return (
<button
onClick={openEdit}
className="text-left font-medium flex items-center gap-2 h-full truncate text-ellipsis w-full rounded-md p-2 transition-colors duration-200"
>
{/* <div className="flex-shrink-0">
<IconComponent size={16} />
</div> */}
<span className="hover:text-primary truncate text-[.9rem] block font-medium transition-colors duration-200">
{row.original.data.label}
</span>
</button>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Type
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
size: 100,
minSize: 110,
maxSize: 150,
accessorKey: "data.type",
cell: ({ row }) => {
const type = row.original.data.type
return (
<div className="text-center flex justify-center w-full font-medium flex items-center gap-2">
<TypeBadge className="w-full" type={type} />
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Status
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.status",
cell: ({ row }) => {
const status = row.original.data.status || 'Active'
const style = getStatusStyle(status)
const IconComponent = style.icon
return (
<div className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md border ${style.bg} ${style.text} ${style.border} text-xs font-medium`}>
<IconComponent size={12} />
<span className="capitalize">{status}</span>
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Priority
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.priority",
cell: ({ row }) => {
const priority = row.original.data.priority || 'Medium'
const style = getPriorityStyle(priority)
return (
<Badge
variant="outline"
className={`${style.bg} ${style.text} ${style.border} font-medium capitalize text-xs`}
>
{priority}
</Badge>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Category
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.category",
cell: ({ row }) => {
const category = row.original.data.category || 'Network'
return (
<div className="text-left font-medium text-gray-700 dark:text-gray-300">
{category}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Source
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.source",
cell: ({ row }) => {
const source = row.original.data.source || 'Scanner'
return (
<div className="text-left font-medium text-gray-700 dark:text-gray-300">
{source}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Confidence
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.confidence",
cell: ({ row }) => {
const confidence = 85 // row.original.data.confidence || 85
const color = getConfidenceColor(confidence)
return (
<div className="flex items-center gap-2">
<div className="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-300 ${color.replace('text-', 'bg-')}`}
style={{ width: `${confidence}%` }}
/>
</div>
<span className={`text-xs font-medium ${color} min-w-[2.5rem]`}>
{confidence}%
</span>
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Risk Level
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.riskLevel",
cell: ({ row }) => {
const risk = row.original.data.riskLevel || 'Low'
const style = getRiskStyle(risk)
return (
<div className={`inline-flex items-center px-2 py-1 rounded-md ${style.bg} ${style.text} text-xs font-medium capitalize`}>
{risk}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Tags
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.tags",
cell: ({ row }) => {
const tags = row.original.data.tags || 'infrastructure'
return (
<div className="text-left font-medium text-gray-600 dark:text-gray-400 text-xs">
{tags}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Last Updated
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.lastUpdated",
cell: ({ row }) => {
const date = row.original.data.lastUpdated || '2024-01-15'
return (
<div className="text-left font-medium text-gray-500 dark:text-gray-400 text-xs">
{date}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Owner
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.owner",
cell: ({ row }) => {
const owner = row.original.data.owner || 'Team A'
return (
<div className="text-left font-medium text-gray-700 dark:text-gray-300">
{owner}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Version
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.version",
cell: ({ row }) => {
const version = row.original.data.version || '1.2.3'
return (
<div className="text-left font-medium text-gray-500 dark:text-gray-400 font-mono text-xs">
v{version}
</div>
)
}
},
{
enableResizing: true,
header: ({ column }) => {
return (
<Button
variant="ghost"
className="h-7"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
Environment
<ArrowUpDown className="h-4 w-4" />
</Button>
)
},
accessorKey: "data.environment",
cell: ({ row }) => {
const env = row.original.data.environment || 'Production'
const isProduction = env.toLowerCase() === 'production'
return (
<div className={`text-left font-medium text-xs ${isProduction
? 'text-red-600 dark:text-red-400'
: 'text-emerald-600 dark:text-emerald-400'
}`}>
{env}
</div>
)
}
},
{
id: "actions",
size: 50,
minSize: 50,
maxSize: 50,
enableResizing: false,
header: ({ column }) => {
return (
<div className="w-[50]"></div>
)
},
cell: ({ row }) => {
return (
<div className="flex items-center justify-center w-full h-full">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<div>
<Button variant="ghost" className="h-8 w-8 p-0 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors duration-200">
<span className="sr-only">Open menu</span>
<MoreVertical className="h-4 w-4" />
</Button>
</div>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
></DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
)
},
},
]

View File

@@ -0,0 +1,327 @@
import {
type Table,
type Row,
ColumnDef,
flexRender,
getCoreRowModel,
useReactTable,
SortingState,
getSortedRowModel,
} from "@tanstack/react-table";
import {
useVirtualizer,
} from "@tanstack/react-virtual";
import React from "react";
import { Row as RowItem } from "@/types/table";
import { cn } from "@/lib/utils";
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
}
// Stable style objects to prevent re-renders
const BASE_ROW_STYLE = {
display: "table" as const,
width: "100%",
tableLayout: "fixed" as const,
zIndex: 0,
};
const TABLE_STYLE = {
borderCollapse: "separate" as const,
tableLayout: "fixed" as const,
borderSpacing: 0,
width: "100%",
};
const TBODY_STYLE = {
display: "block" as const,
};
// Memoized row component to prevent unnecessary re-renders
const VirtualRow = React.memo<{
row: Row<RowItem>;
virtualRow: any;
lastCellIndex: number;
columnSizeVars: Record<string, number>;
}>(({ row, virtualRow, lastCellIndex, columnSizeVars }) => {
const visibleCells = row.getVisibleCells();
// Memoize the row style with transform
const rowStyle = React.useMemo(() => ({
...BASE_ROW_STYLE,
height: `${virtualRow.size}px`,
}), [virtualRow.size]);
return (
<tr
key={row.id}
data-index={virtualRow.index}
className="border-b hover:bg-muted/50 divide-x overflow-y-hidden"
style={rowStyle}
>
{visibleCells.map((cell, index) => (
<VirtualCell
key={cell.id}
cell={cell}
index={index}
isFirst={index === 0 || index === 1}
isBeforeLast={index === lastCellIndex - 1}
isLast={index === lastCellIndex}
columnSizeVars={columnSizeVars}
/>
))}
</tr>
);
});
// Memoized cell component
const VirtualCell = React.memo<{
cell: any;
index: number;
isFirst: boolean;
isLast: boolean;
isBeforeLast: boolean,
columnSizeVars: Record<string, number>;
}>(({ cell, index, isFirst, isLast, isBeforeLast, columnSizeVars }) => {
const cellStyle = React.useMemo(() => ({
width: `calc(var(--col-${cell.column.id}-size) * 1px)`,
minWidth: `calc(var(--col-${cell.column.id}-size) * 1px)`,
maxWidth: `calc(var(--col-${cell.column.id}-size) * 1px)`,
height: "40px",
verticalAlign: "middle",
textAlign: "center"
}), [cell.column.id, columnSizeVars[`--col-${cell.column.id}-size`]]);
const cellClassName = React.useMemo(() => cn(
"px-4 py-1 truncate relative overflow-hidden",
!isBeforeLast && !isLast && "border-r",
index === 0 && "sticky z-10 left-0 bg-card border-b border-r",
index === 1 && "sticky z-11 left-[50px] bg-card border-b border-r",
isLast && "sticky right-0 z-10 bg-background px-0 border-l border-b",
), [isFirst, isLast, index, isBeforeLast]);
return (
<td
className={cellClassName}
//@ts-ignore
style={cellStyle}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
);
});
// Table body component
const TableBody = ({ table, tableContainerRef, columnSizeVars }: {
table: Table<any>;
tableContainerRef: React.RefObject<HTMLDivElement>;
columnSizeVars: Record<string, number>;
}) => {
const { rows } = table.getRowModel();
const rowVirtualizer = useVirtualizer({
count: rows.length,
estimateSize: () => 40, // Hauteur estimée de chaque ligne
getScrollElement: () => tableContainerRef.current,
});
// Memoize empty state
const emptyState = React.useMemo(() => (
<tbody>
<tr>
<td colSpan={table.getHeaderGroups()[0]?.headers.length || 1}>
<div className="flex items-center justify-center h-32 text-muted-foreground">
<div className="text-center">
<div className="text-md font-medium">No data found yet.</div>
<div className="text-xs">No nodes to display</div>
</div>
</div>
</td>
</tr>
</tbody>
), [table.getHeaderGroups()[0]?.headers.length]);
const virtualRows = rowVirtualizer.getVirtualItems();
const totalSize = rowVirtualizer.getTotalSize();
const lastCellIndex = rows[0]?.getVisibleCells().length - 1 || 0;
const tbodyStyle = React.useMemo(() => ({
...TBODY_STYLE,
height: `${totalSize}px`,
paddingTop: virtualRows.length > 0 ? `${virtualRows[0]?.start ?? 0}px` : 0,
}), [totalSize, virtualRows]);
if (rows.length === 0) {
return emptyState;
}
return (
<tbody style={tbodyStyle}>
{virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<RowItem>;
return (
<VirtualRow
key={row.id}
row={row}
virtualRow={virtualRow}
lastCellIndex={lastCellIndex}
columnSizeVars={columnSizeVars}
/>
);
})}
{/* Spacer pour maintenir la hauteur totale */}
{virtualRows.length > 0 && (
<tr style={{ height: `${totalSize - (virtualRows[virtualRows.length - 1]?.end ?? 0)}px` }}>
<td colSpan={table.getHeaderGroups()[0]?.headers.length || 1} style={{ padding: 0, border: 'none' }} />
</tr>
)}
</tbody>
);
};
export const MemoizedTableBody = React.memo(
TableBody,
(prev, next) => {
const prevRows = prev.table.getRowModel().rows;
const nextRows = next.table.getRowModel().rows;
const prevSort = prev.table.getState().sorting;
const nextSort = next.table.getState().sorting;
const prevFilter = prev.table.getState().columnFilters;
const nextFilter = next.table.getState().columnFilters;
// 1. Si le tri ou les filtres changent, on re-render
if (prevSort !== nextSort) return false;
if (prevFilter !== nextFilter) return false;
// 2. Si le nombre de lignes change, on re-render
if (prevRows.length !== nextRows.length) return false;
return prevRows === nextRows;
}
) as typeof TableBody
// Memoized header cell component
const HeaderCell = React.memo<{
header: any;
index: number;
isFirst: boolean;
isLast: boolean;
isBeforeLast: boolean;
columnSizeVars: Record<string, number>;
}>(({ header, index, isFirst, isLast, isBeforeLast, columnSizeVars }) => {
const headerStyle = React.useMemo(() => ({
width: `calc(var(--header-${header.id}-size) * 1px)`,
minWidth: `calc(var(--header-${header.id}-size) * 1px)`,
maxWidth: `calc(var(--header-${header.id}-size) * 1px)`,
}), [header.id, columnSizeVars[`--header-${header.id}-size`]]);
const headerClassName = React.useMemo(() => cn(
!isBeforeLast && !isLast && "border-r",
"px-4 py-1 text-center font-medium text-muted-foreground border-b overflow-hidde",
isFirst && `!sticky z-30`,
index === 0 && "left-0 bg-card",
index === 1 && "left-[50px] bg-card",
isLast && "!sticky right-0 z-30 bg-background border-l",
), [isFirst, isLast, index, isBeforeLast]);
return (
<th
className={headerClassName}
style={headerStyle}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getCanResize() && (
<div
onDoubleClick={() => header.column.resetSize()}
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={`resizer ${header.column.getIsResizing() ? "isResizing" : ""}`}
/>
)}
</th>
);
});
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const tableContainerRef = React.useRef<HTMLDivElement>(null);
const [sorting, setSorting] = React.useState<SortingState>([]);
const table = useReactTable({
data,
columns,
state: {
sorting,
},
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
getCoreRowModel: getCoreRowModel(),
columnResizeMode: "onChange",
defaultColumn: {
minSize: 60,
maxSize: 800,
},
debugTable: false,
debugHeaders: false,
debugColumns: false,
});
// Optimize column size variables calculation
const columnSizeVars = React.useMemo(() => {
const headers = table.getFlatHeaders();
const colSizes: { [key: string]: number } = {};
for (let i = 0; i < headers.length; i++) {
const header = headers[i]!;
colSizes[`--header-${header.id}-size`] = header.getSize();
colSizes[`--col-${header.column.id}-size`] = header.column.getSize();
}
return colSizes;
}, [
table.getState().columnSizing,
// Only depend on the actual sizing values, not the entire state objects
JSON.stringify(table.getState().columnSizing),
]);
// Memoize table style
const tableStyle = React.useMemo(() => ({
...TABLE_STYLE,
...columnSizeVars,
width: table.getTotalSize(),
}), [columnSizeVars, table.getTotalSize()]);
// Memoize header groups and calculate indices once
const headerGroups = table.getHeaderGroups();
const lastColumnIndex = headerGroups[0]?.headers.length - 1;
return (
<div className="h-full w-full overflow-auto border-t p-0 relative" ref={tableContainerRef}>
<table style={tableStyle}>
<thead className="sticky top-0 z-20 bg-background">
{headerGroups.map((headerGroup) => (
<tr key={headerGroup.id} className="px-0">
{headerGroup.headers.map((header, index) => (
<HeaderCell
key={header.id}
header={header}
index={index}
isBeforeLast={index === lastColumnIndex - 1}
isFirst={index === 0 || index === 1}
isLast={index === lastColumnIndex}
columnSizeVars={columnSizeVars}
/>
))}
</tr>
))}
</thead>
<MemoizedTableBody
table={table}
tableContainerRef={tableContainerRef}
columnSizeVars={columnSizeVars}
/>
</table>
</div>
);
}

View File

@@ -0,0 +1,17 @@
import { useGraphStore } from "@/stores/graph-store"
import { columns } from "./columns"
import { DataTable } from "./data-table"
import React from "react"
export default function NodesTable() {
const nodes = useGraphStore(s => s.nodes)
// Memoize columns to prevent unnecessary re-renders
const memoizedColumns = React.useMemo(() => columns, [])
return (
<div className="w-full pt-14">
<DataTable columns={memoizedColumns} data={nodes} />
</div>
)
}

View File

@@ -68,13 +68,13 @@ const TransformEditorFlow = memo(({ initialEdges, initialNodes, theme, transform
const { transformId } = useParams({ strict: false })
const [showModal, setShowModal] = useState(false)
const hasInitialized = useRef(false)
// #### Simulation State ####
const [isSimulating, setIsSimulating] = useState(false)
const [currentStepIndex, setCurrentStepIndex] = useState(0)
const [simulationSpeed, setSimulationSpeed] = useState(1000) // ms per step
const [transformBranches, setTransformsBranches] = useState<any[]>([])
// #### Transform Store State ####
const nodes = useTransformStore(state => state.nodes)
const edges = useTransformStore(state => state.edges)
@@ -312,7 +312,15 @@ const TransformEditorFlow = memo(({ initialEdges, initialNodes, theme, transform
// Update the updateNodeState function with proper types
const updateNodeState = useCallback((nds: TransformNode[], nodeId: string, state: 'pending' | 'processing' | 'completed' | 'error') => {
return nds.map((node) => {
// focus on node
if (node.id === nodeId) {
const nodeWidth = node.measured?.width ?? 0
const nodeHeight = node.measured?.height ?? 0
setCenter(
node.position.x + nodeWidth / 2,
node.position.y + nodeHeight / 2 + 20,
{ duration: 500, zoom: 1 }
)
return {
...node,
data: {
@@ -323,7 +331,7 @@ const TransformEditorFlow = memo(({ initialEdges, initialNodes, theme, transform
}
return node
})
}, [])
}, [setCenter])
// #### Simulation Effect ####
useEffect(() => {
@@ -378,6 +386,7 @@ const TransformEditorFlow = memo(({ initialEdges, initialNodes, theme, transform
}
} else {
// End of simulation
fitView({ duration: 500 })
setIsSimulating(false)
}
@@ -525,8 +534,8 @@ const TransformEditorFlow = memo(({ initialEdges, initialNodes, theme, transform
<SelectContent>
<SelectItem value="2000">Slow</SelectItem>
<SelectItem value="1000">Normal</SelectItem>
<SelectItem value="500">Fast</SelectItem>
<SelectItem value="100">Very Fast</SelectItem>
<SelectItem value="750">Fast</SelectItem>
<SelectItem value="400">Very fast</SelectItem>
</SelectContent>
</Select>
</Panel>}

View File

@@ -14,7 +14,7 @@ function Checkbox({
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
@@ -23,7 +23,7 @@ function Checkbox({
data-slot="checkbox-indicator"
className="flex items-center justify-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
<CheckIcon className="size-3.5 text-white" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)

View File

@@ -100,7 +100,7 @@ export const useChat = ({ onContentUpdate, onSuccess, editor }: UseChatOptions)
}
const content = typeof currentContent === 'string' ? currentContent : JSON.stringify(currentContent)
const nodeLabel = selectedNodes.map(node => node.data.label).join(", ")
const context = selectedNodes ? `${nodeLabel}\n\nContext:\n${content}` : content
const context = selectedNodes ? `Tu te mets dans la peau d'un analyste CTI/OSINT. Dans tes éléments de réponse, n'indique pas que tu es un analyste CTI/OSINT, juste la réponse. Aujourd'hui, voici ton cas: ${nodeLabel}\n\nContext:\n${content}` : content
const fullPrompt = `${customPrompt}\n\nContext:\n${context}`
setPromptOpen(false)
setCustomPrompt("")

View File

@@ -363,4 +363,9 @@ export function hexToRgba(hex: string, alpha: number) {
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
}
export function capitalizeFirstLetter(string: string) {
return string.charAt(0).toUpperCase() + string.slice(1)
}

View File

@@ -2,14 +2,14 @@ import { create } from 'zustand';
import { persist } from 'zustand/middleware';
type GraphControlsStore = {
view: 'force' | 'hierarchy' | 'force3d';
view: 'force' | 'hierarchy' | 'table';
zoomToFit: () => void;
zoomIn: () => void;
zoomOut: () => void;
onLayout: (layout: any) => void;
setActions: (actions: Partial<GraphControlsStore>) => void;
refetchGraph: () => void;
setView: (view: 'force' | 'hierarchy' | 'force3d') => void;
setView: (view: 'force' | 'hierarchy' | 'table') => void;
};
export const useGraphControls = create<GraphControlsStore>()(

View File

@@ -61,6 +61,9 @@ interface GraphState {
setCurrentNodeType: (nodeType: ActionItem | null) => void
handleOpenFormModal: (key: string) => void
// === Action Type for Edit form ===
handleEdit: (node: GraphNode) => void
// === Filters ===
filters: Record<string, unknown>
setFilters: (filters: Record<string, unknown>) => void
@@ -122,7 +125,7 @@ export const useGraphStore = create<GraphState>()((set, get) => ({
updateNode: (nodeId, updates) => {
const { nodes } = get()
const updatedNodes = nodes.map(node =>
node.id === nodeId
node.id === nodeId
? { ...node, data: { ...node.data, ...updates } }
: node
)
@@ -191,6 +194,9 @@ export const useGraphStore = create<GraphState>()((set, get) => ({
setOpenAddRelationDialog: (open) => set({ openAddRelationDialog: open }),
setOpenNodeEditorModal: (open) => set({ openNodeEditorModal: open }),
// === Action Type for Edit form ===
handleEdit: (node) => set({ currentNode: node, openNodeEditorModal: true }),
// === Action Type for Form ===
currentNodeType: null,
setCurrentNodeType: (nodeType) => set({ currentNodeType: nodeType }),

View File

@@ -493,7 +493,7 @@ html {
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
--xy-edge-label-background-color: var(--background)!important;
--xy-edge-label-background-color: var(--background) !important;
}
.minimal-tiptap-editor .ProseMirror {
@@ -659,3 +659,198 @@ html {
.tiptap pre .hljs-strong {
font-weight: 700;
}
:root {
--background: oklch(1.0000 0 0);
--foreground: oklch(0.2686 0 0);
--card: oklch(1.0000 0 0);
--card-foreground: oklch(0.2686 0 0);
--popover: oklch(1.0000 0 0);
--popover-foreground: oklch(0.2686 0 0);
--primary: oklch(0.7686 0.1647 70.0804);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.9670 0.0029 264.5419);
--secondary-foreground: oklch(0.4461 0.0263 256.8018);
--muted: oklch(0.9846 0.0017 247.8389);
--muted-foreground: oklch(0.5510 0.0234 264.3637);
--accent: oklch(0.9869 0.0214 95.2774);
--accent-foreground: oklch(0.4732 0.1247 46.2007);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.9276 0.0058 264.5313);
--input: oklch(0.9276 0.0058 264.5313);
--ring: oklch(0.7686 0.1647 70.0804);
--chart-1: oklch(0.7686 0.1647 70.0804);
--chart-2: oklch(0.6658 0.1574 58.3183);
--chart-3: oklch(0.5553 0.1455 48.9975);
--chart-4: oklch(0.4732 0.1247 46.2007);
--chart-5: oklch(0.4137 0.1054 45.9038);
--sidebar: oklch(0.9846 0.0017 247.8389);
--sidebar-foreground: oklch(0.2686 0 0);
--sidebar-primary: oklch(0.7686 0.1647 70.0804);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.9869 0.0214 95.2774);
--sidebar-accent-foreground: oklch(0.4732 0.1247 46.2007);
--sidebar-border: oklch(0.9276 0.0058 264.5313);
--sidebar-ring: oklch(0.7686 0.1647 70.0804);
--font-sans: Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.375rem;
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
--tracking-normal: 0em;
--spacing: 0.25rem;
}
.dark {
--background: oklch(0.25 0 0);
--foreground: oklch(0.9219 0 0);
--card: oklch(0.2686 0 0);
--card-foreground: oklch(0.9219 0 0);
--popover: oklch(0.2686 0 0);
--popover-foreground: oklch(0.9219 0 0);
--primary: oklch(0.7686 0.1647 70.0804);
--primary-foreground: oklch(0 0 0);
--secondary: oklch(0.2686 0 0);
--secondary-foreground: oklch(0.9219 0 0);
--muted: oklch(0.2686 0 0);
--muted-foreground: oklch(0.7155 0 0);
--accent: oklch(0.4732 0.1247 46.2007);
--accent-foreground: oklch(0.9243 0.1151 95.7459);
--destructive: oklch(0.6368 0.2078 25.3313);
--destructive-foreground: oklch(1.0000 0 0);
--border: oklch(0.3715 0 0);
--input: oklch(0.3715 0 0);
--ring: oklch(0.7686 0.1647 70.0804);
--chart-1: oklch(0.8369 0.1644 84.4286);
--chart-2: oklch(0.6658 0.1574 58.3183);
--chart-3: oklch(0.4732 0.1247 46.2007);
--chart-4: oklch(0.5553 0.1455 48.9975);
--chart-5: oklch(0.4732 0.1247 46.2007);
--sidebar: oklch(0.1684 0 0);
--sidebar-foreground: oklch(0.9219 0 0);
--sidebar-primary: oklch(0.7686 0.1647 70.0804);
--sidebar-primary-foreground: oklch(1.0000 0 0);
--sidebar-accent: oklch(0.4732 0.1247 46.2007);
--sidebar-accent-foreground: oklch(0.9243 0.1151 95.7459);
--sidebar-border: oklch(0.3715 0 0);
--sidebar-ring: oklch(0.7686 0.1647 70.0804);
--font-sans: Inter, sans-serif;
--font-serif: Source Serif 4, serif;
--font-mono: JetBrains Mono, monospace;
--radius: 0.375rem;
--shadow-2xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-xs: 0px 4px 8px -1px hsl(0 0% 0% / 0.05);
--shadow-sm: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 1px 2px -2px hsl(0 0% 0% / 0.10);
--shadow-md: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 2px 4px -2px hsl(0 0% 0% / 0.10);
--shadow-lg: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 4px 6px -2px hsl(0 0% 0% / 0.10);
--shadow-xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.10), 0px 8px 10px -2px hsl(0 0% 0% / 0.10);
--shadow-2xl: 0px 4px 8px -1px hsl(0 0% 0% / 0.25);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
}
body {
@apply m-0;
font-family:
'Oxanium', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
letter-spacing: var(--tracking-normal);
}
.tr {
display: flex;
}
th,
.th {
position: relative;
}
.resizer {
position: absolute;
top: 0;
height: 100%;
right: 0;
width: 5px;
background: rgba(0, 0, 0, 0.5);
cursor: col-resize;
user-select: none;
touch-action: none;
}
.resizer.isResizing {
background: var(--primary);
opacity: 1;
}
@media (hover: hover) {
.resizer {
opacity: 0;
}
*:hover>.resizer {
opacity: 1;
}
}

View File

@@ -25,6 +25,7 @@ export type NodeData = {
type: string,
caption: string,
label: string,
created_at: string,
// Allow any other properties
[key: string]: any;
};

View File

@@ -0,0 +1,26 @@
export type Row = {
id: string
label: string
type: string,
created_at: string
}
export const rows: Row[] = [
{
id: "728ed52f",
label: "https://alliage.io",
type: "wesbite",
created_at: "2021-01-01"
},
{
id: "728ed52E",
label: "alliage.io",
type: "domain",
created_at: "2021-01-01"
}, {
id: "722ed52E",
label: "12.32.43.12",
type: "ip",
created_at: "2021-01-01"
},
]

View File

@@ -440,9 +440,9 @@
optionalDependencies:
global-agent "^3.0.0"
"@electron/node-gyp@git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2":
"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
version "10.2.0-electron.1"
resolved "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2"
resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
dependencies:
env-paths "^2.2.0"
exponential-backoff "^3.1.1"
@@ -1946,6 +1946,13 @@
"@tanstack/store" "0.7.1"
use-sync-external-store "^1.5.0"
"@tanstack/react-table@^8.21.3":
version "8.21.3"
resolved "https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.21.3.tgz#2c38c747a5731c1a07174fda764b9c2b1fb5e91b"
integrity sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==
dependencies:
"@tanstack/table-core" "8.21.3"
"@tanstack/react-virtual@^3.13.9":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz#d372dc2783739cc04ec1a728ca8203937687a819"
@@ -2043,6 +2050,11 @@
resolved "https://registry.yarnpkg.com/@tanstack/store/-/store-0.7.1.tgz#e00a7f382b079a154f5d938886a72e7a1b5a9085"
integrity sha512-PjUQKXEXhLYj2X5/6c1Xn/0/qKY0IVFxTJweopRfF26xfjVyb14yALydJrHupDh3/d+1WKmfEgZPBVCmDkzzwg==
"@tanstack/table-core@8.21.3":
version "8.21.3"
resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.21.3.tgz#2977727d8fc8dfa079112d9f4d4c019110f1732c"
integrity sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==
"@tanstack/virtual-core@3.13.12":
version "3.13.12"
resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz#1dff176df9cc8f93c78c5e46bcea11079b397578"
@@ -5490,6 +5502,11 @@ lazy-val@^1.0.5:
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.5.tgz#6cf3b9f5bc31cee7ee3e369c0832b7583dcd923d"
integrity sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==
leaflet@^1.9.4:
version "1.9.4"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.9.4.tgz#23fae724e282fa25745aff82ca4d394748db7d8d"
integrity sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"