diff --git a/apps/web/src/app/app.tsx b/apps/web/src/app/app.tsx index 882fb72c80da3f75972efd8f21e927be80bd147f..b8f1628d72539e632a172c7f9dfe3c840d1d04bc 100644 --- a/apps/web/src/app/app.tsx +++ b/apps/web/src/app/app.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useAppDispatch, useAuthorizationCache, @@ -13,16 +13,19 @@ import { Navbar } from '../components/navbar/navbar'; import { Resizable } from '@graphpolaris/shared/lib/components/Resizable'; import { DashboardAlerts } from '@graphpolaris/shared/lib/data-access/authorization/dashboardAlerts'; import { EventBus } from '@graphpolaris/shared/lib/data-access/api/eventBus'; -import Onboarding from '../components/onboarding/onboarding'; +import { Onboarding } from '../components/onboarding/onboarding'; import { wsQueryRequest } from '@graphpolaris/shared/lib/data-access/broker'; import { URLParams, setParam } from '@graphpolaris/shared/lib/data-access/api/url'; - -import { Schema } from '@graphpolaris/shared/lib/schema/panel'; import { VisualizationPanel } from '@graphpolaris/shared/lib/vis'; import { QueryBuilder } from '@graphpolaris/shared/lib/querybuilder'; -// const Schema = React.lazy(() => import('@graphpolaris/shared/lib/schema/panel')); -// const VisualizationPanel = React.lazy(() => import('@graphpolaris/shared/lib/vis')); -// const QueryBuilder = React.lazy(() => import('@graphpolaris/shared/lib/querybuilder')); +import { SideNavTab, Sidebar } from '@graphpolaris/shared/lib/sidebar'; +import { useVisualizationManager } from '@graphpolaris/shared/lib/vis/hooks'; +import { ConfigPanel } from '@graphpolaris/shared/lib/vis/components/config'; +import { Tooltip, TooltipTrigger, Button, TooltipContent, TooltipProvider } from '@graphpolaris/shared'; +import { ControlContainer } from '@graphpolaris/shared/lib/components/controls'; +import { Searchbar } from '@graphpolaris/shared/lib/sidebar/search/searchbar'; +import { Schema } from '@graphpolaris/shared/lib/schema/panel'; +import { Fullscreen, FitScreen, Remove, Schema as SchemaIcon, Search as SearchIcon } from '@mui/icons-material'; export type App = { load?: string; @@ -35,6 +38,7 @@ export function App(props: App) { const session = useSessionCache(); const dispatch = useAppDispatch(); const queryBuilderSettings = useQuerybuilderSettings(); + const manager = useVisualizationManager(); const runQuery = () => { if (session?.currentSaveState && query) { @@ -54,6 +58,8 @@ export function App(props: App) { }, [props]); const [authCheck, setAuthCheck] = useState(false); + const [tab, setTab] = useState<SideNavTab>('Schema'); + const [visFullSize, setVisFullSize] = useState<boolean>(false); return ( <div className="h-screen w-screen overflow-clip"> @@ -69,25 +75,74 @@ export function App(props: App) { <DashboardAlerts /> <div className={'h-screen w-screen ' + (!auth.authorized ? 'blur-sm pointer-events-none ' : '')}> <div className="flex flex-col h-screen max-h-screen relative"> - <aside className="h-auto w-auto"> + <aside className="absolute w-full h-12"> <Navbar /> </aside> - <main className="flex w-screen h-[calc(100%-4.2rem)]"> + <main className="grow flex flex-row h-screen pt-12"> + <Sidebar onTab={(tab) => setTab(tab)} /> <Resizable divisorSize={3} horizontal={true} defaultProportion={0.33}> - <div className="h-full w-full panel"> - <Schema auth={authCheck} /> - </div> - <div className="h-full w-full"> - <Resizable divisorSize={3} horizontal={false}> - <div className="w-full h-full panel"> - <VisualizationPanel /> + {tab !== undefined ? ( + <div className="flex flex-col border w-full h-full bg-light"> + <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="flex items-center"> + <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">{tab}</h1> + </div> + <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> + <ControlContainer> + <TooltipProvider delayDuration={100}> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Remove />} onClick={() => {}} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Hide</p> + </TooltipContent> + </Tooltip> + {tab === 'Schema' && ( + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<FitScreen />} onClick={() => {}} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Fit to screen</p> + </TooltipContent> + </Tooltip> + )} + {tab === 'Search' && ( + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={() => {}} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Mock icon</p> + </TooltipContent> + </Tooltip> + )} + </TooltipProvider> + </ControlContainer> + </div> </div> - <div className="w-full h-full panel"> - <QueryBuilder onRunQuery={runQuery} /> - </div> - </Resizable> - </div> + {tab === 'Search' && <Searchbar />} + {tab === 'Schema' && <Schema auth={authCheck} />} + </div> + ) : null} + + <Resizable divisorSize={3} horizontal={false}> + <VisualizationPanel + manager={manager} + fullSize={() => { + setVisFullSize(!visFullSize); + tab === undefined && setTab('Schema'); + tab !== undefined && setTab(undefined); + }} + /> + + <QueryBuilder onRunQuery={runQuery} /> + </Resizable> </Resizable> + <div className="info-panel flex h-full w-60 ml-[3px] shrink-0 overflow-auto bg-light border"> + <ConfigPanel manager={manager} /> + </div> </main> </div> </div> diff --git a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx index 0a68e06a10a9e1243440fef80c8fce7a5924fe68..6ed8758c79b6eda3bf540d77777189015deb367f 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/dbConnectionSelector.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import { Add, Delete, Settings } from '@mui/icons-material'; +import { Add, Delete, Settings, StorageOutlined } from '@mui/icons-material'; import { useAppDispatch, useSchemaGraph, useSessionCache, useAuthorizationCache } from '@graphpolaris/shared/lib/data-access'; import { deleteSaveState, selectSaveState } from '@graphpolaris/shared/lib/data-access/store/sessionSlice'; import { SettingsForm } from './forms/settings'; @@ -10,6 +10,7 @@ import { clearQB } from '@graphpolaris/shared/lib/data-access/store/querybuilder import { clearSchema } from '@graphpolaris/shared/lib/data-access/store/schemaSlice'; import { DatabaseStatus, SaveStateI, nilUUID, wsDeleteState } from '@graphpolaris/shared/lib/data-access/broker'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; +import { Icon } from '@graphpolaris/shared'; export default function DatabaseSelector({}) { const dispatch = useAppDispatch(); @@ -85,8 +86,9 @@ export default function DatabaseSelector({}) { }} /> )} - <DropdownContainer ref={dbSelectionMenuRef} className="w-[20rem]"> + <DropdownContainer ref={dbSelectionMenuRef} className="w-[18rem]"> <DropdownButton + size="md" disabled={connecting || authCache.authorized === false || !!authCache.roomID} title={ <div className="flex items-center"> @@ -96,14 +98,17 @@ export default function DatabaseSelector({}) { <p className="ml-2 truncate">Connecting to {session.saveStates[session.currentSaveState].name}</p> </> ) : session.currentSaveState && session.currentSaveState in session.saveStates && session.currentSaveState !== nilUUID ? ( - <> - <div - className={`h-2 w-2 rounded-full ${ - session.testedSaveState[session.currentSaveState] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500' - }`} - /> - <p className="ml-2 truncate">Connected DB: {session.saveStates[session.currentSaveState].name}</p> - </> + <div className="flex items-center"> + <div className="relative self-center"> + <Icon component={<StorageOutlined />} size={20} /> + + <div + className={`absolute bottom-0 left-0 h-2 w-2 border border-light rounded-full ${session.testedSaveState[session.currentSaveState] === DatabaseStatus.tested ? 'bg-success-500' : 'bg-danger-500'}`} + /> + </div> + + <p className="ml-2 truncate">{session.saveStates[session.currentSaveState].name}</p> + </div> ) : session.saveStates === undefined ? ( <> <LoadingSpinner /> @@ -128,7 +133,7 @@ export default function DatabaseSelector({}) { /> {dbSelectionMenuOpen && session.saveStates !== undefined && ( - <DropdownItemContainer align="top-10 w-full"> + <DropdownItemContainer align="top-10 w-full z-30"> <li className="flex items-center p-2 hover:bg-secondary-50 cursor-pointer" onClick={(e) => { @@ -210,7 +215,7 @@ export default function DatabaseSelector({}) { <TooltipTrigger> <Settings /> </TooltipTrigger> - <TooltipContent side={'bottom'}> + <TooltipContent side={'top'}> <p>Change the connection details</p> </TooltipContent> </Tooltip> @@ -233,7 +238,7 @@ export default function DatabaseSelector({}) { <TooltipTrigger> <Delete /> </TooltipTrigger> - <TooltipContent side={'bottom'}> + <TooltipContent side={'top'}> <p>Delete the database</p> </TooltipContent> </Tooltip> diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx index 4eeba3be4be2c36cf016879a3bd9191996086186..db78c47401a7861c44ec01af0120bd69174bc22d 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/databaseForm.tsx @@ -7,7 +7,7 @@ import { databaseProtocolMapping, nilUUID, } from '@graphpolaris/shared/lib/data-access'; -import Input from '@graphpolaris/shared/lib/components/inputs'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; import { useImmer } from 'use-immer'; import { initialState as qbInitialState } from '@graphpolaris/shared/lib/data-access/store/querybuilderSlice'; diff --git a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx index eef51b9ed6db2de71a7e463ce97a1a9cd4feca93..233899f095f55b60ee2ab7c426d041f499910a6d 100644 --- a/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx +++ b/apps/web/src/components/navbar/DatabaseManagement/forms/settings.tsx @@ -114,7 +114,7 @@ export const SettingsForm = (props: { onClose(): void; open: 'add' | 'update'; s return ( <Dialog open={!!props.open} onClose={props.onClose} className="lg:min-w-[50rem]"> <div className="flex justify-between align-center"> - <h1 className="text-xl font-bold">{formTitle} Database Connection</h1> + <h2 className="text-xl font-bold">{formTitle} Database Connection</h2> <div> {sampleDataPanel === true ? ( <Button variant="outline" label="Go back" onClick={() => setSampleDataPanel(false)} /> diff --git a/apps/web/src/components/navbar/databasemenu.tsx b/apps/web/src/components/navbar/databasemenu.tsx deleted file mode 100644 index 15a2e2f18c995b282869d9c37dd58797c7035b7e..0000000000000000000000000000000000000000 --- a/apps/web/src/components/navbar/databasemenu.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from 'react'; -import { SaveStateI, useSessionCache } from '@graphpolaris/shared/lib/data-access'; - -export const DatabaseMenu = (props: { onClick: (database: string) => void }) => { - const session = useSessionCache(); - - return ( - <ul className="menu dropdown-content absolute right-48 z-[1] p-2 shadow-xl bg-secondary-50 rounded-box w-52" tabIndex={0}> - {session.saveStates && - Object.values(session.saveStates).map((ss: SaveStateI) => ( - <li key={ss.name}> - <button onClick={() => props.onClick(ss.name)}>{ss.name}</button> - </li> - ))} - </ul> - ); -}; diff --git a/apps/web/src/components/navbar/navbar.tsx b/apps/web/src/components/navbar/navbar.tsx index 92c7a548dbb5dfc3f36d743943c36eeea110909e..f9c975f77cfa4502628b185ebfd94f72727a0b5c 100644 --- a/apps/web/src/components/navbar/navbar.tsx +++ b/apps/web/src/components/navbar/navbar.tsx @@ -12,10 +12,8 @@ import React, { useState, useRef, useEffect } from 'react'; import logo_white from './gp-logo-white.svg'; import logo from './gp-logo.svg'; import { useAuthorizationCache, useAuth } from '@graphpolaris/shared/lib/data-access'; -import { SearchBar } from './search/SearchBar'; import DatabaseSelector from './DatabaseManagement/dbConnectionSelector'; import { DropdownItem, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; -import ColorMode from '@graphpolaris/shared/lib/components/color-mode'; import GpLogo from './gp-logo'; export const Navbar = () => { @@ -24,8 +22,6 @@ export const Navbar = () => { const authCache = useAuthorizationCache(); const [menuOpen, setMenuOpen] = useState(false); - const currentLogo = !'dark' ? logo_white : logo; // TODO: support dark mode - useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { @@ -41,75 +37,65 @@ export const Navbar = () => { const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; return ( - <div className="w-full h-auto px-5"> - <div className="navbar flex items-center justify-between w-auto gap-2 xl:gap-10"> - <a href="https://graphpolaris.com/" target="_blank" className="w-[10rem] md:w-fit shrink-0 text-dark"> - <GpLogo className="w-48" /> - </a> - <DatabaseSelector /> + <nav className="w-full px-4 h-12 flex flex-row items-center gap-2 md:gap-3 lg:gap-4"> + <a href="https://graphpolaris.com/" target="_blank" className="shrink-0 text-dark"> + <GpLogo className="h-7" /> + </a> + <DatabaseSelector /> - <div> - <SearchBar /> - <ColorMode /> - <div className="w-fit" ref={dropdownRef}> - <div - className="relative inline-flex items-center justify-center w-8 h-8 overflow-hidden bg-secondary-500 rounded hover:bg-secondary-600 transition-colors duration-150 ease-in-out cursor-pointer" - onClick={() => setMenuOpen(!menuOpen)} - > - <span className="font-medium text-light">{authCache.username?.slice(0, 2).toUpperCase()}</span> - </div> + <div className="ml-auto"> + <div className="w-fit" ref={dropdownRef}> + <div + className="relative inline-flex items-center justify-center w-8 h-8 overflow-hidden bg-secondary-500 rounded-full hover:bg-secondary-600 transition-colors duration-150 ease-in-out cursor-pointer" + onClick={() => setMenuOpen(!menuOpen)} + > + <span className="font-medium text-light">{authCache.username?.slice(0, 2).toUpperCase()}</span> + </div> - {menuOpen && ( - <DropdownItemContainer className="w-56" align="right-7"> - <div className="menu-title border-b"> - <h2>user: {authCache.username}</h2> - <h3 className="text-xs break-words">session: {authCache.sessionID}</h3> - </div> + {menuOpen && ( + <DropdownItemContainer className="w-56 z-30" align="right-3"> + <div className="p-2 text-sm border-b"> + <h2 className="font-bold">user: {authCache.username}</h2> + <h3 className="text-xs break-words">session: {authCache.sessionID}</h3> + </div> - {authCache.authorized ? ( - <> - <DropdownItem - value="Share" - onClick={() => { - auth.newShareRoom(); - }} - // submenu={ - // <> - // <DropdownItem value="Visual" onClick={() => {}} /> - // <DropdownItem value="Knowledge base" onClick={() => {}} /> - // </> - // } - /> - <DropdownItem - value="Advanced" - submenu={ - <> - <DropdownItem value="TBD" onClick={() => {}} /> - </> - } - /> - <DropdownItem value="Settings" onClick={() => {}} /> - <DropdownItem value="Log out" onClick={() => {}} /> - </> - ) : ( - <> - <DropdownItem value="Login" onClick={() => {}} /> - </> - )} + {authCache.authorized ? ( + <> + <DropdownItem + value="Share" + onClick={() => { + auth.newShareRoom(); + }} + /> + <DropdownItem + value="Advanced" + submenu={ + <> + <DropdownItem value="TBD" onClick={() => {}} /> + </> + } + /> + <DropdownItem value="Settings" onClick={() => {}} /> + <DropdownItem value="Log out" onClick={() => {}} /> + </> + ) : ( + <> + <DropdownItem value="Login" onClick={() => {}} /> + </> + )} - {authCache?.roomID && ( - <div className="menu-title border-b"> - <h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3> - </div> - )} - <div className="menu-title border-t"> - <h3 className="text-xs">Version: {buildInfo}</h3> + {authCache?.roomID && ( + <div className="p-2 border-b"> + <h3 className="text-xs break-words">Share ID: {authCache.roomID}</h3> </div> - </DropdownItemContainer> - )} - </div> + )} + <div className="p-2 border-t"> + <h3 className="text-xs">Version: {buildInfo}</h3> + </div> + </DropdownItemContainer> + )} </div> </div> - </div> + </nav> ); }; diff --git a/apps/web/src/components/navbar/search/SearchBar.tsx b/apps/web/src/components/navbar/search/SearchBar.tsx deleted file mode 100644 index e5182032b701ec9b80b743a7ddb38c3675bbed6c..0000000000000000000000000000000000000000 --- a/apps/web/src/components/navbar/search/SearchBar.tsx +++ /dev/null @@ -1,260 +0,0 @@ -import React from 'react'; -import { - useAppDispatch, - useGraphQueryResult, - useSchemaGraph, - useQuerybuilderGraph, - useSearchResult, - AppDispatch, - useRecentSearches, -} from '@graphpolaris/shared/lib/data-access'; -import { filterData } from './similarity'; -import { Search as SearchIcon } from '@mui/icons-material'; -import { - addSearchResultData, - addSearchResultSchema, - addSearchResultQueryBuilder, - CATEGORY_KEYS, - addRecentSearch, -} from '@graphpolaris/shared/lib/data-access/store/searchResultSlice'; -import { QueryMultiGraph } from '@graphpolaris/shared/lib/querybuilder/model/graphology/utils'; - -const SIMILARITY_THRESHOLD = 0.7; - -const CATEGORY_ACTIONS: { - [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void; -} = { - data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { - dispatch(addSearchResultData(payload)); - }, - schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { - dispatch(addSearchResultSchema(payload)); - }, - querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { - dispatch(addSearchResultQueryBuilder(payload)); - }, -}; - -const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[]; - -export function SearchBar({}) { - const inputRef = React.useRef<HTMLInputElement>(null); - const searchbarRef = React.useRef<HTMLDivElement>(null); - const dispatch = useAppDispatch(); - const results = useSearchResult(); - const recentSearches = useRecentSearches(); - const schema = useSchemaGraph(); - const graphData = useGraphQueryResult(); - const querybuilderData = useQuerybuilderGraph(); - const [search, setSearch] = React.useState<string>(''); - const [searchOpen, setSearchOpen] = React.useState<boolean>(false); - - const dataSources: { - [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; - } = { - data: graphData, - schema: schema, - querybuilder: querybuilderData as QueryMultiGraph, - }; - - const toggleSearch = () => { - setSearchOpen(true); - if (!searchOpen && inputRef.current) { - inputRef.current.focus(); - } - }; - - React.useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { - if (event.key === 'Enter') { - if (searchOpen && search !== '') { - dispatch(addRecentSearch(search)); - } - } - }; - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [searchOpen, search]); - - React.useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { - if (event.key === '/') { - if (!searchOpen) { - setSearchOpen(true); - setSearch(''); - } - } - }; - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [searchOpen]); - - React.useEffect(() => { - if (searchOpen && inputRef.current) { - inputRef.current.focus(); - setSearch(''); - } - }, [searchOpen]); - - React.useEffect(() => { - const handleKeyPress = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - if (searchOpen) { - setSearchOpen(false); - setSearch(''); - } - } - }; - window.addEventListener('keydown', handleKeyPress); - return () => window.removeEventListener('keydown', handleKeyPress); - }, [searchOpen]); - - React.useEffect(() => { - handleSearch(); - }, [search]); - - const handleSearch = () => { - let query = search.toLowerCase(); - const categories = search.match(/@[^ ]+/g); - - if (categories) { - categories.map((category) => { - query = query.replace(category, '').trim(); - const cat = category.substring(1); - - if (cat in CATEGORY_ACTIONS) { - const categoryAction = CATEGORY_ACTIONS[cat as CATEGORY_KEYS]; - const data = dataSources[cat]; - - const payload = { - nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), - edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), - }; - categoryAction(payload, dispatch); - } - }); - } else { - for (const category of SEARCH_CATEGORIES) { - const categoryAction = CATEGORY_ACTIONS[category]; - const data = dataSources[category]; - - const payload = { - nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), - edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), - }; - - categoryAction(payload, dispatch); - } - } - }; - - React.useEffect(() => { - const handleClickOutside = ({ target }: MouseEvent) => { - if (inputRef.current && target && !inputRef.current.contains(target as Node) && !searchbarRef?.current?.contains(target as Node)) { - setSearch(''); - setSearchOpen(false); - } - }; - document.addEventListener('click', handleClickOutside); - return () => { - document.removeEventListener('click', handleClickOutside); - }; - }, []); - - return ( - <div className="searchbar"> - {searchOpen && <div className="fixed inset-0 bg-black bg-opacity-50 z-40"></div>} - <div className="mr-2" ref={searchbarRef}> - <div - className="flex items-center border border-secondary-300 hover:bg-secondary-50 px-2 rounded text-sm w-44 h-8 text-secondary-900 cursor-pointer" - onClick={toggleSearch} - > - <SearchIcon /> - <span className="ml-1 text-secondary-900"> - Type <span className="border border-secondary-900 rounded px-1">/</span> to search - </span> - </div> - - {searchOpen && ( - <div className="fixed flex flex-col left-1/2 -translate-x-1/2 w-9/12 max-h-1/2 top-2 items-start justify-center z-50 bg-secondary-200 rounded"> - <div className="p-3 w-full"> - {/* <label className="sr-only">Search</label> */} - <div className="relative"> - <div className="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none"> - <SearchIcon className="text-secondary500" /> - </div> - <input - type="text" - ref={inputRef} - value={search} - onChange={(e) => setSearch(e.target.value)} - id="input-group-search" - className="block w-full p-2 ps-10 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0" - placeholder="Search database" - ></input> - </div> - </div> - - {recentSearches.length !== 0 && ( - <div className="px-3 pb-3"> - <p className="text-sm">Recent searches</p> - {recentSearches.slice(0, 3).map((term) => ( - <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}> - {term} - </p> - ))} - </div> - )} - {search !== '' && ( - <div className="z-10 rounded card-bordered w-full overflow-auto max-h-[60vh] px-3 pb-3"> - <p className="font-bold text-sm">Results</p> - {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( - <div className="ml-1 text-sm"> - <p className="text-secondary-500">Found no matches...</p> - </div> - ) : ( - SEARCH_CATEGORIES.map((category, index) => { - if (results[category].nodes.length > 0 || results[category].edges.length > 0) { - return ( - <div key={index}> - <div className="flex justify-between p-2 text-lg"> - <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p> - <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> - </div> - <div className="h-[1px] w-full bg-secondary-200"></div> - {Object.values(Object.values(results[category])) - .flat() - .map((item, index) => ( - <div - key={index} - className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2" - title={JSON.stringify(item)} - onClick={() => { - CATEGORY_ACTIONS[category]( - { - nodes: results[category].nodes.includes(item) ? [item] : [], - edges: results[category].edges.includes(item) ? [item] : [], - }, - dispatch, - ); - }} - > - <div className="font-bold text-sm"> - {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} - </div> - <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> - </div> - ))} - </div> - ); - } else return <></>; - }) - )} - </div> - )} - </div> - )} - </div> - </div> - ); -} diff --git a/apps/web/src/components/onboarding/onboarding.tsx b/apps/web/src/components/onboarding/onboarding.tsx index e7526d3f80e327dcb5fa7a81007c8d722aa6b670..d811f1df28fa83729e1d348cdaefaf8572d622a8 100644 --- a/apps/web/src/components/onboarding/onboarding.tsx +++ b/apps/web/src/components/onboarding/onboarding.tsx @@ -11,7 +11,7 @@ interface OnboardingState { stepIndex?: number; } -export default function Onboarding({}) { +export function Onboarding({}) { const location = useLocation(); const auth = useAuthorizationCache(); const [showWalkthrough, setShowWalkthrough] = useState<boolean>(false); diff --git a/apps/web/src/components/onboarding/use-cases/types.tsx b/apps/web/src/components/onboarding/use-cases/types.ts similarity index 100% rename from apps/web/src/components/onboarding/use-cases/types.tsx rename to apps/web/src/components/onboarding/use-cases/types.ts diff --git a/apps/web/src/main.css b/apps/web/src/main.css index 3f399b3f961b6ae85d44cc03a59abbb9448bb1d6..b05311668417118d4d3585830d420c70004e5f31 100644 --- a/apps/web/src/main.css +++ b/apps/web/src/main.css @@ -16,7 +16,7 @@ html * { html, body { - @apply font-sans bg-light text-dark; + @apply font-sans bg-secondary-50 text-dark; } .panel { diff --git a/libs/config/tailwind.config.js b/libs/config/tailwind.config.js index 05385a971cb17f52be606dcc02cf7c381d9b0aef..a6e1334dedafdf03c975f19f4e378c99fade6bc4 100644 --- a/libs/config/tailwind.config.js +++ b/libs/config/tailwind.config.js @@ -37,6 +37,9 @@ export default { mono: ['Roboto Mono', ...defaultTheme.fontFamily.mono], }, extend: { + borderColor: { + DEFAULT: 'hsl(var(--clr-sec--200) / <alpha-value>)', + }, colors: tailwindColors, animation: { openmenu: 'openmenu 0.3s ease-out', diff --git a/libs/shared/lib/components/Dialog.tsx b/libs/shared/lib/components/Dialog.tsx index bec6280cd5674986742a30ca014dcc77ff907aa8..75781e135187155ae552d737041d0b0aa9609768 100644 --- a/libs/shared/lib/components/Dialog.tsx +++ b/libs/shared/lib/components/Dialog.tsx @@ -16,13 +16,10 @@ export const Dialog = (props: DialogProps) => { }, [props.open]); return ( - <dialog className={"modal"} ref={ref} onClose={() => props.onClose()}> - <form method="dialog" className={"modal-box card flex gap-4 " + (props?.className ? props?.className : "")}> + <dialog className={'fixed inset-0 z-10 overflow-y-auto rounded p-4 bg-light border'} ref={ref} onClose={() => props.onClose()}> + <form method="dialog" className={'flex flex-col gap-4 ' + (props?.className ? props?.className : '')}> {props.children} </form> - <form method="dialog" className="modal-backdrop"> - <button>close</button> - </form> </dialog> ); }; diff --git a/libs/shared/lib/components/Resizable.tsx b/libs/shared/lib/components/Resizable.tsx index 522f011b6540fc395da6c4a8ed6f3de9d5603557..18069c8db366208ee0c2d3fcc035e607e07194b8 100644 --- a/libs/shared/lib/components/Resizable.tsx +++ b/libs/shared/lib/components/Resizable.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useState } from 'react'; type Props = { children: React.ReactNode; @@ -7,32 +7,59 @@ type Props = { horizontal: boolean; divisorSize: number; defaultProportion?: number; + classNameLeft?: string; + classNameRight?: string; }; function convertRemToPixels(rem: number) { return rem * parseFloat(getComputedStyle(document.documentElement).fontSize); } -export const Resizable = ({ children, className, style, horizontal, divisorSize, defaultProportion, ...props }: Props) => { +export const Resizable = ({ + children, + className, + style, + horizontal, + divisorSize, + defaultProportion, + classNameLeft, + classNameRight, + ...props +}: Props) => { const ref = useRef<HTMLDivElement>(null); const children2 = children as React.ReactElement[]; const [firstSize, setFirstSize] = React.useState<number>(0); const [secondSize, setSecondSize] = React.useState<number>(0); const [dragging, setDragging] = React.useState<boolean>(false); + const [windowSize, setWindowSize] = useState({ width: window.innerWidth, height: window.innerHeight }); + + useEffect(() => { + const handleResize = () => { + setWindowSize({ width: window.innerWidth, height: window.innerHeight }); + }; + + window.addEventListener('resize', handleResize); + + return () => { + window.removeEventListener('resize', handleResize); + }; + }, []); + + // Store the current proportion of the first area + const [currentProportion, setCurrentProportion] = useState(defaultProportion || 0.5); useEffect(() => { if (ref.current) { const rect = ref.current.getBoundingClientRect(); - const _defaultProportion = defaultProportion || 0.5; if (horizontal) { - setFirstSize(rect.width * _defaultProportion - divisorSize); - setSecondSize(rect.width * (1 / _defaultProportion) - divisorSize); + setFirstSize((rect.width - divisorSize) * currentProportion); + setSecondSize((rect.width - divisorSize) * (1 - currentProportion)); } else { - setFirstSize(rect.height * _defaultProportion - divisorSize); - setSecondSize(rect.height * (1 / _defaultProportion) - divisorSize); + setFirstSize((rect.height - divisorSize) * currentProportion); + setSecondSize((rect.height - divisorSize) * (1 - currentProportion)); } } - }, [ref.current]); + }, [ref.current, windowSize]); function onMouseDown(e: React.MouseEvent<HTMLDivElement, MouseEvent>) { setDragging(true); @@ -42,15 +69,27 @@ export const Resizable = ({ children, className, style, horizontal, divisorSize, setDragging(false); window.removeEventListener('mouseup', onMouseUp); }; + function onMouseMove(e: React.MouseEvent<HTMLDivElement, MouseEvent>) { if (dragging) { - if (horizontal && ref.current) { + if (ref.current) { const rect = ref.current.getBoundingClientRect(); - setFirstSize(e.clientX); - setSecondSize(rect.width - e.clientX); - } else { - setFirstSize(e.clientY); - setSecondSize(window.innerHeight - e.clientY); + const relativeX = e.clientX - rect.left; + const relativeY = e.clientY - rect.top; + const minSizeX = 16; + const minSizeY = 28; + + if (horizontal) { + const newFirstSize = Math.max(minSizeX, relativeX); + setFirstSize(newFirstSize); + setSecondSize(Math.max(minSizeX, rect.width - relativeX)); + setCurrentProportion(newFirstSize / rect.width); + } else { + const newFirstSize = Math.max(minSizeY, relativeY); + setFirstSize(newFirstSize); + setSecondSize(Math.max(minSizeY, rect.height - relativeY)); + setCurrentProportion(newFirstSize / rect.height); + } } } } @@ -65,17 +104,28 @@ export const Resizable = ({ children, className, style, horizontal, divisorSize, setDragging(false); } + if (!children2[0]) return children2[1]; + if (!children2[1]) return children2[0]; + return ( <> {dragging && <div className="absolute top-0 left-0 w-screen h-screen z-10 cursor-grabbing" onMouseMove={onMouseMove}></div>} - <div className={` w-full h-full flex ${horizontal ? 'flex-row' : 'flex-col'} ${className}`} style={style} {...props} ref={ref}> + <div + className={`w-full h-full flex ${horizontal ? 'flex-row' : 'flex-col'} ${className !== undefined ? className : ''} `} + style={style} + {...props} + ref={ref} + > {firstSize > 0 && ( <> - <div className="h-full w-full" style={horizontal ? { maxWidth: firstSize } : { maxHeight: firstSize }}> + <div + className={'h-full w-full' + (classNameLeft !== undefined ? classNameLeft : '')} + style={horizontal ? { maxWidth: firstSize } : { maxHeight: firstSize }} + > {children2[0]} </div> <div - className={' ' + (horizontal ? 'cursor-col-resize' : 'cursor-row-resize')} + className={' ' + (horizontal ? 'cursor-col-resize' : 'cursor-row-resize') + (dragging ? ' bg-primary-200' : '')} style={horizontal ? { minWidth: divisorSize } : { minHeight: divisorSize }} onMouseDown={onMouseDown} onTouchStart={onTouchStart} @@ -83,7 +133,10 @@ export const Resizable = ({ children, className, style, horizontal, divisorSize, onTouchEnd={onTouchEnd} onTouchCancel={onTouchCancel} ></div> - <div className="h-full w-full" style={horizontal ? { maxWidth: secondSize } : { maxHeight: secondSize }}> + <div + className={'h-full w-full' + (classNameRight !== undefined ? classNameRight : '')} + style={horizontal ? { maxWidth: secondSize } : { maxHeight: secondSize }} + > {children2[1]} </div> </> diff --git a/libs/shared/lib/components/buttons/buttons.module.scss b/libs/shared/lib/components/buttons/buttons.module.scss index eae142ad98aa19373590136969c78712dc33f0d8..ec502e7d748f127e8c7d347841d24e7338f1ca00 100644 --- a/libs/shared/lib/components/buttons/buttons.module.scss +++ b/libs/shared/lib/components/buttons/buttons.module.scss @@ -42,7 +42,9 @@ @apply cursor-not-allowed; } } - +.btn-icon-only { + line-height: 1; +} .btn-lg { @apply text-lg h-10 gap-1.5; &.btn-icon-only { @@ -67,6 +69,12 @@ @apply w-6; } } +.btn-2xs { + @apply text-2xs h-4 gap-0.5; + &.btn-icon-only { + @apply w-4 p-0; + } +} .btn-primary { --btn-color: var(--clr-pri--600); diff --git a/libs/shared/lib/components/buttons/buttons.module.scss.d.ts b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts index 090a2928f1558d1bcecbdbfd73ac9089eea3458f..83aafa7918952944b7108678579d269c74792d98 100644 --- a/libs/shared/lib/components/buttons/buttons.module.scss.d.ts +++ b/libs/shared/lib/components/buttons/buttons.module.scss.d.ts @@ -1,10 +1,11 @@ declare const classNames: { readonly btn: 'btn'; - readonly 'btn-lg': 'btn-lg'; readonly 'btn-icon-only': 'btn-icon-only'; + readonly 'btn-lg': 'btn-lg'; readonly 'btn-md': 'btn-md'; readonly 'btn-sm': 'btn-sm'; readonly 'btn-xs': 'btn-xs'; + readonly 'btn-2xs': 'btn-2xs'; readonly 'btn-primary': 'btn-primary'; readonly 'btn-secondary': 'btn-secondary'; readonly 'btn-danger': 'btn-danger'; diff --git a/libs/shared/lib/components/buttons/index.tsx b/libs/shared/lib/components/buttons/index.tsx index c98e59fda4a69d15b76c2ec9c3bd722ff901697b..04c2fad2c66a4d0d39d0da2d2408f8cae4143092 100644 --- a/libs/shared/lib/components/buttons/index.tsx +++ b/libs/shared/lib/components/buttons/index.tsx @@ -6,7 +6,7 @@ import { forwardRef } from 'react'; type ButtonProps = { type?: 'primary' | 'secondary' | 'danger'; variant?: 'solid' | 'outline' | 'ghost'; - size?: 'xs' | 'sm' | 'md' | 'lg'; + size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg'; label?: string; rounded?: boolean; disabled?: boolean; @@ -17,6 +17,7 @@ type ButtonProps = { ariaLabel?: string; children?: React.ReactNode; additionalClasses?: string; + notAButton?: boolean; }; export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HTMLAttributes<HTMLButtonElement>>( @@ -35,6 +36,7 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT ariaLabel, additionalClasses, children, + notAButton, ...props }, forwardRef, @@ -76,6 +78,10 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT let iconSize: Sizes = 24; switch (size) { + case '2xs': + sizeClass = styles['btn-2xs']; + iconSize = 12; + break; case 'xs': sizeClass = styles['btn-xs']; iconSize = 16; @@ -100,6 +106,14 @@ export const Button = React.forwardRef<HTMLButtonElement, ButtonProps & React.HT const iconOnlyClass = iconComponent && !label && !children ? styles['btn-icon-only'] : ''; + if (notAButton) + return ( + <div + className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${additionalClasses}`} + > + {icon} + </div> + ); return ( <button className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${additionalClasses}`} diff --git a/libs/shared/lib/components/color-mode/index.tsx b/libs/shared/lib/components/color-mode/index.tsx index 97abb3e04d28af79a1f8f3c03253638d57542a20..6b43428c1cd5d6e231f7f5b6d999e21f19f58968 100644 --- a/libs/shared/lib/components/color-mode/index.tsx +++ b/libs/shared/lib/components/color-mode/index.tsx @@ -45,9 +45,9 @@ const ColorMode = () => { <TooltipProvider delayDuration={0}> <Tooltip> <TooltipTrigger asChild> - <Button variant="ghost" iconComponent={iconComponent} onClick={toggleTheme} /> + <Button variant="ghost" size="sm" iconComponent={iconComponent} onClick={toggleTheme} /> </TooltipTrigger> - <TooltipContent side={'bottom'}> + <TooltipContent side={'right'}> <p>{`Switch to ${theme === 'dark-mode' ? 'light' : 'dark'}-mode`}</p> </TooltipContent> </Tooltip> diff --git a/libs/shared/lib/components/controls/index.tsx b/libs/shared/lib/components/controls/index.tsx index 92fc2cc4cf01d8e129b00c63ea672f92d01ff587..3660df5a1d67161217fd98ee9188d84302e2d516 100644 --- a/libs/shared/lib/components/controls/index.tsx +++ b/libs/shared/lib/components/controls/index.tsx @@ -4,6 +4,6 @@ type Props = { children: ReactNode; }; -export default function ControlContainer({ children }: Props) { +export function ControlContainer({ children }: Props) { return <div className="top-4 right-4 flex flex-row-reverse justify-between z-50">{children}</div>; } diff --git a/libs/shared/lib/components/dropdowns/dropdowns.module.scss b/libs/shared/lib/components/dropdowns/dropdowns.module.scss index a564d157f1e0764f25d045dabec6dced46c7f625..8d45e152009f106d2d55b5d8a0b69f5145db0a67 100644 --- a/libs/shared/lib/components/dropdowns/dropdowns.module.scss +++ b/libs/shared/lib/components/dropdowns/dropdowns.module.scss @@ -8,7 +8,7 @@ .dropdown-container { width: inherit; max-width: 100%; - @apply absolute z-10 mt-2 origin-top-right rounded-sm shadow-lg border border-secondary-200; + @apply absolute z-10 mt-2 origin-top-right rounded-sm shadow-lg border border-secondary-200 truncate; ul { //todo: color @apply divide-y; diff --git a/libs/shared/lib/components/dropdowns/index.tsx b/libs/shared/lib/components/dropdowns/index.tsx index 96ec000e594db2fc1ef5bf1c8010ce16d648545a..54cfd4bab505dd9aec5349544743709229393b67 100644 --- a/libs/shared/lib/components/dropdowns/index.tsx +++ b/libs/shared/lib/components/dropdowns/index.tsx @@ -1,5 +1,7 @@ import React, { useState, useEffect, useRef, ReactNode } from 'react'; import styles from './dropdowns.module.scss'; +import Icon from '../icon'; +import { ArrowDropDown } from '@mui/icons-material'; type DropdownContainerProps = { children: ReactNode; @@ -9,11 +11,11 @@ type DropdownContainerProps = { export const DropdownContainer = React.forwardRef<HTMLDivElement, DropdownContainerProps>( ({ children, className }, ref: React.ForwardedRef<HTMLDivElement>) => { return ( - <div className={`border-1 border-secondary-800 relative inline-block text-left ${className && className}`} ref={ref}> + <div className={`relative inline-block text-left ${className && className}`} ref={ref}> {children} </div> ); - } + }, ); type DropdownButtonProps = { @@ -24,21 +26,18 @@ type DropdownButtonProps = { }; export function DropdownButton({ title, onClick, size, disabled }: DropdownButtonProps) { + const paddingClass = size === 'xs' ? 'py-1' : size === 'sm' ? 'px-1 py-1' : size === 'md' ? 'px-2 py-1' : 'px-4 py-2'; + const textSizeClass = size === 'xs' ? 'text-xs' : size === 'sm' ? 'text-sm' : size === 'md' ? 'text-base' : 'text-lg'; + return ( <> <button - className="inline-flex w-full justify-between items-center gap-x-1.5 rounded bg-light px-3 py-2 text-secondary-900 shadow-sm ring-1 ring-inset ring-secondary-300 hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400" + className={`inline-flex border w-full justify-between items-center gap-x-1.5 rounded bg-light ${textSizeClass} ${paddingClass} text-secondary-900 shadow-sm hover:bg-secondary-50 disabled:bg-secondary-100 disabled:cursor-not-allowed disabled:text-secondary-400 pl-1 truncate`} onClick={onClick} disabled={disabled} > <span className={`text-${size}`}>{title}</span> - <svg className="-mr-1 h-5 w-5 text-secondary-400" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"> - <path - fillRule="evenodd" - d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z" - clipRule="evenodd" - /> - </svg> + <Icon component={<ArrowDropDown />} size={16} /> </button> </> ); diff --git a/libs/shared/lib/components/icon/icon.stories.tsx b/libs/shared/lib/components/icon/icon.stories.tsx index 1b506cf2e62db8feac3eedc1eaa03056f5d8aa2c..b57f7c09eb64b72801e5339ffbe0a3227294fe79 100644 --- a/libs/shared/lib/components/icon/icon.stories.tsx +++ b/libs/shared/lib/components/icon/icon.stories.tsx @@ -2,10 +2,9 @@ import { StoryObj, Meta } from '@storybook/react'; import Icon from '../icon'; import { ArrowBack, DeleteOutline, KeyboardArrowLeft, Settings } from '@mui/icons-material'; -export default { +const Component: Meta<typeof Icon> = { title: 'Components/Icon', component: Icon, - decorators: [(Story) => <div className="p-5">{Story()}</div>], argTypes: { component: { control: 'select', @@ -16,14 +15,14 @@ export default { options: [16, 20, 24, 28, 32, 40], }, }, -} as Meta; -type Story = StoryObj<typeof Icon>; + decorators: [(Story) => <div className="p-5">{Story()}</div>], +}; -const BaseIcon: Story = { - args: { - name: 'ArrowBack', - size: 24, - }, +export default Component; +type Story = StoryObj<typeof Component>; + +export const BaseIcon: Story = (args: any) => { + return <Icon component={<ArrowBack />} size={24} {...args} />; }; -export const Default = { ...BaseIcon }; +BaseIcon.args = {}; diff --git a/libs/shared/lib/components/icon/index.tsx b/libs/shared/lib/components/icon/index.tsx index 68950afb578f4358dc6ea48dcaf443bd4f7ef4b9..4a4c74652fa19df9b26db87afd6ae74e3f7dbcdb 100644 --- a/libs/shared/lib/components/icon/index.tsx +++ b/libs/shared/lib/components/icon/index.tsx @@ -1,7 +1,7 @@ import React, { ReactElement } from 'react'; import { SVGProps } from 'react'; -export type Sizes = 14 | 16 | 20 | 24 | 28 | 32 | 40; +export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 40; export type IconProps = SVGProps<SVGSVGElement> & { component: ReactElement<any>; size?: Sizes; diff --git a/libs/shared/lib/components/inputs/index.tsx b/libs/shared/lib/components/inputs/index.tsx index e52a53dd7861867fda9c6bd4d0b10c149566978b..2ef371c0cd0c7aec6e0bae851773c8cb630f6a0b 100644 --- a/libs/shared/lib/components/inputs/index.tsx +++ b/libs/shared/lib/components/inputs/index.tsx @@ -31,6 +31,21 @@ type TextProps = { onChange?: (value: string) => void; }; +type NumberProps = { + label: string; + type: 'number'; + placeholder?: string; + value: number; + required?: boolean; + errorText?: string; + visible?: boolean; + disabled?: boolean; + tooltip?: string; + info?: string; + validate?: (value: any) => boolean; + onChange?: (value: number) => void; +}; + type CheckboxProps = { label?: string; type: 'checkbox'; @@ -63,6 +78,7 @@ type DropdownProps = { value: string | number | undefined; type: 'dropdown'; options: any; + size?: 'xs' | 'sm' | 'md' | 'xl'; tooltip?: string; onChange?: (value: string | number) => void; required?: boolean; @@ -70,9 +86,9 @@ type DropdownProps = { disabled?: boolean; }; -export type InputProps = TextProps | SliderProps | CheckboxProps | DropdownProps | RadioProps | BooleanProps; +export type InputProps = TextProps | SliderProps | CheckboxProps | DropdownProps | RadioProps | BooleanProps | NumberProps; -const Input = (props: InputProps) => { +export const Input = (props: InputProps) => { switch (props.type) { case 'slider': return <SliderInput {...(props as SliderProps)} />; @@ -86,6 +102,8 @@ const Input = (props: InputProps) => { return <RadioInput {...(props as RadioProps)} />; case 'boolean': return <BooleanInput {...(props as BooleanProps)} />; + case 'number': + return <NumberInput {...(props as NumberProps)} />; default: return null; } @@ -167,6 +185,52 @@ export const TextInput = ({ ); }; +export const NumberInput = ({ + label, + placeholder, + value = 0, + required = false, + visible = true, + errorText, + validate, + disabled = false, + onChange, + tooltip, + info, +}: NumberProps) => { + const [isValid, setIsValid] = React.useState<boolean>(true); + + return ( + <div data-tip={tooltip || null} className={'form-control w-full' + (tooltip ? ' tooltip' : '')}> + <label className="label"> + <span className={`text-sm font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}> + {label} + </span> + {required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>} + {info && <Info tooltip={info} side={'left'} />} + </label> + <input + type="number" + placeholder={placeholder} + className={`px-3 py-2 bg-light border border-secondary-300 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ + isValid ? '' : 'input-error' + }`} + value={value.toString()} + onChange={(e) => { + if (required && validate) { + setIsValid(validate(e.target.value)); + } + if (onChange) { + onChange(Number(e.target.value)); + } + }} + required={required} + disabled={disabled} + /> + </div> + ); +}; + export const RadioInput = ({ label, value, options, onChange, tooltip }: RadioProps) => { return ( <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> @@ -226,7 +290,7 @@ export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) return ( <div data-tip={tooltip || null} className={tooltip ? 'tooltip' : ''}> <label className={`label cursor-pointer w-fit gap-2 px-0 py-1`}> - <span className="label-text">{label}</span> + <span className="text-sm">{label}</span> <input type="checkbox" checked={value} @@ -242,7 +306,17 @@ export const BooleanInput = ({ label, value, onChange, tooltip }: BooleanProps) ); }; -export const DropDownInput = ({ label, value, options, onChange, required = false, tooltip, disabled = false, info }: DropdownProps) => { +export const DropDownInput = ({ + label, + value, + options, + onChange, + required = false, + tooltip, + size = 'sm', + disabled = false, + info, +}: DropdownProps) => { const dropdownRef = React.useRef<HTMLDivElement>(null); const [isDropdownOpen, setIsDropdownOpen] = React.useState<boolean>(false); @@ -273,6 +347,7 @@ export const DropDownInput = ({ label, value, options, onChange, required = fals <DropdownContainer className="w-full" ref={dropdownRef}> <DropdownButton title={value} + size={size} disabled={disabled} onClick={(e) => { e.stopPropagation(); @@ -302,5 +377,3 @@ export const DropDownInput = ({ label, value, options, onChange, required = fals </div> ); }; - -export default Input; diff --git a/libs/shared/lib/components/inputs/number.stories.tsx b/libs/shared/lib/components/inputs/number.stories.tsx new file mode 100644 index 0000000000000000000000000000000000000000..e31f0c74597aff1a760a9dea6e95cf430a214c96 --- /dev/null +++ b/libs/shared/lib/components/inputs/number.stories.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { NumberInput } from '.'; + +const Component: Meta<typeof NumberInput> = { + title: 'Components/Inputs', + component: NumberInput, + argTypes: { onChange: {} }, + decorators: [(Story) => <div className="w-52 m-5">{Story()}</div>], +}; + +export default Component; +type Story = StoryObj<typeof Component>; + +export const NumberInputStory: Story = { + args: { + type: 'number', + label: 'Number input', + placeholder: 'Put here a number', + required: true, + onChange: (value) => {}, + }, +}; + +export const RequiredNumberInputStory: Story = { + args: { + type: 'number', + label: 'Number input', + placeholder: 'Put here a number', + errorText: 'This field is required', + validate: (value) => { + return value.length > 0; + }, + onChange: (value) => {}, + }, +}; diff --git a/libs/shared/lib/components/inputs/overview.mdx b/libs/shared/lib/components/inputs/overview.mdx index 2682f0b10af242b24b9bcd72c1dd71c7a41a7147..8f26649d2cd3821b42acdda5d95138f886e9655f 100644 --- a/libs/shared/lib/components/inputs/overview.mdx +++ b/libs/shared/lib/components/inputs/overview.mdx @@ -1,5 +1,5 @@ import { Meta } from '@storybook/blocks'; -import Input from '.'; // Adjust the import path as needed +import { Input } from '.'; // Adjust the import path as needed export const components = { Input, diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/axis.stories.tsx b/libs/shared/lib/components/selectors/axis.stories.tsx similarity index 100% rename from libs/shared/lib/vis/configuration/encodings/selectors/axis.stories.tsx rename to libs/shared/lib/components/selectors/axis.stories.tsx diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/axis.tsx b/libs/shared/lib/components/selectors/axis.tsx similarity index 66% rename from libs/shared/lib/vis/configuration/encodings/selectors/axis.tsx rename to libs/shared/lib/components/selectors/axis.tsx index bd987f71de2e13f55bc3e4ecf79aa4ad7040585d..0415ad71dbc225d8b7750bfcc088e0db855de93d 100644 --- a/libs/shared/lib/vis/configuration/encodings/selectors/axis.tsx +++ b/libs/shared/lib/components/selectors/axis.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import Input from '@graphpolaris/shared/lib/components/inputs'; -import { SelectorProps } from '../encodings.types'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { SelectorProps } from './encodings.types'; export default function Axis({ marking, onChange }: SelectorProps) { return ( diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/color.stories.tsx b/libs/shared/lib/components/selectors/color.stories.tsx similarity index 100% rename from libs/shared/lib/vis/configuration/encodings/selectors/color.stories.tsx rename to libs/shared/lib/components/selectors/color.stories.tsx diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/color.tsx b/libs/shared/lib/components/selectors/color.tsx similarity index 83% rename from libs/shared/lib/vis/configuration/encodings/selectors/color.tsx rename to libs/shared/lib/components/selectors/color.tsx index e67a6059e14728a7231db1fea21344599468e60f..a1daab8daf88d6aee5d890ba84c5e087424efa0c 100644 --- a/libs/shared/lib/vis/configuration/encodings/selectors/color.tsx +++ b/libs/shared/lib/components/selectors/color.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; -import { SelectorProps } from '../encodings.types'; +import { SelectorProps } from './encodings.types'; import DropdownColorLegend from '@graphpolaris/shared/lib/components/colorComponents/colorDropdown'; -import Input from '@graphpolaris/shared/lib/components/inputs'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; export default function Color({ marking, onChange, statistics, dimension }: SelectorProps) { const [colors, setColors] = useState<any>(null); diff --git a/libs/shared/lib/vis/configuration/encodings/encodings.types.ts b/libs/shared/lib/components/selectors/encodings.types.ts similarity index 94% rename from libs/shared/lib/vis/configuration/encodings/encodings.types.ts rename to libs/shared/lib/components/selectors/encodings.types.ts index 6b1b367b64a462bd03ba73038bb806d9206d12bf..38ee8f0dfc51f14476483984f62d11a7e6a7c3dd 100644 --- a/libs/shared/lib/vis/configuration/encodings/encodings.types.ts +++ b/libs/shared/lib/components/selectors/encodings.types.ts @@ -1,5 +1,5 @@ import { DimensionType } from '@graphpolaris/shared/lib/schema'; -import { EncodingSelector } from './selectors'; +import { EncodingSelector } from '.'; import { ElementStats } from '@graphpolaris/shared/lib/data-access/statistics'; export type SelectorType = keyof typeof EncodingSelector; diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/index.ts b/libs/shared/lib/components/selectors/index.ts similarity index 100% rename from libs/shared/lib/vis/configuration/encodings/selectors/index.ts rename to libs/shared/lib/components/selectors/index.ts diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/opacity.stories.tsx b/libs/shared/lib/components/selectors/opacity.stories.tsx similarity index 100% rename from libs/shared/lib/vis/configuration/encodings/selectors/opacity.stories.tsx rename to libs/shared/lib/components/selectors/opacity.stories.tsx diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/opacity.tsx b/libs/shared/lib/components/selectors/opacity.tsx similarity index 82% rename from libs/shared/lib/vis/configuration/encodings/selectors/opacity.tsx rename to libs/shared/lib/components/selectors/opacity.tsx index 2d16f8a76246d02ae330149946c872ab617897e8..3d69051c39316d838d67d2a7f80ac347bb7e40dd 100644 --- a/libs/shared/lib/vis/configuration/encodings/selectors/opacity.tsx +++ b/libs/shared/lib/components/selectors/opacity.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import Input from '@graphpolaris/shared/lib/components/inputs'; -import { SelectorProps } from '../encodings.types'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { SelectorProps } from './encodings.types'; export default function Opacity({ marking, onChange, statistics, dimension }: SelectorProps) { const [custom, setCustom] = useState<boolean>(false); diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/shape.stories.tsx b/libs/shared/lib/components/selectors/shape.stories.tsx similarity index 100% rename from libs/shared/lib/vis/configuration/encodings/selectors/shape.stories.tsx rename to libs/shared/lib/components/selectors/shape.stories.tsx diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/shape.tsx b/libs/shared/lib/components/selectors/shape.tsx similarity index 74% rename from libs/shared/lib/vis/configuration/encodings/selectors/shape.tsx rename to libs/shared/lib/components/selectors/shape.tsx index 1f0b3880973e5ae83cde4e5a560973ea418a9778..9238fc83e0a93f8c830b5966212950a46095f466 100644 --- a/libs/shared/lib/vis/configuration/encodings/selectors/shape.tsx +++ b/libs/shared/lib/components/selectors/shape.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { SelectorProps } from '../encodings.types'; +import { SelectorProps } from './encodings.types'; export default function Shape({ marking, onChange, statistics, dimension }: SelectorProps) { return <div>shape</div>; diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/size.stories.tsx b/libs/shared/lib/components/selectors/size.stories.tsx similarity index 100% rename from libs/shared/lib/vis/configuration/encodings/selectors/size.stories.tsx rename to libs/shared/lib/components/selectors/size.stories.tsx diff --git a/libs/shared/lib/vis/configuration/encodings/selectors/size.tsx b/libs/shared/lib/components/selectors/size.tsx similarity index 81% rename from libs/shared/lib/vis/configuration/encodings/selectors/size.tsx rename to libs/shared/lib/components/selectors/size.tsx index e5f68f47ba0d546551c863ecfeac11a8df938092..4b74a1bf082b8ab9c461e5159f022927da92686a 100644 --- a/libs/shared/lib/vis/configuration/encodings/selectors/size.tsx +++ b/libs/shared/lib/components/selectors/size.tsx @@ -1,6 +1,6 @@ import React, { useState } from 'react'; -import Input from '@graphpolaris/shared/lib/components/inputs'; -import { SelectorProps } from '../encodings.types'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { SelectorProps } from './encodings.types'; export default function Size({ marking, onChange, statistics, dimension }: SelectorProps) { const [item, setItem] = useState<string>(''); diff --git a/libs/shared/lib/components/tooltip/index.tsx b/libs/shared/lib/components/tooltip/index.tsx index 79e5bfe8837fc3746903db753518aa3c339ff5aa..4d3d7c9913189121fbc259adbb0fad627b349b4f 100644 --- a/libs/shared/lib/components/tooltip/index.tsx +++ b/libs/shared/lib/components/tooltip/index.tsx @@ -55,51 +55,3 @@ const TooltipContent = React.forwardRef< TooltipContent.displayName = TooltipPrimitive.Content.displayName; export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; - -// Old Tooltip - -export type TooltipProps2 = { - position?: 'top' | 'bottom' | 'left' | 'right'; - children: ReactNode; - tooltip: string; - disabled?: boolean; -}; -export default function Tooltip2({ position = 'bottom', children, tooltip, disabled = false }: TooltipProps2) { - return ( - <div className="group relative cursor-pointer"> - <div className="">{children}</div> - {!disabled && ( - <div className="z-50"> - <span - className={`absolute hidden group-hover:inline-block bg-neutral-900 text-white text-xs p-2 whitespace-nowrap rounded ${ - position === 'top' ? 'left-1/2 -translate-x-1/2 bottom-[calc(100%+5px)]' : '' - } ${position === 'bottom' ? 'left-1/2 -translate-x-1/2 top-[calc(100%+5px)]' : ''} ${ - position === 'left' ? 'top-1/2 -translate-y-1/2 right-[calc(100%+5px)]' : '' - } ${position === 'right' ? 'top-1/2 -translate-y-1/2 left-[calc(100%+5px)]' : ''} `} - > - {tooltip} - </span> - <span - className={`absolute hidden group-hover:inline-block border-[6px] ${ - position === 'top' - ? 'left-1/2 -translate-x-1/2 bottom-full border-l-transparent border-r-transparent border-b-0 border-t-neutral-900' - : '' - } ${ - position === 'bottom' - ? 'left-1/2 -translate-x-1/2 top-full border-l-transparent border-r-transparent border-t-0 border-b-neutral-900' - : '' - } ${ - position === 'left' - ? 'top-1/2 -translate-y-1/2 right-full border-t-transparent border-b-transparent border-r-0 border-l-neutral-900' - : '' - } ${ - position === 'right' - ? 'top-1/2 -translate-y-1/2 left-full border-t-transparent border-b-transparent border-l-0 border-r-neutral-900' - : '' - } `} - ></span> - </div> - )} - </div> - ); -} diff --git a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts index 5372882d49a859d223e15b03ebb814f02f44e73e..62df2ef3289773267c455dcf801f058f6c046a34 100755 --- a/libs/shared/lib/data-access/store/graphQueryResultSlice.ts +++ b/libs/shared/lib/data-access/store/graphQueryResultSlice.ts @@ -63,68 +63,72 @@ export const initialState: GraphQueryResult = { queryingBackend: false, }; +export const graphQueryBackend2graphQuery = (payload: GraphQueryResultFromBackend) => { + // Only keep one node and one edge per id. This is also done in the backend, but we do it here as well to be sure. + const nodeIDs = new Set(payload.nodes.map((node) => node.id)); + const edgeIDs = new Set(payload.edges.map((edge) => edge.id)); + let nodes = [...nodeIDs].map((nodeID) => payload.nodes.find((node) => node.id === nodeID) as unknown as Node); + let edges = [...edgeIDs].map((edgeID) => payload.edges.find((edge) => edge.id === edgeID) as unknown as Edge); + + const metaData: GraphMetaData = { + nodes: { labels: [], count: nodes.length, types: {} }, + edges: { labels: [], count: edges.length, types: {} }, + }; + + nodes = nodes.map((node) => { + let _node = { ...node }; + // TODO!: only works for neo4j + let nodeType: string = node.id.split('/')[0]; + let innerLabels = node?.attributes?.labels as string[]; + + if (innerLabels?.length > 0) nodeType = innerLabels[0] as string; + if (!metaData.nodes.labels.includes(nodeType)) metaData.nodes.labels?.push(nodeType); + if (!metaData.nodes.types[nodeType]) metaData.nodes.types[nodeType] = { count: 0, attributes: {} }; + metaData.nodes.types[nodeType].count++; + + Object.entries(_node.attributes).forEach(([attributeId, attributeValue]) => { + if (!metaData.nodes.types[nodeType].attributes[attributeId]) { + const dimension = getDimension(attributeValue); + metaData.nodes.types[nodeType].attributes[attributeId] = { dimension, values: [], statistics: {} }; + } + metaData.nodes.types[nodeType].attributes[attributeId].values?.push(attributeValue); + }); + + _node.label = nodeType; + return _node; + }); + + edges = edges.map((edge) => { + let _edge = { ...edge }; + // TODO!: only works for neo4j + let edgeType: string = edge.id.split('/')[0]; + + if (!_edge.id.includes('/')) edgeType = edge.attributes.Type as string; + if (!metaData.edges.labels?.includes(edgeType)) metaData.edges.labels?.push(edgeType); + if (!metaData.edges.types[edgeType]) metaData.edges.types[edgeType] = { count: 0, attributes: {} }; + metaData.edges.types[edgeType].count++; + + Object.entries(_edge.attributes).forEach(([attributeId, attributeValue]) => { + if (!metaData.edges.types[edgeType].attributes[attributeId]) { + const dimension = getDimension(attributeValue); + metaData.edges.types[edgeType].attributes[attributeId] = { dimension, values: [], statistics: {} }; + } + metaData.edges.types[edgeType].attributes[attributeId].values?.push(attributeValue); + }); + + _edge.label = edgeType; + return _edge; + }); + return { metaData: extractStatistics(metaData), nodes, edges }; +}; + export const graphQueryResultSlice = createSlice({ name: 'graphQueryResult', initialState, reducers: { setNewGraphQueryResult: (state, action: PayloadAction<GraphQueryResultFromBackendPayload>) => { const payload = action.payload.result.payload; - - // Only keep one node and one edge per id. This is also done in the backend, but we do it here as well to be sure. - const nodeIDs = new Set(payload.nodes.map((node) => node.id)); - const edgeIDs = new Set(payload.edges.map((edge) => edge.id)); - let nodes = [...nodeIDs].map((nodeID) => payload.nodes.find((node) => node.id === nodeID) as unknown as Node); - let edges = [...edgeIDs].map((edgeID) => payload.edges.find((edge) => edge.id === edgeID) as unknown as Edge); - - const metaData: GraphMetaData = { - nodes: { labels: [], count: nodes.length, types: {} }, - edges: { labels: [], count: edges.length, types: {} }, - }; - - nodes = nodes.map((node) => { - let _node = { ...node }; - // TODO!: only works for neo4j - let nodeType: string = node.id.split('/')[0]; - let innerLabels = node?.attributes?.labels as string[]; - - if (innerLabels?.length > 0) nodeType = innerLabels[0] as string; - if (!metaData.nodes.labels.includes(nodeType)) metaData.nodes.labels?.push(nodeType); - if (!metaData.nodes.types[nodeType]) metaData.nodes.types[nodeType] = { count: 0, attributes: {} }; - metaData.nodes.types[nodeType].count++; - - Object.entries(_node.attributes).forEach(([attributeId, attributeValue]) => { - if (!metaData.nodes.types[nodeType].attributes[attributeId]) { - const dimension = getDimension(attributeValue); - metaData.nodes.types[nodeType].attributes[attributeId] = { dimension, values: [], statistics: {} }; - } - metaData.nodes.types[nodeType].attributes[attributeId].values?.push(attributeValue); - }); - - _node.label = nodeType; - return _node; - }); - - edges = edges.map((edge) => { - let _edge = { ...edge }; - // TODO!: only works for neo4j - let edgeType: string = edge.id.split('/')[0]; - - if (!_edge.id.includes('/')) edgeType = edge.attributes.Type as string; - if (!metaData.edges.labels?.includes(edgeType)) metaData.edges.labels?.push(edgeType); - if (!metaData.edges.types[edgeType]) metaData.edges.types[edgeType] = { count: 0, attributes: {} }; - metaData.edges.types[edgeType].count++; - - Object.entries(_edge.attributes).forEach(([attributeId, attributeValue]) => { - if (!metaData.edges.types[edgeType].attributes[attributeId]) { - const dimension = getDimension(attributeValue); - metaData.edges.types[edgeType].attributes[attributeId] = { dimension, values: [], statistics: {} }; - } - metaData.edges.types[edgeType].attributes[attributeId].values?.push(attributeValue); - }); - - _edge.label = edgeType; - return _edge; - }); + const { metaData, nodes, edges } = graphQueryBackend2graphQuery(payload); // Assign new state state.metaData = extractStatistics(metaData); diff --git a/libs/shared/lib/data-access/store/interactionSlice.ts b/libs/shared/lib/data-access/store/interactionSlice.ts new file mode 100644 index 0000000000000000000000000000000000000000..89d18d41050d1f9f66c00566d1f21c93b5b338f6 --- /dev/null +++ b/libs/shared/lib/data-access/store/interactionSlice.ts @@ -0,0 +1,35 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import type { RootState } from './store'; + +export type HoverType = { [id: string]: any }; + +export type SelectType = { [id: string]: any }; + +// Define the initial state using that type +export type InteractionsType = { + hover: HoverType | undefined; + select: SelectType | undefined; +}; + +export const initialState: InteractionsType = { + hover: {}, + select: {}, +}; + +export const interactionSlice = createSlice({ + name: 'interaction', + // `createSlice` will infer the state type from the `initialState` argument + initialState, + reducers: { + addHover: (state, action: PayloadAction<HoverType | undefined>) => { + state.hover = action.payload; + }, + addSelect: (state, action: PayloadAction<SelectType | undefined>) => { + state.select = action.payload; + }, + }, +}); + +export const { addHover, addSelect } = interactionSlice.actions; + +export default interactionSlice.reducer; diff --git a/libs/shared/lib/data-access/store/visualizationSlice.ts b/libs/shared/lib/data-access/store/visualizationSlice.ts index 4cb0d79e4f04e944fbae222ad2c6d619945de413..217118a0ef90fe54f3c98ca00669954bc1ca37cb 100644 --- a/libs/shared/lib/data-access/store/visualizationSlice.ts +++ b/libs/shared/lib/data-access/store/visualizationSlice.ts @@ -1,46 +1,28 @@ -import { createSelector, createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import type { RootState } from './store'; -import { globalConfigTypes, VisualizationConfiguration } from '../../vis/types'; +import { VisualizationConfiguration } from '../../vis/common'; import { isEqual } from 'lodash-es'; -import { EncodingTypes } from '../../vis/configuration/encodings'; -import { SettingTypes } from '../../vis/configuration/settings'; -import { InteractionTypes } from '../../vis/configuration/interactions'; export type VisStateSettings = { - general: globalConfigTypes; [id: string]: VisualizationConfiguration; }; export type VisState = { activeVisualization?: string; - settings: VisStateSettings; + openVisualizations: VisStateSettings; }; export const initialState: VisState = { - activeVisualization: 'NodeLinkVis', - settings: { - general: {}, - }, + // activeVisualization: 'NodeLinkVis', + openVisualizations: {}, }; export const visualizationSlice = createSlice({ name: 'visualization', initialState, reducers: { - addVisualization: ( - state, - action: PayloadAction<{ - id: string; - settings: SettingTypes; - encodings: EncodingTypes; - interactions: InteractionTypes; - }>, - ) => { - const { id, settings, encodings, interactions } = action.payload; - state.settings[id] = { settings, encodings, interactions }; - }, removeVisualization: (state, action: PayloadAction<string>) => { - if (state.settings[action.payload]) { - delete state.settings[action.payload]; + if (state.openVisualizations[action.payload]) { + delete state.openVisualizations[action.payload]; } }, setActiveVisualization: (state, action: PayloadAction<string>) => { @@ -49,21 +31,23 @@ export const visualizationSlice = createSlice({ setVisualizationState: (state, action: PayloadAction<VisState>) => { if (action.payload.activeVisualization && !isEqual(action.payload, state)) { state.activeVisualization = action.payload.activeVisualization; - state.settings = action.payload.settings; + state.openVisualizations = action.payload.openVisualizations || {}; } }, - updateConfiguration: (state, action: PayloadAction<any>) => { - const { general, visualization } = action.payload; - state.settings.general = general; - if (state.activeVisualization) state.settings[state.activeVisualization] = visualization; + updateVisualization: (state, action: PayloadAction<{ id: string; settings: any }>) => { + const { id, settings } = action.payload; + state.openVisualizations[id] = settings; + }, + reorderVisState: (state, action: PayloadAction<{ [id: string]: VisualizationConfiguration }>) => { + state.openVisualizations = action.payload; }, }, }); -export const { addVisualization, removeVisualization, setActiveVisualization, setVisualizationState, updateConfiguration } = +export const { removeVisualization, setActiveVisualization, setVisualizationState, updateVisualization, reorderVisState } = visualizationSlice.actions; export const visualizationState = (state: RootState) => state.visualize; -export const visualizationAllSettings = (state: RootState) => state.visualize.settings; +export const visualizationAllSettings = (state: RootState) => state.visualize.openVisualizations; export default visualizationSlice.reducer; diff --git a/libs/shared/lib/data-access/theme/colorPaletteConfigSlice.ts b/libs/shared/lib/data-access/theme/colorPaletteConfigSlice.ts index f8c83ebf12077737950b7813903824c664ff3976..70132ebb417ade32c0e6a3b860cb54917eb6cc38 100644 --- a/libs/shared/lib/data-access/theme/colorPaletteConfigSlice.ts +++ b/libs/shared/lib/data-access/theme/colorPaletteConfigSlice.ts @@ -1,6 +1,6 @@ /** Extra properties that are not present in the default PaletteOptions of mui. For autocompletion */ export interface ExtraColorsForMui5 { - /** Colors that can be used for data visualisation, e.g. nodes, edges */ + /** Colors that can be used for data visualization, e.g. nodes, edges */ dataPointColors: string[]; nodes: Array<string>; diff --git a/libs/shared/lib/querybuilder/panel/querybuilder.tsx b/libs/shared/lib/querybuilder/panel/querybuilder.tsx index a594becbcf803fdf197f3e51a68605b346bc116e..1491281f27be219fbae44bdd1b0e86a03b2397e1 100644 --- a/libs/shared/lib/querybuilder/panel/querybuilder.tsx +++ b/libs/shared/lib/querybuilder/panel/querybuilder.tsx @@ -26,13 +26,13 @@ import ReactFlow, { } from 'reactflow'; import { Dialog } from '../../components/Dialog'; import { Button } from '../../components/buttons'; -import ControlContainer from '../../components/controls'; +import { ControlContainer } from '../../components/controls'; import { addError } from '../../data-access/store/configSlice'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; import { LayoutFactory } from '../../graph-layout'; import { AllLogicMap, QueryElementTypes, createReactFlowElements, isLogicHandle, toHandleData } from '../model'; import { ConnectionDragLine, ConnectionLine, EntityFlowElement, RelationPill } from '../pills'; -import LogicPill from '../pills/customFlowPills/logicpill/logicpill'; +import { LogicPill } from '../pills/customFlowPills/logicpill/logicpill'; import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill'; import styles from './querybuilder.module.scss'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/queryBuilderLogicPillsPanel'; @@ -434,137 +434,141 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { }, [queryBuilderSettings]); return ( - <div ref={reactFlowWrapper} className="h-full w-full"> + <div ref={reactFlowWrapper} className="h-full w-full flex flex-col"> <QuerySettingsDialog open={toggleSettings === 'settings'} onClose={() => setToggleSettings(undefined)} /> <QueryMLDialog open={toggleSettings === 'ml'} onClose={() => setToggleSettings(undefined)} /> - <div className="relative flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100 border-b border-secondary-200"> - <h1 className="text-xs font-semibold text-secondary-800">Query builder</h1> - <ControlContainer> - <TooltipProvider delayDuration={0}> - <Tooltip> - <TooltipTrigger asChild> - <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={fitView} /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Fit to screen</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button type="secondary" variant="ghost" size="xs" iconComponent={<Delete />} onClick={() => clearAllNodes()} /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Clear query panel</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<CameraAlt />} - onClick={(event) => { - event.stopPropagation(); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Capture screen</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<ImportExport />} - onClick={(event) => { - event.stopPropagation(); - applyLayout(); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Layouts</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<Settings />} - additionalClasses="query-settings" - onClick={(event) => { - event.stopPropagation(); - if (toggleSettings === 'settings') setToggleSettings(undefined); - else setToggleSettings('settings'); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'} disabled={toggleSettings === 'settings'}> - <p>Query builder settings</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<Cached />} - onClick={(event) => { - event.stopPropagation(); - if (props.onRunQuery) props.onRunQuery(); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Rerun query</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<Difference />} - onClick={(event) => { - event.stopPropagation(); - if (toggleSettings === 'logic') setToggleSettings(undefined); - else setToggleSettings('logic'); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'} disabled={toggleSettings === 'logic'}> - <p>Logic settings</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<Lightbulb />} - onClick={(event) => { - event.stopPropagation(); - if (toggleSettings === 'ml') setToggleSettings(undefined); - else setToggleSettings('ml'); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'} disabled={toggleSettings === 'ml'}> - <p>Machine learning</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </ControlContainer> + <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="flex items-center"> + <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">Query builder</h1> + </div> + <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> + <ControlContainer> + <TooltipProvider delayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={fitView} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Fit to screen</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Delete />} onClick={() => clearAllNodes()} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Clear query panel</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<CameraAlt />} + onClick={(event) => { + event.stopPropagation(); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Capture screen</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<ImportExport />} + onClick={(event) => { + event.stopPropagation(); + applyLayout(); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Layouts</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Settings />} + additionalClasses="query-settings" + onClick={(event) => { + event.stopPropagation(); + if (toggleSettings === 'settings') setToggleSettings(undefined); + else setToggleSettings('settings'); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'} disabled={toggleSettings === 'settings'}> + <p>Query builder settings</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Cached />} + onClick={(event) => { + event.stopPropagation(); + if (props.onRunQuery) props.onRunQuery(); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Rerun query</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Difference />} + onClick={(event) => { + event.stopPropagation(); + if (toggleSettings === 'logic') setToggleSettings(undefined); + else setToggleSettings('logic'); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'} disabled={toggleSettings === 'logic'}> + <p>Logic settings</p> + </TooltipContent> + </Tooltip> + <Tooltip> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Lightbulb />} + onClick={(event) => { + event.stopPropagation(); + if (toggleSettings === 'ml') setToggleSettings(undefined); + else setToggleSettings('ml'); + }} + /> + </TooltipTrigger> + <TooltipContent side={'top'} disabled={toggleSettings === 'ml'}> + <p>Machine learning</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </ControlContainer> + </div> </div> <Dialog @@ -652,7 +656,7 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { export const QueryBuilder = (props: QueryBuilderProps) => { return ( - <div className="query-panel flex w-full h-full"> + <div className="query-panel flex w-full h-full border bg-light"> <ReactFlowProvider> <QueryBuilderInner {...props} /> </ReactFlowProvider> diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx index a7a98076eeea80d7b6a63e8c81021f1e6da542af..0c409aa29afa09c84541cb048ed4b8e41fc6833c 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/queryMLDialog.tsx @@ -8,6 +8,7 @@ import { setShortestPathEnabled, } from '@graphpolaris/shared/lib/data-access/store/mlSlice'; import { FormDiv, FormCard, FormBody, FormTitle, FormHBar } from '@graphpolaris/shared/lib/components/forms'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; type QueryMLDialogProps = DialogProps; @@ -28,27 +29,22 @@ export const QueryMLDialog = React.forwardRef<HTMLDivElement, QueryMLDialogProps <FormTitle title="Machine Learning Options" onClose={props.onClose} /> <FormHBar /> - <div className="form-control px-5"> - <label className="label cursor-pointer gap-2 w-fit"> - <input - type="checkbox" - checked={ml.linkPrediction.enabled} - className="checkbox checkbox-sm" - onChange={(e) => dispatch(setLinkPredictionEnabled(e.target.checked))} - /> - <span className="label-text">Link Prediction</span> - </label> + <div className="px-5"> + <Input + type="boolean" + label="Link Prediction" + value={ml.linkPrediction.enabled} + onChange={() => dispatch(setLinkPredictionEnabled(!ml.linkPrediction.enabled))} + /> {ml.linkPrediction.enabled && ml.linkPrediction.result && <span># of predictions: {ml.linkPrediction.result.length}</span>} {ml.linkPrediction.enabled && !ml.linkPrediction.result && <span>Loading...</span>} - <label className="label cursor-pointer gap-2 w-fit"> - <input - type="checkbox" - checked={ml.centrality.enabled} - className="checkbox checkbox-sm" - onChange={(e) => dispatch(setCentralityEnabled(e.target.checked))} - /> - <span className="label-text">Centrality</span> - </label> + + <Input + type="boolean" + label="Centrality" + value={ml.centrality.enabled} + onChange={() => dispatch(setCentralityEnabled(!ml.centrality.enabled))} + /> {ml.centrality.enabled && Object.values(ml.centrality.result).length > 0 && ( <span> sum of centers: @@ -58,28 +54,24 @@ export const QueryMLDialog = React.forwardRef<HTMLDivElement, QueryMLDialogProps </span> )} {ml.centrality.enabled && Object.values(ml.centrality.result).length === 0 && <span>No Centers Found</span>} - <label className="label cursor-pointer gap-2 w-fit"> - <input - type="checkbox" - checked={ml.communityDetection.enabled} - className="checkbox checkbox-sm" - onChange={(e) => dispatch(setCommunityDetectionEnabled(e.target.checked))} - /> - <span className="label-text">Community Detection</span> - </label> + + <Input + type="boolean" + label="Community detection" + value={ml.communityDetection.enabled} + onChange={() => dispatch(setCommunityDetectionEnabled(!ml.communityDetection.enabled))} + /> {ml.communityDetection.enabled && ml.communityDetection.result && ( <span># of communities: {ml.communityDetection.result.length}</span> )} {ml.communityDetection.enabled && !ml.communityDetection.result && <span>Loading...</span>} - <label className="label cursor-pointer gap-2 w-fit"> - <input - type="checkbox" - checked={ml.shortestPath.enabled} - className="checkbox checkbox-sm" - onChange={(e) => dispatch(setShortestPathEnabled(e.target.checked))} - /> - <span className="label-text">Shortest Path</span> - </label> + + <Input + type="boolean" + label="Shortest path" + value={ml.shortestPath.enabled} + onChange={() => dispatch(setShortestPathEnabled(!ml.shortestPath.enabled))} + /> {ml.shortestPath.enabled && ml.shortestPath.result?.length > 0 && <span># of hops: {ml.shortestPath.result.length}</span>} {ml.shortestPath.enabled && !ml.shortestPath.srcNode && <span>Please select source node</span>} {ml.shortestPath.enabled && ml.shortestPath.srcNode && !ml.shortestPath.trtNode && <span>Please select target node</span>} diff --git a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx index bddfd287afc89ae82e35ad6237a97131768e41b2..b5103ac70a2973418c19158997d5126a08de8f1c 100644 --- a/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx +++ b/libs/shared/lib/querybuilder/panel/querysidepanel/querySettingsDialog.tsx @@ -6,7 +6,7 @@ import { QueryBuilderSettings, setQuerybuilderSettings } from '../../../data-acc import { addWarning } from '../../../data-access/store/configSlice'; import { FormActions, FormBody, FormCard, FormControl, FormDiv, FormHBar, FormTitle } from '../../../components/forms'; import { Layouts } from '@graphpolaris/shared/lib/graph-layout'; -import Input from '@graphpolaris/shared/lib/components/inputs'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; type QuerySettingsDialogProps = DialogProps; diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx index d0ec8779aea94dc7fab2a1e0fba4a7c1cf1753fd..7953484ba1c08a1f83d3a31b2c00853c3792e667 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/entitypill/entitypill.tsx @@ -65,7 +65,9 @@ export const EntityFlowElement = React.memo((node: SchemaReactflowEntityNode) => // outerClassName={'!bg-blue-700 !rounded-none'} className={'!bg-accent-700'} /> - <div className="text-center py-1">{data.name}</div> + <div className=""> + <div className="text-center py-1">{data.name}</div> + </div> {data?.attributes && ( <PillDropdown node={node} diff --git a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx index 0a5e0b8843e13ee7835c3a1f77796c3ab2bf096d..bb3ec30967629b7d46bc790db044b7cbfc116fc8 100644 --- a/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx +++ b/libs/shared/lib/querybuilder/pills/customFlowPills/logicpill/logicpill.tsx @@ -21,7 +21,7 @@ import { LogicInput } from './logicInput'; * Component to render an entity flow element * @param param0 Data of the flow element. */ -export default function LogicPill(node: SchemaReactflowLogicNode) { +export function LogicPill(node: SchemaReactflowLogicNode) { const dispatch = useAppDispatch(); const data = node.data; const logic = data.logic; diff --git a/libs/shared/lib/schema/panel/index.ts b/libs/shared/lib/schema/panel/index.ts index c25be6796cbd24f3a2918be51462273417adac06..e27a6e2f57ba2d6862dada85b06a5b46d9c6587a 100644 --- a/libs/shared/lib/schema/panel/index.ts +++ b/libs/shared/lib/schema/panel/index.ts @@ -1,4 +1 @@ export * from './schema'; - -import Schema from './schema'; -export default Schema; diff --git a/libs/shared/lib/schema/panel/schema.stories.tsx b/libs/shared/lib/schema/panel/schema.stories.tsx index 61457bbf4f1476dc00c81396e5f00f2d36f64d09..051acd2f7995ed333ee6a369ccfbd2c24b2b38f7 100644 --- a/libs/shared/lib/schema/panel/schema.stories.tsx +++ b/libs/shared/lib/schema/panel/schema.stories.tsx @@ -1,12 +1,12 @@ import React from 'react'; -import { Meta, Story, ComponentStory } from '@storybook/react'; +import { Meta } from '@storybook/react'; import { SchemaUtils } from '@graphpolaris/shared/lib/schema/schema-utils'; import { schemaSlice, setSchema } from '@graphpolaris/shared/lib/data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; -import Schema from './schema'; +import { Schema } from './schema'; import { movieSchemaRaw, diff --git a/libs/shared/lib/schema/panel/schema.tsx b/libs/shared/lib/schema/panel/schema.tsx index 4e47cd9e3ac2222263526f16528862ba74952118..b396083e7b1eed374bb3f33f5b7b66979c2e759e 100644 --- a/libs/shared/lib/schema/panel/schema.tsx +++ b/libs/shared/lib/schema/panel/schema.tsx @@ -1,22 +1,16 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; import { SmartBezierEdge, SmartStepEdge, SmartStraightEdge } from '@tisoap/react-flow-smart-edge'; - -import { useEffect, useMemo, useRef, useState } from 'react'; import ReactFlow, { Edge, Node, ReactFlowInstance, ReactFlowProvider, useEdgesState, useNodesState } from 'reactflow'; - import 'reactflow/dist/style.css'; - import { Button } from '../../components/buttons'; -import ControlContainer from '../../components/controls'; -import { useSchemaGraph, useSchemaSettings, useSearchResultSchema, useSessionCache, wsGetStates, wsSchemaRequest } from '../../data-access'; +import { useSchemaGraph, useSchemaSettings, useSearchResultSchema } from '../../data-access'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; -import NodeEdge from '../pills/edges/node-edge'; -import SelfEdge from '../pills/edges/self-edge'; +import { NodeEdge } from '../pills/edges/node-edge'; +import { SelfEdge } from '../pills/edges/self-edge'; import { EntityNode } from '../pills/nodes/entity/entity-node'; import { RelationNode } from '../pills/nodes/relation/relation-node'; import { SchemaDialog } from './schemaDialog'; -import { Fullscreen, Cached, Settings } from '@mui/icons-material'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; -import React from 'react'; +import { KeyboardArrowDown, KeyboardArrowRight } from '@mui/icons-material'; import { AlgorithmToLayoutProvider, AllLayoutAlgorithms, LayoutFactory } from '../../graph-layout'; import { ConnectionLine, ConnectionDragLine } from '../../querybuilder'; import { schemaExpandRelation, schemaGraphology2Reactflow } from '../schema-utils'; @@ -44,7 +38,6 @@ const edgeTypes = { }; export const Schema = (props: Props) => { - const session = useSessionCache(); const settings = useSchemaSettings(); const searchResults = useSearchResultSchema(); const [toggleSchemaSettings, setToggleSchemaSettings] = useState(false); @@ -52,6 +45,7 @@ export const Schema = (props: Props) => { const [edges, setEdges, onEdgesChange] = useEdgesState([] as Edge[]); const [firstUserConnection, setFirstUserConnection] = useState<boolean>(true); const [auth, setAuth] = useState(props.auth); + const [expanded, setExpanded] = useState<boolean>(false); const reactFlowInstanceRef = useRef<ReactFlowInstance | null>(null); const reactFlowRef = useRef<HTMLDivElement>(null); @@ -91,6 +85,7 @@ 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); @@ -113,87 +108,50 @@ export const Schema = (props: Props) => { }, [searchResults]); return ( - <div className="schema-panel w-full h-full"> - <SchemaDialog open={toggleSchemaSettings} onClose={() => setToggleSchemaSettings(false)} /> - <div className="relative flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100 border-b border-secondary-200"> - <h1 className="text-xs font-semibold text-secondary-800">Schema</h1> - <ControlContainer> - <TooltipProvider delayDuration={0}> - <Tooltip> - <TooltipTrigger asChild> - <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={fitView} /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Fit to screen</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<Cached />} - onClick={(e) => { - e.stopPropagation(); - if (session.currentSaveState) wsSchemaRequest(session.currentSaveState); - else wsGetStates(); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'}> - <p>Refresh schema</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<Settings />} - additionalClasses="schema-settings" - onClick={(e) => { - e.stopPropagation(); - setToggleSchemaSettings(!toggleSchemaSettings); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'} disabled={toggleSchemaSettings}> - <p>Schema settings</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </ControlContainer> - </div> + <div className="schema-panel w-full h-full flex flex-col justify-between" ref={reactFlowRef}> {nodes.length === 0 ? ( <p className="text-sm">No Elements</p> ) : ( <ReactFlowProvider> - <div className="h-[calc(100%-.8rem)] w-full" ref={reactFlowRef}> - <ReactFlow - snapGrid={[10, 10]} - snapToGrid - onlyRenderVisibleElements={false} - nodesDraggable={true} - nodeTypes={nodeTypes} - edgeTypes={edgeTypes} - connectionLineComponent={ConnectionDragLine} - onNodesChange={onNodesChange} - onEdgesChange={onEdgesChange} - nodes={nodes} - edges={edges} - onInit={(reactFlowInstance) => { - reactFlowInstanceRef.current = reactFlowInstance; - onInit(reactFlowInstance); - }} - proOptions={{ hideAttribution: true }} - ></ReactFlow> - </div> + <ReactFlow + snapGrid={[10, 10]} + snapToGrid + onlyRenderVisibleElements={false} + nodesDraggable={true} + nodeTypes={nodeTypes} + edgeTypes={edgeTypes} + connectionLineComponent={ConnectionDragLine} + onNodesChange={onNodesChange} + onEdgesChange={onEdgesChange} + nodes={nodes} + edges={edges} + onInit={(reactFlowInstance) => { + reactFlowInstanceRef.current = reactFlowInstance; + onInit(reactFlowInstance); + }} + proOptions={{ hideAttribution: true }} + ></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> ); }; - -export default Schema; diff --git a/libs/shared/lib/schema/panel/schemaDialog.tsx b/libs/shared/lib/schema/panel/schemaDialog.tsx index 644be97cdfa30ac662d0ad0034fa883c298b153e..3a4313d257a1001eaa5b76b4a2450967f513a991 100644 --- a/libs/shared/lib/schema/panel/schemaDialog.tsx +++ b/libs/shared/lib/schema/panel/schemaDialog.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useSchemaSettings } from '../../data-access'; import { SchemaSettings, setSchemaSettings } from '../../data-access/store/schemaSlice'; import { FormActions, FormBody, FormCard, FormControl, FormHBar, FormTitle, FormDiv } from '../../components/forms'; import { Layouts } from '../../graph-layout'; -import Input from '../../components/inputs'; +import { Input } from '../../components/inputs'; export const SchemaDialog = (props: DialogProps) => { const settings = useSchemaSettings(); @@ -22,59 +22,50 @@ export const SchemaDialog = (props: DialogProps) => { } return ( - <> - {props.open && ( - <FormDiv hAnchor="left"> - <FormCard> - <FormBody - onSubmit={(e) => { - e.preventDefault(); - submit(); - }} - > - <FormTitle title="Schema Settings" onClose={props.onClose} /> - <FormHBar /> - <FormControl> - <Input - type="dropdown" - label="Type of Connection" - value={state.connectionType} - options={['Default', 'Step', 'Straight', 'Bezier']} - onChange={(value: string | number) => { - setState({ ...state, connectionType: value as any }); - }} - /> - </FormControl> - <FormHBar /> - <FormControl> - <Input - type="boolean" - value={state.animatedEdges} - label="Animated Edges" - onChange={(value: boolean) => { - setState({ ...state, animatedEdges: value as any }); - }} - /> - </FormControl> - <FormHBar /> - <FormControl> - <Input - type="dropdown" - label="Layout Type" - value={state.layoutName} - options={Object.values(Layouts)} - onChange={(value: string | number) => { - setState({ ...state, layoutName: value as any }); - }} - /> - </FormControl> - <FormHBar /> + <form + className="w-full" + onSubmit={(e) => { + e.preventDefault(); + submit(); + }} + > + <FormControl> + <Input + type="dropdown" + label="Type of Connection" + value={state.connectionType} + options={['Default', 'Step', 'Straight', 'Bezier']} + onChange={(value: string | number) => { + setState({ ...state, connectionType: value as any }); + }} + /> + </FormControl> + <FormHBar /> + <FormControl> + <Input + type="boolean" + value={state.animatedEdges} + label="Animated Edges" + onChange={(value: boolean) => { + setState({ ...state, animatedEdges: value as any }); + }} + /> + </FormControl> + <FormHBar /> + <FormControl> + <Input + type="dropdown" + label="Layout Type" + value={state.layoutName} + options={Object.values(Layouts)} + onChange={(value: string | number) => { + setState({ ...state, layoutName: value as any }); + }} + /> + </FormControl> + <FormHBar /> - <FormActions onClose={props.onClose} /> - </FormBody> - </FormCard> - </FormDiv> - )} - </> + <FormActions onClose={props.onClose} /> + </form> ); }; diff --git a/libs/shared/lib/schema/pills/edges/node-edge.tsx b/libs/shared/lib/schema/pills/edges/node-edge.tsx index 691c78f0e4a32a9a953947c0c17b611598d79032..c24b0c9df6df1f87bbb8242df84874262c8a19a5 100644 --- a/libs/shared/lib/schema/pills/edges/node-edge.tsx +++ b/libs/shared/lib/schema/pills/edges/node-edge.tsx @@ -16,7 +16,7 @@ import { getCenter } from '../../schema-utils'; * It has a path that is altered depending on the algorithm in the SchemaViewModelImpl. * @param EdgeProps All the data that is stored inside of the React Flow edge. */ -export default function NodeEdge({ +export function NodeEdge({ id, source, target, diff --git a/libs/shared/lib/schema/pills/edges/self-edge.tsx b/libs/shared/lib/schema/pills/edges/self-edge.tsx index d0c1b217fb226c8d16b3519e393340d5c0cf5762..2b011d7cf706decb568bde2697a61940d10970e4 100644 --- a/libs/shared/lib/schema/pills/edges/self-edge.tsx +++ b/libs/shared/lib/schema/pills/edges/self-edge.tsx @@ -17,7 +17,7 @@ import { EdgeProps, getMarkerEnd } from 'reactflow'; * It has a path that is altered depending on the algorithm in the SchemaViewModelImpl. * @param EdgeProps All the data that is stored inside of the React Flow edge. */ -export default function SelfEdge({ +export function SelfEdge({ id, source, target, diff --git a/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx b/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx index e8755e486fe1f9e2febc12abb832ffdcadd33add..fa5325917177661b5ece2cb989fca32f229df3bf 100644 --- a/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx +++ b/libs/shared/lib/schema/pills/nodes/popup/attribute-analytics-popup-menu.tsx @@ -47,7 +47,7 @@ export const AttributeAnalyticsPopupMenu = ({ data }: NodeProps<AttributeAnalyti // </AccordionDetails> // <AccordionDetails className="accordionDetails"> // <div className="attributeButtons"> - // <span>See visualisation</span> + // <span>See visualization</span> // <span className="rightSideValue"> // <Visibility className="visualisationEye" /> // </span> diff --git a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx index 5ef056983dca184b60d49ce6790491b4993c319f..97eade305675cd747a20d6709f726d92eed8fcf4 100644 --- a/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx +++ b/libs/shared/lib/schema/pills/nodes/popup/popupmenus/attribute-analytics-popup-menu.tsx @@ -58,7 +58,7 @@ export const AttributeAnalyticsPopupMenu = ({ data }: NodeProps<AttributeAnalyti // </AccordionDetails> // <AccordionDetails className="accordionDetails"> // <div className="attributeButtons"> - // <span>See visualisation</span> + // <span>See visualization</span> // <span className="rightSideValue"> // <Visibility className="visualisationEye" /> // </span> diff --git a/libs/shared/lib/schema/schema-utils/flow-utils.ts b/libs/shared/lib/schema/schema-utils/flow-utils.ts index 284fb7a105ef96f76986d1c08e9715caffea75fd..ba4dd9699a54766a9aa340096cb1461e8d25eb12 100644 --- a/libs/shared/lib/schema/schema-utils/flow-utils.ts +++ b/libs/shared/lib/schema/schema-utils/flow-utils.ts @@ -4,7 +4,7 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ import { Position } from 'reactflow'; -import { BoundingBox } from '../../vis/shared/Types'; // TODO: MOVE ELSEWHERE +import { BoundingBox } from '../../vis/common'; // TODO: MOVE ELSEWHERE /** This is the interface to get the center of an edge */ export interface GetCenterParams { diff --git a/libs/shared/lib/sidebar/index.tsx b/libs/shared/lib/sidebar/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6b2a03c0cd9c8c4e39c7a1a8e30fcd201ef8b767 --- /dev/null +++ b/libs/shared/lib/sidebar/index.tsx @@ -0,0 +1,55 @@ +import React, { useState } from 'react'; +import { Button, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../components'; +import { Fullscreen, Schema as SchemaIcon, Search as SearchIcon } from '@mui/icons-material'; +import ColorMode from '../components/color-mode'; + +export type SideNavTab = 'Schema' | 'Search' | undefined; +const tabs: Array<{ + name: SideNavTab; + icon: JSX.Element; +}> = [ + { + name: 'Search', + icon: <SearchIcon />, + }, + { name: 'Schema', icon: <SchemaIcon /> }, +]; + +export function Sidebar({ onTab }: { onTab: (tab: SideNavTab) => void }) { + const [tab, setTab] = useState<SideNavTab>('Schema'); + + return ( + <div className="side-bar w-fit h-full flex shrink"> + <TooltipProvider delayDuration={100}> + <div className="w-11 flex flex-col items-center"> + {tabs.map((t) => ( + <Tooltip key={t.name}> + <TooltipTrigger asChild> + <Button + type="secondary" + variant="ghost" + size="sm" + iconComponent={t.icon} + onClick={() => { + if (tab === t.name) { + onTab(undefined); + setTab(undefined); + } else { + onTab(t.name); + setTab(t.name); + } + }} + additionalClasses={tab === t.name ? 'bg-secondary-100' : ''} + /> + </TooltipTrigger> + <TooltipContent side={'right'}>{t.name}</TooltipContent> + </Tooltip> + ))} + <div className="mt-auto mb-2"> + <ColorMode /> + </div> + </div> + </TooltipProvider> + </div> + ); +} diff --git a/libs/shared/lib/sidebar/search/searchbar.tsx b/libs/shared/lib/sidebar/search/searchbar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..83708591168d4218223525a6f6ab1d76bbffdea6 --- /dev/null +++ b/libs/shared/lib/sidebar/search/searchbar.tsx @@ -0,0 +1,182 @@ +import React, { useRef, useState, useEffect } from 'react'; +import { + AppDispatch, + useAppDispatch, + useGraphQueryResult, + useQuerybuilderGraph, + useRecentSearches, + useSchemaGraph, + useSearchResult, +} from '../../data-access'; +import { QueryMultiGraph } from '../../querybuilder'; +import { + CATEGORY_KEYS, + addRecentSearch, + addSearchResultData, + addSearchResultQueryBuilder, + addSearchResultSchema, +} from '../../data-access/store/searchResultSlice'; +import { filterData } from './similarity'; + +const SIMILARITY_THRESHOLD = 0.7; + +const CATEGORY_ACTIONS: { + [key in CATEGORY_KEYS]: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => void; +} = { + data: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { + dispatch(addSearchResultData(payload)); + }, + schema: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { + dispatch(addSearchResultSchema(payload)); + }, + querybuilder: (payload: { nodes: Record<string, any>[]; edges: Record<string, any>[] }, dispatch: AppDispatch) => { + dispatch(addSearchResultQueryBuilder(payload)); + }, +}; + +const SEARCH_CATEGORIES: CATEGORY_KEYS[] = Object.keys(CATEGORY_ACTIONS) as CATEGORY_KEYS[]; + +export function Searchbar() { + const inputRef = useRef<HTMLInputElement>(null); + const [search, setSearch] = useState<string>(''); + const searchbarRef = useRef<HTMLDivElement>(null); + const dispatch = useAppDispatch(); + const results = useSearchResult(); + const recentSearches = useRecentSearches(); + const schema = useSchemaGraph(); + const graphData = useGraphQueryResult(); + const querybuilderData = useQuerybuilderGraph(); + + const dataSources: { + [key: string]: { nodes: Record<string, any>[]; edges: Record<string, any>[] }; + } = { + data: graphData, + schema: schema, + querybuilder: querybuilderData as QueryMultiGraph, + }; + + useEffect(() => { + const handleKeyPress = (event: KeyboardEvent) => { + if (event.key === 'Enter') { + if (search !== '') { + dispatch(addRecentSearch(search)); + } + } + }; + window.addEventListener('keydown', handleKeyPress); + return () => window.removeEventListener('keydown', handleKeyPress); + }, [search]); + + useEffect(() => { + handleSearch(); + }, [search]); + + const handleSearch = () => { + let query = search.toLowerCase(); + const categories = search.match(/@[^ ]+/g); + + if (categories) { + categories.map((category) => { + query = query.replace(category, '').trim(); + const cat = category.substring(1); + + if (cat in CATEGORY_ACTIONS) { + const categoryAction = CATEGORY_ACTIONS[cat as CATEGORY_KEYS]; + const data = dataSources[cat]; + + const payload = { + nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), + edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), + }; + categoryAction(payload, dispatch); + } + }); + } else { + for (const category of SEARCH_CATEGORIES) { + const categoryAction = CATEGORY_ACTIONS[category]; + const data = dataSources[category]; + + const payload = { + nodes: filterData(query, data.nodes, SIMILARITY_THRESHOLD), + edges: filterData(query, data.edges, SIMILARITY_THRESHOLD), + }; + + categoryAction(payload, dispatch); + } + } + }; + + return ( + <div className="flex flex-col w-full p-2"> + <div className="w-full"> + <input + type="text" + ref={inputRef} + value={search} + onChange={(e) => setSearch(e.target.value)} + id="input-group-search" + className="block w-full p-2 ps-2 text-sm text-secondary-900 border border-secondary-300 rounded bg-secondary-50 focus:ring-blue-500 focus:border-blue-500 focus:ring-0" + placeholder="Search database" + ></input> + </div> + <div> + {recentSearches.length !== 0 && ( + <div className="px-3 pb-3"> + <p className="text-sm">Recent searches</p> + {recentSearches.slice(0, 3).map((term) => ( + <p key={term} className="ml-1 text-sm text-secondary-500 cursor-pointer" onClick={() => setSearch(term)}> + {term} + </p> + ))} + </div> + )} + {search !== '' && ( + <div className="z-10 w-full overflow-y-auto scroll h-full px-2 pb-2"> + {SEARCH_CATEGORIES.every((category) => results[category].nodes.length === 0 && results[category].edges.length === 0) ? ( + <div className="ml-1 text-sm"> + <p className="text-secondary-500">Found no matches...</p> + </div> + ) : ( + SEARCH_CATEGORIES.map((category, index) => { + if (results[category].nodes.length > 0 || results[category].edges.length > 0) { + return ( + <div key={index}> + <div className="flex justify-between p-2 text-lg"> + <p className="font-bold text-sm">{category.charAt(0).toUpperCase() + category.slice(1)}</p> + <p className="font-bold text-sm">{results[category].nodes.length + results[category].edges.length} results</p> + </div> + <div className="h-[1px] w-full bg-secondary-200"></div> + {Object.values(Object.values(results[category])) + .flat() + .map((item, index) => ( + <div + key={index} + className="flex flex-col hover:bg-secondary-300 px-2 py-1 cursor-pointer rounded ml-2" + title={JSON.stringify(item)} + onClick={() => { + CATEGORY_ACTIONS[category]( + { + nodes: results[category].nodes.includes(item) ? [item] : [], + edges: results[category].edges.includes(item) ? [item] : [], + }, + dispatch, + ); + }} + > + <div className="font-bold text-sm"> + {item?.key?.slice(0, 18) || item?.id?.slice(0, 18) || Object.values(item)?.[0]?.slice(0, 18)} + </div> + <div className="font-light text-secondary-800 text-xs">{JSON.stringify(item).substring(0, 40)}...</div> + </div> + ))} + </div> + ); + } else return <></>; + }) + )} + </div> + )} + </div> + </div> + ); +} diff --git a/apps/web/src/components/navbar/search/similarity.ts b/libs/shared/lib/sidebar/search/similarity.ts similarity index 96% rename from apps/web/src/components/navbar/search/similarity.ts rename to libs/shared/lib/sidebar/search/similarity.ts index a5714c0aa60c00d8045bf43790078d56f0a32ced..a6e0a9378a2917a27814806d1fd89d81d0f3183c 100644 --- a/apps/web/src/components/navbar/search/similarity.ts +++ b/libs/shared/lib/sidebar/search/similarity.ts @@ -30,7 +30,7 @@ const matches = (query: string, object: Record<string, any>): number => { return highestScore; }; -const jaroSimilarity = (s1: string, s2: string) => { +export const jaroSimilarity = (s1: string, s2: string) => { if (s1 == s2) return 1.0; const len1 = s1.length; diff --git a/libs/shared/lib/vis/common/index.ts b/libs/shared/lib/vis/common/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..fcb073fefcd6bebc8afd726f830e66828ebc5e90 --- /dev/null +++ b/libs/shared/lib/vis/common/index.ts @@ -0,0 +1 @@ +export * from './types'; diff --git a/libs/shared/lib/vis/shared/Types.tsx b/libs/shared/lib/vis/common/types.ts similarity index 61% rename from libs/shared/lib/vis/shared/Types.tsx rename to libs/shared/lib/vis/common/types.ts index 18616628186351da17989aa1487dde8cdd6e2ce2..52e1513b2d4204b568e2486b652c0d2389fe2160 100644 --- a/libs/shared/lib/vis/shared/Types.tsx +++ b/libs/shared/lib/vis/common/types.ts @@ -1,87 +1,73 @@ -/** - * 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) - */ +import { GraphQueryResult } from '../../data-access/store/graphQueryResultSlice'; +import { ML } from '../../data-access/store/mlSlice'; +import { SchemaGraph } from '../../schema'; +import type { AppDispatch } from '../../data-access'; +import { FC } from 'react'; +import { Visualizations } from '../hooks'; import { Edge, Node } from 'reactflow'; -/** - * List of schema elements for react flow - */ +export type VisualizationConfiguration = { [id: string]: any }; + +export type VISComponentType = { + displayName: keyof typeof Visualizations; + component: FC<any>; + settings: FC<any>; + configuration: { [id: string]: any }; +}; + +export type VisualizationPropTypes = { + data: GraphQueryResult; + schema: SchemaGraph; + ml: ML; + configuration: VisualizationConfiguration; + dispatch: AppDispatch; + handleHover: (val: any) => void; + handleSelect: (val: any) => void; +}; + export type SchemaElements = { nodes: Node[]; edges: Edge[]; selfEdges: Edge[]; }; -/** - * Point that has an x and y coordinate - */ export interface Point { x: number; y: number; } -/** - * Bounding box described by a top left and bottom right coordinate - */ export interface BoundingBox { topLeft: Point; bottomRight: Point; } -/** - * Typing for the Node Quality data of an entity. - * It is used for the Node quality analytics and will be displayed in the corresponding popup. - */ export interface NodeQualityDataForEntities { nodeCount: number; attributeNullCount: number; notConnectedNodeCount: number; - isAttributeDataIn: boolean; // is true when the data to display has arrived - - // for user interactions onClickCloseButton: () => void; } -/** - * Typing for the Node Quality data of a relation. - * It is used for the Node quality analytics and will be displayed in the corresponding popup. - */ export interface NodeQualityDataForRelations { nodeCount: number; attributeNullCount: number; - // from-entity node --relation--> to-entity node fromRatio: number; // the ratio of from-entity nodes to nodes that have this relation toRatio: number; // the ratio of to-entity nodes to nodes that have this relation - isAttributeDataIn: boolean; // is true when the data to display has arrived - - // for user interactions onClickCloseButton: () => void; } -/** - * Typing for the Node Quality popup of an entity or relation node. - */ export interface NodeQualityPopupNode extends Node { data: NodeQualityDataForEntities | NodeQualityDataForRelations; nodeID: string; //ID of the node for which the popup is } -/** - * Typing for the attribute analytics popup menu data of an entity or relation. - */ export interface AttributeAnalyticsData { nodeType: NodeType; nodeID: string; attributes: AttributeWithData[]; - // nullAmount: number; - isAttributeDataIn: boolean; // is true when the data to display has arrived - - // for user interactions onClickCloseButton: () => void; onClickPlaceInQueryBuilderButton: (name: string, type: string) => void; searchForAttributes: (id: string, searchbarValue: string) => void; @@ -89,7 +75,6 @@ export interface AttributeAnalyticsData { applyAttributeFilters: (id: string, category: AttributeCategory, predicate: string, percentage: number) => void; } -/** All possible options of categories of an attribute */ export enum AttributeCategory { categorical = 'Categorical', numerical = 'Numerical', @@ -97,21 +82,16 @@ export enum AttributeCategory { undefined = 'undefined', } -/** All possible options of node-types */ export enum NodeType { entity = 'entity', relation = 'relation', } -/** - * Typing for the attribute analytics popup menu of entity or relation nodes - */ export interface AttributeAnalyticsPopupMenuNode extends Node { nodeID: string; //ID of the node for which the popup is data: AttributeAnalyticsData; } -/** Typing of the attributes which are stored in the popup menu's */ export type AttributeWithData = { attribute: any; category: AttributeCategory; diff --git a/libs/shared/lib/vis/components/bar.tsx b/libs/shared/lib/vis/components/bar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..409690202d59a99b3f0208e381fd13659859d692 --- /dev/null +++ b/libs/shared/lib/vis/components/bar.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import { Button, Icon } from '../../components'; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { Add, Close, Fullscreen } from '@mui/icons-material'; +import { ControlContainer } from '../../components/controls'; +import { Visualizations } from '../hooks'; +import { VisualizationManagerType } from '../hooks'; + +type Props = { + manager: VisualizationManagerType; + fullSize: () => void; +}; + +export default function VisualizationBar({ manager, fullSize }: Props) { + const handleDragStart = (e: React.DragEvent<HTMLDivElement>, visId: string) => { + e.dataTransfer.setData('text/plain', visId); + }; + + const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => { + e.preventDefault(); + }; + + const handleDrop = (e: React.DragEvent<HTMLDivElement>, dropVisId: string) => { + e.preventDefault(); + const draggedVisId = e.dataTransfer.getData('text/plain'); + manager.reorderVisualizations({ draggedVisId, dropVisId }); + manager.changeActive(draggedVisId); + }; + + 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="flex items-center"> + <h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">Visualization</h1> + </div> + <div className="items-center shrink-0 px-0.5"> + <DropdownMenu.Root> + <DropdownMenu.Trigger> + <TooltipProvider delayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Add />} onClick={() => {}} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Add visualization</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </DropdownMenu.Trigger> + <DropdownMenu.Portal> + <DropdownMenu.Content className="bg-light p-1 rounded border"> + {Object.keys(Visualizations).map((key) => ( + <DropdownMenu.Item + key={key} + className="text-sm px-2 py-1 rounded cursor-pointer hover:bg-secondary-200" + onClick={(e) => { + manager.changeActive(key); + }} + > + {key} + </DropdownMenu.Item> + ))} + </DropdownMenu.Content> + </DropdownMenu.Portal> + </DropdownMenu.Root> + </div> + <div className="flex items-stretch divide-x divide-secondary-200 border-x border-secondary-200 overflow-x-auto -my-px"> + {manager.tabs.map((visId: string) => { + const isActive = manager.activeVisualization === visId; + return ( + <div + key={visId} + className={`flex items-center pl-2 pr-1 gap-1 cursor-pointer relative border-secondary-200 before:content-[''] before:absolute before:left-0 before:bottom-0 before:h-[2px] before:w-full ${isActive && 'before:bg-primary-500'} ${!isActive && 'before:bg-transparent hover:before:bg-secondary-300 hover:bg-secondary-200'}`} + onClick={() => manager.changeActive(visId)} + onDragStart={(e) => handleDragStart(e, visId)} + onDragOver={(e) => handleDragOver(e)} + onDrop={(e) => handleDrop(e, visId)} + draggable + > + <p className={`text-xs text-secondary-500 font-semibold ${isActive && 'text-secondary-950'}`}>{visId}</p> + <Button + type="secondary" + variant="ghost" + rounded + size="2xs" + iconComponent={<Close />} + onClick={(e) => { + e.stopPropagation(); + manager.deleteVisualization(visId); + }} + /> + </div> + ); + })} + </div> + <div className="shrink-0 sticky right-0 px-0.5 ml-auto items-center flex"> + <ControlContainer> + <TooltipProvider delayDuration={0}> + <Tooltip> + <TooltipTrigger asChild> + <Button type="secondary" variant="ghost" size="xs" iconComponent={<Fullscreen />} onClick={fullSize} /> + </TooltipTrigger> + <TooltipContent side={'top'}> + <p>Full screen</p> + </TooltipContent> + </Tooltip> + </TooltipProvider> + </ControlContainer> + </div> + </div> + ); +} diff --git a/libs/shared/lib/vis/components/config/components.tsx b/libs/shared/lib/vis/components/config/components.tsx new file mode 100644 index 0000000000000000000000000000000000000000..842f508ceeb6fda641ffba2f0e49861c044bb177 --- /dev/null +++ b/libs/shared/lib/vis/components/config/components.tsx @@ -0,0 +1,24 @@ +import React, { ReactNode } from 'react'; + +type SettingsContainerProps = { + children: ReactNode; +}; + +export function SettingsContainer({ children }: SettingsContainerProps) { + return <div className="">{children}</div>; +} + +type SettingsHeaderProps = { + name: string; + icon?: ReactNode; + onClickIcon?: () => void; +}; + +export function SettingsHeader({ name, icon, onClickIcon }: SettingsHeaderProps) { + return ( + <div className="flex justify-between items-center"> + <span className="text-xs font-bold">{name}</span> + {icon && icon} + </div> + ); +} diff --git a/libs/shared/lib/vis/components/config/index.tsx b/libs/shared/lib/vis/components/config/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..2f98579c75ad64611ab47810bdcfc20c8bb14bbe --- /dev/null +++ b/libs/shared/lib/vis/components/config/index.tsx @@ -0,0 +1,2 @@ +export { ConfigPanel } from './panel'; +export { SettingsContainer, SettingsHeader } from './components'; diff --git a/libs/shared/lib/vis/components/config/panel.tsx b/libs/shared/lib/vis/components/config/panel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..722e241fb52c1746ee88b419da47832282861a71 --- /dev/null +++ b/libs/shared/lib/vis/components/config/panel.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { Button, Icon } from '../../../components'; +import { Delete, Person } from '@mui/icons-material'; +import { Input } from '../../../components/inputs'; +import { VISUALIZATION_TYPES } from '../../hooks'; +import { VisualizationManagerType } from '../../hooks'; +import { SettingsHeader } from './components'; +import { useSessionCache } from '../../../data-access'; + +type Props = { + manager: VisualizationManagerType; +}; + +export function ConfigPanel({ manager }: Props) { + const session = useSessionCache(); + + const buildInfo = import.meta.env.GRAPHPOLARIS_VERSION; + + return ( + <div className="flex flex-col w-full"> + {manager.activeVisualization ? ( + <> + <div className="border-b py-2"> + <div className="flex justify-between items-center px-4 py-2"> + <span className="text-xs font-bold">Visualization</span> + <Button + type="secondary" + variant="ghost" + size="xs" + iconComponent={<Delete />} + onClick={() => { + if (manager.activeVisualization) manager.deleteVisualization(manager.activeVisualization); + }} + /> + </div> + <div className="flex justify-between items-center px-4 py-1"> + <span className="text-xs font-normal">Type</span> + <div className="w-36"> + <Input type="dropdown" size="xs" options={VISUALIZATION_TYPES} value={manager.activeVisualization} onChange={() => {}} /> + </div> + </div> + <div className="flex justify-between items-center px-4 py-1"> + <span className="text-xs font-normal">Name</span> + <input type="text" className="border rouded text-xs w-36" value={manager.activeVisualization} onChange={() => {}} /> + </div> + </div> + {manager.activeVisualization && ( + <div className="border-b p-4"> + <SettingsHeader name="Configuration" /> + {manager.renderSettings()} + </div> + )} + </> + ) : ( + <div> + {session && session.currentSaveState && ( + <div className="flex flex-col p-4 border-b"> + <span className="text-sm font-bold">Connection details</span> + <span className="text-xs">Database: {session.saveStates[session.currentSaveState].name}</span> + <span className="text-xs">Port: {session.saveStates[session.currentSaveState].db.port}</span> + <span className="text-xs">Protocol: {session.saveStates[session.currentSaveState].db.protocol}</span> + </div> + )} + </div> + )} + + {buildInfo === 'dev' && ( + <div className="mt-auto p-2 bg-light"> + <Button + type="primary" + variant="outline" + size="xs" + label="Report an issue" + onClick={() => + window.open( + 'https://app.asana.com/-/login?u=https%3A%2F%2Fform.asana.com%2F%3Fk%3D2QEC88Dl7ETs2wYYWjkMXg%26d%3D1206648675960041&error=01', + '_blank', + ) + } + additionalClasses="block w-full" + /> + </div> + )} + </div> + ); +} diff --git a/libs/shared/lib/vis/components/panel.tsx b/libs/shared/lib/vis/components/panel.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8e5653c1913c7fca9e41a0cb7a6d00f202e4c534 --- /dev/null +++ b/libs/shared/lib/vis/components/panel.tsx @@ -0,0 +1,30 @@ +import React, { useMemo } from 'react'; +import { useGraphQueryResult, useQuerybuilderGraph } from '@graphpolaris/shared/lib/data-access'; +import VisualizationBar from './bar'; +import { VisualizationManagerType } from '../hooks'; +import { Recommender, NoData, Querying } from '../views'; + +export const VisualizationPanel = ({ manager, fullSize }: { manager: VisualizationManagerType; fullSize: () => void }) => { + const query = useQuerybuilderGraph(); + const graphQueryResult = useGraphQueryResult(); + + const renderContent = useMemo(() => { + if (graphQueryResult.queryingBackend) { + return <Querying />; + } else if (graphQueryResult.nodes.length === 0) { + return <NoData dataAvailable={query.nodes.length > 0} />; + } else if (manager.tabs.length === 0) { + return <Recommender onClick={(id: string) => manager.changeActive(id)} />; + } + return <div className="w-full h-full flex">{manager.renderComponent()}</div>; + }, [graphQueryResult, manager]); + + return ( + <div className="vis-panel h-full w-full flex flex-col border bg-light"> + <VisualizationBar manager={manager} fullSize={fullSize} /> + <div className="grow overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}> + {renderContent} + </div> + </div> + ); +}; diff --git a/libs/shared/lib/vis/configuration/advanced/advanced.tsx b/libs/shared/lib/vis/configuration/advanced/advanced.tsx deleted file mode 100644 index 287aa679a1d7939b8e5aeb00a097b59a0d2893d4..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/advanced/advanced.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import ReactJSONView from 'react-json-view'; -import { Configuration, PanelProps } from '../panel/panel.types'; - -interface AdvancedPanelProps extends PanelProps { - state: Configuration; - update: (configType: string, key: string, value: any) => void; // Adjusted type for update function -} - -export default function AdvancedPanel({ state, update }: AdvancedPanelProps) { - return ( - state && ( - <div className="m-2"> - <ReactJSONView - src={state} - name={false} - collapsed={1} - quotesOnKeys={false} - displayDataTypes={false} - onEdit={(v) => update('', '', v.updated_src)} - /> - </div> - ) - ); -} diff --git a/libs/shared/lib/vis/configuration/advanced/index.ts b/libs/shared/lib/vis/configuration/advanced/index.ts deleted file mode 100644 index a7584d4b2419e2fc6b2b26fe9a4c643639719453..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/advanced/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as AdvancedPanel } from './advanced'; diff --git a/libs/shared/lib/vis/configuration/encodings/accessor.tsx b/libs/shared/lib/vis/configuration/encodings/accessor.tsx deleted file mode 100644 index 63db7d674fda32ce1149e0570a54f20a38827327..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/encodings/accessor.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import Input from '@graphpolaris/shared/lib/components/inputs'; -import { useGraphQueryResultMeta } from '@graphpolaris/shared/lib/data-access'; -import { DimensionType } from '@graphpolaris/shared/lib/schema'; -import { CompressedElement } from '@graphpolaris/shared/lib/data-access/statistics'; - -type Props = { - value: string | undefined; - onChange: (val: string | number) => void; - element: 'node' | 'edge'; - dimension?: DimensionType[]; - disabled?: boolean; -}; - -const extractAttributesByDimension = (summaryGraph: CompressedElement, dimensions: DimensionType[]): string[] => { - const selectedAttributes: { [label: string]: string[] } = {}; - - Object.keys(summaryGraph.types).map((label) => { - selectedAttributes[label] = []; - Object.keys(summaryGraph.types[label].attributes).map((attribute) => { - const dim: DimensionType = summaryGraph.types[label]?.attributes[attribute].dimension as DimensionType; - dimensions.includes(dim) && selectedAttributes[label].push(attribute); - }); - }); - - let commonAttributes: Set<string> = new Set(); - Object.values(selectedAttributes).forEach((attributes) => { - if (commonAttributes.size === 0) { - commonAttributes = new Set(attributes); - } else { - commonAttributes = new Set([...commonAttributes].filter((attr) => attributes.includes(attr))); - } - }); - - return commonAttributes ? Array.from(commonAttributes) : []; -}; - -export default function Accessor({ value, onChange, element, dimension, disabled = false }: Props) { - const resultStatistics = useGraphQueryResultMeta(); - const [options, setOptions] = useState<string[]>([]); - const [loading, setLoading] = useState<boolean>(true); - - useEffect(() => { - setLoading(Object.keys(resultStatistics.nodes.types).length === 0 && Object.keys(resultStatistics.edges.types).length === 0); - }, [resultStatistics]); - - useEffect(() => { - if (!loading) { - setOptions(extractAttributesByDimension(resultStatistics[element === 'node' ? 'nodes' : 'edges'], dimension || ['categorical'])); - disabled = true; - } else { - disabled = false; - } - }, [resultStatistics, loading, element, dimension]); - - return ( - <div className="mb-2"> - <Input type="dropdown" value={value} onChange={(val: string | number) => onChange(val)} options={options} disabled={disabled} /> - </div> - ); -} diff --git a/libs/shared/lib/vis/configuration/encodings/encoding.tsx b/libs/shared/lib/vis/configuration/encodings/encoding.tsx deleted file mode 100644 index 96acc27ec4004cf5d13be05292a0024d201cd1b1..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/encodings/encoding.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import React, { useState } from 'react'; -import Accessor from './accessor'; -import Info from '@graphpolaris/shared/lib/components/info'; -import Selector from './selector'; -import { PanelProps } from '../panel/panel.types'; - -export default function EncodingPanel({ state, update }: PanelProps) { - const [encodingUpdates, setEncodingUpdates] = useState<{ - [id: string]: { - marking: any; - accessorPath: string; - }; - }>({}); - - const updateEncoding = (encoding: string, val: any, type: 'accessorPath' | 'marking') => { - setEncodingUpdates((prevState) => { - if (prevState) { - return { - ...prevState, - [encoding]: { - ...(prevState[encoding] || {}), - [type]: val, - }, - }; - } else { - return prevState; - } - }); - }; - - return ( - state && ( - <div> - {Object.keys(state).map((key) => { - const item = state[key]; - const value = encodingUpdates[key] ? encodingUpdates[key] : { marking: undefined, accessorPath: undefined }; - - return ( - <div key={key} className="bg-secondary-50 p-2 m-1"> - <div className="flex items-center justify-between"> - <h1 className="text-xs">{item.label ? item.label : key}</h1> - {item.description && <Info tooltip={item.description} />} - </div> - <Accessor - value={value.accessorPath} - onChange={(value: string | number) => updateEncoding(key, value, 'accessorPath')} - // onChange={(value: string | number) => update(key, { ...state[key], accessorPath: value })} - element={item.element} - dimension={item.dimension ?? []} - /> - {value.accessorPath && ( - <Selector - key={key} - selectorType={item.selector} - elementType={item.element} - marking={value.marking} - onChange={(value: any) => updateEncoding(key, value, 'marking')} - accessorPath={value.accessorPath} - /> - )} - </div> - ); - })} - </div> - ) - ); -} diff --git a/libs/shared/lib/vis/configuration/encodings/index.ts b/libs/shared/lib/vis/configuration/encodings/index.ts deleted file mode 100644 index 056c6ae4da4344f331c8bf5368e0d41fd1a88994..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/encodings/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { Encoding, EncodingTypes, EncodingProps } from './encodings.types'; -export { default as EncodingPanel } from './encoding'; diff --git a/libs/shared/lib/vis/configuration/encodings/selector.tsx b/libs/shared/lib/vis/configuration/encodings/selector.tsx deleted file mode 100644 index cc8e31c3c2d8dc8a9ed699bd0d464b11e910dea6..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/encodings/selector.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { EncodingSelector } from './selectors'; -import { useGraphQueryResultMeta } from '@graphpolaris/shared/lib/data-access'; -import { ElementType } from './encodings.types'; -import { DimensionType } from '@graphpolaris/shared/lib/schema'; - -type Props = { - selectorType: keyof typeof EncodingSelector; - marking: any; - elementType: ElementType; - onChange: (val: any) => void; - accessorPath: string; -}; - -export default function Selector({ selectorType, elementType, marking, onChange, accessorPath }: Props) { - const graphStatistics = useGraphQueryResultMeta(); - const SelectorComponent = EncodingSelector[selectorType]; - const [statistics, setStatistics] = useState<any>(); - const [dimension, setDimension] = useState<DimensionType>('categorical'); - - useEffect(() => { - const group = graphStatistics[elementType === 'node' ? 'nodes' : 'edges']; - - const combinedValues = Array.from( - new Set( - Object.keys(group.types).flatMap((label) => { - const obj = group.types[label].attributes[accessorPath]; - const { dimension } = obj; - setDimension(dimension); - - if (dimension === 'categorical') { - const { values } = obj; - return values || []; - } else if (dimension === 'numerical') { - console.log(obj); - const { min, max } = obj.statistics; - return [min, max]; - } else if (dimension === 'temporal') { - const { minDate, maxDate } = obj.statistics; - return [minDate, maxDate]; - } else if (dimension === 'spatial') { - const { boundingBox } = obj.statistics; - return boundingBox; - } - return []; - }), - ), - ); - - setStatistics(combinedValues); - }, [accessorPath]); - - return <SelectorComponent marking={marking} onChange={onChange} dimension={dimension} statistics={statistics} />; -} diff --git a/libs/shared/lib/vis/configuration/index.ts b/libs/shared/lib/vis/configuration/index.ts deleted file mode 100644 index 3ded492f1ed3e4796ad053193fbce2df8a5ec6c9..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as VisualizationDialog } from './panel/panel'; diff --git a/libs/shared/lib/vis/configuration/interactions/index.ts b/libs/shared/lib/vis/configuration/interactions/index.ts deleted file mode 100644 index 61687e06e2fff5a8911823cc0ee1359e047f91e5..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/interactions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { Interaction, InteractionTypes, InteractionProps } from './interaction.types'; -export { default as InteractionPanel } from './interaction'; diff --git a/libs/shared/lib/vis/configuration/interactions/interaction.tsx b/libs/shared/lib/vis/configuration/interactions/interaction.tsx deleted file mode 100644 index 5d25826ebfba7ce5ed00b37114a89276b489f1d7..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/interactions/interaction.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import { PanelProps } from '../panel/panel.types'; - -export default function InteractionPanel({ state, update }: PanelProps) { - return ( - state && ( - <div> - <p>Interaction settings</p> - </div> - ) - ); -} diff --git a/libs/shared/lib/vis/configuration/interactions/interaction.types.ts b/libs/shared/lib/vis/configuration/interactions/interaction.types.ts deleted file mode 100644 index bb81b490925dd4e432865958c757150bdef092e8..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/interactions/interaction.types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { InputProps } from '@graphpolaris/shared/lib/components/inputs'; - -export type Interaction = InputProps & { condition?: (config: Record<string, any>) => boolean }; - -export type InteractionTypes = { [id: string]: Interaction }; - -export type InteractionProps = { [K in keyof InteractionTypes]?: any }; diff --git a/libs/shared/lib/vis/configuration/panel/panel-header.tsx b/libs/shared/lib/vis/configuration/panel/panel-header.tsx deleted file mode 100644 index f28390360f4b7a1cb846cb69bd5e47e075a62f63..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/panel/panel-header.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import React from 'react'; -import { FormTitle } from '@graphpolaris/shared/lib/components/forms'; -import { AutoAwesome, CandlestickChart, LockOpen, Settings } from '@mui/icons-material'; -import { useVisualization } from '@graphpolaris/shared/lib/data-access'; -import PanelItem from './panel-item'; - -type Props = { - onClose: any; - activeTab: number; - setActiveTab: any; -}; - -export default function PanelHeader({ onClose, activeTab, setActiveTab }: Props) { - const vis = useVisualization(); - - let encodingsExist: boolean = !!vis.settings[vis.activeVisualization || '']?.encodings || false; - let settingsExist: boolean = !!vis.settings[vis.activeVisualization || '']?.settings || false; - let interactionsExist: boolean = !!vis.settings[vis.activeVisualization || '']?.interactions || false; - - return ( - <div className="flex flex-col pt-2 bg-secondary-100"> - <FormTitle title="Settings" onClose={onClose} /> - <ul className="flex flex-wrap pt-4 pl-5 text-sm font-medium text-center text-gray-500 dark:text-gray-400"> - {encodingsExist && ( - <PanelItem active={activeTab === 0} onClick={() => setActiveTab(0)} icon={<AutoAwesome />} tooltip="Encodings" /> - )} - {settingsExist && <PanelItem active={activeTab === 1} onClick={() => setActiveTab(1)} icon={<Settings />} tooltip="Settings" />} - {interactionsExist && ( - <PanelItem active={activeTab === 2} onClick={() => setActiveTab(2)} icon={<CandlestickChart />} tooltip="Interactions" /> - )} - {(settingsExist || encodingsExist || interactionsExist) && ( - <PanelItem active={activeTab === 3} onClick={() => setActiveTab(3)} icon={<LockOpen />} tooltip="Advanced" /> - )} - </ul> - </div> - ); -} diff --git a/libs/shared/lib/vis/configuration/panel/panel-item.tsx b/libs/shared/lib/vis/configuration/panel/panel-item.tsx deleted file mode 100644 index 05623c8f0fbfbcd59f72bb6dafdfe6327b66fc30..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/panel/panel-item.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import React from 'react'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@graphpolaris/shared/lib/components/tooltip'; - -type PanelItemProps = { - active: boolean; - onClick: () => void; - icon: JSX.Element; - tooltip: string; -}; - -export default function PanelItem({ active, onClick, icon, tooltip }: PanelItemProps) { - return ( - <li - className={`me-2 inline-block bg-secondary-100 cursor-pointer ${active && 'border-b-2 border-primary-200 text-primary-200'}`} - onClick={onClick} - > - <TooltipProvider delayDuration={0}> - <Tooltip> - <TooltipTrigger>{icon}</TooltipTrigger> - <TooltipContent side={'top'}> - <p>{tooltip}</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - </li> - ); -} diff --git a/libs/shared/lib/vis/configuration/panel/panel.tsx b/libs/shared/lib/vis/configuration/panel/panel.tsx deleted file mode 100644 index 62ea4bca2a9d84db836b604ad5ed88a56ef5fe50..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/panel/panel.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { FormActions, FormBody, FormCard, FormDiv, FormHBar } from '@graphpolaris/shared/lib/components/forms'; -import { DialogProps } from '@graphpolaris/shared/lib/components/Dialog'; -import PanelHeader from './panel-header'; -import { SettingsPanel } from '../settings'; -import { AdvancedPanel } from '../advanced'; -import { EncodingPanel } from '../encodings'; -import { InteractionPanel } from '../interactions'; -import { useAppDispatch, useVisualization } from '@graphpolaris/shared/lib/data-access'; -import { updateConfiguration } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; -import { ConfigTypes, Configuration } from './panel.types'; - -export default function VisualizationDialog(props: DialogProps) { - const dispatch = useAppDispatch(); - const vis = useVisualization(); - const [activeTab, setActiveTab] = useState<number>(1); - const [configuration, setConfiguration] = useState<Configuration>({}); - - useEffect(() => { - if (vis.activeVisualization) { - setConfiguration({ - general: vis.settings.general ?? {}, - settings: vis.settings[vis.activeVisualization]?.settings ?? {}, - encodings: vis.settings[vis.activeVisualization]?.encodings ?? {}, - interactions: vis.settings[vis.activeVisualization]?.interactions ?? {}, - }); - } - }, [vis]); - - const handlePanelUpdate = (configType: ConfigTypes, key: string, value: any) => { - if (configType === 'configuration') { - setConfiguration(value); - } else { - setConfiguration((prevState) => ({ - ...prevState, - [configType]: { - [key]: value, - ...prevState[configType], - }, - })); - } - }; - - const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => { - e.preventDefault(); - dispatch(updateConfiguration(configuration)); - props.onClose(); - }; - - return ( - <> - {props.open && ( - <FormDiv> - <FormCard> - <FormBody onSubmit={handleSubmit}> - <PanelHeader onClose={props.onClose} activeTab={activeTab} setActiveTab={setActiveTab} /> - {activeTab === 0 && ( - <EncodingPanel state={configuration.encodings} update={(key, value) => handlePanelUpdate('encodings', key, value)} /> - )} - {activeTab === 1 && ( - <SettingsPanel - state={{ general: configuration.general, settings: configuration.settings }} - update={(configType, key, value) => handlePanelUpdate(configType, key, value)} - /> - )} - {activeTab === 2 && ( - <InteractionPanel - state={configuration.interactions} - update={(key, value) => handlePanelUpdate('interactions', key, value)} - /> - )} - {activeTab === 3 && <AdvancedPanel state={configuration} update={(value) => handlePanelUpdate('configuration', '', value)} />} - - <FormHBar /> - <FormActions onClose={props.onClose} /> - </FormBody> - </FormCard> - </FormDiv> - )} - </> - ); -} diff --git a/libs/shared/lib/vis/configuration/panel/panel.types.ts b/libs/shared/lib/vis/configuration/panel/panel.types.ts deleted file mode 100644 index 46edb57481764e8df92416cb6ec9e626205b22e0..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/panel/panel.types.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type ConfigTypes = 'configuration' | 'general' | 'settings' | 'encodings' | 'interactions'; - -export type PanelProps = { - state: any; - update: (configType: ConfigTypes, key: string, value: any) => void; -}; - -export type Configuration = { - general?: any; - settings?: any; - encodings?: any; - interactions?: any; -}; diff --git a/libs/shared/lib/vis/configuration/settings/index.ts b/libs/shared/lib/vis/configuration/settings/index.ts deleted file mode 100644 index 825e0031f676fb1604549de65832fce77fe9de90..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/settings/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type { Setting, SettingTypes, SettingProps } from './settings.types'; -export { default as SettingsPanel } from './settings'; diff --git a/libs/shared/lib/vis/configuration/settings/settings.tsx b/libs/shared/lib/vis/configuration/settings/settings.tsx deleted file mode 100644 index 1255476dca7bf009290291bbe241e0a4103b9e09..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/settings/settings.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react'; -import { FormControl } from '@graphpolaris/shared/lib/components/forms'; -import Input from '@graphpolaris/shared/lib/components/inputs'; -import { PanelProps } from '../panel/panel.types'; - -export default function SettingsPanel({ state, update }: PanelProps) { - return ( - state && ( - <div> - <div> - {Object.keys(state?.general).map((key) => ( - <FormControl key={key}> - <Input - {...state.general[key]} - value={state.general[key]?.value as any} - onChange={(value: any) => update('general', key, { ...state[key].general, value: value })} - /> - </FormControl> - ))} - </div> - - <div> - {Object.keys(state?.settings).map((key) => { - const currentSetting = state.settings[key]; - const shouldShowSetting = currentSetting.condition ? currentSetting.condition?.(state.settings) : true; - return ( - shouldShowSetting && ( - <div key={key} className="bg-secondary-50 p-2 m-2"> - <FormControl> - <Input - {...state.settings[key]} - value={state.settings[key]?.value as any} - onChange={(value: any) => update('settings', key, { ...state.settings[key], value: value })} - /> - </FormControl> - </div> - ) - ); - })} - </div> - </div> - ) - ); -} diff --git a/libs/shared/lib/vis/configuration/settings/settings.types.ts b/libs/shared/lib/vis/configuration/settings/settings.types.ts deleted file mode 100644 index 9c93ba813d26201c93681f9346a39fd64914d5a9..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/configuration/settings/settings.types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { InputProps } from '@graphpolaris/shared/lib/components/inputs'; - -export type Setting = InputProps & { - condition?: (config: Record<string, any>) => boolean; - description?: string; -}; - -export type SettingTypes = { [id: string]: Setting }; - -export type SettingProps = { [K in keyof SettingTypes]: any }; diff --git a/libs/shared/lib/vis/hooks/hooks.types.ts b/libs/shared/lib/vis/hooks/hooks.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..fca458010aee0de7652f0e50659fe09911c14ef9 --- /dev/null +++ b/libs/shared/lib/vis/hooks/hooks.types.ts @@ -0,0 +1,12 @@ +import React from 'react'; + +export type VisualizationManagerType = { + renderComponent: () => React.ReactNode; + renderSettings: () => React.ReactNode; + activeVisualization: string | undefined; + activeType: string | undefined; + tabs: string[]; + changeActive: (id: string) => void; + reorderVisualizations: (args: { draggedVisId: string; dropVisId: string }) => void; + deleteVisualization: (id: string) => void; +}; diff --git a/libs/shared/lib/vis/hooks/index.ts b/libs/shared/lib/vis/hooks/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..bfd4ddd04972baccc4ef356d57738dc2a95fddc2 --- /dev/null +++ b/libs/shared/lib/vis/hooks/index.ts @@ -0,0 +1,2 @@ +export { useVisualizationManager, Visualizations, VISUALIZATION_TYPES } from './useVisualizationManager'; +export type { VisualizationManagerType } from './hooks.types'; diff --git a/libs/shared/lib/vis/hooks/useVisualizationManager.tsx b/libs/shared/lib/vis/hooks/useVisualizationManager.tsx new file mode 100644 index 0000000000000000000000000000000000000000..b79925468502dbb18780df854330991a41d3f09e --- /dev/null +++ b/libs/shared/lib/vis/hooks/useVisualizationManager.tsx @@ -0,0 +1,154 @@ +import React, { useState, useMemo, useEffect, Suspense } from 'react'; +import { VISComponentType, VisualizationConfiguration } from '../common'; +import { + removeVisualization, + reorderVisState, + setActiveVisualization, + updateVisualization, +} from '../../data-access/store/visualizationSlice'; +import { + useAppDispatch, + useGraphQueryResult, + useGraphQueryResultMeta, + useML, + useSchemaGraph, + useSessionCache, + useVisualization, +} from '../../data-access'; +import { VisualizationManagerType } from '../hooks'; +import { HoverType, SelectType, addHover, addSelect } from '../../data-access/store/interactionSlice'; + +export const Visualizations: Record<string, Function> = { + TableVis: () => import('../visualizations/tablevis/tablevis'), + PaohVis: () => import('../visualizations/paohvis/paohvis'), + RawJSONVis: () => import('../visualizations/rawjsonvis/rawjsonvis'), + NodeLinkVis: () => import('../visualizations/nodelinkvis/nodelinkvis'), + MatrixVis: () => import('../visualizations/matrixvis/matrixvis'), + SemanticSubstratesVis: () => import('../visualizations/semanticsubstratesvis/semanticsubstratesvis'), + // MapVis: () => import('../visualizations/mapvis/mapvis'), +}; + +export const VISUALIZATION_TYPES: string[] = Object.keys(Visualizations); + +export const useVisualizationManager = (): VisualizationManagerType => { + const dispatch = useAppDispatch(); + const session = useSessionCache(); + const ml = useML(); + const schema = useSchemaGraph(); + const graphQueryResult = useGraphQueryResult(); + const meta = useGraphQueryResultMeta(); + const { activeVisualization, openVisualizations } = useVisualization(); + + const [configuration, setConfiguration] = useState<any>(); + const [visualization, setVisualization] = useState<VISComponentType>(); + const [selected, setSelected] = useState<any>(); + const activeType = useMemo( + () => (activeVisualization ? openVisualizations[activeVisualization]?.displayName : undefined), + [openVisualizations, activeVisualization], + ); + const tabs = useMemo(() => (Object.keys(openVisualizations).length ? Object.keys(openVisualizations) : []), [openVisualizations]); + + useEffect(() => { + loadVisualization(); + }, [activeVisualization]); + + const loadVisualization = async () => { + if (activeVisualization && Visualizations[activeVisualization]) { + const componentModule = await Visualizations[activeVisualization](); + const component = componentModule.default; + + if (!(activeVisualization in Object.keys(openVisualizations))) { + // Visualization doesn't yet exist so add its configuration + const configuration = component.configuration; + dispatch(updateVisualization({ id: activeVisualization, settings: configuration })); + setConfiguration(configuration); + } else { + setConfiguration(openVisualizations[activeVisualization]); + } + + setVisualization(component); + } + }; + + const changeActive = (id: string) => { + dispatch(setActiveVisualization(id)); + }; + + const deleteVisualization = (id: string) => { + dispatch(removeVisualization(id)); + if (Object.keys(openVisualizations).length > 0) { + const newActive = tabs.find((v: string) => id !== v); + changeActive(newActive || ''); + } + }; + + const reorderVisualizations = ({ draggedVisId, dropVisId }: { draggedVisId: string; dropVisId: string }) => { + const settingsCopy = { ...openVisualizations }; + const keys = Object.keys(settingsCopy); + const draggedIndex = keys.indexOf(draggedVisId); + const dropIndex = keys.indexOf(dropVisId); + + if (draggedIndex !== -1 && dropIndex !== -1) { + keys.splice(dropIndex, 0, draggedVisId); + const newSettings: { [id: string]: VisualizationConfiguration } = {}; + keys.forEach((key) => { + newSettings[key] = settingsCopy[key]; + }); + + dispatch(reorderVisState(newSettings)); + } + }; + + const handleHover = (item: HoverType | undefined) => { + dispatch(addHover(item)); + }; + + const handleSelect = (item: SelectType | undefined) => { + dispatch(addSelect(item)); + }; + + const updateSettings = (newSettings: any) => { + if (activeVisualization) { + const updatedSettings = { ...configuration, ...newSettings }; + setConfiguration(updatedSettings); + dispatch(updateVisualization({ id: activeVisualization, settings: updatedSettings })); + } + }; + + const renderSettings = () => { + return ( + visualization?.settings && + configuration && <visualization.settings configuration={configuration} graph={meta} updateSettings={updateSettings} /> + ); + }; + + // TODO: we should remove the renderable part of this useFunction into its own component, since this here is an anti-pattern + const renderComponent = () => { + return ( + <Suspense fallback={<div>Loading...</div>}> + {visualization?.component && configuration && ( + <visualization.component + data={graphQueryResult} + schema={schema} + ml={ml} + configuration={configuration} + dispatch={dispatch} + handleHover={handleHover} + handleSelect={handleSelect} + /> + )} + </Suspense> + ); + }; + + return { + renderComponent, + renderSettings, + activeVisualization, + activeType, + tabs, + changeActive, + reorderVisualizations, + deleteVisualization, + }; +}; diff --git a/libs/shared/lib/vis/index.ts b/libs/shared/lib/vis/index.ts index 92ed4302df37a1cab0b048c461ceb6e0efbac8dd..ac473812c0eb2ee567344f3224e7dfa6c367b18f 100644 --- a/libs/shared/lib/vis/index.ts +++ b/libs/shared/lib/vis/index.ts @@ -1 +1 @@ -export * from './visualizationPanel'; +export * from './components/panel'; diff --git a/libs/shared/lib/vis/shared/AttributeDataType.test.tsx b/libs/shared/lib/vis/shared/AttributeDataType.test.tsx deleted file mode 100644 index 43f9fe1b48895a00bbd573445bb52dc4bb7fbb42..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/AttributeDataType.test.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { assert, describe, expect, it } from 'vitest'; - -import { isAttributeDataEntity } from './AttributeDataType'; - -describe('AttributeDataType', () => { - it('true', () => { - expect(true).toEqual(true); - }); -}); - -// /** -// * 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) -// */ -// -// import { isAttributeDataEntity, isAttributeDataRelation } from './AttributeDataType'; -// import { -// mockAttributeDataNLDifferentTypes1, -// mockAttributeDataNLDifferentTypes2, -// mockAttributeDataNLDifferentTypes3, -// mockAttributeDataNLEdge1, -// mockAttributeDataNLInvalidLengthNumerical, -// mockAttributeDataNLInvalidType, -// mockAttributeDataNLNode1, -// mockAttributeDataNLNode1AttributeArrayIsNoArray, -// mockAttributeDataNLUniqueCategoricalValuesIsNoArray, -// mockAttributeDataNLUniqueCategoricalValuesLengthIsZero, -// } from '@graphpolaris/shared/lib/mock-data/schema/MockAttributeDataBatchedNL'; -// -// describe('AttributeDataType', () => { -// it('should return true for a valid attributeData object with an entity', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLNode1)).toEqual(true); -// }); -// -// it('should return true for a valid attributeData object with an relation', () => { -// expect(isAttributeDataRelation(mockAttributeDataNLEdge1)).toEqual(true); -// }); -// -// it('should return false if the attributes array is no array', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLNode1AttributeArrayIsNoArray)).toEqual(false); -// }); -// -// it('should return false if the unique-categorical-values array is no array', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLUniqueCategoricalValuesIsNoArray)).toEqual( -// false, -// ); -// }); -// -// it('should return false if the unique-categorical-values array has length 0, while having "Categorical" type', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLUniqueCategoricalValuesLengthIsZero)).toEqual( -// false, -// ); -// }); -// -// it('should return false if the unique-categorical-values have different types', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLDifferentTypes1)).toEqual(false); -// expect(isAttributeDataEntity(mockAttributeDataNLDifferentTypes2)).toEqual(false); -// expect(isAttributeDataEntity(mockAttributeDataNLDifferentTypes3)).toEqual(false); -// }); -// -// it('should return false if the unique-categorical-values array has an element with an invalid type', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLInvalidType)).toEqual(false); -// }); -// -// it('should return false if the unique-categorical-values array contains an element while the category is not "Categorical', () => { -// expect(isAttributeDataEntity(mockAttributeDataNLInvalidLengthNumerical)).toEqual(false); -// }); -// }); diff --git a/libs/shared/lib/vis/shared/AttributeDataType.tsx b/libs/shared/lib/vis/shared/AttributeDataType.tsx deleted file mode 100644 index 2ba60eea09631cc9291e945975e7c4dc0e42b407..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/AttributeDataType.tsx +++ /dev/null @@ -1,119 +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) - */ -import { AttributeData, AttributeFromAttributeData } from './InputDataTypes'; - -/** - * Checks if an object has all the properties of an attributeData type. If true, the object will be casted to attributeDataType - * @param {any} object The object to check if it is an attributeData object. - * @returns If true, the object is a of type attributeDataType. - */ -export function isAttributeDataEntity(object: any): object is AttributeData { - if ( - typeof object === 'object' && - 'id' in object && - object.id != undefined && - 'length' in object && - object.length != undefined && - 'connectedRatio' in object && - object.connectedRatio != undefined && - 'summedNullAmount' in object && - object.summedNullAmount != undefined && - 'attributes' in object && - object.attributes != undefined - ) { - // Check the structure of a node. - return ( - typeof object.id == 'string' && - typeof object.length == 'number' && - typeof object.connectedRatio == 'number' && - typeof object.summedNullAmount == 'number' && - isAttributeArray(object.attributes) - ); - } - return false; -} - -/** - * Checks if an object has all the properties of an attributeData type. If true, the object will be casted to attributeDataType - * @param {any} object The object to check if it is an attributeData object. - * @returns If true, the object is a of type attributeDataType. - */ -export function isAttributeDataRelation(object: any): object is AttributeData { - if ( - typeof object === 'object' && - 'id' in object && - object.id != undefined && - 'length' in object && - object.length != undefined && - 'fromRatio' in object && - object.fromRatio != undefined && - 'toRatio' in object && - object.toRatio != undefined && - 'summedNullAmount' in object && - object.summedNullAmount != undefined && - 'attributes' in object && - object.attributes != undefined - ) { - // Check the structure of a edge. - return ( - typeof object.id == 'string' && - typeof object.length == 'number' && - typeof object.fromRatio == 'number' && - typeof object.toRatio == 'number' && - typeof object.summedNullAmount == 'number' && - isAttributeArray(object.attributes) - ); - } - return false; -} -/** - * Checks if an object has the correct structure of an attribute from AttributeData. - * @param {any} object The object to check. - * @returns If true, the object has the correct structure of an attribute from AttributeData. - */ -function isAttributeArray(object: any): object is AttributeFromAttributeData[] { - if (!Array.isArray(object)) return false; - - return object.every( - (attribute: AttributeFromAttributeData) => - typeof attribute.name == 'string' && - ['Categorical', 'Numerical', 'Other'].includes(attribute.type) && - typeof attribute.nullAmount == 'number' && - isCorrectUniqueCategoricalValues(attribute) - ); -} - -/** - * Checks whether uniqueCategoricalValues has the correct values. - * @param attribute The attribute that contains the array (possibly null) with all unique values of the categories. - * @returns If true, uniqueCategoricalValues is or null, or has an array with values of the same type. - */ -function isCorrectUniqueCategoricalValues(attribute: any): attribute is AttributeFromAttributeData { - const uniqueCategoricalValues = attribute.uniqueCategoricalValues; - - // Check if uniqueCategoricalValues is an array. - if (!Array.isArray(uniqueCategoricalValues)) return false; - - // Check if uniqueCategoricalValues.type is Categorical. - if (attribute.type === 'Categorical') { - // If true, it has to have a length bigger than 0. - if (uniqueCategoricalValues.length == 0) return false; - - // All types in the array has to be the same. - if (typeof uniqueCategoricalValues[0] == 'string') { - return uniqueCategoricalValues.every((value: any) => typeof value == 'string'); - } else if (typeof uniqueCategoricalValues[0] == 'number') { - return uniqueCategoricalValues.every((value: any) => typeof value == 'number'); - } else if (typeof uniqueCategoricalValues[0] == 'boolean') { - return uniqueCategoricalValues.every((value: any) => typeof value == 'boolean'); - } else return false; - } - // If the type is Numerical or Other (the two remaining), check if the length of the array is 0. - else { - if (uniqueCategoricalValues.length == 0) return true; - else return false; - } -} diff --git a/libs/shared/lib/vis/shared/InputDataTypes.tsx b/libs/shared/lib/vis/shared/InputDataTypes.tsx deleted file mode 100644 index 4d97b65297bab87cb071bad6a1563d028632396d..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/InputDataTypes.tsx +++ /dev/null @@ -1,52 +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) - */ -import { SchemaAttribute } from '../../schema'; -import { AttributeCategory } from './Types'; - -/** Node type, consist of a name and a list of attributes */ -export type Node = { - name: string; - attributes: SchemaAttribute[]; -}; - -/** Edge type, consist of a name, start point, end point and a list of attributes */ -export type Edge = { - name: string; - to: string; - from: string; - collection: string; - attributes: SchemaAttribute[]; -}; - -/** Type of the attribute-data, which could be either of a node (entity) or an edge (relation) */ -export type AttributeData = NodeAttributeData | EdgeAttributeData; - -/** Type for a node containing all attribute-data */ -export type NodeAttributeData = { - id: string; - attributes: AttributeFromAttributeData[]; - length: number; - connectedRatio: number; - summedNullAmount: number; -}; - -/** Type for an edge containing all attribute-data */ -export type EdgeAttributeData = { - id: string; - attributes: AttributeFromAttributeData[]; - length: number; - fromRatio: number; - toRatio: number; - summedNullAmount: number; -}; - -/** Type for an attribute of a node or an edge, containing attribute-data */ -export type AttributeFromAttributeData = { - name: string; - type: AttributeCategory; - nullAmount: number; - uniqueCategoricalValues: null | string[] | number[] | boolean[]; -}; diff --git a/libs/shared/lib/vis/shared/SchemaResultType.test.tsx b/libs/shared/lib/vis/shared/SchemaResultType.test.tsx deleted file mode 100644 index cb08d75ade15ae22bbc4021eec94f284586490df..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/SchemaResultType.test.tsx +++ /dev/null @@ -1,87 +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) - */ -import { assert, describe, expect, it } from 'vitest'; -import { isSchemaResult } from './SchemaResultType'; - -/** Testsuite to test the schema result checker */ -describe('isSchemaResult', () => { - it('should return true for a valid schema object', () => { - const schema: any = { - nodes: [], - edges: [], - }; - expect(isSchemaResult(schema)).toBe(true); - - schema.nodes.push({ - name: 'hoi', - attributes: [{ name: 'attr', type: 'string' }], - }); - schema.edges.push({ - name: 'edge', - from: '1', - to: '2', - collection: 'flights', - attributes: [], - }); - expect(isSchemaResult(schema)).toBe(true); - - schema.TEST = 2; - expect(isSchemaResult(schema)).toBe(true); - }); - - it('should return false for an invalid schema object', () => { - const schema: any = { - nodes: [], - edges: [], - }; - expect(isSchemaResult(schema)).toBe(true); - - schema.edges = 2; - expect(isSchemaResult(schema)).toBe(false); - - schema.edges = [ - { - name: 'test', - from: '2', - TO: '2', - collection: 'flights', - attributes: [], - }, - ]; - expect(isSchemaResult(schema)).toBe(false); - - schema.edges = [ - { - name: 'test', - from: '2', - to: '2', - collection: 'flights', - attributes: 2, - }, - ]; - expect(isSchemaResult(schema)).toBe(false); - - schema.edges = [ - { - name: 'test', - from: '2', - to: '2', - collection: 'flights', - attributes: [ - { name: 'attr', type: 'text' }, - { name: 'attr', type: 'ff at 20' }, - ], - }, - ]; - expect(isSchemaResult(schema)).toBe(false); - - // It should return false for incomplete schema - const onlyNodes: any = { nodes: [] }; - const onlyEdges: any = { egdes: [] }; - expect(isSchemaResult(onlyNodes)).toEqual(false); - expect(isSchemaResult(onlyEdges)).toEqual(false); - }); -}); diff --git a/libs/shared/lib/vis/shared/SchemaResultType.tsx b/libs/shared/lib/vis/shared/SchemaResultType.tsx deleted file mode 100644 index 804ed3f9c469e44033f1970d9225877c421d9063..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/SchemaResultType.tsx +++ /dev/null @@ -1,46 +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) - */ - -/** - * Checks if an object has all the properties of a schema result. If true, the object will be casted to SchemaResultType - * @param {any} object The object to check if it is a SchemaResult object. - * @returns If true, the object is a of type SchemaResultType. - * @deprecated //TODO remove - */ -export function isSchemaResult(object: any): object is any { - if (typeof object === 'object' && 'nodes' in object && object.nodes != undefined && 'edges' in object && object.edges != undefined) { - if (!Array.isArray(object.nodes) || !Array.isArray(object.edges)) return false; - - // Check the structure of all nodes - const validNodes = object.nodes.every((node: any) => typeof node.name == 'string' && isAttributeArray(node.attributes)); - - // Check the structure of all edges - const validEdges = object.edges.every( - (edge: any) => - typeof edge.name == 'string' && - typeof edge.collection == 'string' && - typeof edge.from == 'string' && - typeof edge.to == 'string' && - isAttributeArray(edge.attributes) - ); - return validNodes && validEdges; - } - return false; -} - -/** - * Checks if an object has the structure of a SchemaAttribute. - * @param {any} object The object to check. - * @returns If true, the object has the structure of SchemaAttribute type. - * @deprecated //TODO remove - */ -function isAttributeArray(object: any): object is any[] { - if (!Array.isArray(object)) { - return false; - } - - return object.every((attribute) => typeof attribute.name == 'string' && ['string', 'int', 'bool', 'float'].includes(attribute.type)); -} diff --git a/libs/shared/lib/vis/shared/VisConfigPanel/ArrowRightIcon.svg b/libs/shared/lib/vis/shared/VisConfigPanel/ArrowRightIcon.svg deleted file mode 100644 index 0b775d11e4de0393af2ccc0f37f91f7a0d09b6d0..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/VisConfigPanel/ArrowRightIcon.svg +++ /dev/null @@ -1 +0,0 @@ -<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22"><defs><clipPath><path fill="#00f" fill-opacity=".514" d="m-7 1024.36h34v34h-34z"/></clipPath><clipPath><path fill="#aade87" fill-opacity=".472" d="m-6 1028.36h32v32h-32z"/></clipPath></defs><path d="m345.44 248.29l-194.29 194.28c-12.359 12.365-32.397 12.365-44.75 0-12.354-12.354-12.354-32.391 0-44.744l171.91-171.91-171.91-171.9c-12.354-12.359-12.354-32.394 0-44.748 12.354-12.359 32.391-12.359 44.75 0l194.29 194.28c6.177 6.18 9.262 14.271 9.262 22.366 0 8.099-3.091 16.196-9.267 22.373" transform="matrix(.03541-.00013.00013.03541 2.98 3.02)" fill="#4d4d4d"/></svg> \ No newline at end of file diff --git a/libs/shared/lib/vis/shared/VisConfigPanel/QuestionMarkIcon.svg b/libs/shared/lib/vis/shared/VisConfigPanel/QuestionMarkIcon.svg deleted file mode 100644 index 75c116bed4da2237ce27230a8b5d4ad888e0b9be..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/VisConfigPanel/QuestionMarkIcon.svg +++ /dev/null @@ -1,5 +0,0 @@ -<svg id="svg2" xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 200 200" version="1.0"> - <g id="layer1"> - <path id="path2413" d="m100 0c-55.2 0-100 44.8-100 100-5.0495e-15 55.2 44.8 100 100 100s100-44.8 100-100-44.8-100-100-100zm0 12.812c48.13 0 87.19 39.058 87.19 87.188s-39.06 87.19-87.19 87.19-87.188-39.06-87.188-87.19 39.058-87.188 87.188-87.188zm1.47 21.25c-5.45 0.03-10.653 0.737-15.282 2.063-4.699 1.346-9.126 3.484-12.876 6.219-3.238 2.362-6.333 5.391-8.687 8.531-4.159 5.549-6.461 11.651-7.063 18.687-0.04 0.468-0.07 0.868-0.062 0.876 0.016 0.016 21.702 2.687 21.812 2.687 0.053 0 0.113-0.234 0.282-0.937 1.941-8.085 5.486-13.521 10.968-16.813 4.32-2.594 9.808-3.612 15.778-2.969 2.74 0.295 5.21 0.96 7.38 2 2.71 1.301 5.18 3.361 6.94 5.813 1.54 2.156 2.46 4.584 2.75 7.312 0.08 0.759 0.05 2.48-0.03 3.219-0.23 1.826-0.7 3.378-1.5 4.969-0.81 1.597-1.48 2.514-2.76 3.812-2.03 2.077-5.18 4.829-10.78 9.407-3.6 2.944-6.04 5.156-8.12 7.343-4.943 5.179-7.191 9.069-8.564 14.719-0.905 3.72-1.256 7.55-1.156 13.19 0.025 1.4 0.062 2.73 0.062 2.97v0.43h21.598l0.03-2.4c0.03-3.27 0.21-5.37 0.56-7.41 0.57-3.27 1.43-5 3.94-7.81 1.6-1.8 3.7-3.76 6.93-6.47 4.77-3.991 8.11-6.99 11.26-10.125 4.91-4.907 7.46-8.26 9.28-12.187 1.43-3.092 2.22-6.166 2.46-9.532 0.06-0.816 0.07-3.03 0-3.968-0.45-7.043-3.1-13.253-8.15-19.032-0.8-0.909-2.78-2.887-3.72-3.718-4.96-4.394-10.69-7.353-17.56-9.094-4.19-1.062-8.23-1.6-13.35-1.75-0.78-0.023-1.59-0.036-2.37-0.032zm-10.908 103.6v22h21.998v-22h-21.998z"/> - </g> -</svg> \ No newline at end of file diff --git a/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.module.scss.d.ts b/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.module.scss.d.ts deleted file mode 100644 index 5099a5d37475d6ea75785f42f93dc56063d2e7e4..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.module.scss.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -declare const classNames: { - readonly expandButtonSize: 'expandButtonSize'; - readonly container: 'container'; - readonly expandButton: 'expandButton'; - readonly arrowLeft: 'arrowLeft'; - readonly childrenContainer: 'childrenContainer'; - readonly children: 'children'; -}; -export = classNames; diff --git a/libs/shared/lib/vis/types.ts b/libs/shared/lib/vis/types.ts deleted file mode 100644 index cf79a63d5f46d182445ac9ce591e329258d58bcc..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { GraphQueryResult } from '../data-access/store/graphQueryResultSlice'; -import { ML } from '../data-access/store/mlSlice'; -import { SchemaGraph } from '../schema'; -import type { AppDispatch } from '../data-access'; -import { InputProps } from '../components/inputs'; -import { FC } from 'react'; -import { EncodingProps, EncodingTypes } from './configuration/encodings'; -import { SettingProps, SettingTypes } from './configuration/settings'; -import { InteractionProps, InteractionTypes } from './configuration/interactions'; -import { Visualizations } from './visualizationManager'; - -export type globalConfigTypes = { [id: string]: InputProps }; - -export type globalConfigPropTypes = { [K in keyof globalConfigTypes]: any }; - -export type VisualizationConfiguration = { - settings?: SettingTypes; - encodings?: EncodingTypes; - interactions?: InteractionTypes; -}; - -export type VISComponentType = { - displayName: keyof typeof Visualizations; - VIS: FC<any>; - settings?: SettingTypes; - encodings?: EncodingTypes; - interactions?: InteractionTypes; -}; - -export type VisualizationPropTypes = { - data: GraphQueryResult; - schema: SchemaGraph; - ml: ML; - dispatch: AppDispatch; - globalConfig: globalConfigPropTypes; - settings: SettingProps; - encodings?: EncodingProps; - interactions?: InteractionProps; -}; diff --git a/libs/shared/lib/vis/views/index.tsx b/libs/shared/lib/vis/views/index.tsx new file mode 100644 index 0000000000000000000000000000000000000000..de85438c82c94e67e9fb3626f4cdb21c4643d74b --- /dev/null +++ b/libs/shared/lib/vis/views/index.tsx @@ -0,0 +1,3 @@ +export { NoData } from './noData'; +export { Recommender } from './recommender'; +export { Querying } from './querying'; diff --git a/libs/shared/lib/vis/views/noData.tsx b/libs/shared/lib/vis/views/noData.tsx new file mode 100644 index 0000000000000000000000000000000000000000..fafa7b810ea8114ae28dc383b1bf29901ab6a516 --- /dev/null +++ b/libs/shared/lib/vis/views/noData.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Button } from '../../components'; +import { InfoOutlined } from '@mui/icons-material'; + +type Props = { dataAvailable: boolean }; + +export function NoData({ dataAvailable }: Props) { + return ( + <div className="flex justify-center items-center h-full"> + <div className="max-w-lg mx-auto text-left"> + <p className="text-xl font-normal text-secondary-600">No data available to be shown</p> + {dataAvailable ? ( + <p>Query resulted in empty dataset</p> + ) : ( + <div> + <p>Query for data to visualize</p> + <Button + type="primary" + variant="outline" + label="Learn how to query data" + size="sm" + iconComponent={<InfoOutlined />} + onClick={() => window.open('https://graphpolaris.com', '_blank')} + /> + </div> + )} + </div> + </div> + ); +} diff --git a/libs/shared/lib/vis/views/querying.tsx b/libs/shared/lib/vis/views/querying.tsx new file mode 100644 index 0000000000000000000000000000000000000000..d466a33ae43dc908dad7cda0340e18a24c659670 --- /dev/null +++ b/libs/shared/lib/vis/views/querying.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { LoadingSpinner } from '../../components'; + +export function Querying() { + return ( + <div className="w-full h-full flex flex-col items-center justify-center overflow-hidden"> + <LoadingSpinner>Querying backend...</LoadingSpinner> + </div> + ); +} diff --git a/libs/shared/lib/vis/views/recommender.tsx b/libs/shared/lib/vis/views/recommender.tsx new file mode 100644 index 0000000000000000000000000000000000000000..1bc681b192e8358c479dca948bd7c987fc6f6c3f --- /dev/null +++ b/libs/shared/lib/vis/views/recommender.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import Info from '../../components/info'; + +type Props = { onClick: (id: string) => void }; + +const options = { + TableVis: '', + NodeLinkVis: '', + PaohVis: '', + SemanticSubstratesVis: '', + MatrixVis: '', +}; + +export function Recommender({ onClick }: Props) { + return ( + <div className="p-4"> + <span className="text-md font-thin">Select a visualization</span> + <div className="grid grid-cols-3 gap-4"> + {Object.entries(options).map(([name, image]) => ( + <div + key={name} + className="p-4 cursor-pointer border hover:bg-secondary-100" + onClick={(e) => { + e.preventDefault(); + onClick(name); + }} + > + <div className="flex items-center justify-between"> + <span className="text-sm font-semibold">{name}</span> + <Info tooltip="Here an explanation" side="top" /> + </div> + {/* <image src={image} /> */} + </div> + ))} + </div> + </div> + ); +} diff --git a/libs/shared/lib/vis/visualizationManager.tsx b/libs/shared/lib/vis/visualizationManager.tsx deleted file mode 100644 index 12b474b332630d2895e6fbb691ac878c06199d20..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizationManager.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { useAppDispatch } from '@graphpolaris/shared/lib/data-access'; -import { addVisualization } from '../data-access/store/visualizationSlice'; -import { useGraphQueryResult, useML, useSchemaGraph, useVisualization } from '@graphpolaris/shared/lib/data-access/store/hooks'; -import { VISComponentType, globalConfigPropTypes } from './types'; - -export const Visualizations: Record<string, Function> = { - TableVis: () => import('./visualizations/tablevis/tablevis'), - PaohVis: () => import('./visualizations/paohvis/paohvis'), - RawJSONVis: () => import('./visualizations/rawjsonvis/rawjsonvis'), - NodeLinkVis: () => import('./visualizations/nodelinkvis/nodelinkvis'), - // MapVis: () => import('./visualizations/mapvis/mapvis'), - MatrixVis: () => import('./visualizations/matrixvis/matrixvis'), - SemSubstrVis: () => import('./visualizations/semanticsubstratesvis/semanticsubstratesvis'), -}; - -export const VisualizationManager = () => { - const dispatch = useAppDispatch(); - const vis = useVisualization(); - const graphQueryResult = useGraphQueryResult(); - const schema = useSchemaGraph(); - const ml = useML(); - - const [visualizationComponent, setVisualizationComponent] = useState<VISComponentType>(); - - useEffect(() => { - if (vis.activeVisualization && vis.activeVisualization in Visualizations) { - Visualizations[vis.activeVisualization]().then((r: any) => { - setVisualizationComponent(r.default); - }); - } - }, [vis.activeVisualization]); - - useEffect(() => { - if (visualizationComponent) { - const { displayName, settings = {}, encodings = {}, interactions = {} } = visualizationComponent; - dispatch(addVisualization({ id: displayName, settings, encodings, interactions })); - } - }, [visualizationComponent]); - - if (!visualizationComponent) { - return <></>; - } - - const globalConfig: globalConfigPropTypes = vis.settings.general - ? Object.keys(vis.settings.general).reduce((propsObject, val) => { - return { - ...propsObject, - [val]: vis.settings.general[val].value, - }; - }, {}) - : {}; - - let visSettings = {}; - let visEncodings = {}; - let visInteractions = {}; - - if (vis.activeVisualization && vis.settings[vis.activeVisualization]) { - const activeVisSettings = vis.settings[vis.activeVisualization]?.settings; - const activeVisEncodings = vis.settings[vis.activeVisualization]?.encodings; - const activeVisInteractions = vis.settings[vis.activeVisualization]?.interactions; - - visSettings = Object.keys(activeVisSettings ?? {}).reduce((propsObject, val) => { - return { - ...propsObject, - [val]: activeVisSettings?.[val]?.value, - }; - }, {}); - - visEncodings = Object.keys(activeVisEncodings ?? {}).reduce((propsObject, val) => { - return { - ...propsObject, - [val]: activeVisEncodings?.[val]?.marking, - }; - }, {}); - - visInteractions = Object.keys(activeVisInteractions ?? {}).reduce((propsObject, val) => { - return { - ...propsObject, - [val]: activeVisInteractions?.[val]?.value, - }; - }, {}); - } - - try { - return ( - vis.activeVisualization && ( - <visualizationComponent.VIS - data={graphQueryResult} - schema={schema} - ml={ml} - dispatch={dispatch} - globalConfig={globalConfig} - settings={visSettings} - encodings={visEncodings} - interactions={visInteractions} - /> - ) - ); - } catch (error) { - return <div className="w-full h-full flex items-center justify-center">Something went wrong in the visualization component.</div>; - } -}; diff --git a/libs/shared/lib/vis/visualizationPanel.tsx b/libs/shared/lib/vis/visualizationPanel.tsx deleted file mode 100644 index 7a42403afc916cd67b8b4397baf40fa90a549d8e..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizationPanel.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import React, { useState, useRef, useEffect } from 'react'; -import { useAppDispatch, useGraphQueryResult, useQuerybuilderGraph, useVisualization } from '@graphpolaris/shared/lib/data-access'; -import { LoadingSpinner } from '@graphpolaris/shared/lib/components/LoadingSpinner'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; -import { DropdownItem, DropdownItemContainer } from '@graphpolaris/shared/lib/components/dropdowns'; -import ControlContainer from '@graphpolaris/shared/lib/components/controls'; -import { Button } from '@graphpolaris/shared/lib/components/buttons'; - -import { VisualizationDialog } from './configuration'; -import { Settings as SettingsIcon, Apps as AppsIcon, Fullscreen } from '@mui/icons-material'; -import { VisualizationManager, Visualizations } from './visualizationManager'; -import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../components/tooltip'; - -export const VisualizationPanel = () => { - const graphQueryResult = useGraphQueryResult(); - const query = useQuerybuilderGraph(); - const dispatch = useAppDispatch(); - const vis = useVisualization(); - const [visDropdownOpen, setVisDropdownOpen] = useState<boolean>(false); - const [showVisSettings, setShowVisSettings] = useState<boolean>(false); - const visDropdownRef = useRef<HTMLDivElement>(null); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (visDropdownRef.current && !visDropdownRef.current.contains(event.target as Node)) { - setVisDropdownOpen(false); - } - }; - if (visDropdownOpen) document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [visDropdownOpen]); - - return ( - <div className="vis-panel h-full w-full overflow-y-auto" style={graphQueryResult.nodes.length === 0 ? { overflow: 'hidden' } : {}}> - <VisualizationDialog open={showVisSettings} onClose={() => setShowVisSettings(false)} /> - <div className="sticky top-0 flex items-center justify-between z-[2] py-0 px-2 bg-secondary-100 border-b border-secondary-200"> - <h1 className="text-xs font-semibold text-secondary-800">{vis.activeVisualization} visualization</h1> - <ControlContainer> - <TooltipProvider delayDuration={0}> - <Tooltip disabled={showVisSettings}> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<SettingsIcon />} - onClick={() => { - // TODO - // setShowVisSettings(!showVisSettings); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'} disabled={showVisSettings}> - <p>Visualization settings</p> - </TooltipContent> - </Tooltip> - <Tooltip> - <TooltipTrigger asChild> - <Button - type="secondary" - variant="ghost" - size="xs" - iconComponent={<AppsIcon />} - onClick={() => { - setVisDropdownOpen(!visDropdownOpen); - }} - /> - </TooltipTrigger> - <TooltipContent side={'bottom'} disabled={visDropdownOpen}> - <p>Change visualization</p> - </TooltipContent> - </Tooltip> - </TooltipProvider> - - {visDropdownOpen && ( - <div ref={visDropdownRef}> - <DropdownItemContainer align="top-6 right-6"> - {Object.keys(Visualizations).map((key) => ( - <DropdownItem - key={key} - value={key} - onClick={() => { - setVisDropdownOpen(false); - dispatch(setActiveVisualization(key)); - }} - /> - ))} - </DropdownItemContainer> - </div> - )} - </ControlContainer> - </div> - - {graphQueryResult.queryingBackend ? ( - <div className="w-full h-full flex flex-col items-center justify-center overflow-hidden"> - <LoadingSpinner>Querying backend...</LoadingSpinner> - </div> - ) : graphQueryResult.nodes.length === 0 ? ( - <div className="w-full h-full flex flex-col items-center justify-center"> - <p>No data available to be shown</p> - {query.nodes.length > 0 ? <p>Query resulted in empty dataset</p> : <p>Query for data to visualize</p>} - </div> - ) : ( - <VisualizationManager /> - )} - </div> - ); -}; diff --git a/libs/shared/lib/vis/visualizations/mapvis/archive/geovis/types.tsx b/libs/shared/lib/vis/visualizations/mapvis/archive/geovis/types.ts similarity index 100% rename from libs/shared/lib/vis/visualizations/mapvis/archive/geovis/types.tsx rename to libs/shared/lib/vis/visualizations/mapvis/archive/geovis/types.ts diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/FilterMenu.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/FilterMenu.tsx index 6cb870b9d2eafdabdfb36190e3be96d050371dc8..1b1fb98cd912af195ecfe312ad0bcefc5de218ae 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/FilterMenu.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/FilterMenu.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Close, ExpandLess, ExpandMore, PlayArrow } from '@mui/icons-material'; -import { GraphType } from '../Types'; +import { GraphType } from '../types'; type Props = { graph: GraphType; diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/LayerPanel.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/LayerPanel.tsx index af8342ed084def1b0a966cd0729bcbd2d889f02a..6a5d57b2c179674f587c3b70e74dfdeb9ca5062b 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/LayerPanel.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/LayerPanel.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { layerTypes } from '../layers'; -import { Layer } from '../Types'; +import { Layer } from '../types'; import { makeLayer } from '../utlis'; type Props = { diff --git a/libs/shared/lib/vis/visualizations/mapvis/components/MapPanel.tsx b/libs/shared/lib/vis/visualizations/mapvis/components/MapPanel.tsx index 877464ba4d13debaae69f74cfe99c849c3b4d658..dbf41b61dbd6b31b03d776fae673a2b6d3c3f78e 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/components/MapPanel.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/components/MapPanel.tsx @@ -3,7 +3,7 @@ import DeckGL from '@deck.gl/react/typed'; import { FlyToInterpolator, MapView, WebMercatorViewport } from '@deck.gl/core/typed'; import { createBaseMap } from './BaseMap'; import FilterMenu from './FilterMenu'; -import { GraphType, Layer } from '../Types'; +import { GraphType, Layer } from '../types'; import { SelectionLayer } from '@nebula.gl/layers'; import SelectedMenu from './SelectedMenu'; import SecondaryMenu from './SecondaryMenu'; @@ -39,7 +39,7 @@ export function MapPanel({ graph, layers, showFilter, setShowFilter }: Props) { [minLon, minLat], [maxLon, maxLat], ], - { padding: 20 } + { padding: 20 }, ); const { zoom, longitude, latitude } = viewportWebMercator; return { zoom, longitude, latitude }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx b/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx index 4e86887ee97723b631acda4654433a4bfa04162a..73464a385b6bdcf16e6e0becf70c973f03f593c1 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/graphModel.tsx @@ -1,4 +1,4 @@ -import { GraphType, Node, Edge, Coordinate } from './Types'; +import { GraphType, Node, Edge, Coordinate } from './types'; export default class GraphModel implements GraphType { nodeMap: { [id: string]: Node }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethLayer.tsx index bd72091c9629c31db10417f570f9a29c0c10d3cb..bc2f56d980ea764992208c9d53cb515cb788ddcc 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/choropleth-layer/ChoroplethLayer.tsx @@ -5,7 +5,7 @@ import { getDistance } from '../../utlis'; import * as d3 from 'd3'; import ChoroplethOptions from './ChoroplethOptions'; import { europeData, usaData, worldData, netherlands } from '../../../../../mock-data/geo-json'; -import { Edge, Node, LayerProps, GeoJSONData } from '../../Types'; +import { Edge, Node, LayerProps, GeoJSONData } from '../../types'; export const circumferencesMap = { netherlands: netherlands, @@ -129,7 +129,7 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { feature.properties.nodes.push(node.id); const nIncomingEdges: number = node.connectedEdges.filter((edge: string) => this.props.graph.getEdge(edge).to === node.id).length; const nOutgoingEdges: number = node.connectedEdges.filter( - (edge: string) => this.props.graph.getEdge(edge).from === node.id + (edge: string) => this.props.graph.getEdge(edge).from === node.id, ).length; feature.properties.incomingEdges = @@ -144,10 +144,10 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { township.properties.nodes = township.properties.nodes ?? []; township.properties.nodes.push(node.id); const nIncomingEdges: number = node.connectedEdges.filter( - (edge: string) => this.props.graph.getEdge(edge).to === node.id + (edge: string) => this.props.graph.getEdge(edge).to === node.id, ).length; const nOutgoingEdges: number = node.connectedEdges.filter( - (edge: string) => this.props.graph.getEdge(edge).from === node.id + (edge: string) => this.props.graph.getEdge(edge).from === node.id, ).length; township.properties.incomingEdges = @@ -204,7 +204,7 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { getTargetPosition: (d: any) => graph.getNodeLocation(d.to), getSourceColor: (d: any) => [220, 220, 220], getTargetColor: (d: any) => [220, 220, 220], - }) + }), ), new ScatterplotLayer( this.getSubLayerProps({ @@ -219,7 +219,7 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { getFillColor: (d: any) => [0, 0, 0], getRadius: (d: any) => 1, getPosition: (d: any) => graph.getNodeLocation(d.to), - }) + }), ), ]); } @@ -247,7 +247,7 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { getTargetPosition: (d: any) => graph.getNodeLocation(d.to), getSourceColor: (d: any) => [220, 220, 220], getTargetColor: (d: any) => [220, 220, 220], - }) + }), ), new ScatterplotLayer( this.getSubLayerProps({ @@ -262,7 +262,7 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { getFillColor: (d: any) => [0, 0, 0], getRadius: (d: any) => 1, getPosition: (d: any) => graph.getNodeLocation(d.to), - }) + }), ), ]); } @@ -287,8 +287,8 @@ export class ChoroplethLayer extends CompositeLayer<LayerProps> { getLineWidth: (d: any) => 1, getLineColor: (d: any) => [220, 220, 220], getFillColor: (d: any) => this.getColor(d), - }) - ) + }), + ), ); }); diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/edge-arc-layer/EdgeArcLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/edge-arc-layer/EdgeArcLayer.tsx index 619bd86ca7ade1d0c9d1c2c265677101cb95535f..9d84fc0068d70a5c6d5b2ee07a8229cbd25c869d 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/edge-arc-layer/EdgeArcLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/edge-arc-layer/EdgeArcLayer.tsx @@ -5,7 +5,7 @@ import ArcLayerOptions from './ArcLayerOptions'; import * as d3 from 'd3'; import { getProperty } from '../../utlis'; import { BrushingExtension } from '@deck.gl/extensions/typed'; -import { Edge, LayerProps } from '../../Types'; +import { Edge, LayerProps } from '../../types'; export const EdgeArcLayerConfig = { width: { diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/edge-layer/EdgeLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/edge-layer/EdgeLayer.tsx index c1b6a533c158fa31c520d27171eb21c36c0b89e2..65f5178b57947d640bd1dc6fa67ccaccceb95924 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/edge-layer/EdgeLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/edge-layer/EdgeLayer.tsx @@ -5,7 +5,7 @@ import EdgeOptions from './EdgeOptions'; import * as d3 from 'd3'; import { getDistance, getProperty } from '../../utlis'; import { BrushingExtension } from '@deck.gl/extensions/typed'; -import { Edge, LayerProps } from '../../Types'; +import { Edge, LayerProps } from '../../types'; export const EdgeLayerConfig = { width: { @@ -156,7 +156,7 @@ export class EdgeLayer extends CompositeLayer<LayerProps> { ...edge, path: [this.props.graph.getNodeLocation(edge.from), this.props.graph.getNodeLocation(edge.to)], }; - }) + }), ); console.log('displayed edges', edges); diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayer.tsx index ce2f5069389c47120490bfedc3ae6242c2eedf96..06494eda439f1642bb832f7845acc8170552deec 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/heatmap-layer/HeatLayer.tsx @@ -3,7 +3,7 @@ import { CompositeLayer, HeatmapLayer } from 'deck.gl/typed'; import HeatLayerOptions from './HeatLayerOptions'; import * as d3 from 'd3'; import { getDistance, getProperty } from '../../utlis'; -import { Edge, LayerProps } from '../../Types'; +import { Edge, LayerProps } from '../../types'; /* Potential use cases: @@ -94,8 +94,8 @@ export class HeatLayer extends CompositeLayer<LayerProps> { : graph.getEdges().map((edge: Edge) => graph.getNode(edge.to)), getPosition: (d: any) => [d.attributes.long, d.attributes.lat], aggregation: 'SUM', - }) - ) + }), + ), ); } else if (config.type === 'distance') { layers.push( @@ -114,8 +114,8 @@ export class HeatLayer extends CompositeLayer<LayerProps> { getPosition: (d: any) => [d.attributes.long, d.attributes.lat], getWeight: (d: any) => d.distance, aggregation: 'MEAN', - }) - ) + }), + ), ); } else if (config.type === 'attribute') { console.log('attribute'); @@ -129,8 +129,8 @@ export class HeatLayer extends CompositeLayer<LayerProps> { return 1; }, aggregation: 'SUM', - }) - ) + }), + ), ); } else if (config.type === 'path') { layers.push( @@ -142,12 +142,12 @@ export class HeatLayer extends CompositeLayer<LayerProps> { ...edge, path: [this.props.graph.getNodeLocation(edge.from), this.props.graph.getNodeLocation(edge.to)], }; - }) + }), ).flatMap((edge) => edge.path), getPosition: (d: any) => d, aggregation: 'SUM', - }) - ) + }), + ), ); } diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/icon-layer/IconLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/icon-layer/IconLayer.tsx index 3aad1baf23b7e03d42047e0621474b4f11451509..7c062302200163cf7cb63eacb640750c06519a29 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/icon-layer/IconLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/icon-layer/IconLayer.tsx @@ -4,7 +4,7 @@ import { IconLayer } from '@deck.gl/layers/typed'; import { getProperty } from '../../utlis'; import * as d3 from 'd3'; import IconOptions from './IconOptions'; -import { LayerProps } from '../../Types'; +import { LayerProps } from '../../types'; // TODO: Make icons based on node property @@ -43,7 +43,7 @@ export class NodeIconLayer extends CompositeLayer<LayerProps> { getPosition: (d: any) => [d.attributes.long, d.attributes.lat], getSize: (d: any) => 5, getColor: (d: any) => [Math.sqrt(d.exits), 140, 0], - }) + }), ); } } diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/node-layer/NodeLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/node-layer/NodeLayer.tsx index 50ec2622664b0929836d3c71cf9ebf2c9b20780b..73c0cc4d4286682341ff14f7d9266148a2f9d976 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/node-layer/NodeLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/node-layer/NodeLayer.tsx @@ -4,7 +4,7 @@ import { ScatterplotLayer } from '@deck.gl/layers/typed'; import { getProperty } from '../../utlis'; import * as d3 from 'd3'; import NodeOptions from './NodeOptions'; -import { Node, LayerProps } from '../../Types'; +import { Node, LayerProps } from '../../types'; export const NodeLayerConfig = { colisionFilter: true, diff --git a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx index bf2e7b214536ffdac918af4475b248e3b6fa6b3e..19b497c79a42d14c85052262d74a13d8b99c9a65 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/layers/nodelink-layer/NodeLinkLayer.tsx @@ -4,7 +4,7 @@ import { IconLayer, LineLayer, TextLayer } from '@deck.gl/layers/typed'; import NodeLinkOptions from './NodeLinkOptions'; import { createIcon } from './shapeFactory'; import { getProperty } from '../../utlis'; -import { Edge, Node, LayerProps } from '../../Types'; +import { Edge, Node, LayerProps } from '../../types'; export const NodeLinkConfig = { showLabels: false, @@ -63,8 +63,8 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { }; }, mask: true, - }) - ) + }), + ), ); if (this.props.selected.length > 0) { @@ -94,7 +94,7 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { }, mask: true, getColor: (d: any) => [200, 140, 0], - }) + }), ), new IconLayer( this.getSubLayerProps({ @@ -117,7 +117,7 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { }, mask: true, getColor: (d: any) => [200, 140, 0], - }) + }), ), new LineLayer( this.getSubLayerProps({ @@ -128,7 +128,7 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { getSourcePosition: (d: any) => graph.getNodeLocation(d.from), getTargetPosition: (d: any) => graph.getNodeLocation(d.to), getColor: (d: any) => [0, 0, 0], - }) + }), ), new TextLayer( this.getSubLayerProps({ @@ -142,7 +142,7 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { background: true, getBackgroundColor: [255, 125, 0], getPixelOffset: [10, 10], - }) + }), ), new TextLayer( this.getSubLayerProps({ @@ -155,7 +155,7 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { getAlignmentBaseline: 'top', background: true, getPixelOffset: [10, 10], - }) + }), ), ]); } @@ -171,8 +171,8 @@ export class NodeLinkLayer extends CompositeLayer<LayerProps> { getSourcePosition: (d: any) => graph.getNodeLocation(d.from), getTargetPosition: (d: any) => graph.getNodeLocation(d.to), getColor: (d: any) => [0, 0, 0], - }) - ) + }), + ), ); } diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.stories.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.stories.tsx index e3194bdbffd7acdfdaf9097b7b7cba5599dbd36f..a89ba6db0c8b29053562878715779c4e1ddb09d9 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.stories.tsx @@ -1,21 +1,23 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { VisualizationPanel } from '../../visualizationPanel'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; -import { - setNewGraphQueryResult, - graphQueryResultSlice, - querybuilderSlice, - schemaSlice, - visualizationSlice, -} from '../../../data-access/store'; +import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { mockMobilityQueryResult, bigMockQueryResults } from '../../../mock-data'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import { MapComponent } from './mapvis'; -const Component: Meta<typeof VisualizationPanel> = { +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, + visualize: visualizationSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); + +const Component: Meta<typeof MapComponent.component> = { title: 'Visualizations/MapVis', - component: VisualizationPanel, + component: MapComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -32,28 +34,15 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - graphQueryResult: graphQueryResultSlice.reducer, - visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, - }, -}); - export const DutchVehicleTheft = { - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: mockMobilityQueryResult } })); - dispatch(setActiveVisualization('MapVis')); + args: { + data: mockMobilityQueryResult, }, }; export const AmericanFlights = { - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: bigMockQueryResults } })); - dispatch(setActiveVisualization('MapVis')); + args: { + data: bigMockQueryResults, }, }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx index 20d21800528020cc56ce8bbbdb5892794bcc930f..0562b81188137b0ae5c82f430ddf2ed4d1a6011f 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/mapvis.tsx @@ -1,12 +1,15 @@ import React from 'react'; import { MapPanel, LayerPanel } from './components'; import GraphModel from './graphModel'; -import { GraphType, Layer } from './Types'; -import { VISComponentType, VisualizationPropTypes } from '../../types'; +import { GraphType, Layer } from './types'; +import { VISComponentType, VisualizationPropTypes } from '../../common'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; export type MapProps = {}; -export default function MapVis({ data, schema, settings }: VisualizationPropTypes) { +const configuration: MapProps = {}; + +export default function MapVis({ data }: VisualizationPropTypes) { const [layers, setLayers] = React.useState<Layer[]>([]); const [showFilter, setShowFilter] = React.useState<boolean>(false); @@ -31,8 +34,21 @@ export default function MapVis({ data, schema, settings }: VisualizationPropType ); } +const MapSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: MapProps; + graph: GraphMetaData; + updateSettings: (val: any) => void; +}) => { + return <div>To be implemented</div>; +}; + export const MapComponent: VISComponentType = { - displayName: 'Map', - VIS: MapVis, - settings: {}, + displayName: 'MapVis', + component: MapVis, + settings: MapSettings, + configuration: configuration, }; diff --git a/libs/shared/lib/vis/visualizations/mapvis/Types.tsx b/libs/shared/lib/vis/visualizations/mapvis/types.ts similarity index 100% rename from libs/shared/lib/vis/visualizations/mapvis/Types.tsx rename to libs/shared/lib/vis/visualizations/mapvis/types.ts diff --git a/libs/shared/lib/vis/visualizations/mapvis/utlis.tsx b/libs/shared/lib/vis/visualizations/mapvis/utlis.tsx index 23ee31499b778410efefeb330dd27277cd67740e..8f5e38de3aae97ce22ddfd6984fef09b03209167 100644 --- a/libs/shared/lib/vis/visualizations/mapvis/utlis.tsx +++ b/libs/shared/lib/vis/visualizations/mapvis/utlis.tsx @@ -1,4 +1,4 @@ -import { Coordinate, Layer } from './Types'; +import { Coordinate, Layer } from './types'; import { layerTypes } from './layers'; import { Layer as DeckLayer } from '@deck.gl/core/typed'; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx index 5c48d1f27cb154a8d509201958a5e111a620d11d..1eddd255babf290682fa478d858b135a1c6ebda4 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPixi.tsx @@ -16,15 +16,15 @@ import { Texture, } from 'pixi.js'; import { useEffect, useRef, useState } from 'react'; -import { LinkType, NodeType } from '../Types'; +import { LinkType, NodeType } from '../types'; import { NLPopup } from './MatrixPopup'; import { Actions, Interpolations } from 'pixi-actions'; import Color from 'color'; -import { SettingTypes } from '../../../configuration/settings'; import { createColumn } from './ColumnGraphicsComponent'; import { ReorderingManager } from './ReorderingManager'; +import { VisualizationConfiguration } from '../../../common'; type Props = { // onClick: (node: NodeType, pos: IPointData) => void; @@ -34,7 +34,7 @@ type Props = { currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; graph?: GraphQueryResult; - localConfig: SettingTypes; + configuration: VisualizationConfiguration; }; const app = new Application({ background: 0xffffff, antialias: true, autoDensity: true, eventMode: 'auto' }); @@ -108,7 +108,6 @@ export const MatrixPixi = (props: Props) => { }, [ref]); useEffect(() => { - console.log('graph change'); // console.log('graph changed', props.graph, ref.current, ref.current.children.length > 0, imperative.current); if (props.graph && ref.current && ref.current.children.length > 0) { if (!isSetup.current) setup(); @@ -117,12 +116,11 @@ export const MatrixPixi = (props: Props) => { }, [props.graph]); useEffect(() => { - console.log('config change'); // console.log('graph changed', props.graph, ref.current, ref.current.children.length > 0, imperative.current); if (props.graph && ref.current && ref.current.children.length > 0) { setup(); } - }, [props.localConfig]); + }, [props.configuration]); // TODO implement search results // useEffect(() => { @@ -145,7 +143,7 @@ export const MatrixPixi = (props: Props) => { function onButtonDown(event: FederatedPointerEvent) { console.log( - event.currentTarget + event.currentTarget, // graph.nodes.find((node) => node.id === event.currentTarget.name) ); } @@ -200,7 +198,6 @@ export const MatrixPixi = (props: Props) => { col.alpha = 0.25; }); - // console.log(event.currentTarget); const edgesForThisColumn = props.graph.edges.filter((edge) => { return edge.from === currentNode.id || edge.to === currentNode.id; }); @@ -231,7 +228,7 @@ export const MatrixPixi = (props: Props) => { if (columnTextPositions.length !== columnPositions.length) throw new Error( - 'columnTextPositions and columnPositions have different length ' + columnTextPositions.length + ' / ' + columnPositions.length + 'columnTextPositions and columnPositions have different length ' + columnTextPositions.length + ' / ' + columnPositions.length, ); for (let i = 0; i < columns.length; i++) { @@ -257,7 +254,7 @@ export const MatrixPixi = (props: Props) => { rowOrder: string[], colorScale: any, cellWidth: number, - cellHeight: number + cellHeight: number, ) => { const rows = rowsContainer.children; let rowPositions: Point[] = []; @@ -278,7 +275,6 @@ export const MatrixPixi = (props: Props) => { } const colOrder = columnsContainer.children.map((col) => col.name) as string[]; - console.log('colOrder', colOrder); if (!colOrder) throw new Error('colOrder is undefined'); columnsContainer.removeChildren(); setupColumns(edges, colOrder, rowOrder); @@ -306,18 +302,18 @@ export const MatrixPixi = (props: Props) => { viewport.current.addChild(columnsContainer); viewport.current.addChild(rowsContainer); - const groupByType = props.graph.nodes.reduce((group: any, node: Node) => { - if (!group[node.label]) group[node.label] = []; - group[node.label].push(node); - return group; - }, {} as { [key: string]: Node[] }); + const groupByType = props.graph.nodes.reduce( + (group: any, node: Node) => { + if (!group[node.label]) group[node.label] = []; + group[node.label].push(node); + return group; + }, + {} as { [key: string]: Node[] }, + ); // order groupByType by size const ordered = Object.entries(groupByType).sort((a: any[], b: any[]) => b[1].length - a[1].length); - // console.log('ordered', ordered); - // console.log('edges', props.graph.edges); - let cols = [] as Node[]; let rows = [] as Node[]; if (ordered.length == 2) { @@ -348,9 +344,7 @@ export const MatrixPixi = (props: Props) => { config.cellWidth = Math.max((size?.width || 1000) / props.graph.nodes.length, (size?.height || 1000) / props.graph.nodes.length); config.cellHeight = config.cellWidth; - // console.log('currentColumnOrder', columnOrder); - - setupVisualizationEncodingMapping(props.localConfig); + setupVisualizationEncodingMapping(props.configuration); setupColumns(props.graph.edges, columnOrder, rowOrder); setupColumnLegend(columnOrder); @@ -359,7 +353,7 @@ export const MatrixPixi = (props: Props) => { setupRowLegend(rows, rowOrder); setupRowInteractivity(cols, rows, rowOrder, d3.scaleOrdinal(d3.schemeCategory10)); - console.log('setup matrixvis with graph:', props.graph); + console.debug('setup matrixvis with graph:', props.graph); // activate plugins viewport.current.drag().pinch().wheel().animate({}).decelerate({ friction: 0.75 }); @@ -369,7 +363,7 @@ export const MatrixPixi = (props: Props) => { isSetup.current = true; }; - const setupVisualizationEncodingMapping = (localConfig: SettingTypes) => { + const setupVisualizationEncodingMapping = (configuration: VisualizationConfiguration) => { if (!props.graph) throw new Error('Graph is undefined; cannot setup matrix'); const visMapping = []; // TODO type @@ -381,7 +375,7 @@ export const MatrixPixi = (props: Props) => { const adjacenyScale = d3.scaleLinear([colorNeutral, tailwindColors.entity.DEFAULT]); visMapping.push({ attribute: 'adjacency', - encoding: localConfig.marks, + encoding: configuration.marks, colorScale: adjacenyScale, renderFunction: function (i: number, color: ColorSource, gfxContext: Graphics) { gfxContext.beginFill(color, 1); @@ -440,7 +434,6 @@ export const MatrixPixi = (props: Props) => { const edgesForThisColumn = edges.filter((edge) => { return edge.from === oneColumn.name || edge.to === oneColumn.name; }); - // console.log('edgesForThisColumn', oneColumn.name, edgesForThisColumn); const col = createColumn(rowOrder, edgesForThisColumn, config.visMapping, config.cellWidth, config.cellHeight); oneColumn.addChild(col); @@ -468,7 +461,6 @@ export const MatrixPixi = (props: Props) => { }; const setupColumnLegend = (columnOrder: string[]) => { - // console.log('setupColumnLegend'); // columnAxisContainer.removeChildren(); columnAxisContainer.position.set(config.textOffsetX, 0); for (let j = 0; j < columnOrder.length; j++) { diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPopup.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPopup.tsx index cab6d5b29d129197bd484c404aeb85679a742b56..ae0cf66b53b1721b5b2d3384298823592b0706d3 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPopup.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/components/MatrixPopup.tsx @@ -1,5 +1,5 @@ import { IPointData } from 'pixi.js'; -import { NodeType } from '../Types'; +import { NodeType } from '../types'; export type NodelinkPopupProps = { data: { node: NodeType; pos: IPointData }; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/components/ReorderingManager.tsx b/libs/shared/lib/vis/visualizations/matrixvis/components/ReorderingManager.tsx index ae69d44e285be9e15fd93bb3cdaf49e9dd4f5398..da0119c1164491f7e3dd2a4e35856abcba9ef389 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/components/ReorderingManager.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/components/ReorderingManager.tsx @@ -110,7 +110,7 @@ export class ReorderingManager { ...node, index: 0, weight: 0, - } as any) + }) as any, ); const nodes = columnOrder.map((id) => nodesTemp.find((node) => node.id === id)); @@ -190,7 +190,6 @@ export class ReorderingManager { }; public reorderMatrix = (orderingname = 'leafordering', columnOrder: string[], rowOrder: string[]) => { - console.log('reorderMatrix', orderingname); switch (orderingname.toLowerCase()) { case 'leafordering': { return this.computeLeaforder(columnOrder, rowOrder); diff --git a/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx b/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx index f6d1db5548bfe952a623d19dc6cb4cd0ae401301..5fdecb2410f6d7a268d6ce69e6b39e7b80e3b0ba 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/matrix.stories.tsx @@ -3,7 +3,7 @@ import { Meta } from '@storybook/react'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { big2ndChamberQueryResult, smallFlightsQueryResults, mockLargeQueryResults } from '../../../mock-data'; -import { VisualizationPanel } from '../../visualizationPanel'; +import { VisualizationPanel } from '../../components/panel'; import { setNewGraphQueryResult, @@ -17,10 +17,11 @@ import { import { SchemaUtils } from '../../../schema/schema-utils'; import { simpleSchemaAirportRaw } from '../../../mock-data/schema/simpleAirportRaw'; import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import MatrixVisComponent from './matrixvis'; -const Component: Meta<typeof VisualizationPanel> = { +const Component: Meta<typeof MatrixVisComponent.component> = { title: 'Visualizations/MatrixVis', - component: VisualizationPanel, + component: MatrixVisComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -49,75 +50,55 @@ const Mockstore: any = configureStore({ export const TestWithData = { layout: 'fullscreen', args: { - loading: false, + data: { + nodes: [ + { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, + { id: 'villain', attributes: { name: 'Le Chiffre' } }, + ], + edges: [{ id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }], + }, + ml: {}, + configuration: MatrixVisComponent.configuration, }, play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw); - dispatch(setSchema(schema.export())); - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [ - { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, - { id: 'villain', attributes: { name: 'Le Chiffre' } }, - ], - edges: [{ id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }], - }, - }, - }), - ); - dispatch(setActiveVisualization('MatrixVis')); + // const dispatch = Mockstore.dispatch; + // const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw); + // dispatch(setSchema(schema.export())); }, }; export const TestWithNoData = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [], - edges: [], - }, - }, - }), - ); - dispatch(setActiveVisualization('MatrixVis')); + args: { + data: { + nodes: [], + edges: [], + }, + ml: {}, + configuration: MatrixVisComponent.configuration, }, }; export const TestWithBig2ndChamber = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } })); - dispatch(setActiveVisualization('MatrixVis')); + args: { + data: big2ndChamberQueryResult, + ml: {}, + configuration: MatrixVisComponent.configuration, }, }; export const TestWithSmallFlights = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: smallFlightsQueryResults } })); - dispatch(setActiveVisualization('MatrixVis')); + args: { + data: smallFlightsQueryResults, + ml: {}, + configuration: MatrixVisComponent.configuration, }, }; export const TestWithLargeQueryResult = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: mockLargeQueryResults } })); - dispatch(setActiveVisualization('MatrixVis')); + args: { + data: mockLargeQueryResults, + ml: {}, + configuration: MatrixVisComponent.configuration, }, }; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx b/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx index 911a5fbae1729c34867306e5dff68b45101c2cec..b5ee7fe95c27ade3dfc02ce1656f101627379c7d 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/matrixvis.tsx @@ -1,11 +1,25 @@ import React, { useEffect, useRef, useState } from 'react'; import { useImmer } from 'use-immer'; import { GraphQueryResult } from '../../../data-access/store'; -import { LinkType, NodeType } from './Types'; +import { LinkType, NodeType } from './types'; import { MatrixPixi } from './components/MatrixPixi'; -import { VisualizationPropTypes, VISComponentType } from '../../types'; +import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; +import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; +import { dataColors, tailwindColors } from 'config'; -export const MatrixVis = React.memo(({ data, ml, settings }: VisualizationPropTypes) => { +export interface MatrixVisProps { + marks: string; + color: string; +} + +const configuration: MatrixVisProps = { + marks: 'rect', + color: 'blue', +}; + +export const MatrixVis = React.memo(({ data, ml, configuration }: VisualizationPropTypes) => { const ref = useRef<HTMLDivElement>(null); const [graph, setGraph] = useImmer<GraphQueryResult | undefined>(undefined); const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); @@ -20,25 +34,47 @@ export const MatrixVis = React.memo(({ data, ml, settings }: VisualizationPropTy return ( <> <div className="h-full w-full overflow-hidden" ref={ref}> - <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} localConfig={settings} /> + <MatrixPixi graph={graph} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} configuration={configuration} /> </div> </> ); }); -const displayName = 'MatrixVis'; +const MatrixSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: MatrixVisProps; + graph: GraphMetaData; + updateSettings: (val: any) => void; +}) => { + return ( + <SettingsContainer> + <Input + type="dropdown" + label="Configure marks" + value={configuration.marks} + options={['rect', 'circle']} + onChange={(val) => updateSettings({ marks: val })} + /> + + <Input + type="dropdown" + label="Color" + value={configuration.color} + options={['blue', 'green']} + onChange={(val) => updateSettings({ color: val })} + /> + </SettingsContainer> + ); +}; export const MatrixVisComponent: VISComponentType = { - displayName: displayName, - VIS: MatrixVis, - settings: { - marks: { - type: 'dropdown', - options: ['rect', 'circle'], - value: 'rect', - label: 'Configure Marks', - }, - }, + displayName: 'MatrixVis', + component: MatrixVis, + settings: MatrixSettings, + configuration: configuration, }; export default MatrixVisComponent; diff --git a/libs/shared/lib/vis/visualizations/matrixvis/Types.tsx b/libs/shared/lib/vis/visualizations/matrixvis/types.ts similarity index 92% rename from libs/shared/lib/vis/visualizations/matrixvis/Types.tsx rename to libs/shared/lib/vis/visualizations/matrixvis/types.ts index 4103fa516745ce12b044603549e0b17d6ff89220..c8f6bec1d6baa52b7d150c8e8a00ceceb4c37ca3 100644 --- a/libs/shared/lib/vis/visualizations/matrixvis/Types.tsx +++ b/libs/shared/lib/vis/visualizations/matrixvis/types.ts @@ -76,17 +76,17 @@ export type TypeNode = { name: string; //Collection name attributes: string[]; //attributes. This includes all attributes found in the collection type: number | undefined; //number that represents collection of node, for colorscheme - visualisations: Visualisation[]; //The way to visualize attributes of this Node kind + visualisations: Visualization[]; //The way to visualize attributes of this Node kind }; export type CommunityDetectionNode = { cluster: number; //group as used by colouring scheme }; -/**Visualisation holds the visualisation method for an attribute */ -export type Visualisation = { +/**Visualization holds the visualization method for an attribute */ +export type Visualization = { attribute: string; //attribute type (e.g. 'age') - vis: string; //visualisation type (e.g. 'radius') + vis: string; //visualization type (e.g. 'radius') }; /** possible colors to pick from*/ diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx index 7f51d9148f8bdd70efda8af318b97b54fe6c7044..6ac50ebe2f9a0021e1b73f8b772b5aa66223464e 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLMachineLearning.tsx @@ -1,6 +1,4 @@ import { useState } from 'react'; -import { AttributeData, NodeAttributeData } from '../../../shared/InputDataTypes'; -import { AttributeCategory } from '../../../shared/Types'; import { GraphType, LinkType, NodeType } from '../types'; import { ML } from '../../../../data-access/store/mlSlice'; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx index df4e62737add52f85da8fc97737f6196e4f4dc47..983a2713dff5f1a50e15de1d9351409dd2a88a0b 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/components/NLPixi.tsx @@ -9,12 +9,14 @@ import { NLPopup } from './NLPopup'; import { hslStringToHex, nodeColor } from './utils'; import { CytoscapeLayout, GraphologyLayout, LayoutFactory, Layouts } from '../../../../graph-layout'; import { MultiGraph } from 'graphology'; +import { VisualizationConfiguration } from '../../../common'; type Props = { onClick: (node: NodeType, pos: IPointData) => void; // onHover: (data: { node: NodeType; pos: IPointData }) => void; // onUnHover: (data: { node: NodeType; pos: IPointData }) => void; highlightNodes: NodeType[]; + configuration: VisualizationConfiguration; currentShortestPathEdges?: LinkType[]; highlightedLinks?: LinkType[]; graph?: GraphType; @@ -398,7 +400,6 @@ export const NLPixi = (props: Props) => { useEffect(() => { if (props.graph && ref.current && ref.current.children.length > 0 && imperative.current) { - console.log(props.graph); if (isSetup.current === false) setup(); else update(false); } diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx index 72719dc5aed97c5a7b4e1bb1e96b187f6db97dc7..a19fe1e7fc0f8d02dab9a781231992d423cadd15 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.stories.tsx @@ -1,13 +1,6 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { VisualizationPanel } from '../../visualizationPanel'; -import { - setNewGraphQueryResult, - graphQueryResultSlice, - querybuilderSlice, - schemaSlice, - visualizationSlice, -} from '../../../data-access/store'; +import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { @@ -17,11 +10,20 @@ import { recommendationPersonActedInMovieQueryResultPayload, slackReactionToThreadedMessageQueryResultPayload, } from '../../../mock-data'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import { NodeLinkComponent } from './nodelinkvis'; -const Component: Meta<typeof VisualizationPanel> = { +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, + visualize: visualizationSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); + +const Component: Meta<typeof NodeLinkComponent.component> = { title: 'Visualizations/NodeLinkVis', - component: VisualizationPanel, + component: NodeLinkComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -38,127 +40,93 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - graphQueryResult: graphQueryResultSlice.reducer, - visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, - }, -}); - export const TestWithData = { - layout: 'fullscreen', - play: async () => { - const dispatch = Mockstore.dispatch; - - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [ - { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, - { id: 'villain', attributes: { name: 'Le Chiffre' } }, - ], - edges: [{ id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }], - }, - }, - }), - ); - dispatch(setActiveVisualization('NodeLinkVis')); + args: { + data: { + nodes: [ + { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, + { id: 'villain', attributes: { name: 'Le Chiffre' } }, + ], + edges: [{ id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }], + }, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithDoubleArchData = { - layout: 'fullscreen', - play: async () => { - const dispatch = Mockstore.dispatch; - - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [ - { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, - { id: 'villain', attributes: { name: 'Le Chiffre' } }, - ], - edges: [ - { id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }, - { id: 'escape/escape', to: 'agent/007', from: 'villain', attributes: { name: 'Escape' } }, - ], - }, - }, - }), - ); - dispatch(setActiveVisualization(Visualizations.NodeLink)); + args: { + data: { + nodes: [ + { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, + { id: 'villain', attributes: { name: 'Le Chiffre' } }, + ], + edges: [ + { id: 'escape/escape', from: 'agent/007', to: 'villain', attributes: { name: 'Escape' } }, + { id: 'escape/escape', to: 'agent/007', from: 'villain', attributes: { name: 'Escape' } }, + ], + }, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithNoData = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [], - edges: [], - }, - }, - }), - ); - dispatch(setActiveVisualization('NodeLinkVis')); + args: { + data: { + nodes: [], + edges: [], + }, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithBig2ndChamber = { - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } })); - dispatch(setActiveVisualization('NodeLinkVis')); + args: { + data: big2ndChamberQueryResult, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithSmallFlights = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: smallFlightsQueryResults } })); - dispatch(setActiveVisualization('NodeLinkVis')); + args: { + data: smallFlightsQueryResults, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithLargeQueryResult = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: mockLargeQueryResults } })); - dispatch(setActiveVisualization('NodeLinkVis')); + args: { + data: mockLargeQueryResults, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithRecommendationPersonActedInMovieQueryResult = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult(recommendationPersonActedInMovieQueryResultPayload)); - dispatch(setActiveVisualization(Visualizations.NodeLink)); + args: { + data: recommendationPersonActedInMovieQueryResultPayload.result.payload, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; export const TestWithSlackReactionToThreadedMessageQueryResult = { - args: { loading: false }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch(setNewGraphQueryResult(slackReactionToThreadedMessageQueryResultPayload)); - dispatch(setActiveVisualization(Visualizations.NodeLink)); + args: { + data: slackReactionToThreadedMessageQueryResultPayload.result.payload, + ml: {}, + configuration: NodeLinkComponent.configuration, + dispatch: () => {}, }, }; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx index f9a42c5d51cc9f3e34dbc256cad9987624fe6efc..2af16aca3afde3f1bd4dd7f5e51a478f7aa3a240 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/nodelinkvis.tsx @@ -4,10 +4,42 @@ import { NLPixi } from './components/NLPixi'; import { parseQueryResult } from './components/query2NL'; import { useImmer } from 'use-immer'; import { ML, setShortestPathSource, setShortestPathTarget } from '../../../data-access/store/mlSlice'; -import { VisualizationPropTypes, VISComponentType } from '../../types'; import { Layouts } from '../../../graph-layout/types'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; +import { SettingsContainer, SettingsHeader } from '@graphpolaris/shared/lib/vis/components/config'; +import { VISComponentType, VisualizationPropTypes } from '../../common'; -export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings }: VisualizationPropTypes) => { +export interface NodelinkVisProps { + layout: string; + showPopUpOnHover: boolean; + shapes: { + similar: boolean; + shape: 'circle' | 'rectangle'; + shapeMap: { [id: string]: 'circle' | 'rectangle' } | undefined; + }; + edges: { + width: { + similar: boolean; + width: number; + }; + }; +} + +const configuration: NodelinkVisProps = { + layout: Layouts.FORCEATLAS2WEBWORKER as string, + showPopUpOnHover: true, + shapes: { + similar: true, + shape: 'circle', + shapeMap: undefined, + }, + edges: { + width: { similar: true, width: 0.2 }, + }, +}; + +export const NodeLinkVis = React.memo(({ data, ml, dispatch, configuration }: VisualizationPropTypes) => { const ref = useRef<HTMLDivElement>(null); const [graph, setGraph] = useImmer<GraphType | undefined>(undefined); const [highlightNodes, setHighlightNodes] = useState<NodeType[]>([]); @@ -58,58 +90,104 @@ export const NodeLinkVis = React.memo(({ data, ml, dispatch, settings }: Visuali return ( <NLPixi graph={graph} + configuration={configuration} highlightNodes={highlightNodes} highlightedLinks={highlightedLinks} onClick={(node, pos) => { onClickedNode(node, ml); }} - layoutAlgorithm={settings.layout} + layoutAlgorithm={configuration.layout} /> ); }); +const NodelinkSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: NodelinkVisProps; + graph: GraphMetaData; + updateSettings: (val: any) => void; +}) => { + return ( + <SettingsContainer> + <div className="mb-4"> + <h1 className="text-sm font-bold">General</h1> + <Input + type="dropdown" + label="Layout" + value={configuration.layout} + options={Object.values(Layouts) as string[]} + onChange={(val) => updateSettings({ layout: val })} + /> + <Input + type="boolean" + label="Show pop-up on hover" + value={configuration.showPopUpOnHover} + onChange={(val) => updateSettings({ showPopUpOnHover: val })} + /> + </div> + + <div className="mb-4"> + <h1 className="text-sm font-bold">Nodes</h1> + + <div> + <span className="text-xs font-semibold">Shape</span> + <Input + type="boolean" + label="Common shape?" + value={configuration.shapes.similar} + onChange={(val) => updateSettings({ shapes: { ...configuration.shapes, similar: val } })} + /> + {configuration.shapes.similar ? ( + <Input + type="dropdown" + label="Shape" + value={configuration.shapes.shape} + options={['Circle', 'Square']} + onChange={(val) => updateSettings({ shapes: { ...configuration.shapes, shape: val } })} + /> + ) : ( + <span>Map shapes to labels (to be implemented)</span> + )} + </div> + + <div> + <span className="text-xs font-semibold">Color</span> + </div> + </div> + + <div> + <h1 className="text-sm font-bold">Edges</h1> + <div> + <span className="text-xs font-semibold">Edge width</span> + <Input + type="boolean" + label="Common width" + value={configuration.edges.width.similar} + onChange={(val) => updateSettings({ edges: { ...configuration.edges, width: { ...configuration.edges.width, similar: val } } })} + /> + <Input + type="slider" + label="Width" + value={configuration.edges.width.width} + onChange={(val) => updateSettings({ edges: { ...configuration.edges, width: { ...configuration.edges.width, width: val } } })} + min={0.1} + max={2} + step={0.1} + /> + </div> + </div> + </SettingsContainer> + ); +}; + export const NodeLinkComponent: VISComponentType = { displayName: 'NodeLinkVis', - VIS: NodeLinkVis, - settings: { - layout: { - value: Layouts.FORCEATLAS2WEBWORKER as string, - type: 'dropdown', - label: 'Layout', - options: Object.values(Layouts) as string[], - description: 'Select a layout that is used for the visualization', - }, - }, - encodings: { - color: { - label: 'Node color', - element: 'node', - dimension: ['categorical', 'numerical'], - selector: 'Color', - description: 'Select a color for the nodes', - }, - shape: { - label: 'Node shape', - element: 'node', - dimension: ['categorical'], - selector: 'Shape', - description: 'Select a shape for the nodes', - }, - size: { - label: 'Node size', - element: 'node', - dimension: ['categorical'], - selector: 'Size', - description: 'Select a size for the nodes', - }, - }, - interactions: { - showPopUpOnHover: { - value: true, - type: 'boolean', - label: 'Show pop-up', - }, - }, + component: NodeLinkVis, + settings: NodelinkSettings, + configuration: configuration, }; export default NodeLinkComponent; diff --git a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts index 4103fa516745ce12b044603549e0b17d6ff89220..c8f6bec1d6baa52b7d150c8e8a00ceceb4c37ca3 100644 --- a/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts +++ b/libs/shared/lib/vis/visualizations/nodelinkvis/types.ts @@ -76,17 +76,17 @@ export type TypeNode = { name: string; //Collection name attributes: string[]; //attributes. This includes all attributes found in the collection type: number | undefined; //number that represents collection of node, for colorscheme - visualisations: Visualisation[]; //The way to visualize attributes of this Node kind + visualisations: Visualization[]; //The way to visualize attributes of this Node kind }; export type CommunityDetectionNode = { cluster: number; //group as used by colouring scheme }; -/**Visualisation holds the visualisation method for an attribute */ -export type Visualisation = { +/**Visualization holds the visualization method for an attribute */ +export type Visualization = { attribute: string; //attribute type (e.g. 'age') - vis: string; //visualisation type (e.g. 'radius') + vis: string; //visualization type (e.g. 'radius') }; /** possible colors to pick from*/ diff --git a/libs/shared/lib/vis/visualizations/paohvis/components/HyperEdgesRange.tsx b/libs/shared/lib/vis/visualizations/paohvis/components/HyperEdgesRange.tsx index c525b545e8809459eda9b57eb33909de52f48586..5e9d44b05f9ed8be687fc25c04583d9362155efb 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/components/HyperEdgesRange.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/components/HyperEdgesRange.tsx @@ -10,7 +10,7 @@ * See testing plan for more details.*/ import { select } from 'd3'; import React, { useEffect, useRef } from 'react'; -import { HyperEdgeI } from '../Types'; +import { HyperEdgeI } from '../types'; import CustomLine from './CustomLine'; type HyperEdgeRangeProps = { diff --git a/libs/shared/lib/vis/visualizations/paohvis/components/MakePaohvisMenu.tsx b/libs/shared/lib/vis/visualizations/paohvis/components/MakePaohvisMenu.tsx index 6ceac15732c47a3fd2e02814d32c2a4c8cb7e950..142c374e2d20c84335801311accccd07634fa759 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/components/MakePaohvisMenu.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/components/MakePaohvisMenu.tsx @@ -8,7 +8,7 @@ /* 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 React, { ChangeEventHandler, ReactElement, useEffect, useMemo } from 'react'; +import React, { ReactElement, useEffect, useMemo } from 'react'; import { Attribute, AttributeNames, @@ -19,19 +19,22 @@ import { PaohvisNodeOrder, RelationsFromSchema, ValueType, -} from '../Types'; +} from '../types'; import { Sort } from '@mui/icons-material'; import './MakePaohvisMenu.scss'; import { useImmer } from 'use-immer'; -import { useGraphQueryResult, useSchemaGraph } from '@graphpolaris/shared/lib/data-access'; import { calculateAttributesAndRelations, calculateAttributesFromRelation } from '../utils/utils'; -import calcEntitiesFromQueryResult from '../utils/CalcEntitiesFromQueryResult'; -import { isNodeLinkResult } from '../../../shared/ResultNodeLinkParserUseCase'; +import { calcEntitiesFromQueryResult } from '../utils/CalcEntitiesFromQueryResult'; +import { isNodeLinkResult } from '../utils/ResultNodeLinkParserUseCase'; import { select } from 'd3'; import { Button } from '../../../../components/buttons'; +import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access'; +import { SchemaGraph } from '@graphpolaris/shared/lib/schema'; /** The typing for the props of the Paohvis menu */ type MakePaohvisMenuProps = { + graphQueryResult: GraphQueryResult; + schema: SchemaGraph; makePaohvis: ( entityVertical: string, entityHorizontal: string, @@ -66,10 +69,8 @@ type MakePaohvisMenuState = { attributeValue: string; }; -/** React component that renders a menu with input fields for adding a new Paohvis visualisation. */ +/** React component that renders a menu with input fields for adding a new Paohvis visualization. */ export const MakePaohvisMenu = (props: MakePaohvisMenuProps) => { - const graphQueryResult = useGraphQueryResult(); - const schema = useSchemaGraph(); const [state, setState] = useImmer<MakePaohvisMenuState>({ entityVertical: '', entityVerticalListed: '', @@ -400,24 +401,24 @@ export const MakePaohvisMenu = (props: MakePaohvisMenuProps) => { useEffect(() => { resetConfig(); setState((draft) => { - draft.entitiesFromSchema = calculateAttributesAndRelations(schema); - draft.relationsFromSchema = calculateAttributesFromRelation(schema); + draft.entitiesFromSchema = calculateAttributesAndRelations(props.schema); + draft.relationsFromSchema = calculateAttributesFromRelation(props.schema); return draft; }); - }, [schema]); + }, [props.schema]); /** This method filters and makes a new Paohvis table. */ useEffect(() => { - if (isNodeLinkResult(graphQueryResult)) { + if (isNodeLinkResult(props.graphQueryResult)) { resetConfig(); setState((draft) => { - draft.entitiesFromQueryResult = calcEntitiesFromQueryResult(graphQueryResult); + draft.entitiesFromQueryResult = calcEntitiesFromQueryResult(props.graphQueryResult); return draft; }); } else { console.error('Invalid query result!'); } - }, [graphQueryResult]); + }, [props.graphQueryResult]); /** This resets the configuration. Should be called when the possible entities and relations change. */ function resetConfig(): void { @@ -571,5 +572,3 @@ export const MakePaohvisMenu = (props: MakePaohvisMenuProps) => { </div> ); }; - -export default MakePaohvisMenu; diff --git a/libs/shared/lib/vis/visualizations/paohvis/components/PaohvisFilterComponent.tsx b/libs/shared/lib/vis/visualizations/paohvis/components/PaohvisFilterComponent.tsx index 1856512535ed0d58a98d8d7c5e4685082c8178ee..652f974b360cab675b71d28101e1ce2387a55fa0 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/components/PaohvisFilterComponent.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/components/PaohvisFilterComponent.tsx @@ -9,10 +9,10 @@ * We do not test components/renderfunctions/styling files. * See testing plan for more details.*/ import React, { ChangeEventHandler, ReactElement, useState, MouseEventHandler, useEffect, useMemo } from 'react'; -import { AttributeNames, FilterType } from '../Types'; +import { AttributeNames, FilterType } from '../types'; import { useImmer } from 'use-immer'; import { useGraphQueryResult, useSchemaGraph } from '@graphpolaris/shared/lib/data-access'; -import { isNodeLinkResult } from '../../../shared/ResultNodeLinkParserUseCase'; +import { isNodeLinkResult } from '../utils/ResultNodeLinkParserUseCase'; import { calculateAttributesAndRelations, calculateAttributesFromRelation } from '../utils/utils'; import { boolPredicates, numberPredicates, textPredicates } from '../models/FilterPredicates'; import { style } from 'd3'; @@ -155,6 +155,11 @@ export const PaohvisFilterComponent = (props: PaohvisFilterProps) => { function onChangeCompareValue(event: React.ChangeEvent<HTMLInputElement>): void { setState((draft) => { draft.compareValue = event.target.value; + console.log( + containsFilterTargetChosenAttribute(draft.attributeNameAndType), + isPredicateValid(draft.predicate), + isCompareValueTypeValid(draft.compareValue), + ); draft.isFilterButtonEnabled = isFilterConfigurationValid(draft); return draft; diff --git a/libs/shared/lib/vis/visualizations/paohvis/paohvis.stories.tsx b/libs/shared/lib/vis/visualizations/paohvis/paohvis.stories.tsx index 6b20d52456c9f16f179ccd066bd9a70a4690106e..b22021f6a8a8ec7943bdcb6b87953a01ea33b4c1 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/paohvis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/paohvis.stories.tsx @@ -1,14 +1,7 @@ -import { - setNewGraphQueryResult, - graphQueryResultSlice, - querybuilderSlice, - schemaSlice, - setSchema, - visualizationSlice, -} from '../../../data-access/store'; +import React from 'react'; +import { graphQueryResultSlice, querybuilderSlice, schemaSlice, setSchema, visualizationSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Meta } from '@storybook/react'; -import { VisualizationPanel } from '../../visualizationPanel'; import { Provider } from 'react-redux'; import { SchemaUtils } from '../../../schema/schema-utils'; import { @@ -20,11 +13,21 @@ import { mockRecommendationsActorMovie, } from '../../../mock-data'; import { simpleSchemaAirportRaw } from '../../../mock-data/schema/simpleAirportRaw'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import PaohVisComponent from './paohvis'; +import { graphQueryBackend2graphQuery } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; + +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, + visualize: visualizationSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); -const Component: Meta<typeof VisualizationPanel> = { +const Component: Meta<typeof PaohVisComponent.component> = { title: 'Visualizations/Paohvis', - component: VisualizationPanel, + component: PaohVisComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -41,19 +44,22 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - graphQueryResult: graphQueryResultSlice.reducer, - visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, - }, -}); - export const TestWithData = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology({ + args: { + data: graphQueryBackend2graphQuery({ + nodes: [ + { id: '1/a', label: 'a', attributes: { a: 's1' } }, + { id: '1/b1', label: 'b1', attributes: { a: 's1' } }, + { id: '1/b2', label: 'b2', attributes: { a: 's1' } }, + { id: '1/b3', label: 'b3', attributes: { a: 's1' } }, + ], + edges: [ + { id: '1c/z1', label: 'z1', from: '1/b1', to: '1/a', attributes: { a: 's1' } }, + { id: '1c/z2', label: 'z2', from: '1/a', to: '1/b1', attributes: { a: 's1' } }, + { id: '1c/z3', label: 'z3', from: '1/b2', to: '1/b3', attributes: { a: 's2' } }, + ], + }), + schema: SchemaUtils.schemaBackend2Graphology({ nodes: [ { name: '1', @@ -70,73 +76,40 @@ export const TestWithData = { attributes: [{ name: 'a', type: 'string' }], }, ], - }); - - dispatch(setSchema(schema.export())); - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [ - { id: '1/a', label: 'a', attributes: { a: 's1' } }, - { id: '1/b1', label: 'b1', attributes: { a: 's1' } }, - { id: '1/b2', label: 'b2', attributes: { a: 's1' } }, - { id: '1/b3', label: 'b3', attributes: { a: 's1' } }, - ], - edges: [ - { id: '1c/z1', label: 'z1', from: '1/b1', to: '1/a', attributes: { a: 's1' } }, - { id: '1c/z2', label: 'z2', from: '1/a', to: '1/b1', attributes: { a: 's1' } }, - { id: '1c/z3', label: 'z3', from: '1/b2', to: '1/b3', attributes: { a: 's2' } }, - ], - }, - }, - }), - ); - dispatch(setActiveVisualization('PaohVis')); + }).export(), + configuration: PaohVisComponent.configuration, }, }; export const TestWithMarieBoucherSample = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(marieBoucherSampleSchemaRaw); - - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: marieBoucherSample } })); - dispatch(setActiveVisualization('PaohVis')); + args: { + data: graphQueryBackend2graphQuery(marieBoucherSample), + schema: SchemaUtils.schemaBackend2Graphology(marieBoucherSampleSchemaRaw).export(), + configuration: PaohVisComponent.configuration, }, }; export const TestWithBig2ndChamber = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(big2ndChamberSchemaRaw); - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } })); - dispatch(setActiveVisualization('PaohVis')); + args: { + data: graphQueryBackend2graphQuery(big2ndChamberQueryResult), + schema: SchemaUtils.schemaBackend2Graphology(big2ndChamberSchemaRaw).export(), + configuration: PaohVisComponent.configuration, }, }; export const TestWithAirport = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw); - - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: bigMockQueryResults } })); - dispatch(setActiveVisualization('PaohVis')); + args: { + data: graphQueryBackend2graphQuery(bigMockQueryResults), + schema: SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw).export(), + configuration: PaohVisComponent.configuration, }, }; export const TestWithRecommendationsActorMovie = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(marieBoucherSampleSchemaRaw); - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: mockRecommendationsActorMovie } })); - dispatch(setActiveVisualization('PaohVis')); + args: { + data: graphQueryBackend2graphQuery(mockRecommendationsActorMovie), + schema: SchemaUtils.schemaBackend2Graphology(marieBoucherSampleSchemaRaw).export(), + configuration: PaohVisComponent.configuration, }, }; diff --git a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx index f889bee9d767ab3f277641d81b868b65b8628117..f345bbe9e9feff187b721f24d312446c47e68b85 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/paohvis.tsx @@ -1,6 +1,4 @@ import { useEffect, useRef, useState, useMemo } from 'react'; -import SortByAlphaIcon from '@mui/icons-material/SortByAlpha'; -import SortIcon from '@mui/icons-material/Sort'; import styles from './paohvis.module.scss'; import { Attribute, @@ -13,27 +11,23 @@ import { PaohvisNodeOrder, RelationsFromSchema, ValueType, -} from './Types'; +} from './types'; import { useImmer } from 'use-immer'; import { getWidthOfText } from '../../../schema/schema-utils'; -import { useGraphQueryResult, useSchemaGraph } from '../../../data-access'; -import { isNodeLinkResult } from '../../shared/ResultNodeLinkParserUseCase'; -import { calculateAttributesAndRelations, calculateAttributesFromRelation, calcTextWidthAndStringText } from './utils/utils'; import { processDataColumn } from './utils/processAttributes'; -import VisConfigPanelComponent from '../../shared/VisConfigPanel/VisConfigPanel'; -import { PaohvisFilterComponent } from './components/PaohvisFilterComponent'; -import { pointer, select, selectAll, scaleOrdinal } from 'd3'; +import { select, selectAll, scaleOrdinal } from 'd3'; import { HyperEdgeRange } from './components/HyperEdgesRange'; -import ToPaohvisDataParserUseCase from './utils/ToPaohvisDataParserUsecase'; -import MakePaohvisMenu from './components/MakePaohvisMenu'; +import { ToPaohvisDataParserUseCase } from './utils/ToPaohvisDataParserUsecase'; +import { MakePaohvisMenu } from './components/MakePaohvisMenu'; import { RowLabelColumn } from './components/RowLabelColumn'; -import { VISComponentType, VisualizationPropTypes } from '../../types'; import { categoricalColors } from '../../../../../config/src/colors.js'; -import { NodeAttributes, Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; -import { SchemaAttributeTypes } from '@graphpolaris/shared/lib/schema'; -import { SchemaAttribute } from '../../../schema'; -import Graph, { MultiGraph } from 'graphology'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; +import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { isNodeLinkResult } from './utils/ResultNodeLinkParserUseCase'; +import { calculateAttributesAndRelations, calculateAttributesFromRelation, calcTextWidthAndStringText } from './utils/utils'; type PaohvisViewModelState = { //rowHeight: number; @@ -64,18 +58,22 @@ type PaohvisViewModelState = { }; export type PaohVisProps = { - //rowHeight: number; - //hyperedgeColumnWidth: number; - //gapBetweenRanges: number; - //data?: PaohvisData; + rowHeight: number; + hyperedgeColumnWidth: number; + gapBetweenRanges: number; + data?: PaohvisData; showColor: boolean; }; -const displayName = 'PaohVis'; -export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { - //export const PaohVis = ({ settings }: VisualizationPropTypes<typeof displayName>) => { +const configuration: PaohVisProps = { + rowHeight: 30, + hyperedgeColumnWidth: 20, + gapBetweenRanges: 5, + showColor: false, +}; + +export const PaohVis = ({ data, schema, configuration }: VisualizationPropTypes) => { const graphQueryResult = data; - //const schema = useSchemaGraph(); const [isButtonPressed, setIsButtonPressed] = useState(false); @@ -154,7 +152,7 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { useEffect(() => { //console.log(colorRowsRef.current); - if (settings.showColor) { + if (configuration.showColor) { selectAll('.rowsLabel') .select('rect') .data(colorRowsRef.current.vip) @@ -181,7 +179,7 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { } // - }, [settings.showColor]); + }, [configuration.showColor]); // old props /* @@ -484,13 +482,13 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { } /** - * Makes the new PAOHvis visualisation. + * Makes the new PAOHvis visualization. * @param {string} entityVertical This is the name of the vertical entity (so on the left). * @param {string} entityHorizontal This is the name of the horizontal entity (so at the top). * @param {string} relationName This is the (collection)-name of the relation. * @param {boolean} isEntityVerticalEqualToRelationFrom Tells if the vertical entity is the from or to of the relation. * @param {Attribute} chosenAttribute This is the attribute on which the PAOHvis must be grouped by. - * @param {PaohvisNodeOrder} nodeOrder Defines the sorting order of the PAOHvis visualisation. + * @param {PaohvisNodeOrder} nodeOrder Defines the sorting order of the PAOHvis visualization. */ function onClickMakeButton( entityVertical: string, @@ -660,9 +658,10 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { // RENDER // // - const hyperEdgeRanges = dataModel.hyperEdgeRanges; // hyperedges dataModelset - const rowLabelColumnWidth = dataModel.maxRowLabelWidth; // max width of the rows - const hyperedgeColumnWidth = props_hyperedgeColumnWidth; // column width from user defined + + const hyperEdgeRanges = dataModel.hyperEdgeRanges; + const rowLabelColumnWidth = dataModel.maxRowLabelWidth; + const hyperedgeColumnWidth = configuration.hyperedgeColumnWidth; //calculate yOffset - hyperedges /* @@ -692,7 +691,7 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { /*Math.cos(columnLabelAngleInRadians) * */ getWidthOfText(hyperEdgeRange.rangeText, styles.tableFontFamily, styles.tableFontSize, styles.tableFontWeight); - const columnWidth = hyperEdgeRange.hyperEdges.length * hyperedgeColumnWidth + props_gapBetweenRanges * 3; + const columnWidth = hyperEdgeRange.hyperEdges.length * hyperedgeColumnWidth + configuration.gapBetweenRanges * 3; // new text compute tableWidth += columnWidth; @@ -720,10 +719,10 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { colOffset={colOffset} xOffset={rowLabelColumnWidth} yOffset={yOffset} - rowHeight={props_rowHeight} + rowHeight={configuration.rowHeight} textBothMargins={0.1 * maxSizeTextColumns} hyperedgeColumnWidth={hyperedgeColumnWidth} - gapBetweenRanges={props_gapBetweenRanges} + gapBetweenRanges={configuration.gapBetweenRanges} onMouseEnter={onMouseEnterHyperEdge} onMouseLeave={onMouseLeaveHyperEdge} />, @@ -731,10 +730,10 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { colOffset += hyperEdgeRange.hyperEdges.length; }); - // returns the whole PAOHvis visualisation panel + // returns the whole PAOHvis visualization panel return ( <div className={styles.container}> - <MakePaohvisMenu makePaohvis={onClickMakeButton} /> + <MakePaohvisMenu makePaohvis={onClickMakeButton} graphQueryResult={graphQueryResult} schema={schema} /> <div className="w-full h-full"> <div className="w-full h-full"> <div className="w-full h-full flex flex-row justify-center" ref={secondContainerRef}> @@ -742,7 +741,7 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { ref={svgRef} style={{ width: tableWidthWithExtraColumnLabelWidth, - height: yOffset + (dataModel.rowLabels.length + 1) * props_rowHeight, + height: yOffset + (dataModel.rowLabels.length + 1) * configuration.rowHeight, }} > <RowLabelColumn @@ -750,7 +749,7 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { onMouseLeave={onMouseLeaveRow} titles={dataModel.rowLabels} width={rowLabelColumnWidth} - rowHeight={props_rowHeight} + rowHeight={configuration.rowHeight} // viewModel.rowHeight? yOffset={yOffset} /> {hyperEdgeRangeColumns} @@ -761,75 +760,40 @@ export const PaohVis = ({ data, schema, settings }: VisualizationPropTypes) => { </div> ); }; -/* -const localConfigSchema: localConfigSchemaType = { - - showColor: { - value: false, - type: 'boolean', - label: 'Color rows!', - }, - entitySelected: { - // data driven - value: '', - type: 'dropdown', - label: 'Entity:', - options: ['merchant'], - }, - attributeSelected: { - // data driven. After select entity, it updates this one - value: '', - type: 'dropdown', - label: 'Attribute:', - options: ['no attribute', 'name: Entity', 'time: Relation'], - }, - sortMethodSelected: { - // no data driven - value: '', - type: 'dropdown', - label: 'Sort by:', - options: ['Alphabetical', 'Degree (Amount of hyperedges)', 'By Group'], - }, - - sortMethodAscDscSelected: { - // no data driven. Depends on sortMethodSelected. - // if sortMethodSelected == "Alphabetical" this: - - value: 'A-Z', - type: 'dropdown', - label: 'Short method:', - options: [<SortIcon />, <SortByAlphaIcon />], - // else: sortMethodSelected == "Degree" this: - - value: 'A-Z', - type: 'dropdown', - label: 'Ascending/Descending:', - options: ['A-Z', 'Z-A'], - - }, - sortMethodRowsSelected: { - // Data driven. It should only appear when sortMethodSelected == Group - // Show categorical node's attribute. No limitation of nodes. - value: '', - type: 'dropdown', - label: 'Group By:', - options: ['city', 'country', 'state', 'name'], - }, - colorRowsBySelected: { - // Data driven. - // Show categorical node's attribute with less categories than colors availabe. - value: '', - type: 'dropdown', - label: 'Color Rows by:', - options: ['city', 'country', 'state', 'name'], - }, - + +const PaohSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: PaohVisProps; + graph: GraphMetaData; + updateSettings: (val: any) => void; +}) => { + return ( + <SettingsContainer> + <Input type="number" label="Row height" value={configuration.rowHeight} onChange={(val) => updateSettings({ rowHeight: val })} /> + <Input + type="number" + label="Hyper edge column width" + value={configuration.hyperedgeColumnWidth} + onChange={(val) => updateSettings({ hyperedgeColumnWidth: val })} + /> + <Input + type="number" + label="Gap between ranges" + value={configuration.gapBetweenRanges} + onChange={(val) => updateSettings({ gapBetweenRanges: val })} + /> + </SettingsContainer> + ); }; -*/ + export const PaohVisComponent: VISComponentType = { displayName: 'PaohVis', - VIS: PaohVis, - settings: {}, + component: PaohVis, + settings: PaohSettings, + configuration: configuration, }; export default PaohVisComponent; diff --git a/libs/shared/lib/vis/visualizations/paohvis/Types.tsx b/libs/shared/lib/vis/visualizations/paohvis/types.ts similarity index 100% rename from libs/shared/lib/vis/visualizations/paohvis/Types.tsx rename to libs/shared/lib/vis/visualizations/paohvis/types.ts diff --git a/libs/shared/lib/vis/visualizations/paohvis/utils/AttributesFilterUseCase.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/AttributesFilterUseCase.tsx index 193c898cc94411658fdd26b69fba1e595a0ef69d..35cf2539ca5c138955e2d7c23df38e5185828c23 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/utils/AttributesFilterUseCase.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/AttributesFilterUseCase.tsx @@ -4,8 +4,8 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { FilterInfo, PaohvisFilters } from '../Types'; -import { AxisType, isNotInGroup } from '../../../shared/ResultNodeLinkParserUseCase'; +import { FilterInfo, PaohvisFilters } from '../types'; +import { AxisType, isNotInGroup } from './ResultNodeLinkParserUseCase'; import { boolPredicates, numberPredicates, textPredicates } from '../models/FilterPredicates'; import { Edge, GraphQueryResult, Node } from '@graphpolaris/shared/lib/data-access'; diff --git a/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntitiesFromQueryResult.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntitiesFromQueryResult.tsx index c6988eab456eec6753ddc67f1879ea7745112670..ff160f5c0997cfe0a99c31fd4fd41b4ddb53028e 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntitiesFromQueryResult.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntitiesFromQueryResult.tsx @@ -5,14 +5,14 @@ */ import { GraphQueryResult } from '@graphpolaris/shared/lib/data-access'; -import { getGroupName } from '../../../shared/ResultNodeLinkParserUseCase'; +import { getGroupName } from './ResultNodeLinkParserUseCase'; /** * This calculates all entities from the query result. * @param {NodeLinkResultType} message This is the message with all the information about the entities and relations. * @returns {string[]} All names of the entities which are in the query result. */ -export default function calcEntitiesFromQueryResult(message: GraphQueryResult): string[] { +export function calcEntitiesFromQueryResult(message: GraphQueryResult): string[] { const entityTypesFromQueryResult: string[] = []; message.nodes.forEach((node) => { const group = getGroupName(node); diff --git a/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntityAttrAndRelNamesFromSchemaUseCase.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntityAttrAndRelNamesFromSchemaUseCase.tsx deleted file mode 100644 index b369cfa304cf338b65d4088c30d18b01fd14d475..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/paohvis/utils/CalcEntityAttrAndRelNamesFromSchemaUseCase.tsx +++ /dev/null @@ -1,130 +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) - */ -import { SchemaGraph, SchemaGraphologyEdge } from '@graphpolaris/shared/lib/schema'; -import { AttributeNames, EntitiesFromSchema, RelationsFromSchema } from '../Types'; - -/** Use case for retrieving entity names, relation names and attribute names from a schema result. */ -export default class CalcEntityAttrAndRelNamesFromSchemaUseCase { - /** - * Takes a schema result and calculates all the entity names, and relation names and attribute names per entity. - * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering. - * @param {SchemaResultType} schemaResult A new schema result from the backend. - * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity. - */ - public static calculateAttributesAndRelations(schemaResult: SchemaGraph): EntitiesFromSchema { - const attributesPerEntity: Record<string, AttributeNames> = this.calculateAttributes(schemaResult); - const relationsPerEntity: Record<string, string[]> = this.calculateRelations(schemaResult); - - return { - entityNames: schemaResult.nodes.filter((node) => node?.attributes?.name !== undefined).map((node) => node.attributes!.name), - attributesPerEntity, - relationsPerEntity, - }; - } - - /** - * Takes a schema result and calculates all the attribute names per entity. - * @param {SchemaResultType} schemaResult A new schema result from the backend. - * @returns {Record<string, AttributeNames>} All attribute names per entity. - */ - public static calculateAttributes(schemaResult: SchemaGraph): Record<string, AttributeNames> { - const attributesPerEntity: Record<string, AttributeNames> = {}; - // Go through each entity. - schemaResult.nodes.forEach((node) => { - if (node.attributes === undefined || node.attributes.name === undefined) { - console.error('ERROR: Node has no attributes/name.', node); - return; - } - - // Extract the attribute names per datatype for each entity. - const textAttributeNames: string[] = []; - const boolAttributeNames: string[] = []; - const numberAttributeNames: string[] = []; - node.attributes.attributes.forEach((attr) => { - if (attr.type == 'string') textAttributeNames.push(attr.name); - else if (attr.type == 'int' || attr.type == 'float') numberAttributeNames.push(attr.name); - else boolAttributeNames.push(attr.name); - }); - - // Create a new object with the arrays with attribute names per datatype. - attributesPerEntity[node.attributes.name] = { - textAttributeNames, - boolAttributeNames, - numberAttributeNames, - }; - }); - return attributesPerEntity; - } - - /** - * Takes a schema result and calculates all the relation names per entity. - * @param {SchemaResultType} schemaResult A new schema result from the backend. - * @returns {Record<string, AttributeNames>} All relation (from and to) names per entity. - */ - public static calculateRelations(schemaResult: SchemaGraph): Record<string, string[]> { - const relationsPerEntity: Record<string, string[]> = {}; - // Go through each relation. - schemaResult.edges.forEach((edge) => { - if (edge.attributes === undefined || edge.attributes.name === undefined) { - console.error('ERROR: Edge has no attributes/name.', edge); - return; - } - - // Extract the from-node-name (collection name) from every relation. - if (relationsPerEntity[edge.attributes.from]) relationsPerEntity[edge.attributes.from].push(edge.attributes.collection); - else relationsPerEntity[edge.attributes.from] = [edge.attributes.collection]; - // Extract the to-node-name (collection name) from every relation. - if (relationsPerEntity[edge.attributes.to]) relationsPerEntity[edge.attributes.to].push(edge.attributes.collection); - else relationsPerEntity[edge.attributes.to] = [edge.attributes.collection]; - }); - return relationsPerEntity; - } - - /** - * Takes a schema result and calculates all the relation collection names, and relation names and attribute names per relation. - * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering. - * @param {SchemaResultType} schemaResult A new schema result from the backend. - * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity. - */ - public static calculateAttributesFromRelation(schemaResult: SchemaGraph): RelationsFromSchema { - const relationCollections: string[] = []; - const attributesPerRelation: Record<string, AttributeNames> = {}; - const nameOfCollectionPerRelation: Record<string, string> = {}; - // Go through each relation. - schemaResult.edges.forEach((edge) => { - if (edge.attributes === undefined || edge.attributes.name === undefined) { - console.error('ERROR: Edge has no attributes/name.', edge); - return; - } - - if (!nameOfCollectionPerRelation[edge.attributes.collection]) { - relationCollections.push(edge.attributes.collection); - nameOfCollectionPerRelation[edge.attributes.collection] = `${edge.attributes.name}`; - } - // Extract the attribute names per datatype for each relation. - const textAttributeNames: string[] = []; - const boolAttributeNames: string[] = []; - const numberAttributeNames: string[] = []; - edge.attributes.attributes.forEach((attr: SchemaGraphologyEdge) => { - if (attr.type == 'string') textAttributeNames.push(attr.name); - else if (attr.type == 'int' || attr.type == 'float') numberAttributeNames.push(attr.name); - else boolAttributeNames.push(attr.name); - }); - - // Create a new object with the arrays with attribute names per datatype. - attributesPerRelation[edge.attributes.collection] = { - textAttributeNames, - boolAttributeNames, - numberAttributeNames, - }; - }); - return { - relationCollection: relationCollections, - relationNames: nameOfCollectionPerRelation, - attributesPerRelation: attributesPerRelation, - }; - } -} diff --git a/libs/shared/lib/vis/shared/ResultNodeLinkParserUseCase.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/ResultNodeLinkParserUseCase.tsx similarity index 73% rename from libs/shared/lib/vis/shared/ResultNodeLinkParserUseCase.tsx rename to libs/shared/lib/vis/visualizations/paohvis/utils/ResultNodeLinkParserUseCase.tsx index 611d8d1208a3ce5e43e3770d27a5bd1e91397edc..2af931efa0be54e2dd8f0bce197b3add43e065f4 100644 --- a/libs/shared/lib/vis/shared/ResultNodeLinkParserUseCase.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/ResultNodeLinkParserUseCase.tsx @@ -3,38 +3,8 @@ * Utrecht University within the Software Project course. * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { GraphType, LinkType, NodeType } from '../visualizations/nodelinkvis/types'; -import { Edge, Node, GraphQueryResult } from '../../data-access/store'; -import { ML } from '../../data-access/store/mlSlice'; -/** ResultNodeLinkParserUseCase implements methods to parse and translate websocket messages from the backend into a GraphType. */ - -/** - * 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) - */ -/** A node link data-type for a query result object from the backend. */ -// export type NodeLinkResultType = { DEPRECATED USE GraphQueryResult -// nodes: Node[]; -// edges: Link[]; -// mlEdges?: Link[]; -// }; - -/** Typing for nodes and links in the node-link result. Nodes and links should always have an id and attributes. */ -// export interface AxisType { -// id: string; -// attributes: Record<string, any>; -// mldata?: Record<string, string[]> | number; // This is shortest path data . This name is needs to be changed together with backend TODO: Change this. -// } - -/** Typing for a node in the node-link result */ -// export type Node = AxisType; - -/** Typing for a link in the node-link result */ -// export interface Link extends AxisType { -// from: string; -// to: string; -// } +import { GraphType, LinkType, NodeType } from '../../nodelinkvis/types'; +import { Edge, Node, GraphQueryResult } from '../../../../data-access/store'; export type AxisType = Node | Edge; diff --git a/libs/shared/lib/vis/visualizations/paohvis/utils/SortUseCase.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/SortUseCase.tsx index 85686eca501f35892d116a7162e5d5ef9712198e..ef2aea133e89a8fdab4ff7c2cb070c2b3e8006dd 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/utils/SortUseCase.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/SortUseCase.tsx @@ -4,7 +4,7 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { HyperEdgeI, HyperEdgeRange, NodeOrder, PaohvisNodeOrder, ValueType } from '../Types'; +import { HyperEdgeI, HyperEdgeRange, NodeOrder, PaohvisNodeOrder, ValueType } from '../types'; import { Node } from '@graphpolaris/shared/lib/data-access'; import { NodeAttributes } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; @@ -46,7 +46,7 @@ export default class SortUseCase { */ private static sortHyperEdgeIndices(hyperEdgeRanges: HyperEdgeRange[]): void { hyperEdgeRanges.forEach((hyperEdgeRange) => - hyperEdgeRange.hyperEdges.forEach((hyperEdge) => hyperEdge.indices.sort((n1, n2) => n1 - n2)) + hyperEdgeRange.hyperEdges.forEach((hyperEdge) => hyperEdge.indices.sort((n1, n2) => n1 - n2)), ); } @@ -61,7 +61,7 @@ export default class SortUseCase { nodeOrder: PaohvisNodeOrder, nodes: Node[], hyperEdgeDegree: Record<string, number>, - entityVerticalListed: string + entityVerticalListed: string, ): void { switch (nodeOrder.orderBy) { //sort nodes on their degree (# number of hyperedges) (entities with most hyperedges first) @@ -176,11 +176,14 @@ export default class SortUseCase { public static sortNodesByGroupAlphabetically(nodes: Node[], attributeSort: string): void { const groupBy = (array: Node[], keyFunc: (item: Node) => string) => { - return array.reduce((result, item) => { - const key = keyFunc(item); - (result[key] = result[key] || []).push(item); - return result; - }, {} as { [key: string]: Node[] }); + return array.reduce( + (result, item) => { + const key = keyFunc(item); + (result[key] = result[key] || []).push(item); + return result; + }, + {} as { [key: string]: Node[] }, + ); }; // Groups nodes according attributeSort diff --git a/libs/shared/lib/vis/visualizations/paohvis/utils/ToPaohvisDataParserUsecase.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/ToPaohvisDataParserUsecase.tsx index 6004bbcc4ff3eb198200ab98b1e5272b55006a34..85f43afc256cfe9bea743bbd9f50a0943e7adbcb 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/utils/ToPaohvisDataParserUsecase.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/ToPaohvisDataParserUsecase.tsx @@ -4,12 +4,11 @@ * © Copyright Utrecht University (Department of Information and Computing Sciences) */ -import { AxisType, getGroupName } from '../../../shared/ResultNodeLinkParserUseCase'; -import { Edge, GraphQueryResult, Node } from '@graphpolaris/shared/lib/data-access'; -import Graph, { MultiGraph } from 'graphology'; +import { getGroupName } from './ResultNodeLinkParserUseCase'; +import { GraphQueryResult, Node } from '@graphpolaris/shared/lib/data-access'; +import { MultiGraph } from 'graphology'; import { - AttributeOrigin, HyperEdgeI, HyperEdgeRange, PaohvisAxisInfo, @@ -19,24 +18,21 @@ import { PaohvisNodeOrder, Relation, ValueType, - GraphData, AugmentedEdgeAttributes, AugmentedNodeAttributesGraph, connectionFromTo, -} from '../Types'; -import AttributeFilterUsecase, { filterUnusedEdges, getIds } from './AttributesFilterUseCase'; +} from '../types'; +import AttributeFilterUsecase, { getIds } from './AttributesFilterUseCase'; import SortUseCase from './SortUseCase'; import { getWidthOfText, getUniqueArrays, findConnectionsNodes, buildGraphology } from './utils'; import style from '../paohvis.module.scss'; -import { ConstructionOutlined } from '@mui/icons-material'; -//import { CropLandscapeOutlined } from '@mui/icons-material'; type Index = number; /** * This parser is used to parse the incoming query result to the format that's needed to make the Paohvis table. */ -export default class ToPaohvisDataParserUseCase { +export class ToPaohvisDataParserUseCase { private queryResult: GraphQueryResult; private xAxisNodeGroup: string; private yAxisNodeGroup: string; diff --git a/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.module.scss b/libs/shared/lib/vis/visualizations/paohvis/utils/VisConfigPanel.module.scss similarity index 97% rename from libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.module.scss rename to libs/shared/lib/vis/visualizations/paohvis/utils/VisConfigPanel.module.scss index 4904ce9dbf522bf430586ca1694beaf9c8632116..0b117146151f8374de1b3031143a6027acb18a45 100644 --- a/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.module.scss +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/VisConfigPanel.module.scss @@ -21,7 +21,9 @@ $expandButtonSize: 13px; top: 0; background-color: #f0f0f0; - transition: right 0.2s, left 0.2s; + transition: + right 0.2s, + left 0.2s; z-index: 100; border-radius: 0 0 0 5px; diff --git a/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/VisConfigPanel.tsx similarity index 62% rename from libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.tsx rename to libs/shared/lib/vis/visualizations/paohvis/utils/VisConfigPanel.tsx index 4b52f95953e7b12b880dc94424044bc759cd14b4..b0575f99e70ee168341b5e293769b202335f5cb8 100644 --- a/libs/shared/lib/vis/shared/VisConfigPanel/VisConfigPanel.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/VisConfigPanel.tsx @@ -1,21 +1,6 @@ -/** - * 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.*/ - -/** A component for rendering a configuration panel to the right of the visualization. */ - import React, { useState } from 'react'; import styles from './VisConfigPanel.module.scss'; -//TODO import { ReactComponent as ArrowRightSVG } from './ArrowRightIcon.svg'; - export default function VisConfigPanelComponent({ width = 250, children, diff --git a/libs/shared/lib/vis/visualizations/paohvis/utils/utils.tsx b/libs/shared/lib/vis/visualizations/paohvis/utils/utils.tsx index 6871bbc6789cc9a0d372dddae18b1252a43d241a..277c62af7bf896ada830541c28e594e39891e754 100644 --- a/libs/shared/lib/vis/visualizations/paohvis/utils/utils.tsx +++ b/libs/shared/lib/vis/visualizations/paohvis/utils/utils.tsx @@ -1,5 +1,4 @@ -import { log } from 'console'; -import { SchemaAttribute, SchemaGraph, SchemaGraphologyEdge, SchemaGraphologyNode } from '../../../../schema'; +import { SchemaAttribute, SchemaGraph } from '../../../../schema'; import { AttributeNames, EntitiesFromSchema, @@ -8,12 +7,12 @@ import { idConnections, GraphData, AugmentedNodeAttributesGraph, -} from '../Types'; -import Graph, { MultiGraph } from 'graphology'; +} from '../types'; +import { MultiGraph } from 'graphology'; /** * Takes a schema result and calculates all the entity names, and relation names and attribute names per entity. - * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering. + * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualization or when filtering. * @param {SchemaResultType} schemaResult A new schema result from the backend. * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity. */ @@ -88,7 +87,7 @@ export function calculateRelations(schemaResult: SchemaGraph): Record<string, st /** * Takes a schema result and calculates all the relation collection names, and relation names and attribute names per relation. - * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualisation or when filtering. + * Used by PAOHvis to show all possible options to choose from when adding a new PAOHvis visualization or when filtering. * @param {SchemaResultType} schemaResult A new schema result from the backend. * @returns {EntitiesFromSchema} All entity names, and relation names and attribute names per entity. */ diff --git a/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.stories.tsx b/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.stories.tsx index b040c99c245dd16d8d63aabb7253a117b80e4214..bdfc75c5bef072f3c8fcf1cef13be0550dc68fb3 100644 --- a/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.stories.tsx @@ -1,23 +1,23 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { VisualizationPanel } from '../../visualizationPanel'; -import { - setNewGraphQueryResult, - graphQueryResultSlice, - querybuilderSlice, - resetGraphQueryResults, - schemaSlice, - store, - visualizationSlice, -} from '../../../data-access/store'; +import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { mockLargeQueryResults } from '../../../mock-data/query-result'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import RawJSONComponent from './rawjsonvis'; -const Component: Meta<typeof VisualizationPanel> = { +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, + visualize: visualizationSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); + +const Component: Meta<typeof RawJSONComponent.component> = { title: 'Visualizations/RawJSONVIS', - component: VisualizationPanel, + component: RawJSONComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -34,61 +34,33 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - graphQueryResult: graphQueryResultSlice.reducer, - visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, - }, -}); - export const SimpleData = { - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: { - nodes: [ - { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, - { id: 'villain', attributes: { name: 'Le Chiffre' } }, - ], - edges: [], - }, - }, - }), - ); - dispatch(setActiveVisualization('RawJSONVis')); + args: { + data: { + nodes: [ + { id: 'agent/007', attributes: { name: 'Daniel Craig' } }, + { id: 'villain', attributes: { name: 'Le Chiffre' } }, + ], + edges: [], + }, + configuration: RawJSONComponent.configuration, }, }; export const LargeData = { args: { - loading: false, - }, - play: async () => { - const dispatch = Mockstore.dispatch; - dispatch( - setNewGraphQueryResult({ - queryID: '1', - result: { - type: 'nodelink', - payload: mockLargeQueryResults, - }, - }), - ); - dispatch(setActiveVisualization('RawJSONVis')); + data: mockLargeQueryResults, + configuration: RawJSONComponent.configuration, }, }; export const Empty = { - play: async () => { - const dispatch = store.dispatch; - dispatch(resetGraphQueryResults()); - dispatch(setActiveVisualization('RawJSONVis')); + args: { + data: { + nodes: [], + edges: [], + }, + configuration: RawJSONComponent.configuration, }, }; diff --git a/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx b/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx index f62cd2a94022d4592188021dc9650224818afbd7..fa8ec23553a90a924004f48485e4f065e9c742e3 100644 --- a/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx +++ b/libs/shared/lib/vis/visualizations/rawjsonvis/rawjsonvis.tsx @@ -1,32 +1,93 @@ import React, { useEffect } from 'react'; import ReactJSONView from 'react-json-view'; -import { VisualizationPropTypes, VISComponentType } from '../../types'; +import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; +import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; -export interface RawJSONVisProps {} +export interface RawJSONVisProps { + showDataTypes: boolean; + showObjectSize: boolean; + enableClipboard: boolean; + theme: string; + iconStyle: string; +} -const displayName = 'RawJSONVis'; +const configuration: RawJSONVisProps = { + showDataTypes: false, + showObjectSize: false, + enableClipboard: false, + theme: 'bright:inverted', + iconStyle: 'circle', +}; -export const RawJSONVis = React.memo(({ data }: VisualizationPropTypes) => { +export const RawJSONVis = React.memo(({ data, configuration }: VisualizationPropTypes) => { return ( - <div> - <div style={{ overflowY: 'auto' }}> - <div - style={{ - marginTop: '50px', - paddingLeft: '30px', - }} - className="font-mono text-sm" - > - <ReactJSONView src={data} collapsed={1} quotesOnKeys={false} displayDataTypes={false} /> - </div> - </div> - </div> + <ReactJSONView + src={data} + collapsed={1} + quotesOnKeys={false} + style={{ padding: '20px', flexGrow: 1 }} + theme={configuration.theme} + iconStyle={configuration.iconStyle} + displayDataTypes={configuration.displayDataTypes} + displayObjectSize={configuration.displayObjectSize} + enableClipboard={configuration.enableClipboard} + /> ); }); +const RawJSONSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: RawJSONVisProps; + graph: GraphMetaData; + updateSettings: (val: any) => void; +}) => { + return ( + <SettingsContainer> + <Input + type="dropdown" + label="Select a theme" + value={configuration.theme} + options={['bright:inverted', 'monokai', 'ocean']} + onChange={(val) => updateSettings({ theme: val })} + /> + <Input + type="dropdown" + label="Icon style" + value={configuration.iconStyle} + options={['circle', 'square', 'triangle']} + onChange={(val) => updateSettings({ iconStyle: val })} + /> + {/* <Input + type="boolean" + label="Show data types" + value={configuration.showDataTypes} + onChange={(val) => updateSettings({ showDataTypes: val })} + /> + <Input + type="boolean" + label="Show object size" + value={configuration.showObjectSize} + onChange={(val) => updateSettings({ showObjectSize: val })} + /> + <Input + type="boolean" + label="Enable clipboard" + value={configuration.enableClipboard} + onChange={(val) => updateSettings({ enableClipboard: val })} + /> */} + </SettingsContainer> + ); +}; + export const RawJSONComponent: VISComponentType = { displayName: 'RawJSONVis', - VIS: RawJSONVis, - settings: {}, + component: RawJSONVis, + settings: RawJSONSettings, + configuration: configuration, }; export default RawJSONComponent; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstrates/subcomponents/OptimizedAutocomplete.tsx b/libs/shared/lib/vis/visualizations/semanticsubstrates/subcomponents/OptimizedAutocomplete.tsx deleted file mode 100644 index 194a3bbf5a6beaba88d9d1ace2136278c20f8f8c..0000000000000000000000000000000000000000 --- a/libs/shared/lib/vis/visualizations/semanticsubstrates/subcomponents/OptimizedAutocomplete.tsx +++ /dev/null @@ -1,94 +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 React from 'react'; -import { VariableSizeList, ListChildComponentProps } from 'react-window'; -import styles from './OptimizedAutocomplete.module.scss'; - -const LISTBOX_PADDING = 8; // px - -function renderRow(props: ListChildComponentProps) { - const { data, index, style } = props; - return React.cloneElement(data[index], { - style: { - ...style, - top: (style.top as number) + LISTBOX_PADDING, - }, - }); -} - -const OuterElementContext = React.createContext({}); - -const OuterElementType = React.forwardRef<HTMLDivElement>((props, ref) => { - const outerProps = React.useContext(OuterElementContext); - return <div ref={ref} {...props} {...outerProps} />; -}); - -function useResetCache(data: any) { - const ref = React.useRef<VariableSizeList>(null); - React.useEffect(() => { - if (ref.current != null) { - ref.current.resetAfterIndex(0, true); - } - }, [data]); - return ref; -} - -// Adapter for react-window -const ListboxComponent = React.forwardRef<HTMLDivElement>(function ListboxComponent(props: any, ref) { - const { children, ...other } = props; - const itemData = React.Children.toArray(children); - const itemCount = itemData.length; - const itemSize = 36; - - return <div ref={ref}></div>; -}); - -const renderGroup = (params: any) => [<div key={params.key}>{params.group}</div>, params.children]; - -type OptimizedAutocomplete = { - currentValue: string; - options: string[]; - useMaterialStyle?: { label: string; helperText: string }; - /** Called when the value of the input field changes. */ - onChange?(value: string): void; - /** Called when the user leaves focus of the input field. */ - onLeave?(value: string): void; -}; -/** Renders the autocomplete input field with the given props. */ -export default function OptimizedAutocomplete(props: OptimizedAutocomplete) { - let newValue = props.currentValue; - console.log(props); - - return ( - <input - id="optimized-autocomplete" - style={{ width: 200 }} - className={styles.listbox} - // defaultValue={props.currentValue} - placeholder={props.currentValue} - // renderOption={(props, option, state) => <Typography noWrap>{option}</Typography>} - onChange={(e) => { - let value = e.target.value; - console.log(value); - - newValue = value || '?'; - if (props.onChange) props.onChange(newValue); - }} - onBlur={() => { - if (props.onLeave) props.onLeave(newValue); - }} - onKeyDown={(e: React.KeyboardEvent<HTMLInputElement>): void => { - if (e.key == 'Enter' && props.onLeave) props.onLeave(newValue); - }} - /> - ); -} diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.ts similarity index 100% rename from libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.tsx rename to libs/shared/lib/vis/visualizations/semanticsubstratesvis/components/types.ts diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.stories.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.stories.tsx index 752304cb0663f7cf8a7f7d920580d4bf0f29d498..89b021f5fc439633d676b415a035a78473449004 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.stories.tsx @@ -1,16 +1,7 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { VisualizationPanel } from '../../visualizationPanel'; -import { SchemaUtils } from '../../../schema/schema-utils'; - -import { - setNewGraphQueryResult, - graphQueryResultSlice, - querybuilderSlice, - schemaSlice, - setSchema, - visualizationSlice, -} from '../../../data-access/store'; +import { SemSubstrVisComponent } from './semanticsubstratesvis'; +import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { @@ -20,15 +11,20 @@ import { marieBoucherSampleSchemaRaw, big2ndChamberSchemaRaw, } from '../../../mock-data'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import { graphQueryBackend2graphQuery } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; + +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, + visualize: visualizationSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); -const Component: Meta<typeof VisualizationPanel> = { - /* 👇 The title prop is optional. - * See https://storybook.js.org/docs/react/configure/overview#configure-story-loading - * to learn how to generate automatic titles - */ - title: 'Visualizations/VisSemanticSubstrates', - component: VisualizationPanel, +const Component: Meta<typeof SemSubstrVisComponent.component> = { + title: 'Visualizations/SemanticSubstrates', + component: SemSubstrVisComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -45,42 +41,24 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - graphQueryResult: graphQueryResultSlice.reducer, - visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, - }, -}); - export const TestWithBig2ndChamber = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(big2ndChamberSchemaRaw); - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } })); - dispatch(setActiveVisualization('SemSubstrVis')); + args: { + data: graphQueryBackend2graphQuery(big2ndChamberQueryResult), + configuration: SemSubstrVisComponent.configuration, }, }; export const TestWithRecommendationsActorMovie = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(marieBoucherSampleSchemaRaw); - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: mockRecommendationsActorMovie } })); - dispatch(setActiveVisualization('SemSubstrVis')); + args: { + data: graphQueryBackend2graphQuery(mockRecommendationsActorMovie), + configuration: SemSubstrVisComponent.configuration, }, }; export const TestWithGOTcharacter2character = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(marieBoucherSampleSchemaRaw); - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: gotCharacter2Character } })); - dispatch(setActiveVisualization('SemSubstrVis')); + args: { + data: graphQueryBackend2graphQuery(gotCharacter2Character), + configuration: SemSubstrVisComponent.configuration, }, }; diff --git a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx index 56bc0a031a74f5522975a36c8b078f54a6ebfd7c..8b73e3768f5c0da8d034b9398805094b308bc734 100644 --- a/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx +++ b/libs/shared/lib/vis/visualizations/semanticsubstratesvis/semanticsubstratesvis.tsx @@ -1,8 +1,10 @@ import React, { useRef, useState, useMemo, useEffect } from 'react'; import Scatterplot, { KeyedScatterplotProps } from './components/Scatterplot'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; +import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; import { categoricalColors } from 'config/src/colors'; -import { VISComponentType, VisualizationPropTypes } from '../../types'; +import { VisualizationPropTypes, VISComponentType } from '../../common'; import { findConnectionsNodes, getRegionData, setExtension, filterArray, getUniqueValues } from './components/utils'; import { Node } from '@graphpolaris/shared/lib/data-access/store/graphQueryResultSlice'; @@ -24,15 +26,21 @@ import { MultiGraph } from 'graphology'; import { select, selectAll, scaleOrdinal } from 'd3'; import { buildGraphology, configVisualRegion, config, numColorsCategorical, marginAxis, isColorCircleFix } from './utils'; +import { Input } from '../../..'; export type SemSubstrProps = { showColor: boolean; }; +const configuration: SemSubstrProps = { + showColor: true, +}; + const displayName = 'SemSubstrVis'; -export const VisSemanticSubstrates = ({ data: graphQueryResult, schema, settings }: VisualizationPropTypes) => { - const nodes = graphQueryResult.nodes; - const edges = graphQueryResult.edges; + +export const VisSemanticSubstrates = ({ data, configuration }: VisualizationPropTypes) => { + const nodes = data.nodes; + const edges = data.edges; const divRef = useRef<HTMLDivElement>(null); const idEdges = useRef<string[][]>([]); @@ -415,10 +423,27 @@ export const VisSemanticSubstrates = ({ data: graphQueryResult, schema, settings ); }; +const SemSubstrSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: SemSubstrProps; + graph: GraphMetaData; + updateSettings: (val: any) => void; +}) => { + return ( + <SettingsContainer> + <Input type="boolean" label="Show color" value={configuration.showColor} onChange={(val) => updateSettings({ showColor: val })} /> + </SettingsContainer> + ); +}; + export const SemSubstrVisComponent: VISComponentType = { displayName: displayName, - VIS: VisSemanticSubstrates, - settings: {}, + component: VisSemanticSubstrates, + settings: SemSubstrSettings, + configuration: configuration, }; export default SemSubstrVisComponent; diff --git a/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx b/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx index c4474068698226562546502c4d8a940d7c7a8fe5..e5133ca74d067517f6a54549c6c2b52aaa3efdfb 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/components/Table.tsx @@ -16,6 +16,7 @@ export type TableProps = { data: AugmentedNodeAttributes[]; itemsPerPage: number; showBarPlot: boolean; + showAttributes: string[]; }; type Data2RenderI = { name: string; @@ -27,7 +28,7 @@ type Data2RenderI = { const THRESHOLD_WIDTH = 100; -export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { +export const Table = ({ data, itemsPerPage, showBarPlot, showAttributes }: TableProps) => { const maxUniqueValues = 29; const barPlotNumBins = 10; const fetchAttributes = 0; @@ -42,9 +43,12 @@ export const Table = ({ data, itemsPerPage, showBarPlot }: TableProps) => { currentData: AugmentedNodeAttributes[]; } | null>(null); const [data2Render, setData2Render] = useState<Data2RenderI[]>([]); - //const dataColumns = useMemo(() => Object.keys(data[0].attribute), [data]); - - const dataColumns = useMemo(() => Object.keys(data[data.length > fetchAttributes ? fetchAttributes : 0].attribute), [data]); + const dataColumns = useMemo(() => { + if (showAttributes && showAttributes.length > 0) { + return showAttributes.filter((attr) => Object.keys(data[0].attribute).includes(attr)); + } + return Object.keys(data[0].attribute); + }, [data, showAttributes]); const totalPages = Math.ceil(sortedData.length / itemsPerPage); const [columnWidths, setColumnWidths] = useState<number[]>([]); diff --git a/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx b/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx index 0b0c85d84acf0fa5806a6c9f6574f6233dca8cef..26488d4a2914d6f2c78a4c62044e90133f96495f 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/tablevis.stories.tsx @@ -1,14 +1,6 @@ import React from 'react'; import { Meta } from '@storybook/react'; -import { VisualizationPanel } from '../../visualizationPanel'; -import { - setNewGraphQueryResult, - graphQueryResultSlice, - querybuilderSlice, - schemaSlice, - setSchema, - visualizationSlice, -} from '../../../data-access/store'; +import { graphQueryResultSlice, querybuilderSlice, schemaSlice, visualizationSlice } from '../../../data-access/store'; import { configureStore } from '@reduxjs/toolkit'; import { Provider } from 'react-redux'; import { @@ -18,14 +10,21 @@ import { typesMockQueryResults, typesMockSchemaRaw, } from '../../../mock-data'; - -import { SchemaUtils } from '../../../schema/schema-utils'; import { simpleSchemaAirportRaw } from '../../../mock-data/schema/simpleAirportRaw'; -import { setActiveVisualization } from '@graphpolaris/shared/lib/data-access/store/visualizationSlice'; +import TableComponent from './tablevis'; + +const Mockstore = configureStore({ + reducer: { + schema: schemaSlice.reducer, + graphQueryResult: graphQueryResultSlice.reducer, + visualize: visualizationSlice.reducer, + querybuilder: querybuilderSlice.reducer, + }, +}); -const Component: Meta<typeof VisualizationPanel> = { +const Component: Meta<typeof TableComponent.component> = { title: 'Visualizations/TableVis', - component: VisualizationPanel, + component: TableComponent.component, decorators: [ (story) => ( <Provider store={Mockstore}> @@ -42,45 +41,27 @@ const Component: Meta<typeof VisualizationPanel> = { ], }; -const Mockstore = configureStore({ - reducer: { - schema: schemaSlice.reducer, - graphQueryResult: graphQueryResultSlice.reducer, - visualize: visualizationSlice.reducer, - querybuilder: querybuilderSlice.reducer, - }, -}); - export const TestWithAirport = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(simpleSchemaAirportRaw); - - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: bigMockQueryResults } })); - dispatch(setActiveVisualization('TableVis')); + args: { + data: bigMockQueryResults, + schema: simpleSchemaAirportRaw, + configuration: TableComponent.configuration, }, }; export const TestWithBig2ndChamber = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(big2ndChamberSchemaRaw); - - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: big2ndChamberQueryResult } })); - dispatch(setActiveVisualization('TableVis')); + args: { + data: big2ndChamberQueryResult, + schema: big2ndChamberSchemaRaw, + configuration: TableComponent.configuration, }, }; export const TestWithTypesMock = { - play: async () => { - const dispatch = Mockstore.dispatch; - const schema = SchemaUtils.schemaBackend2Graphology(typesMockSchemaRaw); - - dispatch(setSchema(schema.export())); - dispatch(setNewGraphQueryResult({ queryID: '1', result: { type: 'nodelink', payload: typesMockQueryResults } })); - dispatch(setActiveVisualization('TableVis')); + args: { + data: typesMockQueryResults, + schema: typesMockSchemaRaw, + configuration: TableComponent.configuration, }, }; diff --git a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx index f1cc8dac14a57ed89f9c1bda0dc1454d167f5920..552b40db3dfefc778ff6a725ee335cc27d0fa2ee 100644 --- a/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx +++ b/libs/shared/lib/vis/visualizations/tablevis/tablevis.tsx @@ -1,14 +1,24 @@ import React, { useMemo, useRef } from 'react'; import { Table, AugmentedNodeAttributes } from './components/Table'; import { SchemaAttribute } from '../../../schema'; -import { VisualizationPropTypes, VISComponentType } from '../../types'; +import { VisualizationPropTypes, VISComponentType } from '../../common'; +import { Input } from '@graphpolaris/shared/lib/components/inputs'; +import { GraphMetaData } from '@graphpolaris/shared/lib/data-access/statistics'; +import { SettingsContainer } from '@graphpolaris/shared/lib/vis/components/config'; export type TableProps = { showBarplot: boolean; itemsPerPage: number; + displayAttributes: string[]; }; -export const TableVis = ({ data, schema, settings }: VisualizationPropTypes) => { +const configuration: TableProps = { + itemsPerPage: 10, + showBarplot: false, + displayAttributes: [], +}; + +export const TableVis = ({ data, schema, configuration }: VisualizationPropTypes) => { const ref = useRef<HTMLDivElement>(null); const attributesArray = useMemo<AugmentedNodeAttributes[]>( @@ -24,33 +34,85 @@ export const TableVis = ({ data, schema, settings }: VisualizationPropTypes) => type: Object.fromEntries(types.map((t) => [t.name, t.type])), }; }), - [data.nodes] + [data.nodes], ); return ( <div className="h-full w-full" ref={ref}> {attributesArray.length > 0 && ( - <Table data={attributesArray} itemsPerPage={settings.itemsPerPage} showBarPlot={settings.showBarplot} /> + <Table + data={attributesArray} + itemsPerPage={configuration.itemsPerPage} + showBarPlot={configuration.showBarplot} + showAttributes={configuration.displayAttributes} + /> )} </div> ); }; +const TableSettings = ({ + configuration, + graph, + updateSettings, +}: { + configuration: TableProps; + 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); + }, []); + + // 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 uniqueIntersectionAttributes = Array.from(new Set(intersectionAttributes)); + + return ( + <SettingsContainer> + <Input + type="dropdown" + label="Items per page" + value={configuration.itemsPerPage} + onChange={(val) => updateSettings({ itemsPerPage: val })} + options={[10, 25, 50, 100]} + /> + <Input + type="boolean" + label="Show barplot" + value={configuration.showBarplot} + onChange={(val) => updateSettings({ showBarplot: val })} + /> + + <div> + <span className="text-sm">Attributes to display</span> + <div className=""> + <Input + type="checkbox" + value={configuration.displayAttributes} + options={uniqueIntersectionAttributes} + onChange={(val: string[] | string) => { + const updatedVal = Array.isArray(val) ? val : [val]; + updateSettings({ displayAttributes: updatedVal }); + }} + /> + </div> + </div> + </SettingsContainer> + ); +}; + export const TableComponent: VISComponentType = { displayName: 'TableVis', - VIS: TableVis, - settings: { - showBarplot: { - value: true, - type: 'boolean', - label: 'Show barplot', - }, - itemsPerPage: { - value: 10, - type: 'dropdown', - label: 'Items per page', - options: [10, 20, 30], - }, - }, + component: TableVis, + settings: TableSettings, + configuration: configuration, }; + export default TableComponent; diff --git a/libs/shared/package.json b/libs/shared/package.json index bccd77823328ecce1e2a60a10ea98e560245b7bd..aef1e4dc926dfce3e2eb10496b49ee5675be7043 100644 --- a/libs/shared/package.json +++ b/libs/shared/package.json @@ -24,6 +24,7 @@ "@mui/icons-material": "^5.15.13", "@nebula.gl/layers": "^1.0.4", "@pixi-essentials/cull": "^2.0.0", + "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-tooltip": "^1.0.7", "@reactflow/node-resizer": "^2.2.9", "@reduxjs/toolkit": "^2.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cce9a26896e47d988f541e425d3d2f856547a783..8edaf1eedf1b045eac41275587c93d2412692849 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: '@pixi-essentials/cull': specifier: ^2.0.0 version: 2.0.0(@pixi/display@7.4.0)(@pixi/math@7.4.0) + '@radix-ui/react-dropdown-menu': + specifier: ^2.0.6 + version: 2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) '@radix-ui/react-tooltip': specifier: ^1.0.7 version: 1.0.7(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) @@ -5902,7 +5905,6 @@ packages: '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-compose-refs@1.0.1(@types/react@18.2.65)(react@18.2.0): resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} @@ -5942,7 +5944,6 @@ packages: '@babel/runtime': 7.24.0 '@types/react': 18.2.65 react: 18.2.0 - dev: true /@radix-ui/react-dismissable-layer@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==} @@ -5994,6 +5995,33 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: false + /@radix-ui/react-dropdown-menu@2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-i6TuFOoWmLWq+M/eCLGd/bQ2HfAX1RJgvrBQ6AQLmzfvsLdefxbWu8G9zczcPFfcSPehz9GcpF6K9QYreFV8hA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-menu': 2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-focus-guards@1.0.1(@types/react@18.2.65)(react@18.2.0): resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: @@ -6006,7 +6034,6 @@ packages: '@babel/runtime': 7.24.0 '@types/react': 18.2.65 react: 18.2.0 - dev: true /@radix-ui/react-focus-scope@1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==} @@ -6031,6 +6058,29 @@ packages: react-dom: 18.2.0(react@18.2.0) dev: true + /@radix-ui/react-focus-scope@1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + dev: false + /@radix-ui/react-id@1.0.1(@types/react@18.2.65)(react@18.2.0): resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: @@ -6045,6 +6095,44 @@ packages: '@types/react': 18.2.65 react: 18.2.0 + /@radix-ui/react-menu@2.0.6(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): + resolution: {integrity: sha512-BVkFLS+bUC8HcImkRKPSiVumA1VPOOEC5WBMiT+QAVsPzW1FJzI9KnqgGxVDPBcql5xXrHkD3JOVoXWEXD8SYA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 + react-dom: ^16.8 || ^17.0 || ^18.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + dependencies: + '@babel/runtime': 7.24.0 + '@radix-ui/primitive': 1.0.1 + '@radix-ui/react-collection': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-compose-refs': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-context': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-direction': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-dismissable-layer': 1.0.5(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-focus-guards': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-focus-scope': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-id': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-popper': 1.1.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-portal': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0) + '@radix-ui/react-slot': 1.0.2(@types/react@18.2.65)(react@18.2.0) + '@radix-ui/react-use-callback-ref': 1.0.1(@types/react@18.2.65)(react@18.2.0) + '@types/react': 18.2.65 + '@types/react-dom': 18.2.22 + aria-hidden: 1.2.3 + react: 18.2.0 + react-dom: 18.2.0(react@18.2.0) + react-remove-scroll: 2.5.5(@types/react@18.2.65)(react@18.2.0) + dev: false + /@radix-ui/react-popper@1.1.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==} peerDependencies: @@ -6216,7 +6304,6 @@ packages: '@types/react-dom': 18.2.22 react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - dev: true /@radix-ui/react-select@1.2.2(@types/react-dom@18.2.22)(@types/react@18.2.65)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==} @@ -9954,7 +10041,6 @@ packages: engines: {node: '>=10'} dependencies: tslib: 2.6.2 - dev: true /aria-query@5.1.3: resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} @@ -10621,7 +10707,7 @@ packages: engines: {node: '>=10.0.0'} requiresBuild: true dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.24.0 '@types/raf': 3.4.0 core-js: 3.36.0 raf: 3.4.1 @@ -12135,7 +12221,6 @@ packages: /detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - dev: true /detect-package-manager@2.0.1: resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==} @@ -13796,7 +13881,6 @@ packages: /get-nonce@1.0.1: resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} engines: {node: '>=6'} - dev: true /get-npm-tarball-url@2.1.0: resolution: {integrity: sha512-ro+DiMu5DXgRBabqXupW38h7WPZ9+Ad8UjwhvsmmN8w1sU7ab0nzAXvVZ4kqYg57OrqomRtJvepX5/xvFKNtjA==} @@ -17875,7 +17959,7 @@ packages: peerDependencies: react: '>=16.13.1' dependencies: - '@babel/runtime': 7.21.0 + '@babel/runtime': 7.24.0 react: 18.2.0 dev: true @@ -18244,6 +18328,7 @@ packages: /react-remove-scroll-bar@2.3.5(@types/react@18.2.65)(react@18.2.0): resolution: {integrity: sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw==} engines: {node: '>=10'} + deprecated: please update to the following version as this contains a bug (https://github.com/theKashey/react-remove-scroll-bar/issues/57) peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 @@ -18255,7 +18340,6 @@ packages: react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.65)(react@18.2.0) tslib: 2.6.2 - dev: true /react-remove-scroll@2.5.5(@types/react@18.2.65)(react@18.2.0): resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==} @@ -18274,7 +18358,6 @@ packages: tslib: 2.6.2 use-callback-ref: 1.3.1(@types/react@18.2.65)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.65)(react@18.2.0) - dev: true /react-resizable@3.0.5(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==} @@ -18349,7 +18432,6 @@ packages: invariant: 2.2.4 react: 18.2.0 tslib: 2.6.2 - dev: true /react-test-renderer@18.2.0(react@18.2.0): resolution: {integrity: sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA==} @@ -20807,7 +20889,6 @@ packages: '@types/react': 18.2.65 react: 18.2.0 tslib: 2.6.2 - dev: true /use-composed-ref@1.3.0(react@18.2.0): resolution: {integrity: sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==} @@ -20879,7 +20960,6 @@ packages: detect-node-es: 1.1.0 react: 18.2.0 tslib: 2.6.2 - dev: true /use-sync-external-store@1.2.0(react@18.2.0): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==}