Add tree layout modes
package.json, global.css, nodes.css, tables.css, EntityEditor.tsx, Headers.tsx, Loaders.tsx, OverlayModal.tsx, graphSlice.ts, AppLayout.tsx, EntityDetails.tsx, GraphOverview.tsx, form.module.css, Subpanel.tsx, subpanel.module.css, index.tsx, BaseMiniNode.tsx, BaseNode.tsx, EntityOptions.tsx, ProjectGraph.tsx, ConnectionLine.tsx, Graph.tsx, SimpleFloatingEdge.tsx, utils.ts, and 5 more files
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
"d3-force": "^3.0.0",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"dagre": "^0.8.5",
|
||||
"elkjs": "^0.8.2",
|
||||
"entitree-flex": "^0.4.1",
|
||||
"graphology": "^0.25.4",
|
||||
"i18next": "^23.6.0",
|
||||
|
||||
@@ -83,7 +83,7 @@ h6,
|
||||
button,
|
||||
a,
|
||||
figcaption {
|
||||
@apply text-dark-400 font-display;
|
||||
@apply text-mirage-400 font-display;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -194,26 +194,22 @@ input[type='number'] {
|
||||
/* end hide number inputs */
|
||||
|
||||
/* start react flow style overrides */
|
||||
.react-flow__controls-button {
|
||||
@apply !border-dark-400 !fill-slate-400 hover:!bg-dark-200 !bg-dark-600;
|
||||
}
|
||||
|
||||
.react-flow__attribution {
|
||||
@apply !bg-dark-600;
|
||||
@apply !bg-mirage-600;
|
||||
}
|
||||
.react-flow__attribution a {
|
||||
@apply !text-slate-500;
|
||||
}
|
||||
|
||||
.react-flow__edge-path {
|
||||
stroke: #3a3c40 !important;
|
||||
stroke: #334155 !important;
|
||||
}
|
||||
.react-flow__edge-path:active {
|
||||
stroke: #60666a !important;
|
||||
}
|
||||
.react-flow__edge {
|
||||
|
||||
/* .react-flow__edge {
|
||||
cursor: grab !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
} */
|
||||
.react-flow__node {
|
||||
cursor: default !important;
|
||||
}
|
||||
@@ -236,10 +232,10 @@ input[type='file'] {
|
||||
:root {
|
||||
--toastify-color-light: #fff;
|
||||
--toastify-color-dark: #121212;
|
||||
--toastify-color-info: #3498db;
|
||||
--toastify-color-success: #07bc0c;
|
||||
--toastify-color-warning: #f1c40f;
|
||||
--toastify-color-error: #e74c3c;
|
||||
--toastify-color-info: #0163A4;
|
||||
--toastify-color-success: #48B758;
|
||||
--toastify-color-warning: #CC9900;
|
||||
--toastify-color-error: #C1292E;
|
||||
--toastify-color-transparent: rgba(255, 255, 255, 0.7);
|
||||
|
||||
--toastify-icon-color-info: var(--toastify-color-info);
|
||||
@@ -250,7 +246,7 @@ input[type='file'] {
|
||||
--toastify-toast-width: 320px;
|
||||
--toastify-toast-background: #fff;
|
||||
--toastify-toast-min-height: 64px;
|
||||
--toastify-toast-max-height: 800px;
|
||||
--toastify-toast-max-height: 600px;
|
||||
--toastify-font-family: sans-serif;
|
||||
--toastify-z-index: 9999;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
.node .side-header {
|
||||
@apply px-0 py-2 flex-shrink-0 flex flex-col items-center justify-center w-10 text-slate-200 text-sm font-medium rounded-l-sm;
|
||||
@apply px-0 py-2 flex-shrink-0 flex flex-col items-center justify-center w-10 text-slate-50 text-sm font-medium rounded-l-sm;
|
||||
}
|
||||
|
||||
.node .side-header svg {
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
|
||||
.node .header {
|
||||
@apply flex h-full w-full items-center justify-between rounded-t-md text-slate-400/40 py-2 px-1 cursor-grab focus:cursor-grabbing;
|
||||
@apply flex h-full w-full items-center justify-between rounded-t-md text-slate-50/70 py-2 px-1 cursor-grab focus:cursor-grabbing;
|
||||
}
|
||||
|
||||
.node .header svg {
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
.text-container p:first-child,
|
||||
.text-container p:first-child > span {
|
||||
@apply text-[0.4rem] text-slate-400 font-display;
|
||||
@apply text-[0.4rem] text-slate-100/90 font-display;
|
||||
}
|
||||
|
||||
.text-container p:last-child {
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
|
||||
/* global table styles */
|
||||
.primary-table {
|
||||
@apply min-w-full divide-y-2 rounded-t-lg overflow-hidden divide-dark-800 table-fixed;
|
||||
@apply min-w-full divide-y-2 rounded-t-lg overflow-hidden divide-mirage-800 table-fixed;
|
||||
}
|
||||
.primary-table-wrap {
|
||||
@apply overflow-y-scroll max-h-[20rem];
|
||||
}
|
||||
.primary-table thead {
|
||||
@apply min-w-full w-full bg-dark-600;
|
||||
@apply min-w-full w-full bg-mirage-600;
|
||||
}
|
||||
.primary-table thead tr th {
|
||||
@apply bg-dark-600 text-slate-300 sticky top-0 z-50 first:py-3.5 pr-2 py-3.5 text-left text-sm font-semibold first:sm:pl-8 last:w-96 even:w-52 first:w-20 first:pr-20 whitespace-nowrap truncate ;
|
||||
@apply bg-mirage-600 text-slate-300 sticky top-0 z-50 first:py-3.5 pr-2 py-3.5 text-left text-sm font-semibold first:sm:pl-8 last:w-96 even:w-52 first:w-20 first:pr-20 whitespace-nowrap truncate ;
|
||||
}
|
||||
.primary-table tbody {
|
||||
@apply bg-dark-500 pb-2 max-h-[20rem];
|
||||
@apply bg-mirage-500 pb-2 max-h-[20rem];
|
||||
}
|
||||
.primary-table-wrap.no-scroll {
|
||||
@apply overflow-hidden;
|
||||
@@ -30,7 +30,7 @@
|
||||
@apply lg:first:w-48 py-2 pr-3 text-sm text-slate-400 first:pl-8
|
||||
}
|
||||
.entity-row > td:nth-child(2) {
|
||||
@apply min-w-[32rem];
|
||||
@apply min-w-[32rem];
|
||||
}
|
||||
/* end global table styles */
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Icon } from "../Icons";
|
||||
import { useCreateEntityMutation, useGetEntitiesQuery, useUpdateEntityByIdMutation, useGetEntityQuery, Entity } from '@/app/api';
|
||||
import { UpdateEntityByIdApiArg } from '../../app/api';
|
||||
import { useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
export const tokyoNightTheme = auraInit({
|
||||
settings: {
|
||||
@@ -39,19 +40,17 @@ const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
interface EntityEditorProps {
|
||||
activeEntity?: Entity
|
||||
refetchEntity: () => void
|
||||
}
|
||||
|
||||
export default function EntityEditor({ activeEntity }: EntityEditorProps) {
|
||||
export default function EntityEditor({ activeEntity, refetchEntity }: EntityEditorProps) {
|
||||
const [isEntityDraggable, setEntityDraggable] = useState(false);
|
||||
const [code, setCode] = useState(activeEntity?.source)
|
||||
useEffect(() => {
|
||||
if (activeEntity?.source) setCode(activeEntity.source)
|
||||
}, [activeEntity?.source])
|
||||
const [updateEntityById] = useUpdateEntityByIdMutation()
|
||||
const { refetch: refetchEntities } = useGetEntitiesQuery({
|
||||
limit: 50,
|
||||
skip: 0,
|
||||
})
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -104,17 +103,10 @@ export default function EntityEditor({ activeEntity }: EntityEditorProps) {
|
||||
<div className="flex justify-between items-center w-full text-slate-400 ">
|
||||
<button
|
||||
onClick={() => {
|
||||
// sdk.entities.updateEntityByUuid(activeEntity.uuid, {
|
||||
// source: pythonCode,
|
||||
// label: activeEntity.label,
|
||||
// description: activeEntity.description,
|
||||
// author: activeEntity.author
|
||||
// }).then(() => {
|
||||
// toast.info(
|
||||
// `The ${activeEntity.label} entity has been saved.`
|
||||
// );
|
||||
// }).catch(error => console.error(error));
|
||||
updateEntityById({ hid: activeEntity?.id ?? "", entityBase: { source: code } })
|
||||
updateEntityById({ hid: activeEntity?.id ?? "", entityBase: { source: code } }).then(() => toast.info(
|
||||
`The ${activeEntity?.label} entity has been saved.`
|
||||
))
|
||||
refetchEntity()
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -15,7 +15,7 @@ export const InquiryHeader = ({ title, header, className, btnTxt, btnAction }: P
|
||||
<div className='min-w-0 max-w-2xl flex-auto pt-3 lg:max-w-none lg:pr-0 px-2'>
|
||||
<section>
|
||||
<header className='space-y-1'>
|
||||
{title && <h4 className='font-display text-sm font-medium text-sky-500'>{title}</h4>}
|
||||
{title && <h4 className='font-display text-sm font-medium text-primary-300'>{title}</h4>}
|
||||
{header && (
|
||||
<h1 className='font-display leading-4 text-xl tracking-tight text-slate-400 dark:text-slate-300'>{header}</h1>
|
||||
)}
|
||||
@@ -25,7 +25,7 @@ export const InquiryHeader = ({ title, header, className, btnTxt, btnAction }: P
|
||||
{btnTxt && <button
|
||||
type='button'
|
||||
onClick={() => btnAction ? btnAction() : null}
|
||||
className='ml-auto relative mb-3.5 ring-1 bg-dark-800 pr-3 text-left text-sm font-semibold text-info-100 hover:text-info-200 flex items-center border border-info-200 hover:border-info-300 py-2 px-3 rounded-md mr-1 z-50'
|
||||
className='ml-auto relative mb-3.5 ring-1 bg-dark-800 pr-3 text-left text-sm font-semibold text-primary-100 hover:text-primary-200 flex items-center border border-primary-200 hover:border-primary-300 py-2 px-3 rounded-md mr-1 z-50'
|
||||
>
|
||||
{btnTxt}
|
||||
<PlusIcon className='ml-2 w-5 h-5 ' />
|
||||
|
||||
@@ -23,11 +23,11 @@ export default function RoundLoader({ className }: RoundLoaderProps): React.Reac
|
||||
export function UnderConstruction({ header, description, className = 'flex px-6' }: any) {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className='bg-dark-600 w-full block shadow sm:rounded-lg '>
|
||||
<div className='border-b border-dark-300 mx-4 py-5 sm:px-6'>
|
||||
<div className='bg-mirage-700/50 w-full block shadow sm:rounded-lg '>
|
||||
<div className='border-b border-mirage-300 mx-4 py-5 sm:px-6'>
|
||||
<div className='-ml-6 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap'>
|
||||
<div className='ml-4 mt-2'>
|
||||
<h1 className='font-display text-2xl tracking-tight text-slate-200 dark:text-white'>
|
||||
<h1 className='font-display text-2xl tracking-tight text-slate-300'>
|
||||
Under Construction
|
||||
</h1>
|
||||
</div>
|
||||
@@ -38,7 +38,7 @@ export function UnderConstruction({ header, description, className = 'flex px-6'
|
||||
<p className='my-3 ml-6 text-slate-400 max-w-xl'>
|
||||
{description}
|
||||
<a
|
||||
className='hover:text-info-100 text-info-200'
|
||||
className='hover:text-primary-100 text-primary-200'
|
||||
href='https://github.com/jerlendds/osintbuddy/discussions'
|
||||
target='_blank'
|
||||
>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function OverlayModal({
|
||||
leaveFrom='opacity-100'
|
||||
leaveTo='opacity-0'
|
||||
>
|
||||
<div className='fixed inset-0 bg-dark-900 bg-opacity-75 transition-opacity' />
|
||||
<div className='fixed inset-0 bg-mirage-600 bg-opacity-75 transition-opacity' />
|
||||
</Transition.Child>
|
||||
|
||||
<div className='fixed inset-0 z-10 overflow-y-auto'>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
|
||||
export type ProjectViewModes = 'base' | 'mini'
|
||||
|
||||
export type PositionModes = 'manual' | 'force' | 'hierarchy' | 'tree' | 'cola'
|
||||
export type PositionModes = 'manual' | 'force' | 'hierarchy' | 'tree' | 'cola' | 'right tree' | 'tree 1'
|
||||
|
||||
export interface Graph extends EditState {
|
||||
nodes: Node[];
|
||||
@@ -139,13 +139,13 @@ export const graph = createSlice({
|
||||
state.edges.push({
|
||||
...action.payload,
|
||||
style: {
|
||||
strokeWidth: 1.5,
|
||||
strokeWidth: 2.5,
|
||||
},
|
||||
markerEnd: {
|
||||
type: MarkerType.ArrowClosed,
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: '#3a3c40',
|
||||
width: 19,
|
||||
height: 20,
|
||||
color: '#334155',
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -173,8 +173,8 @@ export const graph = createSlice({
|
||||
resetGraph: (state) => {
|
||||
state.nodes = [];
|
||||
state.edges = [];
|
||||
state.positionMode = 'manual';
|
||||
state.viewMode = 'base';
|
||||
// state.positionMode = 'manual';
|
||||
// state.viewMode = 'base';
|
||||
},
|
||||
|
||||
updateNodeData: (state, action: PayloadAction<Node>) => {
|
||||
@@ -221,6 +221,7 @@ export const graph = createSlice({
|
||||
},
|
||||
|
||||
updateEdgeEvent: (state, action: PayloadAction<UpdateEdgeEvent>) => {
|
||||
console.log(updateEdge(action.payload.oldEdge, action.payload.newConnection, state.edges))
|
||||
state.edges = updateEdge(action.payload.oldEdge, action.payload.newConnection, state.edges);
|
||||
},
|
||||
addNodeUpdate: (state, action: PayloadAction<any>) => {
|
||||
|
||||
@@ -58,10 +58,10 @@ export default function AppLayout() {
|
||||
pauseOnFocusLoss
|
||||
draggable
|
||||
pauseOnHover
|
||||
theme="light"
|
||||
theme="dark"
|
||||
toastStyle={{
|
||||
backgroundColor: "rgb(23 35 65)",
|
||||
color: "rgb(148, 163, 184)",
|
||||
backgroundColor: "#1C233B",
|
||||
color: "#94a3b8",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -6,12 +6,11 @@ import { useGetEntityQuery } from "@/app/api";
|
||||
export default function EntityDetailsPage() {
|
||||
const { hid = "" } = useParams()
|
||||
const { isLoading, data: activeEntity, refetch: refetchEntity } = useGetEntityQuery({ hid })
|
||||
console.log(activeEntity, hid)
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<section className="flex w-full h-full relative">
|
||||
<EntityEditor key={`${isLoading}${hid}`} activeEntity={activeEntity} />
|
||||
<EntityEditor activeEntity={activeEntity} refetchEntity={refetchEntity} />
|
||||
</section>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function GraphOverview() {
|
||||
return <>
|
||||
<div className="w-full items-center justify-center my-14">
|
||||
<div className="grid place-items-center relative z-10 text-slate-400">
|
||||
<div className="from-mirage-400/30 bg-gradient-to-tr from-40% to-mirage-300/10 border-dark-400 border rounded-md grid place-items-center my-8 py-16 px-4 md:px-16 lg:px-20">
|
||||
<div className="from-mirage-400/30 bg-gradient-to-tr from-40% to-mirage-300/10 border-mirage-400 border rounded-md grid place-items-center my-8 py-16 px-4 md:px-16 lg:px-20">
|
||||
{favoriteGraphs.length === 0 && graphs.length === 0 && (
|
||||
<>
|
||||
<h1 className="text-slate-300 text-3xl lg:text-4xl font-bold pt-12 sm:pt-14 lg:pt-8 ">Oh no!</h1>
|
||||
|
||||
@@ -3,14 +3,13 @@
|
||||
}
|
||||
|
||||
.modal-form section:first-child {
|
||||
@apply border-b border-dark-300 py-5 pb-1 sm:px-6;
|
||||
@apply border-b border-mirage-300 py-5 pb-1 sm:px-6;
|
||||
}
|
||||
|
||||
.modal-form section:first-child > div {
|
||||
@apply -ml-6 -mt-2 flex flex-wrap items-center justify-between sm:flex-nowrap
|
||||
}
|
||||
|
||||
|
||||
.modal-form section:first-child > div h1 {
|
||||
@apply font-display text-2xl tracking-tight text-slate-400;
|
||||
}
|
||||
|
||||
@@ -8,13 +8,13 @@ import { SerializedError } from "@reduxjs/toolkit"
|
||||
|
||||
|
||||
const MAX_DESCRIPTION_LENGTH = 63
|
||||
|
||||
const MAX_LABEL_LENGTH = 31
|
||||
|
||||
export function GraphLoaderCard() {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-2">
|
||||
<div className="w-full py-6 space-y-5 rounded-md rounded-r-none bg-mirage-400/30 before:absolute px-4 bg-gradient-to-r from-transparent via-mirage-400/10 to-transparent relative before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-mirage-300/5 before:to-transparent isolate overflow-hidden shadow-xl shadow-black/5 border-l border-y border-dark-500">
|
||||
<div className="w-full py-6 space-y-5 rounded-md rounded-r-none bg-mirage-400/30 before:absolute px-4 bg-gradient-to-r from-transparent via-mirage-400/10 to-transparent relative before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-mirage-300/5 before:to-transparent isolate overflow-hidden shadow-xl shadow-black/5 border-l border-y border-mirage-500">
|
||||
<div className="space-y-3">
|
||||
<div className="h-2 w-3/5 rounded-lg bg-mirage-500/60 animate-pulse"></div>
|
||||
<div className="h-2 w-4/5 rounded-lg bg-mirage-500/60 animate-pulse"></div>
|
||||
@@ -83,7 +83,7 @@ export default function Subpanel({
|
||||
to={`${to}/${item.id}`}
|
||||
className={styles["subpanel-link"] + " " + styles[`subpanel-link-${hid === item.id}`]}>
|
||||
<div>
|
||||
<p className={styles["subpanel-label"] + " " + styles[`subpanel-label-${hid === item.id}`]}>{item?.label.length > 30 ? `${item.label.slice(0, 30)}...` : item.label}</p>
|
||||
<p className={styles["subpanel-label"] + " " + styles[`subpanel-label-${hid === item.id}`]}>{item?.label.length > MAX_LABEL_LENGTH ? `${item.label.slice(0, MAX_LABEL_LENGTH)}...` : item.label}</p>
|
||||
<p className={styles["subpanel-desc"] + " " + styles[`subpanel-desc-${hid === item.id}`]}>{item?.description?.length ?? 0 >= MAX_DESCRIPTION_LENGTH ? `${item?.description?.slice(0, MAX_DESCRIPTION_LENGTH)}...` : item.description}</p>
|
||||
<p className={styles["subpanel-desc"] + " " + styles[`subpanel-desc-${hid === item.id}`]}>Last seen {formatPGDate(item?.last_edited ? item.last_edited : item.last_seen)}</p>
|
||||
</div>
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
}
|
||||
|
||||
svg.link-icon {
|
||||
@apply bg-mirage-500/80 border-transparent border ml-auto rounded-md p-1 h-8 w-8 transition-colors duration-150 hover:text-primary-300 hover:border-mirage-300/20
|
||||
@apply bg-mirage-500/80 border-transparent border ml-auto rounded-md p-1 h-8 w-8 transition-colors duration-150 hover:text-primary-300 hover:border-mirage-300/20 mr-2
|
||||
}
|
||||
svg.link-icon-true {
|
||||
@apply !text-primary-300 hover:!text-primary-200;
|
||||
|
||||
@@ -175,7 +175,7 @@ export default function DashboardPage() {
|
||||
) : (
|
||||
<button
|
||||
className='btn-primary mx-4 mr-6'
|
||||
>Upload plugin</button>
|
||||
>Share plugin(s)</button>
|
||||
)}
|
||||
</aside>
|
||||
<Outlet context={{
|
||||
|
||||
@@ -7,6 +7,7 @@ import { Handle, Position } from 'reactflow';
|
||||
import { GripIcon, Icon } from '@/components/Icons';
|
||||
import { Dialog } from '@headlessui/react';
|
||||
import { useAppDispatch, useAppSelector, useComponentVisible } from '@/app/hooks';
|
||||
import Color from 'color';
|
||||
import { type ThunkDispatch } from 'redux-thunk';
|
||||
import {
|
||||
setEditState,
|
||||
@@ -32,7 +33,7 @@ const getNodeKey = () => {
|
||||
return `k_${nodeKey}`;
|
||||
};
|
||||
|
||||
const handleStyle = { borderColor: '#60666A' };
|
||||
const handleStyle = { borderColor: '#39477899', background: '#12172720', width: 12, margin: -1, height: 12 };
|
||||
|
||||
type NodeElement = NodeInput & {
|
||||
nodeId: string;
|
||||
@@ -41,76 +42,13 @@ type NodeElement = NodeInput & {
|
||||
Dispatch<AnyAction>;
|
||||
};
|
||||
|
||||
const getNodeElement = (nodeId, value, element: NodeInput, key: string | null = getNodeKey(), sendJsonMessage) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
switch (element.type) {
|
||||
case 'dropdown':
|
||||
return (
|
||||
<DropdownInput
|
||||
key={key}
|
||||
nodeId={nodeId}
|
||||
value={value}
|
||||
options={element.options || []}
|
||||
label={element.label}
|
||||
value={element.value as string}
|
||||
sendJsonMessage={sendJsonMessage}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
);
|
||||
case 'text':
|
||||
return (
|
||||
<TextInput
|
||||
key={key}
|
||||
nodeId={nodeId}
|
||||
label={element?.label}
|
||||
icon={element?.icon || 'ballpen'}
|
||||
sendJsonMessage={sendJsonMessage}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
);
|
||||
case 'upload':
|
||||
return (
|
||||
<h2 className='text-slate-400 text-xs max-w-sm font-sans'>
|
||||
Editing upload entity elements is not currently supported.
|
||||
</h2>
|
||||
);
|
||||
case 'title':
|
||||
return (
|
||||
<h2 className='text-slate-400 text-xs max-w-sm font-sans'>
|
||||
Editing title entity elements is not currently supported.
|
||||
</h2>
|
||||
);
|
||||
case 'section':
|
||||
return (
|
||||
<h2 className='text-slate-400 text-xs max-w-sm font-sans'>
|
||||
Editing section entity elements is not currently supported.
|
||||
</h2>
|
||||
);
|
||||
case 'copy-text':
|
||||
return (
|
||||
<h2 className='text-slate-400 text-xs max-w-sm font-sans'>
|
||||
Editing copy entity elements is not currently supported.
|
||||
</h2>
|
||||
);
|
||||
case 'empty':
|
||||
return <div className='hidden' />;
|
||||
}
|
||||
};
|
||||
|
||||
export default function BaseMiniNode({
|
||||
ctx,
|
||||
sendJsonMessage,
|
||||
closeRef,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
dispatch
|
||||
}: JSONObject) {
|
||||
const node = ctx.data;
|
||||
|
||||
|
||||
const backgroundColor = node?.color?.length === 7 ? `${node.color}77` : node?.color ? node.color : '#145070';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle position={Position.Right} id='r1' key='r1' type='source' style={handleStyle} />
|
||||
@@ -124,134 +62,20 @@ export default function BaseMiniNode({
|
||||
<Handle position={Position.Left} id='l2' key='l2' type='target' style={handleStyle} />
|
||||
<div
|
||||
onDoubleClick={() => {
|
||||
setIsOpen(!isOpen)
|
||||
if (!isOpen) {
|
||||
dispatch(setEditId(ctx.id));
|
||||
|
||||
} else {
|
||||
dispatch(setEditId(null))
|
||||
}
|
||||
dispatch(setEditId(ctx.id));
|
||||
}}
|
||||
data-label-type={node.label}
|
||||
className='node container !rounded-full'
|
||||
style={node.style}
|
||||
>
|
||||
<div
|
||||
style={{ backgroundColor }} className='header !rounded-full !p-4'>
|
||||
// 99 === 0.6 opacity
|
||||
style={{ backgroundColor: node?.color?.length === 7 ? `${node.color}99` : node?.color ?? '#145070' }}
|
||||
className='header !rounded-full !p-4'
|
||||
>
|
||||
<Icon icon={node.icon} className='!h-20 !w-20 cursor-grab focus:cursor-grabbing' />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
function TextInput({ nodeId, label, sendJsonMessage, icon, dispatch }: NodeElement) {
|
||||
// @todo remove this hack once firefox supports `:has`
|
||||
const [isFocused, setFocused] = useState(false);
|
||||
const value = useAppSelector((state) => selectNodeValue(state, nodeId, label));
|
||||
|
||||
return (
|
||||
<>
|
||||
<p className='text-[0.5rem] ml-1 text-slate-400 whitespace-wrap font-semibold font-display mt-1'>{label}</p>
|
||||
<div className={classNames('nodrag node-field', isFocused && 'ring-info-400')}>
|
||||
<Icon icon={icon} className='h-6 w-6' />
|
||||
<input
|
||||
id={`${nodeId}-${label}`}
|
||||
type='text'
|
||||
data-label={label}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => {
|
||||
sendJsonMessage({ action: 'update:node', node: { id: nodeId, [label]: value } });
|
||||
setFocused(false);
|
||||
}}
|
||||
onChange={(event: InputEvent) => dispatch(saveUserEdits({ value: event.target.value, nodeId, label }))}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DropdownInput({ options, label, nodeId, sendJsonMessage, dispatch }: NodeElement) {
|
||||
const [query, setQuery] = useState('');
|
||||
const filteredOptions =
|
||||
query === ''
|
||||
? options ?? []
|
||||
: options?.filter((option: DropdownOption) => {
|
||||
return option.label.toLowerCase().includes(query.toLowerCase());
|
||||
}) ?? [];
|
||||
|
||||
const activeValue = useAppSelector((state) => selectNodeValue(state, nodeId, label));
|
||||
|
||||
const activeOption = options.find((option) => option.value === activeValue || option.label === activeValue) ?? {
|
||||
label: '',
|
||||
value: '',
|
||||
tooltip: '',
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Combobox
|
||||
className=' w-full z-[999] dropdown-input !pr-2'
|
||||
as='div'
|
||||
value={activeOption}
|
||||
onChange={(option) => {
|
||||
sendJsonMessage({
|
||||
action: 'update:node',
|
||||
node: {
|
||||
id: nodeId,
|
||||
[label]: option?.value ? option.value : option.label,
|
||||
},
|
||||
});
|
||||
dispatch(
|
||||
saveUserEdits({
|
||||
value: option?.value ? option.value : option.label,
|
||||
nodeId,
|
||||
label,
|
||||
})
|
||||
);
|
||||
}}
|
||||
>
|
||||
<Combobox.Label>
|
||||
<p className='text-[0.5rem] ml-1 text-slate-400 whitespace-wrap font-semibold font-display mt-1'>{label}</p>
|
||||
</Combobox.Label>
|
||||
<div className='relative mt-1 '>
|
||||
<Combobox.Input
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
displayValue={(option: DropdownOption) => option.label}
|
||||
className='nodrag focus:ring-info-400 node-field outline-none pl-2'
|
||||
/>
|
||||
<Combobox.Button className='absolute inset-y-0 right-0 flex items-center rounded-r-md px-2 focus:outline-none'>
|
||||
<ChevronUpDownIcon className='h-5 w-5 text-slate-600' aria-hidden='true' />
|
||||
</Combobox.Button>
|
||||
|
||||
{filteredOptions.length > 0 && (
|
||||
<Combobox.Options className='absolute z-10 mt-1 max-h-80 w-full overflow-auto rounded-b-md bg-dark-400 py-1 text-base shadow-lg ring-1 ring-gray-200 ring-opacity-5 focus:outline-none sm:text-sm'>
|
||||
{filteredOptions.map((option: DropdownOption) => (
|
||||
<Combobox.Option
|
||||
key={getKey()}
|
||||
value={option}
|
||||
className={({ active }) =>
|
||||
classNames(
|
||||
'relative nodrag nowheel cursor-default select-none py-2 pl-3 pr-9',
|
||||
active ? 'bg-slate-900 text-slate-300' : 'text-slate-400'
|
||||
)
|
||||
}
|
||||
>
|
||||
<span
|
||||
className={classNames('block truncate pl-2')}
|
||||
title={option?.tooltip !== option.label ? option.tooltip : 'No description found'}
|
||||
>
|
||||
{option.label}
|
||||
</span>
|
||||
</Combobox.Option>
|
||||
))}
|
||||
</Combobox.Options>
|
||||
)}
|
||||
</div>
|
||||
</Combobox>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const getNodeKey = () => {
|
||||
return `k_${nodeKey}`;
|
||||
};
|
||||
|
||||
const handleStyle = { borderColor: '#60666A' };
|
||||
const handleStyle = { borderColor: '#39477899', background: '#12172720', width: 12, margin: -1, height: 12 }
|
||||
|
||||
type NodeElement = NodeInput & {
|
||||
nodeId: string;
|
||||
@@ -105,8 +105,6 @@ export default function BaseNode({ ctx, sendJsonMessage, closeRef }: JSONObject)
|
||||
}
|
||||
};
|
||||
|
||||
const backgroundColor = node?.color?.length === 7 ? `${node.color}70` : node?.color ? node.color : '#145070';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Handle position={Position.Right} id='r1' key='r1' type='source' style={handleStyle} />
|
||||
@@ -119,14 +117,18 @@ export default function BaseNode({ ctx, sendJsonMessage, closeRef }: JSONObject)
|
||||
<Handle position={Position.Bottom} id='b2' key='b2' type='target' style={handleStyle} />
|
||||
<Handle position={Position.Left} id='l2' key='l2' type='target' style={handleStyle} />
|
||||
<div onDoubleClick={() => dispatch(clearEditId(ctx.id))} data-label-type={node.label} className=' node container' style={node.style}>
|
||||
<div style={{ backgroundColor }} className='header '>
|
||||
<GripIcon className='' />
|
||||
<div
|
||||
// 99 === 0.6 opacity
|
||||
style={{ backgroundColor: node?.color?.length === 7 ? `${node.color}99` : node?.color ?? '#145070' }}
|
||||
className='header '
|
||||
>
|
||||
<GripIcon />
|
||||
<div className='text-container '>
|
||||
<p className='text-[0.4rem] text-light-900 whitespace-wrap font-display'>
|
||||
<span className='text-[0.5rem] text-light-900 max-w-xl whitespace-wrap font-display'>ID: </span>
|
||||
<p className='text-[0.4rem] flex font-black text-mirage-900 whitespace-wrap font-display'>
|
||||
<span className='mr-1 text-[0.5rem] text-mirage-900 font-extralight max-w-xl whitespace-wrap '> ID: </span>
|
||||
{ctx.id}
|
||||
</p>
|
||||
<p className='text-xs text-light-200 max-w-xl whitespace-wrap font-display font-bold'>{node.label}</p>
|
||||
<p className='text-xs text-slate-200 max-w-xl whitespace-wrap font-display font-bold'>{node.label}</p>
|
||||
</div>
|
||||
<Icon icon={node.icon} className='h-5 w-5 mr-2 cursor-grab focus:cursor-grabbing' />
|
||||
</div>
|
||||
|
||||
@@ -110,7 +110,9 @@ export function EntityOption({ entity, onDragStart }: JSONObject) {
|
||||
|
||||
const ResponsiveGridLayout = WidthProvider(Responsive);
|
||||
|
||||
export default function EntityOptions({ allNodes, allEdges, positionMode, activeGraph, toggleForceLayout, isForceActive, setIsForceActive, }: JSONObject) {
|
||||
const MAX_GRAPH_LABEL_LENGTH = 22;
|
||||
|
||||
export default function EntityOptions({ allManualNodes, allManualEdges, positionMode, activeGraph, setElkLayout, toggleForceLayout, isForceActive }: JSONObject) {
|
||||
const {
|
||||
data: entitiesData = { entities: [], count: 0, favorite_entities: [], favorite_count: 0 },
|
||||
isLoading,
|
||||
@@ -204,13 +206,13 @@ export default function EntityOptions({ allNodes, allEdges, positionMode, active
|
||||
<ol className='text-sm flex select-none relative px-4 pt-2'>
|
||||
<li className='flex mr-auto'>
|
||||
<h5
|
||||
title={activeGraph.name}
|
||||
title={activeGraph.label}
|
||||
className='flex whitespace-nowrap truncate justify-between items-center w-full text-slate-600 text-inherit font-display '>
|
||||
<Link title='View all graphs' className='text-slate-600' to='/dashboard/graph' replace>
|
||||
Entities
|
||||
</Link>{" /"}
|
||||
<span className="text-slate-400/60">
|
||||
{activeGraph.label.length > 22 ? `${activeGraph.label.slice(0, 22)}...` : activeGraph.label}
|
||||
{activeGraph.label.length > MAX_GRAPH_LABEL_LENGTH ? `${activeGraph.label.slice(0, MAX_GRAPH_LABEL_LENGTH)}...` : activeGraph.label}
|
||||
</span>
|
||||
/
|
||||
</h5>
|
||||
@@ -251,7 +253,6 @@ export default function EntityOptions({ allNodes, allEdges, positionMode, active
|
||||
<ol className='text-sm flex select-none relative px-4 pt-1'>
|
||||
<li className='flex mr-auto'>
|
||||
<h5
|
||||
title={activeGraph.name}
|
||||
className='flex whitespace-nowrap truncate justify-between items-center w-full text-slate-600 text-inherit font-display '>
|
||||
<p className='text-slate-600' >
|
||||
Layouts
|
||||
@@ -280,8 +281,8 @@ export default function EntityOptions({ allNodes, allEdges, positionMode, active
|
||||
onClick={() => {
|
||||
isForceActive !== undefined && !isForceActive && toggleForceLayout(false)
|
||||
dispatch(setPositionMode('manual'))
|
||||
dispatch(setAllNodes([...allNodes]))
|
||||
dispatch(setAllEdges(allEdges))
|
||||
dispatch(setAllNodes(allManualNodes))
|
||||
dispatch(setAllEdges(allManualEdges))
|
||||
}}
|
||||
type='button'
|
||||
className={classNames(
|
||||
@@ -314,28 +315,38 @@ export default function EntityOptions({ allNodes, allEdges, positionMode, active
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
isForceActive && toggleForceLayout()
|
||||
|
||||
dispatch(setPositionMode('hierarchy'))
|
||||
isForceActive !== undefined && !isForceActive && toggleForceLayout(false)
|
||||
// setElkLayout({ 'elk.algorithm': 'org.eclipse.elk.radial', })
|
||||
// setElkLayout({ 'elk.algorithm': 'layered', 'elk.direction': 'DOWN' })
|
||||
// setElkLayout({ 'elk.algorithm': 'layered', 'elk.direction': 'RIGHT' })
|
||||
dispatch(setPositionMode('right tree'))
|
||||
setElkLayout({
|
||||
'elk.algorithm': 'layered',
|
||||
'elk.direction': 'RIGHT'
|
||||
})
|
||||
}}
|
||||
type='button'
|
||||
className={classNames(
|
||||
'justify-center flex-grow from-mirage-300/10 to-mirage-300/20 bg-gradient-to-br hover:from-mirage-500/20 hover:from-40% hover:to-mirage-300/30 border-mirage-300/20 relative py-3 inline-flex items-center border transition-colors duration-100 ease-in-out hover:border-primary-400/50 outline-none px-3 text-slate-500 hover:text-primary-300/80 focus:bg-mirage-800 hover:bg-mirage-600 focus:z-10',
|
||||
positionMode === 'hierarchy' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
positionMode === 'right tree' && 'bg-mirage-800 hover:bg-mirage-800 border-primary-400/50 hover:border-primary-400/50 '
|
||||
)}
|
||||
>
|
||||
<span className='sr-only'>List view</span>
|
||||
<Icon
|
||||
icon="route"
|
||||
className={classNames('h-5 w-5 text-inherit', positionMode === 'hierarchy' && 'text-primary-300')}
|
||||
icon="binary-tree-2"
|
||||
className={classNames('h-5 w-5 -rotate-90 origin-center text-inherit', positionMode === 'right tree' && 'text-primary-300')}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
isForceActive && toggleForceLayout()
|
||||
isForceActive && toggleForceLayout(false)
|
||||
|
||||
dispatch(setPositionMode('tree'))
|
||||
setElkLayout({
|
||||
'elk.algorithm': 'layered',
|
||||
'elk.direction': 'DOWN'
|
||||
})
|
||||
}}
|
||||
type='button'
|
||||
className={classNames(
|
||||
@@ -351,6 +362,9 @@ export default function EntityOptions({ allNodes, allEdges, positionMode, active
|
||||
/>
|
||||
</button>
|
||||
</ul>
|
||||
<section>
|
||||
<p className='py-4 px-2 text-slate-500 text-xs'>More layouts, options, and controls for managing entity layouts will be here one day. Get the latest updates on the <a className='text-primary-300' href="https://forum.osintbuddy.com/">forum</a> or <a className='text-primary-300' href="https://discord.gg/gsbbYHA3K3">discord</a></p>
|
||||
</section>
|
||||
</div>
|
||||
</ResponsiveGridLayout>
|
||||
);
|
||||
|
||||
21
frontend/src/routes/graph-inquiry/graph/ConnectionLine.tsx
Normal file
21
frontend/src/routes/graph-inquiry/graph/ConnectionLine.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
export default function NewConnectionLine({ fromX, fromY, toX, toY }: JSONObject) {
|
||||
return (
|
||||
<g>
|
||||
<path
|
||||
fill="none"
|
||||
stroke={'#394778'}
|
||||
strokeWidth={1.5}
|
||||
className="animated"
|
||||
d={`M${fromX},${fromY} C ${fromX} ${toY} ${fromX} ${toY} ${toX},${toY}`}
|
||||
/>
|
||||
<circle
|
||||
cx={toX}
|
||||
cy={toY}
|
||||
fill="#fff"
|
||||
r={3}
|
||||
stroke={'#394778'}
|
||||
strokeWidth={1.5}
|
||||
/>
|
||||
</g>
|
||||
);
|
||||
};
|
||||
@@ -2,21 +2,20 @@ import { useCallback, useState, useMemo, DragEventHandler, useEffect } from 'rea
|
||||
import ReactFlow, {
|
||||
Edge,
|
||||
Background,
|
||||
Controls,
|
||||
BackgroundVariant,
|
||||
FitViewOptions,
|
||||
NodeDragHandler,
|
||||
Connection,
|
||||
Panel,
|
||||
} from 'reactflow';
|
||||
import BaseNode from './BaseNode';
|
||||
import BaseNode from '../_components/BaseNode';
|
||||
import { addNodeUpdate, createEdge, onEdgesChange, updateEdgeEvent, updateNodeFlow } from '@/features/graph/graphSlice';
|
||||
import { useAppDispatch } from '@/app/hooks';
|
||||
import { toast } from 'react-toastify';
|
||||
import BaseMiniNode from './BaseMiniNode';
|
||||
import { CreateGraphEntityApiResponse, CreateNode, useCreateEntityMutation, useCreateGraphEntityMutation, useGetEntityTransformsQuery, useRefreshPluginsQuery } from '@/app/api';
|
||||
import { CreateGraphEntityApiArg } from '../../../app/api';
|
||||
import BaseMiniNode from '../_components/BaseMiniNode';
|
||||
import { CreateGraphEntityApiResponse, useCreateGraphEntityMutation, useRefreshPluginsQuery } from '@/app/api';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import NewConnectionLine from './ConnectionLine';
|
||||
import SimpleFloatingEdge from './SimpleFloatingEdge';
|
||||
|
||||
const viewOptions: FitViewOptions = {
|
||||
padding: 50,
|
||||
@@ -26,21 +25,19 @@ const viewOptions: FitViewOptions = {
|
||||
interface ProjectGraphProps extends JSONObject {
|
||||
}
|
||||
|
||||
export default function ProjectGraph({
|
||||
export default function Graph({
|
||||
onSelectionCtxMenu,
|
||||
onMultiSelectionCtxMenu,
|
||||
onPaneCtxMenu,
|
||||
onPaneClick,
|
||||
addEdge,
|
||||
graphRef,
|
||||
nodes,
|
||||
edges,
|
||||
graphInstance,
|
||||
setGraphInstance,
|
||||
addEdge,
|
||||
sendJsonMessage,
|
||||
onPaneClick,
|
||||
onPaneCtxMenu,
|
||||
onSelectionCtxMenu,
|
||||
onMultiSelectionCtxMenu,
|
||||
setIsEditingMini,
|
||||
isEditingMini,
|
||||
closeMiniRef,
|
||||
fitView
|
||||
}: ProjectGraphProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const onEdgeUpdate = useCallback(
|
||||
@@ -76,7 +73,7 @@ export default function ProjectGraph({
|
||||
const createNode = { label, position }
|
||||
createGraphEntity({ createNode, hid })
|
||||
.then(({ data }: CreateGraphEntityApiResponse) => dispatch(addNodeUpdate({ position, label, ...data, })))
|
||||
.catch((error) => {
|
||||
.catch((error: any) => {
|
||||
console.error(error)
|
||||
toast.error(`We ran into a problem creating the ${label} entity. Please try again`)
|
||||
})
|
||||
@@ -91,20 +88,13 @@ export default function ProjectGraph({
|
||||
() => ({
|
||||
base: (data: JSONObject) => (
|
||||
<BaseNode
|
||||
dispatch={dispatch}
|
||||
ctx={data}
|
||||
closeRef={closeMiniRef}
|
||||
sendJsonMessage={sendJsonMessage}
|
||||
dispatch={dispatch}
|
||||
/>),
|
||||
mini: (data: JSONObject) => (
|
||||
<BaseMiniNode
|
||||
dispatch={dispatch}
|
||||
isOpen={isEditingMini}
|
||||
setIsOpen={setIsEditingMini}
|
||||
closeRef={closeMiniRef}
|
||||
setIsEditing={setIsEditingMini}
|
||||
ctx={data}
|
||||
sendJsonMessage={sendJsonMessage}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
@@ -112,7 +102,9 @@ export default function ProjectGraph({
|
||||
);
|
||||
|
||||
const edgeTypes = useMemo(
|
||||
() => ({}),
|
||||
() => ({
|
||||
float: SimpleFloatingEdge
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -121,6 +113,11 @@ export default function ProjectGraph({
|
||||
sendJsonMessage({ action: 'update:node', node: { id: node.id, y: node.position.y } });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fitView && fitView()
|
||||
|
||||
}, [fitView])
|
||||
|
||||
return (
|
||||
<ReactFlow
|
||||
minZoom={0.05}
|
||||
@@ -144,6 +141,8 @@ export default function ProjectGraph({
|
||||
onPaneContextMenu={onPaneCtxMenu}
|
||||
onNodeContextMenu={onSelectionCtxMenu}
|
||||
onSelectionContextMenu={onMultiSelectionCtxMenu}
|
||||
connectionLineComponent={NewConnectionLine}
|
||||
|
||||
>
|
||||
|
||||
<Background variant={BackgroundVariant.Dots} className='bg-transparent' color='#394778' />
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useStore, getBezierPath } from 'reactflow';
|
||||
|
||||
import { getEdgeParams } from './utils';
|
||||
|
||||
function SimpleFloatingEdge({ id, source, target, markerEnd, style }: JSONObject) {
|
||||
const sourceNode = useStore(useCallback((store) => store.nodeInternals.get(source), [source]));
|
||||
const targetNode = useStore(useCallback((store) => store.nodeInternals.get(target), [target]));
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { sx, sy, tx, ty, sourcePos, targetPos } = getEdgeParams(sourceNode, targetNode);
|
||||
|
||||
const [edgePath] = getBezierPath({
|
||||
sourceX: sx,
|
||||
sourceY: sy,
|
||||
sourcePosition: sourcePos,
|
||||
targetPosition: targetPos,
|
||||
targetX: tx,
|
||||
targetY: ty,
|
||||
});
|
||||
|
||||
return (
|
||||
<path
|
||||
id={id}
|
||||
className="react-flow__edge-path"
|
||||
d={edgePath}
|
||||
strokeWidth={5}
|
||||
markerEnd={markerEnd}
|
||||
style={style}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default SimpleFloatingEdge;
|
||||
78
frontend/src/routes/graph-inquiry/graph/utils.ts
Normal file
78
frontend/src/routes/graph-inquiry/graph/utils.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { Position, internalsSymbol } from 'reactflow';
|
||||
|
||||
// returns the position (top,right,bottom or right) passed node compared to
|
||||
function getParams(nodeA: any, nodeB: any) {
|
||||
const centerA = getNodeCenter(nodeA);
|
||||
const centerB = getNodeCenter(nodeB);
|
||||
|
||||
const horizontalDiff = Math.abs(centerA.x - centerB.x);
|
||||
const verticalDiff = Math.abs(centerA.y - centerB.y);
|
||||
|
||||
let position;
|
||||
|
||||
// when the horizontal difference between the nodes is bigger, we use Position.Left or Position.Right for the handle
|
||||
if (horizontalDiff > verticalDiff) {
|
||||
position = centerA.x > centerB.x ? Position.Left : Position.Right;
|
||||
} else {
|
||||
// here the vertical difference between the nodes is bigger, so we use Position.Top or Position.Bottom for the handle
|
||||
position = centerA.y > centerB.y ? Position.Top : Position.Bottom;
|
||||
}
|
||||
|
||||
const [x, y] = getHandleCoordsByPosition(nodeA, position);
|
||||
return [x, y, position];
|
||||
}
|
||||
|
||||
function getHandleCoordsByPosition(node: any, handlePosition: any) {
|
||||
// all handles are from type source, that's why we use handleBounds.source here
|
||||
const handle = node[internalsSymbol].handleBounds.source.find(
|
||||
(h: any) => h.position === handlePosition
|
||||
);
|
||||
|
||||
let offsetX = handle.width / 2;
|
||||
let offsetY = handle.height / 2;
|
||||
|
||||
// this is a tiny detail to make the markerEnd of an edge visible.
|
||||
// The handle position that gets calculated has the origin top-left, so depending which side we are using, we add a little offset
|
||||
// when the handlePosition is Position.Right for example, we need to add an offset as big as the handle itself in order to get the correct position
|
||||
switch (handlePosition) {
|
||||
case Position.Left:
|
||||
offsetX = 0;
|
||||
break;
|
||||
case Position.Right:
|
||||
offsetX = handle.width;
|
||||
break;
|
||||
case Position.Top:
|
||||
offsetY = 0;
|
||||
break;
|
||||
case Position.Bottom:
|
||||
offsetY = handle.height;
|
||||
break;
|
||||
}
|
||||
|
||||
const x = node.positionAbsolute.x + handle.x + offsetX;
|
||||
const y = node.positionAbsolute.y + handle.y + offsetY;
|
||||
|
||||
return [x, y];
|
||||
}
|
||||
|
||||
function getNodeCenter(node: any) {
|
||||
return {
|
||||
x: node.positionAbsolute.x + node.width / 2,
|
||||
y: node.positionAbsolute.y + node.height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
// returns the parameters (sx, sy, tx, ty, sourcePos, targetPos) you need to create an edge
|
||||
export function getEdgeParams(source: any, target: any) {
|
||||
const [sx, sy, sourcePos] = getParams(source, target);
|
||||
const [tx, ty, targetPos] = getParams(target, source);
|
||||
|
||||
return {
|
||||
sx,
|
||||
sy,
|
||||
tx,
|
||||
ty,
|
||||
sourcePos,
|
||||
targetPos,
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useRef, useEffect, useMemo, useCallback, useContext } from 'react';
|
||||
import { UNSAFE_NavigationContext as NavigationContext, unstable_BlockerFunction } from 'react-router-dom';
|
||||
import { Edge, XYPosition, Node } from 'reactflow';
|
||||
import { Edge, XYPosition, Node, ReactFlowInstance } from 'reactflow';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { useParams, useLocation, useBlocker, useBeforeUnload } from 'react-router-dom';
|
||||
import useWebSocket, { ReadyState } from 'react-use-websocket';
|
||||
@@ -10,7 +10,8 @@ import 'reactflow/dist/style.css';
|
||||
import EntityOptions from './_components/EntityOptions';
|
||||
import ContextMenu from './_components/ContextMenu';
|
||||
import { toast } from 'react-toastify';
|
||||
import ProjectGraph from './_components/ProjectGraph';
|
||||
import Graph from './graph/Graph';
|
||||
import ELK from 'elkjs/lib/elk.bundled.js';
|
||||
import { useAppDispatch, useAppSelector, useComponentVisible, useEffectOnce } from '@/app/hooks';
|
||||
import {
|
||||
ProjectViewModes,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
selectAllEdges,
|
||||
selectPositionMode,
|
||||
selectViewMode,
|
||||
setAllEdges,
|
||||
setAllNodes,
|
||||
setPositionMode,
|
||||
} from '@/features/graph/graphSlice';
|
||||
@@ -47,10 +49,6 @@ interface UseWebsocket {
|
||||
interface GraphInquiryProps {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
const dispatch = useAppDispatch();
|
||||
const { hid } = useParams()
|
||||
@@ -74,7 +72,7 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
const [edgesBeforeLayout, setEdgesBeforeLayout] = useState(initialEdges)
|
||||
|
||||
const graphRef = useRef<HTMLDivElement>(null);
|
||||
const [graphInstance, setGraphInstance] = useState(null);
|
||||
const [graphInstance, setGraphInstance] = useState<ReactFlowInstance>();
|
||||
|
||||
const [showCommandPalette, setShowCommandPalette] = useState<boolean>(false);
|
||||
|
||||
@@ -110,17 +108,34 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
target: string,
|
||||
sourceHandle: string = 'r1',
|
||||
targetHandle: string = 'l2',
|
||||
type: string = 'default'
|
||||
type: string = 'float',
|
||||
label: string = '',
|
||||
id: any = null
|
||||
): void {
|
||||
dispatch(
|
||||
createEdge({
|
||||
source,
|
||||
target,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type,
|
||||
})
|
||||
);
|
||||
if (id) {
|
||||
dispatch(
|
||||
createEdge({
|
||||
source,
|
||||
target,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type,
|
||||
label,
|
||||
id
|
||||
})
|
||||
);
|
||||
} else {
|
||||
dispatch(
|
||||
createEdge({
|
||||
source,
|
||||
target,
|
||||
sourceHandle,
|
||||
targetHandle,
|
||||
type,
|
||||
label
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const togglePalette = () => setShowCommandPalette(!showCommandPalette);
|
||||
@@ -186,7 +201,7 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
if (!Array.isArray(lastJsonMessage)) {
|
||||
if (lastJsonMessage.action === 'addInitialLoad') {
|
||||
lastJsonMessage.nodes.forEach((node: JSONObject, idx: JSONObject) => addNode(node.id.toString(), node.data, node.position, 'mini'));
|
||||
lastJsonMessage.edges.forEach((edge: JSONObject) => addEdge(edge.source.toString(), edge.target.toString()));
|
||||
lastJsonMessage.edges.forEach((edge: JSONObject) => addEdge(`${edge.source}`, `${edge.target}`, 'r1', 'l2', 'float', '', `${edge.id}`));
|
||||
}
|
||||
if (lastJsonMessage.action === 'addNode') {
|
||||
lastJsonMessage.position.x += 560;
|
||||
@@ -263,6 +278,50 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
|
||||
const positionMode = useAppSelector((state) => selectPositionMode(state))
|
||||
|
||||
let fitView: any;
|
||||
if (graphInstance) {
|
||||
fitView = graphInstance.fitView
|
||||
}
|
||||
|
||||
// TODO: Also implement d3-hierarchy, entitree-flex, dagre, webcola, and graphology layout modes
|
||||
// Once implemented measure performance and deprecate whatever performs worse
|
||||
const elk = new ELK();
|
||||
const useElkLayoutElements = () => {
|
||||
const defaultOptions = {
|
||||
'elk.algorithm': 'layered',
|
||||
'elk.layered.spacing.nodeNodeBetweenLayers': 420,
|
||||
'elk.spacing.nodeNode': 180,
|
||||
};
|
||||
const allNodes = [...useAppSelector(state => selectAllNodes(state))]
|
||||
const allEdges = [...useAppSelector(state => selectAllEdges(state))]
|
||||
const setElkLayout = useCallback((options: any) => {
|
||||
const layoutOptions = { ...defaultOptions, ...options };
|
||||
const graph = {
|
||||
id: 'root',
|
||||
layoutOptions: layoutOptions,
|
||||
children: allNodes.map((node: any) => ({ ...node, x: node.position.x, y: node.position.y })),
|
||||
edges: allEdges.map((edge: any) => ({ ...edge, })),
|
||||
};
|
||||
elk.layout(graph as any).then(({ children, edges }: any) => {
|
||||
children.forEach((node: any) => {
|
||||
node.position = { x: node.x, y: node.y };
|
||||
});
|
||||
console.log('children', children)
|
||||
dispatch(resetGraph())
|
||||
dispatch(setAllNodes(children));
|
||||
dispatch(setAllEdges(edges))
|
||||
window.requestAnimationFrame(() => {
|
||||
fitView && fitView();
|
||||
});
|
||||
});
|
||||
}, [allNodes]);
|
||||
|
||||
return { setElkLayout };
|
||||
};
|
||||
|
||||
const { setElkLayout } = useElkLayoutElements();
|
||||
|
||||
|
||||
const useForceLayoutElements = () => {
|
||||
const allNodes = [...useAppSelector(state => selectAllNodes(state))]
|
||||
const allEdges = [...useAppSelector(state => selectAllEdges(state))]
|
||||
@@ -311,7 +370,9 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
dispatch(setAllNodes(forceNodes.map((node: any) => ({ ...node, position: { x: node.x, y: node.y } }))) as any);
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
if (running) tick();
|
||||
if (running) {
|
||||
tick()
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -322,6 +383,8 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
running = !running
|
||||
}
|
||||
running && window.requestAnimationFrame(tick);
|
||||
running && fitView && fitView();
|
||||
|
||||
};
|
||||
|
||||
return [true, { toggleForceLayout, isForceRunning: running }];
|
||||
@@ -329,7 +392,6 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
}
|
||||
|
||||
const [forceInitialized, { toggleForceLayout, isForceRunning }] = useForceLayoutElements();
|
||||
const { ref, isOpen, setIsOpen } = useComponentVisible(false);
|
||||
|
||||
|
||||
// Prevents bugs from occurring on navigate away and returning to a graph
|
||||
@@ -339,6 +401,10 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
[toggleForceLayout]
|
||||
));
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(setPositionMode('manual'))
|
||||
}, [activeGraph?.id])
|
||||
|
||||
return (
|
||||
<>
|
||||
{isError && (
|
||||
@@ -355,13 +421,13 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
toggleForceLayout={toggleForceLayout}
|
||||
isForceActive={isForceRunning}
|
||||
activeGraph={activeGraph}
|
||||
allNodes={nodesBeforeLayout}
|
||||
allEdges={edgesBeforeLayout}
|
||||
allManualNodes={nodesBeforeLayout}
|
||||
allManualEdges={edgesBeforeLayout}
|
||||
setElkLayout={setElkLayout}
|
||||
/>
|
||||
<div className='h-full w-full justify-between bg-mirage-600/95'>
|
||||
<div style={{ width: '100%', height: '100vh' }} ref={graphRef}>
|
||||
<ProjectGraph
|
||||
activeProject={activeGraph}
|
||||
<Graph
|
||||
onSelectionCtxMenu={onSelectionCtxMenu}
|
||||
onMultiSelectionCtxMenu={onMultiSelectionCtxMenu}
|
||||
onPaneCtxMenu={onPaneCtxMenu}
|
||||
@@ -373,11 +439,7 @@ export default function GraphInquiry({ }: GraphInquiryProps) {
|
||||
graphInstance={graphInstance}
|
||||
setGraphInstance={setGraphInstance}
|
||||
sendJsonMessage={sendJsonMessage}
|
||||
lastMessage={lastMessage}
|
||||
updateNode={createNodeUpdate}
|
||||
setIsEditingMini={setIsOpen}
|
||||
isEditingMini={isOpen}
|
||||
closeMiniRef={ref}
|
||||
fitView={fitView}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -50,20 +50,20 @@ export function SearchBar() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='flex items-start w-full pl-6 my-3'>
|
||||
<div className='h-12 flex items-center flex-col bg-dark-700 mr-2'>
|
||||
<div className='flex items-start w-full pl-6 my-3'>
|
||||
<div className='h-12 flex items-center flex-col bg-mirage-700 mr-2'>
|
||||
<fieldset className='isolate inline-flex shadow-sm '>
|
||||
<button
|
||||
onClick={() => setViewMode('list')}
|
||||
type='button'
|
||||
className={classNames(
|
||||
'relative py-3 inline-flex items-center rounded-l-md outline-none hover:ring-2 px-3 text-slate-400 ring-1 focus:bg-dark-800 ring-inset hover:bg-dark-600 focus:z-10 ring-dark-300',
|
||||
'relative py-3 inline-flex items-center rounded-l-md outline-none hover:ring-2 px-3 text-slate-400 ring-1 ring-primary-400 focus:bg-dark-800 ring-inset hover:bg-dark-600 focus:z-10 ring-dark-300',
|
||||
viewMode === 'list' && 'bg-dark-900'
|
||||
)}
|
||||
>
|
||||
<span className='sr-only'>List view</span>
|
||||
<ListBulletIcon
|
||||
className={classNames('h-6 w-6', viewMode === 'list' && 'text-info-100')}
|
||||
className={classNames('h-6 w-6', viewMode === 'list' && 'text-primary-100')}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
@@ -71,24 +71,24 @@ export function SearchBar() {
|
||||
onClick={() => setViewMode('map')}
|
||||
type='button'
|
||||
className={classNames(
|
||||
'relative py-3 inline-flex items-center outline-none hover:ring-2 px-3 text-slate-400 ring-1 focus:bg-dark-800 ring-inset hover:bg-dark-600 focus:z-10 ring-dark-300',
|
||||
'relative py-3 inline-flex items-center outline-none hover:ring-2 px-3 text-slate-400 ring-1 focus:bg-dark-800 ring-primary-400 ring-inset hover:bg-dark-600 focus:z-10 ring-dark-300',
|
||||
viewMode === 'map' && 'bg-dark-900'
|
||||
)}
|
||||
>
|
||||
<span className='sr-only'>List view</span>
|
||||
<MapIcon className={classNames('h-6 w-6', viewMode === 'map' && 'text-info-100')} aria-hidden='true' />
|
||||
<MapIcon className={classNames('h-6 w-6', viewMode === 'map' && 'text-primary-100')} aria-hidden='true' />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('grid')}
|
||||
type='button'
|
||||
className={classNames(
|
||||
'relative py-3 inline-flex items-center rounded-r-md outline-none hover:ring-2 px-3 text-slate-400 ring-1 focus:bg-dark-800 ring-inset hover:bg-dark-600 focus:z-10 ring-dark-300',
|
||||
'relative py-3 inline-flex items-center rounded-r-md outline-none hover:ring-2 px-3 text-slate-400 ring-1 ring-primary-400 focus:bg-dark-800 ring-inset hover:bg-dark-600 focus:z-10 ring-dark-300',
|
||||
viewMode === 'grid' && 'bg-dark-900'
|
||||
)}
|
||||
>
|
||||
<span className='sr-only'>List view</span>
|
||||
<Squares2X2Icon
|
||||
className={classNames('h-6 w-6', viewMode === 'grid' && 'text-info-100')}
|
||||
className={classNames('h-6 w-6', viewMode === 'grid' && 'text-primary-100')}
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
@@ -255,12 +255,12 @@ export function SearchBar() {
|
||||
export default function IncidentsPage() {
|
||||
return (
|
||||
<>
|
||||
<InquiryHeader title='Incidents' />
|
||||
<SearchBar />
|
||||
{/* <InquiryHeader title='Incidents' /> */}
|
||||
{/* <SearchBar /> */}
|
||||
<UnderConstruction
|
||||
header='OSINTBuddy Incidents'
|
||||
description='This feature is currently being planned out and created and will be finished by around August.
|
||||
You can help shape this feature by contributing to the discussions on Github!'
|
||||
description='This feature is currently being planned out and created and will be finished eventually.
|
||||
You can help shape this feature by creating/contributing to a discussion on Github!'
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useRef, useState } from "react";
|
||||
import { useEffectOnce } from "@/app/hooks";
|
||||
import { UnderConstruction } from "@/components/Loaders";
|
||||
|
||||
interface SettingCardProps {
|
||||
title: string;
|
||||
@@ -28,7 +29,7 @@ export function ScanMachineCard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<section className="relative bg-slate-900 mb-4 border border-dark-400 font-medium text-slate-400 p-4 rounded-md max-w-md min-w-[32rem] mx-2 text-sm flex-col flex justify-between">
|
||||
<section className="relative bg-slate-900 mb-4 border border-mirage-400 font-medium text-slate-400 p-4 rounded-md max-w-md min-w-[32rem] mx-2 text-sm flex-col flex justify-between">
|
||||
<div className="flex flex-col">
|
||||
<div className="flex w-full justify-between">
|
||||
<div className="flex flex-col">
|
||||
@@ -46,7 +47,7 @@ export function ScanMachineCard({
|
||||
</div>
|
||||
<ul className="flex w-full items-center justify-between mt-4">
|
||||
<li className="w-full mr-4">
|
||||
<button className="w-full ml-auto pr-3 text-left text-sm font-semibold text-info-100 hover:text-info-200 flex items-center justify-between border border-info-200 hover:border-info-300 py-2 px-3 rounded-md mr-1">
|
||||
<button className="w-full ml-auto pr-3 text-left text-sm font-semibold text-info-100 hover:text-info-200 flex items-center justify-between border border-mirage-400 hover:border-info-300 py-2 px-3 rounded-md mr-1">
|
||||
<span className="mr-4 whitespace-nowrap">Start scan</span>
|
||||
<DocumentMagnifyingGlassIcon className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -54,7 +55,7 @@ export function ScanMachineCard({
|
||||
<li className="w-full mr-4">
|
||||
<button
|
||||
onClick={() => navigate(`${id}`)}
|
||||
className="w-ful ml-auto pr-3 text-left text-sm font-semibold text-info-100 hover:text-info-200 flex items-center justify-between border border-info-200 hover:border-info-300 py-2 px-3 rounded-md mr-1"
|
||||
className="w-ful ml-auto pr-3 text-left text-sm font-semibold text-info-100 hover:text-info-200 flex items-center justify-between border border-mirage-400 hover:border-info-300 py-2 px-3 rounded-md mr-1"
|
||||
>
|
||||
<span className="mr-4 whitespace-nowrap">Edit workflow</span>
|
||||
<PencilIcon className="h-5 w-5" />
|
||||
@@ -86,41 +87,18 @@ export default function ScansPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<InquiryHeader title="Scans" header="Passive Reconnaissance" />
|
||||
{/* <InquiryHeader title="Scans" header="Passive Reconnaissance" />
|
||||
<div className="flex flex-col sm:px-2 lg:px-6 my-2 relative mx-auto w-full justify-center">
|
||||
<p className="text-slate-400">
|
||||
Automate your data gathering by scheduling scans
|
||||
</p>
|
||||
|
||||
<div className="mt-6 flex flex-col ">
|
||||
<div className="flex items-center">
|
||||
<h3 className="font-display leading-4 mt-6 text-xl tracking-tight text-slate-400 dark:text-slate-300">
|
||||
Browse your scan machines
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
className="ring-1 ml-auto bg-dark-800 pr-3 text-left text-sm font-semibold text-info-100 hover:text-info-200 flex items-center border border-info-200 hover:border-info-300 py-2 px-3 rounded-md mr-1"
|
||||
>
|
||||
Create template
|
||||
<PlusIcon className="ml-2 w-5 h-5 " />
|
||||
</button>
|
||||
</div>
|
||||
<section className="border-dark-300 w-full flex-wrap overflow-y-scroll my-3 border-t flex py-6">
|
||||
{scanMachines.map((machine: JSONObject) => {
|
||||
return (
|
||||
<ScanMachineCard
|
||||
title={machine.name}
|
||||
updatedAt={machine.updated}
|
||||
created={machine.created}
|
||||
id={machine.id}
|
||||
description={machine.description}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div> */}
|
||||
<UnderConstruction
|
||||
header='OSINTBuddy Scans'
|
||||
description='This feature is currently being planned out and created and will be finished eventually.
|
||||
You can help shape this feature by contributing to a discussion on Github!'
|
||||
/>
|
||||
{/* <CreateScanModal
|
||||
closeModal={closeModal}
|
||||
isOpen={isModalOpen}
|
||||
|
||||
@@ -43,47 +43,6 @@ const obColors = {
|
||||
900: "#0f172a",
|
||||
950: "#020617",
|
||||
},
|
||||
dark: {
|
||||
DEFAULT: "#0F172A",
|
||||
50: "#21335D",
|
||||
100: "#1F3057",
|
||||
200: "#1B2A4C",
|
||||
300: "#172341",
|
||||
400: "#131D35",
|
||||
500: "#0F172A",
|
||||
600: "#0E1526",
|
||||
700: "#0C1322",
|
||||
800: "#0B111F",
|
||||
900: "#0A0F1B",
|
||||
},
|
||||
black: {
|
||||
DEFAULT: "#0a0f1b",
|
||||
50: "#10182c",
|
||||
100: "#10172a",
|
||||
200: "#0e1526",
|
||||
300: "#0d1322",
|
||||
400: "#0b111f",
|
||||
500: "#0a0f1b",
|
||||
600: "#090d17",
|
||||
700: "#070b14",
|
||||
800: "#060910",
|
||||
900: "#04070c",
|
||||
950: "#04060a",
|
||||
},
|
||||
darker: {
|
||||
DEFAULT: "#080c16",
|
||||
50: "#141e38",
|
||||
100: "#131c34",
|
||||
200: "#10182c",
|
||||
300: "#0d1425",
|
||||
400: "#0b101d",
|
||||
500: "#080c16",
|
||||
600: "#070a12",
|
||||
700: "#05080f",
|
||||
800: "#04060b",
|
||||
900: "#030407",
|
||||
950: "#020305",
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: "#0215FF",
|
||||
50: "#5E6AFF",
|
||||
@@ -178,18 +137,7 @@ const obColors = {
|
||||
900: "#170A28",
|
||||
},
|
||||
};
|
||||
// DEFAULT: "#14192A",
|
||||
// 50: "#394778",
|
||||
// 100: "#35426F",
|
||||
// 200: "#2D385E",
|
||||
// 300: "#242E4D",
|
||||
// 400: "#1C233B",
|
||||
// 500: "#14192A",
|
||||
// 600: "#121727",
|
||||
// 700: "#111523",
|
||||
// 800: "#0F1320",
|
||||
// 900: "#0D111C",
|
||||
// 950: "#0D101A",
|
||||
|
||||
module.exports = {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
darkMode: "class",
|
||||
|
||||
@@ -5392,6 +5392,11 @@ electron-to-chromium@^1.4.535:
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.597.tgz#9848b9aa52749bf35be88013bab52be232889d94"
|
||||
integrity sha512-0XOQNqHhg2YgRVRUrS4M4vWjFCFIP2ETXcXe/0KIQBjXE9Cpy+tgzzYfuq6HGai3hWq0YywtG+5XK8fyG08EjA==
|
||||
|
||||
elkjs@^0.8.2:
|
||||
version "0.8.2"
|
||||
resolved "https://registry.yarnpkg.com/elkjs/-/elkjs-0.8.2.tgz#c37763c5a3e24e042e318455e0147c912a7c248e"
|
||||
integrity sha512-L6uRgvZTH+4OF5NE/MBbzQx/WYpru1xCBE9respNj6qznEewGUIfhzmm7horWWxbNO2M0WckQypGctR8lH79xQ==
|
||||
|
||||
emittery@^0.10.2:
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.10.2.tgz#902eec8aedb8c41938c46e9385e9db7e03182933"
|
||||
|
||||
Reference in New Issue
Block a user