From 79aab4d3acd8173916b350d639350846bd543e63 Mon Sep 17 00:00:00 2001 From: Marcos Pieras <pieras.marcos@gmail.com> Date: Thu, 11 Jul 2024 17:06:59 +0000 Subject: [PATCH] feat: redesign tooltips for schema and nodelink --- .../lib/assets/carbonIcons/carbonIcons.tsx | 56 +++++++ .../CardToolTipVis/VisualizationTooltip.tsx | 140 ++++++++++++++++++ .../CardToolTipVis/cardtooltipvis.module.scss | 6 + .../CardToolTipVis/cardtooltipvis.stories.tsx | 85 +++++++++++ .../lib/components/CardToolTipVis/index.tsx | 1 + .../lib/components/icon/icon.stories.tsx | 13 +- libs/shared/lib/components/icon/index.tsx | 34 ++++- libs/shared/lib/inspector/InspectorPanel.tsx | 2 +- libs/shared/lib/schema/model/reactflow.tsx | 5 + libs/shared/lib/schema/panel/Schema.tsx | 38 ++++- .../pills/nodes/entity/SchemaEntityPill.tsx | 41 +++-- .../pills/nodes/entity/SchemaEntityPopup.tsx | 84 ----------- .../nodes/relation/SchemaRelationPill.tsx | 43 +++++- .../nodelinkvis/components/NLPixi.tsx | 44 +++--- .../nodelinkvis/components/utils.tsx | 4 +- 15 files changed, 458 insertions(+), 138 deletions(-) create mode 100644 libs/shared/lib/assets/carbonIcons/carbonIcons.tsx create mode 100644 libs/shared/lib/components/CardToolTipVis/VisualizationTooltip.tsx create mode 100644 libs/shared/lib/components/CardToolTipVis/cardtooltipvis.module.scss create mode 100644 libs/shared/lib/components/CardToolTipVis/cardtooltipvis.stories.tsx create mode 100644 libs/shared/lib/components/CardToolTipVis/index.tsx delete mode 100644 libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx diff --git a/libs/shared/lib/assets/carbonIcons/carbonIcons.tsx b/libs/shared/lib/assets/carbonIcons/carbonIcons.tsx new file mode 100644 index 000000000..de379ae1d --- /dev/null +++ b/libs/shared/lib/assets/carbonIcons/carbonIcons.tsx @@ -0,0 +1,56 @@ +import React from 'react'; +import type { SVGProps } from 'react'; + +export function CarbonStringInteger(props: SVGProps<SVGSVGElement>) { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 32 32" {...props}> + <path + fill="currentColor" + d="M26 12h-4v2h4v2h-3v2h3v2h-4v2h4a2.003 2.003 0 0 0 2-2v-6a2 2 0 0 0-2-2m-7 10h-6v-4a2 2 0 0 1 2-2h2v-2h-4v-2h4a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2h-2v2h4zM8 20v-8H6v1H4v2h2v5H4v2h6v-2z" + ></path> + </svg> + ); +} + +export function CarbonStringText(props: SVGProps<SVGSVGElement>) { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 32 32" {...props}> + <path + fill="currentColor" + d="M29 22h-5a2.003 2.003 0 0 1-2-2v-6a2 2 0 0 1 2-2h5v2h-5v6h5zM18 12h-4V8h-2v14h6a2.003 2.003 0 0 0 2-2v-6a2 2 0 0 0-2-2m-4 8v-6h4v6zm-6-8H3v2h5v2H4a2 2 0 0 0-2 2v2a2 2 0 0 0 2 2h6v-8a2 2 0 0 0-2-2m0 8H4v-2h4z" + ></path> + </svg> + ); +} + +export function CarbonCalendar(props: SVGProps<SVGSVGElement>) { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 32 32" {...props}> + <path + fill="currentColor" + d="M26 4h-4V2h-2v2h-8V2h-2v2H6c-1.1 0-2 .9-2 2v20c0 1.1.9 2 2 2h20c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2m0 22H6V12h20zm0-16H6V6h4v2h2V6h8v2h2V6h4z" + ></path> + </svg> + ); +} + +export function CarbonBoolean(props: SVGProps<SVGSVGElement>) { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 32 32" {...props}> + <path fill="currentColor" d="M23 23a7 7 0 1 1 7-7a7.01 7.01 0 0 1-7 7m0-12a5 5 0 1 0 5 5a5.006 5.006 0 0 0-5-5"></path> + <circle cx={9} cy={16} r={7} fill="currentColor"></circle> + </svg> + ); +} + +export function CarbonUndefined(props: SVGProps<SVGSVGElement>) { + return ( + <svg xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem" viewBox="0 0 32 32" {...props}> + <path fill="currentColor" d="M11 14h10v4H11z"></path> + <path + fill="currentColor" + d="M29.391 14.527L17.473 2.609A2.08 2.08 0 0 0 16 2c-.533 0-1.067.203-1.473.609L2.609 14.527C2.203 14.933 2 15.466 2 16s.203 1.067.609 1.473L14.526 29.39c.407.407.941.61 1.474.61s1.067-.203 1.473-.609L29.39 17.474c.407-.407.61-.94.61-1.474s-.203-1.067-.609-1.473M16 28.036L3.965 16L16 3.964L28.036 16z" + ></path> + </svg> + ); +} diff --git a/libs/shared/lib/components/CardToolTipVis/VisualizationTooltip.tsx b/libs/shared/lib/components/CardToolTipVis/VisualizationTooltip.tsx new file mode 100644 index 000000000..53bebb390 --- /dev/null +++ b/libs/shared/lib/components/CardToolTipVis/VisualizationTooltip.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; +import { Numbers, Close } from '@mui/icons-material'; +import styles from './cardtooltipvis.module.scss'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@graphpolaris/shared/lib/components/tooltip'; +import { + CarbonStringInteger, + CarbonStringText, + CarbonCalendar, + CarbonBoolean, + CarbonUndefined, +} from '@graphpolaris/shared/lib/assets/carbonIcons/carbonIcons'; + +export type CardToolTipVisProps = { + type: string; + name: string; + data: Record<string, any>; + typeOfSchema?: string; + colorHeader: string; + connectedTo?: string; + connectedFrom?: string; + maxVisibleItems?: number; + numberOfElements?: number; +}; + +const formatNumber = (number: number) => { + return number.toLocaleString('de-DE'); // Format number with dots as thousand separators +}; + +export const VisualizationTooltip: React.FC<CardToolTipVisProps> = ({ + type, + name, + data, + colorHeader, + maxVisibleItems = 5, + connectedFrom, + connectedTo, + typeOfSchema, + numberOfElements, +}) => { + const itemsToShow = Object.entries(data).slice(0, maxVisibleItems); + + return ( + <div className="border-1 border-sec-200 bg-white w-[12rem] -mx-2 -my-1"> + <div className="flex m-0 justify-start items-stretch border-b border-sec-200 relative"> + <div className="left-0 top-0 h-auto w-1.5" style={{ backgroundColor: colorHeader }}></div> + <div className="px-2.5 py-1 truncate flex"> + <Tooltip> + <TooltipTrigger className={'flex max-w-full'}> + <span className="text-base font-semibold truncate">{name}</span> + </TooltipTrigger> + <TooltipContent side={'top'}> + <span>{name}</span> + </TooltipContent> + </Tooltip> + </div> + {/* + <div className="flex-shrink-0 ml-2"> + <Button variantType="secondary" variant="ghost" size="xs" rounded={true} iconComponent={<Close />} onClick={() => {}} /> + </div> + */} + </div> + + {type === 'schema' && numberOfElements && ( + <div className="px-4 py-1 border-b border-sec-200"> + <div className="flex flex-row gap-1 items-center justify-between"> + <Icon component={<Numbers />} size={24} /> <span className="ml-auto text-right">{formatNumber(numberOfElements)}</span> + </div> + </div> + )} + + {type === 'schema' && typeOfSchema === 'relationship' && ( + <div className="px-4 py-1 border-b border-sec-200"> + <div className="flex flex-row gap-3 items-center justify-between"> + <span className="font-semibold">From</span> + <span className="ml-auto text-right">{connectedFrom}</span> + </div> + + <div className="flex flex-row gap-1 items-center justify-between"> + <span className="font-semibold">To</span> + <span className="ml-auto text-right">{connectedTo}</span> + </div> + </div> + )} + + <TooltipProvider delayDuration={300}> + <div className={`px-3 py-1.5 ${data.length > maxVisibleItems ? 'max-h-20 overflow-y-auto' : ''}`}> + {data && Object.keys(data).length === 0 ? ( + <div className="flex justify-center items-center h-full"> + <span>No attributes</span> + </div> + ) : ( + Object.entries(data).map(([k, v]) => ( + <Tooltip key={k}> + <div className="flex flex-row gap-1 items-center min-h-6"> + <span className={`font-semibold truncate ${type === 'schema' ? 'w-[90%]' : 'min-w-[40%]'}`}>{k}</span> + <TooltipTrigger asChild> + <span className="ml-auto text-right truncate grow-1 flex items-center"> + {type === 'schema' ? ( + <Icon + className="ml-auto text-right flex-shrink-0" + component={ + v === 'int' || v === 'float' ? ( + <CarbonStringInteger /> + ) : v === 'string' ? ( + <CarbonStringText /> + ) : v === 'boolean' ? ( + <CarbonBoolean /> + ) : v === 'date' ? ( + <CarbonCalendar /> + ) : v === 'undefined' ? ( + <CarbonUndefined /> + ) : ( + <CarbonUndefined /> + ) + } + color="hsl(var(--clr-sec--400))" + size={24} + /> + ) : v !== undefined && (typeof v !== 'object' || Array.isArray(v)) && v != '' ? ( + <span className="ml-auto text-right truncate">{typeof v === 'number' ? formatNumber(v) : v.toString()}</span> + ) : ( + <div className={`ml-auto mt-auto h-4 w-12 ${styles['diagonal-lines']}`}></div> + )} + </span> + </TooltipTrigger> + <TooltipContent side="right"> + <div className="max-w-[18rem] break-all line-clamp-6"> + {v !== undefined && (typeof v !== 'object' || Array.isArray(v)) && v != '' ? v : 'noData'} + </div> + </TooltipContent> + </div> + </Tooltip> + )) + )} + </div> + </TooltipProvider> + </div> + ); +}; diff --git a/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.module.scss b/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.module.scss new file mode 100644 index 000000000..87c1972b5 --- /dev/null +++ b/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.module.scss @@ -0,0 +1,6 @@ +.diagonal-lines { + border: 1px solid lightgray; + background: + repeating-linear-gradient(-45deg, transparent, transparent 6px, #eaeaea 6px, #eaeaea 8px), + /* Gray diagonal lines */ linear-gradient(to bottom, transparent, transparent); /* Vertical gradient */ +} diff --git a/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.stories.tsx b/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.stories.tsx new file mode 100644 index 000000000..ed1149af7 --- /dev/null +++ b/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.stories.tsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { VisualizationTooltip, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis'; + +const metaCardToolTipVis: Meta<typeof VisualizationTooltip> = { + component: VisualizationTooltip, + title: 'Components/CardToolTipVis', +}; + +export default metaCardToolTipVis; +type Story = StoryObj<typeof VisualizationTooltip>; + +export const SchemaNode: Story = { + render: (args) => { + return ( + <div className="w-1/4 my-10 m-auto flex items-center justify-center"> + <VisualizationTooltip {...args} /> + </div> + ); + }, + args: { + type: 'schema', + typeOfSchema: 'node', + name: 'Person', + data: { + born: 'int', + name: 'string', + description: 'string', + }, + colorHeader: '#fb7b04', + numberOfElements: 1000, + }, +}; + +export const SchemaRelationship: Story = { + render: (args) => { + return ( + <div className="w-1/4 my-10 m-auto flex items-center justify-center"> + <VisualizationTooltip {...args} /> + </div> + ); + }, + args: { + type: 'schema', + typeOfSchema: 'relationship', + name: 'Directed', + data: { + born: 'int', + name: 'string', + description: 'string', + imdb: 'string', + imdbVotes: 'int', + }, + colorHeader: '#0676C1', + connectedTo: 'Person', + numberOfElements: 231230, + connectedFrom: 'Movie', + }, +}; + +export const PopUpVis: Story = { + render: (args) => { + return ( + <div className="w-1/4 my-10 m-auto flex items-center justify-center"> + <VisualizationTooltip {...args} /> + </div> + ); + }, + args: { + name: 'Person', + type: 'popupvis', + data: { + bio: 'From wikipedia was born in usa from a firefighter father', + name: 'Charlotte Henry', + born: {}, + imdbRank: 21213, + imdbVotes: 1213, + poster: 'https://image.tmdb.org/t/p/w440_and_h660_face/kTKiREs37qd8GUlNI4Koiupwy6W.jpg', + tmdbId: '94105', + country: undefined, + labels: ['Actor', 'Person', 'Human'], + }, + colorHeader: '#B69AEf', + }, +}; diff --git a/libs/shared/lib/components/CardToolTipVis/index.tsx b/libs/shared/lib/components/CardToolTipVis/index.tsx new file mode 100644 index 000000000..868ce59de --- /dev/null +++ b/libs/shared/lib/components/CardToolTipVis/index.tsx @@ -0,0 +1 @@ +export * from './VisualizationTooltip'; diff --git a/libs/shared/lib/components/icon/icon.stories.tsx b/libs/shared/lib/components/icon/icon.stories.tsx index cad42fd6e..9277a7c9f 100644 --- a/libs/shared/lib/components/icon/icon.stories.tsx +++ b/libs/shared/lib/components/icon/icon.stories.tsx @@ -1,7 +1,7 @@ import { StoryObj, Meta } from '@storybook/react'; import { Icon } from '../icon'; -import { ArrowBack, DeleteOutline, KeyboardArrowLeft, Settings } from '@mui/icons-material'; - +import { ArrowBack } from '@mui/icons-material'; +import { CarbonStringInteger } from '@graphpolaris/shared/lib/assets/carbonIcons/carbonIcons'; const Component: Meta<typeof Icon> = { title: 'Components/Icon', component: Icon, @@ -21,8 +21,13 @@ const Component: Meta<typeof Icon> = { export default Component; type Story = StoryObj<typeof Component>; -export const BaseIcon: Story = (args: any) => { +export const MUIIcon: Story = (args: any) => { return <Icon component={<ArrowBack />} size={24} {...args} />; }; -BaseIcon.args = {}; +export const CarbonIcon: Story = (args: any) => { + return <Icon component={<CarbonStringInteger />} size={24} {...args} />; +}; + +MUIIcon.args = {}; +CarbonIcon.args = {}; diff --git a/libs/shared/lib/components/icon/index.tsx b/libs/shared/lib/components/icon/index.tsx index 4f2695cb5..aa206f3a0 100644 --- a/libs/shared/lib/components/icon/index.tsx +++ b/libs/shared/lib/components/icon/index.tsx @@ -1,18 +1,44 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, ReactNode } from 'react'; import { SVGProps } from 'react'; +// Define Sizes and IconProps types export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 40; export type IconProps = SVGProps<SVGSVGElement> & { - component: ReactElement<any>; + component: ReactNode | ReactElement<any>; size?: Sizes; color?: string; }; +// Icon component definition export const Icon: React.FC<IconProps> = ({ component, size = 24, color, ...props }) => { if (!component) { - console.error(`No icon found`); + console.error('No icon found'); return <div></div>; } - return React.cloneElement(component, { style: { fontSize: size }, width: size, height: size, ...props }); + const style = { fontSize: size, color }; + + // Check if component is a valid React element + if (React.isValidElement(component)) { + return React.cloneElement(component as ReactElement<any>, { + style: { ...style, ...(component as ReactElement<any>).props.style }, + width: size, + height: size, + ...props, + }); + } + + // Check if component is a function (assume it's a custom SVG component) + if (typeof component === 'function') { + // Render the custom SVG component directly + return ( + <svg xmlns="http://www.w3.org/2000/svg" width={size} height={size} viewBox="0 0 32 32" style={style} {...props}> + {(component as () => ReactNode)()} + </svg> + ); + } + + // Default case: render null or fallback + console.error('Unsupported icon type'); + return null; }; diff --git a/libs/shared/lib/inspector/InspectorPanel.tsx b/libs/shared/lib/inspector/InspectorPanel.tsx index 84257549e..5aebf62b2 100644 --- a/libs/shared/lib/inspector/InspectorPanel.tsx +++ b/libs/shared/lib/inspector/InspectorPanel.tsx @@ -18,7 +18,7 @@ export function InspectorPanel(props: { children?: React.ReactNode }) { const { activeVisualizationIndex } = useVisualization(); const inspector = useMemo(() => { - if (selection) return <SelectionConfig />; + //if (selection) return <SelectionConfig />; // if (!focus) return <ConnectionInspector />; // if (activeVisualizationIndex !== -1) return <ConnectionInspector />; return <VisualizationSettings />; diff --git a/libs/shared/lib/schema/model/reactflow.tsx b/libs/shared/lib/schema/model/reactflow.tsx index 39a121106..d03ac9135 100644 --- a/libs/shared/lib/schema/model/reactflow.tsx +++ b/libs/shared/lib/schema/model/reactflow.tsx @@ -34,6 +34,9 @@ export type SchemaReactflowEntity = SchemaReactflowData & { // handles: string[]; connectedRatio: number; name: string; + x: number; + y: number; + reactFlowRef: any; }; export type SchemaReactflowRelation = SchemaReactflowData & { @@ -42,6 +45,8 @@ export type SchemaReactflowRelation = SchemaReactflowData & { collection: string; fromRatio: number; toRatio: number; + x: number; + y: number; }; export type SchemaReactflowNodeWithFunctions = SchemaReactflowEntity & { diff --git a/libs/shared/lib/schema/panel/Schema.tsx b/libs/shared/lib/schema/panel/Schema.tsx index ff7da823e..ca35b37ad 100644 --- a/libs/shared/lib/schema/panel/Schema.tsx +++ b/libs/shared/lib/schema/panel/Schema.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge'; -import ReactFlow, { Edge, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow'; +import ReactFlow, { Edge, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState, MiniMap } from 'reactflow'; import 'reactflow/dist/style.css'; import { Button } from '../../components/buttons'; import { useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../data-access'; @@ -10,8 +10,7 @@ import { SelfEdge } from '../pills/edges/self-edge'; import { SchemaEntityPill } from '../pills/nodes/entity/SchemaEntityPill'; import { SchemaRelationPill } from '../pills/nodes/relation/SchemaRelationPill'; import { SchemaSettings } from './SchemaSettings'; -import { Settings } from '@mui/icons-material'; -import { ContentCopy, FitScreen, Fullscreen, KeyboardArrowDown, KeyboardArrowRight, Remove } from '@mui/icons-material'; +import { Settings, ContentCopy, Fullscreen, Remove } from '@mui/icons-material'; import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout'; import { ConnectionLine, ConnectionDragLine } from '../../querybuilder'; import { schemaExpandRelation, schemaGraphology2Reactflow } from '../schema-utils'; @@ -97,7 +96,13 @@ export const Schema = (props: Props) => { const xy = bounds ? { x1: 50, x2: bounds.width - 50, y1: 50, y2: bounds.height - 200 } : { x1: 0, x2: 500, y1: 0, y2: 1000 }; await layout.current?.layout(expandedSchema, xy); const schemaFlow = schemaGraphology2Reactflow(expandedSchema, settings.connectionType, settings.animatedEdges); - setNodes(schemaFlow.nodes); + + const nodesWithRef = schemaFlow.nodes.map((node) => ({ + ...node, + data: { ...node.data, reactFlowRef }, + })); + + setNodes(nodesWithRef); setEdges(schemaFlow.edges); setTimeout(() => fitView(), 100); } @@ -115,6 +120,17 @@ export const Schema = (props: Props) => { ); }, [searchResults]); + const nodeColor = (node: any) => { + switch (node.type) { + case 'entity': + return '#fb7b04'; + case 'relation': + return '#0676C1'; + default: + return '#ff0072'; + } + }; + return ( <Panel title="Schema" @@ -155,7 +171,15 @@ export const Schema = (props: Props) => { </Tooltip> <Tooltip> <TooltipTrigger> - <Button variantType="secondary" variant="ghost" size="xs" iconComponent={<FitScreen />} onClick={() => {}} /> + <Button + variantType="secondary" + variant="ghost" + size="xs" + iconComponent={<Fullscreen />} + onClick={() => { + fitView(); + }} + /> </TooltipTrigger> <TooltipContent> <p>Fit to screen</p> @@ -202,7 +226,9 @@ export const Schema = (props: Props) => { onInit(reactFlowInstance); }} proOptions={{ hideAttribution: true }} - ></ReactFlow> + > + <MiniMap nodeColor={nodeColor} /> + </ReactFlow> </ReactFlowProvider> )} {/* <div> diff --git a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx index d3bfe7ef2..3bf2158a5 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx @@ -1,15 +1,15 @@ -import React, { useState } from 'react'; +import React, { useState, useRef, useEffect } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow'; import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; -import { SchemaEntityPopup } from './SchemaEntityPopup'; -import { Popup } from '@graphpolaris/shared/lib/components/Popup'; import { SchemaNode } from '../../../model'; import { EntityPill } from '@graphpolaris/shared/lib/components'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; +import { VisualizationTooltip, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis'; export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); - + const ref = useRef<HTMLDivElement>(null); /** * adds drag functionality in order to be able to drag the entityNode to the schema * @param event React Mouse drag event @@ -40,12 +40,6 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc return ( <> - {openPopup && ( - <Popup open={openPopup} hAnchor="left" className="-top-8" offset="-20rem"> - <SchemaEntityPopup data={data} onClose={() => setOpenPopup(false)} /> - </Popup> - )} - <div className="w-fit h-fit" onDragStart={(event) => onDragStart(event)} @@ -57,7 +51,34 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc setOpenPopup(!openPopup); }} draggable + ref={ref} > + {openPopup && ( + <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> + <TooltipTrigger /> + <TooltipContent side="right"> + <div> + <VisualizationTooltip + type="schema" + typeOfSchema="node" + name={data.name} + colorHeader="#fb7b04" + numberOfElements={1000} + data={data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + )} + /> + </div> + </TooltipContent> + </Tooltip> + )} + <EntityPill draggable title={id} diff --git a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx deleted file mode 100644 index c0f5c9247..000000000 --- a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPopup.tsx +++ /dev/null @@ -1,84 +0,0 @@ -/** - * This program has been developed by students from the bachelor Computer Science at - * Utrecht University within the Software Project course. - * © Copyright Utrecht University (Department of Information and Computing Sciences) - */ - -/* istanbul ignore file */ -/* The comment above was added so the code coverage wouldn't count this file towards code coverage. - * We do not test components/renderfunctions/styling files. - * See testing plan for more details.*/ - -import { FormBody, FormCard, FormControl, FormHBar, FormTitle } from '@graphpolaris/shared/lib/components/forms'; -import { SchemaReactflowEntity } from '@graphpolaris/shared/lib/schema/model'; -import { FormEvent } from 'react'; - -export type SchemaEntityPopupProps = { - data: SchemaReactflowEntity; - onClose: () => void; -}; - -/** - * NodeQualityEntityPopupNode is the node that represents the popup that shows the node quality for an entity - * @param data Input data of type NodeQualityDataForEntities, which is for the popup. - */ -export const SchemaEntityPopup = (props: SchemaEntityPopupProps) => { - function submit() { - // dispatch(setSchemaSettings(state)); - props.onClose(); - } - - return ( - // <FormDiv hAnchor="left"> - <> - <FormCard> - <FormBody - onSubmit={(e: FormEvent<HTMLFormElement>) => { - e.preventDefault(); - submit(); - }} - > - <FormTitle - title="Node Statistics" - // title={props.data.name} - onClose={props.onClose} - /> - <FormHBar /> - - <span className="px-5 pt-2"> - <span>Name</span> - <span className="float-right break-all text-wrap text-pretty font-light font-mono">{props.data.name}</span> - </span> - - <FormHBar /> - - <span className="px-5 pt-2"> - <span>Attributes</span> - <span className="float-right font-light font-mono">{props.data.attributes.length}</span> - </span> - - {props.data.attributes.map((attribute: any) => { - return ( - <div key={attribute.name} className="px-5 pt-1"> - <span>{attribute.name}</span> - <span className="float-right font-light font-mono">{attribute.type}</span> - </div> - ); - })} - <FormHBar /> - - <FormControl> - <button - className="btn btn-outline btn-accent border-0 btn-sm p-0 m-0 text-[0.8rem] mb-2 mx-2.5 min-h-0 h-5" - onClick={() => { - submit(); - }} - > - Close - </button> - </FormControl> - </FormBody> - </FormCard> - </> - ); -}; diff --git a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx index 86cdfcdb8..10643f810 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useRef } from 'react'; import { Handle, Position, NodeProps } from 'reactflow'; import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; @@ -7,8 +7,12 @@ import { SchemaRelationshipPopup } from './SchemaRelationshipPopup'; import { SchemaEdge } from '../../../model'; import { RelationPill } from '@graphpolaris/shared/lib/components'; +import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; +import { VisualizationTooltip, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/CardToolTipVis'; + export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => { const [openPopup, setOpenPopup] = useState(false); + const ref = useRef<HTMLDivElement>(null); /** * Adds drag functionality in order to be able to drag the relationNode to the schema. @@ -43,14 +47,8 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: const onClickToggleAttributeAnalyticsPopupMenu = (): void => { data.toggleAttributeAnalyticsPopupMenu(data.collection); }; - return ( <> - {openPopup && ( - <Popup open={openPopup} hAnchor="left" className="-top-8" offset="-20rem"> - <SchemaRelationshipPopup data={data} onClose={() => setOpenPopup(false)} /> - </Popup> - )} <div className="w-fit h-fit" onDragStart={(event) => onDragStart(event)} @@ -63,6 +61,37 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: }} draggable > + {openPopup && ( + <Tooltip key={data.name} open={true} boundaryElement={ref} showArrow={true}> + <TooltipTrigger /> + <TooltipContent side="top"> + <div> + <VisualizationTooltip + type="schema" + typeOfSchema="relationship" + name={data.collection} + colorHeader="#0676C1" + numberOfElements={1000} + connectedFrom={data.from} + connectedTo={data.to} + data={ + data.attributes.length > 0 + ? data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + ) + : {} + } + /> + </div> + </TooltipContent> + </Tooltip> + )} <RelationPill draggable title={data.collection} diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 0cd2b32b9..9f18518da 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -175,7 +175,7 @@ export const NLPixi = (props: Props) => { onMouseUpStage(event: FederatedPointerEvent) { if (props.configuration.showPopUpOnHover) return; - + // If its a short click (not a drag) on the stage but not on a node: clear the selection and remove all popups. const holdDownTime = event.timeStamp - (event as any).mouseDownTimeStamp; if (holdDownTime < mouseClickThreshold) { @@ -210,7 +210,7 @@ export const NLPixi = (props: Props) => { }, onMoved(event: MovedEvent) { if (props.configuration.showPopUpOnHover) return; - + for (const popup of popups) { if (popup.node.x == null || popup.node.y == null) continue; popup.pos.x = event.viewport.transform.position.x + popup.node.x * event.viewport.scale.x; @@ -220,17 +220,17 @@ export const NLPixi = (props: Props) => { }, onZoom(event: FederatedPointerEvent) { const scale = viewport.current!.transform.scale.x; - + if (graph.current.nodes.length < config.LABEL_MAX_NODES) { - labelLayer.alpha = (scale > 2) ? Math.min(1, (scale - 2) * 3) : 0; + labelLayer.alpha = scale > 2 ? Math.min(1, (scale - 2) * 3) : 0; if (labelLayer.alpha > 0) { labelLayer.renderable = true; - - const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0. + + const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0. // Only change the fontSize for specific intervals, continuous change has too big of an impact on performance - const fontSize = (scale < 0.1) ? 30 : (scale < 0.2) ? 40 : (scale < 0.3) ? 50 : 60; + const fontSize = scale < 0.1 ? 30 : scale < 0.2 ? 40 : scale < 0.3 ? 50 : 60; const strokeWidth = fontSize / 2; labelMap.current.forEach((text) => { text.style.fontSize = fontSize; @@ -240,7 +240,7 @@ export const NLPixi = (props: Props) => { labelLayer.renderable = false; } } - } + }, })); function resize() { @@ -358,7 +358,7 @@ export const NLPixi = (props: Props) => { const linkMeta = props.graph.links[link._id]; - const text = new Text(linkMeta.name, { + const text = new Text(linkMeta.name, { fontSize: 60, fill: config.LINE_COLOR_DEFAULT, stroke: 0xffffff, @@ -366,7 +366,7 @@ export const NLPixi = (props: Props) => { }); text.cullable = true; text.anchor.set(0.5, 0.5); - text.scale.set(0.1, .1); + text.scale.set(0.1, 0.1); labelMap.current.set(link._id, text); labelLayer.addChild(text); @@ -466,11 +466,12 @@ export const NLPixi = (props: Props) => { text.x = (source.x + target.x) / 2; text.y = (source.y + target.y) / 2; - + const length = Math.hypot(target.x - source.x, target.y - source.y); // Skip rendering labels on very short edges - if (length < text.width + 10) { // 10 to account for size of node + if (length < text.width + 10) { + // 10 to account for size of node text.alpha = 0; return; } else { @@ -478,7 +479,7 @@ export const NLPixi = (props: Props) => { } const rads = Math.atan2(target.y - source.y, target.x - source.x); - text.rotation = rads + text.rotation = rads; const degrees = Math.abs(text.angle % 360); @@ -488,8 +489,7 @@ export const NLPixi = (props: Props) => { } else { text.rotation = rads; } - } - + }; // const text = labelMap.current.get(link._id); // if (!text) return; @@ -501,7 +501,7 @@ export const NLPixi = (props: Props) => { // text.x = (source.x + target.x) / 2; // text.y = (source.y + target.y) / 2; - + // const rads = Math.atan2(target.y - source.y, target.x - source.x); // const degrees = Math.abs(text.angle % 360); @@ -773,18 +773,22 @@ export const NLPixi = (props: Props) => { <Tooltip key={popup.node._id} open={true} interactive={!dragging} boundaryElement={ref} showArrow={true}> <TooltipTrigger x={popup.pos.x} y={popup.pos.y} /> <TooltipContent> - <NLPopup onClose={() => {}} data={{node: props.graph.nodes[popup.node._id], pos: popup.pos}} key={popup.node._id} /> + <NLPopup onClose={() => {}} data={{ node: props.graph.nodes[popup.node._id], pos: popup.pos }} key={popup.node._id} /> </TooltipContent> </Tooltip> ))} - {quickPopup != null && + {quickPopup != null && ( <Tooltip key={quickPopup.node._id} open={true} boundaryElement={ref} showArrow={true}> <TooltipTrigger x={quickPopup.pos.x} y={quickPopup.pos.y} /> <TooltipContent> - <NLPopup onClose={() => {}} data={{node: props.graph.nodes[quickPopup.node._id], pos: quickPopup.pos}} key={quickPopup.node._id} /> + <NLPopup + onClose={() => {}} + data={{ node: props.graph.nodes[quickPopup.node._id], pos: quickPopup.pos }} + key={quickPopup.node._id} + /> </TooltipContent> </Tooltip> - } + )} <div className="h-full w-full overflow-hidden" ref={ref} diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx index 173dbecc6..6e336b56f 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/utils.tsx @@ -9,7 +9,7 @@ import { GraphType, LinkType, NodeType } from '../types'; export function nodeColor(num: number) { // num = num % 4; // const col = '#000000'; - let entityColors = Object.values(visualizationColors.GPSeq.colors[9]); + //let entityColors = Object.values(visualizationColors.GPSeq.colors[9]); const col = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length]; return binaryColor(col); } @@ -18,7 +18,7 @@ export function nodeColorHex(num: number) { // num = num % 4; // const col = '#000000'; - let entityColors = Object.values(visualizationColors.GPSeq.colors[9]); + //let entityColors = Object.values(visualizationColors.GPSeq.colors[9]); const col = visualizationColors.GPCat.colors[14][num % visualizationColors.GPCat.colors[14].length]; return col; } -- GitLab