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 Feedback from "@/components/dashboard/feedback";
import Link from "next/link";
import { Separator } from "@/components/ui/separator";
const DashboardLayout = async ({
children,
@@ -35,6 +36,7 @@ const DashboardLayout = async ({
<Radar className="mr-2 h-6 w-6" />
<h2 className="text-lg font-semibold mr-6">flowsint</h2>
</Link>
{/* <Separator orientation="vertical" className="h-6" /> */}
<MainNav className="mx-6" />
<div className="ml-auto flex 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 (
<html suppressHydrationWarning lang="en">
<head>
{process.env.NODE_ENV === "development" && (
{/* {process.env.NODE_ENV === "development" && ( */}
<script
crossOrigin="anonymous"
src="//unpkg.com/react-scan/dist/auto.global.js"
/>
)}
{/* )} */}
</head>
<body
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 CaseSelector from "../investigations/case-selector"
import { useParams } from "next/navigation"
import { Separator } from "../ui/separator"
export function MainNav({ className, ...props }: React.HTMLAttributes<HTMLElement>) {
const { investigation_id, project_id } = useParams()

View File

@@ -3,12 +3,11 @@
import type { Investigation } from "@/types/investigation"
import { useQuery } from "@tanstack/react-query"
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 { formatDistanceToNow } from "date-fns"
import { number, string } from "zod"
const RecentSketches = () => {
const {
@@ -95,15 +94,12 @@ const RecentSketches = () => {
{investigations?.map((investigation: Investigation) => {
return (
<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">
<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>
<Card className="bg-background shadow-none h-full transition-all duration-200 hover:border-primary rounded-md">
<CardContent className="p-4 relative">
<h3 className="font-medium line-clamp-1 group-hover:text-primary transition-colors">
{investigation?.project?.name}/{investigation.title}
</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>
</Card>
</Link>

View File

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

View File

@@ -27,7 +27,7 @@ export const ButtonEdge = memo(({
return (
<>
<BaseEdge path={edgePath} markerEnd={markerEnd} style={style} />
<BaseEdge className="opacity-30" path={edgePath} markerEnd={markerEnd} style={style} />
<EdgeLabelRenderer>
<div
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">
<Mail className="h-8 w-8 text-primary" />
</div>
<div className="w-full">
<div className="w-full text-center">
<h2 className="text-xl w-full font-bold break-all">{data.email}</h2>
<p className="text-sm text-muted-foreground">Email Address</p>
</div>
<SearchEmail investigation_id={investigation_id as string} email={data.email} />
</div>
<Separator />
<div className="space-y-4">
<div className="space-y-1">
<div className="flex items-center gap-2 text-primary">
<div className="space-y-4 text-center">
<div className="space-y-1 text-center">
<div className="flex items-center justify-center gap-2 text-primary">
<Shield className="h-4 w-4" />
<span className="text-sm">Security Status</span>
</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">
{data.breach_found ? "Breach Found" : "No Breach Detected"}
</Badge>
</div>
</div>
<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" />
<span className="text-sm">Email ID</span>
</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>
{/* Right column with related info */}
<div className="space-y-6">
<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" />
<span className="text-sm">Associated Individual</span>
</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 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" />
<span className="text-sm">Investigation ID</span>
</div>
<p className="text-xs font-mono 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>
<p className="text-xs font-mono text-center text-muted-foreground">{data.investigation_id}</p>
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -15,7 +15,7 @@
--card-foreground: hsl(0 0% 3.9%);
--popover: hsl(0 0% 100%);
--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%);
--secondary: hsl(0 0% 96.1%);
--secondary-foreground: hsl(0 0% 9%);
@@ -53,7 +53,7 @@
--card-foreground: hsl(0 0% 98%);
--popover: hsl(240 0% 9.9%);
--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%);
--secondary: hsl(0 0% 14.9%);
--secondary-foreground: hsl(0 0% 98%);