feat: new color and recent sketches

This commit is contained in:
dextmorgn
2025-04-04 19:11:42 +02:00
parent 870a9abf1d
commit 82df77c4c7
11 changed files with 126 additions and 273 deletions

View File

@@ -11,6 +11,7 @@ import { NavUser } from "@/components/nav-user";
import { SubNav } from "@/components/dashboard/sub-nav"; import { SubNav } from "@/components/dashboard/sub-nav";
import Feedback from "@/components/dashboard/feedback"; import Feedback from "@/components/dashboard/feedback";
import Link from "next/link"; import Link from "next/link";
import { Separator } from "@/components/ui/separator";
const DashboardLayout = async ({ const DashboardLayout = async ({
children, children,
@@ -35,6 +36,7 @@ const DashboardLayout = async ({
<Radar className="mr-2 h-6 w-6" /> <Radar className="mr-2 h-6 w-6" />
<h2 className="text-lg font-semibold mr-6">flowsint</h2> <h2 className="text-lg font-semibold mr-6">flowsint</h2>
</Link> </Link>
{/* <Separator orientation="vertical" className="h-6" /> */}
<MainNav className="mx-6" /> <MainNav className="mx-6" />
<div className="ml-auto flex items-center space-x-2"> <div className="ml-auto flex items-center space-x-2">
<div className="lg:flex hidden items-center space-x-2"> <div className="lg:flex hidden items-center space-x-2">

View File

@@ -36,12 +36,12 @@ export default function RootLayout({
return ( return (
<html suppressHydrationWarning lang="en"> <html suppressHydrationWarning lang="en">
<head> <head>
{process.env.NODE_ENV === "development" && ( {/* {process.env.NODE_ENV === "development" && ( */}
<script <script
crossOrigin="anonymous" crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js" src="//unpkg.com/react-scan/dist/auto.global.js"
/> />
)} {/* )} */}
</head> </head>
<body <body
className={clsx( className={clsx(

View File

@@ -1,164 +0,0 @@
"use client"
import type * as React from "react"
import { usePathname } from "next/navigation"
import Link from "next/link"
import { FolderSearch, Globe, KeyIcon, MapPin, Network, SettingsIcon, UserIcon, Users } from "lucide-react"
import { NavUser } from "@/components/nav-user"
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarSeparator,
SidebarTrigger,
} from "@/components/ui/sidebar"
import { TeamSwitcher } from "./team-switcher"
// Define navigation item type
interface NavItem {
title: string
href: string
icon: React.ElementType
}
interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
user: any
}
export function AppSidebar({ user, ...props }: AppSidebarProps) {
const pathname = usePathname()
// Main navigation items
const mainNavItems: NavItem[] = [
{
title: "Investigations",
href: "/dashboard",
icon: FolderSearch,
},
{
title: "Networks",
href: "/dashboard/networks",
icon: Network,
},
{
title: "Entities",
href: "/dashboard/entities",
icon: Users,
},
{
title: "OSINT sources",
href: "/dashboard/sources",
icon: Globe,
},
{
title: "Map",
href: "/dashboard/map",
icon: MapPin,
},
]
// Team navigation items
const teamNavItems: NavItem[] = [
{
title: "My account",
href: "/dashboard/settings/account",
icon: UserIcon,
},
{
title: "Team members",
href: "/dashboard/settings/team",
icon: Users,
},
]
// Preferences navigation items
const preferencesNavItems: NavItem[] = [
{
title: "Settings",
href: "/dashboard/settings",
icon: SettingsIcon,
},
{
title: "API keys",
href: "/dashboard/keys",
icon: KeyIcon,
},
]
// Function to check if a link is active
const isActive = (href: string) => {
// Exact match for dashboard
if (href === "/dashboard" && pathname === "/dashboard") {
return true
}
// For other routes, check if pathname starts with href (for nested routes)
return href !== "/dashboard" && pathname.startsWith(href)
}
return (
<Sidebar collapsible="icon" {...props} >
<SidebarHeader>
<TeamSwitcher />
</SidebarHeader>
<SidebarContent className="p-0">
<SidebarGroup className="group-data-[collapsible=icon]:p-auto">
<SidebarGroupLabel>NAVIGATION</SidebarGroupLabel>
<SidebarMenu>
{mainNavItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={isActive(item.href)}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
<SidebarSeparator />
<SidebarGroup className="group-data-[collapsible=icon]:p-auto">
<SidebarGroupLabel>TEAMS</SidebarGroupLabel>
<SidebarMenu>
{teamNavItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={isActive(item.href)}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
<SidebarGroup className="group-data-[collapsible=icon]:p-auto">
<SidebarGroupLabel>PREFERENCES</SidebarGroupLabel>
<SidebarMenu>
{preferencesNavItems.map((item) => (
<SidebarMenuItem key={item.href}>
<SidebarMenuButton asChild isActive={isActive(item.href)}>
<Link href={item.href}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
))}
</SidebarMenu>
</SidebarGroup>
</SidebarContent>
<SidebarFooter>
<NavUser user={user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>
)
}

View File

@@ -5,6 +5,7 @@ import { cn } from "@/lib/utils"
import ProjectSelector from "../investigations/project-selector" import ProjectSelector from "../investigations/project-selector"
import CaseSelector from "../investigations/case-selector" import CaseSelector from "../investigations/case-selector"
import { useParams } from "next/navigation" import { useParams } from "next/navigation"
import { Separator } from "../ui/separator"
export function MainNav({ className, ...props }: React.HTMLAttributes<HTMLElement>) { export function MainNav({ className, ...props }: React.HTMLAttributes<HTMLElement>) {
const { investigation_id, project_id } = useParams() const { investigation_id, project_id } = useParams()

View File

@@ -3,12 +3,11 @@
import type { Investigation } from "@/types/investigation" import type { Investigation } from "@/types/investigation"
import { useQuery } from "@tanstack/react-query" import { useQuery } from "@tanstack/react-query"
import Link from "next/link" import Link from "next/link"
import { FileSearch, FileText, Folder, Layers, Search, Users, Waypoints } from "lucide-react" import { Search } from "lucide-react"
import { Card, CardContent, CardFooter } from "@/components/ui/card" import { Card, CardContent } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { formatDistanceToNow } from "date-fns" import { formatDistanceToNow } from "date-fns"
import { number, string } from "zod"
const RecentSketches = () => { const RecentSketches = () => {
const { const {
@@ -95,15 +94,12 @@ const RecentSketches = () => {
{investigations?.map((investigation: Investigation) => { {investigations?.map((investigation: Investigation) => {
return ( return (
<Link href={`/dashboard/projects/${investigation.project_id}/investigations/${investigation.id}`} key={investigation.id} className="group"> <Link href={`/dashboard/projects/${investigation.project_id}/investigations/${investigation.id}`} key={investigation.id} className="group">
<Card className="bg-transparent shadow-none h-full transition-all duration-200 border-none"> <Card className="bg-background shadow-none h-full transition-all duration-200 hover:border-primary rounded-md">
<div className={`flex items-center justify-center overflow-hidden bg-foreground/5 h-40 border group-hover:border-primary/80 group-hover:border-2 rounded-md`}>
<FlowchartDiagram />
</div>
<CardContent className="p-4 relative"> <CardContent className="p-4 relative">
<h3 className="font-medium line-clamp-1 group-hover:text-primary transition-colors"> <h3 className="font-medium line-clamp-1 group-hover:text-primary transition-colors">
{investigation?.project?.name}/{investigation.title} {investigation?.project?.name}/{investigation.title}
</h3> </h3>
<span className="text-xs opacity-60">Last updated {formatDistanceToNow(investigation.last_updated_at, { addSuffix: true })}</span> <span className="text-xs opacity-60 group-hover:text-primary">Last updated {formatDistanceToNow(investigation.last_updated_at, { addSuffix: true })}</span>
</CardContent> </CardContent>
</Card> </Card>
</Link> </Link>

View File

@@ -21,7 +21,6 @@ import EmailNode from "./nodes/email"
import SocialNode from "./nodes/social" import SocialNode from "./nodes/social"
import AddressNode from "./nodes/physical_address" import AddressNode from "./nodes/physical_address"
import { import {
AlignCenterVertical,
MaximizeIcon, MaximizeIcon,
ZoomInIcon, ZoomInIcon,
ZoomOutIcon, ZoomOutIcon,
@@ -29,6 +28,8 @@ import {
PlusIcon, PlusIcon,
GroupIcon, GroupIcon,
WorkflowIcon, WorkflowIcon,
NetworkIcon,
WaypointsIcon,
} from "lucide-react" } from "lucide-react"
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import NewActions from "../new-actions" import NewActions from "../new-actions"
@@ -134,13 +135,27 @@ const FlowControls = memo(
size="icon" size="icon"
variant="outline" variant="outline"
onClick={() => { onClick={() => {
onLayout("TB", fitView) onLayout("dagre", fitView)
}} }}
> >
<AlignCenterVertical className="h-4 w-4" /> <NetworkIcon className="h-4 w-4" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>Auto layout</TooltipContent> <TooltipContent>Dagre layout</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="outline"
onClick={() => {
onLayout("force", fitView)
}}
>
<WaypointsIcon className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Force layout</TooltipContent>
</Tooltip> </Tooltip>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -353,10 +368,12 @@ const LayoutFlow = ({ refetch, theme }: LayoutFlowProps) => {
<ResizableHandle /> <ResizableHandle />
<ResizablePanel defaultSize={20} className="h-full"> <ResizablePanel defaultSize={20} className="h-full">
<ResizablePanelGroup autoSaveId="conditional" direction="vertical"> <ResizablePanelGroup autoSaveId="conditional" direction="vertical">
{currentNode && <ResizablePanel order={1} id="top" defaultSize={50}> {currentNode && <>
<ProfilePanel data={currentNode.data} type={currentNode.type} /> <ResizablePanel order={1} id="top" defaultSize={50}>
</ResizablePanel>} <ProfilePanel data={currentNode.data} type={currentNode.type} />
<ResizableHandle /> </ResizablePanel>
<ResizableHandle />
</>}
<ResizablePanel order={2} id="bottom" defaultSize={50}> <ResizablePanel order={2} id="bottom" defaultSize={50}>
<NodesPanel nodes={processedNodes} /> <NodesPanel nodes={processedNodes} />
</ResizablePanel> </ResizablePanel>

View File

@@ -27,7 +27,7 @@ export const ButtonEdge = memo(({
return ( return (
<> <>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} /> <BaseEdge className="opacity-30" path={edgePath} markerEnd={markerEnd} style={style} />
<EdgeLabelRenderer> <EdgeLabelRenderer>
<div <div
className="nodrag nopan pointer-events-auto absolute" className="nodrag nopan pointer-events-auto absolute"

View File

@@ -29,54 +29,49 @@ export default function ProfilePanel({ data, type }: { data: any, type: "individ
<div className="bg-primary/10 p-3 rounded-full"> <div className="bg-primary/10 p-3 rounded-full">
<Mail className="h-8 w-8 text-primary" /> <Mail className="h-8 w-8 text-primary" />
</div> </div>
<div className="w-full"> <div className="w-full text-center">
<h2 className="text-xl w-full font-bold break-all">{data.email}</h2> <h2 className="text-xl w-full font-bold break-all">{data.email}</h2>
<p className="text-sm text-muted-foreground">Email Address</p> <p className="text-sm text-muted-foreground">Email Address</p>
</div> </div>
<SearchEmail investigation_id={investigation_id as string} email={data.email} /> <SearchEmail investigation_id={investigation_id as string} email={data.email} />
</div> </div>
<Separator /> <Separator />
<div className="space-y-4"> <div className="space-y-4 text-center">
<div className="space-y-1"> <div className="space-y-1 text-center">
<div className="flex items-center gap-2 text-primary"> <div className="flex items-center justify-center gap-2 text-primary">
<Shield className="h-4 w-4" /> <Shield className="h-4 w-4" />
<span className="text-sm">Security Status</span> <span className="text-sm">Security Status</span>
</div> </div>
<div className="flex gap-2 mt-1"> <div className="flex gap-2 mt-1 items-center justify-center">
<Badge variant={data.breach_found ? "destructive" : "outline"} className="px-3 py-1"> <Badge variant={data.breach_found ? "destructive" : "outline"} className="px-3 py-1">
{data.breach_found ? "Breach Found" : "No Breach Detected"} {data.breach_found ? "Breach Found" : "No Breach Detected"}
</Badge> </Badge>
</div> </div>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2 text-primary"> <div className="flex items-center justify-center gap-2 text-primary">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
<span className="text-sm">Email ID</span> <span className="text-sm">Email ID</span>
</div> </div>
<p className="text-xs font-mono text-muted-foreground">{data.id}</p> <p className="text-xs font-mono text-center text-muted-foreground">{data.id}</p>
</div> </div>
</div> </div>
</div> </div>
{/* Right column with related info */} {/* Right column with related info */}
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2 text-primary"> <div className="flex items-center justify-center gap-2 text-primary">
<Link2 className="h-4 w-4" /> <Link2 className="h-4 w-4" />
<span className="text-sm">Associated Individual</span> <span className="text-sm">Associated Individual</span>
</div> </div>
<p className="text-xs font-mono text-muted-foreground">{data.individual_id}</p> <p className="text-xs font-mono text-center text-muted-foreground">{data.individual_id}</p>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-2 text-primary"> <div className="flex items-center justify-center gap-2 text-primary">
<Info className="h-4 w-4" /> <Info className="h-4 w-4" />
<span className="text-sm">Investigation ID</span> <span className="text-sm">Investigation ID</span>
</div> </div>
<p className="text-xs font-mono text-muted-foreground">{data.investigation_id}</p> <p className="text-xs font-mono text-center text-muted-foreground">{data.investigation_id}</p>
</div>
<div className="mt-auto pt-4">
<Badge variant="outline" className="px-2 py-1">
Email Record
</Badge>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -133,80 +133,80 @@ interface LayoutOptions {
iterations?: number iterations?: number
} }
// export const getLayoutedElements = ( export const getForceLayoutedElements = (
// nodes: AppNode[], nodes: AppNode[],
// edges: Edge[], edges: Edge[],
// options: LayoutOptions = { options: LayoutOptions = {
// direction: "LR", direction: "LR",
// strength: -300, strength: -300,
// distance: 100, distance: 100,
// iterations: 300, iterations: 300,
// }, },
// ) => { ) => {
// // Create a map of node IDs to indices for the simulation // Create a map of node IDs to indices for the simulation
// const nodeMap = new Map(nodes.map((node, i) => [node.id, i])) const nodeMap = new Map(nodes.map((node, i) => [node.id, i]))
// // Create a copy of nodes with positions for the simulation // Create a copy of nodes with positions for the simulation
// const nodesCopy = nodes.map((node) => ({ const nodesCopy = nodes.map((node) => ({
// ...node, ...node,
// x: node.position?.x || Math.random() * 500, x: node.position?.x || Math.random() * 500,
// y: node.position?.y || Math.random() * 500, y: node.position?.y || Math.random() * 500,
// width: node.measured?.width || 0, width: node.measured?.width || 0,
// height: node.measured?.height || 0, height: node.measured?.height || 0,
// })) }))
// // Create links for the simulation using indices // Create links for the simulation using indices
// const links = edges.map((edge) => ({ const links = edges.map((edge) => ({
// source: nodeMap.get(edge.source), source: nodeMap.get(edge.source),
// target: nodeMap.get(edge.target), target: nodeMap.get(edge.target),
// original: edge, original: edge,
// })) }))
// // Create the simulation // Create the simulation
// const simulation = d3 const simulation = d3
// .forceSimulation(nodesCopy) .forceSimulation(nodesCopy)
// .force( .force(
// "link", "link",
// d3.forceLink(links).id((d: any) => nodeMap.get(d.id)), d3.forceLink(links).id((d: any) => nodeMap.get(d.id)),
// ) )
// .force("charge", d3.forceManyBody().strength(options.strength || -300)) .force("charge", d3.forceManyBody().strength(options.strength || -300))
// .force("center", d3.forceCenter(250, 250)) .force("center", d3.forceCenter(250, 250))
// .force( .force(
// "collision", "collision",
// d3.forceCollide().radius((d: any) => Math.max(d.width, d.height) / 2 + 10), d3.forceCollide().radius((d: any) => Math.max(d.width, d.height) / 2 + 10),
// ) )
// // If direction is horizontal, adjust forces // If direction is horizontal, adjust forces
// if (options.direction === "LR") { if (options.direction === "LR") {
// simulation.force("x", d3.forceX(250).strength(0.1)) simulation.force("x", d3.forceX(250).strength(0.1))
// simulation.force("y", d3.forceY(250).strength(0.05)) simulation.force("y", d3.forceY(250).strength(0.05))
// } else { } else {
// simulation.force("x", d3.forceX(250).strength(0.05)) simulation.force("x", d3.forceX(250).strength(0.05))
// simulation.force("y", d3.forceY(250).strength(0.1)) simulation.force("y", d3.forceY(250).strength(0.1))
// } }
// // Run the simulation synchronously // Run the simulation synchronously
// simulation.stop() simulation.stop()
// for (let i = 0; i < (options.iterations || 300); i++) { for (let i = 0; i < (options.iterations || 300); i++) {
// simulation.tick() simulation.tick()
// } }
// // Update node positions based on simulation results // Update node positions based on simulation results
// const updatedNodes = nodesCopy.map((node) => ({ const updatedNodes = nodesCopy.map((node) => ({
// ...node, ...node,
// position: { position: {
// x: node.x - node.width / 2, x: node.x - node.width / 2,
// y: node.y - node.height / 2, y: node.y - node.height / 2,
// }, },
// })) }))
// return { return {
// nodes: updatedNodes, nodes: updatedNodes,
// edges, edges,
// } }
// } }
export const getLayoutedElements = (nodes: AppNode[], export const getDagreLayoutedElements = (nodes: AppNode[],
edges: Edge[], edges: Edge[],
options: LayoutOptions = { options: LayoutOptions = {
direction: "TB", direction: "TB",

View File

@@ -8,7 +8,7 @@ import {
type OnNodesChange, type OnNodesChange,
type OnEdgesChange, type OnEdgesChange,
} from '@xyflow/react'; } from '@xyflow/react';
import { getLayoutedElements } from '@/lib/utils'; import { getForceLayoutedElements, getDagreLayoutedElements } from '@/lib/utils';
export type AppNode = Node; export type AppNode = Node;
@@ -36,7 +36,7 @@ export type AppState = {
setNodes: (nodes: AppNode[]) => void; setNodes: (nodes: AppNode[]) => void;
setEdges: (edges: Edge[]) => void; setEdges: (edges: Edge[]) => void;
highlightPath: (selectedNode: Node | null) => void; highlightPath: (selectedNode: Node | null) => void;
onLayout: (direct: string, fitView: () => void) => void, onLayout: (layout: string, fitView: () => void) => void,
onConnect: (params: any, investigation_id?: string) => Promise<void>; onConnect: (params: any, investigation_id?: string) => Promise<void>;
onNodeClick: (_: React.MouseEvent, node: Node) => void; onNodeClick: (_: React.MouseEvent, node: Node) => void;
onPaneClick: (_: React.MouseEvent) => void, onPaneClick: (_: React.MouseEvent) => void,
@@ -139,10 +139,16 @@ const createStore = (initialNodes: AppNode[] = [], initialEdges: Edge[] = []) =>
set({ currentNode: null }); set({ currentNode: null });
// get().resetNodeStyles(); // get().resetNodeStyles();
}, },
onLayout: (direction = 'TB', fitView: () => void) => { onLayout: (layout = "dagre", fitView: () => void) => {
const { nodes, edges } = getLayoutedElements(get().nodes, get().edges, { direction }); if (layout === "force") {
// @ts-ignore const { nodes, edges } = getForceLayoutedElements(get().nodes, get().edges);
set({ nodes, edges }); // Fixed the edges type issue // @ts-ignore
set({ nodes, edges });
} else {
const { nodes, edges } = getDagreLayoutedElements(get().nodes, get().edges);
// @ts-ignore
set({ nodes, edges });
}
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
fitView(); fitView();
}); });

View File

@@ -15,7 +15,7 @@
--card-foreground: hsl(0 0% 3.9%); --card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%); --popover: hsl(0 0% 100%);
--popover-foreground: hsl(0 0% 3.9%); --popover-foreground: hsl(0 0% 3.9%);
--primary: oklch(0.623 0.214 259.815);; --primary: oklch(0.49 0.24 293);
--primary-foreground: hsl(0 0% 98%); --primary-foreground: hsl(0 0% 98%);
--secondary: hsl(0 0% 96.1%); --secondary: hsl(0 0% 96.1%);
--secondary-foreground: hsl(0 0% 9%); --secondary-foreground: hsl(0 0% 9%);
@@ -53,7 +53,7 @@
--card-foreground: hsl(0 0% 98%); --card-foreground: hsl(0 0% 98%);
--popover: hsl(240 0% 9.9%); --popover: hsl(240 0% 9.9%);
--popover-foreground: hsl(0 0% 98%); --popover-foreground: hsl(0 0% 98%);
--primary: oklch(0.623 0.214 259.815);; --primary: oklch(0.49 0.24 293);
--primary-foreground: hsl(0, 0%, 100%); --primary-foreground: hsl(0, 0%, 100%);
--secondary: hsl(0 0% 14.9%); --secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%); --secondary-foreground: hsl(0 0% 98%);