feat: social accounts

This commit is contained in:
dextmorgn
2025-02-04 13:31:09 +01:00
parent d8ef2d3c9a
commit eca47d028e
9 changed files with 233 additions and 92 deletions

View File

@@ -7,7 +7,6 @@ const DashboardPage = async ({
params: Promise<{ investigation_id: string }>
}) => {
const { investigation_id } = await (params)
console.log(investigation_id)
const { nodes, edges } = await getInvestigationData(investigation_id)
return (
<div>

View File

@@ -21,7 +21,7 @@ export default function CustomEdge({ id, label, confidence_level, sourceX, sourc
<BaseEdge id={id} path={edgePath} />
<EdgeLabelRenderer>
{settings.showEdgeLabel &&
<Badge color={label === "relation" ? 'orange' : "blue"} style={{
<Badge size={"1"} color={label === "relation" ? 'orange' : "blue"} style={{
position: 'absolute',
transform: `translate(-50%, -50%) translate(${labelX}px,${labelY}px)`,
pointerEvents: 'all',

View File

@@ -20,12 +20,13 @@ import PhoneNode from './nodes/phone';
import CustomEdge from './custom-edge';
import IpNode from './nodes/ip_address';
import EmailNode from './nodes/email';
import { AlignCenterHorizontal, AlignCenterVertical, MaximizeIcon, ZoomInIcon, ZoomOutIcon } from 'lucide-react';
import SocialNode from './nodes/social'
import { AlignCenterHorizontal, AlignCenterVertical, MaximizeIcon, RotateCcwIcon, ZoomInIcon, ZoomOutIcon } from 'lucide-react';
import { useTheme } from 'next-themes';
import NewActions from './new-actions';
import { IconButton, Tooltip, Spinner } from '@radix-ui/themes';
const nodeTypes = { individual: IndividualNode, phone: PhoneNode, ip: IpNode, email: EmailNode };
const nodeTypes = { individual: IndividualNode, phone: PhoneNode, ip: IpNode, email: EmailNode, social: SocialNode };
const edgeTypes = {
'custom': CustomEdge,
};
@@ -100,6 +101,9 @@ const LayoutFlow = ({ initialNodes, initialEdges, theme }: { initialNodes: any,
onEdgesChange={onEdgesChange}
onConnect={onConnect}
fitView
proOptions={{
hideAttribution: true
}}
nodeTypes={nodeTypes}
// @ts-ignore
edgeTypes={edgeTypes}
@@ -117,6 +121,11 @@ const LayoutFlow = ({ initialNodes, initialEdges, theme }: { initialNodes: any,
</Tooltip>
</Panel>
<Panel position="top-right" className='flex items-center gap-1'>
<Tooltip content="Reload schema">
<IconButton onClick={() => window.location.reload()} variant="soft">
<RotateCcwIcon className='h-4 w-4' />
</IconButton>
</Tooltip>
<NewActions addNodes={addNodes} />
</Panel>
<Panel position="bottom-left" className='flex flex-col items-center gap-1'>

View File

@@ -1,31 +1,47 @@
"use client"
import React, { memo } from 'react';
import { Handle, Position } from '@xyflow/react';
import { Card, Box, Text, ContextMenu, Badge, Flex, Inset } from '@radix-ui/themes';
import { Card, Box, Text, ContextMenu, Badge, Flex, Inset, Tooltip, Avatar } from '@radix-ui/themes';
import { NodeProvider, useNodeContext } from './node-context';
import { AtSignIcon } from 'lucide-react';
import { cn } from '@/utils';
import { useInvestigationContext } from '../investigation-provider';
function EmailNode({ data }: any) {
const { handleDeleteNode } = useNodeContext()
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationContext()
return (
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box>
<Card>
<Inset>
<Flex className='items-center p-0'>
<Badge className='!h-[24px] !rounded-r-none'>
<AtSignIcon className='h-3 w-3' />
</Badge>
<Box className='p-1'>
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
</Box>
</Flex>
</Inset>
</Card>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel ?
<Card>
<Inset>
<Flex className='items-center p-0'>
<Badge className='!h-[24px] !rounded-r-none'>
<AtSignIcon className='h-3 w-3' />
</Badge>
<Box className='p-1'>
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
</Box>
</Flex>
</Inset>
</Card>
:
<Tooltip content={data.username || data.profile_url}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={<AtSignIcon className='h-3 w-3' />}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
@@ -33,7 +49,6 @@ function EmailNode({ data }: any) {
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>

View File

@@ -2,28 +2,28 @@
import React, { memo, useState } from 'react';
import { Handle, Position } from '@xyflow/react';
import { useInvestigationContext } from '../investigation-provider';
import { Avatar, Card, Box, Flex, Text, ContextMenu, Dialog, TextField, Button } from '@radix-ui/themes';
import { AtSignIcon, CameraIcon, FacebookIcon, InstagramIcon, LocateIcon, PhoneIcon, UserIcon } from 'lucide-react';
import { Avatar, Card, Box, Flex, Text, ContextMenu, Dialog, TextField, Button, Spinner, Badge, Tooltip, Inset } from '@radix-ui/themes';
import { AtSignIcon, CameraIcon, FacebookIcon, InstagramIcon, LocateIcon, MessageCircleDashedIcon, PhoneIcon, SendIcon, UserIcon } from 'lucide-react';
import { NodeProvider, useNodeContext } from './node-context';
import { cn } from '@/utils';
function Custom({ data }: any) {
console.log(data)
const { settings } = useInvestigationContext()
const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode } = useNodeContext()
const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading } = useNodeContext()
const [open, setOpen] = useState(false);
if (!data) return
return (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box>{settings.showNodeLabel ?
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>{settings.showNodeLabel ?
<Card className='!py-1' onClick={() => setOpen(true)}>
<Flex gap="2" align="center">
<Avatar
size="2"
src={data?.image_url}
radius="full"
fallback={data.full_name?.[0]}
fallback={loading ? <Spinner /> : data.full_name[0]}
/>
{settings.showNodeLabel &&
<Box>
@@ -35,12 +35,17 @@ function Custom({ data }: any) {
</Text>
</Box>}
</Flex>
</Card> : <Avatar
size="3"
src={data?.image_url}
radius="full"
fallback={data.full_name?.[0]}
/>}
</Card> :
<Tooltip content={data.full_name}>
<button className='!rounded-full border-transparent' onClick={() => setOpen(true)}>
<Avatar
size="3"
src={data?.image_url}
radius="full"
fallback={<UserIcon className='h-4 w-4' />}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
@@ -64,10 +69,12 @@ function Custom({ data }: any) {
<ContextMenu.Sub>
<ContextMenu.SubTrigger >Social account</ContextMenu.SubTrigger>
<ContextMenu.SubContent>
<ContextMenu.Item><FacebookIcon className='h-4 w-4' />Facebook</ContextMenu.Item>
<ContextMenu.Item><InstagramIcon className='h-4 w-4' />Instagram</ContextMenu.Item>
<ContextMenu.Item><CameraIcon className='h-4 w-4' />Snapchat</ContextMenu.Item>
<ContextMenu.Item>Coco</ContextMenu.Item>
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_facebook", data.id)}><FacebookIcon className='h-4 w-4' />Facebook</ContextMenu.Item>
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_instagram", data.id)}><InstagramIcon className='h-4 w-4' />Instagram</ContextMenu.Item>
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_telegram", data.id)}><SendIcon className='h-4 w-4' />Telegram</ContextMenu.Item>
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_signal", data.id)}><MessageCircleDashedIcon className='h-4 w-4' />Signal</ContextMenu.Item>
<ContextMenu.Item onClick={() => setOpenAddNodeModal("social_accounts_snapchat", data.id)}><CameraIcon className='h-4 w-4' />Snapchat</ContextMenu.Item>
<ContextMenu.Item disabled onClick={() => setOpenAddNodeModal("social_accounts_coco", data.id)}>Coco <Badge radius='full'>soon</Badge></ContextMenu.Item>
</ContextMenu.SubContent>
</ContextMenu.Sub>
</ContextMenu.SubContent>

View File

@@ -4,15 +4,16 @@ import { Handle, Position } from '@xyflow/react';
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge } from '@radix-ui/themes';
import { NodeProvider, useNodeContext } from './node-context';
import { LocateIcon } from 'lucide-react';
import { cn } from '@/utils';
function Custom({ data }: any) {
const { handleDeleteNode } = useNodeContext()
const { handleDeleteNode, loading } = useNodeContext()
return (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
<Card>
<Inset>
<Flex className='items-center p-0'>

View File

@@ -8,15 +8,20 @@ import { useNodeId, useReactFlow } from "@xyflow/react";
interface NodeContextType {
setOpenAddNodeModal: any,
handleDuplicateNode: any,
handleDeleteNode: any
handleDeleteNode: any,
loading: boolean
}
const nodesTypes = {
"emails": { table: "emails", type: "email", field: "email" },
"individuals": { table: "individuals", type: "individual", field: "full_name" },
"phone_numbers": { table: "phone_numbers", type: "phone", field: "phone_number" },
"ip_addresses": { table: "ip_addresses", type: "ip", field: "ip_address" },
"social_accounts": { table: "social_accounts", type: "social", field: "username" },
"emails": { table: "emails", type: "email", fields: ["email"] },
"individuals": { table: "individuals", type: "individual", fields: ["full_name"] },
"phone_numbers": { table: "phone_numbers", type: "phone", fields: ["phone_number"] },
"ip_addresses": { table: "ip_addresses", type: "ip", fields: ["ip_address"] },
"social_accounts_facebook": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:facebook"] },
"social_accounts_instagram": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:instagram"] },
"social_accounts_telegram": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:telegram"] },
"social_accounts_snapchat": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:snapchat"] },
"social_accounts_signal": { table: "social_accounts", type: "social", fields: ["profile_url", "username", "platform:signal"] },
}
const NodeContext = createContext<NodeContextType | undefined>(undefined);
@@ -41,10 +46,25 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
setOpenNodeModal(true)
}
const onSubmitNewNodeModal = (e: { preventDefault: () => void; currentTarget: HTMLFormElement | undefined; }) => {
const onSubmitNewNodeModal = async (e: { preventDefault: () => void; currentTarget: HTMLFormElement | undefined; }) => {
e.preventDefault();
const data = Object.fromEntries(new FormData(e.currentTarget));
handleAddNode(data);
const newNodeId = crypto.randomUUID()
addNodes({
id: newNodeId,
type: nodeType.type,
data: { ...data, label: data[nodeType.fields[0]] },
position: { x: -100, y: -100 }
});
if (nodeId)
addEdges({
source: nodeId,
target: newNodeId,
type: 'custom',
id: `${nodeId}-${newNodeId}`.toString(),
label: nodeType.type,
});
await handleAddNode({ ...data, id: newNodeId });
};
const handleAddNode = async (data: any) => {
@@ -74,19 +94,6 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
relation_type: "relation"
}).then(({ error }) => console.log(error))
}
addNodes({
id: node.id,
type: nodeType.type,
data: { ...node, label: node[nodeType.field] },
position: { x: -100, y: -100 }
});
addEdges({
source: nodeId,
target: node.id,
type: 'custom',
id: `${nodeId}-${node.id}`.toString(),
label: nodeType.type,
});
setLoading(false)
setOpenNodeModal(false)
}
@@ -119,7 +126,7 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
}, [nodeId, setNodes, setEdges]);
return (
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode }}>
<NodeContext.Provider {...props} value={{ setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading }}>
{props.children}
<Dialog.Root open={openAddNodeModal && nodeType} onOpenChange={setOpenNodeModal}>
<Dialog.Content maxWidth="450px">
@@ -129,16 +136,23 @@ export const NodeProvider: React.FC<NodeProviderProps> = (props: any) => {
</Dialog.Description>
<form onSubmit={onSubmitNewNodeModal}>
<Flex direction="column" gap="3">
<label>
<Text as="div" size="2" mb="1" weight="bold">
Value
</Text>
<TextField.Root
defaultValue={""}
name={nodeType?.field}
placeholder={`Your value here (${nodeType?.field})`}
/>
</label>
{nodeType?.fields.map((field: any, i: number) => {
const [key, value] = field.split(":")
console.log(Boolean(value))
return (
<label key={i}>
<Text as="div" size="2" mb="1" weight="bold">
{key}
</Text>
<TextField.Root
defaultValue={value || ""}
disabled={Boolean(value)}
name={key}
placeholder={`Your value here (${key})`}
/>
</label>
)
})}
</Flex>
<Flex gap="3" mt="4" justify="end">
<Dialog.Close>

View File

@@ -1,39 +1,55 @@
"use client"
import React, { memo } from 'react';
import { Handle, Position } from '@xyflow/react';
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge } from '@radix-ui/themes';
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes';
import { NodeProvider, useNodeContext } from './node-context';
import { PhoneIcon } from 'lucide-react';
import { cn } from '@/utils';
import { useInvestigationContext } from '../investigation-provider';
function Custom({ data }: any) {
const { handleDeleteNode } = useNodeContext()
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationContext()
return (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box>
<Card>
<Inset>
<Flex className='items-center p-0'>
<Badge className='!h-[24px] !rounded-r-none'>
<PhoneIcon className='h-3 w-3' />
</Badge>
<Box className='p-1'>
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
</Box>
</Flex>
</Inset>
</Card>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel ?
<Card>
<Inset>
<Flex className='items-center p-0'>
<Badge className='!h-[24px] !rounded-r-none'>
<PhoneIcon className='h-3 w-3' />
</Badge>
<Box className='p-1'>
<Text as="div" size="1" weight="regular">
{data.label}
</Text>
</Box>
</Flex>
</Inset>
</Card>
:
<Tooltip content={data.label}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={<PhoneIcon className='h-3 w-3' />}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
className="w-16 !bg-teal-500"
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>

View File

@@ -0,0 +1,80 @@
"use client"
import React, { memo, useMemo } from 'react';
import { Handle, Position } from '@xyflow/react';
import { Card, Box, Text, ContextMenu, Flex, Inset, Badge, Tooltip, Avatar } from '@radix-ui/themes';
import { NodeProvider, useNodeContext } from './node-context';
import { CameraIcon, FacebookIcon, InstagramIcon, MessageCircleDashedIcon, SendIcon } from 'lucide-react';
import { cn } from '@/utils';
import { useInvestigationContext } from '../investigation-provider';
function Custom({ data }: any) {
const { handleDeleteNode, loading } = useNodeContext()
const { settings } = useInvestigationContext()
const platformsIcons = useMemo(() => ({
"facebook": <FacebookIcon className='h-3 w-3' />,
"instagram": <InstagramIcon className='h-3 w-3' />,
"telegram": <SendIcon className='h-3 w-3' />,
"signal": <MessageCircleDashedIcon className='h-3 w-3' />,
"snapchat": <CameraIcon className='h-3 w-3' />
}), [])
return (
<>
<ContextMenu.Root>
<ContextMenu.Trigger>
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
{settings.showNodeLabel ?
<Card>
<Inset>
<Flex className='items-center p-0'>
<Badge className='!h-[24px] !rounded-r-none'>
{/* @ts-ignore */}
{platformsIcons[data.platform]}
</Badge>
<Box maxWidth={"20px"} className='!max-w-[240px] p-1'>
<Text as="div" size="1" weight="medium" color='blue' className='truncate text-ellipsis underline'>
{data.username || data.profile_url}
</Text>
</Box>
</Flex>
</Inset>
</Card>
:
<Tooltip content={data.username || data.profile_url}>
<button className='!rounded-full border-transparent'>
<Avatar
size="1"
src={data?.image_url}
radius="full"
/* @ts-ignore */
fallback={platformsIcons[data.platform]}
/>
</button>
</Tooltip>}
<Handle
type="target"
position={Position.Top}
className="w-16 !bg-teal-500"
/>
</Box>
</ContextMenu.Trigger>
<ContextMenu.Content>
<ContextMenu.Item shortcut="⌘ C">Copy content</ContextMenu.Item>
<ContextMenu.Item shortcut="⌘ D">Duplicate</ContextMenu.Item>
<ContextMenu.Separator />
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
Delete
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Root >
</>
);
}
const SocialNode = (props: any) => (
<NodeProvider>
<Custom {...props} />
</NodeProvider>
);
export default memo(SocialNode);