Guest User

Untitled

a guest
Sep 29th, 2024
48
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. "use client"
  2. import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
  3. import {
  4.   ReactFlow,
  5.   MiniMap,
  6.   Controls,
  7.   Background,
  8.   useNodesState,
  9.   useEdgesState,
  10.   addEdge,
  11.   BackgroundVariant,
  12.   type Connection,
  13.   type NodeProps,
  14.   type Node,
  15. } from "@xyflow/react"
  16.  
  17. import "@xyflow/react/dist/style.css"
  18. import { useTheme } from "next-themes"
  19.  
  20. import Yoga, { Edge, FlexDirection, Gutter, Wrap, type Node as YogaNode } from "yoga-layout"
  21. import { MoreHorizontal } from "lucide-react"
  22. import { cn } from "@/lib/utils"
  23. import Color from "color"
  24. import Link from "next/link"
  25. import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, useToast } from "@/components/ui"
  26. import { LinkButton } from "@/components/custom-ui/buttons/link-buttons"
  27. import { Pencil2Icon, TrashIcon } from "@radix-ui/react-icons"
  28. import { deleteModal } from "@/components/custom-ui/modal/toast"
  29. import { deleteAsset } from "../assets/actions"
  30. import { useRouter } from "next/navigation"
  31. import { severityToColorSafe } from "@/components/severity-badge"
  32. import Image from "next/image"
  33.  
  34. export type Zone = "dmz" | "it" | "ot" | "others"
  35.  
  36. export type Asset = {
  37.   id: string
  38.   name: string
  39.   zone: Zone
  40.   address: string
  41.   findings: number
  42.   criticality: string
  43.   type: string
  44. }
  45.  
  46. function groupAssets(assets: Asset[]): Record<Zone, Asset[]> {
  47.   const result: Record<Zone, Asset[]> = {
  48.     dmz: [],
  49.     it: [],
  50.     ot: [],
  51.     others: [],
  52.   }
  53.  
  54.   for (const asset of assets) {
  55.     result[asset.zone].push(asset)
  56.   }
  57.  
  58.   return result
  59. }
  60.  
  61. type MainContextData = {
  62.   currentDropdown: string | null
  63.   setCurrentDropdown: (id: string | null) => void
  64.   canDelete: boolean
  65.   canEdit: boolean
  66.   handleDelete: (asset: Pick<AssetData, "id" | "name">) => void
  67. }
  68.  
  69. const MainContext = createContext<MainContextData>({
  70.   currentDropdown: null,
  71.   canDelete: false,
  72.   canEdit: false,
  73.   setCurrentDropdown: () => { },
  74.   handleDelete: () => { }
  75. })
  76.  
  77. const chooseAssetIcon = (type: string): string => {
  78.   const images: Record<typeof type, string> = {
  79.     // TODO: add images
  80.   }
  81.  
  82.   return images[type] ?? "https://picsum.photos/104/104"
  83. }
  84.  
  85. type AssetData = Asset
  86. type AssetNodeType = Node<AssetData, "asset">
  87. const AssetNode = ({ data: asset }: NodeProps<AssetNodeType>) => {
  88.   const { currentDropdown, setCurrentDropdown, canDelete, canEdit, handleDelete } = useContext(MainContext)
  89.   const id = asset.address
  90.   const dropdownStyle = "h-fit w-full justify-start gap-3.5 px-3.5 py-2.5 text-xs"
  91.   return (
  92.     <div key={asset.id} className="nodrag flex h-[220px] w-[140px] flex-col rounded-md bg-white text-black">
  93.       <div className="flex w-full justify-end pt-1 pr-2 min-h-4">
  94.         {(canEdit || canDelete) && (
  95.           <DropdownMenu open={currentDropdown === id} onOpenChange={() => setCurrentDropdown(id)}>
  96.             <DropdownMenuTrigger>
  97.               <MoreHorizontal size={16} />
  98.             </DropdownMenuTrigger>
  99.             <DropdownMenuContent>
  100.               <DropdownMenuLabel>Manage Asset</DropdownMenuLabel>
  101.               <DropdownMenuSeparator />
  102.               {canDelete && (
  103.                 <DropdownMenuItem asChild>
  104.                   <Button
  105.                     onClick={async (e) => {
  106.                       e.stopPropagation()
  107.                       handleDelete(asset)
  108.                     }}
  109.                     variant={"ghost"}
  110.                     size={"sm"}
  111.                     className={dropdownStyle}
  112.                   >
  113.                     <TrashIcon />
  114.                     Delete
  115.                   </Button>
  116.                 </DropdownMenuItem>
  117.               )}
  118.             </DropdownMenuContent>
  119.           </DropdownMenu>
  120.         )}
  121.       </div>
  122.       <div className="flex w-full justify-center py-2">
  123.         <div className="h-[104px] w-[104px]">
  124.           <Image alt="asset type icon" width={104} height={104} src={chooseAssetIcon(asset.type)} />
  125.         </div>
  126.       </div>
  127.       <div className="mt-2.5 flex-col px-2.5">
  128.         <div className="flex gap-1">
  129.           <div className="flex-1 min-w-0">
  130.             <p className="truncate">{asset.name}</p>
  131.           </div>
  132.           <div className={cn("mt-1 size-[6px] self-center rounded-full", severityToColorSafe(asset.criticality))}></div>
  133.         </div>
  134.         <div className="flex-1 min-w-0 text-xs text-muted">
  135.           <p className="truncate">{asset.address}</p>
  136.         </div>
  137.         <Link href={`/view-findings?affectedAsset.ipAddress=${asset.address}`}>
  138.           <div className="text-xs text-muted underline">{asset.findings >= 100 ? "99+" : asset.findings} Findings</div>
  139.         </Link>
  140.       </div>
  141.     </div>
  142.   )
  143. }
  144.  
  145. type ZoneData = { name: string; size: { x: number; y: number }; className?: string; color?: string; isEmpty: boolean }
  146. type ZoneNodeType = Node<ZoneData, "zone">
  147. const ZoneNode = ({ data: { size, className, color, name, isEmpty } }: NodeProps<ZoneNodeType>) => {
  148.   const { resolvedTheme } = useTheme()
  149.   const colors = color
  150.     ? genColors(color)
  151.     : {
  152.       border: resolvedTheme === 'dark' ? "white" : "black",
  153.       text: resolvedTheme === 'dark' ? "white" : "black",
  154.       textMuted: "gray",
  155.       background: "transparent",
  156.     }
  157.   return (
  158.     <div
  159.       className={cn("nodrag relative rounded-md border grid place-items-center", className ?? "")}
  160.       style={{
  161.         width: `${size.x}px`,
  162.         height: `${size.y}px`,
  163.         borderColor: colors.border,
  164.         backgroundColor: colors.background,
  165.         color: colors.text,
  166.       }}
  167.     >
  168.       <span className="absolute left-0 top-0 -translate-y-full pb-1">{name}</span>
  169.       {isEmpty && <span style={{ color: colors.textMuted }}>There's no asset in this zone.</span>}
  170.    </div>
  171.  )
  172. }
  173.  
  174. type BoxData = { size: { x: number; y: number } }
  175. type BoxNodeType = Node<BoxData, "box">
  176. const BoxNode = ({ data: { size } }: NodeProps<BoxNodeType>) => {
  177.  return (
  178.    <div
  179.      className="nodrag"
  180.      style={{
  181.        width: `${size.x}px`,
  182.        height: `${size.y}px`,
  183.        border: "1px solid white",
  184.        display: "grid",
  185.        placeItems: "center center",
  186.      }}
  187.    ></div>
  188.  )
  189. }
  190.  
  191. function genColors(base: string) {
  192.  // TODO: take color mode into consideration
  193.  // background: the lighten factor should be higher for dark mode
  194.  const baseColor = Color(base)
  195.  const background = baseColor.alpha(0.2).lighten(0.5)
  196.  const text = base
  197.  const border = baseColor.darken(0.2)
  198.  const textMuted = baseColor.mix(Color(background), 0.4)
  199.  
  200.  return {
  201.    background: background.string(),
  202.    text,
  203.    border: border.string(),
  204.    textMuted: textMuted.string(),
  205.  }
  206. }
  207.  
  208. function generateNodesForZone(assets: Asset[]): [YogaNode, { node: YogaNode; asset: Asset }[]] {
  209.  const zone = Yoga.Node.create()
  210.  zone.setMinHeight(260)
  211.  zone.setWidth(40 + 60 + 4 * 140)
  212.  zone.setPadding(Edge.All, 20)
  213.  zone.setGap(Gutter.All, 20)
  214.  zone.setFlexDirection(FlexDirection.Row)
  215.  zone.setFlexWrap(Wrap.Wrap)
  216.  
  217.  const mappedAssets = assets.map((asset, idx) => {
  218.    const node = Yoga.Node.create()
  219.    node.setWidth(140)
  220.    node.setHeight(220)
  221.  
  222.    zone.insertChild(node, idx)
  223.    return { node, asset }
  224.  })
  225.  
  226.  return [zone, mappedAssets]
  227. }
  228.  
  229. function createZoneNode(
  230.  node: YogaNode,
  231.  { id, name, color, isEmpty }: { id: string; name: string; color?: string; isEmpty: boolean }
  232. ) {
  233.  const { left, top } = getAbsolutePosition(node)
  234.  return {
  235.    id,
  236.    type: "zone",
  237.    data: { size: { x: node.getComputedWidth(), y: node.getComputedHeight() }, name, color, isEmpty },
  238.    position: { x: left, y: top },
  239.    zIndex: 0,
  240.    selectable: false,
  241.  }
  242. }
  243.  
  244. function getAbsolutePosition(node: YogaNode) {
  245.  let left = node.getComputedLeft()
  246.  let top = node.getComputedTop()
  247.  
  248.  while (node.getParent() !== null) {
  249.    node = node.getParent()!
  250.    left += node.getComputedLeft()
  251.    top += node.getComputedTop()
  252.  }
  253.  
  254.  return { left, top }
  255. }
  256.  
  257. function generateNodes(assets: Record<Zone, Asset[]>) {
  258.  const root = Yoga.Node.create()
  259.  const org = Yoga.Node.create()
  260.  const ext = Yoga.Node.create()
  261.  org.setWidth("auto")
  262.  org.setHeight("auto")
  263.  org.setPadding(Edge.Horizontal, 20)
  264.  org.setPadding(Edge.Top, 40)
  265.  org.setPadding(Edge.Bottom, 20)
  266.  org.setGap(Gutter.All, 40)
  267.  
  268.  ext.setWidth("auto")
  269.  ext.setHeight("auto")
  270.  ext.setGap(Gutter.All, 40)
  271.  
  272.  root.setGap(Gutter.All, 40)
  273.  root.setFlexDirection(FlexDirection.Row)
  274.  const [it, its] = generateNodesForZone(assets.it)
  275.  const [dmz, dmzs] = generateNodesForZone(assets.dmz)
  276.  const [ot, ots] = generateNodesForZone(assets.ot)
  277.  const [other, others] = generateNodesForZone(assets.others)
  278.  org.insertChild(it, 0)
  279.  org.insertChild(ot, 1)
  280.  ext.insertChild(dmz, 0)
  281.  ext.insertChild(other, 1)
  282.  
  283.  root.insertChild(org, 0)
  284.  root.insertChild(ext, 1)
  285.  
  286.  root.calculateLayout(undefined, undefined)
  287.  
  288.  return [
  289.    createZoneNode(dmz, { id: "dmz", name: "DMZ", color: "#9200D6", isEmpty: dmzs.length === 0 }),
  290.    createZoneNode(it, { id: "it", name: "Internal Technology", color: "#1387b9", isEmpty: its.length === 0 }),
  291.    createZoneNode(ot, { id: "ot", name: "Operational Technology", color: "#FF2EB7", isEmpty: ots.length === 0 }),
  292.    createZoneNode(other, { id: "others", name: "Others", color: "#999", isEmpty: others.length === 0 }),
  293.    createZoneNode(org, { id: "org", name: "Organization", isEmpty: false }),
  294.    ...[dmzs, its, ots, others].flat().map((it, idx) => {
  295.      const { left, top } = getAbsolutePosition(it.node)
  296.      return {
  297.        id: idx.toString(),
  298.        type: "asset",
  299.        data: it.asset,
  300.        zIndex: 1,
  301.        position: {
  302.          x: left,
  303.          y: top,
  304.        },
  305.      }
  306.    }),
  307.  ]
  308. }
  309.  
  310. const nodeTypes = {
  311.  asset: AssetNode,
  312.  zone: ZoneNode,
  313.  box: BoxNode,
  314. }
  315.  
  316. export default function View({ canDelete, canEdit, assets }: { canDelete: boolean; canEdit: boolean; assets: Asset[] }) {
  317.  const { toast } = useToast()
  318.  const router = useRouter()
  319.  const handleDelete = async (asset: Pick<AssetData, "id" | "name">) => {
  320.    const isConfirmed = await deleteModal(`Are you sure you want to delete ${asset.name}?`)
  321.    if (!isConfirmed) {
  322.      return
  323.    }
  324.  
  325.    const response = await deleteAsset(asset.id)
  326.    toast({
  327.      variant: response.success ? "default" : "destructive",
  328.      title: response.message,
  329.    })
  330.  
  331.    if (!response.success) {
  332.      return
  333.    }
  334.  
  335.    console.log('should be refreshed now', { router })
  336.    router.refresh()
  337.  }
  338.  
  339.  const colorMode = ((theme: string | undefined) => {
  340.    switch (theme) {
  341.      case "dark":
  342.        return "dark"
  343.      case "light":
  344.        return "light"
  345.      default:
  346.        return "system"
  347.    }
  348.  })(useTheme().resolvedTheme)
  349.  
  350.  const grouped = groupAssets(assets)
  351.  const initialNodes = generateNodes(grouped)
  352.  const [nodes, _setNodes, onNodesChange] = useNodesState(initialNodes)
  353.  const [edges, setEdges, onEdgesChange] = useEdgesState([])
  354.  
  355.  const onConnect = useCallback(
  356.    (params: Connection) => setEdges((eds) => addEdge(params, eds)),
  357.    [setEdges]
  358.  )
  359.  
  360.  const [currentDropdown, set] = useState<MainContextData['currentDropdown']>(null)
  361.  return (
  362.    <MainContext.Provider value={{
  363.      canDelete, canEdit, handleDelete,
  364.      currentDropdown, setCurrentDropdown(id) {
  365.        set(prevId => (prevId === id ? null : id))
  366.      },
  367.    }}>
  368.      <ReactFlow
  369.        nodes={nodes}
  370.        edges={edges}
  371.        nodeTypes={nodeTypes}
  372.        onNodesChange={onNodesChange}
  373.        onEdgesChange={onEdgesChange}
  374.        onConnect={onConnect}
  375.        colorMode={colorMode}
  376.        fitView
  377.      >
  378.        <Controls />
  379.        <MiniMap />
  380.        <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
  381.      </ReactFlow>
  382.    </MainContext.Provider>
  383.  )
  384. }
  385.  
Advertisement
Add Comment
Please, Sign In to add comment