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:
jerlendds
2023-12-01 06:13:50 -07:00
parent bb702ab591
commit a50a032b52
28 changed files with 380 additions and 424 deletions

View File

@@ -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",

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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 */

View File

@@ -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

View File

@@ -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 ' />

View File

@@ -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}&nbsp;
<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'
>

View File

@@ -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'>

View File

@@ -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>) => {

View File

@@ -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",
}}
/>
</>

View File

@@ -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>
</>

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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;

View File

@@ -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={{

View File

@@ -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>
</>
);
}

View File

@@ -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>

View File

@@ -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&nbsp;
</Link>{" /"}&nbsp;
<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>
&nbsp;/
</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&nbsp;
@@ -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>
);

View 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>
);
};

View File

@@ -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' />

View File

@@ -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;

View 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,
};
}

View File

@@ -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>

View File

@@ -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!'
/>
</>
);

View File

@@ -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}

View File

@@ -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",

View File

@@ -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"