Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- "use client"
- import React, { createContext, useCallback, useContext, useEffect, useState } from "react"
- import {
- ReactFlow,
- MiniMap,
- Controls,
- Background,
- useNodesState,
- useEdgesState,
- addEdge,
- BackgroundVariant,
- type Connection,
- type NodeProps,
- type Node,
- } from "@xyflow/react"
- import "@xyflow/react/dist/style.css"
- import { useTheme } from "next-themes"
- import Yoga, { Edge, FlexDirection, Gutter, Wrap, type Node as YogaNode } from "yoga-layout"
- import { MoreHorizontal } from "lucide-react"
- import { cn } from "@/lib/utils"
- import Color from "color"
- import Link from "next/link"
- import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger, useToast } from "@/components/ui"
- import { LinkButton } from "@/components/custom-ui/buttons/link-buttons"
- import { Pencil2Icon, TrashIcon } from "@radix-ui/react-icons"
- import { deleteModal } from "@/components/custom-ui/modal/toast"
- import { deleteAsset } from "../assets/actions"
- import { useRouter } from "next/navigation"
- import { severityToColorSafe } from "@/components/severity-badge"
- import Image from "next/image"
- export type Zone = "dmz" | "it" | "ot" | "others"
- export type Asset = {
- id: string
- name: string
- zone: Zone
- address: string
- findings: number
- criticality: string
- type: string
- }
- function groupAssets(assets: Asset[]): Record<Zone, Asset[]> {
- const result: Record<Zone, Asset[]> = {
- dmz: [],
- it: [],
- ot: [],
- others: [],
- }
- for (const asset of assets) {
- result[asset.zone].push(asset)
- }
- return result
- }
- type MainContextData = {
- currentDropdown: string | null
- setCurrentDropdown: (id: string | null) => void
- canDelete: boolean
- canEdit: boolean
- handleDelete: (asset: Pick<AssetData, "id" | "name">) => void
- }
- const MainContext = createContext<MainContextData>({
- currentDropdown: null,
- canDelete: false,
- canEdit: false,
- setCurrentDropdown: () => { },
- handleDelete: () => { }
- })
- const chooseAssetIcon = (type: string): string => {
- const images: Record<typeof type, string> = {
- // TODO: add images
- }
- return images[type] ?? "https://picsum.photos/104/104"
- }
- type AssetData = Asset
- type AssetNodeType = Node<AssetData, "asset">
- const AssetNode = ({ data: asset }: NodeProps<AssetNodeType>) => {
- const { currentDropdown, setCurrentDropdown, canDelete, canEdit, handleDelete } = useContext(MainContext)
- const id = asset.address
- const dropdownStyle = "h-fit w-full justify-start gap-3.5 px-3.5 py-2.5 text-xs"
- return (
- <div key={asset.id} className="nodrag flex h-[220px] w-[140px] flex-col rounded-md bg-white text-black">
- <div className="flex w-full justify-end pt-1 pr-2 min-h-4">
- {(canEdit || canDelete) && (
- <DropdownMenu open={currentDropdown === id} onOpenChange={() => setCurrentDropdown(id)}>
- <DropdownMenuTrigger>
- <MoreHorizontal size={16} />
- </DropdownMenuTrigger>
- <DropdownMenuContent>
- <DropdownMenuLabel>Manage Asset</DropdownMenuLabel>
- <DropdownMenuSeparator />
- {canDelete && (
- <DropdownMenuItem asChild>
- <Button
- onClick={async (e) => {
- e.stopPropagation()
- handleDelete(asset)
- }}
- variant={"ghost"}
- size={"sm"}
- className={dropdownStyle}
- >
- <TrashIcon />
- Delete
- </Button>
- </DropdownMenuItem>
- )}
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </div>
- <div className="flex w-full justify-center py-2">
- <div className="h-[104px] w-[104px]">
- <Image alt="asset type icon" width={104} height={104} src={chooseAssetIcon(asset.type)} />
- </div>
- </div>
- <div className="mt-2.5 flex-col px-2.5">
- <div className="flex gap-1">
- <div className="flex-1 min-w-0">
- <p className="truncate">{asset.name}</p>
- </div>
- <div className={cn("mt-1 size-[6px] self-center rounded-full", severityToColorSafe(asset.criticality))}></div>
- </div>
- <div className="flex-1 min-w-0 text-xs text-muted">
- <p className="truncate">{asset.address}</p>
- </div>
- <Link href={`/view-findings?affectedAsset.ipAddress=${asset.address}`}>
- <div className="text-xs text-muted underline">{asset.findings >= 100 ? "99+" : asset.findings} Findings</div>
- </Link>
- </div>
- </div>
- )
- }
- type ZoneData = { name: string; size: { x: number; y: number }; className?: string; color?: string; isEmpty: boolean }
- type ZoneNodeType = Node<ZoneData, "zone">
- const ZoneNode = ({ data: { size, className, color, name, isEmpty } }: NodeProps<ZoneNodeType>) => {
- const { resolvedTheme } = useTheme()
- const colors = color
- ? genColors(color)
- : {
- border: resolvedTheme === 'dark' ? "white" : "black",
- text: resolvedTheme === 'dark' ? "white" : "black",
- textMuted: "gray",
- background: "transparent",
- }
- return (
- <div
- className={cn("nodrag relative rounded-md border grid place-items-center", className ?? "")}
- style={{
- width: `${size.x}px`,
- height: `${size.y}px`,
- borderColor: colors.border,
- backgroundColor: colors.background,
- color: colors.text,
- }}
- >
- <span className="absolute left-0 top-0 -translate-y-full pb-1">{name}</span>
- {isEmpty && <span style={{ color: colors.textMuted }}>There's no asset in this zone.</span>}
- </div>
- )
- }
- type BoxData = { size: { x: number; y: number } }
- type BoxNodeType = Node<BoxData, "box">
- const BoxNode = ({ data: { size } }: NodeProps<BoxNodeType>) => {
- return (
- <div
- className="nodrag"
- style={{
- width: `${size.x}px`,
- height: `${size.y}px`,
- border: "1px solid white",
- display: "grid",
- placeItems: "center center",
- }}
- ></div>
- )
- }
- function genColors(base: string) {
- // TODO: take color mode into consideration
- // background: the lighten factor should be higher for dark mode
- const baseColor = Color(base)
- const background = baseColor.alpha(0.2).lighten(0.5)
- const text = base
- const border = baseColor.darken(0.2)
- const textMuted = baseColor.mix(Color(background), 0.4)
- return {
- background: background.string(),
- text,
- border: border.string(),
- textMuted: textMuted.string(),
- }
- }
- function generateNodesForZone(assets: Asset[]): [YogaNode, { node: YogaNode; asset: Asset }[]] {
- const zone = Yoga.Node.create()
- zone.setMinHeight(260)
- zone.setWidth(40 + 60 + 4 * 140)
- zone.setPadding(Edge.All, 20)
- zone.setGap(Gutter.All, 20)
- zone.setFlexDirection(FlexDirection.Row)
- zone.setFlexWrap(Wrap.Wrap)
- const mappedAssets = assets.map((asset, idx) => {
- const node = Yoga.Node.create()
- node.setWidth(140)
- node.setHeight(220)
- zone.insertChild(node, idx)
- return { node, asset }
- })
- return [zone, mappedAssets]
- }
- function createZoneNode(
- node: YogaNode,
- { id, name, color, isEmpty }: { id: string; name: string; color?: string; isEmpty: boolean }
- ) {
- const { left, top } = getAbsolutePosition(node)
- return {
- id,
- type: "zone",
- data: { size: { x: node.getComputedWidth(), y: node.getComputedHeight() }, name, color, isEmpty },
- position: { x: left, y: top },
- zIndex: 0,
- selectable: false,
- }
- }
- function getAbsolutePosition(node: YogaNode) {
- let left = node.getComputedLeft()
- let top = node.getComputedTop()
- while (node.getParent() !== null) {
- node = node.getParent()!
- left += node.getComputedLeft()
- top += node.getComputedTop()
- }
- return { left, top }
- }
- function generateNodes(assets: Record<Zone, Asset[]>) {
- const root = Yoga.Node.create()
- const org = Yoga.Node.create()
- const ext = Yoga.Node.create()
- org.setWidth("auto")
- org.setHeight("auto")
- org.setPadding(Edge.Horizontal, 20)
- org.setPadding(Edge.Top, 40)
- org.setPadding(Edge.Bottom, 20)
- org.setGap(Gutter.All, 40)
- ext.setWidth("auto")
- ext.setHeight("auto")
- ext.setGap(Gutter.All, 40)
- root.setGap(Gutter.All, 40)
- root.setFlexDirection(FlexDirection.Row)
- const [it, its] = generateNodesForZone(assets.it)
- const [dmz, dmzs] = generateNodesForZone(assets.dmz)
- const [ot, ots] = generateNodesForZone(assets.ot)
- const [other, others] = generateNodesForZone(assets.others)
- org.insertChild(it, 0)
- org.insertChild(ot, 1)
- ext.insertChild(dmz, 0)
- ext.insertChild(other, 1)
- root.insertChild(org, 0)
- root.insertChild(ext, 1)
- root.calculateLayout(undefined, undefined)
- return [
- createZoneNode(dmz, { id: "dmz", name: "DMZ", color: "#9200D6", isEmpty: dmzs.length === 0 }),
- createZoneNode(it, { id: "it", name: "Internal Technology", color: "#1387b9", isEmpty: its.length === 0 }),
- createZoneNode(ot, { id: "ot", name: "Operational Technology", color: "#FF2EB7", isEmpty: ots.length === 0 }),
- createZoneNode(other, { id: "others", name: "Others", color: "#999", isEmpty: others.length === 0 }),
- createZoneNode(org, { id: "org", name: "Organization", isEmpty: false }),
- ...[dmzs, its, ots, others].flat().map((it, idx) => {
- const { left, top } = getAbsolutePosition(it.node)
- return {
- id: idx.toString(),
- type: "asset",
- data: it.asset,
- zIndex: 1,
- position: {
- x: left,
- y: top,
- },
- }
- }),
- ]
- }
- const nodeTypes = {
- asset: AssetNode,
- zone: ZoneNode,
- box: BoxNode,
- }
- export default function View({ canDelete, canEdit, assets }: { canDelete: boolean; canEdit: boolean; assets: Asset[] }) {
- const { toast } = useToast()
- const router = useRouter()
- const handleDelete = async (asset: Pick<AssetData, "id" | "name">) => {
- const isConfirmed = await deleteModal(`Are you sure you want to delete ${asset.name}?`)
- if (!isConfirmed) {
- return
- }
- const response = await deleteAsset(asset.id)
- toast({
- variant: response.success ? "default" : "destructive",
- title: response.message,
- })
- if (!response.success) {
- return
- }
- console.log('should be refreshed now', { router })
- router.refresh()
- }
- const colorMode = ((theme: string | undefined) => {
- switch (theme) {
- case "dark":
- return "dark"
- case "light":
- return "light"
- default:
- return "system"
- }
- })(useTheme().resolvedTheme)
- const grouped = groupAssets(assets)
- const initialNodes = generateNodes(grouped)
- const [nodes, _setNodes, onNodesChange] = useNodesState(initialNodes)
- const [edges, setEdges, onEdgesChange] = useEdgesState([])
- const onConnect = useCallback(
- (params: Connection) => setEdges((eds) => addEdge(params, eds)),
- [setEdges]
- )
- const [currentDropdown, set] = useState<MainContextData['currentDropdown']>(null)
- return (
- <MainContext.Provider value={{
- canDelete, canEdit, handleDelete,
- currentDropdown, setCurrentDropdown(id) {
- set(prevId => (prevId === id ? null : id))
- },
- }}>
- <ReactFlow
- nodes={nodes}
- edges={edges}
- nodeTypes={nodeTypes}
- onNodesChange={onNodesChange}
- onEdgesChange={onEdgesChange}
- onConnect={onConnect}
- colorMode={colorMode}
- fitView
- >
- <Controls />
- <MiniMap />
- <Background variant={BackgroundVariant.Dots} gap={12} size={1} />
- </ReactFlow>
- </MainContext.Provider>
- )
- }
Advertisement
Add Comment
Please, Sign In to add comment