diff --git a/libs/config/styling/variables.css b/libs/config/styling/variables.css index 47bab4049b2c5b4151665b156a374be4f14994bb..09ef3956bb4f2b8f1d3b4fbc35c8844318b16be7 100644 --- a/libs/config/styling/variables.css +++ b/libs/config/styling/variables.css @@ -97,6 +97,11 @@ --clr-cat-12: var(--clr-neutral-50); --clr-cat-13: var(--clr-neutral-50); --clr-cat-14: var(--clr-neutral-50); + + /* Colors pills */ + --clr-node: var(--clr-acc); + --clr-relation: var(--clr-pri); + --clr-filter: var(--clr-acc--800); } body.light-mode { diff --git a/libs/shared/lib/assets/carbonIcons/carbonIcons.tsx b/libs/shared/lib/assets/carbonIcons/carbonIcons.tsx deleted file mode 100644 index de379ae1ddce563e3c20fcd96ed0b4dfe8e7c803..0000000000000000000000000000000000000000 --- a/libs/shared/lib/assets/carbonIcons/carbonIcons.tsx +++ /dev/null @@ -1,56 +0,0 @@ -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 deleted file mode 100644 index a6e5fc675aa24ca262b92122b7aaae344d4cd145..0000000000000000000000000000000000000000 --- a/libs/shared/lib/components/CardToolTipVis/VisualizationTooltip.tsx +++ /dev/null @@ -1,140 +0,0 @@ -import React from 'react'; -import { Icon } from '@graphpolaris/shared/lib/components/icon'; -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="icon-[ic--baseline-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 deleted file mode 100644 index 87c1972b5ca11a5a2a5982ba44b80737f97a29bc..0000000000000000000000000000000000000000 --- a/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.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 deleted file mode 100644 index ed1149af71faed28de008cc99595b62fef53c177..0000000000000000000000000000000000000000 --- a/libs/shared/lib/components/CardToolTipVis/cardtooltipvis.stories.tsx +++ /dev/null @@ -1,85 +0,0 @@ -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 deleted file mode 100644 index 868ce59def8610e245aa616468def7f05ba42c87..0000000000000000000000000000000000000000 --- a/libs/shared/lib/components/CardToolTipVis/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './VisualizationTooltip'; diff --git a/libs/shared/lib/components/DesignGuides/styleGuide.mdx b/libs/shared/lib/components/DesignGuides/styleGuide.mdx index 6847394b8ff3039c83650338843597bc1360954d..fd0755de185809d6a87a836dde0ab9d3b5fefc53 100644 --- a/libs/shared/lib/components/DesignGuides/styleGuide.mdx +++ b/libs/shared/lib/components/DesignGuides/styleGuide.mdx @@ -57,6 +57,7 @@ import { visualizationColors } from '../../../../config/src/colors.ts'; }} /> + {' '} <ColorItem title="Extra" colors={{ @@ -64,6 +65,16 @@ import { visualizationColors } from '../../../../config/src/colors.ts'; dark: 'hsl(var(--clr-dark))', }} /> + + {' '} + <ColorItem + title="Entities" + colors={{ + node: 'hsl(var(--clr-node))', + relation: 'hsl(var(--clr-relation))', + filter: 'hsl(var(--clr-filter))', + }} + /> </ColorPalette> #### Usage of colors diff --git a/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.module.scss b/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.module.scss deleted file mode 100644 index 87c1972b5ca11a5a2a5982ba44b80737f97a29bc..0000000000000000000000000000000000000000 --- a/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.module.scss +++ /dev/null @@ -1,6 +0,0 @@ -.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/VisualizationTooltip/VisualizationTooltip.stories.tsx b/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.stories.tsx index 0a4f40459efd6e6618409ad755ade74d5c455484..8f96cbf3c3c9d96c406513ea262f5218e52daddd 100644 --- a/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.stories.tsx +++ b/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.stories.tsx @@ -1,6 +1,8 @@ -import React, { useState } from 'react'; +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; -import { VisualizationTooltip, CardToolTipVisProps } from '@graphpolaris/shared/lib/components/VisualizationTooltip'; +import { VisualizationTooltip, VisualizationTooltipProps } from '@graphpolaris/shared/lib/components/VisualizationTooltip'; +import { SchemaPopUp, SchemaPopUpProps } from '@graphpolaris/shared/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp'; +import { NLPopUp, NLPopUpProps } from '@graphpolaris/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup'; const meta: Meta<typeof VisualizationTooltip> = { component: VisualizationTooltip, @@ -8,67 +10,96 @@ const meta: Meta<typeof VisualizationTooltip> = { }; export default meta; -type Story = StoryObj<typeof VisualizationTooltip>; + +type CombinedProps = VisualizationTooltipProps & SchemaPopUpProps & NLPopUpProps & { attributes: { name: string; type: string }[] }; + +type Story = StoryObj<CombinedProps>; export const SchemaNode: Story = { render: (args) => { + const { name, attributes, colorHeader, numberOfElements } = args; + const data = attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, any>, + ); + return ( <div className="w-1/4 my-10 m-auto flex items-center justify-center"> - <VisualizationTooltip {...args} /> + <VisualizationTooltip name={name} colorHeader={colorHeader}> + <SchemaPopUp data={data} numberOfElements={numberOfElements} /> + </VisualizationTooltip> </div> ); }, args: { - type: 'schema', - typeOfSchema: 'node', name: 'Person', - data: { - born: 'int', - name: 'string', - description: 'string', - }, - colorHeader: '#fb7b04', + attributes: [ + { name: 'int', type: 'int' }, + { name: 'float', type: 'float' }, + { name: 'date', type: 'date' }, + { name: 'string', type: 'string' }, + { name: 'boolean', type: 'boolean' }, + { name: 'undefined', type: 'undefined' }, + ], + colorHeader: 'hsl(var(--clr-node))', numberOfElements: 1000, }, }; export const SchemaRelationship: Story = { render: (args) => { + const { name, attributes, colorHeader, numberOfElements, connections } = args; + const data = attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, any>, + ); + return ( <div className="w-1/4 my-10 m-auto flex items-center justify-center"> - <VisualizationTooltip {...args} /> + <VisualizationTooltip name={name} colorHeader={colorHeader}> + <SchemaPopUp data={data} numberOfElements={numberOfElements} connections={connections} /> + </VisualizationTooltip> </div> ); }, args: { - type: 'schema', - typeOfSchema: 'relationship', name: 'Directed', - data: { - born: 'int', - name: 'string', - description: 'string', - imdb: 'string', - imdbVotes: 'int', - }, + attributes: [ + { name: 'born', type: 'int' }, + { name: 'name', type: 'string' }, + { name: 'description', type: 'string' }, + { name: 'imdb', type: 'string' }, + { name: 'imdbVotes', type: 'int' }, + ], colorHeader: '#0676C1', - connectedTo: 'Person', numberOfElements: 231230, - connectedFrom: 'Movie', + connections: { to: 'Person', from: 'Movie' }, }, }; -export const PopUpVis: Story = { +export const NodeLinkPopUp: Story = { render: (args) => { + const { name, data, colorHeader } = args; return ( <div className="w-1/4 my-10 m-auto flex items-center justify-center"> - <VisualizationTooltip {...args} /> + <VisualizationTooltip name={name} colorHeader={colorHeader}> + <NLPopUp data={data} /> + </VisualizationTooltip> </div> ); }, args: { name: 'Person', - type: 'popupvis', data: { bio: 'From wikipedia was born in usa from a firefighter father', name: 'Charlotte Henry', diff --git a/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.tsx b/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.tsx index 699c3354af0d8932429f5847554e0708dfc8e307..67ecc6be1e80fc0663d9e544d2f2740df0bde69e 100644 --- a/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.tsx +++ b/libs/shared/lib/components/VisualizationTooltip/VisualizationTooltip.tsx @@ -1,140 +1,24 @@ -import React from 'react'; -import { Icon } from '@graphpolaris/shared/lib/components/icon'; -import styles from './VisualizationTooltip.module.scss'; +import React, { ReactNode } from 'react'; 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; +export type VisualizationTooltipProps = { name: string; - data: Record<string, any>; - typeOfSchema?: string; colorHeader: string; - connectedTo?: string; - connectedFrom?: string; - maxVisibleItems?: number; - numberOfElements?: number; + children: ReactNode; }; -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); - +export const VisualizationTooltip: React.FC<VisualizationTooltipProps> = ({ name, colorHeader, children }) => { return ( - <div className="border-1 border-sec-200 bg-white w-[12rem] -mx-2 -my-1"> + <div className="border-1 border-sec-200 bg-white w-[12rem] -mx-2 -my-2"> <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="icon-[ic--baseline-numbers]" size={24} />{' '} - <span className="ml-auto text-right">{formatNumber(numberOfElements)}</span> + <div className={'flex max-w-full'}> + <span className="text-base font-semibold truncate">{name}</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> + {children} </div> ); }; diff --git a/libs/shared/lib/components/pills/Pill.tsx b/libs/shared/lib/components/pills/Pill.tsx index a479ef713d6e2e8282d2671b432397e356139429..56492a5ed75eaa1ee5aa2384f345d47048f4d615 100644 --- a/libs/shared/lib/components/pills/Pill.tsx +++ b/libs/shared/lib/components/pills/Pill.tsx @@ -156,7 +156,7 @@ export const EntityPill = React.memo((props: Omit<PillI, 'topColor'> & { withHan </> ); return ( - <PillContext.Provider value={{ color: 'hsl(29 96 60)' }}> + <PillContext.Provider value={{ color: 'hsl(var(--clr-node))' }}> <Pill {...props} corner="rounded" handles={handles} /> </PillContext.Provider> ); @@ -184,7 +184,7 @@ export const RelationPill = React.memo((props: Omit<PillI, 'topColor'> & { withH ); return ( - <PillContext.Provider value={{ color: '#0676C1' }}> + <PillContext.Provider value={{ color: 'hsl(var(--clr-relation))' }}> <Pill {...props} corner="diamond" handles={handles} /> </PillContext.Provider> ); @@ -198,7 +198,7 @@ export const LogicPill = React.memo((props: Omit<PillI, 'topColor'>) => { ); return ( - <PillContext.Provider value={{ color: '#543719' }}> + <PillContext.Provider value={{ color: 'hsl(var(--clr-filter))' }}> <Pill {...props} corner="square" handles={handles} /> </PillContext.Provider> ); diff --git a/libs/shared/lib/components/pills/PillContext.tsx b/libs/shared/lib/components/pills/PillContext.tsx index 3ec61a12a9b6d1f0b1ae1cfc5d107db90e2c2e19..34cf06e2e6da8c8df9196f3c286905bf032f677a 100644 --- a/libs/shared/lib/components/pills/PillContext.tsx +++ b/libs/shared/lib/components/pills/PillContext.tsx @@ -1,5 +1,5 @@ import { createContext } from 'react'; export const PillContext = createContext({ - color: 'hsl(29 96 60)', + color: 'hsl(var(--clr-node))', }); diff --git a/libs/shared/lib/components/tooltip/Tooltip.tsx b/libs/shared/lib/components/tooltip/Tooltip.tsx index 4f6b91f7dba0dc8c80a73d4c3dab6f2a02c0aa39..ac37a2506cccbfb9eae449e2e9627a165d88fd13 100644 --- a/libs/shared/lib/components/tooltip/Tooltip.tsx +++ b/libs/shared/lib/components/tooltip/Tooltip.tsx @@ -205,7 +205,7 @@ export const TooltipContent = React.forwardRef< <FloatingPortal> <div ref={ref} - className={`z-50 max-w-64 rounded bg-light px-2 py-2 shadow text-xs border border-secondary-200 + className={`max-w-64 rounded bg-light px-2 py-2 shadow text-xs border border-secondary-200 text-dark animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2${className ? ` ${className}` : ''}`} diff --git a/libs/shared/lib/data-access/store/schemaSlice.ts b/libs/shared/lib/data-access/store/schemaSlice.ts index 39bd69e4e9e631e7800c908aa7e5e986a8968fb9..e0398dac1e1b315cc247e58973b449b41f72e3df 100644 --- a/libs/shared/lib/data-access/store/schemaSlice.ts +++ b/libs/shared/lib/data-access/store/schemaSlice.ts @@ -12,6 +12,7 @@ export type SchemaSettings = { connectionType: SchemaConnectionTypes; layoutName: AllLayoutAlgorithms; animatedEdges: boolean; + showMinimap: boolean; }; type schemaSliceI = { @@ -37,6 +38,7 @@ export const initialState: schemaSliceI = { connectionType: 'connection', layoutName: Layouts.DAGRE, animatedEdges: false, + showMinimap: true, }, }; export const schemaSlice = createSlice({ diff --git a/libs/shared/lib/schema/model/reactflow.tsx b/libs/shared/lib/schema/model/reactflow.tsx index d03ac91359ca2df9017ee351b923550b1c5bde11..60567d10ab47a6ff53e88c0a83b58e09df4ac2c0 100644 --- a/libs/shared/lib/schema/model/reactflow.tsx +++ b/libs/shared/lib/schema/model/reactflow.tsx @@ -37,6 +37,7 @@ export type SchemaReactflowEntity = SchemaReactflowData & { x: number; y: number; reactFlowRef: any; + tooltipClose: boolean; }; export type SchemaReactflowRelation = SchemaReactflowData & { @@ -47,6 +48,8 @@ export type SchemaReactflowRelation = SchemaReactflowData & { toRatio: number; x: number; y: number; + reactFlowRef: any; + tooltipClose: boolean; }; export type SchemaReactflowNodeWithFunctions = SchemaReactflowEntity & { diff --git a/libs/shared/lib/schema/panel/Schema.tsx b/libs/shared/lib/schema/panel/Schema.tsx index 086af4dbb1c25affd59f23cd0e1aee38c5b5b31e..aae59bad9af71d24fecc51943351bd01c1755e12 100644 --- a/libs/shared/lib/schema/panel/Schema.tsx +++ b/libs/shared/lib/schema/panel/Schema.tsx @@ -49,6 +49,13 @@ export const Schema = (props: Props) => { const [nodes, setNodes, onNodesChange] = useNodesState([] as Node[]); const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]); + // viewport + const initialViewportRef = useRef<{ x: number; y: number; zoom: number } | null>(null); + const [hasLayoutBeenRun, setHasLayoutBeenRun] = useState(false); + + // Time threshold for distinguishing between a click and a drag + const isPillClicked = useRef<boolean>(false); + const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null); const reactFlowRef = useRef<HTMLDivElement>(null); @@ -87,18 +94,45 @@ export const Schema = (props: Props) => { updateLayout(); const expandedSchema = schemaExpandRelation(schemaGraphology); const bounds = reactFlowRef.current?.getBoundingClientRect(); - 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); - const nodesWithRef = schemaFlow.nodes.map((node) => ({ - ...node, - data: { ...node.data, reactFlowRef }, - })); + let nodesWithRef, edgesWithRef; + if (!hasLayoutBeenRun) { + nodesWithRef = schemaFlow.nodes.map((node) => { + return { + ...node, + data: { ...node.data, reactFlowRef, tooltipClose: false }, + }; + }); + + edgesWithRef = schemaFlow.edges.map((edge) => { + return { + ...edge, + data: { ...edge.data, reactFlowRef, tooltipClose: false }, + }; + }); + + setHasLayoutBeenRun(true); + } else { + nodesWithRef = nodes.map((node) => { + return { + ...node, + data: { ...node.data }, + }; + }); + + edgesWithRef = edges.map((edge) => { + return { + ...edge, + data: { ...edge.data }, + }; + }); + } setNodes(nodesWithRef); - setEdges(schemaFlow.edges); + setEdges(edgesWithRef); setTimeout(() => fitView(), 100); } @@ -118,14 +152,39 @@ export const Schema = (props: Props) => { const nodeColor = (node: any) => { switch (node.type) { case 'entity': - return '#fb7b04'; + return 'hsl(var(--clr-node))'; case 'relation': - return '#0676C1'; + return 'hsl(var(--clr-relation))'; default: return '#ff0072'; } }; + const handleOnClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => { + const target = event.target as HTMLElement; + const clickedOutsideNode = target.classList.contains('react-flow__pane'); + + setNodes((nds) => + nds.map((node) => ({ + ...node, + data: { + ...node.data, + tooltipClose: clickedOutsideNode, + }, + })), + ); + + setEdges((edg) => + edg.map((edge) => ({ + ...edge, + data: { + ...edge.data, + tooltipClose: clickedOutsideNode, + }, + })), + ); + }; + return ( <Panel title="Schema" @@ -204,7 +263,10 @@ export const Schema = (props: Props) => { </> } > - <div className="schema-panel w-full h-full flex flex-col justify-between" ref={reactFlowRef}> + <div + className="schema-panel w-full h-full flex flex-col justify-between" + ref={reactFlowRef} + > {nodes.length === 0 ? ( <p className="m-3 text-xl font-bold">No Elements</p> ) : ( @@ -226,31 +288,13 @@ export const Schema = (props: Props) => { reactFlowInstanceRef.current = reactFlowInstance; onInit(reactFlowInstance); }} + onClick={handleOnClick} proOptions={{ hideAttribution: true }} > - <MiniMap nodeColor={nodeColor} /> + {settings.showMinimap && <MiniMap nodeColor={nodeColor} />} </ReactFlow> </ReactFlowProvider> )} - {/* <div> - <div - className="w-full py-0 px-2 bg-secondary-50 cursor-pointer border-y flex items-center gap-1" - onClick={() => setExpanded(!expanded)} - > - <Button - size="xs" - variant="ghost" - iconComponent={expanded ? <KeyboardArrowDown /> : <KeyboardArrowRight />} - onClick={() => setExpanded(!expanded)} - /> - <span className="text-xs font-semibold text-secondary-600 truncate">Schema settings</span> - </div> - {expanded && ( - <div className="h-full w-full overflow-y-auto"> - <SchemaDialog open={toggleSchemaSettings} onClose={() => setToggleSchemaSettings(false)} /> - </div> - )} - </div> */} </div> </Panel> ); diff --git a/libs/shared/lib/schema/panel/SchemaSettings.tsx b/libs/shared/lib/schema/panel/SchemaSettings.tsx index 0a6534d5212921d12e0467b5d874266372856015..572472aa0d4bc02b4439675b8a55bd6b8594856e 100644 --- a/libs/shared/lib/schema/panel/SchemaSettings.tsx +++ b/libs/shared/lib/schema/panel/SchemaSettings.tsx @@ -19,6 +19,14 @@ export const SchemaSettings = () => { dispatch(setSchemaSettings({ ...settings, animatedEdges: value as any })); }} /> + <Input + type="boolean" + value={settings.showMinimap} + label="Show Minimap" + onChange={(value: boolean) => { + dispatch(setSchemaSettings({ ...settings, showMinimap: value as any })); + }} + /> <Input type="dropdown" label="Type of Connection" diff --git a/libs/shared/lib/schema/panel/schema.stories.tsx b/libs/shared/lib/schema/panel/schema.stories.tsx index 17679da8a89b4534198dac0b9ec55b280031c9f8..69e3b95a697cd04bedb1200a5649cb8c8db68cc2 100644 --- a/libs/shared/lib/schema/panel/schema.stories.tsx +++ b/libs/shared/lib/schema/panel/schema.stories.tsx @@ -91,6 +91,27 @@ export const TestSimple = { }, }; +export const TestTooltip = { + play: async () => { + const dispatch = Mockstore.dispatch; + const schema = SchemaUtils.schemaBackend2Graphology({ + nodes: [ + { + name: 'Thijs', + attributes: [ + { name: 'city', type: 'string' }, + { name: 'vip', type: 'bool' }, + { name: 'state', type: 'string' }, + ], + }, + ], + edges: [], + }); + + dispatch(setSchema(schema.export())); + }, +}; + export const TestMovieSchema = { play: async () => { const dispatch = Mockstore.dispatch; diff --git a/libs/shared/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx b/libs/shared/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx new file mode 100644 index 0000000000000000000000000000000000000000..331e0761ec3093f37990b409814672cb2713c623 --- /dev/null +++ b/libs/shared/lib/schema/pills/nodes/SchemaPopUp/SchemaPopUp.tsx @@ -0,0 +1,89 @@ +import React from 'react'; +import { Icon } from '@graphpolaris/shared/lib/components/icon'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@graphpolaris/shared/lib/components/tooltip'; +import { useSchemaStats } from '@graphpolaris/shared/lib/data-access'; + +const formatNumber = (number: number) => { + return number.toLocaleString('de-DE'); +}; + +export type SchemaPopUpProps = { + data: Record<string, any>; + connections?: { to: string; from: string }; + numberOfElements?: number; +}; + +export const SchemaPopUp: React.FC<SchemaPopUpProps> = ({ data, numberOfElements, connections }) => { + return ( + <> + <div className=""> + {numberOfElements != null && numberOfElements != 0 && ( + <div className="border-b border-sec-200"> + <div className="flex flex-row gap-1 items-center justify-between px-3 py-1"> + <Icon component="icon-[ic--baseline-numbers]" size={24} /> + <span className="ml-auto text-right">{formatNumber(numberOfElements)}</span> + </div> + </div> + )} + {connections && ( + <div className="border-b border-sec-200 px-3 py-1"> + <div className="flex flex-row gap-3 items-center justify-between"> + <span className="font-semibold">From</span> + <span className="ml-auto text-right">{connections.from}</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">{connections.to}</span> + </div> + </div> + )} + <TooltipProvider delayDuration={300}> + <div className="px-3 py-1"> + {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 w-[90%]`}>{k}</span> + <TooltipTrigger asChild> + <span className="ml-auto text-right truncate grow-1 flex items-center"> + <Icon + className="ml-auto text-right flex-shrink-0" + component={ + v === 'int' || v === 'float' ? ( + <Icon component="icon-[carbon--string-integer]" size={24} color={'hsl(var(--clr-sec--500))'} /> + ) : v === 'string' ? ( + <Icon component="icon-[carbon--string-text]" size={24} color={'hsl(var(--clr-sec--500))'} /> + ) : v === 'boolean' || v === 'bool' ? ( + <Icon component="icon-[carbon--boolean]" size={24} color={'hsl(var(--clr-sec--500))'} /> + ) : v === 'date' || v === 'time' || v === 'duration' || v === 'datetime' ? ( + <Icon component="icon-[carbon--calendar]" size={24} color={'hsl(var(--clr-sec--500))'} /> + ) : v === 'undefined' ? ( + <Icon component="icon-[carbon--undefined]" size={24} color={'hsl(var(--clr-sec--500))'} /> + ) : ( + <Icon component="icon-[carbon--undefined]" size={24} color={'hsl(var(--clr-sec--500))'} /> + ) + } + color="hsl(var(--clr-sec--400))" + size={24} + /> + </span> + </TooltipTrigger> + <TooltipContent side="right"> + <div className="max-w-[18rem] break-all line-clamp-6 mx-1"> + {v !== undefined && (typeof v !== 'object' || Array.isArray(v)) && v != '' ? v : 'noData'} + </div> + </TooltipContent> + </div> + </Tooltip> + )) + )} + </div> + </TooltipProvider> + </div> + </> + ); +}; diff --git a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx index 3bf2158a58d8906758c2ed834dce67560e62287e..c4fb7f010b2b1dd82d8604230e565c280208d53b 100644 --- a/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx +++ b/libs/shared/lib/schema/pills/nodes/entity/SchemaEntityPill.tsx @@ -1,14 +1,20 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { Handle, Position, NodeProps } from 'reactflow'; +import React, { useState, useRef, useEffect, useMemo } from 'react'; +import { Handle, Position, NodeProps, useViewport } from 'reactflow'; import { SchemaReactflowNodeWithFunctions } from '../../../model/reactflow'; import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; 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'; +import { VisualizationTooltip } from '@graphpolaris/shared/lib/components/VisualizationTooltip'; +import { SchemaPopUp } from '../SchemaPopUp/SchemaPopUp'; +import { useSchemaStats } from '@graphpolaris/shared/lib/data-access'; export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<SchemaReactflowNodeWithFunctions>) => { - const [openPopup, setOpenPopup] = useState(false); + const [openPopupLocation, setOpenPopupLocation] = useState<{ x: number; y: number } | null>(null); + + const viewport = useViewport(); + const schemaStats = useSchemaStats(); + const ref = useRef<HTMLDivElement>(null); /** * adds drag functionality in order to be able to drag the entityNode to the schema @@ -24,19 +30,23 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc event.dataTransfer.effectAllowed = 'move'; }; - /** - * displays the NodeQualityPopup when clicked, and hides them when clicked again - */ - const onClickToggleNodeQualityPopup = (): void => { - data.toggleNodeQualityPopup(id); - }; + useEffect(() => { + if (data.tooltipClose === true) { + setOpenPopupLocation(null); + } + }, [data.tooltipClose]); - /** - * displays the attribute analytics popup menu when clicked, and hides them when clicked again - */ - const onClickToggleAttributeAnalyticsPopupMenu = (): void => { - data.toggleAttributeAnalyticsPopupMenu(id); - }; + const tooltipX = useMemo(() => { + if (ref.current == null || openPopupLocation == null) return -1; + const rect = ref.current.getBoundingClientRect(); + return rect.x - openPopupLocation.x + (rect.width / 2); + }, [viewport.x, openPopupLocation]); + + const tooltipY = useMemo(() => { + if (ref.current == null || openPopupLocation == null) return -1; + const rect = ref.current.getBoundingClientRect(); + return rect.y - openPopupLocation.y + (rect.height / 2); + }, [viewport.y, openPopupLocation]); return ( <> @@ -48,32 +58,34 @@ export const SchemaEntityPill = React.memo(({ id, selected, data }: NodeProps<Sc if (!event.shiftKey) event.stopPropagation(); }} onClickCapture={(event) => { - setOpenPopup(!openPopup); + if (openPopupLocation != null || ref.current == null) { + return; + } + + setOpenPopupLocation(ref.current.getBoundingClientRect()); }} draggable ref={ref} > - {openPopup && ( + {openPopupLocation !== null && ( <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> - <TooltipTrigger /> - <TooltipContent side="right"> + <TooltipTrigger x={tooltipX} y={tooltipY} /> + <TooltipContent> <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>, - )} - /> + <VisualizationTooltip name={data.name} colorHeader={'hsl(var(--clr-node))'}> + <SchemaPopUp + data={data.attributes.reduce( + (acc, attr) => { + if (attr.name && attr.type) { + acc[attr.name] = attr.type; + } + return acc; + }, + {} as Record<string, string>, + )} + numberOfElements={schemaStats.nodeStats[data.name]?.count} + /> + </VisualizationTooltip> </div> </TooltipContent> </Tooltip> diff --git a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx index 10643f8106a77a4fa3db12dce2ecb3d656f303f0..2bee28161f69e9353e350711944877d2e4a40722 100644 --- a/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx +++ b/libs/shared/lib/schema/pills/nodes/relation/SchemaRelationPill.tsx @@ -1,19 +1,22 @@ -import React, { useState, useRef } from 'react'; -import { Handle, Position, NodeProps } from 'reactflow'; +import React, { useState, useRef, useEffect, useMemo } from 'react'; +import { Handle, Position, NodeProps, useViewport } from 'reactflow'; import { SchemaReactflowRelationWithFunctions } from '../../../model/reactflow'; import { QueryElementTypes } from '@graphpolaris/shared/lib/querybuilder'; -import { Popup } from '@graphpolaris/shared/lib/components/Popup'; -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'; +import { VisualizationTooltip } from '@graphpolaris/shared/lib/components/VisualizationTooltip'; +import { SchemaPopUp } from '../SchemaPopUp/SchemaPopUp'; +import { useSchemaStats } from '@graphpolaris/shared/lib/data-access'; export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: NodeProps<SchemaReactflowRelationWithFunctions>) => { - const [openPopup, setOpenPopup] = useState(false); - const ref = useRef<HTMLDivElement>(null); + const [openPopupLocation, setOpenPopupLocation] = useState<{ x: number; y: number } | null>(null); + + const viewport = useViewport(); + const schemaStats = useSchemaStats(); + const ref = useRef<HTMLDivElement>(null); /** * Adds drag functionality in order to be able to drag the relationNode to the schema. * @param event React Mouse drag event. @@ -34,19 +37,24 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: event.dataTransfer.effectAllowed = 'move'; }; - /** - * Displays the NodeQualityPopup when clicked, and hides them when clicked again - */ - const onClickToggleNodeQualityPopup = (): void => { - data.toggleNodeQualityPopup(data.collection); - }; + useEffect(() => { + if (data.tooltipClose === true) { + setOpenPopupLocation(null); + } + }, [data.tooltipClose]); + + const tooltipX = useMemo(() => { + if (ref.current == null || openPopupLocation == null) return -1; + const rect = ref.current.getBoundingClientRect(); + return rect.x - openPopupLocation.x + (rect.width / 2); + }, [viewport.x, openPopupLocation]); + + const tooltipY = useMemo(() => { + if (ref.current == null || openPopupLocation == null) return -1; + const rect = ref.current.getBoundingClientRect(); + return rect.y - openPopupLocation.y + (rect.height / 2); + }, [viewport.y, openPopupLocation]); - /** - * displays the attribute analytics popup menu when clicked, and hides them when clicked again - */ - const onClickToggleAttributeAnalyticsPopupMenu = (): void => { - data.toggleAttributeAnalyticsPopupMenu(data.collection); - }; return ( <> <div @@ -57,37 +65,39 @@ export const SchemaRelationPill = React.memo(({ id, selected, data, ...props }: if (!event.shiftKey) event.stopPropagation(); }} onClickCapture={(event) => { - setOpenPopup(!openPopup); + if (openPopupLocation != null || ref.current == null) { + return; + } + + setOpenPopupLocation(ref.current.getBoundingClientRect()); }} draggable + ref={ref} > - {openPopup && ( - <Tooltip key={data.name} open={true} boundaryElement={ref} showArrow={true}> - <TooltipTrigger /> - <TooltipContent side="top"> + {openPopupLocation !== null && ( + <Tooltip key={data.name} open={true} boundaryElement={data.reactFlowRef} showArrow={true}> + <TooltipTrigger x={tooltipX} y={tooltipY} /> + <TooltipContent> <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>, - ) - : {} - } - /> + <VisualizationTooltip name={data.collection} colorHeader={'hsl(var(--clr-relation))'}> + <SchemaPopUp + 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>, + ) + : {} + } + connections={{ from: data.from, to: data.to }} + numberOfElements={schemaStats.edgeStats[data.collection]?.count} + /> + </VisualizationTooltip> </div> </TooltipContent> </Tooltip> diff --git a/libs/shared/lib/vis/components/VisualizationPanel.tsx b/libs/shared/lib/vis/components/VisualizationPanel.tsx index b3aa4c1030b7010a81da4fa7f147826189214bee..65772caaaf0bc1b1df56d7267c804e7f855202e3 100644 --- a/libs/shared/lib/vis/components/VisualizationPanel.tsx +++ b/libs/shared/lib/vis/components/VisualizationPanel.tsx @@ -81,10 +81,9 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { return ( <div - className="vis-panel h-full w-full flex flex-col border bg-light" + className="relative pt-7 vis-panel h-full w-full flex flex-col border bg-light" onMouseDownCapture={() => dispatch(resultSetFocus({ focusType: 'visualization' }))} > - <VisualizationTabBar fullSize={fullSize} /> <div className="grow overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}> {graphQueryResult.queryingBackend ? ( <Querying /> @@ -116,6 +115,7 @@ export const VisualizationPanel = ({ fullSize }: { fullSize: () => void }) => { </div> )} </div> + <VisualizationTabBar fullSize={fullSize} /> </div> ); }; diff --git a/libs/shared/lib/vis/components/VisualizationTabBar.tsx b/libs/shared/lib/vis/components/VisualizationTabBar.tsx index 8ff06c7a4541e239aa332c7da13b7c960481fd35..96b68a0349b98084f828c727daf932959549a3da 100644 --- a/libs/shared/lib/vis/components/VisualizationTabBar.tsx +++ b/libs/shared/lib/vis/components/VisualizationTabBar.tsx @@ -36,7 +36,7 @@ export default function VisualizationTabBar(props: { fullSize: () => void }) { }; return ( - <div className="sticky shrink-0 top-0 flex items-stretch justify-between h-7 bg-secondary-100 border-b border-secondary-200 max-w-full"> + <div className="absolute shrink-0 top-0 left-0 right-0 flex items-stretch justify-between h-7 bg-secondary-100 border-b border-secondary-200 max-w-full"> <div className="flex items-center"> <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">Visualization</h1> </div> diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index 47f722d31f55585d98d7d60c65376e1552d88584..52b711cb7119615fa60c67152377a45204193da3 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -12,10 +12,10 @@ import { Text, Texture, Resource, - RenderTexture + RenderTexture, } from 'pixi.js'; import { useAppDispatch, useML, useSearchResultData } from '../../../../data-access'; -import { NLPopup } from './NLPopup'; +import { NLPopUp } from './NLPopup'; import { hslStringToHex, nodeColor } from './utils'; import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts, GraphologyForceAtlas2Webworker } from '../../../../graph-layout'; import { MultiGraph } from 'graphology'; @@ -23,6 +23,8 @@ import { Viewport } from 'pixi-viewport'; import { NodelinkVisProps } from '../nodelinkvis'; import { Tooltip, TooltipContent, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; import { MovedEvent } from 'pixi-viewport/dist/types'; +import { VisualizationTooltip } from '@graphpolaris/shared/lib/components/VisualizationTooltip'; +import { nodeColorHex } from './utils'; import { Theme } from '@graphpolaris/shared/lib/data-access/store/configSlice'; import { useConfig } from '@graphpolaris/shared/lib/data-access/store'; @@ -107,23 +109,23 @@ export const NLPixi = (props: Props) => { let size = config.NODE_RADIUS * (1 / responsiveScale); let lineWidth = selected ? 12 : 6; renderTexture.resize(size + lineWidth, size + lineWidth); - + const graphics = new Graphics(); graphics.lineStyle(lineWidth, 0x4e586a); graphics.beginFill(0xffffff, 1); - if (props.configuration.shapes?.shape == "circle") { - graphics.drawCircle((size / 2) + (lineWidth / 2), (size / 2) + (lineWidth/2), (size / 2)); + if (props.configuration.shapes?.shape == 'circle') { + graphics.drawCircle(size / 2 + lineWidth / 2, size / 2 + lineWidth / 2, size / 2); } else { graphics.drawRect(lineWidth, lineWidth, size - lineWidth, size - lineWidth); } graphics.endFill(); - + app.renderer.render(graphics, { renderTexture }); return renderTexture; - } - + }; + // Pixi viewport zoom scale, but discretized to single decimal. const [responsiveScale, setResponsiveScale] = useState(1); @@ -147,7 +149,8 @@ export const NLPixi = (props: Props) => { if (graph.current.nodes.length > config.LABEL_MAX_NODES) return; // Change font size at specific scale intervals - const fontSize = (responsiveScale <= 0.1) ? 15 : (responsiveScale <= 0.2) ? 22.5 : (responsiveScale <= 0.4) ? 30 : (responsiveScale <= 0.6) ? 37.5 : 45; + const fontSize = + responsiveScale <= 0.1 ? 15 : responsiveScale <= 0.2 ? 22.5 : responsiveScale <= 0.4 ? 30 : responsiveScale <= 0.6 ? 37.5 : 45; const strokeWidth = fontSize / 2; linkLabelMap.current.forEach((text) => { @@ -296,15 +299,15 @@ export const NLPixi = (props: Props) => { onZoom(event: FederatedPointerEvent) { const scale = viewport.current!.transform.scale.x; - if (scale > 2) { - const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0. - setResponsiveScale((scale < 0.05) ? 0.1 : (scale < 0.1) ? 0.2 : (scale < 0.2) ? 0.4 : (scale < 0.3) ? 0.6 : 0.8); + if (scale > 2) { + const scale = 1 / viewport.current!.scale.x; // starts from 0.5 down to 0. + setResponsiveScale(scale < 0.05 ? 0.1 : scale < 0.1 ? 0.2 : scale < 0.2 ? 0.4 : scale < 0.3 ? 0.6 : 0.8); } else { setResponsiveScale(1); } if (graph.current.nodes.length < config.LABEL_MAX_NODES) { - linkLabelLayer.alpha = (scale > 2) ? Math.min(1, (scale - 2) * 3) : 0; + linkLabelLayer.alpha = scale > 2 ? Math.min(1, (scale - 2) * 3) : 0; if (linkLabelLayer.alpha > 0) { linkLabelLayer.renderable = true; @@ -312,7 +315,7 @@ export const NLPixi = (props: Props) => { linkLabelLayer.renderable = false; } - nodeLabelLayer.alpha = (scale > 5) ? Math.min(1, (scale - 5) * 3) : 0; + nodeLabelLayer.alpha = scale > 5 ? Math.min(1, (scale - 5) * 3) : 0; if (nodeLabelLayer.alpha > 0) { nodeLabelLayer.renderable = true; } else { @@ -377,7 +380,6 @@ export const NLPixi = (props: Props) => { const nodeMeta = props.graph.nodes[node._id]; const texture = (gfx as any).selected ? selectedTexture : glyphTexture; gfx.texture = texture; - // Cluster colors if (nodeMeta?.cluster) { @@ -405,8 +407,8 @@ export const NLPixi = (props: Props) => { }; const getNodeLabel = (nodeMeta: NodeType) => { - return nodeMeta.label - } + return nodeMeta.label; + }; const createNode = (node: NodeTypeD3, selected?: boolean) => { const nodeMeta = props.graph.nodes[node._id]; @@ -442,12 +444,12 @@ export const NLPixi = (props: Props) => { // Node label const attribute = getNodeLabel(nodeMeta); - const text = new Text(attribute, { + const text = new Text(attribute, { fontSize: 20, fill: 0xffffff, wordWrap: true, wordWrapWidth: 65, - align: 'center' + align: 'center', }); text.eventMode = 'none'; text.cullable = true; @@ -617,19 +619,18 @@ export const NLPixi = (props: Props) => { const nodeMeta = props.graph.nodes[node._id]; const originalText = getNodeLabel(nodeMeta); - text.text = originalText; // This is required to ensure the text size check (next line) works + text.text = originalText; // This is required to ensure the text size check (next line) works - if ((text.width/text.scale.x) <= 90 && (text.height/text.scale.y) <= 90) { + if (text.width / text.scale.x <= 90 && text.height / text.scale.y <= 90) { text.text = originalText; } else { // Change character limit at specific scale intervals - const charLimit = (responsiveScale > 0.2) ? 15 : (responsiveScale > 0.1) ? 30 : 75; - text.text = `${ originalText.slice(0, charLimit)}…`; + const charLimit = responsiveScale > 0.2 ? 15 : responsiveScale > 0.1 ? 30 : 75; + text.text = `${originalText.slice(0, charLimit)}…`; } - text.alpha = ((text.width/text.scale.x) <= 90 && (text.height/text.scale.y) <= 90) ? 1 : 0; - } - + text.alpha = text.width / text.scale.x <= 90 && text.height / text.scale.y <= 90 ? 1 : 0; + }; // const text = linkLabelMap.current.get(link._id); // if (!text) return; @@ -662,7 +663,7 @@ export const NLPixi = (props: Props) => { nodeLabelLayer.removeChildren(); const layout = layoutAlgorithm.current as GraphologyForceAtlas2Webworker; - if(layout?.cleanup != null) layout.cleanup(); + if (layout?.cleanup != null) layout.cleanup(); }; }, []); @@ -908,7 +909,9 @@ 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} /> + <VisualizationTooltip name={popup.node._id} colorHeader={nodeColorHex(props.graph.nodes[popup.node._id].type)}> + <NLPopUp data={props.graph.nodes[popup.node._id].attributes} /> + </VisualizationTooltip> </TooltipContent> </Tooltip> ))} @@ -916,11 +919,9 @@ export const NLPixi = (props: Props) => { <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} - /> + <VisualizationTooltip name={quickPopup.node._id} colorHeader={nodeColorHex(props.graph.nodes[quickPopup.node._id].type)}> + <NLPopUp data={props.graph.nodes[quickPopup.node._id].attributes} /> + </VisualizationTooltip> </TooltipContent> </Tooltip> )} diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx index db7f37d0df74b76c270de20334b39cae891c5fb8..07d23ad2c556fa2654adbeda8365aa5f8bd54e16 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPopup.tsx @@ -1,44 +1,43 @@ -import { IPointData } from 'pixi.js'; -import { NodeType } from '../types'; +import React from 'react'; +import { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } from '@graphpolaris/shared/lib/components/tooltip'; -export type NodelinkPopupProps = { - data: { node: NodeType; pos: IPointData }; - onClose: () => void; +const formatNumber = (number: number) => { + return number.toLocaleString('de-DE'); }; -export const NLPopup = (props: NodelinkPopupProps) => { - const node = props.data.node; +export type NLPopUpProps = { + data: Record<string, any>; +}; +export const NLPopUp: React.FC<NLPopUpProps> = ({ data }) => { return ( - <div - className="text-[0.9rem] min-w-[10rem]" - > - <div className="card-body p-0"> - <span className="px-2.5 pt-2"> - <span>Node</span> - <span className="float-right">{node._id}</span> - </span> - <div className="h-[1px] w-full bg-secondary-200"></div> - <div className="px-2.5 text-[0.8rem]"> - {node.attributes && - Object.entries(node.attributes).map(([k, v], i) => { - return ( - <div key={k} className="flex flex-row gap-3"> - <span className="">{k}: </span> - <span className="ml-auto max-w-[10rem] text-right truncate"> - <span title={JSON.stringify(v)}>{JSON.stringify(v)}</span> - </span> - </div> - ); - })} - {node.cluster && ( - <p> - Cluster: <span className="float-right">{node.cluster}</span> - </p> - )} - </div> - <div className="h-[1px] w-full"></div> + <TooltipProvider delayDuration={100}> + <div className={`px-2`}> + {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], index) => ( + <div className="flex flex-row gap-1 items-center min-h-5" key={k}> + <span className={`font-semibold truncate min-w-[40%]`}>{k}</span> + <span className="ml-auto text-right truncate grow-1 flex items-center"> + {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 border-[1px] solid border-gray`} + style={{ + background: + 'repeating-linear-gradient(-45deg, transparent, transparent 6px, #eaeaea 6px, #eaeaea 8px), linear-gradient(to bottom, transparent, transparent)', + }} + ></div> + )} + </span> + </div> + )) + )} </div> - </div> + </TooltipProvider> ); };