mirror of
https://github.com/reconurge/flowsint.git
synced 2026-04-28 02:09:31 -05:00
feat: user modal
This commit is contained in:
@@ -34,6 +34,8 @@
|
||||
"@tailwindcss/postcss": "^4.0.3",
|
||||
"@xyflow/react": "^12.4.2",
|
||||
"clsx": "2.1.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"d3-quadtree": "^3.0.1",
|
||||
"framer-motion": "11.13.1",
|
||||
"intl-messageformat": "^10.5.0",
|
||||
"lucide-react": "^0.474.0",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getInvestigationData } from '@/src/lib/actions/investigations'
|
||||
import InvestigationGraph from '@/src/components/investigations/graph'
|
||||
import IndividualModal from '@/src/components/investigations/individual-modal'
|
||||
|
||||
const DashboardPage = async ({
|
||||
params,
|
||||
@@ -8,9 +9,11 @@ const DashboardPage = async ({
|
||||
}) => {
|
||||
const { investigation_id } = await (params)
|
||||
const { nodes, edges } = await getInvestigationData(investigation_id)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InvestigationGraph initialNodes={nodes} initialEdges={edges} />
|
||||
<IndividualModal />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export default function NewCase() {
|
||||
</Text>
|
||||
<TextField.Root
|
||||
name="description"
|
||||
placeholder="Investigation sur une campagne de phishing via Twitter et LinkedIn."
|
||||
placeholder="Investigation sur une campagne de phishing via LinkedIn."
|
||||
/>
|
||||
</label>
|
||||
</Flex>
|
||||
|
||||
285
src/components/investigations/individual-modal.tsx
Normal file
285
src/components/investigations/individual-modal.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
"use client"
|
||||
import type React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
import {
|
||||
Flex,
|
||||
Dialog,
|
||||
TextField,
|
||||
Button,
|
||||
Box,
|
||||
Skeleton,
|
||||
Card,
|
||||
Inset,
|
||||
Separator,
|
||||
Avatar,
|
||||
Tabs,
|
||||
IconButton,
|
||||
} from "@radix-ui/themes"
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation"
|
||||
import { useIndividual } from "@/src/lib/hooks/use-individual"
|
||||
import { Pencil1Icon, Cross2Icon, PlusIcon, TrashIcon } from "@radix-ui/react-icons"
|
||||
import social from "./nodes/social"
|
||||
|
||||
const IndividualModal = () => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const individual_id = searchParams.get("individual_id")
|
||||
const { individual, isLoading } = useIndividual(individual_id)
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
const [emails, setEmails] = useState([""])
|
||||
const [phones, setPhones] = useState([""])
|
||||
const [accounts, setAccounts] = useState([""])
|
||||
const [ipAddresses, setIpAddresses] = useState([""])
|
||||
|
||||
console.log(individual)
|
||||
useEffect(() => {
|
||||
setEmails(individual?.emails.map(({ email }: any) => email) || [""])
|
||||
setPhones(individual?.phone_numbers.map(({ phone_number }: any) => phone_number) || [""])
|
||||
setIpAddresses(individual?.ip_addresses.map(({ ip_address }: any) => ip_address) || [""])
|
||||
setAccounts(individual?.social_accounts.map(({ username }: any) => username) || [""])
|
||||
}, [individual])
|
||||
|
||||
const handleCloseModal = () => {
|
||||
router.push(pathname)
|
||||
}
|
||||
|
||||
const handleSave = (event: React.FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const formData = new FormData(event.currentTarget)
|
||||
const formContent = Object.fromEntries(formData.entries())
|
||||
formContent.emails = emails as any
|
||||
formContent.phones = phones as any
|
||||
formContent.ip_addresses = ipAddresses as any
|
||||
alert(JSON.stringify(formContent, null, 2))
|
||||
setEditMode(false)
|
||||
}
|
||||
|
||||
const handleAddField = (setter: React.Dispatch<React.SetStateAction<string[]>>) => {
|
||||
setter((prev) => [...prev, ""])
|
||||
}
|
||||
|
||||
const handleRemoveField = (index: number, setter: React.Dispatch<React.SetStateAction<string[]>>) => {
|
||||
setter((prev) => prev.filter((_, i) => i !== index))
|
||||
}
|
||||
|
||||
const handleFieldChange = (index: number, value: string, setter: React.Dispatch<React.SetStateAction<string[]>>) => {
|
||||
setter((prev) => prev.map((item, i) => (i === index ? value : item)))
|
||||
}
|
||||
|
||||
if (!isLoading && !individual) {
|
||||
return (
|
||||
<Dialog.Root open={Boolean(individual_id)} onOpenChange={handleCloseModal}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Title>No data</Dialog.Title>
|
||||
<Dialog.Description size="2" mb="4">
|
||||
No data found for this individual.
|
||||
</Dialog.Description>
|
||||
<Dialog.Close>
|
||||
<Button variant="soft" color="gray">
|
||||
Close
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root open={Boolean(individual_id)} onOpenChange={handleCloseModal}>
|
||||
<Dialog.Content style={{ maxWidth: "900px", width: "90vw" }} minHeight={"80vh"}>
|
||||
<Skeleton loading={isLoading}>
|
||||
<form className="flex flex-col gap-3 justify-between h-full" onSubmit={handleSave}>
|
||||
<Flex direction="column" gap="4">
|
||||
<Flex justify="between" align="center">
|
||||
<Dialog.Title>User Profile</Dialog.Title>
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setEditMode(!editMode)}
|
||||
aria-label={editMode ? "Cancel edit" : "Edit profile"}
|
||||
>
|
||||
{editMode ? <Cross2Icon /> : <Pencil1Icon />}
|
||||
</IconButton>
|
||||
</Flex>
|
||||
<Flex gap="6">
|
||||
<Flex direction={"column"}>
|
||||
<Avatar
|
||||
size="9"
|
||||
fallback={individual?.full_name?.[0] || "?"}
|
||||
radius="full"
|
||||
/>
|
||||
{editMode && (
|
||||
<Button type="button" size="1" variant="soft" style={{ marginTop: "8px" }}>
|
||||
Change Photo
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
<Box style={{ flexGrow: 1 }}>
|
||||
<Tabs.Root defaultValue="overview">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="overview">Overview</Tabs.Trigger>
|
||||
<Tabs.Trigger value="social_account">Social accounts</Tabs.Trigger>
|
||||
<Tabs.Trigger value="emails">Emails</Tabs.Trigger>
|
||||
<Tabs.Trigger value="phone_numbers">Phone numbers</Tabs.Trigger>
|
||||
<Tabs.Trigger value="ip_addresses">IP addresses</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Box pt="3">
|
||||
<Tabs.Content value="overview">
|
||||
<Flex direction="column" gap="3">
|
||||
<TextField.Root
|
||||
defaultValue={individual?.full_name}
|
||||
placeholder="Full Name"
|
||||
name="full_name"
|
||||
disabled={!editMode}
|
||||
/>
|
||||
{emails.map((email: string | number | undefined, index: React.Key | null | undefined) => (
|
||||
<Flex key={index} gap="2" align="center">
|
||||
<TextField.Root
|
||||
value={email}
|
||||
onChange={(e) => handleFieldChange(index, e.target.value, setEmails)}
|
||||
placeholder="Email"
|
||||
type="email"
|
||||
disabled={!editMode}
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
{editMode && (
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveField(index, setEmails)}
|
||||
aria-label="Remove email"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
{editMode && (
|
||||
<Button type="button" onClick={() => handleAddField(setEmails)} variant="soft">
|
||||
<PlusIcon /> Add Email
|
||||
</Button>
|
||||
)}
|
||||
{phones.map((phone, index) => (
|
||||
<Flex key={index} gap="2" align="center">
|
||||
<TextField.Root
|
||||
value={phone}
|
||||
onChange={(e) => handleFieldChange(index, e.target.value, setPhones)}
|
||||
placeholder="Phone Number"
|
||||
type="tel"
|
||||
disabled={!editMode}
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
{editMode && (
|
||||
<IconButton
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveField(index, setPhones)}
|
||||
aria-label="Remove phone"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
{editMode && (
|
||||
<Button type="button" onClick={() => handleAddField(setPhones)} variant="soft">
|
||||
<PlusIcon /> Add Phone
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="social_account">
|
||||
<Flex direction="column" gap="3">
|
||||
<TextField.Root
|
||||
defaultValue={individual?.username}
|
||||
placeholder="Username"
|
||||
name="username"
|
||||
disabled={!editMode}
|
||||
/>
|
||||
{editMode && (
|
||||
<TextField.Root placeholder="New Password" name="new_password" type="password" />
|
||||
)}
|
||||
{accounts.map((account, index) => (
|
||||
<Flex key={index} gap="2" align="center">
|
||||
<TextField.Root
|
||||
value={account}
|
||||
onChange={(e) => handleFieldChange(index, e.target.value, setAccounts)}
|
||||
placeholder="Social account"
|
||||
disabled={!editMode}
|
||||
style={{ flexGrow: 1 }}
|
||||
/>
|
||||
{editMode && (
|
||||
<IconButton
|
||||
variant="ghost"
|
||||
onClick={() => handleRemoveField(index, setAccounts)}
|
||||
aria-label="Remove social account"
|
||||
>
|
||||
<TrashIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Flex>
|
||||
))}
|
||||
{editMode && (
|
||||
<Button type="button" onClick={() => handleAddField(setIpAddresses)} variant="soft">
|
||||
<PlusIcon /> Add IP Address
|
||||
</Button>
|
||||
)}
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="emails">
|
||||
<Flex direction="column" gap="3">
|
||||
<TextField.Root
|
||||
defaultValue={individual?.language}
|
||||
placeholder="Preferred Language"
|
||||
name="language"
|
||||
disabled={!editMode}
|
||||
/>
|
||||
<TextField.Root
|
||||
defaultValue={individual?.timezone}
|
||||
placeholder="Timezone"
|
||||
name="timezone"
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
<Tabs.Content value="phone_numbers">
|
||||
<Flex direction="column" gap="3">
|
||||
<TextField.Root
|
||||
defaultValue={individual?.language}
|
||||
placeholder="Preferred Language"
|
||||
name="language"
|
||||
disabled={!editMode}
|
||||
/>
|
||||
<TextField.Root
|
||||
defaultValue={individual?.timezone}
|
||||
placeholder="Timezone"
|
||||
name="timezone"
|
||||
disabled={!editMode}
|
||||
/>
|
||||
</Flex>
|
||||
</Tabs.Content>
|
||||
</Box>
|
||||
</Tabs.Root>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<Flex gap="3" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="soft" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
{editMode && <Button type="submit">Save Changes</Button>}
|
||||
</Flex>
|
||||
</form>
|
||||
</Skeleton>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndividualModal
|
||||
|
||||
@@ -42,6 +42,7 @@ export default function NewActions({ addNodes }: { addNodes: any }) {
|
||||
/>
|
||||
<Box flexGrow="1">
|
||||
<TextField.Root
|
||||
required
|
||||
defaultValue={""}
|
||||
name={"full_name"}
|
||||
placeholder={`Name of the individual`}
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
"use client"
|
||||
import React, { memo, useState } from 'react';
|
||||
import React, { memo, useCallback } from 'react';
|
||||
import { Handle, Position } from '@xyflow/react';
|
||||
import { useInvestigationContext } from '../investigation-provider';
|
||||
import { Avatar, Card, Box, Flex, Text, ContextMenu, Dialog, TextField, Button, Spinner, Badge, Tooltip, Inset } from '@radix-ui/themes';
|
||||
import { Avatar, Card, Box, Flex, Text, ContextMenu, Spinner, Badge, Tooltip } 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';
|
||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
||||
|
||||
function Custom({ data }: any) {
|
||||
const { settings } = useInvestigationContext()
|
||||
const { setOpenAddNodeModal, handleDuplicateNode, handleDeleteNode, loading } = useNodeContext()
|
||||
const [open, setOpen] = useState(false);
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const createQueryString = useCallback(
|
||||
(name: string, value: string) => {
|
||||
const params = new URLSearchParams(searchParams.toString())
|
||||
params.set(name, value)
|
||||
|
||||
return params.toString()
|
||||
},
|
||||
[searchParams]
|
||||
)
|
||||
const handleOpenIndividualModal = () => router.push(pathname + '?' + createQueryString('individual_id', data.id))
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>{settings.showNodeLabel ?
|
||||
<Card className='!py-1' onClick={() => setOpen(true)}>
|
||||
<Box onClick={handleOpenIndividualModal} className={cn(loading ? "!opacity-40" : "!opacity-100")}>{settings.showNodeLabel ?
|
||||
<Card className='!py-1'>
|
||||
<Flex gap="2" align="center">
|
||||
<Avatar
|
||||
size="2"
|
||||
@@ -37,13 +49,13 @@ function Custom({ data }: any) {
|
||||
</Flex>
|
||||
</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 onClick={handleOpenIndividualModal} className='!rounded-full border-transparent'>
|
||||
<Avatar
|
||||
size="3"
|
||||
src={data?.image_url}
|
||||
radius="full"
|
||||
fallback={<UserIcon className='h-4 w-4' />}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<Handle
|
||||
@@ -79,7 +91,7 @@ function Custom({ data }: any) {
|
||||
</ContextMenu.Sub>
|
||||
</ContextMenu.SubContent>
|
||||
</ContextMenu.Sub>
|
||||
<ContextMenu.Item onClick={() => setOpen(true)}>View and edit</ContextMenu.Item>
|
||||
<ContextMenu.Item>View and edit</ContextMenu.Item>
|
||||
<ContextMenu.Item onClick={handleDuplicateNode}>Duplicate</ContextMenu.Item>
|
||||
<ContextMenu.Separator />
|
||||
<ContextMenu.Item onClick={handleDeleteNode} shortcut="⌘ ⌫" color="red">
|
||||
@@ -87,47 +99,6 @@ function Custom({ data }: any) {
|
||||
</ContextMenu.Item>
|
||||
</ContextMenu.Content>
|
||||
</ContextMenu.Root>
|
||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
||||
<Dialog.Content maxWidth="40vw">
|
||||
<Dialog.Title>{data.full_name}</Dialog.Title>
|
||||
<Dialog.Description size="2" mb="4">
|
||||
{data.notes}
|
||||
</Dialog.Description>
|
||||
|
||||
<Flex direction="column" gap="3">
|
||||
<label>
|
||||
<Text as="div" size="2" mb="1" weight="bold">
|
||||
Name
|
||||
</Text>
|
||||
<TextField.Root
|
||||
defaultValue={data?.full_name}
|
||||
placeholder="Enter your full name"
|
||||
name="full_name"
|
||||
/>
|
||||
</label>
|
||||
<label>
|
||||
<Text as="div" size="2" mb="1" weight="bold">
|
||||
Email
|
||||
</Text>
|
||||
<TextField.Root
|
||||
defaultValue="freja@example.com"
|
||||
placeholder="Enter your email"
|
||||
/>
|
||||
</label>
|
||||
</Flex>
|
||||
|
||||
<Flex gap="3" mt="4" justify="end">
|
||||
<Dialog.Close>
|
||||
<Button variant="soft" color="gray">
|
||||
Cancel
|
||||
</Button>
|
||||
</Dialog.Close>
|
||||
<Dialog.Close>
|
||||
<Button>Save</Button>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,33 +1,48 @@
|
||||
"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 { LocateIcon } from 'lucide-react';
|
||||
import { cn } from '@/utils';
|
||||
import { useInvestigationContext } from '../investigation-provider';
|
||||
|
||||
function Custom({ data }: any) {
|
||||
const { handleDeleteNode, loading } = useNodeContext()
|
||||
const { settings } = useInvestigationContext()
|
||||
|
||||
return (
|
||||
<>
|
||||
<ContextMenu.Root>
|
||||
<ContextMenu.Trigger>
|
||||
<Box className={cn(loading ? "!opacity-40" : "!opacity-100")}>
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
<LocateIcon className='h-3 w-3' />
|
||||
</Badge>
|
||||
<Box className='p-1'>
|
||||
<Text as="div" size="1" weight="regular">
|
||||
{data.label}
|
||||
</Text>
|
||||
</Box>
|
||||
</Flex>
|
||||
</Inset>
|
||||
</Card>
|
||||
{settings.showNodeLabel ?
|
||||
<Card>
|
||||
<Inset>
|
||||
<Flex className='items-center p-0'>
|
||||
<Badge className='!h-[24px] !rounded-r-none'>
|
||||
<LocateIcon 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={<LocateIcon className='h-3 w-3' />}
|
||||
/>
|
||||
</button>
|
||||
</Tooltip>}
|
||||
<Handle
|
||||
type="target"
|
||||
position={Position.Top}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { createClient } from "../supabase/server"
|
||||
import { Investigation } from "@/src/types/investigation"
|
||||
import { NodeData, EdgeData } from "@/src/types"
|
||||
import { notFound } from "next/navigation"
|
||||
interface ReturnTypeGetInvestigations {
|
||||
investigations: Investigation[],
|
||||
error?: any
|
||||
@@ -30,7 +31,7 @@ export async function getInvestigation(investigationId: string): Promise<ReturnT
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
throw error
|
||||
throw notFound()
|
||||
}
|
||||
return { investigation: investigation as Investigation, error: error }
|
||||
}
|
||||
@@ -41,7 +42,8 @@ export async function getInvestigationData(investigationId: string): Promise<{ n
|
||||
.from('individuals')
|
||||
.select('*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*)')
|
||||
.eq('investigation_id', investigationId);
|
||||
if (indError) throw indError;
|
||||
if (indError) throw notFound();
|
||||
|
||||
if (!individuals) individuals = [];
|
||||
|
||||
// Extraire les IDs
|
||||
|
||||
17
src/lib/hooks/use-individual.ts
Normal file
17
src/lib/hooks/use-individual.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { supabase } from "@/src/lib/supabase/client";
|
||||
import { useQuery } from "@supabase-cache-helpers/postgrest-swr";
|
||||
|
||||
export function useIndividual(individualId: string | null | undefined) {
|
||||
const { data: individual, mutate, isLoading, error } = useQuery(Boolean(individualId) ?
|
||||
supabase
|
||||
.from('individuals')
|
||||
.select('*, ip_addresses(*), phone_numbers(*), social_accounts(*), emails(*)')
|
||||
.eq("id", individualId)
|
||||
.single() : null,
|
||||
{
|
||||
revalidateOnFocus: false,
|
||||
revalidateOnReconnect: false,
|
||||
}
|
||||
);
|
||||
return { individual: individual ?? null, isLoading, refetch: mutate, error };
|
||||
}
|
||||
@@ -23,6 +23,6 @@
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../ReconOps/src/lib/utils.ts"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "../../ReconOps/src/lib/utils.ts", "src/components/investigations/collide.js"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
14
yarn.lock
14
yarn.lock
@@ -4666,6 +4666,15 @@ csstype@^3.0.2:
|
||||
resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4"
|
||||
integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
|
||||
|
||||
d3-force@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4"
|
||||
integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
|
||||
dependencies:
|
||||
d3-dispatch "1 - 3"
|
||||
d3-quadtree "1 - 3"
|
||||
d3-timer "1 - 3"
|
||||
|
||||
"d3-interpolate@1 - 3":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d"
|
||||
@@ -4673,6 +4682,11 @@ csstype@^3.0.2:
|
||||
dependencies:
|
||||
d3-color "1 - 3"
|
||||
|
||||
"d3-quadtree@1 - 3", d3-quadtree@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f"
|
||||
integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
|
||||
|
||||
"d3-selection@2 - 3", d3-selection@3, d3-selection@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31"
|
||||
|
||||
Reference in New Issue
Block a user