Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
import { useAppDispatch, useQuerybuilderHash } from '@/lib/data-access/store';
import { setQuerybuilderGraphology } from '@/lib/data-access/store/sessionSlice';
import { SchemaGraphology } from '@/lib/schema';
import React, { useCallback, useRef, useState } from 'react';
import ReactFlow, {
Background,
Connection,
Edge,
HandleType,
Node,
NodeChange,
NodePositionChange,
OnConnectStartParams,
ReactFlowInstance,
isNode,
useReactFlow,
} from 'reactflow';
import { AllLogicMap, Query, QueryElementTypes, SaveStateWithAuthorization, isLogicHandle } from 'ts-common';
import { ConfigStateI, addError } from '../../../data-access/store/configSlice';
import { QueryMultiGraphology, toHandleData } from '../../model';
import { ConnectionDragLine, ConnectionLine, QueryEntityPill, QueryRelationPill } from '../../pills';
import { QueryLogicPill } from '../../pills/customFlowPills/logicpill/QueryLogicPill';
import { dragPillStarted, movePillTo } from '../../pills/dragging/dragPill';
import styles from '../querybuilder.module.scss';
import { ConnectingNodeDataI } from '../utils/connectorDrop';
export type QueryInputVisualProps = {
reactFlowRef: React.RefObject<HTMLDivElement | null>;
reactFlowWrapper: React.RefObject<HTMLDivElement | null>;
elements: { nodes: Node[]; edges: Edge[] };
graphologyGraph: QueryMultiGraphology;
schema: SchemaGraphology;
activeQuery: Query | undefined;
config: ConfigStateI;
ss: SaveStateWithAuthorization | undefined;
connectingNodeId: React.MutableRefObject<ConnectingNodeDataI | null>;
reactFlowInstanceRef: React.MutableRefObject<ReactFlowInstance | null>;
onSetToggleSettings: (settings?: string) => void;
onMouseDown: (event: React.MouseEvent | React.TouchEvent) => void;
onNodeContextMenu: (event: React.MouseEvent, node: Node) => void;
onAddSchemaToQueryBuilder?: () => void;
};
const nodeTypes = {
entity: QueryEntityPill,
relation: QueryRelationPill,
logic: QueryLogicPill,
};
const edgeTypes = {
connection: ConnectionLine,
attribute_connection: ConnectionLine,
};
export const QueryInputVisual: React.FC<QueryInputVisualProps> = ({
reactFlowRef,
reactFlowWrapper,
elements,
graphologyGraph,
schema,
activeQuery,
config,
ss,
connectingNodeId,
reactFlowInstanceRef,
onSetToggleSettings,
onMouseDown,
onNodeContextMenu,
onAddSchemaToQueryBuilder,
}) => {
const dispatch = useAppDispatch();
const reactFlow = useReactFlow();
const qbHash = useQuerybuilderHash();
const isEdgeUpdating = useRef(false);
const isOnConnect = useRef(false);
const isDraggingPill = useRef(false);
const [allowZoom, setAllowZoom] = useState(true);
const onInit = (reactFlowInstance: ReactFlowInstance) => {
setTimeout(() => reactFlow.fitView(), 0);
};
/**
* TODO?
*/
function onNodesChange(nodes: NodeChange[]) {
nodes.forEach(n => {
if (n.type !== 'position') {
// updated = true;
// graphologyGraph.updateAttributes(n.id, n.data);
} else {
const node = n as NodePositionChange;
// Get the node in the elements list to get the previous location
const pNode = elements.nodes.find(e => e?.id === node?.id);
if (!(pNode && isNode(pNode)) || !node?.position) return;
// This is then used to calculate the delta position
const dx = node.position.x - pNode.position.x;
const dy = node.position.y - pNode.position.y;
// Check if we started dragging, if so, call the drag started usecase
if (!isDraggingPill.current) {
dragPillStarted(node.id, graphologyGraph);
isDraggingPill.current = true;
}
// Call the drag usecase
movePillTo(node.id, graphologyGraph, dx, dy, node.position);
// Dispatch the new graphology object, so reactflow will get rerendered
dispatch(setQuerybuilderGraphology(graphologyGraph));
}
});
}
const onDragOver = (event: React.DragEvent<HTMLDivElement>): void => {
event.preventDefault();
event.dataTransfer.dropEffect = 'move';
};
/**
* The onDrop is called when the user drops an element from the schema onto the QueryBuilder.
* In the onDrop query elements will be created based on the data stored in the drag event (datastrasfer).
* @param event Drag event.
*/
const onDrop = (event: React.DragEvent<HTMLDivElement>): void => {
if (!activeQuery) {
console.error('No active query');
return;
}
event.preventDefault();
// The dropped element should be a valid reactflow element
const data: string = event.dataTransfer.getData('application/reactflow');
if (data.length == 0 || !reactFlow) return;
const mouse_x = parseFloat(event.dataTransfer.getData('mouse_x'));
const mouse_y = parseFloat(event.dataTransfer.getData('mouse_y'));
const dragData = JSON.parse(data);
const position = reactFlow.screenToFlowPosition({
x: event.clientX,
y: event.clientY,
});
switch (dragData.type) {
case QueryElementTypes.Entity:
graphologyGraph.addPill2Graphology(
{
type: QueryElementTypes.Entity,
x: position.x - mouse_x,
y: position.y - mouse_y,
name: dragData.name,
schemaKey: dragData.name,
attributes: [],
},
schema.getNodeAttribute(dragData.name, 'attributes'),
);
dispatch(setQuerybuilderGraphology(graphologyGraph));
break;
// Creates a relation element and will also create the 2 related entities together with the connections
case QueryElementTypes.Relation: {
const relation = graphologyGraph.addPill2Graphology(
{
type: QueryElementTypes.Relation,
x: position.x,
y: position.y,
depth: { min: activeQuery.settings.depth.min, max: activeQuery.settings.depth.max },
name: dragData.collection,
schemaKey: dragData.label,
collection: dragData.collection,
attributes: [],
},
schema.getEdgeAttribute(dragData.label, 'attributes'),
);
if (config.autoSendQueries) {
// sendQuery();
}
if (activeQuery.settings.autocompleteRelation == true) {
let fromNodeID: any = null;
let toNodeID: any = null;
schema.nodes().forEach((node: string) => {
if (node === dragData.from) fromNodeID = node;
if (node === dragData.to) toNodeID = node;
if (fromNodeID && toNodeID) return;
});
if (fromNodeID && toNodeID) {
const fromNode = graphologyGraph.addPill2Graphology(
{
type: QueryElementTypes.Entity,
x: position.x - 180,
y: position.y,
name: fromNodeID,
schemaKey: fromNodeID,
attributes: [],
},
schema.getNodeAttribute(fromNodeID, 'attributes'),
);
const toNode = graphologyGraph.addPill2Graphology(
{
type: QueryElementTypes.Entity,
x: position.x + 250,
y: position.y,
name: toNodeID,
schemaKey: toNodeID,
attributes: [],
},
schema.getNodeAttribute(toNodeID, 'attributes'),
);
graphologyGraph.addEdge2Graphology(fromNode, relation, {
type: 'connection',
sourceHandleData: toHandleData(fromNodeID),
targetHandleData: toHandleData(dragData.collection),
});
graphologyGraph.addEdge2Graphology(relation, toNode, {
type: 'connection',
sourceHandleData: toHandleData(dragData.collection),
targetHandleData: toHandleData(toNodeID),
});
}
}
dispatch(setQuerybuilderGraphology(graphologyGraph));
break;
}
default: {
const logic = dragData.value?.key ? AllLogicMap[dragData.value.key] : null;
if (!logic) return;
const firstLeftLogicInput = logic.input;
if (!firstLeftLogicInput) return;
const logicNode = graphologyGraph.addLogicPill2Graphology({
name: dragData.value.name,
type: QueryElementTypes.Logic,
x: position.x,
y: position.y,
logic: logic,
attributes: [],
inputs: {},
});
dispatch(setQuerybuilderGraphology(graphologyGraph));
}
}
};
const onNodeMouseEnter = (event: React.MouseEvent, node: Node) => {
setAllowZoom(false);
};
const onNodeMouseLeave = (event: React.MouseEvent, node: Node) => {
setAllowZoom(true);
};
const onConnect = useCallback(
(connection: Connection) => {
if (!isEdgeUpdating.current) {
isOnConnect.current = true;
if (!connection.sourceHandle || !connection.targetHandle) throw new Error('Connection has no source or target');
graphologyGraph.addEdge(connection.source, connection.target, {
type: 'connection',
sourceHandleData: toHandleData(connection.sourceHandle),
targetHandleData: toHandleData(connection.targetHandle),
});
dispatch(setQuerybuilderGraphology(graphologyGraph));
}
},
[graphologyGraph, qbHash],
);
const onConnectStart = useCallback(
(event: React.MouseEvent | React.TouchEvent, params: OnConnectStartParams) => {
if (!params?.handleId) return;
const node = graphologyGraph.getNodeAttributes(params.nodeId);
const handleData = toHandleData(params.handleId);
connectingNodeId.current = {
params,
node,
position: { x: 0, y: 0 },
attribute: { handleData: handleData },
};
},
[graphologyGraph, qbHash],
);
const onConnectEnd = useCallback(
(event: any) => {
const targetIsPane = event.target.classList.contains('react-flow__pane');
if (isOnConnect.current) {
isOnConnect.current = false;
return;
}
if (targetIsPane && !isEdgeUpdating.current) {
let clientX: number = 0;
let clientY: number = 0;
if ('touches' in event) clientX = event?.touches?.[0]?.clientX;
else if ('clientX' in event) clientX = event?.clientX;
if ('touches' in event) clientY = event?.touches?.[0]?.clientY;
else if ('clientY' in event) clientY = event?.clientY;
const position = reactFlow.screenToFlowPosition({ x: clientX, y: clientY });
if (connectingNodeId?.current) connectingNodeId.current.position = position;
if (!connectingNodeId?.current?.params?.handleId) {
dispatch(addError('Connection has no source or target handle id'));
return;
} else {
const data = toHandleData(connectingNodeId.current.params.handleId);
if (isLogicHandle(data.handleType)) onSetToggleSettings('logic');
else onSetToggleSettings('relatedNodes');
}
}
},
[reactFlow],
);
const onEdgeUpdateStart = useCallback(() => {
isEdgeUpdating.current = true;
}, []);
const onEdgeUpdate = useCallback(
(oldEdge: Edge, newConnection: Connection) => {
if (isEdgeUpdating.current) {
isEdgeUpdating.current = false;
if (graphologyGraph.hasEdge(oldEdge.id)) {
graphologyGraph.dropEdge(oldEdge.id);
}
if (!newConnection.sourceHandle || !newConnection.targetHandle) throw new Error('Connection has no source or target');
graphologyGraph.addEdge(newConnection.source, newConnection.target, {
type: 'connection',
sourceHandleData: toHandleData(newConnection.sourceHandle),
targetHandleData: toHandleData(newConnection.targetHandle),
});
dispatch(setQuerybuilderGraphology(graphologyGraph));
}
},
[graphologyGraph, qbHash],
);
const onEdgeUpdateEnd = useCallback(
(event: MouseEvent | TouchEvent, edge: Edge, handleType: HandleType) => {
if (isEdgeUpdating.current) {
if (graphologyGraph.hasEdge(edge.id)) {
graphologyGraph.dropEdge(edge.id);
}
dispatch(setQuerybuilderGraphology(graphologyGraph));
}
isEdgeUpdating.current = false;
},
[graphologyGraph, qbHash],
);
return (
<>
<svg height={0}>
<defs>
<marker id="arrowIn" markerWidth="9" markerHeight="10" refX={0} refY="5" orient="auto">
<polygon points="0 0, 9 5, 0 10" />
</marker>
<marker id="arrowOut" markerWidth="10" markerHeight="10" refX={0} refY="5" orient="auto">
<polygon points="0 0, 10 5, 0 10" />
</marker>
</defs>
</svg>
<ReactFlow
ref={reactFlowRef}
edges={elements.edges}
nodes={elements.nodes}
snapGrid={[10, 10]}
zoomOnScroll={allowZoom}
snapToGrid
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
connectionLineComponent={ConnectionDragLine}
onMouseDownCapture={onMouseDown}
// connectionMode={ConnectionMode.Loose}
onInit={reactFlowInstance => {
reactFlowInstanceRef.current = reactFlowInstance;
onInit(reactFlowInstance);
}}
onNodesChange={ss?.authorization.query.W ? onNodesChange : () => {}}
onDragOver={ss?.authorization.query.W ? onDragOver : () => {}}
onConnect={ss?.authorization.query.W ? onConnect : () => {}}
onConnectStart={ss?.authorization.query.W ? onConnectStart : () => {}}
onConnectEnd={ss?.authorization.query.W ? onConnectEnd : () => {}}
// onNodeMouseEnter={onNodeMouseEnter}
// onNodeMouseLeave={onNodeMouseLeave}
onEdgeUpdate={ss?.authorization.query.W ? onEdgeUpdate : () => {}}
onEdgeUpdateStart={ss?.authorization.query.W ? onEdgeUpdateStart : () => {}}
onEdgeUpdateEnd={ss?.authorization.query.W ? onEdgeUpdateEnd : () => {}}
onDrop={ss?.authorization.query.W ? onDrop : () => {}}
// onContextMenu={onContextMenu}
onNodeContextMenu={ss?.authorization.query.W ? onNodeContextMenu : () => {}}
// onNodesDelete={onNodesDelete}
deleteKeyCode="Backspace"
className={styles.reactflow}
proOptions={{ hideAttribution: true }}
>
<Background gap={10} size={0.7} />
</ReactFlow>
</>
);
};
export default QueryInputVisual;
import { ControlContainer, TooltipProvider } from '@/lib/components';
import { Tabs } from '@/lib/components/tabs';
import { wsUpdateQuery } from '@/lib/data-access/broker';
import { addError } from '@/lib/data-access/store/configSlice';
import objectHash from 'object-hash';
import { useEffect, useMemo, useRef } from 'react';
import Sortable from 'sortablejs';
import { useActiveQuery, useActiveSaveState, useAppDispatch, useGraphQuery, useML } from '../../../data-access';
import { clearQB, reorderQueryState, setQueryName } from '../../../data-access/store/sessionSlice';
import {
AddQueryButton,
ClearNodesButton,
FitViewButton,
LimitButton,
LogicButton,
MLButton,
RunQueryButton,
ScreenshotButton,
SettingsButton,
} from '../QueryBuilderControlButtons';
import QueryBuilderTab from './QueryBuilderTab';
export type QueryBuilderToggleSettings = 'settings' | 'ml' | 'logic' | 'relatedNodes' | undefined;
export type QueryBuilderNavProps = {
toggleSettings: QueryBuilderToggleSettings;
onFitView: () => void;
onRunQuery: (useCached: boolean) => void;
onScreenshot: () => void;
onLogic: () => void;
};
export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
const dispatch = useAppDispatch();
const activeQuery = useActiveQuery();
const ss = useActiveSaveState();
const graphQuery = useGraphQuery();
const resultSize = useMemo((): number => {
if (!graphQuery) return 0;
return graphQuery.graph.nodes.length;
}, [graphQuery]);
const totalSize = useMemo((): number => {
if (!activeQuery || !graphQuery || !ss) return 0;
const nodeCounts = activeQuery.graph.nodes
.filter(x => x.attributes.type == 'entity')
.map(x => graphQuery.graphCounts?.nodeCounts[`${x.key}_count`] ?? 0);
return nodeCounts.reduce((a, b) => a + b, 0);
}, [graphQuery]);
const ml = useML();
/**
* Clears all nodes in the graph.
*/
function clearAllNodes(): void {
dispatch(clearQB());
}
const tabsRef = useRef<HTMLDivElement | null>(null);
useEffect(() => {
if (!ss || !tabsRef.current) return;
const sortable = new Sortable(tabsRef.current, {
animation: 150,
draggable: '[data-type="tab"]',
ghostClass: 'bg-secondary-300',
dragClass: 'bg-secondary-100',
onEnd: evt => {
if (evt.oldIndex != null && evt.newIndex != null && evt.oldIndex !== evt.newIndex) {
dispatch(
reorderQueryState({
oldIndex: evt.oldIndex,
newIndex: evt.newIndex,
}),
);
}
},
});
const sortedQueries = ss.queryStates.openQueryArray
.filter(query => query.id != null)
.sort((a, b) => {
return a.order < b.order ? -1 : 1;
});
sortable.sort(sortedQueries.map(x => String(x.id)));
return () => {
sortable.destroy();
};
}, [ss ? objectHash(Object.fromEntries(ss?.queryStates.openQueryArray.map(x => [x.id, x.order]))) : null]);
const mlEnabled = ml.linkPrediction.enabled || ml.centrality.enabled || ml.communityDetection.enabled || ml.shortestPath.enabled;
// const handleManualQuery = (query: string): void => {
// wsManualQueryRequest({ query: query });
// };
function updateQueryName(queryId: number, text: string): void {
if (!ss || !activeQuery) return;
wsUpdateQuery(
{
saveStateID: ss.id,
query: { ...activeQuery, name: text },
},
({ status }) => {
if (status !== 'success') {
addError('Failed to update query');
}
dispatch(setQueryName(text));
},
);
}
if (!ss || !activeQuery) {
console.debug('No active query found in query nav');
return null;
}
// Get sorted queries
const sortedQueries = ss.queryStates.openQueryArray.filter(query => query.id != null).sort((a, b) => (a.order < b.order ? -1 : 1));
// Check if there's more than one query tab
const hasMultipleQueries = sortedQueries.length > 1;
const canWrite = !!ss?.authorization.database?.W;
const canQuery = !!ss?.authorization.query?.W;
return (
<div className="sticky shrink-0 top-0 left-0 right-0 flex items-stretch justify-start h-7 bg-secondary-100 border-b border-secondary-200 max-w-full">
<div className="flex items-center px-2">
<h1 className="text-xs font-semibold text-secondary-600 truncate">Query builder</h1>
</div>
<div className="flex items-center px-0.5 gap-1 border-l border-secondary-200">
<TooltipProvider>
<AddQueryButton disabled={!canWrite} saveStateId={ss.id} />
</TooltipProvider>
</div>
<Tabs ref={tabsRef} className={`-my-px overflow-x-auto overflow-y-hidden no-scrollbar divide-x divide-secondary-200 border-x`}>
{sortedQueries.map((query, i) => (
<QueryBuilderTab
key={i}
query={query}
index={i}
activeQueryId={activeQuery?.id || -1}
isActive={query.id === activeQuery?.id}
canWrite={canWrite}
saveStateId={ss.id}
isLastTab={!hasMultipleQueries}
onUpdateQueryName={updateQueryName}
/>
))}
</Tabs>
<div className="sticky right-0 px-0.5 ml-auto items-center flex truncate">
<ControlContainer>
<TooltipProvider>
<FitViewButton onFitView={props.onFitView} />
<ClearNodesButton disabled={!canQuery} />
<ScreenshotButton onScreenshot={props.onScreenshot} />
<SettingsButton disabled={!canQuery} />
<RunQueryButton onRunQuery={props.onRunQuery} />
<LogicButton disabled={!canQuery} onLogic={props.onLogic} isActive={props.toggleSettings === 'logic'} />
<MLButton disabled={!canQuery} mlEnabled={mlEnabled} isActive={props.toggleSettings === 'ml'} mlData={ml} />
<LimitButton disabled={!canQuery} activeQuery={activeQuery} resultSize={resultSize} totalSize={totalSize} />
{/* <ManualQueryButton disabled={!canQuery} /> */}
</TooltipProvider>
</ControlContainer>
</div>
</div>
);
};
import { Button, Icon, Input, Tab, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/lib/components';
import { Menu, MenuContent, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuTrigger } from '@/lib/components/menu';
import { useAppDispatch } from '@/lib/data-access';
import { wsDeleteQuery } from '@/lib/data-access/broker';
import { removeQueryByID, setActiveQueryID, setQuerybuilderSettings, setQueryViewMode } from '@/lib/data-access/store/sessionSlice';
import { useState } from 'react';
import { Query, QueryViewMode } from 'ts-common/src/model';
// QueryBuilderTab component to encapsulate tab-related functionality
interface QueryBuilderTabProps {
query: Query;
index: number;
activeQueryId: number | null;
isActive: boolean;
canWrite: boolean;
saveStateId: string;
isLastTab: boolean;
onUpdateQueryName: (queryId: number, name: string) => void;
}
const QueryBuilderTab = ({ query, isActive, canWrite, saveStateId, isLastTab, onUpdateQueryName }: QueryBuilderTabProps) => {
const dispatch = useAppDispatch();
const [isEditing, setIsEditing] = useState<boolean>(false);
const [editText, setEditText] = useState<string>(query.name ?? '');
const [viewMode, setViewMode] = useState<QueryViewMode>(query.settings.mode ?? QueryViewMode.Visual);
// Toggle between the three modes for this query
const toggleViewMode = () => {
let newViewMode: QueryViewMode = QueryViewMode.Visual;
if (viewMode === QueryViewMode.Visual) {
newViewMode = QueryViewMode.Cypher;
} else if (viewMode === QueryViewMode.Cypher) {
newViewMode = QueryViewMode.Visual;
}
setViewMode(newViewMode);
dispatch(setQueryViewMode(newViewMode));
};
// Get icon component based on the view mode
const getViewModeIcon = (viewMode: QueryViewMode): string => {
switch (viewMode) {
case QueryViewMode.Visual:
return 'icon-[ic--baseline-remove-red-eye]';
case QueryViewMode.Cypher:
return 'icon-[ic--baseline-article]';
// case 'chatBot':
// return 'icon-[ic--baseline-smart-toy]';
default:
return 'icon-[ic--baseline-remove-red-eye]';
}
};
// Get tooltip text based on the view mode
const getViewModeTooltip = (): string => {
switch (viewMode) {
case QueryViewMode.Visual:
return 'Visual Query Mode';
case QueryViewMode.Cypher:
return 'Text Query Mode';
// case 'chatBot':
// return 'AI Chat Mode';
default:
return 'Visual Query Mode';
}
};
const handleUpdateName = () => {
if (query.id) {
onUpdateQueryName(query.id, editText);
setIsEditing(false);
}
};
return (
<Menu atCursor>
<MenuTrigger className="-mb-px" rightClick>
<Tab
text=""
activeTab={isActive}
data-id={query.id}
onClick={() => {
if (query.id == null) throw new Error('Query ID is null; Cannot change tabs in query tab navbar');
dispatch(setActiveQueryID(query.id));
}}
IconComponent={() => <Icon size={14} component={getViewModeIcon(viewMode)} />}
>
{isEditing ? (
<Input
type="text"
size="xs"
value={editText}
label=""
onChange={(e: any) => {
setEditText(typeof e === 'string' ? e : e.target.value);
}}
onBlur={handleUpdateName}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleUpdateName();
}
}}
className="w-20"
style={{
border: 'none',
boxShadow: 'none',
background: 'none',
}}
autoFocus
/>
) : (
<>
<div
onDoubleClick={(e: React.MouseEvent) => {
e.stopPropagation();
if (query.id == null) throw new Error('Query ID is null; Cannot change tabs in query tab navbar');
dispatch(setActiveQueryID(query.id));
setIsEditing(true);
}}
>
{query.name ?? 'Query'}
</div>
</>
)}
{!isLastTab && (
<>
<Button
variantType="secondary"
variant="ghost"
disabled={!canWrite}
rounded
size="3xs"
iconComponent="icon-[ic--baseline-close]"
onClick={(e: React.MouseEvent) => {
e.stopPropagation();
if (query.id !== undefined) {
wsDeleteQuery({ saveStateID: saveStateId, queryID: query.id });
dispatch(removeQueryByID(query.id));
}
}}
/>
</>
)}
</Tab>
</MenuTrigger>
<MenuContent>
<Menu>
<MenuTrigger label="Change mode" />
<MenuContent>
<MenuRadioGroup
value={query.settings.mode}
onValueChange={value => {
dispatch(setQuerybuilderSettings({ ...query.settings, mode: value as QueryViewMode }));
}}
>
<MenuRadioItem value={QueryViewMode.Visual} label="Visual" closeOnClick={true} />
<MenuRadioItem value={QueryViewMode.Cypher} label="Cypher" closeOnClick={true} />
</MenuRadioGroup>
</MenuContent>
</Menu>
<MenuItem
label="Rename"
closeOnClick={true}
onClick={e => {
e.stopPropagation();
setTimeout(() => {
setIsEditing(true);
}, 1);
}}
/>
<MenuSeparator />
<MenuItem
label="Remove"
className="text-danger"
onClick={() => {
if (query.id !== undefined) {
wsDeleteQuery({ saveStateID: ss.id, queryID: query.id });
dispatch(removeQueryByID(query.id));
}
}}
/>
</MenuContent>
</Menu>
);
};
export default QueryBuilderTab;