diff --git a/libs/shared/lib/components/buttons/index.tsx b/libs/shared/lib/components/buttons/index.tsx index 04c2fad2c66a4d0d39d0da2d2408f8cae4143092..cc5a3ae449607b7c2fe1d5a7ef0d481838ba3ff4 100644 --- a/libs/shared/lib/components/buttons/index.tsx +++ b/libs/shared/lib/components/buttons/index.tsx @@ -1,4 +1,4 @@ -import React, { ReactElement } from 'react'; +import React, { ReactElement, useMemo } from 'react'; import styles from './buttons.module.scss'; import Icon, { Sizes } from '../icon'; import { forwardRef } from 'react'; @@ -41,70 +41,75 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT }, forwardRef, ) => { - let typeClass = ''; - let variantClass = ''; - let sizeClass = ''; - const blockClass = block ? styles['btn-block'] : ''; - const roundedClass = rounded ? styles['btn-rounded'] : ''; + let typeClass = useMemo(() => { + switch (type) { + case 'primary': + return styles['btn-primary']; + case 'secondary': + return styles['btn-secondary']; + case 'danger': + return styles['btn-danger']; + default: + return styles['btn-secondary']; + } + }, [type]); - switch (type) { - case 'primary': - typeClass = styles['btn-primary']; - break; - case 'secondary': - typeClass = styles['btn-secondary']; - break; - case 'danger': - typeClass = styles['btn-danger']; - break; - default: - return null; - } + let variantClass = useMemo(() => { + switch (variant) { + case 'solid': + return styles['btn-solid']; + case 'outline': + return styles['btn-outline']; + case 'ghost': + return styles['btn-ghost']; + default: + return styles['btn-solid']; + } + }, [variant]); - switch (variant) { - case 'solid': - variantClass = styles['btn-solid']; - break; - case 'outline': - variantClass = styles['btn-outline']; - break; - case 'ghost': - variantClass = styles['btn-ghost']; - break; - default: - return null; - } + let sizeClass = useMemo(() => { + switch (size) { + case '2xs': + return styles['btn-2xs']; + case 'xs': + return styles['btn-xs']; + case 'sm': + return styles['btn-sm']; + case 'md': + return styles['btn-md']; + case 'lg': + return styles['btn-lg']; + default: + return styles['btn-md']; + } + }, [size]); - let iconSize: Sizes = 24; + const iconSize = useMemo(() => { + switch (size) { + case '2xs': + return 12; + case 'xs': + return 16; + case 'sm': + return 20; + case 'md': + return 24; + case 'lg': + return 28; + default: + return 24; + } + }, [size]); - switch (size) { - case '2xs': - sizeClass = styles['btn-2xs']; - iconSize = 12; - break; - case 'xs': - sizeClass = styles['btn-xs']; - iconSize = 16; - break; - case 'sm': - sizeClass = styles['btn-sm']; - iconSize = 20; - break; - case 'md': - sizeClass = styles['btn-md']; - iconSize = 24; - break; - case 'lg': - sizeClass = styles['btn-lg']; - iconSize = 28; - break; - default: - return null; - } + const blockClass = useMemo(() => (block ? styles['btn-block'] : ''), [block, styles]); + const roundedClass = useMemo(() => (rounded ? styles['btn-rounded'] : ''), [rounded, styles]); - const icon = iconComponent ? <Icon component={iconComponent} size={iconSize} /> : null; + const icon = useMemo(() => (iconComponent ? <Icon component={iconComponent} size={iconSize} /> : null), [iconComponent, iconSize]); - const iconOnlyClass = iconComponent && !label && !children ? styles['btn-icon-only'] : ''; + const iconOnlyClass = useMemo( + () => (iconComponent && !label && !children ? styles['btn-icon-only'] : ''), + [iconComponent, label, children], + ); if (notAButton) return ( diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx index fdd6fc002dd9b14c5a796db21598460848eb12bd..27729ccefd500d9c55fb96435583b9038e24bc41 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryBuilderLogicPillsPanel.tsx @@ -12,6 +12,7 @@ import { ConnectingNodeDataI } from '../utils/connectorDrop'; import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; import { toQuerybuilderGraphology, setQuerybuilderGraphology } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; import { useDispatch } from 'react-redux'; +import { Button } from '../../..'; export const QueryBuilderLogicPillsPanel = (props: { reactFlowWrapper: HTMLDivElement | null; @@ -117,43 +118,43 @@ export const QueryBuilderLogicPillsPanel = (props: { return ( <div className={props.className + ' card'}> {props.title && <h1 className="card-title mb-7">{props.title}</h1>} - <div className="btn-group w-full justify-center"> + <div className="gap-1 flex gap-1"> {dataOps.map((item, index) => ( <div key={item.title} data-tip={item.description} className="tooltip tooltip-top m-0 p-0"> - <button - className={'btn btn-sm ' + (selectedOp === index ? 'btn-active' : '')} + <Button + iconComponent={item.icon} + size="sm" + variant={selectedOp === index ? 'solid' : 'outline'} onClick={(e) => { e.preventDefault(); index === selectedOp ? setSelectedOp(-1) : setSelectedOp(index); }} - > - {item.icon} - </button> + ></Button> </div> ))} <div className="w-2" /> {dataTypes.map((item, index) => ( - <div key={item.title} data-tip={item.description} className=" tooltip tooltip-top m-0 p-0"> - <button - className={'btn btn-sm block ' + (selectedType === index ? 'btn-active' : '')} + <div key={item.title} data-tip={item.description} className="tooltip tooltip-top m-0 p-0"> + <Button + iconComponent={item.icon} + size="sm" + variant={selectedType === index ? 'solid' : 'outline'} onClick={(e) => { e.preventDefault(); index === selectedType ? setSelectedType(-1) : setSelectedType(index); }} - > - {item.icon} - </button> + ></Button> </div> ))} </div> <div className="overflow-x-hidden h-[75rem] w-full mt-1"> - <ul className="menu p-0 [&_li>*]:rounded-none w-full pb-10 h-full"> + <ul className="menu p-0 [&_li>*]:rounded-none w-full pb-10 h-full gap-1"> {Object.values(AllLogicMap) .filter((item) => !filterType || item.key.toLowerCase().includes(filterType)) .filter((item) => selectedOp === -1 || item.key.toLowerCase().includes(dataOps?.[selectedOp].title)) .filter((item) => selectedType === -1 || item.key.toLowerCase().includes(dataTypes?.[selectedType].title)) .map((item, index) => ( - <li key={item.key + item.description} className="h-fit bg-secondary-200 "> + <li key={item.key + item.description} className="h-fit bg-white border-[1px] border-secondary-500 rounded-sm"> <span data-tip={item.description} className="flex before:w-[10rem] before:text-center tooltip tooltip-bottom text-start " diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx index 43ac8efd166f8b94af3f045581ecc8b4810b8646..14075769e33a768ab247802ac800839efab9a83d 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/QueryEntityPill.tsx @@ -1,6 +1,6 @@ import { useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; import React, { useMemo, useState } from 'react'; -import { Handle, Position } from 'reactflow'; +import { Handle, Position, useUpdateNodeInternals } from 'reactflow'; import { NodeAttribute, SchemaReactflowEntityNode, toHandleId } from '../../../model'; import { PillDropdown } from '../../pilldropdown/PillDropdown'; import { EntityPill } from '@graphpolaris/shared/lib/components'; @@ -10,6 +10,8 @@ import { EntityPill } from '@graphpolaris/shared/lib/components'; * @param {NodeProps} param0 The data of an entity flow element. */ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { + const updateNodeInternals = useUpdateNodeInternals(); + const data = node.data; if (!data.leftRelationHandleId) throw new Error('EntityFlowElement: data.leftRelationHandleId is undefined'); if (!data.rightRelationHandleId) throw new Error('EntityFlowElement: data.rightRelationHandleId is undefined'); @@ -25,10 +27,16 @@ export const QueryEntityPill = React.memo((node: SchemaReactflowEntityNode) => { const onMouseEnter = (event: React.MouseEvent) => { if (!hovered) setHovered(true); + setTimeout(() => { + updateNodeInternals(node.id); + }, 100); }; const onMouseLeave = (event: React.MouseEvent) => { if (hovered) setHovered(false); + setTimeout(() => { + updateNodeInternals(node.id); + }, 100); }; const onHandleMouseDown = (attribute: NodeAttribute, i: number, event: React.MouseEvent) => { diff --git a/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx b/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx index e5133ca74d067517f6a54549c6c2b52aaa3efdfb..4e8af9fdf6748c61e7c3e75c8329b4f213b7c4b2 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx @@ -17,6 +17,7 @@ export type TableProps = { itemsPerPage: number; showBarPlot: boolean; showAttributes: string[]; + selectedEntity: string; }; type Data2RenderI = { name: string; @@ -28,11 +29,10 @@ type Data2RenderI = { const THRESHOLD_WIDTH = 100; -export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: TableProps) => { +export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes, selectedEntity }: TableProps) => { const maxUniqueValues = 29; const barPlotNumBins = 10; const fetchAttributes = 0; - const [sortedData, setSortedData] = useState<AugmentedNodeAttributes[]>(data); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc'); const [sortColumn, setSortColumn] = useState<string | null>(null); @@ -48,7 +48,7 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: Table return showAttributes.filter((attr) => Object.keys(data[0].attribute).includes(attr)); } return Object.keys(data[0].attribute); - }, [data, showAttributes]); + }, [data, showAttributes, selectedEntity]); const totalPages = Math.ceil(sortedData.length / itemsPerPage); const [columnWidths, setColumnWidths] = useState<number[]>([]); @@ -65,7 +65,6 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: Table return 0; }); setColumnWidths(widths); - console.log(widths); }, 100); return () => clearTimeout(timeoutId); @@ -85,7 +84,7 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: Table }, [sortOrder, data, sortColumn]); useEffect(() => { - onPageChange(1); // Reset to the first page when sorting or itemsPerPage changes + onPageChange(1); }, [sortColumn, sortOrder, itemsPerPage]); const onPageChange = (page: number) => { @@ -121,6 +120,20 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: Table } }; + useEffect(() => { + setSortedData(data); + setCurrentPage({ + page: 1, + startIndex: 0, + endIndex: itemsPerPage, + currentData: data.slice(0, itemsPerPage), + }); + + // Reset sorting state + setSortColumn(null); + setSortOrder('asc'); + }, [data, itemsPerPage]); + // Barplot on headers data preparation // Data structure to feed the keys-barplot ( name data2Render ) @@ -211,11 +224,12 @@ export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: Table newData2Render.numElements = categoryCounts.length; newData2Render.showBarPlot = false; } + return newData2Render; }); setData2Render(_data2Render); - }, [currentPage, data, sortedData]); + }, [currentPage, data, sortedData, selectedEntity]); return ( <> diff --git a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx index 552b40db3dfefc778ff6a725ee335cc27d0fa2ee..2744b0be651a0e51079ab173a26a7bb455ce2604 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx @@ -1,4 +1,4 @@ -import React, { useMemo, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Table, AugmentedNodeAttributes } from './components/Table'; import { SchemaAttribute } from '../../../schema'; import { VisualizationPropTypes, VISComponentType } from '../../common'; @@ -10,12 +10,14 @@ export type TableProps = { showBarplot: boolean; itemsPerPage: number; displayAttributes: string[]; + displayEntity: string; }; const configuration: TableProps = { itemsPerPage: 10, - showBarplot: false, + showBarplot: true, displayAttributes: [], + displayEntity: '', }; export const TableVis = ({ data, schema, configuration }: VisualizationPropTypes) => { @@ -23,18 +25,20 @@ export const TableVis = ({ data, schema, configuration }: VisualizationPropTypes const attributesArray = useMemo<AugmentedNodeAttributes[]>( () => - data.nodes.map((node) => { - const types: SchemaAttribute[] = - schema.nodes.find((n) => n.key === node.label)?.attributes?.attributes ?? - schema.edges.find((r) => r.key === node.label)?.attributes?.attributes ?? - []; + data.nodes + .filter((node) => node.label === configuration.displayEntity) + .map((node) => { + const types: SchemaAttribute[] = + schema.nodes.find((n) => n.key === node.label)?.attributes?.attributes ?? + schema.edges.find((r) => r.key === node.label)?.attributes?.attributes ?? + []; - return { - attribute: node.attributes, - type: Object.fromEntries(types.map((t) => [t.name, t.type])), - }; - }), - [data.nodes], + return { + attribute: node.attributes, + type: Object.fromEntries(types.map((t) => [t.name, t.type])), + }; + }), + [data.nodes, configuration.displayEntity], ); return ( @@ -45,6 +49,7 @@ export const TableVis = ({ data, schema, configuration }: VisualizationPropTypes itemsPerPage={configuration.itemsPerPage} showBarPlot={configuration.showBarplot} showAttributes={configuration.displayAttributes} + selectedEntity={configuration.displayEntity} /> )} </div> @@ -60,28 +65,36 @@ const TableSettings = ({ graph: GraphMetaData; updateSettings: (val: any) => void; }) => { - const allAttributes: string[] = Object.keys(graph.nodes.types).reduce((acc: string[], label: string) => { - const labelAttributes = Object.keys(graph.nodes.types[label].attributes); - return acc.concat(labelAttributes); - }, []); + useEffect(() => { + if (graph && graph.nodes && graph.nodes.labels.length > 0) { + updateSettings({ displayEntity: graph.nodes.labels[0] }); + } + }, [graph]); - // Find the intersection of attributes across all nodes - const intersectionAttributes = allAttributes.filter((attr) => { - return Object.keys(graph.nodes.types).every((label) => { - return graph.nodes.types[label].attributes.hasOwnProperty(attr); - }); - }); + const selectedNodeAttributes = useMemo(() => { + if (configuration.displayEntity) { + const nodeType = graph.nodes.types[configuration.displayEntity]; + if (nodeType && nodeType.attributes) { + return Object.keys(nodeType.attributes); + } + } + return []; + }, [configuration.displayEntity, graph]); - const uniqueIntersectionAttributes = Array.from(new Set(intersectionAttributes)); + useEffect(() => { + if (graph && graph.nodes && graph.nodes.labels.length > 0) { + updateSettings({ displayAttributes: selectedNodeAttributes }); + } + }, [selectedNodeAttributes]); return ( <SettingsContainer> <Input type="dropdown" - label="Items per page" - value={configuration.itemsPerPage} - onChange={(val) => updateSettings({ itemsPerPage: val })} - options={[10, 25, 50, 100]} + label="Select entity" + value={configuration.displayEntity} + onChange={(val) => updateSettings({ displayEntity: val })} + options={graph.nodes.labels} /> <Input type="boolean" @@ -89,14 +102,20 @@ const TableSettings = ({ value={configuration.showBarplot} onChange={(val) => updateSettings({ showBarplot: val })} /> - + <Input + type="dropdown" + label="Items per page" + value={configuration.itemsPerPage} + onChange={(val) => updateSettings({ itemsPerPage: val })} + options={[10, 25, 50, 100]} + /> <div> <span className="text-sm">Attributes to display</span> <div className=""> <Input type="checkbox" value={configuration.displayAttributes} - options={uniqueIntersectionAttributes} + options={selectedNodeAttributes} onChange={(val: string[] | string) => { const updatedVal = Array.isArray(val) ? val : [val]; updateSettings({ displayAttributes: updatedVal });