Skip to content
Snippets Groups Projects

Compare revisions

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

Source

Select target project
No results found

Target

Select target project
  • graphpolaris/frontend-v2
  • rijkheere/frontend-v-2-reordering-paoh
2 results
Show changes
Commits on Source (8)
Showing
with 527 additions and 282 deletions
apps/web/public/assets/sprite.png

687 B

apps/web/public/assets/sprite_selected.png

697 B

apps/web/public/assets/sprite_selected_square.png

96 B

apps/web/public/assets/sprite_square.png

344 B

...@@ -2,12 +2,13 @@ import React, { ReactElement, ReactPropTypes, useMemo } from 'react'; ...@@ -2,12 +2,13 @@ import React, { ReactElement, ReactPropTypes, useMemo } from 'react';
import styles from './buttons.module.scss'; import styles from './buttons.module.scss';
import { Icon, Sizes } from '../icon'; import { Icon, Sizes } from '../icon';
import { forwardRef } from 'react'; import { forwardRef } from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip';
type ButtonProps = { type ButtonProps = {
as?: 'button' | 'a' | 'div'; as?: 'button' | 'a' | 'div';
variantType?: 'primary' | 'secondary' | 'danger'; variantType?: 'primary' | 'secondary' | 'danger';
variant?: 'solid' | 'outline' | 'ghost'; variant?: 'solid' | 'outline' | 'ghost';
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg'; size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl';
label?: string; label?: string;
rounded?: boolean; rounded?: boolean;
disabled?: boolean; disabled?: boolean;
...@@ -20,6 +21,7 @@ type ButtonProps = { ...@@ -20,6 +21,7 @@ type ButtonProps = {
children?: React.ReactNode; children?: React.ReactNode;
className?: string; className?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
tooltip?: string;
onMouseUp?: (e: any) => void; onMouseUp?: (e: any) => void;
onMouseDown?: (e: any) => void; onMouseDown?: (e: any) => void;
onMouseEnter?: (e: any) => void; onMouseEnter?: (e: any) => void;
...@@ -52,6 +54,7 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H ...@@ -52,6 +54,7 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H
ariaLabel, ariaLabel,
className, className,
children, children,
tooltip,
...props ...props
}, },
forwardedRef, forwardedRef,
...@@ -94,6 +97,10 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H ...@@ -94,6 +97,10 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H
return styles['btn-md']; return styles['btn-md'];
case 'lg': case 'lg':
return styles['btn-lg']; return styles['btn-lg'];
case 'xl':
return styles['btn-xl'];
case '2xl':
return styles['btn-2xl'];
default: default:
return styles['btn-md']; return styles['btn-md'];
} }
...@@ -111,6 +118,10 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H ...@@ -111,6 +118,10 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H
return 24; return 24;
case 'lg': case 'lg':
return 28; return 28;
case 'xl':
return 32;
case '2xl':
return 36;
default: default:
return 24; return 24;
} }
...@@ -134,20 +145,25 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H ...@@ -134,20 +145,25 @@ export const Button = React.forwardRef<HTMLButtonElement | HTMLAnchorElement | H
const isAnchor = as === 'a'; const isAnchor = as === 'a';
return ( return (
<ButtonComponent <Tooltip>
className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${className ? className : ''}`} {tooltip && <TooltipContent>{tooltip}</TooltipContent>}
onClick={onClick} <TooltipTrigger>
disabled={disabled} <ButtonComponent
aria-label={ariaLabel} className={`${styles.btn} ${typeClass} ${variantClass} ${sizeClass} ${blockClass} ${roundedClass} ${iconOnlyClass} ${className ? className : ''}`}
href={isAnchor ? href : undefined} onClick={onClick}
ref={forwardedRef as React.RefObject<any>} disabled={disabled}
{...props} aria-label={ariaLabel}
> href={isAnchor ? href : undefined}
{iconPosition === 'leading' && icon} ref={forwardedRef as React.RefObject<any>}
{label && <span>{label}</span>} {...props}
{children && <span>{children}</span>} >
{iconPosition === 'trailing' && icon} {iconPosition === 'leading' && icon}
</ButtonComponent> {label && <span>{label}</span>}
{children && <span>{children}</span>}
{iconPosition === 'trailing' && icon}
</ButtonComponent>
</TooltipTrigger>
</Tooltip>
); );
}, },
); );
......
...@@ -47,6 +47,16 @@ ...@@ -47,6 +47,16 @@
line-height: 1; line-height: 1;
@apply p-1; @apply p-1;
} }
.btn-2xl {
@apply text-2xl h-12 gap-3;
&.btn-icon-only {
}
}
.btn-xl {
@apply text-xl h-11 gap-2;
&.btn-icon-only {
}
}
.btn-lg { .btn-lg {
@apply text-lg h-10 gap-1.5; @apply text-lg h-10 gap-1.5;
&.btn-icon-only { &.btn-icon-only {
......
declare const classNames: { declare const classNames: {
readonly btn: 'btn'; readonly btn: 'btn';
readonly 'btn-icon-only': 'btn-icon-only'; readonly 'btn-icon-only': 'btn-icon-only';
readonly 'btn-2xl': 'btn-2xl';
readonly 'btn-xl': 'btn-xl';
readonly 'btn-lg': 'btn-lg'; readonly 'btn-lg': 'btn-lg';
readonly 'btn-md': 'btn-md'; readonly 'btn-md': 'btn-md';
readonly 'btn-sm': 'btn-sm'; readonly 'btn-sm': 'btn-sm';
......
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Button } from '../buttons'; // Adjust the import path according to your project structure import { Button } from '../buttons'; // Adjust the import path according to your project structure
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip';
import { setTheme, Theme } from '@graphpolaris/shared/lib/data-access/store/configSlice';
import { useAppDispatch, useConfig } from '@graphpolaris/shared/lib/data-access/store';
const ColorMode = () => { const ColorMode = () => {
// Initialize theme state without setting a default yet const config = useConfig();
const [theme, setTheme] = useState<string>(''); const dispatch = useAppDispatch();
// Function to update the body class and local storage // Function to update the body class
const applyTheme = (themeValue: string) => { const applyTheme = (themeValue: string) => {
document.body.className = themeValue; document.body.className = themeValue;
localStorage.setItem('theme', themeValue);
}; };
// Load the theme preference from local storage on initial render
useEffect(() => {
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
setTheme(savedTheme);
} else {
// Fallback to system preference if no saved theme
// const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const prefersDarkMode = false; // TODO: remove once dark theme is fixed
const defaultTheme = prefersDarkMode ? 'dark-mode' : 'light-mode';
setTheme(defaultTheme);
applyTheme(defaultTheme);
}
}, []);
// Update the local storage and body class whenever the theme changes // Update the local storage and body class whenever the theme changes
useEffect(() => { useEffect(() => {
if (theme) { if (config.theme) {
applyTheme(theme); applyTheme(config.theme);
} }
}, [theme]); }, [config.theme]);
// Function to toggle the theme // Function to toggle the theme
const toggleTheme = () => { const toggleTheme = () => {
setTheme((currentTheme) => (currentTheme === 'light-mode' ? 'dark-mode' : 'light-mode')); const themes = [
Theme.light,
Theme.dark,
]
const newTheme = themes[(themes.indexOf(config.theme) + 1) % themes.length];
dispatch(setTheme(newTheme));
}; };
const iconComponent = theme === 'dark-mode' ? 'icon-[ic--baseline-dark-mode]' : 'icon-[ic--baseline-light-mode]'; const iconComponent = config.theme === Theme.dark ? 'icon-[ic--baseline-dark-mode]' : 'icon-[ic--baseline-light-mode]';
return ( return (
<TooltipProvider delayDuration={0}> <TooltipProvider delayDuration={0}>
...@@ -47,7 +39,7 @@ const ColorMode = () => { ...@@ -47,7 +39,7 @@ const ColorMode = () => {
<Button variant="ghost" size="sm" iconComponent={iconComponent} onClick={toggleTheme} /> <Button variant="ghost" size="sm" iconComponent={iconComponent} onClick={toggleTheme} />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p>{`Switch to ${theme === 'dark-mode' ? 'light' : 'dark'}-mode`}</p> <p>{`Switch to ${config.theme === Theme.dark ? 'light' : 'dark'}-mode`}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
......
...@@ -4,7 +4,7 @@ import { useFloating, autoUpdate, offset, flip, shift, useInteractions, useClick ...@@ -4,7 +4,7 @@ import { useFloating, autoUpdate, offset, flip, shift, useInteractions, useClick
type Props = { type Props = {
value: any; value: any;
updateValue: any; updateValue: (val: [number, number, number]) => void;
}; };
export default function ColorPicker({ value, updateValue }: Props) { export default function ColorPicker({ value, updateValue }: Props) {
...@@ -52,9 +52,10 @@ export default function ColorPicker({ value, updateValue }: Props) { ...@@ -52,9 +52,10 @@ export default function ColorPicker({ value, updateValue }: Props) {
<TwitterPicker <TwitterPicker
triangle="top-right" triangle="top-right"
color={{ r: value[0], g: value[1], b: value[2] }} color={{ r: value[0], g: value[1], b: value[2] }}
onChangeComplete={(color: any) => { onChangeComplete={(color) => {
console.log(color);
const rgb = color.rgb; const rgb = color.rgb;
const newValue = [rgb.r, rgb.g, rgb.b]; const newValue: [number, number, number] = [rgb.r, rgb.g, rgb.b];
updateValue(newValue); updateValue(newValue);
setOpen(false); setOpen(false);
}} }}
......
...@@ -5,5 +5,5 @@ type Props = { ...@@ -5,5 +5,5 @@ type Props = {
}; };
export 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>; return <div className="top-4 right-4 flex flex-row-reverse justify-between z-50 items-center">{children}</div>;
} }
...@@ -2,7 +2,7 @@ import React, { ReactElement, ReactNode } from 'react'; ...@@ -2,7 +2,7 @@ import React, { ReactElement, ReactNode } from 'react';
import { SVGProps } from 'react'; import { SVGProps } from 'react';
// Define Sizes and IconProps types // Define Sizes and IconProps types
export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 40; export type Sizes = 12 | 14 | 16 | 20 | 24 | 28 | 32 | 36 | 40;
export type IconProps = SVGProps<SVGSVGElement> & { export type IconProps = SVGProps<SVGSVGElement> & {
component?: ReactNode | ReactElement<any> | string; component?: ReactNode | ReactElement<any> | string;
size?: Sizes; size?: Sizes;
......
import React from 'react'; import React, { useEffect, useState } from 'react';
import styles from './inputs.module.scss'; import styles from './inputs.module.scss';
import { DropdownTrigger, DropdownContainer, DropdownItem, DropdownItemContainer } from '../dropdowns'; import { DropdownTrigger, DropdownContainer, DropdownItem, DropdownItemContainer } from '../dropdowns';
import Info from '../info'; import Info from '../info';
import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../tooltip';
import { Popover } from '../layout/Popover'; import { Popover } from '../layout/Popover';
import { Button } from '../buttons';
type SliderProps = { type SliderProps = {
label: string; label: string;
...@@ -37,23 +38,28 @@ type TextProps = { ...@@ -37,23 +38,28 @@ type TextProps = {
}; };
type NumberProps = { type NumberProps = {
label: string; label?: string;
type: 'number'; type: 'number';
size?: 'xs' | 'sm' | 'md' | 'xl'; size?: 'xs' | 'sm' | 'md' | 'xl';
variant?: 'primary' | 'secondary' | 'danger' | 'warning' | 'success' | 'info' | 'base';
placeholder?: string; placeholder?: string;
value: number; value: number;
required?: boolean; required?: boolean;
errorText?: string; errorText?: string;
visible?: boolean; visible?: boolean;
disabled?: boolean; disabled?: boolean;
tooltip?: string; tooltip?: string | React.ReactNode;
info?: string; info?: string;
inline?: boolean; inline?: boolean;
validate?: (value: any) => boolean; validate?: (value: any) => boolean;
onChange?: (value: number) => void; onChange?: (value: number) => void;
onRevert?: (value: number) => void;
onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void; onKeyDown?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
max?: number; max?: number;
min?: number; min?: number;
className?: string;
containerClassName?: string;
lazy?: boolean;
}; };
type CheckboxProps = { type CheckboxProps = {
...@@ -88,7 +94,7 @@ type DropdownProps = { ...@@ -88,7 +94,7 @@ type DropdownProps = {
overrideRender?: React.ReactNode; overrideRender?: React.ReactNode;
type: 'dropdown'; type: 'dropdown';
size?: 'xs' | 'sm' | 'md' | 'xl'; size?: 'xs' | 'sm' | 'md' | 'xl';
tooltip?: string; tooltip?: string | React.ReactNode;
required?: boolean; required?: boolean;
inline?: boolean; inline?: boolean;
buttonVariant?: 'primary' | 'outline' | 'ghost'; buttonVariant?: 'primary' | 'outline' | 'ghost';
...@@ -159,7 +165,7 @@ export const SliderInput = ({ label, value, min, max, step, unit, showValue = tr ...@@ -159,7 +165,7 @@ export const SliderInput = ({ label, value, min, max, step, unit, showValue = tr
}; };
export const TextInput = ({ export const TextInput = ({
label, label = undefined,
placeholder, placeholder,
value = '', value = '',
size = 'md', size = 'md',
...@@ -225,55 +231,108 @@ export const NumberInput = ({ ...@@ -225,55 +231,108 @@ export const NumberInput = ({
inline = false, inline = false,
required = false, required = false,
visible = true, visible = true,
variant = 'base',
errorText, errorText,
validate, validate,
disabled = false, disabled = false,
onChange, onChange,
onRevert,
tooltip, tooltip,
info, info,
onKeyDown, onKeyDown,
max, max,
min, min,
className,
containerClassName,
lazy = false,
}: NumberProps) => { }: NumberProps) => {
const [isValid, setIsValid] = React.useState<boolean>(true); const [isValid, setIsValid] = React.useState<boolean>(true);
const [inputValue, setInputValue] = useState<number>(value);
if (!tooltip && inline) tooltip = label; useEffect(() => {
setInputValue(value);
}, [value]);
if (!tooltip && inline && label) tooltip = label;
return ( return (
<Tooltip> <div className={styles['input'] + `${containerClassName ? ` ${containerClassName}` : ''}`}>
<TooltipTrigger className={styles['input'] + ' form-control w-full' + (inline ? ' grid grid-cols-2 items-center gap-0.5' : '')}> <TooltipProvider delayDuration={50}>
<label className="label p-0"> <Tooltip>
<span <TooltipTrigger className={'form-control w-full' + (inline && label ? ' grid grid-cols-2 items-center gap-0.5' : '')}>
className={`text-sm text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`} {label && (
> <label className="label p-0">
{label} <span
</span> className={`text-sm text-left truncate font-medium text-secondary-700 ${required && "after:content-['*'] after:ml-0.5 after:text-danger-500"}`}
{required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>} >
{info && <Info tooltip={info} placement={'left'} />} {label}
</label> </span>
<input {required && isValid ? null : <span className="label-text-alt text-error">{errorText}</span>}
type="number" {info && <Info tooltip={info} placement={'left'} />}
placeholder={placeholder} </label>
className={`${size} bg-light border border-secondary-200 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${ )}
isValid ? '' : 'input-error' <div className="relative items-center">
}`} <input
value={value.toString()} type="number"
onChange={(e) => { placeholder={placeholder}
if (required && validate) { className={`${size} bg-light border border-secondary-200 placeholder-secondary-400 focus:outline-none block w-full sm:text-sm focus:ring-1 ${
setIsValid(validate(e.target.value)); isValid ? '' : 'input-error'
} }${className ? ` ${className}` : ''}`}
if (onChange) { value={inputValue.toString()}
onChange(Number(e.target.value)); onChange={(e) => {
} if (required && validate) {
}} setIsValid(validate(e.target.value));
required={required} }
disabled={disabled} setInputValue(Number(e.target.value));
onKeyDown={onKeyDown} if (!lazy && onChange) {
max={max} onChange(Number(e.target.value));
min={min} }
/> }}
</TooltipTrigger> required={required}
{tooltip && <TooltipContent>{tooltip}</TooltipContent>} disabled={disabled}
</Tooltip> onKeyDown={(e) => {
if (lazy && e.key === 'Enter') {
if (onChange) {
onChange(inputValue);
}
}
if (onKeyDown) onKeyDown(e);
}}
max={max}
min={min}
/>
{lazy && value !== inputValue && (
<div className="absolute top-0 right-1 h-full flex flex-row items-center">
<Button
className="hover:bg-success-600 hover:text-white border-none m-0 p-0 h-fit"
variant="outline"
tooltip="Apply Change"
size={size}
rounded
iconComponent={'icon-[ic--round-check-circle-outline]'}
onClick={() => {
if (onChange) onChange(inputValue);
}}
></Button>
<Button
className="hover:bg-warning-600 hover:text-white border-none m-0 p-0 h-fit"
variant="outline"
tooltip="Revert Change"
size={size}
rounded
iconComponent={'icon-[ic--outline-cancel]'}
onClick={() => {
if (onRevert) onRevert(inputValue);
setInputValue(value);
}}
></Button>
</div>
)}
</div>
</TooltipTrigger>
{tooltip && <TooltipContent>{tooltip}</TooltipContent>}
</Tooltip>
</TooltipProvider>
</div>
); );
}; };
......
...@@ -23,11 +23,16 @@ ...@@ -23,11 +23,16 @@
} }
.input { .input {
input[class~='xs'] { input[class~='2xs'] {
@apply py-0; @apply py-0;
@apply px-0; @apply px-0;
@apply sm:text-2xs; @apply sm:text-2xs;
} }
input[class~='xs'] {
@apply py-0.5;
@apply px-0.5;
@apply sm:text-xs;
}
input[class~='sm'] { input[class~='sm'] {
@apply py-1; @apply py-1;
@apply px-1; @apply px-1;
...@@ -37,10 +42,6 @@ ...@@ -37,10 +42,6 @@
@apply py-2; @apply py-2;
@apply px-3; @apply px-3;
} }
input[class~='md'] {
@apply py-2;
@apply px-3;
}
input[class~='xl'] { input[class~='xl'] {
@apply py-3; @apply py-3;
@apply px-5; @apply px-5;
......
import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import type { RootState } from './store'; import type { RootState } from './store';
export enum Theme {
light = 'light-mode',
dark = 'dark-mode',
}
// Define the initial state using that type // Define the initial state using that type
export type ConfigStateI = { export type ConfigStateI = {
theme: Theme;
autoSendQueries: boolean; autoSendQueries: boolean;
errors: string[]; errors: string[];
warnings: string[]; warnings: string[];
...@@ -10,6 +16,7 @@ export type ConfigStateI = { ...@@ -10,6 +16,7 @@ export type ConfigStateI = {
successes: string[]; successes: string[];
}; };
export const initialState: ConfigStateI = { export const initialState: ConfigStateI = {
theme: localStorage.getItem('theme') as Theme ?? Theme.light,
autoSendQueries: true, autoSendQueries: true,
errors: [], errors: [],
warnings: [], warnings: [],
...@@ -48,10 +55,14 @@ export const configSlice = createSlice({ ...@@ -48,10 +55,14 @@ export const configSlice = createSlice({
removeLastWarning: (state) => { removeLastWarning: (state) => {
state.warnings.shift(); state.warnings.shift();
}, },
setTheme: (state, action: PayloadAction<Theme>) => {
localStorage.setItem('theme', action.payload);
state.theme = action.payload;
},
}, },
}); });
export const { addError, removeLastError, addWarning, removeLastWarning, addSuccess, removeLastSuccess, addInfo, removeLastInfo } = export const { addError, removeLastError, addWarning, removeLastWarning, addSuccess, removeLastSuccess, addInfo, removeLastInfo, setTheme } =
configSlice.actions; configSlice.actions;
// Other code such as selectors can use the imported `RootState` type // Other code such as selectors can use the imported `RootState` type
......
import { import {
useConfig, useConfig,
useGraphQueryResult,
useQuerybuilderGraph, useQuerybuilderGraph,
useQuerybuilderHash, useQuerybuilderHash,
useQuerybuilderSettings, useQuerybuilderSettings,
...@@ -25,9 +26,7 @@ import ReactFlow, { ...@@ -25,9 +26,7 @@ import ReactFlow, {
isNode, isNode,
useReactFlow, useReactFlow,
} from 'reactflow'; } from 'reactflow';
import { Dialog, DialogClose, DialogContent } from '../../components/layout/Dialog'; import { Dialog, DialogContent } from '../../components/layout/Dialog';
import { Button } from '../../components/buttons';
import { ControlContainer } from '../../components/controls';
import { addError } from '../../data-access/store/configSlice'; import { addError } from '../../data-access/store/configSlice';
import { toSchemaGraphology } from '../../data-access/store/schemaSlice'; import { toSchemaGraphology } from '../../data-access/store/schemaSlice';
import { LayoutFactory } from '../../graph-layout'; import { LayoutFactory } from '../../graph-layout';
...@@ -38,25 +37,20 @@ import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill'; ...@@ -38,25 +37,20 @@ import { dragPillStarted, movePillTo } from '../pills/dragging/dragPill';
import styles from './querybuilder.module.scss'; import styles from './querybuilder.module.scss';
import { QueryBuilderLogicPillsPanel } from './querysidepanel/QueryBuilderLogicPillsPanel'; import { QueryBuilderLogicPillsPanel } from './querysidepanel/QueryBuilderLogicPillsPanel';
import { QueryBuilderRelatedNodesPanel } from './querysidepanel/QueryBuilderRelatedNodesPanel'; import { QueryBuilderRelatedNodesPanel } from './querysidepanel/QueryBuilderRelatedNodesPanel';
import { QueryMLDialog } from './querysidepanel/QueryMLDialog';
import { ConnectingNodeDataI } from './utils/connectorDrop'; import { ConnectingNodeDataI } from './utils/connectorDrop';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../components/tooltip';
import { resultSetFocus } from '../../data-access/store/interactionSlice'; import { resultSetFocus } from '../../data-access/store/interactionSlice';
import { QueryBuilderDispatcherContext } from './QueryBuilderDispatcher'; import { QueryBuilderDispatcherContext } from './QueryBuilderDispatcher';
import { Popover, PopoverContent, PopoverTrigger } from '../../components/layout/Popover'; import { QueryBuilderNav, QueryBuilderToggleSettings } from './QueryBuilderNav';
import { QuerySettings } from './querysidepanel/QuerySettings';
export type QueryBuilderProps = { export type QueryBuilderProps = {
onRunQuery?: () => void; onRunQuery?: () => void;
}; };
type SettingsPanel = 'settings' | 'ml' | 'logic' | 'relatedNodes' | undefined;
/** /**
* This is the main querybuilder component. It is responsible for holding all pills and fire off the visual part of the querybuilder panel logic * This is the main querybuilder component. It is responsible for holding all pills and fire off the visual part of the querybuilder panel logic
*/ */
export const QueryBuilderInner = (props: QueryBuilderProps) => { export const QueryBuilderInner = (props: QueryBuilderProps) => {
const [toggleSettings, setToggleSettings] = useState<SettingsPanel>(); const [toggleSettings, setToggleSettings] = useState<QueryBuilderToggleSettings>();
const reactFlowWrapper = useRef<HTMLDivElement>(null); const reactFlowWrapper = useRef<HTMLDivElement>(null);
const queryBuilderSettings = useQuerybuilderSettings(); const queryBuilderSettings = useQuerybuilderSettings();
...@@ -109,13 +103,6 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { ...@@ -109,13 +103,6 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
setTimeout(() => reactFlow.fitView(), 0); setTimeout(() => reactFlow.fitView(), 0);
}; };
/**
* Clears all nodes in the graph.
*/
function clearAllNodes() {
dispatch(clearQB());
}
/** /**
* Clears all nodes in the graph. * Clears all nodes in the graph.
* TODO: only works if the node is clicked and not moved (maybe use onSelectionChange) * TODO: only works if the node is clicked and not moved (maybe use onSelectionChange)
...@@ -537,163 +524,19 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => { ...@@ -537,163 +524,19 @@ export const QueryBuilderInner = (props: QueryBuilderProps) => {
}} }}
> >
<div ref={reactFlowWrapper} className="h-full w-full flex flex-col"> <div ref={reactFlowWrapper} className="h-full w-full flex flex-col">
<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"> <QueryBuilderNav
<div className="flex items-center"> toggleSettings={toggleSettings}
<h1 className="text-xs font-semibold text-secondary-600 px-2 truncate">Query builder</h1> onFitView={fitView}
</div> onApplyLayout={() => applyLayout()}
<div className="sticky right-0 px-0.5 ml-auto items-center flex truncate"> onRunQuery={() => {
<ControlContainer> if (props.onRunQuery) props.onRunQuery();
<TooltipProvider delayDuration={0}> }}
<Tooltip> onScreenshot={() => {}}
<TooltipTrigger> onLogic={() => {
<Button if (toggleSettings === 'logic') setToggleSettings(undefined);
variantType="secondary" else setToggleSettings('logic');
variant="ghost" }}
size="xs" />
iconComponent="icon-[ic--baseline-fullscreen]"
onClick={fitView}
/>
</TooltipTrigger>
<TooltipContent>
<p>Fit to screen</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-delete]"
onClick={() => clearAllNodes()}
/>
</TooltipTrigger>
<TooltipContent>
<p>Clear query panel</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-camera-alt]"
onClick={(event) => {
event.stopPropagation();
}}
/>
</TooltipTrigger>
<TooltipContent>
<p>Capture screen</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-import-export]"
onClick={(event) => {
event.stopPropagation();
applyLayout();
}}
/>
</TooltipTrigger>
<TooltipContent>
<p>Layouts</p>
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-settings]"
className="query-settings"
/>
</TooltipTrigger>
<TooltipContent>
<p>Query builder settings</p>
</TooltipContent>
</Tooltip>
</PopoverTrigger>
<PopoverContent>
<QuerySettings />
</PopoverContent>
</Popover>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-cached]"
onClick={(event) => {
event.stopPropagation();
if (props.onRunQuery) props.onRunQuery();
}}
/>
</TooltipTrigger>
<TooltipContent>
<p>Rerun query</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-difference]"
onClick={(event) => {
event.stopPropagation();
if (toggleSettings === 'logic') setToggleSettings(undefined);
else setToggleSettings('logic');
}}
/>
</TooltipTrigger>
<TooltipContent disabled={toggleSettings === 'logic'}>
<p>Logic settings</p>
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger>
<Tooltip>
<TooltipTrigger>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-lightbulb]" />
</TooltipTrigger>
<TooltipContent disabled={toggleSettings === 'ml'}>
<p>Machine learning</p>
</TooltipContent>
</Tooltip>
</PopoverTrigger>
<PopoverContent>
<QueryMLDialog />
</PopoverContent>
</Popover>
{/* <Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-drive-file-move]"
onClick={() => onAddSchemaToQueryBuilder()}
/>
</TooltipTrigger>
<TooltipContent>
<p>Query All Data</p>
</TooltipContent>
</Tooltip> */}
</TooltipProvider>
</ControlContainer>
</div>
</div>
<Dialog <Dialog
open={toggleSettings === 'logic'} open={toggleSettings === 'logic'}
......
import React, { useMemo, useState } from 'react';
import { ControlContainer, TooltipProvider, Tooltip, TooltipTrigger, Button, TooltipContent, Input } from '../../components';
import { Popover, PopoverTrigger, PopoverContent } from '../../components/layout/Popover';
import { useAppDispatch, useGraphQueryResult, useQuerybuilderSettings, useSchemaStats } from '../../data-access';
import { clearQB, QueryBuilderSettings, setQuerybuilderSettings } from '../../data-access/store/querybuilderSlice';
import { QueryMLDialog } from './querysidepanel/QueryMLDialog';
import { QuerySettings } from './querysidepanel/QuerySettings';
export type QueryBuilderToggleSettings = 'settings' | 'ml' | 'logic' | 'relatedNodes' | undefined;
export type QueryBuilderNavProps = {
toggleSettings: QueryBuilderToggleSettings;
onFitView: () => void;
onApplyLayout: () => void;
onRunQuery: () => void;
onScreenshot: () => void;
onLogic: () => void;
};
export const QueryBuilderNav = (props: QueryBuilderNavProps) => {
const dispatch = useAppDispatch();
const qb = useQuerybuilderSettings();
const result = useGraphQueryResult();
const resultSize = useMemo(() => (result ? result.edges.length : 0), [result]);
/**
* Clears all nodes in the graph.
*/
function clearAllNodes() {
dispatch(clearQB());
}
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">Query builder</h1>
</div>
<div className="sticky right-0 px-0.5 ml-auto items-center flex truncate">
<ControlContainer>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-fullscreen]"
onClick={props.onFitView}
/>
</TooltipTrigger>
<TooltipContent>
<p>Fit to screen</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-delete]"
onClick={() => clearAllNodes()}
/>
</TooltipTrigger>
<TooltipContent>
<p>Clear query panel</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-camera-alt]"
onClick={props.onScreenshot}
/>
</TooltipTrigger>
<TooltipContent>
<p>Capture screen</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-import-export]"
onClick={props.onApplyLayout}
/>
</TooltipTrigger>
<TooltipContent>
<p>Layouts</p>
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-settings]"
className="query-settings"
/>
</TooltipTrigger>
<TooltipContent>
<p>Query builder settings</p>
</TooltipContent>
</Tooltip>
</PopoverTrigger>
<PopoverContent>
<QuerySettings />
</PopoverContent>
</Popover>
<Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-cached]"
onClick={props.onRunQuery}
/>
</TooltipTrigger>
<TooltipContent>
<p>Rerun query</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-difference]"
onClick={props.onLogic}
/>
</TooltipTrigger>
<TooltipContent disabled={props.toggleSettings === 'logic'}>
<p>Logic settings</p>
</TooltipContent>
</Tooltip>
<Popover>
<PopoverTrigger>
<Tooltip>
<TooltipTrigger>
<Button variantType="secondary" variant="ghost" size="xs" iconComponent="icon-[ic--baseline-lightbulb]" />
</TooltipTrigger>
<TooltipContent disabled={props.toggleSettings === 'ml'}>
<p>Machine learning</p>
</TooltipContent>
</Tooltip>
</PopoverTrigger>
<PopoverContent>
<QueryMLDialog />
</PopoverContent>
</Popover>
{/* <Tooltip>
<TooltipTrigger>
<Button
variantType="secondary"
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-drive-file-move]"
onClick={() => onAddSchemaToQueryBuilder()}
/>
</TooltipTrigger>
<TooltipContent>
<p>Query All Data</p>
</TooltipContent>
</Tooltip> */}
<Popover>
<PopoverTrigger>
<Tooltip>
<TooltipTrigger>
<Button
variantType={qb.limit <= resultSize ? 'danger' : 'secondary'}
variant="ghost"
size="xs"
iconComponent="icon-[ic--baseline-filter-alt]"
className={qb.limit <= resultSize ? 'border-danger-600' : ''}
/>
</TooltipTrigger>
<TooltipContent disabled={props.toggleSettings === 'ml'}>
<p className="font-bold text-base">Limit</p>
<p>Limits the number of edges retrieved from the database.</p>
<p>Required to manage performance.</p>
<p className={`font-semibold${qb.limit <= resultSize ? ' text-danger-800' : ''}`}>
Fetched {resultSize} of a maximum of {qb.limit} edges
</p>
</TooltipContent>
</Tooltip>
</PopoverTrigger>
<PopoverContent>
<div className="flex flex-col w-full gap-2 px-4 py-2">
<span className="text-xs font-bold">Limit</span>
<Input
type="number"
size="xs"
value={qb.limit}
lazy
onChange={(e) => {
dispatch(setQuerybuilderSettings({ ...qb, limit: Number(e) }));
}}
className={`w-24${qb.limit <= resultSize ? ' border-danger-600' : ''}`}
containerClassName=""
/>
</div>
</PopoverContent>
</Popover>
</TooltipProvider>
</ControlContainer>
</div>
</div>
);
};
...@@ -40,16 +40,6 @@ export const QuerySettings = React.forwardRef<HTMLDivElement, {}>((props, ref) = ...@@ -40,16 +40,6 @@ export const QuerySettings = React.forwardRef<HTMLDivElement, {}>((props, ref) =
setState({ ...state, autocompleteRelation: value as any }); setState({ ...state, autocompleteRelation: value as any });
}} }}
/> />
<Input
type="number"
tooltip="The maximum number of results to return"
label="Limit"
inline
size="sm"
value={state.limit}
onChange={(e) => setState({ ...state, limit: e })}
/>
<Input <Input
type="number" type="number"
label="Min Depth Default" label="Min Depth Default"
......
...@@ -66,7 +66,7 @@ export function VisualizationSettings({}: Props) { ...@@ -66,7 +66,7 @@ export function VisualizationSettings({}: Props) {
}; };
return ( return (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full overflow-auto">
<div className="text-sm px-4 py-2 flex flex-col gap-1"> <div className="text-sm px-4 py-2 flex flex-col gap-1">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="font-bold">Visualization</span> <span className="font-bold">Visualization</span>
......
import React, { useEffect, useMemo } from 'react';
import { SettingsContainer } from '../../components/config';
import { layerSettings, layerTypes } from './layers';
import { Input } from '../../..';
import { VisualizationSettingsPropTypes } from '../../common';
import { MapEdgeData, MapNodeData, MapNodeOrEdgeData, MapProps } from './mapvis';
export const MapSettings = ({ settings, graphMetadata, updateSettings }: VisualizationSettingsPropTypes<MapProps>) => {
const DataLayerSettings = settings.layer && layerSettings?.[settings.layer];
const spatialAttributes = useMemo(() => {
return Object.fromEntries(
graphMetadata.nodes.labels.map((node) => [
node,
Object.entries(graphMetadata.nodes.types[node].attributes)
.filter(([, value]) => value.dimension === 'numerical')
.map(([key]) => key),
]),
);
}, [graphMetadata]);
useEffect(() => {
const nodes = Object.fromEntries(
graphMetadata.nodes.labels.map(
(node) =>
[
node,
{
color: [252, 185, 0],
hidden: false,
fixed: true,
min: 0,
max: 10,
radius: 1,
lat: undefined,
lon: undefined,
collapsed: false,
shape: 'circle',
size: 10,
...(settings?.nodes && node in settings.nodes ? settings.nodes[node] : {}),
},
] as [string, MapNodeData],
),
);
const edges = Object.fromEntries(
graphMetadata.edges.labels.map(
(edge) =>
[
edge,
{
color: [171, 184, 195],
hidden: false,
fixed: true,
min: 0,
max: 10,
radius: 1,
sizeAttribute: undefined,
collapsed: false,
size: 10,
...(settings?.edges && edge in settings.edges ? settings.edges[edge] : {}),
},
] as [string, MapEdgeData],
),
);
updateSettings({ nodes: nodes, edges: edges });
}, [graphMetadata]);
useEffect(() => {
// Autodetect a lat or lon attribute if not already set
Object.keys(settings.nodes).forEach((node) => {
if ((!settings.nodes[node].lat || !settings.nodes[node].lon) && node in spatialAttributes) {
const lat = spatialAttributes[node].find((attr) => attr.includes('latitude'));
const lon = spatialAttributes[node].find((attr) => attr.includes('longitude'));
if (lat && lon) {
updateSettings({ nodes: { ...settings.nodes, [node]: { ...settings.nodes[node], lat, lon } } });
}
}
});
}, [spatialAttributes, settings]);
return (
<SettingsContainer>
<Input
label="Data layer"
type="dropdown"
inline
value={settings.layer}
options={Object.keys(layerTypes)}
onChange={(val) => updateSettings({ layer: val as string })}
/>
{DataLayerSettings && !!spatialAttributes && (
<DataLayerSettings
settings={settings}
graphMetadata={graphMetadata}
updateSettings={updateSettings}
spatialAttributes={spatialAttributes}
/>
)}
</SettingsContainer>
);
};
...@@ -4,10 +4,10 @@ import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice ...@@ -4,10 +4,10 @@ import { addError } from '@graphpolaris/shared/lib/data-access/store/configSlice
import React, { useState } from 'react'; import React, { useState } from 'react';
interface SearchBarProps { interface SearchBarProps {
onSearch: (boundingbox: [number, number, number, number]) => void; onSearch: (boundingBox: [number, number, number, number]) => void;
} }
const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => { export const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const [query, setQuery] = useState(''); const [query, setQuery] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
...@@ -42,5 +42,3 @@ const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => { ...@@ -42,5 +42,3 @@ const SearchBar: React.FC<SearchBarProps> = ({ onSearch }) => {
</div> </div>
); );
}; };
export default SearchBar;